mirror of
https://github.com/penpot/penpot.git
synced 2026-11-06 01:50:39 +00:00
While saves fail the workspace turns read-only, but only some tools checked it. Edits from the asset, color and token panels, and a drag or a drawing in progress when the lock came, still made commits that queued behind the failed save. commit-changes refuses commits while the save-failure lock holds, except those that are not user edits: the undo of rolled-back commits, text position data and component touches. Undo and redo do nothing while locked. When the lock comes, moves, resizes, rotations, drawings and the drags of flex, guide and grid handles in progress are cancelled and commit nothing, and the design panel shows the Inspect view.
332 lines
13 KiB
Clojure
332 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 SUBSIDIARY SL
|
|
|
|
(ns app.main.data.changes
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.changes :as cpc]
|
|
[app.common.logging :as log]
|
|
[app.common.time :as ct]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.shape-tree :as ctst]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.persistence.commits :as pcm]
|
|
[app.main.data.persistence.lock :as plk]
|
|
[app.main.data.workspace :as-alias dw]
|
|
[app.main.features :as features]
|
|
[app.main.worker :as mw]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.render-wasm.shape :as wasm.shape]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
;; Change this to :info :debug or :trace to debug this module
|
|
(log/set-level! :warn)
|
|
|
|
(def page-change?
|
|
#{:add-page :mod-page :del-page :mov-page})
|
|
|
|
(def update-layout-attr?
|
|
#{:hidden})
|
|
|
|
(def commit?
|
|
(ptk/type? ::commit))
|
|
|
|
(defn- fix-page-id
|
|
"For events that modifies the page, page-id does not comes
|
|
as a property so we assign it from the `id` property."
|
|
[{:keys [id type page] :as change}]
|
|
(cond-> change
|
|
(and (page-change? type)
|
|
(nil? (:page-id change)))
|
|
(assoc :page-id (or id (:id page)))))
|
|
|
|
(defn- update-indexes
|
|
"Given a commit, send the changes to the worker for updating the
|
|
indexes."
|
|
[commit attr]
|
|
(ptk/reify ::update-indexes
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [changes (->> (get commit attr)
|
|
(map fix-page-id)
|
|
(filter :page-id)
|
|
(group-by :page-id))]
|
|
|
|
(->> (rx/from changes)
|
|
(rx/merge-map (fn [[page-id changes]]
|
|
(log/debug :hint "update-indexes" :page-id page-id :changes (count changes))
|
|
(mw/ask! {:cmd :index/update
|
|
:page-id page-id
|
|
:changes changes})))
|
|
(rx/catch (fn [cause]
|
|
(log/warn :hint "unable to update index"
|
|
:cause cause)
|
|
(rx/empty)))
|
|
(rx/ignore))))))
|
|
|
|
(def ^:private xf:map-page-id
|
|
;; Component-only changes have no :page-id; keep drops those nils so
|
|
;; update-object-indices does not create a nil key in :pages-index.
|
|
(keep :page-id))
|
|
|
|
(def ^:private wasm-structural-change-types
|
|
#{:add-obj :mov-objects})
|
|
|
|
(defn- redo-changes-need-wasm-object-sync?
|
|
[redo-changes]
|
|
(some #(contains? wasm-structural-change-types (:type %)) redo-changes))
|
|
|
|
(defn- sync-wasm-structural-changes
|
|
[{:keys [redo-changes]}]
|
|
(ptk/reify ::sync-wasm-structural-changes
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(when (wasm.api/initialized?)
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shapes
|
|
(into []
|
|
(keep (fn [{:keys [type id parent-id]}]
|
|
(when (contains? wasm-structural-change-types type)
|
|
(get objects (case type
|
|
:add-obj id
|
|
:mov-objects parent-id)))))
|
|
redo-changes)]
|
|
|
|
(wasm.api/process-objects shapes)
|
|
(wasm.api/request-render "sync-wasm-structural-changes"))))))
|
|
|
|
(defn- apply-changes-localy
|
|
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
|
(ptk/reify ::apply-changes-localy
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [undo-changes
|
|
(if pending
|
|
(->> pending
|
|
(map :undo-changes)
|
|
(reverse)
|
|
(mapcat identity)
|
|
(vec))
|
|
nil)
|
|
|
|
redo-changes
|
|
(if pending
|
|
(into redo-changes
|
|
(mapcat :redo-changes)
|
|
pending)
|
|
redo-changes)
|
|
|
|
apply-changes
|
|
(fn [fdata]
|
|
(let [fdata (cpc/process-changes fdata undo-changes false)
|
|
fdata (cpc/process-changes fdata redo-changes false)
|
|
pids (into #{} xf:map-page-id redo-changes)]
|
|
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
|
|
|
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
|
;; Update the wasm model
|
|
(let [shape-changes (js/Map.)
|
|
|
|
state
|
|
(binding [cts/*shape-changes* shape-changes]
|
|
(update-in state [:files file-id :data] apply-changes))]
|
|
|
|
(let [objects (dm/get-in state [:files file-id :data :pages-index (:current-page-id state) :objects])]
|
|
(wasm.shape/process-shape-changes! objects (es6-iterator-seq (.entries shape-changes))))
|
|
|
|
state)
|
|
|
|
;; wasm renderer deactivated
|
|
(update-in state [:files file-id :data] apply-changes))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
;; `:add-obj` / `:mov-objects` are not tracked via `*shape-changes*`.
|
|
;; Emit only for structural commits, after file data is in state.
|
|
(when (and (not ignore-wasm?)
|
|
(features/active-feature? state "render-wasm/v1")
|
|
(redo-changes-need-wasm-object-sync? redo-changes))
|
|
(rx/of (sync-wasm-structural-changes {:redo-changes redo-changes}))))))
|
|
|
|
(defn commit
|
|
"Create a commit event instance"
|
|
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
|
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?
|
|
selected-before translation? skip-component-sync? undo-index]}]
|
|
|
|
(assert (cpc/check-changes redo-changes)
|
|
"expect valid vector of changes for redo-changes")
|
|
|
|
(assert (cpc/check-changes undo-changes)
|
|
"expect valid vector of changes for undo-changes")
|
|
|
|
(let [commit-id (or commit-id (uuid/next))
|
|
source (d/nilv source :local)
|
|
local? (= source :local)
|
|
commit {:id commit-id
|
|
:created-at (ct/now)
|
|
:source source
|
|
:origin (ptk/type origin)
|
|
:features features
|
|
:file-id file-id
|
|
:file-revn file-revn
|
|
:file-vern file-vern
|
|
:changes redo-changes
|
|
:redo-changes redo-changes
|
|
:undo-changes undo-changes
|
|
:save-undo? save-undo?
|
|
:undo-group undo-group
|
|
:tags tags
|
|
:stack-undo? stack-undo?
|
|
:ignore-wasm? ignore-wasm?
|
|
:selected-before selected-before
|
|
:translation? translation?
|
|
:skip-component-sync? skip-component-sync?
|
|
;; Undo history index an undo or redo commit moved away from
|
|
:undo-index undo-index}]
|
|
|
|
(ptk/reify ::commit
|
|
cljs.core/IDeref
|
|
(-deref [_] commit)
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [pending (when-not local?
|
|
(not-empty (pcm/pending-commits (:persistence state) file-id)))]
|
|
(rx/concat
|
|
(rx/of (apply-changes-localy commit pending))
|
|
(if pending
|
|
(rx/concat
|
|
(->> (rx/from (reverse pending))
|
|
(rx/map (fn [commit] (update-indexes commit :undo-changes))))
|
|
(rx/of (update-indexes commit :redo-changes))
|
|
(->> (rx/from pending)
|
|
(rx/map (fn [commit] (update-indexes commit :redo-changes)))))
|
|
(rx/of (update-indexes commit :redo-changes)))))))))
|
|
|
|
(defn rebase-commits
|
|
"Undoes `undo`, applies `remote` and redoes `redo` on the local data of
|
|
`file-id`, commits oldest first. Nothing is saved or added to the undo
|
|
history; if the changes fail to apply, the file is reloaded.
|
|
|
|
Not a commit, so commit watchers miss it: once it has run, its deref
|
|
gives `{:file-id :changes :failed}` (`:failed` true when the file
|
|
reloads instead)."
|
|
[file-id {:keys [undo remote redo]}]
|
|
(let [commit {:file-id file-id
|
|
:redo-changes (-> []
|
|
(into (mapcat :undo-changes) (reverse undo))
|
|
(into remote)
|
|
(into (mapcat :redo-changes) redo))}
|
|
event (apply-changes-localy commit nil)
|
|
failed? (volatile! false)]
|
|
(ptk/reify ::rebase-commits
|
|
IDeref
|
|
(-deref [_]
|
|
{:file-id file-id
|
|
:changes (:redo-changes commit)
|
|
:failed @failed?})
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (dsh/lookup-file-data state file-id)
|
|
(try
|
|
(ptk/update event state)
|
|
(catch :default cause
|
|
(log/error :hint "unable to rebase commits" :file-id (str file-id) :cause cause)
|
|
(vreset! failed? true)
|
|
state))
|
|
state))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(cond
|
|
@failed?
|
|
(rx/of (ptk/event ::dw/reload-current-file))
|
|
|
|
(dsh/lookup-file-data state file-id)
|
|
(rx/merge
|
|
(or (ptk/watch event state stream) (rx/empty))
|
|
(rx/of (update-indexes commit :redo-changes)))
|
|
|
|
:else
|
|
(rx/empty))))))
|
|
|
|
(defn- resolve-file-revn
|
|
[state file-id]
|
|
(:revn (dsh/lookup-file state file-id)))
|
|
|
|
(defn- resolve-file-vern
|
|
[state file-id]
|
|
(:vern (dsh/lookup-file state file-id)))
|
|
|
|
(defn commit-changes
|
|
"Schedules a list of changes to execute now, and add the corresponding undo changes to
|
|
the undo stack.
|
|
|
|
Options:
|
|
- save-undo?: if set to false, do not add undo changes.
|
|
- undo-group: if some consecutive changes (or even transactions) share the same
|
|
undo-group, they will be undone or redone in a single step. Without
|
|
it, a commit made while an undo transaction is open joins the
|
|
transaction's group, and any other gets a new one.
|
|
- ignore-save-lock: commit even while the save-failure lock holds. Only for
|
|
commits that are not user edits: the undo of rolled-back
|
|
commits and data derived from commits already made.
|
|
"
|
|
[{:keys [redo-changes undo-changes save-undo? undo-group tags stack-undo? file-id
|
|
translation? skip-component-sync? ignore-save-lock]
|
|
:or {save-undo? true
|
|
stack-undo? false
|
|
tags #{}}
|
|
:as params}]
|
|
(ptk/reify ::commit-changes
|
|
ev/PerformanceEvent
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (or file-id (:current-file-id state))
|
|
uchg (vec undo-changes)
|
|
rchg (vec redo-changes)
|
|
features (get state :features)
|
|
permissions (get state :permissions)]
|
|
|
|
;; Historical previews must not create edits to the live file. Check
|
|
;; this when creating commits so previously queued edits can still save.
|
|
;; While saves fail, a user edit would queue behind the failed save.
|
|
;; Refusals answer with an empty stream (never nil) so callers can
|
|
;; uniformly subscribe and observe termination.
|
|
(if (and (:can-edit permissions)
|
|
(not (dm/get-in state [:workspace-global :preview-id]))
|
|
(or ignore-save-lock (not (plk/locked? state))))
|
|
(do
|
|
(log/trace :hint "commit-changes" :redo-changes redo-changes)
|
|
(let [selected (dm/get-in state [:workspace-local :selected])
|
|
tx (dm/get-in state [:workspace-undo :transaction])
|
|
;; Inside an undo transaction, the group of its history entry
|
|
;; (taken from its first commit), or the one it was opened with
|
|
undo-group (or undo-group (:undo-group tx) (:commit-group tx) (uuid/next))]
|
|
(rx/of (-> params
|
|
(assoc :undo-group undo-group)
|
|
(assoc :features features)
|
|
(assoc :tags tags)
|
|
(assoc :stack-undo? stack-undo?)
|
|
(assoc :save-undo? save-undo?)
|
|
(assoc :file-id file-id)
|
|
(assoc :file-revn (resolve-file-revn state file-id))
|
|
(assoc :file-vern (resolve-file-vern state file-id))
|
|
(assoc :undo-changes uchg)
|
|
(assoc :redo-changes rchg)
|
|
(assoc :selected-before selected)
|
|
(assoc :translation? translation?)
|
|
(assoc :skip-component-sync? skip-component-sync?)
|
|
(commit)))))
|
|
(rx/empty))))))
|