♻️ Make several adjustments to the dashboard deleted page (#7999)

* ♻️ Make several sustantial adjustments to the dashboard deleted page

* 📎 Add PR feedback changes
This commit is contained in:
Andrey Antukh 2025-12-30 09:52:29 +01:00 committed by GitHub
parent e3405eacca
commit 7b5817f407
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 896 additions and 522 deletions

View File

@ -704,7 +704,6 @@
f.created_at, f.created_at,
f.modified_at, f.modified_at,
f.name, f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at, f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id, ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num, row_number() OVER w AS row_num,
@ -814,7 +813,7 @@
AND (f.deleted_at IS NULL OR f.deleted_at > now()) AND (f.deleted_at IS NULL OR f.deleted_at > now())
ORDER BY f.created_at ASC;") ORDER BY f.created_at ASC;")
(defn- absorb-library-by-file! (defn- absorb-library-by-file
[cfg ldata file-id] [cfg ldata file-id]
(assert (db/connection-map? cfg) (assert (db/connection-map? cfg)
@ -838,7 +837,7 @@
:modified-at (ct/now) :modified-at (ct/now)
:has-media-trimmed false})))) :has-media-trimmed false}))))
(defn- absorb-library (defn- absorb-library*
"Find all files using a shared library, and absorb all library assets "Find all files using a shared library, and absorb all library assets
into the file local libraries" into the file local libraries"
[cfg {:keys [id data] :as library}] [cfg {:keys [id data] :as library}]
@ -853,10 +852,10 @@
:library-id (str id) :library-id (str id)
:files (str/join "," (map str ids))) :files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file! cfg data) ids) (run! (partial absorb-library-by-file cfg data) ids)
library)) library))
(defn absorb-library! (defn absorb-library
[{:keys [::db/conn] :as cfg} id] [{:keys [::db/conn] :as cfg} id]
(let [file (-> (bfc/get-file cfg id (let [file (-> (bfc/get-file cfg id
:realize? true :realize? true
@ -873,7 +872,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
(absorb-library cfg file))) (absorb-library* cfg file)))
(defn- set-file-shared (defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@ -886,14 +885,14 @@
;; file, we need to perform more complex operation, ;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and ;; so in this case we retrieve the complete file and
;; perform all required validations. ;; perform all required validations.
(let [file (-> (absorb-library! cfg id) (let [file (-> (absorb-library cfg id)
(assoc :is-shared false))] (assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id}) (db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file (db/update! conn :file
{:is-shared false {:is-shared false
:modified-at (ct/now)} :modified-at (ct/now)}
{:id id}) {:id id})
(select-keys file [:id :name :is-shared])) file)
(and (false? (:is-shared file)) (and (false? (:is-shared file))
(true? (:is-shared params))) (true? (:is-shared params)))
@ -940,6 +939,11 @@
{:id file-id} {:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at {::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})] :project-id :created-at :modified-at]})]
;; Remove all possible relations for that file
(db/delete! conn :file-library-rel
{:library-file-id file-id})
(wrk/submit! {::db/conn conn (wrk/submit! {::db/conn conn
::wrk/task :delete-object ::wrk/task :delete-object
::wrk/params {:object :file ::wrk/params {:object :file
@ -1090,47 +1094,53 @@
;; --- MUTATION COMMAND: delete-files-immediatelly ;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:delete-team-files (def ^:private sql:get-delete-team-files-candidates
"UPDATE file AS uf SET deleted_at = ?::timestamptz "SELECT f.id
FROM ( FROM file AS f
SELECT f.id JOIN project AS p ON (p.id = f.project_id)
FROM file AS f JOIN team AS t ON (t.id = p.team_id)
JOIN project AS p ON (p.id = f.project_id) WHERE t.deleted_at IS NULL
JOIN team AS t ON (t.id = p.team_id) AND t.id = ?
WHERE t.deleted_at IS NULL AND f.id = ANY(?::uuid[])")
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private schema:permanently-delete-team-files (def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"} [:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]]) [:ids [::sm/set ::sm/uuid]]])
(defn- permanently-delete-team-files
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
(let [ids (into #{}
d/xf:map-id
(db/exec! conn [sql:get-delete-team-files-candidates team-id
(db/create-array conn "uuid" ids)]))]
(reduce (fn [acc id]
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
(db/update! conn :file
{:deleted-at request-at}
{:id id}
{::db/return-keys false})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at request-at
:id id}})
(conj acc id))
#{}
ids)))
(sv/defmethod ::permanently-delete-team-files (sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the "Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and specified team. The team-id on params will be used to filter and
check writable permissons on team." check writable permissons on team."
{::doc/added "2.12" {::doc/added "2.13"
::sm/params schema:permanently-delete-team-files ::sm/params schema:permanently-delete-team-files}
::db/transaction true}
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! pool profile-id team-id)
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
;; --- MUTATION COMMAND: restore-files-immediatelly ;; --- MUTATION COMMAND: restore-files-immediatelly
@ -1194,7 +1204,7 @@
{:keys [files projects]} {:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}] (reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)] (let [index (-> result :files count)]
(events/tap :progress {:file-id id :index index :total total-files}) (events/tap :progress {:file-id id :index (inc index) :total total-files})
(restore-file conn id) (restore-file conn id)
(-> result (-> result
@ -1217,7 +1227,7 @@
(sv/defmethod ::restore-deleted-team-files (sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective "Removes the deletion mark from the specified files (and respective
projects) on the specified team." projects) on the specified team."
{::doc/added "2.12" {::doc/added "2.13"
::sse/stream? true ::sse/stream? true
::sm/params schema:restore-deleted-team-files} ::sm/params schema:restore-deleted-team-files}
[cfg params] [cfg params]

View File

@ -45,7 +45,8 @@
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :file (db/update! conn :file
{:deleted-at deleted-at} {:deleted-at deleted-at
:is-shared false}
{:id id} {:id id}
{::db/return-keys false}) {::db/return-keys false})
@ -53,7 +54,7 @@
(not *team-deletion*)) (not *team-deletion*))
;; NOTE: we don't prevent file deletion on absorb operation failure ;; NOTE: we don't prevent file deletion on absorb operation failure
(try (try
(db/tx-run! cfg files/absorb-library! id) (db/tx-run! cfg files/absorb-library id)
(catch Throwable cause (catch Throwable cause
(l/warn :hint "error on absorbing library" (l/warn :hint "error on absorbing library"
:file-id id :file-id id

View File

@ -595,8 +595,8 @@
(px/exec! :virtual #(rcp/write-body-to-stream body nil output)) (px/exec! :virtual #(rcp/write-body-to-stream body nil output))
(into [] (into []
(map (fn [{:keys [event data]}] (map (fn [{:keys [event data]}]
[(keyword event) (d/vec2 (keyword event)
(tr/decode-str data)])) (tr/decode-str data))))
(parse-sse (slurp' input))) (parse-sse (slurp' input)))
(finally (finally
(.close input))))) (.close input)))))

View File

@ -1921,7 +1921,11 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:ids data) result))) (t/is (fn? result))
(let [[ev1 ev2 :as events] (th/consume-sse result)]
(t/is (= 2 (count events)))
(t/is (= (:ids data) (val ev2)))))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now))))))) (t/is (= (:deleted-at row) now)))))))

View File

@ -0,0 +1,29 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.project
(:require
[app.common.schema :as sm]
[app.common.time :as cm]))
(def schema:project
[:map {:title "Profile"}
[:id ::sm/uuid]
[:created-at {:optional true} ::cm/inst]
[:modified-at {:optional true} ::cm/inst]
[:name :string]
[:is-default {:optional true} ::sm/boolean]
[:is-pinned {:optional true} ::sm/boolean]
[:count {:optional true} ::sm/int]
[:total-count {:optional true} ::sm/int]
[:team-id ::sm/uuid]])
(def valid-project?
(sm/lazy-validator schema:project))
(def check-project
(sm/check-fn schema:project))

View File

@ -302,3 +302,9 @@
:height 720}]) :height 720}])
(def max-input-length 255) (def max-input-length 255)
(def ^:const default-slow-progress-threshold
"A constant value that represents a threshold in milliseconds when a
normal progress becomes tagged as slow if no event received in the
specified amount of time"
1000)

View File

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.team :as ctt] [app.common.types.team :as ctt]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -229,6 +230,91 @@
;; Delay so the navigation can finish ;; Delay so the navigation can finish
(rx/delay 250)))))))) (rx/delay 250))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROGRESS EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def noop-fn
(constantly nil))
(def ^:private schema:progress-params
[:map {:title "Progress"}
[:key {:optional true} ::sm/text]
[:index {:optional true} ::sm/int]
[:total ::sm/int]
[:hints
[:map-of :keyword fn?]]
[:slow-progress-threshold {:optional true} ::sm/int]])
(def ^:private check-progress-params
(sm/check-fn schema:progress-params))
(defn initialize-progress
[& {:keys [key index total hints slow-progress-threshold] :as params}]
(assert (check-progress-params params))
(ptk/reify ::initialize-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [_]
(let [hint ((:normal hints noop-fn) params)]
{:threshold (or slow-progress-threshold 5000)
:key key
:last-update (ct/now)
:healthy true
:visible true
:hints hints
:progress (d/nilv index 0)
:total total
:hint hint}))))))
(defn update-progress
[{:keys [index total] :as params}]
(assert (check-progress-params params))
(ptk/reify ::update-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(let [last-update (get state :last-update)
hints (get state :hints)
threshold (get state :slow-progress-threshold)
time-diff (ct/diff-ms last-update (ct/now))
healthy? (< time-diff threshold)
hint (if healthy?
((:normal hints noop-fn) params)
((:slow hints noop-fn) params))]
(-> state
(assoc :progress index)
(assoc :total total)
(assoc :last-update (ct/now))
(assoc :healthy healthy?)
(assoc :hint hint))))))))
(defn toggle-progress-visibility
[]
(ptk/reify ::toggle-progress-visibility
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(update state :visible not))))))
(defn clear-progress
[]
(ptk/reify ::clear-progress
ptk/UpdateEvent
(update [_ state]
(dissoc state :progress))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NAVEGATION EVENTS ;; NAVEGATION EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -13,12 +13,15 @@
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.constants :as mconst]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.websocket :as dws] [app.main.data.websocket :as dws]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
@ -691,6 +694,56 @@
;; --- Delete files immediately ;; --- Delete files immediately
(defn- delete-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::delete-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.deleting-files")
slow-hint #(tr "dashboard.progress-notification.slow-delete")
stream (->> (rp/cmd! ::sse/permanently-delete-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/merge-map (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))
(defn delete-files-immediately (defn delete-files-immediately
[{:keys [team-id ids] :as params}] [{:keys [team-id ids] :as params}]
(assert (uuid? team-id)) (assert (uuid? team-id))
@ -698,145 +751,190 @@
(assert (every? uuid? ids)) (assert (every? uuid? ids))
(ptk/reify ::delete-files-immediately (ptk/reify ::delete-files-immediately
ev/Event
(-data [_]
{:team-id team-id
:num-files (count ids)})
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(let [{:keys [on-success on-error] (let [deleted-files
:or {on-success identity (get state :deleted-files)
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :permanently-delete-team-files {:team-id team-id :ids ids}) on-success
(rx/tap on-success) (fn []
(rx/catch on-error)))))) (if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.delete-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.delete-files-success-notification" (count ids))))))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-files")))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn delete-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::delete-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(rx/of (ntf/success (tr "dashboard.delete-success-notification" name)))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-project" name)))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
;; --- Restore deleted files immediately ;; --- Restore deleted files immediately
(defn- initialize-restore-status
[files]
(ptk/reify ::init-restore-status
ptk/UpdateEvent
(update [_ state]
(let [restore-state {:in-progress true
:healthy? true
:error false
:progress 0
:widget-visible true
:detail-visible true
:files files
:last-update (ct/now)
:cmd :restore-files}]
(assoc state :restore restore-state)))))
(defn- update-restore-status (defn- restore-files
[{:keys [index total] :as data}] [{:keys [team-id ids on-success on-error]}]
(ptk/reify ::upd-restore-status (assert (uuid? team-id))
ptk/UpdateEvent (assert (set? ids))
(update [_ state] (assert (every? uuid? ids))
(let [time-diff (ct/diff-ms (get-in state [:restore :last-update]) (ct/now)) (assert (fn? on-success))
healthy? (< time-diff 6000)] (assert (fn? on-error))
(update state :restore assoc
:progress index
:total total
:last-update (ct/now)
:healthy? healthy?)))))
(defn- complete-restore-status (ptk/reify ::restore-files
[]
(ptk/reify ::comp-restore-status
ptk/UpdateEvent
(update [_ state]
(let [total (get-in state [:restore :total])]
(update state :restore assoc
:in-progress false
:progress total ; Ensure progress equals total on completion
:last-update (ct/now))))))
(defn- error-restore-status
[error]
(ptk/reify ::err-restore-status
ptk/UpdateEvent
(update [_ state]
(update state :restore assoc
:in-progress false
:error error
:last-update (ct/now)
:healthy? false))))
(defn toggle-restore-detail-visibility
[]
(ptk/reify ::toggle-restore-detail
ptk/UpdateEvent
(update [_ state]
(update-in state [:restore :detail-visible] not))))
(defn retry-last-restore
[]
(ptk/reify ::retry-restore
ptk/UpdateEvent
(update [_ state]
;; Reset restore state for retry - actual retry will be handled by UI
(if (get state :restore)
(update state :restore assoc :error false :in-progress false)
state))))
(defn clear-restore-state
[]
(ptk/reify ::clear-restore
ptk/UpdateEvent
(update [_ state]
(dissoc state :restore))))
(defn- projects-restored
[team-id]
(ptk/reify ::projects-restored
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
;; Refetch projects to get the updated state without deleted-at (let [progress-hint #(tr "dashboard.progress-notification.restoring-files")
(rx/of (fetch-projects team-id))))) slow-hint #(tr "dashboard.progress-notification.slow-restore")]
(defn restore-files-immediately
[{:keys [team-id ids] :as params}]
(dm/assert! (uuid? team-id))
(dm/assert! (set? ids))
(dm/assert! (every? uuid? ids))
(ptk/reify ::restore-files-immediately
ev/Event
(-data [_]
{:team-id team-id
:num-files (count ids)})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
files (mapv #(hash-map :id %) ids)]
(rx/merge (rx/merge
(rx/of (initialize-restore-status files)) (rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids}) (let [stream (->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
(rx/tap (fn [event] (rx/share))]
(let [payload (sse/get-payload event)
type (sse/get-type event)]
(when (and payload (= type "progress"))
(let [{:keys [index total]} payload]
(when (and index total)
;; Dispatch progress update
(st/emit! (update-restore-status {:index index :total total}))))))))
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/tap on-success)
(rx/mapcat (fn [_]
(rx/of (complete-restore-status)
(projects-restored team-id))))
(rx/catch (fn [error]
(rx/concat
(rx/of (error-restore-status (ex-message error)))
(on-error error)))))
(rx/of (ptk/data-event ::restore-start {:total (count ids)}))))))) (rx/merge
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/mapcat (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
;; (ntf/success (tr "dashboard.restore-success-notification"))
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))))
(defn restore-files-immediately
[{:keys [team-id ids]}]
(assert (uuid? team-id))
(assert (set? ids))
(ptk/reify ::restore-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.restore-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.restore-files-success-notification" (count ids))))))
on-error
(fn [_cause]
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-file" fname))))
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-files")))))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn restore-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::restore-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(st/emit! (ntf/success (tr "dashboard.restore-success-notification" name)))
on-error
#(st/emit! (ntf/error (tr "dashboard.errors.error-on-restoring-project" name)))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))

View File

@ -637,5 +637,5 @@
(def persistence-state (def persistence-state
(l/derived (comp :status :persistence) st/state)) (l/derived (comp :status :persistence) st/state))
(def restore (def progress
(l/derived :restore st/state)) (l/derived :progress st/state))

View File

@ -87,6 +87,9 @@
{:stream? true {:stream? true
:form-data? true} :form-data? true}
::sse/permanently-delete-team-files
{:stream? true}
::sse/restore-deleted-team-files ::sse/restore-deleted-team-files
{:stream? true} {:stream? true}

View File

@ -0,0 +1,103 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.components.progress
"Assets exportation common components."
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.main.data.common :as dcm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as deprecated-icons]
[app.util.i18n :as i18n :refer [tr]]
[app.util.theme :as theme]
[rumext.v2 :as mf]))
(def ^:private neutral-icon
(deprecated-icons/icon-xref :msg-neutral (stl/css :icon)))
(def ^:private error-icon
(deprecated-icons/icon-xref :delete-text (stl/css :icon)))
(def ^:private close-icon
(deprecated-icons/icon-xref :close (stl/css :close-icon)))
(mf/defc progress-notification-widget*
[]
(let [state (mf/deref refs/progress)
profile (mf/deref refs/profile)
theme (get profile :theme theme/default)
default-theme? (= theme/default theme)
error? (:error state)
healthy? (:healthy state)
visible? (:visible state)
progress (:progress state)
hint (:hint state)
total (:total state)
pwidth
(if error?
280
(/ (* progress 280) total))
color
(cond
error? clr/new-danger
healthy? (if default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if default-theme?
clr/background-quaternary
clr/background-quaternary-light)
toggle-detail-visibility
(mf/use-fn
(fn []
(st/emit! (dcm/toggle-progress-visibility))))]
[:*
(when visible?
[:div {:class (stl/css-case :progress-modal true
:has-error error?)}
(if error?
error-icon
neutral-icon)
[:div {:class (stl/css :title)}
[:div {:class (stl/css :title-text)} hint]
(if error?
[:button {:class (stl/css :retry-btn)
;; :on-click retry-last-operation
}
(tr "labels.retry")]
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)
:on-click toggle-detail-visibility}
close-icon]
(when-not error?
[:svg {:class (stl/css :progress-bar)
:height 4
:width 280}
[:g
[:path {:d "M0 0 L280 0"
:stroke background-clr
:stroke-width 30}]
[:path {:d (dm/str "M0 0 L280 0")
:stroke color
:stroke-width 30
:fill "transparent"
:stroke-dasharray 280
:stroke-dashoffset (- 280 pwidth)
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])])]))

View File

@ -0,0 +1,101 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
// PROGRESS WIDGET
.progress-widget {
@include deprecated.flexCenter;
width: deprecated.$s-28;
height: deprecated.$s-28;
}
// PROGRESS MODAL
.progress-modal {
--export-modal-bg-color: var(--alert-background-color-default);
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
display: grid;
grid-template-columns: deprecated.$s-24 1fr deprecated.$s-24;
grid-template-areas:
"icon text close"
"bar bar bar";
gap: deprecated.$s-4 deprecated.$s-8;
padding-block-start: deprecated.$s-8;
background-color: var(--export-modal-bg-color);
border: deprecated.$s-1 solid var(--export-modal-border-color);
border-radius: deprecated.$br-8;
z-index: deprecated.$z-index-modal;
overflow: hidden;
}
.has-error {
--export-modal-bg-color: var(--alert-background-color-error);
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
@extend .button-icon;
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
stroke: var(--export-modal-icon-color);
}
.title {
@include deprecated.bodyMediumTypography;
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
grid-area: text;
align-self: center;
padding: 0;
margin: 0;
color: var(--export-modal-fg-color);
}
.progress {
@include deprecated.bodyMediumTypography;
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
color: var(--modal-text-foreground-color);
}
.retry-btn {
@include deprecated.buttonStyle;
@include deprecated.bodySmallTypography;
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
margin: 0;
padding: 0;
}
.progress-close-button {
@include deprecated.buttonStyle;
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
@extend .button-icon;
stroke: var(--export-modal-icon-color);
}
.progress-bar {
margin-top: 0;
grid-area: bar;
}

View File

@ -19,6 +19,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.progress :refer [progress-notification-widget*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.dashboard.deleted :refer [deleted-section*]] [app.main.ui.dashboard.deleted :refer [deleted-section*]]
[app.main.ui.dashboard.files :refer [files-section*]] [app.main.ui.dashboard.files :refer [files-section*]]
@ -30,7 +31,6 @@
[app.main.ui.dashboard.sidebar :refer [sidebar*]] [app.main.ui.dashboard.sidebar :refer [sidebar*]]
[app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]] [app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]]
[app.main.ui.dashboard.templates :refer [templates-section*]] [app.main.ui.dashboard.templates :refer [templates-section*]]
[app.main.ui.exports.assets :refer [progress-widget]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.modal :refer [modal-container*]] [app.main.ui.modal :refer [modal-container*]]
[app.main.ui.workspace.plugins] [app.main.ui.workspace.plugins]
@ -87,7 +87,7 @@
:on-click clear-selected-fn :on-click clear-selected-fn
:ref container} :ref container}
[:& progress-widget {:operation :restore}] [:> progress-notification-widget*]
(case section (case section
:dashboard-recent :dashboard-recent

View File

@ -12,7 +12,6 @@
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
@ -27,6 +26,8 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private ref:deleted-files
(l/derived :deleted-files st/state))
(def ^:private menu-icon (def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon))) (deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
@ -40,57 +41,40 @@
[:h1 (tr "dashboard.projects-title")]]]) [:h1 (tr "dashboard.projects-title")]]])
(mf/defc deleted-project-menu* (mf/defc deleted-project-menu*
[{:keys [project files team-id show on-close top left]}] [{:keys [project show on-close top left]}]
(let [top (d/nilv top 0) (let [top (d/nilv top 0)
left (d/nilv left 0) left (d/nilv left 0)
file-ids
(mf/with-memo [files]
(into #{} d/xf:map-id files))
restore-fn
(fn [_]
(st/emit! (dd/restore-files-immediately
(with-meta {:team-id team-id :ids file-ids}
{:on-success #(st/emit! (ntf/success (tr "restore-modal.success-restore-immediately" (:name project)))
(dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id))
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-project" (:name project))))}))))
on-restore-project on-restore-project
(fn [] (mf/use-fn
(st/emit! (mf/deps project)
(modal/show {:type :confirm (fn []
:title (tr "restore-modal.restore-project.title") (let [on-accept #(st/emit! (dd/restore-project-immediately project))]
:message (tr "restore-modal.restore-project.description" (:name project)) (st/emit! (modal/show {:type :confirm
:accept-style :primary :title (tr "dashboard.restore-project-confirmation.title")
:accept-label (tr "labels.continue") :message (tr "dashboard.restore-project-confirmation.description" (:name project))
:on-accept restore-fn}))) :accept-style :primary
:accept-label (tr "labels.continue")
delete-fn :on-accept on-accept})))))
(fn [_]
(st/emit! (ntf/success (tr "delete-forever-modal.success-delete-immediately" (:name project)))
(dd/delete-files-immediately
{:team-id team-id
:ids file-ids})
(dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id)))
on-delete-project on-delete-project
(fn [] (mf/use-fn
(st/emit! (mf/deps project)
(modal/show {:type :confirm (fn []
:title (tr "delete-forever-modal.title") (let [accept-fn #(st/emit! (dd/delete-project-immediately project))]
:message (tr "delete-forever-modal.delete-project.description" (:name project)) (st/emit! (modal/show {:type :confirm
:accept-label (tr "dashboard.deleted.delete-forever") :title (tr "dashboard.delete-forever-confirmation.title")
:on-accept delete-fn}))) :message (tr "dashboard.delete-project-forever-confirmation.description" (:name project))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn})))))
options options
[{:name (tr "dashboard.deleted.restore-project") (mf/with-memo [on-restore-project on-delete-project]
:id "project-restore" [{:name (tr "dashboard.restore-project-button")
:handler on-restore-project} :id "project-restore"
{:name (tr "dashboard.deleted.delete-project") :handler on-restore-project}
:id "project-delete" {:name (tr "dashboard.delete-project-button")
:handler on-delete-project}]] :id "project-delete"
:handler on-delete-project}])]
[:> context-menu* [:> context-menu*
{:on-close on-close {:on-close on-close
@ -102,9 +86,8 @@
:options options}])) :options options}]))
(mf/defc deleted-project-item* (mf/defc deleted-project-item*
{::mf/props :obj {::mf/private true}
::mf/private true} [{:keys [project files]}]
[{:keys [project team files]}]
(let [project-files (filterv #(= (:project-id %) (:id project)) files) (let [project-files (filterv #(= (:project-id %) (:id project)) files)
empty? (empty? project-files) empty? (empty? project-files)
@ -170,8 +153,6 @@
(when (:menu-open @local) (when (:menu-open @local)
[:> deleted-project-menu* [:> deleted-project-menu*
{:project project {:project project
:files project-files
:team-id (:id team)
:show (:menu-open @local) :show (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local))) :left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local)) :top (:y (:menu-pos @local))
@ -193,8 +174,34 @@
:limit limit :limit limit
:selected-files selected-files}])]])) :selected-files selected-files}])]]))
(def ^:private ref:deleted-files
(l/derived :deleted-files st/state)) (mf/defc menu*
[{:keys [team-id section]}]
(let [on-recent-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))
on-deleted-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-deleted :team-id team-id))))]
[:div {:class (stl/css :nav)}
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-recent))]
:data-testid "recent-tab"
:on-click on-recent-click}
(tr "labels.recent")]
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-deleted))]
:variant "ghost"
:type "button"
:data-testid "deleted-tab"
:on-click on-deleted-click}
(tr "labels.deleted")]]))
(mf/defc deleted-section* (mf/defc deleted-section*
[{:keys [team projects]}] [{:keys [team projects]}]
@ -230,53 +237,33 @@
(and (= "enterprise" sub-type) (not canceled?)) 90 (and (= "enterprise" sub-type) (not canceled?)) 90
:else 7)) :else 7))
on-clear on-delete-all
(mf/use-fn (mf/use-fn
(mf/deps team-id deleted-map) (mf/deps team-id deleted-map)
(fn [] (fn []
(when deleted-map (when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [file-ids (into #{} (keys deleted-map))] (let [on-accept #(st/emit! (dd/delete-files-immediately
(when (seq file-ids) {:team-id team-id
(st/emit! :ids ids}))]
(modal/show {:type :confirm (st/emit! (modal/show {:type :confirm
:title (tr "delete-forever-modal.title") :title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "delete-forever-modal.delete-all.description" (count file-ids)) :message (tr "dashboard.delete-all-forever-confirmation.description" (count ids))
:accept-label (tr "dashboard.deleted.delete-forever") :accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept #(st/emit! :on-accept on-accept}))))))
(dd/delete-files-immediately
{:team-id team-id
:ids file-ids})
(dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id))})))))))
restore-fn
(fn [file-ids]
(st/emit! (dd/restore-files-immediately
(with-meta {:team-id team-id :ids file-ids}
{:on-success #(st/emit! (dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id))
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-files")))}))))
on-restore-all on-restore-all
(mf/use-fn (mf/use-fn
(mf/deps team-id deleted-map) (mf/deps team-id deleted-map)
(fn [] (fn []
(when deleted-map (when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [file-ids (into #{} (keys deleted-map))] (let [on-accept #(st/emit! (dd/restore-files-immediately {:team-id team-id :ids ids}))]
(when (seq file-ids) (st/emit! (modal/show {:type :confirm
(st/emit! :title (tr "dashboard.restore-all-confirmation.title")
(modal/show {:type :confirm :message (tr "dashboard.restore-all-confirmation.description" (count ids))
:title (tr "restore-modal.restore-all.title") :accept-label (tr "labels.continue")
:message (tr "restore-modal.restore-all.description" (count file-ids)) :accept-style :primary
:accept-label (tr "labels.continue") :on-accept on-accept}))))))]
:accept-style :primary
:on-accept #(restore-fn file-ids)})))))))
on-recent-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))]
(mf/with-effect [team-id] (mf/with-effect [team-id]
(st/emit! (dd/fetch-projects team-id) (st/emit! (dd/fetch-projects team-id)
@ -289,35 +276,26 @@
[:* [:*
[:div {:class (stl/css :no-bg)} [:div {:class (stl/css :no-bg)}
[:div {:class (stl/css :nav-options)} [:> menu* {:team-id team-id :section :dashboard-deleted}]
[:> button* {:variant "ghost"
:data-testid "recent-tab"
:type "button"
:on-click on-recent-click}
(tr "dashboard.labels.recent")]
[:div {:class (stl/css :selected)
:data-testid "deleted-tab"}
(tr "dashboard.labels.deleted")]]
[:div {:class (stl/css :deleted-content)} [:div {:class (stl/css :deleted-info-content)}
[:div {:class (stl/css :deleted-info)} [:p {:class (stl/css :deleted-info)}
[:div (tr "dashboard.trash-info-text-part1")
(tr "dashboard.deleted.info-text") [:span {:class (stl/css :info-text-highlight)}
[:span {:class (stl/css :info-text-highlight)} (tr "dashboard.trash-info-text-part2" deletion-days)]
(tr "dashboard.deleted.info-days" deletion-days)] (tr "dashboard.trash-info-text-part3")
(tr "dashboard.deleted.info-text2")] [:br]
[:div (tr "dashboard.trash-info-text-part4")]
(tr "dashboard.deleted.restore-text")]]
[:div {:class (stl/css :deleted-options)} [:div {:class (stl/css :deleted-options)}
[:> button* {:variant "ghost" [:> button* {:variant "ghost"
:type "button" :type "button"
:on-click on-restore-all} :on-click on-restore-all}
(tr "dashboard.deleted.restore-all")] (tr "dashboard.restore-all-deleted-button")]
[:> button* {:variant "destructive" [:> button* {:variant "destructive"
:type "button" :type "button"
:icon "delete" :icon "delete"
:on-click on-clear} :on-click on-delete-all}
(tr "dashboard.deleted.clear")]]] (tr "dashboard.clear-trash-button")]]]
(when (seq projects) (when (seq projects)
(for [{:keys [id] :as project} projects] (for [{:keys [id] :as project} projects]
@ -326,6 +304,5 @@
(filterv #(= id (:project-id %))) (filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))] (sort-by :modified-at #(compare %2 %1))))]
[:> deleted-project-item* {:project project [:> deleted-project-item* {:project project
:team team
:files files :files files
:key id}])))]]]])) :key id}])))]]]]))

View File

@ -20,17 +20,19 @@
padding-block-end: var(--sp-xxxl); padding-block-end: var(--sp-xxxl);
} }
.deleted-content { .deleted-info-content {
display: flex; display: flex;
gap: var(--sp-l);
justify-content: space-between; justify-content: space-between;
margin-inline-start: var(--sp-l); padding: var(--sp-s) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
margin-block-start: var(--sp-xxl);
} }
.deleted-info { .deleted-info {
@include t.use-typography("body-medium"); display: block;
height: fit-content;
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
line-height: 0.8;
height: var(--sp-xl);
} }
.info-text-highlight { .info-text-highlight {
@ -43,27 +45,37 @@
flex-shrink: 0; flex-shrink: 0;
} }
.nav-options { .nav {
display: flex; display: flex;
gap: var(--sp-l); gap: var(--sp-l);
justify-content: space-between; justify-content: space-between;
border-bottom: $b-1 solid var(--panel-border-color); border-bottom: $b-1 solid var(--panel-border-color);
padding-inline-start: var(--sp-l); //padding-inline-start: var(--sp-l);
background: var(--color-background-default); background: var(--color-background-default);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--z-index-panels); z-index: var(--z-index-panels);
/* margin: 0 1.5rem; */
/* margin-top: 1rem; */
margin: var(--sp-xxl) var(--sp-xxl) var(--sp-xxl) var(--sp-xxl);
} }
.selected { .nav-option {
@include t.use-typography("headline-small"); color: var(--color-foreground-secondary);
padding: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--color-foreground-primary);
border: $b-1 solid transparent; border: $b-1 solid transparent;
cursor: pointer;
}
.selected {
color: var(--color-foreground-primary);
border-bottom: $b-1 solid var(--color-foreground-primary); border-bottom: $b-1 solid var(--color-foreground-primary);
padding: 0 var(--sp-m);
} }
.project { .project {

View File

@ -194,39 +194,32 @@
(st/emit! (dd/restore-files-immediately (st/emit! (dd/restore-files-immediately
(with-meta {:team-id (:id current-team) (with-meta {:team-id (:id current-team)
:ids #{(:id file)}} :ids #{(:id file)}}
{:on-success #(st/emit! (ntf/success (tr "restore-modal.success-restore-immediately" (:name file))) {:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
(dd/fetch-projects (:id current-team)) (dd/fetch-projects (:id current-team))
(dd/fetch-deleted-files (:id current-team))) (dd/fetch-deleted-files (:id current-team)))
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-file" (:name file))))})))) :on-error #(st/emit! (ntf/error (tr "dashboard.errors.error-on-restore-file" (:name file))))}))))
on-restore-immediately on-restore-immediately
(fn [] (fn []
(st/emit! (st/emit!
(modal/show {:type :confirm (modal/show {:type :confirm
:title (tr "restore-modal.restore-file.title") :title (tr "dashboard-restore-file-confirmation.title")
:message (tr "restore-modal.restore-file.description" (:name file)) :message (tr "dashboard-restore-file-confirmation.description" (:name file))
:accept-label (tr "labels.continue") :accept-label (tr "labels.continue")
:accept-style :primary :accept-style :primary
:on-accept restore-fn}))) :on-accept restore-fn})))
delete-fn
(fn [_]
(st/emit! (ntf/success (tr "delete-forever-modal.success-delete-immediately" (:name file)))
(dd/delete-files-immediately
{:team-id (:id current-team)
:ids #{(:id file)}})
(dd/fetch-projects (:id current-team))
(dd/fetch-deleted-files (:id current-team))))
on-delete-immediately on-delete-immediately
(fn [] (fn []
(st/emit! (let [accept-fn #(st/emit! (dd/delete-files-immediately
(modal/show {:type :confirm {:team-id (:id current-team)
:title (tr "delete-forever-modal.title") :ids #{(:id file)}}))]
:message (tr "delete-forever-modal.delete-file.description" (:name file)) (st/emit!
:accept-label (tr "delete-forever-modal.title") (modal/show {:type :confirm
:on-accept delete-fn})))] :title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-file-forever-confirmation.description" (:name file))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn}))))]
(mf/with-effect [] (mf/with-effect []
(->> (rp/cmd! :get-all-projects) (->> (rp/cmd! :get-all-projects)
@ -268,11 +261,11 @@
options options
(if can-restore (if can-restore
[(when can-restore [(when can-restore
{:name (tr "dashboard.restore-file") {:name (tr "dashboard.restore-file-button")
:id "restore-file" :id "restore-file"
:handler on-restore-immediately}) :handler on-restore-immediately})
(when can-restore (when can-restore
{:name (tr "dashboard.delete-file") {:name (tr "dashboard.delete-file-button")
:id "delete-file" :id "delete-file"
:handler on-delete-immediately})] :handler on-delete-immediately})]
(if multi? (if multi?

View File

@ -240,10 +240,13 @@
;; --- Grid Item ;; --- Grid Item
(mf/defc grid-item-metadata (mf/defc grid-item-metadata*
[{:keys [modified-at]}] [{:keys [file]}]
(let [time (ct/timeago modified-at)] (let [time (ct/timeago (or (:will-be-deleted-at file)
[:span {:class (stl/css :date)} time])) (:modified-at file)))]
[:span {:class (stl/css :date)
:title (tr "dashboard.deleted.will-be-deleted-at" time)}
time]))
(defn create-counter-element (defn create-counter-element
[_element file-count] [_element file-count]
@ -429,7 +432,7 @@
:on-end edit :on-end edit
:max-length 250}] :max-length 250}]
[:h3 (:name file)]) [:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at file)}]] [:> grid-item-metadata* {:file file}]]
[:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)} [:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)}
[:div [:div

View File

@ -17,11 +17,11 @@
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.dashboard.deleted :as deleted]
[app.main.ui.dashboard.grid :refer [line-grid]] [app.main.ui.dashboard.grid :refer [line-grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]
@ -316,40 +316,34 @@
{::mf/props :obj} {::mf/props :obj}
[{:keys [team projects profile]}] [{:keys [team projects profile]}]
(let [projects (let [team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
my-penpot? (= (:default-team-id profile) team-id)
default-team? (:is-default team)
projects
(mf/with-memo [projects] (mf/with-memo [projects]
(->> projects (->> projects
(remove :deleted-at) (remove :deleted-at)
(sort-by :modified-at) (sort-by :modified-at)
(reverse))) (reverse)))
team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
is-my-penpot (= (:default-team-id profile) team-id)
is-defalt-team? (:is-default team)
on-close on-close
(mf/use-fn (mf/use-fn
(fn [] (fn []
(reset! show-team-hero* false) (reset! show-team-hero* false)
(st/emit! (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero" (st/emit! (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero"
::ev/origin "dashboard"})))) ::ev/origin "dashboard"}))))]
on-deleted-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-deleted :team-id team-id))))]
(mf/with-effect [show-team-hero?] (mf/with-effect [show-team-hero?]
(swap! storage/global assoc ::show-team-hero show-team-hero?)) (swap! storage/global assoc ::show-team-hero show-team-hero?))
@ -373,25 +367,19 @@
[:* [:*
(when (and show-team-hero? (when (and show-team-hero?
can-invite can-invite
(not is-defalt-team?)) (not default-team?))
[:> team-hero* {:team team :on-close on-close}]) [:> team-hero* {:team team :on-close on-close}])
[:div {:class (stl/css-case :dashboard-container true [:div {:class (stl/css-case :dashboard-container true
:no-bg true :no-bg true
:dashboard-projects true :dashboard-projects true
:with-team-hero (and (not is-my-penpot) :with-team-hero (and (not my-penpot?)
(not is-defalt-team?) (not default-team?)
show-team-hero? show-team-hero?
can-invite))} can-invite))}
[:div {:class (stl/css :nav-options)}
[:div {:class (stl/css :selected) [:> deleted/menu* {:team-id team-id :section :dashboard-recent}]
:data-testid "recent-tab"}
(tr "dashboard.labels.recent")]
[:> button* {:variant "ghost"
:type "button"
:data-testid "deleted-tab"
:on-click on-deleted-click}
(tr "dashboard.labels.deleted")]]
(for [{:keys [id] :as project} projects] (for [{:keys [id] :as project} projects]
;; FIXME: refactor this, looks inneficient ;; FIXME: refactor this, looks inneficient
(let [files (when recent-map (let [files (when recent-map

View File

@ -248,26 +248,3 @@
width: 0; width: 0;
} }
} }
.nav-options {
display: flex;
gap: var(--sp-l);
justify-content: space-between;
border-bottom: $b-1 solid var(--panel-border-color);
padding-inline-start: var(--sp-l);
background: var(--color-background-default);
position: sticky;
top: 0;
z-index: var(--z-index-panels);
}
.selected {
@include t.use-typography("headline-small");
display: flex;
align-items: center;
justify-content: center;
color: var(--color-foreground-primary);
border: $b-1 solid transparent;
border-bottom: $b-1 solid var(--color-foreground-primary);
padding: 0 var(--sp-m);
}

View File

@ -12,7 +12,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.color :as clr] [app.common.types.color :as clr]
[app.main.data.dashboard :as dd]
[app.main.data.exports.assets :as de] [app.main.data.exports.assets :as de]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -206,13 +205,13 @@
:cmd :export-frames :cmd :export-frames
:origin origin}])) :origin origin}]))
;; FIXME: deprecated, should be refactored in two components and use
;; the generic progress reporter
(mf/defc progress-widget (mf/defc progress-widget
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [operation] :or {operation :export}}] []
(let [state (mf/deref (case operation (let [state (mf/deref refs/export)
:export refs/export
:restore refs/restore
refs/export))
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
theme (or (:theme profile) theme/default) theme (or (:theme profile) theme/default)
is-default-theme? (= theme/default theme) is-default-theme? (= theme/default theme)
@ -221,10 +220,7 @@
detail-visible? (:detail-visible state) detail-visible? (:detail-visible state)
widget-visible? (:widget-visible state) widget-visible? (:widget-visible state)
progress (:progress state) progress (:progress state)
items (case operation items (:exports state)
:export (:exports state)
:restore (:files state)
[])
total (or (:total state) (count items)) total (or (:total state) (count items))
complete? (= progress total) complete? (= progress total)
circ (* 2 Math/PI 12) circ (* 2 Math/PI 12)
@ -250,43 +246,23 @@
title title
(cond (cond
error? (case operation error? (tr "workspace.options.exporting-object-error")
:export (tr "workspace.options.exporting-object-error") complete? (tr "workspace.options.exporting-complete")
:restore (tr "workspace.options.restoring-object-error") healthy? (tr "workspace.options.exporting-object")
(tr "workspace.options.processing-object-error")) (not healthy?) (tr "workspace.options.exporting-object-slow"))
complete? (case operation
:export (tr "workspace.options.exporting-complete")
:restore (tr "workspace.options.restoring-complete")
(tr "workspace.options.processing-complete"))
healthy? (case operation
:export (tr "workspace.options.exporting-object")
:restore (tr "workspace.options.restoring-object")
(tr "workspace.options.processing-object"))
(not healthy?) (case operation
:export (tr "workspace.options.exporting-object-slow")
:restore (tr "workspace.options.restoring-object-slow")
(tr "workspace.options.processing-object-slow")))
retry-last-operation retry-last-operation
(mf/use-fn (mf/use-fn
(mf/deps operation)
(fn [] (fn []
(case operation (st/emit! (de/retry-last-export))))
:export (st/emit! (de/retry-last-export))
:restore (st/emit! (dd/retry-last-restore))
nil)))
toggle-detail-visibility toggle-detail-visibility
(mf/use-fn (mf/use-fn
(mf/deps operation)
(fn [] (fn []
(case operation (st/emit! (de/toggle-detail-visibililty))))]
:export (st/emit! (de/toggle-detail-visibililty))
:restore (st/emit! (dd/toggle-restore-detail-visibility))
nil)))]
[:* [:*
(when (and widget-visible? (= operation :export)) (when widget-visible?
[:div {:class (stl/css :export-progress-widget) [:div {:class (stl/css :export-progress-widget)
:on-click toggle-detail-visibility} :on-click toggle-detail-visibility}
[:svg {:width "24" :height "24"} [:svg {:width "24" :height "24"}

View File

@ -167,7 +167,7 @@
(open-share-dialog))) (open-share-dialog)))
[:div {:class (stl/css :options-zone)} [:div {:class (stl/css :options-zone)}
[:& progress-widget {:operation :export}] [:& progress-widget]
(case section (case section
:interactions [:* :interactions [:*

View File

@ -200,7 +200,7 @@
[:div {:class (stl/css :users-section)} [:div {:class (stl/css :users-section)}
[:& active-sessions]] [:& active-sessions]]
[:& progress-widget {:operation :export}] [:& progress-widget]
[:div {:class (stl/css :separator)}] [:div {:class (stl/css :separator)}]

View File

@ -46,6 +46,10 @@
[event] [event]
(= "end" (get-type event))) (= "end" (get-type event)))
(defn progress?
[event]
(= "progress" (get-type event)))
(defn event? (defn event?
[event] [event]
(= "event" (get-type event))) (= "event" (get-type event)))

View File

@ -8422,110 +8422,113 @@ msgstr "Autosaved versions will be kept for %s days."
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path" msgstr "Click to close the path"
msgid "dashboard.labels.recent" msgid "dashboard.deleted.will-be-deleted-at"
msgstr "Will be deleted %s"
msgid "labels.recent"
msgstr "Recent" msgstr "Recent"
msgid "dashboard.labels.deleted" msgid "labels.deleted"
msgstr "Deleted" msgstr "Deleted"
msgid "dashboard.deleted.restore-all" msgid "dashboard.restore-all-deleted-button"
msgstr "Restore All" msgstr "Restore All"
msgid "dashboard.deleted.clear" msgid "dashboard.clear-trash-button"
msgstr "Clear trash" msgstr "Clear trash"
msgid "dashboard.restore-file" msgid "dashboard.restore-file-button"
msgstr "Restore file" msgstr "Restore file"
msgid "dashboard.delete-file" msgid "dashboard.delete-file-button"
msgstr "Delete file" msgstr "Delete file"
msgid "dashboard.deleted.restore-project" msgid "dashboard.restore-project-button"
msgstr "Restore project" msgstr "Restore project"
msgid "dashboard.deleted.delete-project" msgid "dashboard.delete-project-button"
msgstr "Delete project" msgstr "Delete project"
msgid "dashboard.deleted.info-text" msgid "dashboard.trash-info-text-part1"
msgstr "Deleted files will remain in the trash for" msgstr "Deleted files will remain in the trash for"
msgid "dashboard.deleted.info-days" msgid "dashboard.trash-info-text-part2"
msgstr " %s days. " msgstr " %s days. "
msgid "dashboard.deleted.info-text2" msgid "dashboard.trash-info-text-part3"
msgstr "After that, they will be permanently deleted." msgstr "After that, they will be permanently deleted."
msgid "dashboard.deleted.restore-text" msgid "dashboard.trash-info-text-part4"
msgstr "If you change your mind, you can restore them or delete them permanently from each file's menu." msgstr "If you change your mind, you can restore them or delete them permanently from each file's menu."
msgid "dashboard.deleted.delete-forever" msgid "dashboard.restore-all-confirmation.title"
msgstr "Delete forever"
msgid "restore-modal.restore-all.title"
msgstr "Restore all projects and files" msgstr "Restore all projects and files"
msgid "restore-modal.restore-all.description" msgid "dashboard.restore-all-confirmation.description"
msgstr "You're going to restore all your projects and files. This may take a while." msgstr "You're going to restore all your projects and files. This may take a while."
msgid "restore-modal.restore-file.title" msgid "dashboard-restore-file-confirmation.title"
msgstr "Restore file" msgstr "Restore file"
msgid "restore-modal.restore-file.description" msgid "dashboard-restore-file-confirmation.description"
msgstr "You're going to restore %s." msgstr "You're going to restore %s."
msgid "restore-modal.restore-project.title" msgid "dashboard.restore-project-confirmation.title"
msgstr "Restore Project" msgstr "Restore Project"
msgid "restore-modal.restore-project.description" msgid "dashboard.restore-project-confirmation.description"
msgstr "You're going to restore %s project and all the files contained in it." msgstr "You're going to restore %s project and all the files contained in it."
msgid "delete-forever-modal.title" msgid "dashboard.delete-forever-confirmation.title"
msgstr "Delete forever" msgstr "Delete forever"
msgid "delete-forever-modal.delete-all.description" msgid "dashboard.delete-all-forever-confirmation.description"
msgstr "Are you sure you want to delete forever all your deleted projects and files? This is a non reversible action." msgstr "Are you sure you want to delete forever all your deleted projects and files? This is a non reversible action."
msgid "delete-forever-modal.delete-file.description" msgid "dashboard.delete-file-forever-confirmation.description"
msgstr "Are you sure you want to delete forever %s? This is a non reversible action." msgstr "Are you sure you want to delete forever %s? This is a non reversible action."
msgid "delete-forever-modal.delete-project.description" msgid "dashboard.delete-project-forever-confirmation.description"
msgstr "Are you sure you want to delete forever %s project? You're going to delete it forever an all of the files contained in it. This is a non reeversible action." msgstr "Are you sure you want to delete forever %s project? You're going to delete it forever an all of the files contained in it. This is a non reversible action."
msgid "restore-modal.success-restore-immediately" msgid "dashboard.restore-files-success-notification"
msgstr "%s files have been successfully restored."
msgid "dashboard.restore-success-notification"
msgstr "%s has been successfully restored." msgstr "%s has been successfully restored."
msgid "delete-forever-modal.success-delete-immediately" msgid "dashboard.delete-files-success-notification"
msgstr "%s files have been successfully deleted."
msgid "dashboard.delete-success-notification"
msgstr "%s has been successfully deleted." msgstr "%s has been successfully deleted."
msgid "restore-modal.error-restore-files" msgid "dashboard.errors.error-on-restore-files"
msgstr "There was an error while restoring the files." msgstr "There was an error while restoring the files."
msgid "restore-modal.error-restore-file" msgid "dashboard.errors.error-on-restore-file"
msgstr "There was an error while restoring the file %s." msgstr "There was an error while restoring the file %s."
msgid "restore-modal.error-restore-project" msgid "dashboard.errors.error-on-restoring-project"
msgstr "There was an error while restoring the project %s and its files." msgstr "There was an error while restoring the project %s and its files."
msgid "restore-modal.normal-progress-label" msgid "dashboard.errors.error-on-delete-file"
msgstr "There was an error while deleting the file %s."
msgid "dashboard.errors.error-on-delete-files"
msgstr "There was an error while deleting the files."
msgid "dashboard.errors.error-on-delete-project"
msgstr "There was an error while deleting the project %s."
msgid "dashboard.progress-notification.restoring-files"
msgstr "Restoring files…" msgstr "Restoring files…"
msgid "restore-modal.failed-progress-label" msgid "dashboard.progress-notification.deleting-files"
msgstr "Restore failed" msgstr "Deleting files…"
msgid "restore-modal.slow-progress-label" msgid "dashboard.progress-notification.slow-restore"
msgstr "Restore unexpectedly slow" msgstr "Restore unexpectedly slow"
msgid "restore-modal.complete-process-label" msgid "dashboard.progress-notification.slow-delete"
msgstr "Restore completed" msgstr "Deletion unexpectedly slow"
msgid "progress-widget.default-normal-progress-label"
msgstr "Processing…"
msgid "progress-widget.default-failed-progress-label"
msgstr "Process failed"
msgid "progress-widget.default-slow-progress-label"
msgstr "Process unexpectedly slow"
msgid "progress-widget.default-complete-progress-label"
msgstr "Process completed"

View File

@ -8278,110 +8278,110 @@ msgstr "Los autoguardados duran %s días."
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" msgstr "Pulsar para cerrar la ruta"
msgid "dashboard.labels.recent" msgid "labels.recent"
msgstr "Recientes" msgstr "Recientes"
msgid "dashboard.labels.deleted" msgid "labels.deleted"
msgstr "Eliminados" msgstr "Eliminados"
msgid "dashboard.deleted.restore-all" msgid "dashboard.restore-all-deleted-button"
msgstr "Restaurar todo" msgstr "Restaurar todo"
msgid "dashboard.deleted.clear" msgid "dashboard.clear-trash-button"
msgstr "Vaciar papelera" msgstr "Vaciar papelera"
msgid "dashboard.restore-file" msgid "dashboard.restore-file-button"
msgstr "Restaurar archivo" msgstr "Restaurar archivo"
msgid "dashboard.delete-file" msgid "dashboard.delete-file-button"
msgstr "Eliminar archivo" msgstr "Eliminar archivo"
msgid "dashboard.deleted.restore-project" msgid "dashboard.restore-project-button"
msgstr "Restaurar proyecto" msgstr "Restaurar proyecto"
msgid "dashboard.deleted.delete-project" msgid "dashboard.delete-project-button"
msgstr "Eliminar proyecto" msgstr "Eliminar proyecto"
msgid "dashboard.deleted.info-text" msgid "dashboard.trash-info-text-part1"
msgstr "Los archivos eliminados permanecerán en la papelera durante" msgstr "Los archivos eliminados permanecerán en la papelera durante"
msgid "dashboard.deleted.info-days" msgid "dashboard.trash-info-text-part2"
msgstr " %s días. " msgstr " %s días. "
msgid "dashboard.deleted.info-text2" msgid "dashboard.trash-info-text-part3"
msgstr "Después de eso, serán eliminados permanentemente." msgstr "Después de eso, serán eliminados permanentemente."
msgid "dashboard.deleted.restore-text" msgid "dashboard.trash-info-text-part4"
msgstr "Si cambias de opinión, puedes restaurarlos o eliminarlos permanentemente desde el menú de cada archivo." msgstr "Si cambias de opinión, puedes restaurarlos o eliminarlos permanentemente desde el menú de cada archivo."
msgid "dashboard.deleted.delete-forever" msgid "dashboard.deleted.delete-forever"
msgstr "Eliminar para siempre" msgstr "Eliminar para siempre"
msgid "restore-modal.restore-all.title" msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaurar todos los proyectos y archivos" msgstr "Restaurar todos los proyectos y archivos"
msgid "restore-modal.restore-all.description" msgid "dashboard.restore-all-confirmation.description"
msgstr "Vas a restaurar todos tus proyectos y archivos. Esto puede tardar un poco." msgstr "Vas a restaurar todos tus proyectos y archivos. Esto puede tardar un poco."
msgid "restore-modal.restore-file.title" msgid "dashboard-restore-file-confirmation.title"
msgstr "Restaurar archivo" msgstr "Restaurar archivo"
msgid "restore-modal.restore-file.description" msgid "dashboard-restore-file-confirmation.description"
msgstr "Vas a restaurar %s." msgstr "Vas a restaurar %s."
msgid "restore-modal.restore-project.title" msgid "dashboard.restore-project-confirmation.title"
msgstr "Restaurar proyecto" msgstr "Restaurar proyecto"
msgid "restore-modal.restore-project.description" msgid "dashboard.restore-project-confirmation.description"
msgstr "Vas a restaurar el proyecto %s y todos los archivos que contiene." msgstr "Vas a restaurar el proyecto %s y todos los archivos que contiene."
msgid "delete-forever-modal.title" msgid "dashboard.delete-forever-confirmation.title"
msgstr "Eliminar para siempre" msgstr "Eliminar para siempre"
msgid "delete-forever-modal.delete-all.description" msgid "dashboard.delete-all-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre todos tus proyectos y archivos eliminados? Esta es una acción irreversible." msgstr "¿Estás seguro de que quieres eliminar para siempre todos tus proyectos y archivos eliminados? Esta es una acción irreversible."
msgid "delete-forever-modal.delete-file.description" msgid "dashboard.delete-file-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre %s? Esta es una acción irreversible." msgstr "¿Estás seguro de que quieres eliminar para siempre %s? Esta es una acción irreversible."
msgid "delete-forever-modal.delete-project.description" msgid "dashboard.delete-project-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre el proyecto %s? Vas a eliminarlo para siempre junto con todos los archivos que contiene. Esta es una acción irreversible." msgstr "¿Estás seguro de que quieres eliminar para siempre el proyecto %s? Vas a eliminarlo para siempre junto con todos los archivos que contiene. Esta es una acción irreversible."
msgid "restore-modal.success-restore-immediately" msgid "dashboard.restore-files-success-notification"
msgstr "%s ficheros han sido restaurado correctamente."
msgid "dashboard.restore-success-notification"
msgstr "%s ha sido restaurado correctamente." msgstr "%s ha sido restaurado correctamente."
msgid "delete-forever-modal.success-delete-immediately" msgid "dashboard.delete-files-success-notification"
msgstr "%s ficheros han sido eliminados correctamente."
msgid "dashboard.delete-success-notification"
msgstr "%s ha sido eliminado correctamente." msgstr "%s ha sido eliminado correctamente."
msgid "restore-modal.error-restore-files" msgid "dashboard.errors.error-on-restore-files"
msgstr "Hubo un error al restaurar los archivos." msgstr "Hubo un error al restaurar los archivos."
msgid "restore-modal.error-restore-file" msgid "dashboard.errors.error-on-restore-file"
msgstr "Hubo un error al restaurar el archivo %s." msgstr "Hubo un error al restaurar el archivo %s."
msgid "restore-modal.error-restore-project" msgid "dashboard.errors.error-on-restoring-files"
msgstr "Hubo un error al restaurar el proyecto %s y sus archivos." msgstr "Hubo un error al restaurar archivos."
msgid "restore-modal.normal-progress-label" msgid "dashboard.errors.error-on-delete-files"
msgstr "Hubo un error al eliminar archivos."
msgid "dashboard.errors.error-on-delete-project"
msgstr "Hubo un error al eliminar el proyecto %s"
msgid "dashboard.progress-notification.restoring-files"
msgstr "Restaurando archivos…" msgstr "Restaurando archivos…"
msgid "restore-modal.failed-progress-label" msgid "dashboard.progress-notification.deleting-files"
msgstr "Falló la restauración" msgstr "Eliminando archivos…"
msgid "restore-modal.slow-progress-label" msgid "dashboard.progress-notification.slow-restore"
msgstr "Restauración lenta" msgstr "Restauración inesperadamente lenta"
msgid "restore-modal.complete-process-label" msgid "dashboard.progress-notification.slow-delete"
msgstr "Restauración completada" msgstr "Eliminación inesperadamente lenta"
msgid "progress-widget.default-normal-progress-label"
msgstr "Procesando…"
msgid "progress-widget.default-failed-progress-label"
msgstr "Falló el procesamiento"
msgid "progress-widget.default-slow-progress-label"
msgstr "Procesamiento lento"
msgid "progress-widget.default-complete-progress-label"
msgstr "Procesamiento completado"