From c670aac3394165157646585fb95a9d3d0701cacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Thu, 20 Nov 2025 13:26:21 +0100 Subject: [PATCH] :tada: Added deleted files to dashboard --- CHANGES.md | 1 + .../get-team-deleted-files-empty.json | 1 + .../dashboard/get-team-deleted-files.json | 47 +++ frontend/playwright/ui/pages/DashboardPage.js | 18 + .../ui/specs/dashboard-deleted.spec.js | 31 ++ frontend/src/app/main/data/common.cljs | 18 + frontend/src/app/main/data/dashboard.cljs | 185 +++++++++- frontend/src/app/main/refs.cljs | 3 + frontend/src/app/main/repo.cljs | 3 + frontend/src/app/main/ui.cljs | 3 +- frontend/src/app/main/ui/dashboard.cljs | 10 + .../src/app/main/ui/dashboard/deleted.cljs | 327 ++++++++++++++++++ .../src/app/main/ui/dashboard/deleted.scss | 125 +++++++ .../src/app/main/ui/dashboard/file_menu.cljs | 168 +++++---- frontend/src/app/main/ui/dashboard/grid.cljs | 46 ++- frontend/src/app/main/ui/dashboard/grid.scss | 4 + .../src/app/main/ui/dashboard/projects.cljs | 18 +- .../src/app/main/ui/dashboard/projects.scss | 119 ++++--- .../src/app/main/ui/dashboard/sidebar.cljs | 6 +- frontend/src/app/main/ui/exports/assets.cljs | 65 +++- frontend/src/app/main/ui/routes.cljs | 3 +- frontend/src/app/main/ui/viewer/header.cljs | 4 +- .../app/main/ui/workspace/right_header.cljs | 4 +- frontend/translations/en.po | 108 ++++++ frontend/translations/es.po | 108 ++++++ 25 files changed, 1272 insertions(+), 153 deletions(-) create mode 100644 frontend/playwright/data/dashboard/get-team-deleted-files-empty.json create mode 100644 frontend/playwright/data/dashboard/get-team-deleted-files.json create mode 100644 frontend/playwright/ui/specs/dashboard-deleted.spec.js create mode 100644 frontend/src/app/main/ui/dashboard/deleted.cljs create mode 100644 frontend/src/app/main/ui/dashboard/deleted.scss diff --git a/CHANGES.md b/CHANGES.md index c0bea7e54c..a697530ab4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ - Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201) - Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474) +- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149) ### :bug: Bugs fixed diff --git a/frontend/playwright/data/dashboard/get-team-deleted-files-empty.json b/frontend/playwright/data/dashboard/get-team-deleted-files-empty.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-deleted-files-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-team-deleted-files.json b/frontend/playwright/data/dashboard/get-team-deleted-files.json new file mode 100644 index 0000000000..d3b45e8f1c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-deleted-files.json @@ -0,0 +1,47 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f41234", + "~:revn": 1, + "~:vern": 1, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1705307400000", + "~:modified-at": "~m1732111500000", + "~:deleted-at": "~m1732111500000", + "~:name": "Deleted Design File 1", + "~:is-shared": false, + "~:will-be-deleted-at": "~m1732716300000", + "~:thumbnail-id": null, + "~:row-num": 1, + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d" + }, + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f41235", + "~:revn": 2, + "~:vern": 2, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1704875700000", + "~:modified-at": "~m1732025400000", + "~:deleted-at": "~m1732025400000", + "~:name": "Deleted Design File 2", + "~:is-shared": true, + "~:will-be-deleted-at": "~m1732630200000", + "~:thumbnail-id": null, + "~:row-num": 2, + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d" + }, + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f41236", + "~:revn": 3, + "~:vern": 3, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c", + "~:created-at": "~m1706792400000", + "~:modified-at": "~m1731939600000", + "~:deleted-at": "~m1731939600000", + "~:name": "Old Project Design", + "~:is-shared": false, + "~:will-be-deleted-at": "~m1732544400000", + "~:thumbnail-id": null, + "~:row-num": 3, + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d" + } +] diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 5967d0e74e..111913e5a3 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -106,6 +106,13 @@ export class DashboardPage extends BaseWebSocketPage { ); } + async setupDeletedFiles() { + await this.mockRPC( + "get-team-deleted-files?team-id=*", + "dashboard/get-team-deleted-files.json", + ); + } + async setupDrafts() { await this.mockRPC( "get-project-files?project-id=*", @@ -160,6 +167,10 @@ export class DashboardPage extends BaseWebSocketPage { }); await this.mockRPC("search-files", "dashboard/search-files.json"); await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json"); + await this.mockRPC( + "get-team-deleted-files?team-id=*", + "dashboard/get-team-deleted-files.json", + ); } async setupAccessTokensEmpty() { @@ -289,6 +300,13 @@ export class DashboardPage extends BaseWebSocketPage { await expect(this.mainHeading).toHaveText("Libraries"); } + async goToDeleted() { + await this.page.goto( + `#/dashboard/deleted?team-id=${DashboardPage.anyTeamId}`, + ); + await expect(this.mainHeading).toHaveText("Projects"); + } + async openProfileMenu() { await this.userAccount.click(); } diff --git a/frontend/playwright/ui/specs/dashboard-deleted.spec.js b/frontend/playwright/ui/specs/dashboard-deleted.spec.js new file mode 100644 index 0000000000..6963eac0d5 --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard-deleted.spec.js @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); + await DashboardPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in-no-onboarding.json", + ); +}); + +test.describe("Dashboard Deleted Page", () => { + test("User can navigate to deleted page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + // Setup mock for deleted files API + await dashboardPage.setupDeletedFiles(); + + // Navigate directly to deleted page + await dashboardPage.goToDeleted(); + + // Check for the restore all and clear trash buttons + await expect( + page.getByRole("button", { name: "Restore All" }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Clear trash" }), + ).toBeVisible(); + }); +}); diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 101db147ff..e81daf87a2 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -386,3 +386,21 @@ (rx/of ::dps/force-persist (rt/nav :viewer params options)))))) +(defn go-to-dashboard-deleted + [& {:keys [team-id] :as options}] + (ptk/reify ::go-to-dashboard-deleted + ptk/WatchEvent + (watch [_ state _] + (let [profile (get state :profile) + team-id (cond + (= :default team-id) + (:default-team-id profile) + + (uuid? team-id) + team-id + + :else + (:current-team-id state)) + params {:team-id team-id}] + (rx/of (modal/hide) + (rt/nav :dashboard-deleted params options)))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 8937c3180d..4a8a17a94f 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -21,6 +21,7 @@ [app.main.data.modal :as modal] [app.main.data.websocket :as dws] [app.main.repo :as rp] + [app.main.store :as st] [app.util.i18n :as i18n :refer [tr]] [app.util.sse :as sse] [beicon.v2.core :as rx] @@ -76,7 +77,8 @@ ptk/UpdateEvent (update [_ state] (reduce (fn [state {:keys [id] :as project}] - (update-in state [:projects id] merge project)) + ;; Replace completely instead of merge to ensure deleted-at is removed + (assoc-in state [:projects id] project)) state projects)))) @@ -152,6 +154,34 @@ (->> (rp/cmd! :get-builtin-templates) (rx/map builtin-templates-fetched))))) +;; --- EVENT: deleted-files + +(defn- deleted-files-fetched + [files] + (ptk/reify ::deleted-files-fetched + ptk/UpdateEvent + (update [_ state] + (let [now (ct/now) + filtered-files (filterv (fn [file] + (let [will-be-deleted-at (:will-be-deleted-at file)] + (or (nil? will-be-deleted-at) + (ct/is-after? will-be-deleted-at now)))) + files) + files (d/index-by :id filtered-files)] + (-> state + (assoc :deleted-files files) + (update :files d/merge files)))))) + +(defn fetch-deleted-files + ([] (fetch-deleted-files nil)) + ([team-id] + (ptk/reify ::fetch-deleted-files + ptk/WatchEvent + (watch [_ state _] + (when-let [team-id (or team-id (:current-team-id state))] + (->> (rp/cmd! :get-team-deleted-files {:team-id team-id}) + (rx/map deleted-files-fetched))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Selection ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -656,3 +686,156 @@ :team-role-change (handle-change-team-role msg) :team-membership-change (dcm/team-membership-change msg) nil)) + + +;; --- Delete files immediately + +(defn delete-files-immediately + [{:keys [team-id ids] :as params}] + (assert (uuid? team-id)) + (assert (set? ids)) + (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)))))) + +;; --- 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- 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/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)] + + (rx/merge + (rx/of (initialize-restore-status files)) + + (->> (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))))) + + (rx/of (ptk/data-event ::restore-start {:total (count ids)}))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index e4473c1731..07f6254cab 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -636,3 +636,6 @@ (def persistence-state (l/derived (comp :status :persistence) st/state)) + +(def restore + (l/derived :restore st/state)) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index fe24df216d..0361c75944 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/restore-deleted-team-files + {:stream? true} + :export-binfile {:response-type :blob} :retrieve-list-of-builtin-templates {:query-params :all}}) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 44e7a24549..a247a982a8 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -224,7 +224,8 @@ :dashboard-members :dashboard-invitations :dashboard-webhooks - :dashboard-settings) + :dashboard-settings + :dashboard-deleted) (let [params (get params :query) team-id (some-> params :team-id uuid/parse*) project-id (some-> params :project-id uuid/parse*) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 23277606cd..58370e82bc 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -20,6 +20,7 @@ [app.main.router :as rt] [app.main.store :as st] [app.main.ui.context :as ctx] + [app.main.ui.dashboard.deleted :refer [deleted-section*]] [app.main.ui.dashboard.files :refer [files-section*]] [app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]] [app.main.ui.dashboard.import] @@ -29,6 +30,7 @@ [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] @@ -84,6 +86,9 @@ [:div {:class (stl/css :dashboard-content) :on-click clear-selected-fn :ref container} + + [:& progress-widget {:operation :restore}] + (case section :dashboard-recent (when (seq projects) @@ -140,6 +145,11 @@ :dashboard-settings [:> team-settings-page* {:team team :profile profile}] + :dashboard-deleted + [:> deleted-section* {:team team + :projects projects + :profile profile}] + nil)])) (def ref:dashboard-initialized diff --git a/frontend/src/app/main/ui/dashboard/deleted.cljs b/frontend/src/app/main/ui/dashboard/deleted.cljs new file mode 100644 index 0000000000..483f6e6752 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/deleted.cljs @@ -0,0 +1,327 @@ +;; 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.dashboard.deleted + (:require-macros [app.main.style :as stl]) + (:require + [app.common.geom.point :as gpt] + [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*]] + [app.main.ui.dashboard.grid :refer [grid*]] + [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] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [okulary.core :as l] + [rumext.v2 :as mf])) + + +(def ^:private menu-icon + (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) + +(mf/defc header* + {::mf/props :obj + ::mf/private true} + [] + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} + [:div#dashboard-deleted-title {:class (stl/css :dashboard-title)} + [:h1 (tr "dashboard.projects-title")]]]) + +(mf/defc deleted-project-menu* + [{:keys [project files team-id show on-close top left]}] + (let [top (or top 0) + left (or left 0) + + file-ids (into #{} (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))) + + 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}))) + 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}]] + + [:* + [:> context-menu* + {:on-close on-close + :show show + :fixed (or (not= top 0) (not= left 0)) + :min-width true + :top top + :left left + :options options}]])) + +(mf/defc deleted-project-item* + {::mf/props :obj + ::mf/private true} + [{:keys [project team files]}] + (let [project-files (filterv #(= (:project-id %) (:id project)) files) + + empty? (empty? project-files) + selected-files (mf/deref refs/selected-files) + + dstate (mf/deref refs/dashboard-local) + edit-id (:project-for-edit dstate) + + local (mf/use-state {:menu-open false + :menu-pos nil + :edition (= (:id project) edit-id)}) + + [rowref limit] (hooks/use-dynamic-grid-item-width) + + on-menu-click + (mf/use-fn + (fn [event] + (dom/prevent-default event) + + (let [client-position (dom/get-client-position event) + position (if (and (nil? (:y client-position)) (nil? (:x client-position))) + (let [target-element (dom/get-target event) + points (dom/get-bounding-rect target-element) + y (:top points) + x (:left points)] + (gpt/point x y)) + client-position)] + (swap! local assoc + :menu-open true + :menu-pos position)))) + + on-menu-close + (mf/use-fn #(swap! local assoc :menu-open false)) + + handle-menu-click + (mf/use-callback + (mf/deps on-menu-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-menu-click event))))] + [:article {:class (stl/css-case :dashboard-project-row true)} + [:header {:class (stl/css :project)} + [:div {:class (stl/css :project-name-wrapper)} + [:h2 {:class (stl/css :project-name) + :title (:name project)} + (:name project)] + + (when (:deleted-at project) + [:div {:class (stl/css :info-wrapper)} + [:div {:class (stl/css-case :project-actions true)} + + [:button {:class (stl/css :options-btn) + :on-click on-menu-click + :title (tr "dashboard.options") + :aria-label (tr "dashboard.options") + :data-testid "project-options" + :on-key-down handle-menu-click} + menu-icon]] + + [:> 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)) + :on-close on-menu-close}]])]] + + [:div {:class (stl/css :grid-container) :ref rowref} + (if ^boolean empty? + [:> empty-placeholder* {:title (tr "dashboard.empty-placeholder-files-title") + :class (stl/css :placeholder-placement) + :type 1 + :subtitle (tr "dashboard.empty-placeholder-files-subtitle")}] + + [:> grid* + {:project project + :files project-files + :origin :deleted + :can-edit false + :can-restore true + :limit limit + :selected-files selected-files}])]])) + +(def ^:private ref:deleted-files + (l/derived :deleted-files st/state)) + +(mf/defc deleted-section* + {::mf/props :obj} + [{:keys [team projects]}] + (let [deleted-map + (mf/deref ref:deleted-files) + + projects + (mf/with-memo [projects deleted-map] + (->> projects + (filter (fn [project] + (or (:deleted-at project) + (when deleted-map + (some #(= (:id project) (:project-id %)) + (vals deleted-map)))))) + (filter (fn [project] + (when deleted-map + (some #(= (:id project) (:project-id %)) + (vals deleted-map))))) + (sort-by :modified-at) + (reverse))) + + team-id + (get team :id) + + ;; Calculate deletion days based on team subscription + deletion-days + (let [subscription (get team :subscription) + sub-type (get subscription :type) + sub-status (get subscription :status) + canceled? (contains? #{"canceled" "unpaid"} sub-status)] + (cond + (and (= "unlimited" sub-type) (not canceled?)) 30 + (and (= "enterprise" sub-type) (not canceled?)) 90 + :else 7)) + + on-clear + (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")))})))) + + 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)}))))))) + + 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) + (dd/fetch-deleted-files team-id) + (dd/clear-selected-files))) + + [:* + [:> header* {:team team}] + [:section {:class (stl/css :dashboard-container :no-bg)} + [:* + [: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")]] + + [: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-options)} + [:> button* {:variant "ghost" + :type "button" + :on-click on-restore-all} + (tr "dashboard.deleted.restore-all")] + [:> button* {:variant "destructive" + :type "button" + :icon "delete" + :on-click on-clear} + (tr "dashboard.deleted.clear")]]] + + (when (seq projects) + (for [{:keys [id] :as project} projects] + (let [files (when deleted-map + (->> (vals deleted-map) + (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 new file mode 100644 index 0000000000..732a1e704b --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -0,0 +1,125 @@ +// 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 "common/refactor/common-dashboard"; +@use "../ds/typography.scss" as t; +@use "../ds/_borders.scss" as *; +@use "../ds/spacing.scss" as *; +@use "../ds/_sizes.scss" as *; +@use "../ds/z-index.scss" as *; + +.dashboard-container { + flex: 1 0 0; + width: 100%; + margin-inline-end: var(--sp-l); + border-top: $b-1 solid var(--panel-border-color); + overflow-y: auto; + padding-block-end: var(--sp-xxxl); +} + +.deleted-content { + display: flex; + gap: var(--sp-l); + justify-content: space-between; + margin-inline-start: var(--sp-l); + margin-block-start: var(--sp-xxl); +} + +.deleted-info { + @include t.use-typography("body-medium"); + color: var(--color-foreground-secondary); +} + +.info-text-highlight { + color: var(--color-accent-primary); +} + +.deleted-options { + display: flex; + gap: 5px; + flex-shrink: 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); +} + +.project { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--sp-s); + width: 99%; + max-height: $sz-40; + padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l); + margin-block-start: var(--sp-l); + border-radius: $br-4; +} + +.project-name-wrapper { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + min-height: var(--sp-xxxl); + margin-inline-start: var(--sp-s); +} + +.project-name { + @include t.use-typography("body-large"); + width: fit-content; + margin-inline-end: var(--sp-m); + line-height: 0.8; + color: var(--title-foreground-color-hover); + height: var(--sp-l); +} + +.project-actions { + display: flex; + opacity: var(--actions-opacity); + margin-inline-start: var(--sp-xxxl); +} + +.add-file-btn, +.options-btn { + @extend .button-tertiary; + height: var(--sp-xxxl); + width: var(--sp-xxxl); + margin: 0 var(--sp-s); + padding: var(--sp-s); +} + +.info-wrapper { + display: flex; + align-items: center; + gap: var(--sp-s); +} + +.add-icon, +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 4006caa690..ad08af02a6 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -55,7 +55,7 @@ projects)) (mf/defc file-menu* - [{:keys [files on-edit on-close top left navigate origin parent-id can-edit]}] + [{:keys [files on-edit on-close top left navigate origin parent-id can-edit can-restore]}] (assert (seq files) "missing `files` prop") (assert (fn? on-edit) "missing `on-edit` prop") @@ -187,7 +187,46 @@ on-export-binary-files (fn [] (st/emit! (-> (fexp/open-export-dialog files) - (with-meta {::ev/origin "dashboard"}))))] + (with-meta {::ev/origin "dashboard"})))) + + restore-fn + (fn [_] + (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))) + (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-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)) + :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})))] (mf/with-effect [] (->> (rp/cmd! :get-all-projects) @@ -227,76 +266,85 @@ (:id sub-project))})})}])) options - (if multi? - [(when can-edit - {:name (tr "dashboard.duplicate-multi" file-count) - :id "duplicate-multi" - :handler on-duplicate}) + (if can-restore + [(when can-restore + {:name (tr "dashboard.restore-file") + :id "restore-file" + :handler on-restore-immediately}) + (when can-restore + {:name (tr "dashboard.delete-file") + :id "delete-file" + :handler on-delete-immediately})] + (if multi? + [(when can-edit + {:name (tr "dashboard.duplicate-multi" file-count) + :id "duplicate-multi" + :handler on-duplicate}) - (when (and (or (seq current-projects) (seq other-teams)) can-edit) - {:name (tr "dashboard.move-to-multi" file-count) - :id "file-move-multi" - :options sub-options}) + (when (and (or (seq current-projects) (seq other-teams)) can-edit) + {:name (tr "dashboard.move-to-multi" file-count) + :id "file-move-multi" + :options sub-options}) - {:name (tr "dashboard.export-binary-multi" file-count) - :id "file-binary-export-multi" - :handler on-export-binary-files} + {:name (tr "dashboard.export-binary-multi" file-count) + :id "file-binary-export-multi" + :handler on-export-binary-files} - (when (and (:is-shared file) can-edit) - {:name (tr "labels.unpublish-multi-files" file-count) - :id "file-unpublish-multi" - :handler on-del-shared}) + (when (and (:is-shared file) can-edit) + {:name (tr "labels.unpublish-multi-files" file-count) + :id "file-unpublish-multi" + :handler on-del-shared}) - (when (and (not is-lib-page?) can-edit) - {:name :separator} - {:name (tr "labels.delete-multi-files" file-count) - :id "file-delete-multi" - :handler on-delete})] + (when (and (not is-lib-page?) can-edit) + {:name :separator} + {:name (tr "labels.delete-multi-files" file-count) + :id "file-delete-multi" + :handler on-delete})] - [{:name (tr "dashboard.open-in-new-tab") - :id "file-open-new-tab" - :handler on-new-tab} - (when (and (not is-search-page?) can-edit) - {:name (tr "labels.rename") - :id "file-rename" - :handler on-edit}) + [{:name (tr "dashboard.open-in-new-tab") + :id "file-open-new-tab" + :handler on-new-tab} + (when (and (not is-search-page?) can-edit) + {:name (tr "labels.rename") + :id "file-rename" + :handler on-edit}) - (when (and (not is-search-page?) can-edit) - {:name (tr "dashboard.duplicate") - :id "file-duplicate" - :handler on-duplicate}) + (when (and (not is-search-page?) can-edit) + {:name (tr "dashboard.duplicate") + :id "file-duplicate" + :handler on-duplicate}) - (when (and (not is-lib-page?) - (not is-search-page?) - (or (seq current-projects) (seq other-teams)) - can-edit) - {:name (tr "dashboard.move-to") - :id "file-move-to" - :options sub-options}) + (when (and (not is-lib-page?) + (not is-search-page?) + (or (seq current-projects) (seq other-teams)) + can-edit) + {:name (tr "dashboard.move-to") + :id "file-move-to" + :options sub-options}) - (when (and (not is-search-page?) - can-edit) - (if (:is-shared file) - {:name (tr "dashboard.unpublish-shared") - :id "file-del-shared" - :handler on-del-shared} - {:name (tr "dashboard.add-shared") - :id "file-add-shared" - :handler on-add-shared})) + (when (and (not is-search-page?) + can-edit) + (if (:is-shared file) + {:name (tr "dashboard.unpublish-shared") + :id "file-del-shared" + :handler on-del-shared} + {:name (tr "dashboard.add-shared") + :id "file-add-shared" + :handler on-add-shared})) - {:name :separator} + {:name :separator} - {:name (tr "dashboard.download-binary-file") - :id "download-binary-file" - :handler on-export-binary-files} + {:name (tr "dashboard.download-binary-file") + :id "download-binary-file" + :handler on-export-binary-files} - (when (and (not is-lib-page?) (not is-search-page?) can-edit) - {:name :separator}) + (when (and (not is-lib-page?) (not is-search-page?) can-edit) + {:name :separator}) - (when (and (not is-lib-page?) (not is-search-page?) can-edit) - {:name (tr "labels.delete") - :id "file-delete" - :handler on-delete})])] + (when (and (not is-lib-page?) (not is-search-page?) can-edit) + {:name (tr "labels.delete") + :id "file-delete" + :handler on-delete})]))] [:> context-menu* {:on-close on-close diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index e6cc837b52..c87e5ef699 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -86,7 +86,7 @@ (mf/defc grid-item-thumbnail* {::mf/props :obj ::mf/private true} - [{:keys [can-edit file]}] + [{:keys [can-edit file can-restore]}] (let [file-id (get file :id) revn (get file :revn) thumbnail-id (get file :thumbnail-id) @@ -109,7 +109,8 @@ :message (ex-message cause)))))] (partial rx/dispose! subscription)))) - [:div {:class (stl/css :grid-item-th) + [:div {:class (stl/css-case :grid-item-th true + :deleted-item can-restore) :style {:background-color bg-color} :ref container} (when visible? @@ -131,13 +132,15 @@ (mf/defc grid-item-library* {::mf/props :obj} - [{:keys [file]}] + [{:keys [file can-restore]}] (mf/with-effect [file] (when file (let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))] (run! fonts/ensure-loaded! font-ids)))) - [:div {:class (stl/css :grid-item-th :library)} + [:div {:class (stl/css-case :grid-item-th true + :library true + :deleted-item can-restore)} (if (nil? file) [:> loader* {:class (stl/css :grid-loader) :overlay true @@ -250,7 +253,7 @@ counter-el)) (mf/defc grid-item* - [{:keys [file origin can-edit selected-files]}] + [{:keys [file origin can-edit selected-files can-restore]}] (let [file-id (get file :id) state (mf/deref refs/dashboard-local) @@ -289,12 +292,13 @@ on-navigate (mf/use-fn - (mf/deps file-id) + (mf/deps file-id can-restore) (fn [event] - (let [menu-icon (mf/ref-val menu-ref) - target (dom/get-target event)] - (when-not (dom/child? target menu-icon) - (st/emit! (dcm/go-to-workspace :file-id file-id)))))) + (when-not can-restore + (let [menu-icon (mf/ref-val menu-ref) + target (dom/get-target event)] + (when-not (dom/child? target menu-icon) + (st/emit! (dcm/go-to-workspace :file-id file-id))))))) on-drag-start (mf/use-fn @@ -412,8 +416,8 @@ [:div {:class (stl/css :overlay)}] (if ^boolean is-library-view? - [:> grid-item-library* {:file file}] - [:> grid-item-thumbnail* {:file file :can-edit can-edit}]) + [:> grid-item-library* {:file file :can-restore can-restore}] + [:> grid-item-thumbnail* {:file file :can-edit can-edit :can-restore can-restore}]) (when (and (:is-shared file) (not is-library-view?)) [:div {:class (stl/css :item-badge)} deprecated-icon/library]) @@ -451,11 +455,12 @@ :on-edit on-edit :on-close on-menu-close :origin origin - :parent-id (dm/str file-id "-action-menu")}]])]]]]])) + :parent-id (dm/str file-id "-action-menu") + :can-restore can-restore}]])]]]]])) (mf/defc grid* {::mf/props :obj} - [{:keys [files project origin limit create-fn can-edit selected-files]}] + [{:keys [files project origin limit create-fn can-edit selected-files can-restore]}] (let [dragging? (mf/use-state false) project-id (get project :id) team-id (get project :team-id) @@ -535,7 +540,8 @@ :key (dm/str (:id item)) :origin origin :selected-files selected-files - :can-edit can-edit}])]) + :can-edit can-edit + :can-restore can-restore}])]) :else [:> empty-grid-placeholder* @@ -548,7 +554,7 @@ :on-finish-import on-finish-import}])])) (mf/defc line-grid-row - [{:keys [files selected-files dragging? limit can-edit] :as props}] + [{:keys [files selected-files dragging? limit can-edit can-restore] :as props}] (let [elements limit limit (if dragging? (dec limit) limit)] [:ul {:class (stl/css :grid-row :no-wrap) @@ -563,10 +569,11 @@ :file item :selected-files selected-files :can-edit can-edit - :key (dm/str (:id item))}])])) + :key (dm/str (:id item)) + :can-restore can-restore}])])) (mf/defc line-grid - [{:keys [project team files limit create-fn can-edit] :as props}] + [{:keys [project team files limit create-fn can-edit can-restore] :as props}] (let [dragging? (mf/use-state false) project-id (:id project) team-id (:id team) @@ -664,7 +671,8 @@ :selected-files selected-files :dragging? @dragging? :can-edit can-edit - :limit limit}] + :limit limit + :can-restore can-restore}] :else [:> empty-grid-placeholder* diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index ae9807214d..3f4189c729 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -375,3 +375,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width .grid-loader { --icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); } + +.deleted-item { + opacity: 0.5; +} diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 472ed76ff8..d49e9b31d9 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -21,6 +21,7 @@ [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] @@ -342,7 +343,13 @@ (fn [] (reset! show-team-hero* false) (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?] (swap! storage/global assoc ::show-team-hero show-team-hero?)) @@ -376,6 +383,15 @@ (not is-defalt-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")]] (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 b72a793890..96462ef401 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -4,16 +4,21 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; +@use "common/refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; +@use "../ds/typography.scss" as t; +@use "../ds/_borders.scss" as *; +@use "../ds/spacing.scss" as *; +@use "../ds/_sizes.scss" as *; +@use "../ds/z-index.scss" as *; .dashboard-container { flex: 1 0 0; width: 100%; - margin-right: deprecated.$s-16; - border-top: deprecated.$s-1 solid var(--panel-border-color); + margin-inline-end: var(--sp-l); + border-top: $b-1 solid var(--panel-border-color); overflow-y: auto; - padding-bottom: deprecated.$s-32; + padding-bottom: var(--sp-xxxl); } .dashboard-projects { @@ -27,16 +32,16 @@ .dashboard-shared { width: calc(100vw - deprecated.$s-320); - margin-right: deprecated.$s-52; + margin-inline-end: deprecated.$s-52; } .search { - margin-top: deprecated.$s-12; + margin-block-start: var(--sp-m); } .dashboard-project-row { --actions-opacity: 0; - margin-bottom: deprecated.$s-24; + margin-block-end: var(--sp-xxl); position: relative; &:hover, @@ -60,12 +65,12 @@ flex-direction: row; align-items: center; justify-content: space-between; - gap: deprecated.$s-8; + gap: var(--sp-s); width: 99%; - max-height: deprecated.$s-40; - padding: deprecated.$s-8 deprecated.$s-8 deprecated.$s-8 deprecated.$s-16; - margin-top: deprecated.$s-16; - border-radius: deprecated.$br-4; + max-height: $sz-40; + padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l); + margin-block-start: var(--sp-l); + border-radius: $br-4; } .project-name-wrapper { @@ -73,30 +78,29 @@ align-items: center; justify-content: flex-start; width: 100%; - min-height: deprecated.$s-32; - margin-left: deprecated.$s-8; + min-height: var(--sp-xxxl); + margin-inline-start: var(--sp-s); } .project-name { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include t.use-typography("body-large"); width: fit-content; - margin-right: deprecated.$s-12; + margin-inline-end: var(--sp-m); line-height: 0.8; color: var(--title-foreground-color-hover); cursor: pointer; - height: deprecated.$s-16; + height: var(--sp-l); } .info-wrapper { display: flex; align-items: center; - gap: deprecated.$s-8; + gap: var(--sp-s); } .info, .recent-files-row-title-info { - @include deprecated.bodyMediumTypography; + @include t.use-typography("body-medium"); color: var(--title-foreground-color); @media (max-width: 760px) { display: none; @@ -106,16 +110,16 @@ .project-actions { display: flex; opacity: var(--actions-opacity); - margin-left: deprecated.$s-32; + margin-inline-start: var(--sp-xxxl); } .add-file-btn, .options-btn { @extend .button-tertiary; - height: deprecated.$s-32; - width: deprecated.$s-32; - margin: 0 deprecated.$s-8; - padding: deprecated.$s-8; + height: var(--sp-xxxl); + width: var(--sp-xxxl); + margin: 0 var(--sp-s); + padding: var(--sp-s); } .add-icon, @@ -126,24 +130,24 @@ .grid-container { width: 100%; - padding: 0 deprecated.$s-4; + padding: 0 var(--sp-xs); } .placeholder-placement { - margin: deprecated.$s-16 deprecated.$s-32; + margin: var(--sp-l) var(--sp-xxxl); } .show-more { --show-more-color: var(--button-secondary-foreground-color-rest); @include deprecated.buttonStyle; - @include deprecated.bodyMediumTypography; + @include t.use-typography("body-medium"); position: absolute; - top: deprecated.$s-8; + top: var(--sp-s); right: deprecated.$s-52; display: flex; align-items: center; justify-content: space-between; - column-gap: deprecated.$s-12; + column-gap: var(--sp-m); color: var(--show-more-color); &:hover { @@ -152,8 +156,8 @@ } .show-more-icon { - height: deprecated.$s-16; - width: deprecated.$s-16; + height: var(--sp-l); + width: var(--sp-l); fill: none; stroke: var(--show-more-color); } @@ -164,13 +168,13 @@ border-radius: deprecated.$br-8; border: none; display: flex; - margin: deprecated.$s-16; - padding: deprecated.$s-8; + margin: var(--sp-l); + padding: var(--sp-s); position: relative; img { - border-radius: deprecated.$br-4; - height: deprecated.$s-200; + border-radius: $br-4; + height: var(--sp-xl) 0; width: auto; @media (max-width: 1200px) { @@ -185,18 +189,18 @@ flex-direction: column; align-items: flex-start; flex-grow: 1; - padding: deprecated.$s-20 deprecated.$s-20; + padding: var(--sp-xl) var(--sp-xl); } .title { - font-size: deprecated.$fs-24; + font-size: $sz-24; color: var(--color-foreground-primary); font-weight: deprecated.$fw400; } .info { flex: 1; - font-size: deprecated.$fs-16; + font-size: $sz-16; span { color: var(--color-foreground-secondary); display: block; @@ -204,15 +208,15 @@ a { color: var(--color-accent-primary); } - padding: deprecated.$s-8 0; + padding: var(--sp-s) 0; } .close { --close-icon-foreground-color: var(--icon-foreground); position: absolute; - top: deprecated.$s-20; - right: deprecated.$s-24; - width: deprecated.$s-24; + top: var(--sp-xl); + right: var(--sp-xxl); + width: var(--sp-xxl); background-color: transparent; border: none; cursor: pointer; @@ -227,7 +231,7 @@ } .invite { - height: deprecated.$s-32; + height: var(--sp-xxxl); width: deprecated.$s-180; } @@ -235,8 +239,8 @@ display: flex; align-items: center; justify-content: center; - width: deprecated.$s-200; - height: deprecated.$s-200; + width: var(--sp-xl) 0; + height: var(--sp-xl) 0; overflow: hidden; border-radius: deprecated.$br-4; @media (max-width: 1200px) { @@ -244,3 +248,26 @@ 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/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 8e7ccae588..d9460c6f3f 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -27,11 +27,11 @@ [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] - [app.main.ui.dashboard.subscription :refer [subscription-sidebar* + [app.main.ui.dashboard.subscription :refer [dashboard-cta* + get-subscription-type menu-team-icon* - dashboard-cta* show-subscription-dashboard-banner? - get-subscription-type]] + subscription-sidebar*]] [app.main.ui.dashboard.team-form] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs index cf97f6e498..5531d2f9df 100644 --- a/frontend/src/app/main/ui/exports/assets.cljs +++ b/frontend/src/app/main/ui/exports/assets.cljs @@ -12,6 +12,7 @@ [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] @@ -205,10 +206,13 @@ :cmd :export-frames :origin origin}])) -(mf/defc export-progress-widget +(mf/defc progress-widget {::mf/wrap [mf/memo]} - [] - (let [state (mf/deref refs/export) + [{:keys [operation] :or {operation :export}}] + (let [state (mf/deref (case operation + :export refs/export + :restore refs/restore + refs/export)) profile (mf/deref refs/profile) theme (or (:theme profile) theme/default) is-default-theme? (= theme/default theme) @@ -217,11 +221,14 @@ detail-visible? (:detail-visible state) widget-visible? (:widget-visible state) progress (:progress state) - exports (:exports state) - total (count exports) + items (case operation + :export (:exports state) + :restore (:files state) + []) + total (or (:total state) (count items)) complete? (= progress total) circ (* 2 Math/PI 12) - pct (- circ (* circ (/ progress total))) + pct (if (zero? total) circ (- circ (* circ (/ progress total)))) pwidth (if error? @@ -243,19 +250,43 @@ title (cond - 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")) + 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"))) - retry-last-export - (mf/use-fn #(st/emit! (de/retry-last-export))) + 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))) toggle-detail-visibility - (mf/use-fn #(st/emit! (de/toggle-detail-visibililty)))] + (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)))] [:* - (when widget-visible? + (when (and widget-visible? (= operation :export)) [:div {:class (stl/css :export-progress-widget) :on-click toggle-detail-visibility} [:svg {:width "24" :height "24"} @@ -283,11 +314,11 @@ error-icon neutral-icon) - [:p {:class (stl/css :export-progress-title)} - title + [:div {:class (stl/css :export-progress-title)} + [:div {:class (stl/css :title-text)} title] (if error? [:button {:class (stl/css :retry-btn) - :on-click retry-last-export} + :on-click retry-last-operation} (tr "workspace.options.retry")] [:span {:class (stl/css :progress)} diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 8e3736f69f..e8159d3852 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -64,7 +64,8 @@ ["/fonts" :dashboard-fonts] ["/fonts/providers" :dashboard-font-providers] ["/libraries" :dashboard-libraries] - ["/files" :dashboard-files]] + ["/files" :dashboard-files] + ["/deleted" :dashboard-deleted]] ["/dashboard/team/:team-id" ["/members" :dashboard-legacy-team-members] diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 292db767c1..20a25deda7 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -14,7 +14,7 @@ [app.main.data.viewer.shortcuts :as sc] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.exports.assets :refer [export-progress-widget]] + [app.main.ui.exports.assets :refer [progress-widget]] [app.main.ui.formats :as fmt] [app.main.ui.icons :as deprecated-icon] [app.main.ui.viewer.comments :refer [comments-menu]] @@ -167,7 +167,7 @@ (open-share-dialog))) [:div {:class (stl/css :options-zone)} - [:& export-progress-widget] + [:& progress-widget {:operation :export}] (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 67391535fa..277a470ba2 100644 --- a/frontend/src/app/main/ui/workspace/right_header.cljs +++ b/frontend/src/app/main/ui/workspace/right_header.cljs @@ -22,7 +22,7 @@ [app.main.ui.dashboard.team] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.exports.assets :refer [export-progress-widget]] + [app.main.ui.exports.assets :refer [progress-widget]] [app.main.ui.formats :as fmt] [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.presence :refer [active-sessions]] @@ -200,7 +200,7 @@ [:div {:class (stl/css :users-section)} [:& active-sessions]] - [:& export-progress-widget] + [:& progress-widget {:operation :export}] [:div {:class (stl/css :separator)}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6901029051..0fd7924a87 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8421,3 +8421,111 @@ msgstr "Autosaved versions will be kept for %s days." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" + +msgid "dashboard.labels.recent" +msgstr "Recent" + +msgid "dashboard.labels.deleted" +msgstr "Deleted" + +msgid "dashboard.deleted.restore-all" +msgstr "Restore All" + +msgid "dashboard.deleted.clear" +msgstr "Clear trash" + +msgid "dashboard.restore-file" +msgstr "Restore file" + +msgid "dashboard.delete-file" +msgstr "Delete file" + +msgid "dashboard.deleted.restore-project" +msgstr "Restore project" + +msgid "dashboard.deleted.delete-project" +msgstr "Delete project" + +msgid "dashboard.deleted.info-text" +msgstr "Deleted files will remain in the trash for" + +msgid "dashboard.deleted.info-days" +msgstr " %s days. " + +msgid "dashboard.deleted.info-text2" +msgstr "After that, they will be permanently deleted." + +msgid "dashboard.deleted.restore-text" +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" +msgstr "Restore all projects and files" + +msgid "restore-modal.restore-all.description" +msgstr "You're going to restore all your projects and files. This may take a while." + +msgid "restore-modal.restore-file.title" +msgstr "Restore file" + +msgid "restore-modal.restore-file.description" +msgstr "You're going to restore %s." + +msgid "restore-modal.restore-project.title" +msgstr "Restore Project" + +msgid "restore-modal.restore-project.description" +msgstr "You're going to restore %s project and all the files contained in it." + +msgid "delete-forever-modal.title" +msgstr "Delete forever" + +msgid "delete-forever-modal.delete-all.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" +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 "restore-modal.success-restore-immediately" +msgstr "%s has been successfully restored." + +msgid "delete-forever-modal.success-delete-immediately" +msgstr "%s has been successfully deleted." + +msgid "restore-modal.error-restore-files" +msgstr "There was an error while restoring the files." + +msgid "restore-modal.error-restore-file" +msgstr "There was an error while restoring the file %s." + +msgid "restore-modal.error-restore-project" +msgstr "There was an error while restoring the project %s and its files." + +msgid "restore-modal.normal-progress-label" +msgstr "Restoring files…" + +msgid "restore-modal.failed-progress-label" +msgstr "Restore failed" + +msgid "restore-modal.slow-progress-label" +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 diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 963c55543c..580b18ac60 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8277,3 +8277,111 @@ msgstr "Los autoguardados duran %s días." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + +msgid "dashboard.labels.recent" +msgstr "Recientes" + +msgid "dashboard.labels.deleted" +msgstr "Eliminados" + +msgid "dashboard.deleted.restore-all" +msgstr "Restaurar todo" + +msgid "dashboard.deleted.clear" +msgstr "Vaciar papelera" + +msgid "dashboard.restore-file" +msgstr "Restaurar archivo" + +msgid "dashboard.delete-file" +msgstr "Eliminar archivo" + +msgid "dashboard.deleted.restore-project" +msgstr "Restaurar proyecto" + +msgid "dashboard.deleted.delete-project" +msgstr "Eliminar proyecto" + +msgid "dashboard.deleted.info-text" +msgstr "Los archivos eliminados permanecerán en la papelera durante" + +msgid "dashboard.deleted.info-days" +msgstr " %s días. " + +msgid "dashboard.deleted.info-text2" +msgstr "Después de eso, serán eliminados permanentemente." + +msgid "dashboard.deleted.restore-text" +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" +msgstr "Restaurar todos los proyectos y archivos" + +msgid "restore-modal.restore-all.description" +msgstr "Vas a restaurar todos tus proyectos y archivos. Esto puede tardar un poco." + +msgid "restore-modal.restore-file.title" +msgstr "Restaurar archivo" + +msgid "restore-modal.restore-file.description" +msgstr "Vas a restaurar %s." + +msgid "restore-modal.restore-project.title" +msgstr "Restaurar proyecto" + +msgid "restore-modal.restore-project.description" +msgstr "Vas a restaurar el proyecto %s y todos los archivos que contiene." + +msgid "delete-forever-modal.title" +msgstr "Eliminar para siempre" + +msgid "delete-forever-modal.delete-all.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" +msgstr "¿Estás seguro de que quieres eliminar para siempre %s? Esta es una acción irreversible." + +msgid "delete-forever-modal.delete-project.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" +msgstr "%s ha sido restaurado correctamente." + +msgid "delete-forever-modal.success-delete-immediately" +msgstr "%s ha sido eliminado correctamente." + +msgid "restore-modal.error-restore-files" +msgstr "Hubo un error al restaurar los archivos." + +msgid "restore-modal.error-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 "restore-modal.normal-progress-label" +msgstr "Restaurando archivos…" + +msgid "restore-modal.failed-progress-label" +msgstr "Falló la restauración" + +msgid "restore-modal.slow-progress-label" +msgstr "Restauración 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