diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index da21dd9616..9cbdf08be6 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -704,7 +704,6 @@ f.created_at, f.modified_at, f.name, - f.is_shared, f.deleted_at AS will_be_deleted_at, ft.media_id AS thumbnail_id, row_number() OVER w AS row_num, @@ -814,7 +813,7 @@ AND (f.deleted_at IS NULL OR f.deleted_at > now()) ORDER BY f.created_at ASC;") -(defn- absorb-library-by-file! +(defn- absorb-library-by-file [cfg ldata file-id] (assert (db/connection-map? cfg) @@ -838,7 +837,7 @@ :modified-at (ct/now) :has-media-trimmed false})))) -(defn- absorb-library +(defn- absorb-library* "Find all files using a shared library, and absorb all library assets into the file local libraries" [cfg {:keys [id data] :as library}] @@ -853,10 +852,10 @@ :library-id (str id) :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)) -(defn absorb-library! +(defn absorb-library [{:keys [::db/conn] :as cfg} id] (let [file (-> (bfc/get-file cfg id :realize? true @@ -873,7 +872,7 @@ (-> (cfeat/get-team-enabled-features cf/flags team) (cfeat/check-file-features! (:features file))) - (absorb-library cfg file))) + (absorb-library* cfg file))) (defn- set-file-shared [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] @@ -886,14 +885,14 @@ ;; file, we need to perform more complex operation, ;; so in this case we retrieve the complete file and ;; perform all required validations. - (let [file (-> (absorb-library! cfg id) + (let [file (-> (absorb-library cfg id) (assoc :is-shared false))] (db/delete! conn :file-library-rel {:library-file-id id}) (db/update! conn :file {:is-shared false :modified-at (ct/now)} {:id id}) - (select-keys file [:id :name :is-shared])) + file) (and (false? (:is-shared file)) (true? (:is-shared params))) @@ -940,6 +939,11 @@ {:id file-id} {::db/return-keys [:id :name :is-shared :deleted-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/task :delete-object ::wrk/params {:object :file @@ -1090,47 +1094,53 @@ ;; --- MUTATION COMMAND: delete-files-immediatelly -(def ^:private sql:delete-team-files - "UPDATE file AS uf SET deleted_at = ?::timestamptz - FROM ( - SELECT f.id - FROM file AS f - JOIN project AS p ON (p.id = f.project_id) - JOIN team AS t ON (t.id = p.team_id) - WHERE t.deleted_at IS NULL - AND t.id = ? - AND f.id = ANY(?::uuid[]) - ) AS subquery - WHERE uf.id = subquery.id - RETURNING uf.id, uf.deleted_at;") +(def ^:private sql:get-delete-team-files-candidates + "SELECT f.id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + JOIN team AS t ON (t.id = p.team_id) + WHERE t.deleted_at IS NULL + AND t.id = ? + AND f.id = ANY(?::uuid[])") (def ^:private schema:permanently-delete-team-files [:map {:title "permanently-delete-team-files"} [:team-id ::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 "Mark the specified files to be deleted immediatelly on the specified team. The team-id on params will be used to filter and check writable permissons on team." - {::doc/added "2.12" - ::sm/params schema:permanently-delete-team-files - ::db/transaction true} + {::doc/added "2.13" + ::sm/params schema:permanently-delete-team-files} - [{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}] - (teams/check-edition-permissions! conn profile-id team-id) - - (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)]))) + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + (teams/check-edition-permissions! pool profile-id team-id) + (sse/response #(db/tx-run! cfg permanently-delete-team-files params))) ;; --- MUTATION COMMAND: restore-files-immediatelly @@ -1194,7 +1204,7 @@ {:keys [files projects]} (reduce (fn [result {:keys [id project-id]}] (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) (-> result @@ -1217,7 +1227,7 @@ (sv/defmethod ::restore-deleted-team-files "Removes the deletion mark from the specified files (and respective projects) on the specified team." - {::doc/added "2.12" + {::doc/added "2.13" ::sse/stream? true ::sm/params schema:restore-deleted-team-files} [cfg params] diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index 61282e4134..51386fe616 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -45,7 +45,8 @@ :deleted-at (ct/format-inst deleted-at)) (db/update! conn :file - {:deleted-at deleted-at} + {:deleted-at deleted-at + :is-shared false} {:id id} {::db/return-keys false}) @@ -53,7 +54,7 @@ (not *team-deletion*)) ;; NOTE: we don't prevent file deletion on absorb operation failure (try - (db/tx-run! cfg files/absorb-library! id) + (db/tx-run! cfg files/absorb-library id) (catch Throwable cause (l/warn :hint "error on absorbing library" :file-id id diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 934a671e03..390a19251b 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -595,8 +595,8 @@ (px/exec! :virtual #(rcp/write-body-to-stream body nil output)) (into [] (map (fn [{:keys [event data]}] - [(keyword event) - (tr/decode-str data)])) + (d/vec2 (keyword event) + (tr/decode-str data)))) (parse-sse (slurp' input))) (finally (.close input))))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index a7700a5121..b676d87156 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -1921,7 +1921,11 @@ ;; (th/print-result! out) (t/is (nil? (:error 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])] (t/is (= (:deleted-at row) now))))))) diff --git a/common/src/app/common/types/project.cljc b/common/src/app/common/types/project.cljc new file mode 100644 index 0000000000..bd14e651a4 --- /dev/null +++ b/common/src/app/common/types/project.cljc @@ -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)) diff --git a/frontend/src/app/main/constants.cljs b/frontend/src/app/main/constants.cljs index 291767a234..210705fdec 100644 --- a/frontend/src/app/main/constants.cljs +++ b/frontend/src/app/main/constants.cljs @@ -302,3 +302,9 @@ :height 720}]) (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) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index e81daf87a2..fb55df73de 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.schema :as sm] + [app.common.time :as ct] [app.common.types.team :as ctt] [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] @@ -229,6 +230,91 @@ ;; Delay so the navigation can finish (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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index d4a3860ccc..a1acdf7e0c 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -13,12 +13,15 @@ [app.common.logging :as log] [app.common.schema :as sm] [app.common.time :as ct] + [app.common.types.project :refer [valid-project?]] [app.common.uuid :as uuid] + [app.main.constants :as mconst] [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.fonts :as df] [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.data.websocket :as dws] [app.main.repo :as rp] [app.main.store :as st] @@ -691,6 +694,56 @@ ;; --- 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 [{:keys [team-id ids] :as params}] (assert (uuid? team-id)) @@ -698,145 +751,190 @@ (assert (every? uuid? ids)) (ptk/reify ::delete-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)] - (->> (rp/cmd! :permanently-delete-team-files {:team-id team-id :ids ids}) - (rx/tap on-success) - (rx/catch on-error)))))) + (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.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 -(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 - [{:keys [index total] :as data}] - (ptk/reify ::upd-restore-status - ptk/UpdateEvent - (update [_ state] - (let [time-diff (ct/diff-ms (get-in state [:restore :last-update]) (ct/now)) - healthy? (< time-diff 6000)] - (update state :restore assoc - :progress index - :total total - :last-update (ct/now) - :healthy? healthy?))))) +(defn- restore-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)) -(defn- complete-restore-status - [] - (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/reify ::restore-files ptk/WatchEvent (watch [_ _ _] - ;; Refetch projects to get the updated state without deleted-at - (rx/of (fetch-projects team-id))))) - -(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)] + (let [progress-hint #(tr "dashboard.progress-notification.restoring-files") + slow-hint #(tr "dashboard.progress-notification.slow-restore")] (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}) - (rx/tap (fn [event] - (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))))) + (let [stream (->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids}) + (rx/share))] - (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})))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 07f6254cab..76d02544cc 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -637,5 +637,5 @@ (def persistence-state (l/derived (comp :status :persistence) st/state)) -(def restore - (l/derived :restore st/state)) +(def progress + (l/derived :progress st/state)) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 0361c75944..0ad10286aa 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -87,6 +87,9 @@ {:stream? true :form-data? true} + ::sse/permanently-delete-team-files + {:stream? true} + ::sse/restore-deleted-team-files {:stream? true} diff --git a/frontend/src/app/main/ui/components/progress.cljs b/frontend/src/app/main/ui/components/progress.cljs new file mode 100644 index 0000000000..bc724a9a35 --- /dev/null +++ b/frontend/src/app/main/ui/components/progress.cljs @@ -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"}}]]])])])) diff --git a/frontend/src/app/main/ui/components/progress.scss b/frontend/src/app/main/ui/components/progress.scss new file mode 100644 index 0000000000..0ef02d0f17 --- /dev/null +++ b/frontend/src/app/main/ui/components/progress.scss @@ -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; +} diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 58370e82bc..f8ed5aee43 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -19,6 +19,7 @@ [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] + [app.main.ui.components.progress :refer [progress-notification-widget*]] [app.main.ui.context :as ctx] [app.main.ui.dashboard.deleted :refer [deleted-section*]] [app.main.ui.dashboard.files :refer [files-section*]] @@ -30,7 +31,6 @@ [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.templates :refer [templates-section*]] - [app.main.ui.exports.assets :refer [progress-widget]] [app.main.ui.hooks :as hooks] [app.main.ui.modal :refer [modal-container*]] [app.main.ui.workspace.plugins] @@ -87,7 +87,7 @@ :on-click clear-selected-fn :ref container} - [:& progress-widget {:operation :restore}] + [:> progress-notification-widget*] (case section :dashboard-recent diff --git a/frontend/src/app/main/ui/dashboard/deleted.cljs b/frontend/src/app/main/ui/dashboard/deleted.cljs index 436ace8b1d..254ee249d5 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.cljs +++ b/frontend/src/app/main/ui/dashboard/deleted.cljs @@ -12,7 +12,6 @@ [app.main.data.common :as dcm] [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] - [app.main.data.notifications :as ntf] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu*]] @@ -27,6 +26,8 @@ [okulary.core :as l] [rumext.v2 :as mf])) +(def ^:private ref:deleted-files + (l/derived :deleted-files st/state)) (def ^:private menu-icon (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) @@ -40,57 +41,40 @@ [:h1 (tr "dashboard.projects-title")]]]) (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) 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 - (fn [] - (st/emit! - (modal/show {:type :confirm - :title (tr "restore-modal.restore-project.title") - :message (tr "restore-modal.restore-project.description" (:name project)) - :accept-style :primary - :accept-label (tr "labels.continue") - :on-accept restore-fn}))) - - delete-fn - (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))) + (mf/use-fn + (mf/deps project) + (fn [] + (let [on-accept #(st/emit! (dd/restore-project-immediately project))] + (st/emit! (modal/show {:type :confirm + :title (tr "dashboard.restore-project-confirmation.title") + :message (tr "dashboard.restore-project-confirmation.description" (:name project)) + :accept-style :primary + :accept-label (tr "labels.continue") + :on-accept on-accept}))))) on-delete-project - (fn [] - (st/emit! - (modal/show {:type :confirm - :title (tr "delete-forever-modal.title") - :message (tr "delete-forever-modal.delete-project.description" (:name project)) - :accept-label (tr "dashboard.deleted.delete-forever") - :on-accept delete-fn}))) + (mf/use-fn + (mf/deps project) + (fn [] + (let [accept-fn #(st/emit! (dd/delete-project-immediately project))] + (st/emit! (modal/show {:type :confirm + :title (tr "dashboard.delete-forever-confirmation.title") + :message (tr "dashboard.delete-project-forever-confirmation.description" (:name project)) + :accept-label (tr "dashboard.delete-forever-confirmation.title") + :on-accept accept-fn}))))) options - [{:name (tr "dashboard.deleted.restore-project") - :id "project-restore" - :handler on-restore-project} - {:name (tr "dashboard.deleted.delete-project") - :id "project-delete" - :handler on-delete-project}]] + (mf/with-memo [on-restore-project on-delete-project] + [{:name (tr "dashboard.restore-project-button") + :id "project-restore" + :handler on-restore-project} + {:name (tr "dashboard.delete-project-button") + :id "project-delete" + :handler on-delete-project}])] [:> context-menu* {:on-close on-close @@ -102,9 +86,8 @@ :options options}])) (mf/defc deleted-project-item* - {::mf/props :obj - ::mf/private true} - [{:keys [project team files]}] + {::mf/private true} + [{:keys [project files]}] (let [project-files (filterv #(= (:project-id %) (:id project)) files) empty? (empty? project-files) @@ -170,8 +153,6 @@ (when (:menu-open @local) [:> deleted-project-menu* {:project project - :files project-files - :team-id (:id team) :show (:menu-open @local) :left (+ 24 (:x (:menu-pos @local))) :top (:y (:menu-pos @local)) @@ -193,8 +174,34 @@ :limit limit :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* [{:keys [team projects]}] @@ -230,53 +237,33 @@ (and (= "enterprise" sub-type) (not canceled?)) 90 :else 7)) - on-clear + on-delete-all (mf/use-fn (mf/deps team-id deleted-map) (fn [] - (when deleted-map - (let [file-ids (into #{} (keys deleted-map))] - (when (seq file-ids) - (st/emit! - (modal/show {:type :confirm - :title (tr "delete-forever-modal.title") - :message (tr "delete-forever-modal.delete-all.description" (count file-ids)) - :accept-label (tr "dashboard.deleted.delete-forever") - :on-accept #(st/emit! - (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")))})))) + (when-let [ids (not-empty (into #{} (map key) deleted-map))] + (let [on-accept #(st/emit! (dd/delete-files-immediately + {:team-id team-id + :ids ids}))] + (st/emit! (modal/show {:type :confirm + :title (tr "dashboard.delete-forever-confirmation.title") + :message (tr "dashboard.delete-all-forever-confirmation.description" (count ids)) + :accept-label (tr "dashboard.delete-forever-confirmation.title") + :on-accept on-accept})))))) on-restore-all (mf/use-fn (mf/deps team-id deleted-map) (fn [] - (when deleted-map - (let [file-ids (into #{} (keys deleted-map))] - (when (seq file-ids) - (st/emit! - (modal/show {:type :confirm - :title (tr "restore-modal.restore-all.title") - :message (tr "restore-modal.restore-all.description" (count file-ids)) - :accept-label (tr "labels.continue") - :accept-style :primary - :on-accept #(restore-fn file-ids)}))))))) + (when-let [ids (not-empty (into #{} (map key) deleted-map))] + (let [on-accept #(st/emit! (dd/restore-files-immediately {:team-id team-id :ids ids}))] + (st/emit! (modal/show {:type :confirm + :title (tr "dashboard.restore-all-confirmation.title") + :message (tr "dashboard.restore-all-confirmation.description" (count ids)) + :accept-label (tr "labels.continue") + :accept-style :primary + :on-accept on-accept}))))))] - 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] (st/emit! (dd/fetch-projects team-id) @@ -289,35 +276,26 @@ [:* [:div {:class (stl/css :no-bg)} - [:div {:class (stl/css :nav-options)} - [:> 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")]] + [:> menu* {:team-id team-id :section :dashboard-deleted}] - [:div {:class (stl/css :deleted-content)} - [:div {:class (stl/css :deleted-info)} - [:div - (tr "dashboard.deleted.info-text") - [:span {:class (stl/css :info-text-highlight)} - (tr "dashboard.deleted.info-days" deletion-days)] - (tr "dashboard.deleted.info-text2")] - [:div - (tr "dashboard.deleted.restore-text")]] + [:div {:class (stl/css :deleted-info-content)} + [:p {:class (stl/css :deleted-info)} + (tr "dashboard.trash-info-text-part1") + [:span {:class (stl/css :info-text-highlight)} + (tr "dashboard.trash-info-text-part2" deletion-days)] + (tr "dashboard.trash-info-text-part3") + [:br] + (tr "dashboard.trash-info-text-part4")] [:div {:class (stl/css :deleted-options)} [:> button* {:variant "ghost" :type "button" :on-click on-restore-all} - (tr "dashboard.deleted.restore-all")] + (tr "dashboard.restore-all-deleted-button")] [:> button* {:variant "destructive" :type "button" :icon "delete" - :on-click on-clear} - (tr "dashboard.deleted.clear")]]] + :on-click on-delete-all} + (tr "dashboard.clear-trash-button")]]] (when (seq projects) (for [{:keys [id] :as project} projects] @@ -326,6 +304,5 @@ (filterv #(= id (:project-id %))) (sort-by :modified-at #(compare %2 %1))))] [:> deleted-project-item* {:project project - :team team :files files :key id}])))]]]])) diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss index 732a1e704b..00cdb3258d 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.scss +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -20,17 +20,19 @@ padding-block-end: var(--sp-xxxl); } -.deleted-content { +.deleted-info-content { display: flex; - gap: var(--sp-l); justify-content: space-between; - margin-inline-start: var(--sp-l); - margin-block-start: var(--sp-xxl); + padding: var(--sp-s) var(--sp-xxl) var(--sp-s) var(--sp-xxl); } .deleted-info { - @include t.use-typography("body-medium"); + display: block; + height: fit-content; color: var(--color-foreground-secondary); + @include t.use-typography("body-large"); + line-height: 0.8; + height: var(--sp-xl); } .info-text-highlight { @@ -43,27 +45,37 @@ flex-shrink: 0; } -.nav-options { +.nav { 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); + //padding-inline-start: var(--sp-l); background: var(--color-background-default); position: sticky; top: 0; 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 { - @include t.use-typography("headline-small"); +.nav-option { + color: var(--color-foreground-secondary); + padding: 0.5rem; + display: flex; align-items: center; justify-content: center; - color: var(--color-foreground-primary); border: $b-1 solid transparent; + cursor: pointer; +} + +.selected { + color: var(--color-foreground-primary); border-bottom: $b-1 solid var(--color-foreground-primary); - padding: 0 var(--sp-m); } .project { diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index ad08af02a6..b08a42118e 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -194,39 +194,32 @@ (st/emit! (dd/restore-files-immediately (with-meta {:team-id (:id current-team) :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-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 (fn [] (st/emit! (modal/show {:type :confirm - :title (tr "restore-modal.restore-file.title") - :message (tr "restore-modal.restore-file.description" (:name file)) + :title (tr "dashboard-restore-file-confirmation.title") + :message (tr "dashboard-restore-file-confirmation.description" (:name file)) :accept-label (tr "labels.continue") :accept-style :primary :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 (fn [] - (st/emit! - (modal/show {:type :confirm - :title (tr "delete-forever-modal.title") - :message (tr "delete-forever-modal.delete-file.description" (:name file)) - :accept-label (tr "delete-forever-modal.title") - :on-accept delete-fn})))] + (let [accept-fn #(st/emit! (dd/delete-files-immediately + {:team-id (:id current-team) + :ids #{(:id file)}}))] + (st/emit! + (modal/show {:type :confirm + :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 [] (->> (rp/cmd! :get-all-projects) @@ -268,11 +261,11 @@ options (if can-restore [(when can-restore - {:name (tr "dashboard.restore-file") + {:name (tr "dashboard.restore-file-button") :id "restore-file" :handler on-restore-immediately}) (when can-restore - {:name (tr "dashboard.delete-file") + {:name (tr "dashboard.delete-file-button") :id "delete-file" :handler on-delete-immediately})] (if multi? diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index c87e5ef699..ba3304305e 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -240,10 +240,13 @@ ;; --- Grid Item -(mf/defc grid-item-metadata - [{:keys [modified-at]}] - (let [time (ct/timeago modified-at)] - [:span {:class (stl/css :date)} time])) +(mf/defc grid-item-metadata* + [{:keys [file]}] + (let [time (ct/timeago (or (:will-be-deleted-at file) + (:modified-at file)))] + [:span {:class (stl/css :date) + :title (tr "dashboard.deleted.will-be-deleted-at" time)} + time])) (defn create-counter-element [_element file-count] @@ -429,7 +432,7 @@ :on-end edit :max-length 250}] [: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 diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index d49e9b31d9..af92638e5d 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -17,11 +17,11 @@ [app.main.data.project :as dpj] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.dashboard.deleted :as deleted] [app.main.ui.dashboard.grid :refer [line-grid]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.pin-button :refer [pin-button*]] [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.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] @@ -316,40 +316,34 @@ {::mf/props :obj} [{: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] (->> projects (remove :deleted-at) (sort-by :modified-at) (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 (mf/use-fn (fn [] (reset! show-team-hero* false) (st/emit! (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero" - ::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))))] + ::ev/origin "dashboard"}))))] (mf/with-effect [show-team-hero?] (swap! storage/global assoc ::show-team-hero show-team-hero?)) @@ -373,25 +367,19 @@ [:* (when (and show-team-hero? can-invite - (not is-defalt-team?)) + (not default-team?)) [:> team-hero* {:team team :on-close on-close}]) [:div {:class (stl/css-case :dashboard-container true :no-bg true :dashboard-projects true - :with-team-hero (and (not is-my-penpot) - (not is-defalt-team?) + :with-team-hero (and (not my-penpot?) + (not default-team?) show-team-hero? can-invite))} - [:div {:class (stl/css :nav-options)} - [:div {:class (stl/css :selected) - :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")]] + + [:> deleted/menu* {:team-id team-id :section :dashboard-recent}] + (for [{:keys [id] :as project} projects] ;; FIXME: refactor this, looks inneficient (let [files (when recent-map diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 96462ef401..173a487f8b 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -248,26 +248,3 @@ 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); -} diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs index 5531d2f9df..e6dd1ec51b 100644 --- a/frontend/src/app/main/ui/exports/assets.cljs +++ b/frontend/src/app/main/ui/exports/assets.cljs @@ -12,7 +12,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.types.color :as clr] - [app.main.data.dashboard :as dd] [app.main.data.exports.assets :as de] [app.main.data.modal :as modal] [app.main.refs :as refs] @@ -206,13 +205,13 @@ :cmd :export-frames :origin origin}])) +;; FIXME: deprecated, should be refactored in two components and use +;; the generic progress reporter + (mf/defc progress-widget {::mf/wrap [mf/memo]} - [{:keys [operation] :or {operation :export}}] - (let [state (mf/deref (case operation - :export refs/export - :restore refs/restore - refs/export)) + [] + (let [state (mf/deref refs/export) profile (mf/deref refs/profile) theme (or (:theme profile) theme/default) is-default-theme? (= theme/default theme) @@ -221,10 +220,7 @@ detail-visible? (:detail-visible state) widget-visible? (:widget-visible state) progress (:progress state) - items (case operation - :export (:exports state) - :restore (:files state) - []) + items (:exports state) total (or (:total state) (count items)) complete? (= progress total) circ (* 2 Math/PI 12) @@ -250,43 +246,23 @@ title (cond - error? (case operation - :export (tr "workspace.options.exporting-object-error") - :restore (tr "workspace.options.restoring-object-error") - (tr "workspace.options.processing-object-error")) - 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"))) + error? (tr "workspace.options.exporting-object-error") + complete? (tr "workspace.options.exporting-complete") + healthy? (tr "workspace.options.exporting-object") + (not healthy?) (tr "workspace.options.exporting-object-slow")) retry-last-operation (mf/use-fn - (mf/deps operation) (fn [] - (case operation - :export (st/emit! (de/retry-last-export)) - :restore (st/emit! (dd/retry-last-restore)) - nil))) + (st/emit! (de/retry-last-export)))) toggle-detail-visibility (mf/use-fn - (mf/deps operation) (fn [] - (case operation - :export (st/emit! (de/toggle-detail-visibililty)) - :restore (st/emit! (dd/toggle-restore-detail-visibility)) - nil)))] + (st/emit! (de/toggle-detail-visibililty))))] [:* - (when (and widget-visible? (= operation :export)) + (when widget-visible? [:div {:class (stl/css :export-progress-widget) :on-click toggle-detail-visibility} [:svg {:width "24" :height "24"} diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 20a25deda7..270e30643b 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -167,7 +167,7 @@ (open-share-dialog))) [:div {:class (stl/css :options-zone)} - [:& progress-widget {:operation :export}] + [:& progress-widget] (case section :interactions [:* diff --git a/frontend/src/app/main/ui/workspace/right_header.cljs b/frontend/src/app/main/ui/workspace/right_header.cljs index 277a470ba2..ce10de99cf 100644 --- a/frontend/src/app/main/ui/workspace/right_header.cljs +++ b/frontend/src/app/main/ui/workspace/right_header.cljs @@ -200,7 +200,7 @@ [:div {:class (stl/css :users-section)} [:& active-sessions]] - [:& progress-widget {:operation :export}] + [:& progress-widget] [:div {:class (stl/css :separator)}] diff --git a/frontend/src/app/util/sse.cljs b/frontend/src/app/util/sse.cljs index 0913f052ae..8e1044ec37 100644 --- a/frontend/src/app/util/sse.cljs +++ b/frontend/src/app/util/sse.cljs @@ -46,6 +46,10 @@ [event] (= "end" (get-type event))) +(defn progress? + [event] + (= "progress" (get-type event))) + (defn event? [event] (= "event" (get-type event))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0fd7924a87..f3b19b98e1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8422,110 +8422,113 @@ msgstr "Autosaved versions will be kept for %s days." msgid "workspace.viewport.click-to-close-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" -msgid "dashboard.labels.deleted" +msgid "labels.deleted" msgstr "Deleted" -msgid "dashboard.deleted.restore-all" +msgid "dashboard.restore-all-deleted-button" msgstr "Restore All" -msgid "dashboard.deleted.clear" +msgid "dashboard.clear-trash-button" msgstr "Clear trash" -msgid "dashboard.restore-file" +msgid "dashboard.restore-file-button" msgstr "Restore file" -msgid "dashboard.delete-file" +msgid "dashboard.delete-file-button" msgstr "Delete file" -msgid "dashboard.deleted.restore-project" +msgid "dashboard.restore-project-button" msgstr "Restore project" -msgid "dashboard.deleted.delete-project" +msgid "dashboard.delete-project-button" msgstr "Delete project" -msgid "dashboard.deleted.info-text" +msgid "dashboard.trash-info-text-part1" msgstr "Deleted files will remain in the trash for" -msgid "dashboard.deleted.info-days" +msgid "dashboard.trash-info-text-part2" msgstr " %s days. " -msgid "dashboard.deleted.info-text2" +msgid "dashboard.trash-info-text-part3" 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." -msgid "dashboard.deleted.delete-forever" -msgstr "Delete forever" - -msgid "restore-modal.restore-all.title" +msgid "dashboard.restore-all-confirmation.title" 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." -msgid "restore-modal.restore-file.title" +msgid "dashboard-restore-file-confirmation.title" msgstr "Restore file" -msgid "restore-modal.restore-file.description" +msgid "dashboard-restore-file-confirmation.description" msgstr "You're going to restore %s." -msgid "restore-modal.restore-project.title" +msgid "dashboard.restore-project-confirmation.title" 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." -msgid "delete-forever-modal.title" +msgid "dashboard.delete-forever-confirmation.title" 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." -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." -msgid "delete-forever-modal.delete-project.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." +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 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." -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." -msgid "restore-modal.error-restore-files" +msgid "dashboard.errors.error-on-restore-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." -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." -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…" -msgid "restore-modal.failed-progress-label" -msgstr "Restore failed" +msgid "dashboard.progress-notification.deleting-files" +msgstr "Deleting files…" -msgid "restore-modal.slow-progress-label" +msgid "dashboard.progress-notification.slow-restore" msgstr "Restore unexpectedly slow" -msgid "restore-modal.complete-process-label" -msgstr "Restore completed" - -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" \ No newline at end of file +msgid "dashboard.progress-notification.slow-delete" +msgstr "Deletion unexpectedly slow" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 580b18ac60..ab2cd2d6ce 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8278,110 +8278,110 @@ msgstr "Los autoguardados duran %s días." msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" -msgid "dashboard.labels.recent" +msgid "labels.recent" msgstr "Recientes" -msgid "dashboard.labels.deleted" +msgid "labels.deleted" msgstr "Eliminados" -msgid "dashboard.deleted.restore-all" +msgid "dashboard.restore-all-deleted-button" msgstr "Restaurar todo" -msgid "dashboard.deleted.clear" +msgid "dashboard.clear-trash-button" msgstr "Vaciar papelera" -msgid "dashboard.restore-file" +msgid "dashboard.restore-file-button" msgstr "Restaurar archivo" -msgid "dashboard.delete-file" +msgid "dashboard.delete-file-button" msgstr "Eliminar archivo" -msgid "dashboard.deleted.restore-project" +msgid "dashboard.restore-project-button" msgstr "Restaurar proyecto" -msgid "dashboard.deleted.delete-project" +msgid "dashboard.delete-project-button" msgstr "Eliminar proyecto" -msgid "dashboard.deleted.info-text" +msgid "dashboard.trash-info-text-part1" 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. " -msgid "dashboard.deleted.info-text2" +msgid "dashboard.trash-info-text-part3" 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." msgid "dashboard.deleted.delete-forever" msgstr "Eliminar para siempre" -msgid "restore-modal.restore-all.title" +msgid "dashboard.restore-all-confirmation.title" 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." -msgid "restore-modal.restore-file.title" +msgid "dashboard-restore-file-confirmation.title" msgstr "Restaurar archivo" -msgid "restore-modal.restore-file.description" +msgid "dashboard-restore-file-confirmation.description" msgstr "Vas a restaurar %s." -msgid "restore-modal.restore-project.title" +msgid "dashboard.restore-project-confirmation.title" 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." -msgid "delete-forever-modal.title" +msgid "dashboard.delete-forever-confirmation.title" 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." -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." -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." -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." -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." -msgid "restore-modal.error-restore-files" +msgid "dashboard.errors.error-on-restore-files" 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." -msgid "restore-modal.error-restore-project" -msgstr "Hubo un error al restaurar el proyecto %s y sus archivos." +msgid "dashboard.errors.error-on-restoring-files" +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…" -msgid "restore-modal.failed-progress-label" -msgstr "Falló la restauración" +msgid "dashboard.progress-notification.deleting-files" +msgstr "Eliminando archivos…" -msgid "restore-modal.slow-progress-label" -msgstr "Restauración lenta" +msgid "dashboard.progress-notification.slow-restore" +msgstr "Restauración inesperadamente lenta" -msgid "restore-modal.complete-process-label" -msgstr "Restauración completada" - -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" \ No newline at end of file +msgid "dashboard.progress-notification.slow-delete" +msgstr "Eliminación inesperadamente lenta"