mirror of
https://github.com/penpot/penpot.git
synced 2025-12-23 14:39:00 -05:00
🎉 Added deleted files to dashboard
This commit is contained in:
committed by
Andrey Antukh
parent
1d3fb5434f
commit
c670aac339
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
31
frontend/playwright/ui/specs/dashboard-deleted.spec.js
Normal file
31
frontend/playwright/ui/specs/dashboard-deleted.spec.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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))))))
|
||||
|
||||
@@ -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)})))))))
|
||||
|
||||
@@ -636,3 +636,6 @@
|
||||
|
||||
(def persistence-state
|
||||
(l/derived (comp :status :persistence) st/state))
|
||||
|
||||
(def restore
|
||||
(l/derived :restore st/state))
|
||||
|
||||
@@ -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}})
|
||||
|
||||
|
||||
@@ -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*)
|
||||
|
||||
@@ -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
|
||||
|
||||
327
frontend/src/app/main/ui/dashboard/deleted.cljs
Normal file
327
frontend/src/app/main/ui/dashboard/deleted.cljs
Normal file
@@ -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}])))]]]]))
|
||||
125
frontend/src/app/main/ui/dashboard/deleted.scss
Normal file
125
frontend/src/app/main/ui/dashboard/deleted.scss
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 [:*
|
||||
|
||||
@@ -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)}]
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user