mirror of
https://github.com/penpot/penpot.git
synced 2026-09-03 10:38:57 +00:00
Closes #9092. `restore-version-from-plugin` accepted `_reject` as a dead parameter and its stream had no `rx/catch`, so errors raised during the restore flow (failed `rp/cmd! :restore-file-snapshot`, persistence timeouts, or exceptions inside the watch body) silently swallowed instead of rejecting the plugin-facing promise at `file.cljs:81`. Plugin code that did `await version.restore()` would hang indefinitely on any failure. Wire `reject` through and wrap the emission with the same `rx/catch` pattern already used by `create-version-from-plugins` in this file. - Rename `_reject` to `reject` in the function signature - Wrap the `rx/concat` body with `rx/catch` that calls `(reject error)` and returns `rx/empty` on error, mirroring `create-version-from-plugins` - Add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
388 lines
13 KiB
Clojure
388 lines
13 KiB
Clojure
;; 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.data.workspace.versions
|
|
(: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
|
|
:preview-id nil})
|
|
|
|
(declare fetch-versions)
|
|
|
|
(defn init-versions-state
|
|
[]
|
|
(ptk/reify ::init-versions-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc state :workspace-versions default-state))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (fetch-versions)))))
|
|
|
|
(defn update-versions-state
|
|
[version-state]
|
|
(ptk/reify ::update-versions-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update state :workspace-versions merge version-state))))
|
|
|
|
(defn fetch-versions
|
|
[]
|
|
(ptk/reify ::fetch-versions
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [file-id (:current-file-id state)]
|
|
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
|
|
(rx/map #(update-versions-state {:status :loaded :data %})))))))
|
|
|
|
(defn create-version
|
|
[]
|
|
(ptk/reify ::create-version
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [label (ct/format-inst (ct/now) :localized-date)
|
|
file-id (:current-file-id state)]
|
|
|
|
;; Force persist before creating snapshot, otherwise we could loss changes
|
|
(rx/concat
|
|
(rx/of ::dwp/force-persist
|
|
(ptk/event ::ev/event {::ev/name "create-version"}))
|
|
|
|
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
|
(rx/filter #(or (nil? %) (= :saved %)))
|
|
(rx/take 1)
|
|
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
|
|
(rx/mapcat
|
|
(fn [{:keys [id]}]
|
|
(rx/of (update-versions-state {:editing id})
|
|
(fetch-versions))))))))))
|
|
|
|
(defn rename-version
|
|
[id label]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
(assert (sm/valid-text? label) "expected not empty string for `label`")
|
|
|
|
(ptk/reify ::rename-version
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (:current-file-id state)]
|
|
(rx/merge
|
|
(rx/of (update-versions-state {:editing nil})
|
|
(ptk/event ::ev/event {::ev/name "rename-version"
|
|
:file-id file-id}))
|
|
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
|
|
(rx/map fetch-versions)))))))
|
|
|
|
(defn- initialize-version
|
|
[]
|
|
(ptk/reify ::initialize-version
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(let [page-id (:current-page-id state)
|
|
file-id (:current-file-id state)
|
|
team-id (:current-team-id state)]
|
|
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dw/bundle-fetched))
|
|
(rx/take 1)
|
|
(rx/map #(dwpg/initialize-page file-id page-id)))
|
|
|
|
(rx/of (ntf/hide :tag :restore-dialog)
|
|
(dw/initialize-file team-id file-id)))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ _ _]
|
|
(th/clear-queue!))))
|
|
|
|
(defn- wait-for-persistence
|
|
[file-id snapshot-id]
|
|
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
|
(rx/filter #(or (nil? %) (= :saved %)))
|
|
(rx/take 1)
|
|
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
|
|
|
|
(defn delete-version
|
|
[id]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
|
|
(ptk/reify ::delete-version
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (rp/cmd! :delete-file-snapshot {:id id})
|
|
(rx/map fetch-versions)))))
|
|
|
|
(defn pin-version
|
|
[id]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
(ptk/reify ::pin-version
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [version (->> (dm/get-in state [:workspace-versions :data])
|
|
(d/seek #(= (:id %) id)))]
|
|
(let [params {:id id
|
|
:label (ct/format-inst (:created-at version) :localized-date)}]
|
|
|
|
(->> (rp/cmd! :update-file-snapshot params)
|
|
(rx/mapcat (fn [_]
|
|
(rx/of (update-versions-state {:editing id})
|
|
(fetch-versions)
|
|
(ptk/event ::ev/event {::ev/name "pin-version"}))))))))))
|
|
|
|
(defn lock-version
|
|
[id]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
(ptk/reify ::lock-version
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (rp/cmd! :lock-file-snapshot {:id id})
|
|
(rx/map fetch-versions)))))
|
|
|
|
(defn unlock-version
|
|
[id]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
(ptk/reify ::unlock-version
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (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
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- wait-persisted-status
|
|
[]
|
|
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
|
(rx/filter #(or (nil? %) (= :saved %)))
|
|
(rx/take 1)))
|
|
|
|
(defn create-version-from-plugins
|
|
[file-id label resolve reject]
|
|
|
|
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
|
(assert (sm/valid-text? label) "expected not empty string for `label`")
|
|
|
|
(ptk/reify ::create-version-from-plugins
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [current-file-id (:current-file-id state)]
|
|
;; Force persist before creating snapshot, otherwise we could loss changes
|
|
(->> (rx/concat
|
|
(rx/of (ptk/event ::ev/event {::ev/origin "plugins"
|
|
::ev/name "create-version"}))
|
|
|
|
(when (= file-id current-file-id)
|
|
(rx/of ::dwp/force-persist))
|
|
|
|
(->> (if (= file-id current-file-id)
|
|
(wait-persisted-status)
|
|
(rx/of :nothing))
|
|
(rx/mapcat
|
|
(fn [_]
|
|
(rp/cmd! :create-file-snapshot {:file-id file-id :label label})))
|
|
(rx/mapcat
|
|
(fn [{:keys [id]}]
|
|
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
|
|
(rx/take 1)
|
|
(rx/map (fn [versions] (d/seek #(= id (:id %)) versions))))))
|
|
(rx/tap resolve)
|
|
(rx/ignore)))
|
|
|
|
;; On error reject the promise and empty the stream
|
|
(rx/catch (fn [error]
|
|
(reject error)
|
|
(rx/empty))))))))
|
|
|
|
(defn restore-version-from-plugin
|
|
[file-id id resolve reject]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
|
|
(ptk/reify ::restore-version-from-plugins
|
|
ptk/WatchEvent
|
|
(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)))
|
|
|
|
(->> (rx/of 1)
|
|
(rx/tap resolve)
|
|
(rx/ignore)))
|
|
|
|
;; On error reject the promise and empty the stream
|
|
(rx/catch (fn [error]
|
|
(reject error)
|
|
(rx/empty)))))))
|
|
|
|
|
|
|
|
|