Add read-only preview mode for saved versions (#7622) (#8976)

*  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:
wdeveloper16 2026-04-24 08:13:16 +02:00 committed by GitHub
parent 7c1a29ccf7
commit e280168de9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 363 additions and 150 deletions

View File

@ -112,8 +112,9 @@
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
END"))
(defn- get-snapshot
"Get snapshot with decoded data"
(defn get-snapshot-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]
(let [now (ct/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})
snapshot
(get-snapshot cfg file-id snapshot-id)]
(get-snapshot-data cfg file-id snapshot-id)]
(when-not snapshot
(ex/raise :type :not-found

View File

@ -8,6 +8,7 @@
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.features :as-alias cfeat]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.db :as db]
@ -35,6 +36,43 @@
(files/check-read-permissions! conn profile-id 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
[:map
[:file-id ::sm/uuid]

View File

@ -121,8 +121,10 @@
:features features}
permissions (:permissions state)]
;; Prevent commit changes by a team member without edition permission
(when (:can-edit permissions)
;; Prevent saving changes when in version preview (read-only) mode
;; 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)
(rx/mapcat (fn [{:keys [revn lagged] :as response}]
(log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged))

View File

@ -511,7 +511,8 @@
:workspace-persistence
:workspace-presence
:workspace-tokens
:workspace-undo)
:workspace-undo
:workspace-versions)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)
(update :files d/update-vals #(dissoc % :data))))

View File

@ -8,23 +8,28 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.thumbnails :as th]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defonce default-state
{:status :loading
:data nil
:editing nil})
:editing nil
:preview-id nil})
(declare fetch-versions)
@ -122,32 +127,6 @@
(rx/take 1)
(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
[id]
(assert (uuid? id) "expected valid uuid for `id`")
@ -193,6 +172,145 @@
(->> (rp/cmd! :unlock-file-snapshot {:id id})
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -246,20 +364,18 @@
(ptk/reify ::restore-version-from-plugins
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/concat
(rx/of (ev/event {::ev/name "restore-version-plugin"
:file-id file-id
:team-id team-id})
::dwp/force-persist)
(watch [_ _ _]
(rx/concat
(rx/of (ev/event {::ev/name "restore-version"
::ev/origin "plugins"})
::dwp/force-persist)
(->> (wait-for-persistence file-id id)
(rx/map #(initialize-version)))
(->> (wait-for-persistence file-id id)
(rx/map #(initialize-version)))
(->> (rx/of 1)
(rx/tap resolve)
(rx/ignore)))))))
(->> (rx/of 1)
(rx/tap resolve)
(rx/ignore))))))

View File

@ -11,7 +11,7 @@
[app.common.time :as ct]
[app.common.uuid :as uuid]
[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.refs :as refs]
[app.main.store :as st]
@ -77,20 +77,49 @@
(assoc item :index index)))
(reverse)))
(defn- open-restore-version-dialog
[origin id]
(st/emit! (ntf/dialog
:content (tr "workspace.versions.restore-warning")
:controls :inline-actions
:cancel {:label (tr "workspace.updates.dismiss")
:callback #(st/emit! (ntf/hide))}
:accept {:label (tr "labels.restore")
:callback #(st/emit! (dwv/restore-version id origin))}
:tag :restore-dialog)))
(defn- on-name-input-focus
[event]
(dom/select-text! (dom/get-target event)))
(defn- extract-id-from-event
[event]
(-> event dom/get-current-target (dom/get-data "id") uuid/parse))
(defn- on-create-version
[]
(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/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)
profiles (mf/deref refs/profiles)
@ -108,6 +137,13 @@
(fn [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
(mf/use-fn
(mf/deps entry on-restore)
@ -136,11 +172,6 @@
(when on-unlock
(on-unlock (:id entry)))))
on-name-input-focus
(mf/use-fn
(fn [event]
(dom/select-text! (dom/get-target event))))
on-name-input-blur
(mf/use-fn
(mf/deps entry on-rename on-cancel-edit)
@ -191,6 +222,11 @@
:on-click on-edit}
(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)
:role "button"
:on-click on-restore}
@ -216,7 +252,7 @@
(tr "labels.delete")])])]]))
(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)
entry-ref (mf/use-ref nil)
@ -225,23 +261,22 @@
(mf/use-fn
(mf/deps on-pin-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> node
(dom/get-data "id")
(uuid/parse))]
(when (fn? on-pin-snapshot)
(on-pin-snapshot id event)))))
(when (fn? on-pin-snapshot)
(on-pin-snapshot (extract-id-from-event event) event))))
on-restore-snapshot
(mf/use-fn
(mf/deps on-restore-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> node
(dom/get-data "id")
(uuid/parse))]
(when (fn? on-restore-snapshot)
(on-restore-snapshot id event)))))
(when (fn? on-restore-snapshot)
(on-restore-snapshot (extract-id-from-event event) event))))
on-preview-snapshot
(mf/use-fn
(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
(mf/use-fn
@ -266,6 +301,11 @@
:on-close #(reset! open-menu* nil)}
[:ul {:class (stl/css :version-options-dropdown)
: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)
:role "button"
:data-id (dm/str (:snapshot @open-menu*))
@ -302,66 +342,50 @@
(= (:filter state) (:profile-id %)))))
(group-snapshots)))
on-create-version
on-preview-version
(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
(fn [id _event]
(st/emit! (dwv/update-versions-state {:editing id}))))
on-cancel-version-edition
(mf/use-fn
(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))))
(st/emit! (dwv/enter-preview id)
(ev/event {::ev/name "preview-version"
::ev/origin "workspace:sidebar"
:type "autosaved-version"}))))
on-restore-version
(mf/use-fn
(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
(mf/use-fn
(fn [id _event]
(open-restore-version-dialog :snapshot id)))
on-delete-version
(mf/use-fn
(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))))
(st/emit! (dwv/enter-restore id)
(ev/event {::ev/name "restore-version"
::ev/origin "workspace:sidebar"
:type "autosaved-version"}))))
on-change-filter
(mf/use-fn
(fn [filter]
(fn [filter-value]
(cond
(= :all filter)
(= :all filter-value)
(st/emit! (dwv/update-versions-state {:filter nil}))
(= :own filter)
(= :own filter-value)
(st/emit! (dwv/update-versions-state {:filter (:id profile)}))
:else
(st/emit! (dwv/update-versions-state {:filter filter})))))
(st/emit! (dwv/update-versions-state {:filter filter-value})))))
options
(mf/with-memo [users profile]
@ -415,6 +439,7 @@
:on-edit on-edit-version
:on-cancel-edit on-cancel-version-edition
:on-rename on-rename-version
:on-preview on-preview-version
:on-restore on-restore-version
:on-delete on-delete-version
:on-lock on-lock-version
@ -423,6 +448,7 @@
:snapshot
[:> snapshot-entry* {:key (:index entry)
:entry entry
:on-preview-snapshot on-preview-snapshot
:on-restore-snapshot on-restore-snapshot
:on-pin-snapshot on-pin-version}]

View File

@ -97,6 +97,7 @@
{:keys [options-mode
tooltip
preview-id
show-distances?
picking-color?]}
wglobal
@ -314,23 +315,28 @@
(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)
[: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}])
[:div {:class (stl/css :viewport) :style {"--zoom" zoom} :data-testid "viewport"}
(cond
(some? preview-id)
nil
(when (and ^boolean path-editing?
^boolean single-select?)
[:> path-edition-bar* {:shape editing-shape
:edit-path-state edit-path-state
:layout layout}])
(and read-only? (:can-edit permissions))
[:> view-only-bar* {}]
(when (and ^boolean grid-editing?
^boolean single-select?)
[:> 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)}
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap

View File

@ -20,14 +20,12 @@
;; branch.
(mf/defc view-only-bar*
{::mf/private true}
[]
(let [handle-close-view-mode
(let [on-close
(mf/use-fn
(fn []
(st/emit! :interrupt
(dw/set-options-mode :design)
(dwc/set-workspace-read-only false))))]
#(st/emit! :interrupt
(dw/set-options-mode :design)
(dwc/set-workspace-read-only false)))]
[:div {:class (stl/css :viewport-actions)}
[:div {:class (stl/css :viewport-actions-container)}
[:div {:class (stl/css :viewport-actions-title)}
@ -35,7 +33,7 @@
{:tag-name "span"
:content (tr "workspace.top-bar.view-only")}]]
[:button {:class (stl/css :done-btn)
:on-click handle-close-view-mode}
:on-click on-close}
(tr "workspace.top-bar.read-only.done")]]]))
(mf/defc path-edition-bar*

View File

@ -98,6 +98,7 @@
{:keys [options-mode
tooltip
show-distances?
preview-id
picking-color?]}
wglobal
@ -456,22 +457,28 @@
(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"}
(when (:can-edit permissions)
(if read-only?
[:> view-only-bar* {}]
[:*
(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}])
(cond
(some? preview-id)
nil
(when (and ^boolean grid-editing?
^boolean single-select?)
[:> grid-edition-bar* {:shape editing-shape}])]))
(and read-only? (:can-edit permissions))
[:> view-only-bar* {}]
: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)}
(when show-comments?

View File

@ -3049,6 +3049,9 @@ msgstr "Resend invitation"
msgid "labels.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
msgid "labels.retry"
msgstr "Retry"
@ -9064,6 +9067,9 @@ msgstr "Autosaved %s"
msgid "workspace.versions.button.pin"
msgstr "Pin version"
msgid "workspace.versions.button.preview"
msgstr "Preview version"
#: src/app/main/ui/workspace/sidebar/versions.cljs:273
msgid "workspace.versions.button.restore"
msgstr "Restore version"
@ -9108,6 +9114,12 @@ msgstr "This version is locked by %s and cannot be modified"
msgid "workspace.versions.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
msgid "workspace.versions.restore-warning"
msgstr "Do you want to restore this version?"

View File

@ -2976,6 +2976,9 @@ msgstr "Reenviar invitacion"
msgid "labels.restore"
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
msgid "labels.retry"
msgstr "Reintentar"
@ -8869,6 +8872,9 @@ msgstr "Versiones de %s"
msgid "workspace.versions.loading"
msgstr "Cargando..."
msgid "workspace.versions.preview-banner-title"
msgstr "Previsualizando version: %s"
#: src/app/main/ui/workspace/sidebar/versions.cljs:83
msgid "workspace.versions.restore-warning"
msgstr "¿Quieres restaurar esta versión?"