mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
* ✨ Add read-only preview mode for saved versions (#7622) * 🔧 Address review feedback on version preview (#7622) * 🐛 Fix version preview for WASM renderer (#7622) * 🐛 Fix stylelint color-named and color-function-notation in preview banner (#7622) * 🐛 Fix invalid-arity call to initialize-workspace in exit-preview (#7622) * 🐛 Fix unclosed defn paren in exit-preview (#7622) * ♻️ Refactor version preview/restore flow Separate enter-preview and enter-restore flows with dedicated dialogs instead of a persistent banner. Removes preview-banner component in favor of inline actions dialog. Uses backup/restore pattern for exit-preview instead of full workspace reinitialization. Adds analytics events for preview/restore actions. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ⚡ Extract on-name-input-focus as namespace-level private function The callback had no dependencies on component-local state or props, making it a pure function that can be hoisted to a defn-. This avoids recreating the same callback identity on every render of version-entry*. * ⚡ Extract extract-id-from-event helper to deduplicate snapshot callbacks Three callbacks in snapshot-entry* shared the same DOM extraction logic (get current target, read data-id, parse UUID). Extracted into a private defn- to remove the duplication and simplify each callback. * ⚡ Extract pure state-update callbacks from versions-toolbox* to namespace level Eight callbacks that only emit fixed Potok events with no meaningful deps were hoisted out of the component as defn- functions: - on-create-version - on-edit-version - on-cancel-version-edition - on-rename-version - on-delete-version - on-pin-version - on-lock-version - on-unlock-version These no longer need mf/use-fn wrappers since namespace-level functions have stable identity across renders, avoiding unnecessary callback recreation on each render cycle. * ✨ Rename filter parameter to filter-value in on-change-filter to avoid core shadowing The parameter name 'filter' shadowed clojure.core/filter within the function scope. Renamed to 'filter-value' for clarity and to prevent potential bugs if core/filter were needed in future changes. * 🔧 Fix linter warnings and errors across version-related namespaces frontend/src/app/main/ui/workspace.cljs: - Remove unused requires: app.common.data, app.main.data.notifications, app.main.data.workspace.versions frontend/src/app/main/data/workspace/versions.cljs: - Remove unused require: app.common.uuid - Fix duplicate reify type: enter-restore used ::restore-version (same as the private restore-version fn), renamed to ::enter-restore - Remove unused bindings: state in enter-restore, team-id in exit-preview and restore-version-from-plugin --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> Signed-off-by: wdeveloper16 <wdeveloer16@protonmail.com> Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
7c1a29ccf7
commit
e280168de9
@ -112,8 +112,9 @@
|
|||||||
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
||||||
END"))
|
END"))
|
||||||
|
|
||||||
(defn- get-snapshot
|
(defn get-snapshot-data
|
||||||
"Get snapshot with decoded data"
|
"Get a fully decoded snapshot for read-only preview or restoration.
|
||||||
|
Returns the snapshot map with decoded :data field."
|
||||||
[cfg file-id snapshot-id]
|
[cfg file-id snapshot-id]
|
||||||
(let [now (ct/now)]
|
(let [now (ct/now)]
|
||||||
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
||||||
@ -326,7 +327,7 @@
|
|||||||
(sto/resolve cfg {::db/reuse-conn true})
|
(sto/resolve cfg {::db/reuse-conn true})
|
||||||
|
|
||||||
snapshot
|
snapshot
|
||||||
(get-snapshot cfg file-id snapshot-id)]
|
(get-snapshot-data cfg file-id snapshot-id)]
|
||||||
|
|
||||||
(when-not snapshot
|
(when-not snapshot
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.binfile.common :as bfc]
|
[app.binfile.common :as bfc]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.features :as-alias cfeat]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
@ -35,6 +36,43 @@
|
|||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
(fsnap/get-visible-snapshots conn file-id))))
|
(fsnap/get-visible-snapshots conn file-id))))
|
||||||
|
|
||||||
|
;; --- COMMAND QUERY: get-file-snapshot
|
||||||
|
|
||||||
|
(def ^:private schema:get-file-snapshot
|
||||||
|
[:map {:title "get-file-snapshot"}
|
||||||
|
[:file-id ::sm/uuid]
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:features {:optional true} ::cfeat/features]])
|
||||||
|
|
||||||
|
(sv/defmethod ::get-file-snapshot
|
||||||
|
"Retrieve a file bundle with data from a specific snapshot for
|
||||||
|
read-only preview. Does not modify any database state."
|
||||||
|
{::doc/added "2.16"
|
||||||
|
::sm/params schema:get-file-snapshot
|
||||||
|
::sm/result files/schema:file-with-permissions
|
||||||
|
::db/transaction true}
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
||||||
|
(let [perms (bfc/get-file-permissions conn profile-id file-id)]
|
||||||
|
(files/check-read-permissions! perms)
|
||||||
|
(let [snapshot (fsnap/get-snapshot-data cfg file-id id)]
|
||||||
|
(when-not snapshot
|
||||||
|
(ex/raise :type :not-found
|
||||||
|
:code :snapshot-not-found
|
||||||
|
:hint "unable to find snapshot with the provided id"
|
||||||
|
:snapshot-id id
|
||||||
|
:file-id file-id))
|
||||||
|
;; Load current file metadata only (no data decoding) then overlay
|
||||||
|
;; the snapshot data so the client receives the same shape as a
|
||||||
|
;; normal get-file response but with historical page/object content.
|
||||||
|
(let [base-file (bfc/get-file cfg file-id :load-data? false)]
|
||||||
|
(-> base-file
|
||||||
|
(assoc :data (:data snapshot))
|
||||||
|
(assoc :version (:version snapshot))
|
||||||
|
(assoc :features (:features snapshot))
|
||||||
|
(assoc :revn (:revn snapshot))
|
||||||
|
(assoc :vern (rand-int 100000))
|
||||||
|
(assoc :permissions perms))))))
|
||||||
|
|
||||||
(def ^:private schema:create-file-snapshot
|
(def ^:private schema:create-file-snapshot
|
||||||
[:map
|
[:map
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
|
|||||||
@ -121,8 +121,10 @@
|
|||||||
:features features}
|
:features features}
|
||||||
permissions (:permissions state)]
|
permissions (:permissions state)]
|
||||||
|
|
||||||
;; Prevent commit changes by a team member without edition permission
|
;; Prevent saving changes when in version preview (read-only) mode
|
||||||
(when (:can-edit permissions)
|
;; or when the user does not have edition permission.
|
||||||
|
(when (and (:can-edit permissions)
|
||||||
|
(not (get-in state [:workspace-global :read-only?])))
|
||||||
(->> (rp/cmd! :update-file params)
|
(->> (rp/cmd! :update-file params)
|
||||||
(rx/mapcat (fn [{:keys [revn lagged] :as response}]
|
(rx/mapcat (fn [{:keys [revn lagged] :as response}]
|
||||||
(log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged))
|
(log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged))
|
||||||
|
|||||||
@ -511,7 +511,8 @@
|
|||||||
:workspace-persistence
|
:workspace-persistence
|
||||||
:workspace-presence
|
:workspace-presence
|
||||||
:workspace-tokens
|
:workspace-tokens
|
||||||
:workspace-undo)
|
:workspace-undo
|
||||||
|
:workspace-versions)
|
||||||
(update :workspace-global dissoc :read-only?)
|
(update :workspace-global dissoc :read-only?)
|
||||||
(assoc-in [:workspace-global :options-mode] :design)
|
(assoc-in [:workspace-global :options-mode] :design)
|
||||||
(update :files d/update-vals #(dissoc % :data))))
|
(update :files d/update-vals #(dissoc % :data))))
|
||||||
|
|||||||
@ -8,23 +8,28 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.logging :as log]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
[app.main.data.persistence :as dwp]
|
[app.main.data.persistence :as dwp]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.pages :as dwpg]
|
[app.main.data.workspace.pages :as dwpg]
|
||||||
[app.main.data.workspace.thumbnails :as th]
|
[app.main.data.workspace.thumbnails :as th]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(defonce default-state
|
(defonce default-state
|
||||||
{:status :loading
|
{:status :loading
|
||||||
:data nil
|
:data nil
|
||||||
:editing nil})
|
:editing nil
|
||||||
|
:preview-id nil})
|
||||||
|
|
||||||
(declare fetch-versions)
|
(declare fetch-versions)
|
||||||
|
|
||||||
@ -122,32 +127,6 @@
|
|||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
|
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
|
||||||
|
|
||||||
(defn restore-version
|
|
||||||
[id origin]
|
|
||||||
(assert (uuid? id) "expected valid uuid for `id`")
|
|
||||||
(ptk/reify ::restore-version
|
|
||||||
ptk/WatchEvent
|
|
||||||
(watch [_ state _]
|
|
||||||
(let [file-id (:current-file-id state)
|
|
||||||
team-id (:current-team-id state)
|
|
||||||
event-name (case origin
|
|
||||||
:version "restore-pin-version"
|
|
||||||
:snapshot "restore-autosave"
|
|
||||||
:plugin "restore-version-plugin")]
|
|
||||||
|
|
||||||
(rx/concat
|
|
||||||
(rx/of ::dwp/force-persist
|
|
||||||
(dw/remove-layout-flag :document-history))
|
|
||||||
|
|
||||||
(->> (wait-for-persistence file-id id)
|
|
||||||
(rx/map #(initialize-version)))
|
|
||||||
|
|
||||||
(if event-name
|
|
||||||
(rx/of (ev/event {::ev/name event-name
|
|
||||||
:file-id file-id
|
|
||||||
:team-id team-id}))
|
|
||||||
(rx/empty)))))))
|
|
||||||
|
|
||||||
(defn delete-version
|
(defn delete-version
|
||||||
[id]
|
[id]
|
||||||
(assert (uuid? id) "expected valid uuid for `id`")
|
(assert (uuid? id) "expected valid uuid for `id`")
|
||||||
@ -193,6 +172,145 @@
|
|||||||
(->> (rp/cmd! :unlock-file-snapshot {:id id})
|
(->> (rp/cmd! :unlock-file-snapshot {:id id})
|
||||||
(rx/map fetch-versions)))))
|
(rx/map fetch-versions)))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; RESTORE VERSION EVENTS
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- restore-version
|
||||||
|
[id]
|
||||||
|
(assert (uuid? id) "expected valid uuid for `id`")
|
||||||
|
(ptk/reify ::restore-version
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [file-id (:current-file-id state)]
|
||||||
|
(rx/concat
|
||||||
|
(rx/of ::dwp/force-persist
|
||||||
|
(dw/remove-layout-flag :document-history))
|
||||||
|
|
||||||
|
(->> (wait-for-persistence file-id id)
|
||||||
|
(rx/map #(initialize-version))))))))
|
||||||
|
|
||||||
|
(defn enter-restore
|
||||||
|
[id]
|
||||||
|
(assert (uuid? id) "expected valid uuid for `id`")
|
||||||
|
(ptk/reify ::enter-restore
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(let [output-s (rx/subject)]
|
||||||
|
(rx/merge
|
||||||
|
output-s
|
||||||
|
(rx/of (ntf/dialog
|
||||||
|
:content (tr "workspace.versions.restore-warning")
|
||||||
|
:controls :inline-actions
|
||||||
|
:cancel {:label (tr "workspace.updates.dismiss")
|
||||||
|
:callback #(do
|
||||||
|
(rx/push! output-s (ntf/hide :tag :restore-dialog))
|
||||||
|
(rx/end! output-s))}
|
||||||
|
:accept {:label (tr "labels.restore")
|
||||||
|
:callback #(do
|
||||||
|
(rx/push! output-s (restore-version id))
|
||||||
|
(rx/end! output-s))}
|
||||||
|
:tag :restore-dialog)))))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; PREVIEW VERSION EVENTS
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- apply-snapshot
|
||||||
|
"Swap the file data in app state with the provided snapshot-file
|
||||||
|
response. Used by the version preview feature to show historical
|
||||||
|
file content without modifying the database"
|
||||||
|
[{:keys [id] :as snapshot}]
|
||||||
|
(ptk/reify ::apply-snapshot-data
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :files assoc id snapshot))))
|
||||||
|
|
||||||
|
(defn exit-preview
|
||||||
|
"Exit from preview mode and reload the live file data"
|
||||||
|
[]
|
||||||
|
(ptk/reify ::exit-preview
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [backup (dm/get-in state [:workspace-versions :backup])]
|
||||||
|
(-> state
|
||||||
|
(update :workspace-versions dissoc :backup)
|
||||||
|
(update :workspace-global dissoc :read-only? :preview-id)
|
||||||
|
(update :files assoc (:id backup) backup))))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [file-id (:current-file-id state)
|
||||||
|
page-id (:current-page-id state)]
|
||||||
|
|
||||||
|
(rx/of (dwpg/initialize-page file-id page-id))))))
|
||||||
|
|
||||||
|
(defn enter-preview
|
||||||
|
"Load a snapshot into the workspace for read-only preview without
|
||||||
|
modifying any database state. Sets a read-only flag so no changes
|
||||||
|
are persisted while previewing and enter on the preview mode"
|
||||||
|
[id]
|
||||||
|
(assert (uuid? id) "expected valid uuid for `id`")
|
||||||
|
|
||||||
|
(ptk/reify ::enter-preview
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [file (dsh/lookup-file state)]
|
||||||
|
(-> state
|
||||||
|
(update :workspace-versions assoc :backup file)
|
||||||
|
(update :workspace-global assoc :read-only? true :preview-id id))))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [file-id (:current-file-id state)
|
||||||
|
page-id (:current-page-id state)
|
||||||
|
team-id (:current-team-id state)
|
||||||
|
features (features/get-enabled-features state team-id)
|
||||||
|
snapshot (->> (dm/get-in state [:workspace-versions :data])
|
||||||
|
(d/seek #(= id (:id %))))
|
||||||
|
label (or (:label snapshot)
|
||||||
|
(tr "workspace.versions.preview.unnamed"))
|
||||||
|
output-s (rx/subject)]
|
||||||
|
(rx/merge
|
||||||
|
output-s
|
||||||
|
|
||||||
|
(rx/of (ntf/dialog
|
||||||
|
:content (tr "workspace.versions.preview-banner-title" label)
|
||||||
|
:controls :inline-actions
|
||||||
|
:cancel {:label (tr "labels.exit")
|
||||||
|
:callback #(do
|
||||||
|
(rx/push! output-s (ntf/hide))
|
||||||
|
(rx/push! output-s (exit-preview))
|
||||||
|
(rx/end! output-s))}
|
||||||
|
:accept {:label (tr "labels.restore")
|
||||||
|
:callback #(do
|
||||||
|
(rx/push! output-s (ntf/hide))
|
||||||
|
(rx/push! output-s (restore-version id))
|
||||||
|
(rx/end! output-s))}
|
||||||
|
:tag :preview-dialog))
|
||||||
|
|
||||||
|
(->> (rp/cmd! :get-file-snapshot
|
||||||
|
{:file-id file-id
|
||||||
|
:id id
|
||||||
|
:features features})
|
||||||
|
(rx/mapcat
|
||||||
|
(fn [snapshot]
|
||||||
|
(rx/of
|
||||||
|
;; Swap the file data in state with snapshot content.
|
||||||
|
;; Passing id sets workspace-file-version-id, which
|
||||||
|
;; causes the WASM viewport to reload its shape buffer.
|
||||||
|
(apply-snapshot snapshot)
|
||||||
|
;; Re-initialize the page to rebuild its search index
|
||||||
|
;; and page-local state with the new snapshot
|
||||||
|
;; objects.
|
||||||
|
(dwpg/initialize-page file-id page-id))))
|
||||||
|
|
||||||
|
(rx/catch (fn [err]
|
||||||
|
;; On error roll back the read-only flag so the
|
||||||
|
;; user is not stuck in a broken preview state.
|
||||||
|
(log/error :hint "failed to load snapshot" :cause err :file-id file-id :snapshot-id id)
|
||||||
|
(rx/of (exit-preview))))))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; PLUGINS SPECIFIC EVENTS
|
;; PLUGINS SPECIFIC EVENTS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -246,20 +364,18 @@
|
|||||||
|
|
||||||
(ptk/reify ::restore-version-from-plugins
|
(ptk/reify ::restore-version-from-plugins
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ _ _]
|
||||||
(let [team-id (:current-team-id state)]
|
(rx/concat
|
||||||
(rx/concat
|
(rx/of (ev/event {::ev/name "restore-version"
|
||||||
(rx/of (ev/event {::ev/name "restore-version-plugin"
|
::ev/origin "plugins"})
|
||||||
:file-id file-id
|
::dwp/force-persist)
|
||||||
:team-id team-id})
|
|
||||||
::dwp/force-persist)
|
|
||||||
|
|
||||||
(->> (wait-for-persistence file-id id)
|
(->> (wait-for-persistence file-id id)
|
||||||
(rx/map #(initialize-version)))
|
(rx/map #(initialize-version)))
|
||||||
|
|
||||||
(->> (rx/of 1)
|
(->> (rx/of 1)
|
||||||
(rx/tap resolve)
|
(rx/tap resolve)
|
||||||
(rx/ignore)))))))
|
(rx/ignore))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.workspace.versions :as dwv]
|
[app.main.data.workspace.versions :as dwv]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -77,20 +77,49 @@
|
|||||||
(assoc item :index index)))
|
(assoc item :index index)))
|
||||||
(reverse)))
|
(reverse)))
|
||||||
|
|
||||||
(defn- open-restore-version-dialog
|
(defn- on-name-input-focus
|
||||||
[origin id]
|
[event]
|
||||||
(st/emit! (ntf/dialog
|
(dom/select-text! (dom/get-target event)))
|
||||||
:content (tr "workspace.versions.restore-warning")
|
|
||||||
:controls :inline-actions
|
(defn- extract-id-from-event
|
||||||
:cancel {:label (tr "workspace.updates.dismiss")
|
[event]
|
||||||
:callback #(st/emit! (ntf/hide))}
|
(-> event dom/get-current-target (dom/get-data "id") uuid/parse))
|
||||||
:accept {:label (tr "labels.restore")
|
|
||||||
:callback #(st/emit! (dwv/restore-version id origin))}
|
(defn- on-create-version
|
||||||
:tag :restore-dialog)))
|
[]
|
||||||
|
(st/emit! (dwv/create-version)))
|
||||||
|
|
||||||
|
(defn- on-edit-version
|
||||||
|
[id _event]
|
||||||
|
(st/emit! (dwv/update-versions-state {:editing id})))
|
||||||
|
|
||||||
|
(defn- on-cancel-version-edition
|
||||||
|
[_id _event]
|
||||||
|
(st/emit! (dwv/update-versions-state {:editing nil})))
|
||||||
|
|
||||||
|
(defn- on-rename-version
|
||||||
|
[id label]
|
||||||
|
(st/emit! (dwv/rename-version id label)))
|
||||||
|
|
||||||
|
(defn- on-delete-version
|
||||||
|
[id]
|
||||||
|
(st/emit! (dwv/delete-version id)))
|
||||||
|
|
||||||
|
(defn- on-pin-version
|
||||||
|
[id]
|
||||||
|
(st/emit! (dwv/pin-version id)))
|
||||||
|
|
||||||
|
(defn- on-lock-version
|
||||||
|
[id]
|
||||||
|
(st/emit! (dwv/lock-version id)))
|
||||||
|
|
||||||
|
(defn- on-unlock-version
|
||||||
|
[id]
|
||||||
|
(st/emit! (dwv/unlock-version id)))
|
||||||
|
|
||||||
(mf/defc version-entry*
|
(mf/defc version-entry*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [entry current-profile on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
|
[{:keys [entry current-profile on-preview on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
|
||||||
(let [show-menu? (mf/use-state false)
|
(let [show-menu? (mf/use-state false)
|
||||||
profiles (mf/deref refs/profiles)
|
profiles (mf/deref refs/profiles)
|
||||||
|
|
||||||
@ -108,6 +137,13 @@
|
|||||||
(fn [event]
|
(fn [event]
|
||||||
(on-edit (:id entry) event)))
|
(on-edit (:id entry) event)))
|
||||||
|
|
||||||
|
on-preview
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps entry on-preview)
|
||||||
|
(fn []
|
||||||
|
(when (fn? on-preview)
|
||||||
|
(on-preview (:id entry)))))
|
||||||
|
|
||||||
on-restore
|
on-restore
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps entry on-restore)
|
(mf/deps entry on-restore)
|
||||||
@ -136,11 +172,6 @@
|
|||||||
(when on-unlock
|
(when on-unlock
|
||||||
(on-unlock (:id entry)))))
|
(on-unlock (:id entry)))))
|
||||||
|
|
||||||
on-name-input-focus
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [event]
|
|
||||||
(dom/select-text! (dom/get-target event))))
|
|
||||||
|
|
||||||
on-name-input-blur
|
on-name-input-blur
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps entry on-rename on-cancel-edit)
|
(mf/deps entry on-rename on-cancel-edit)
|
||||||
@ -191,6 +222,11 @@
|
|||||||
:on-click on-edit}
|
:on-click on-edit}
|
||||||
(tr "labels.rename")])
|
(tr "labels.rename")])
|
||||||
|
|
||||||
|
[:li {:class (stl/css :menu-option)
|
||||||
|
:role "button"
|
||||||
|
:on-click on-preview}
|
||||||
|
(tr "workspace.versions.button.preview")]
|
||||||
|
|
||||||
[:li {:class (stl/css :menu-option)
|
[:li {:class (stl/css :menu-option)
|
||||||
:role "button"
|
:role "button"
|
||||||
:on-click on-restore}
|
:on-click on-restore}
|
||||||
@ -216,7 +252,7 @@
|
|||||||
(tr "labels.delete")])])]]))
|
(tr "labels.delete")])])]]))
|
||||||
|
|
||||||
(mf/defc snapshot-entry*
|
(mf/defc snapshot-entry*
|
||||||
[{:keys [entry on-pin-snapshot on-restore-snapshot]}]
|
[{:keys [entry on-pin-snapshot on-restore-snapshot on-preview-snapshot]}]
|
||||||
|
|
||||||
(let [open-menu* (mf/use-state nil)
|
(let [open-menu* (mf/use-state nil)
|
||||||
entry-ref (mf/use-ref nil)
|
entry-ref (mf/use-ref nil)
|
||||||
@ -225,23 +261,22 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-pin-snapshot)
|
(mf/deps on-pin-snapshot)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [node (dom/get-current-target event)
|
(when (fn? on-pin-snapshot)
|
||||||
id (-> node
|
(on-pin-snapshot (extract-id-from-event event) event))))
|
||||||
(dom/get-data "id")
|
|
||||||
(uuid/parse))]
|
|
||||||
(when (fn? on-pin-snapshot)
|
|
||||||
(on-pin-snapshot id event)))))
|
|
||||||
|
|
||||||
on-restore-snapshot
|
on-restore-snapshot
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-restore-snapshot)
|
(mf/deps on-restore-snapshot)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [node (dom/get-current-target event)
|
(when (fn? on-restore-snapshot)
|
||||||
id (-> node
|
(on-restore-snapshot (extract-id-from-event event) event))))
|
||||||
(dom/get-data "id")
|
|
||||||
(uuid/parse))]
|
on-preview-snapshot
|
||||||
(when (fn? on-restore-snapshot)
|
(mf/use-fn
|
||||||
(on-restore-snapshot id event)))))
|
(mf/deps on-preview-snapshot)
|
||||||
|
(fn [event]
|
||||||
|
(when (fn? on-preview-snapshot)
|
||||||
|
(on-preview-snapshot (extract-id-from-event event) event))))
|
||||||
|
|
||||||
on-open-snapshot-menu
|
on-open-snapshot-menu
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -266,6 +301,11 @@
|
|||||||
:on-close #(reset! open-menu* nil)}
|
:on-close #(reset! open-menu* nil)}
|
||||||
[:ul {:class (stl/css :version-options-dropdown)
|
[:ul {:class (stl/css :version-options-dropdown)
|
||||||
:style {"--offset" (dm/str (:offset @open-menu*) "px")}}
|
:style {"--offset" (dm/str (:offset @open-menu*) "px")}}
|
||||||
|
[:li {:class (stl/css :menu-option)
|
||||||
|
:role "button"
|
||||||
|
:data-id (dm/str (:snapshot @open-menu*))
|
||||||
|
:on-click on-preview-snapshot}
|
||||||
|
(tr "workspace.versions.button.preview")]
|
||||||
[:li {:class (stl/css :menu-option)
|
[:li {:class (stl/css :menu-option)
|
||||||
:role "button"
|
:role "button"
|
||||||
:data-id (dm/str (:snapshot @open-menu*))
|
:data-id (dm/str (:snapshot @open-menu*))
|
||||||
@ -302,66 +342,50 @@
|
|||||||
(= (:filter state) (:profile-id %)))))
|
(= (:filter state) (:profile-id %)))))
|
||||||
(group-snapshots)))
|
(group-snapshots)))
|
||||||
|
|
||||||
on-create-version
|
on-preview-version
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [] (st/emit! (dwv/create-version))))
|
(fn [id]
|
||||||
|
(st/emit! (dwv/enter-preview id)
|
||||||
|
(ev/event {::ev/name "preview-version"
|
||||||
|
::ev/origin "workspace:sidebar"
|
||||||
|
:type "pinned-version"}))))
|
||||||
|
|
||||||
on-edit-version
|
on-preview-snapshot
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [id _event]
|
(fn [id _event]
|
||||||
(st/emit! (dwv/update-versions-state {:editing id}))))
|
(st/emit! (dwv/enter-preview id)
|
||||||
|
(ev/event {::ev/name "preview-version"
|
||||||
on-cancel-version-edition
|
::ev/origin "workspace:sidebar"
|
||||||
(mf/use-fn
|
:type "autosaved-version"}))))
|
||||||
(fn [_id _event]
|
|
||||||
(st/emit! (dwv/update-versions-state {:editing nil}))))
|
|
||||||
|
|
||||||
on-rename-version
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [id label]
|
|
||||||
(st/emit! (dwv/rename-version id label))))
|
|
||||||
|
|
||||||
on-restore-version
|
on-restore-version
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [id _event]
|
(fn [id _event]
|
||||||
(open-restore-version-dialog :version id)))
|
(st/emit! (dwv/enter-restore id)
|
||||||
|
(ev/event {::ev/name "restore-version"
|
||||||
|
::ev/origin "workspace:sidebar"
|
||||||
|
:type "pinned-version"}))))
|
||||||
|
|
||||||
on-restore-snapshot
|
on-restore-snapshot
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [id _event]
|
(fn [id _event]
|
||||||
(open-restore-version-dialog :snapshot id)))
|
(st/emit! (dwv/enter-restore id)
|
||||||
|
(ev/event {::ev/name "restore-version"
|
||||||
on-delete-version
|
::ev/origin "workspace:sidebar"
|
||||||
(mf/use-fn
|
:type "autosaved-version"}))))
|
||||||
(fn [id]
|
|
||||||
(st/emit! (dwv/delete-version id))))
|
|
||||||
|
|
||||||
on-pin-version
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [id] (st/emit! (dwv/pin-version id))))
|
|
||||||
|
|
||||||
on-lock-version
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [id]
|
|
||||||
(st/emit! (dwv/lock-version id))))
|
|
||||||
|
|
||||||
on-unlock-version
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [id]
|
|
||||||
(st/emit! (dwv/unlock-version id))))
|
|
||||||
|
|
||||||
on-change-filter
|
on-change-filter
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [filter]
|
(fn [filter-value]
|
||||||
(cond
|
(cond
|
||||||
(= :all filter)
|
(= :all filter-value)
|
||||||
(st/emit! (dwv/update-versions-state {:filter nil}))
|
(st/emit! (dwv/update-versions-state {:filter nil}))
|
||||||
|
|
||||||
(= :own filter)
|
(= :own filter-value)
|
||||||
(st/emit! (dwv/update-versions-state {:filter (:id profile)}))
|
(st/emit! (dwv/update-versions-state {:filter (:id profile)}))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwv/update-versions-state {:filter filter})))))
|
(st/emit! (dwv/update-versions-state {:filter filter-value})))))
|
||||||
|
|
||||||
options
|
options
|
||||||
(mf/with-memo [users profile]
|
(mf/with-memo [users profile]
|
||||||
@ -415,6 +439,7 @@
|
|||||||
:on-edit on-edit-version
|
:on-edit on-edit-version
|
||||||
:on-cancel-edit on-cancel-version-edition
|
:on-cancel-edit on-cancel-version-edition
|
||||||
:on-rename on-rename-version
|
:on-rename on-rename-version
|
||||||
|
:on-preview on-preview-version
|
||||||
:on-restore on-restore-version
|
:on-restore on-restore-version
|
||||||
:on-delete on-delete-version
|
:on-delete on-delete-version
|
||||||
:on-lock on-lock-version
|
:on-lock on-lock-version
|
||||||
@ -423,6 +448,7 @@
|
|||||||
:snapshot
|
:snapshot
|
||||||
[:> snapshot-entry* {:key (:index entry)
|
[:> snapshot-entry* {:key (:index entry)
|
||||||
:entry entry
|
:entry entry
|
||||||
|
:on-preview-snapshot on-preview-snapshot
|
||||||
:on-restore-snapshot on-restore-snapshot
|
:on-restore-snapshot on-restore-snapshot
|
||||||
:on-pin-snapshot on-pin-version}]
|
:on-pin-snapshot on-pin-version}]
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,7 @@
|
|||||||
|
|
||||||
{:keys [options-mode
|
{:keys [options-mode
|
||||||
tooltip
|
tooltip
|
||||||
|
preview-id
|
||||||
show-distances?
|
show-distances?
|
||||||
picking-color?]}
|
picking-color?]}
|
||||||
wglobal
|
wglobal
|
||||||
@ -314,23 +315,28 @@
|
|||||||
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
|
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
|
||||||
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
|
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
|
||||||
|
|
||||||
[:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
|
[:div {:class (stl/css :viewport) :style {"--zoom" zoom} :data-testid "viewport"}
|
||||||
(when (:can-edit permissions)
|
(cond
|
||||||
(if read-only?
|
(some? preview-id)
|
||||||
[:> view-only-bar* {}]
|
nil
|
||||||
[:*
|
|
||||||
(when-not hide-ui?
|
|
||||||
[:> top-toolbar* {:layout layout}])
|
|
||||||
|
|
||||||
(when (and ^boolean path-editing?
|
(and read-only? (:can-edit permissions))
|
||||||
^boolean single-select?)
|
[:> view-only-bar* {}]
|
||||||
[:> path-edition-bar* {:shape editing-shape
|
|
||||||
:edit-path-state edit-path-state
|
|
||||||
:layout layout}])
|
|
||||||
|
|
||||||
(when (and ^boolean grid-editing?
|
:else
|
||||||
^boolean single-select?)
|
[:*
|
||||||
[:> grid-edition-bar* {:shape editing-shape}])]))
|
(when-not hide-ui?
|
||||||
|
[:> top-toolbar* {:layout layout}])
|
||||||
|
|
||||||
|
(when (and ^boolean path-editing?
|
||||||
|
^boolean single-select?)
|
||||||
|
[:> path-edition-bar* {:shape editing-shape
|
||||||
|
:edit-path-state edit-path-state
|
||||||
|
:layout layout}])
|
||||||
|
|
||||||
|
(when (and ^boolean grid-editing?
|
||||||
|
^boolean single-select?)
|
||||||
|
[:> grid-edition-bar* {:shape editing-shape}])])
|
||||||
|
|
||||||
[:div {:class (stl/css :viewport-overlays)}
|
[:div {:class (stl/css :viewport-overlays)}
|
||||||
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
|
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
|
||||||
|
|||||||
@ -20,14 +20,12 @@
|
|||||||
;; branch.
|
;; branch.
|
||||||
|
|
||||||
(mf/defc view-only-bar*
|
(mf/defc view-only-bar*
|
||||||
{::mf/private true}
|
|
||||||
[]
|
[]
|
||||||
(let [handle-close-view-mode
|
(let [on-close
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn []
|
#(st/emit! :interrupt
|
||||||
(st/emit! :interrupt
|
(dw/set-options-mode :design)
|
||||||
(dw/set-options-mode :design)
|
(dwc/set-workspace-read-only false)))]
|
||||||
(dwc/set-workspace-read-only false))))]
|
|
||||||
[:div {:class (stl/css :viewport-actions)}
|
[:div {:class (stl/css :viewport-actions)}
|
||||||
[:div {:class (stl/css :viewport-actions-container)}
|
[:div {:class (stl/css :viewport-actions-container)}
|
||||||
[:div {:class (stl/css :viewport-actions-title)}
|
[:div {:class (stl/css :viewport-actions-title)}
|
||||||
@ -35,7 +33,7 @@
|
|||||||
{:tag-name "span"
|
{:tag-name "span"
|
||||||
:content (tr "workspace.top-bar.view-only")}]]
|
:content (tr "workspace.top-bar.view-only")}]]
|
||||||
[:button {:class (stl/css :done-btn)
|
[:button {:class (stl/css :done-btn)
|
||||||
:on-click handle-close-view-mode}
|
:on-click on-close}
|
||||||
(tr "workspace.top-bar.read-only.done")]]]))
|
(tr "workspace.top-bar.read-only.done")]]]))
|
||||||
|
|
||||||
(mf/defc path-edition-bar*
|
(mf/defc path-edition-bar*
|
||||||
|
|||||||
@ -98,6 +98,7 @@
|
|||||||
{:keys [options-mode
|
{:keys [options-mode
|
||||||
tooltip
|
tooltip
|
||||||
show-distances?
|
show-distances?
|
||||||
|
preview-id
|
||||||
picking-color?]}
|
picking-color?]}
|
||||||
wglobal
|
wglobal
|
||||||
|
|
||||||
@ -456,22 +457,28 @@
|
|||||||
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
|
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
|
||||||
|
|
||||||
[:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
|
[:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
|
||||||
(when (:can-edit permissions)
|
|
||||||
(if read-only?
|
|
||||||
[:> view-only-bar* {}]
|
|
||||||
[:*
|
|
||||||
(when-not hide-ui?
|
|
||||||
[:> top-toolbar* {:layout layout}])
|
|
||||||
|
|
||||||
(when (and ^boolean path-editing?
|
(cond
|
||||||
^boolean single-select?)
|
(some? preview-id)
|
||||||
[:> path-edition-bar* {:shape editing-shape
|
nil
|
||||||
:edit-path-state edit-path-state
|
|
||||||
:layout layout}])
|
|
||||||
|
|
||||||
(when (and ^boolean grid-editing?
|
(and read-only? (:can-edit permissions))
|
||||||
^boolean single-select?)
|
[:> view-only-bar* {}]
|
||||||
[:> grid-edition-bar* {:shape editing-shape}])]))
|
|
||||||
|
:else
|
||||||
|
[:*
|
||||||
|
(when-not hide-ui?
|
||||||
|
[:> top-toolbar* {:layout layout}])
|
||||||
|
|
||||||
|
(when (and ^boolean path-editing?
|
||||||
|
^boolean single-select?)
|
||||||
|
[:> path-edition-bar* {:shape editing-shape
|
||||||
|
:edit-path-state edit-path-state
|
||||||
|
:layout layout}])
|
||||||
|
|
||||||
|
(when (and ^boolean grid-editing?
|
||||||
|
^boolean single-select?)
|
||||||
|
[:> grid-edition-bar* {:shape editing-shape}])])
|
||||||
|
|
||||||
[:div {:class (stl/css :viewport-overlays)}
|
[:div {:class (stl/css :viewport-overlays)}
|
||||||
(when show-comments?
|
(when show-comments?
|
||||||
|
|||||||
@ -3049,6 +3049,9 @@ msgstr "Resend invitation"
|
|||||||
msgid "labels.restore"
|
msgid "labels.restore"
|
||||||
msgstr "Restore"
|
msgstr "Restore"
|
||||||
|
|
||||||
|
msgid "labels.exit"
|
||||||
|
msgstr "Exit"
|
||||||
|
|
||||||
#: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419
|
#: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419
|
||||||
msgid "labels.retry"
|
msgid "labels.retry"
|
||||||
msgstr "Retry"
|
msgstr "Retry"
|
||||||
@ -9064,6 +9067,9 @@ msgstr "Autosaved %s"
|
|||||||
msgid "workspace.versions.button.pin"
|
msgid "workspace.versions.button.pin"
|
||||||
msgstr "Pin version"
|
msgstr "Pin version"
|
||||||
|
|
||||||
|
msgid "workspace.versions.button.preview"
|
||||||
|
msgstr "Preview version"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/versions.cljs:273
|
#: src/app/main/ui/workspace/sidebar/versions.cljs:273
|
||||||
msgid "workspace.versions.button.restore"
|
msgid "workspace.versions.button.restore"
|
||||||
msgstr "Restore version"
|
msgstr "Restore version"
|
||||||
@ -9108,6 +9114,12 @@ msgstr "This version is locked by %s and cannot be modified"
|
|||||||
msgid "workspace.versions.locked-by-you"
|
msgid "workspace.versions.locked-by-you"
|
||||||
msgstr "This version is locked by you"
|
msgstr "This version is locked by you"
|
||||||
|
|
||||||
|
msgid "workspace.versions.preview-banner-title"
|
||||||
|
msgstr "Previewing version: %s"
|
||||||
|
|
||||||
|
msgid "workspace.versions.preview.unnamed"
|
||||||
|
msgstr "Unnamed version"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/versions.cljs:83
|
#: src/app/main/ui/workspace/sidebar/versions.cljs:83
|
||||||
msgid "workspace.versions.restore-warning"
|
msgid "workspace.versions.restore-warning"
|
||||||
msgstr "Do you want to restore this version?"
|
msgstr "Do you want to restore this version?"
|
||||||
|
|||||||
@ -2976,6 +2976,9 @@ msgstr "Reenviar invitacion"
|
|||||||
msgid "labels.restore"
|
msgid "labels.restore"
|
||||||
msgstr "Restaurar"
|
msgstr "Restaurar"
|
||||||
|
|
||||||
|
msgid "labels.exit"
|
||||||
|
msgstr "Salir"
|
||||||
|
|
||||||
#: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419
|
#: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419
|
||||||
msgid "labels.retry"
|
msgid "labels.retry"
|
||||||
msgstr "Reintentar"
|
msgstr "Reintentar"
|
||||||
@ -8869,6 +8872,9 @@ msgstr "Versiones de %s"
|
|||||||
msgid "workspace.versions.loading"
|
msgid "workspace.versions.loading"
|
||||||
msgstr "Cargando..."
|
msgstr "Cargando..."
|
||||||
|
|
||||||
|
msgid "workspace.versions.preview-banner-title"
|
||||||
|
msgstr "Previsualizando version: %s"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/versions.cljs:83
|
#: src/app/main/ui/workspace/sidebar/versions.cljs:83
|
||||||
msgid "workspace.versions.restore-warning"
|
msgid "workspace.versions.restore-warning"
|
||||||
msgstr "¿Quieres restaurar esta versión?"
|
msgstr "¿Quieres restaurar esta versión?"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user