mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 22:10:34 +00:00
🐛 Fix small save bugs found by the rollback test plan
- Ending a drag inside the frame the shapes already belong to sent an empty reparent save and bumped the revision. The move commits only when some shape changes parent. - When a user restored a version, other sessions reloaded the file with no notice. They now see a dialog naming who restored it. - Exports drawn in the browser (PDF, SVG, and PNG when the WASM renderer draws it) and duplicating a file from the dashboard started at once, so an export could show changes a pending save then rejected, and a copy could miss queued edits. Both wait for the file's pending saves. A client export warns that it includes unsaved changes; a copy warns that it misses them.
This commit is contained in:
parent
46209cef13
commit
b0fa06741e
@ -36,6 +36,7 @@
|
||||
- `save-request-timeout-ms` is 7 minutes and wraps the `update-file` request in `rx/timeout`, failing it as a transient `:network` error so the retry path resends instead of blocking the queue forever. It is deliberately longer than `saving-stall-timeout-ms` (5 min) so the stalled report fires before the retry.
|
||||
- `sync-unload-guard` keeps `:persistence` in `st/ongoing-tasks` while edits are buffered or queued, which is what holds the browser leave-page warning on. It reads `[:persistence :buffer]` and `:queue` and syncs on `::buffer-commit`, `::append-commit`, `::discard-commit` and `::release-failed-file` (every buffer and queue change). It is the one effect that must stay in `store.cljs`'s `omitset`, and `store.cljs`'s `ongoing-tasks` watcher is guarded by `exists? js/window` because tests run without a window.
|
||||
- `leave-signals` (document `visibilitychange`→hidden, window `blur`) emits `persist-buffered`, which forces a save when `[:persistence :buffer]` is not empty, so leaving the tab/window sends the 3 s batch at once; tests mock `dps/leave-signals` BEFORE `initialize-persistence` runs. pagehide/beforeunload are too late for an async save.
|
||||
- Exports (exporter and client/WASM PDF/SVG/PNG alike), the dashboard `duplicate-file` and the `.penpot` download call `force-persist-files-and-wait file-ids timeout` first. A client export draws local data, so its warning says the export includes unsaved changes (`workspace.export.unsaved-changes-included`); exporter exports and the duplicate copy server data and warn they miss them.
|
||||
- Exports and the `.penpot` download call `force-persist-files-and-wait file-ids timeout` (waits only for those files, ends on `:error` or timeout, never raises) and then `unsaved-changes? state file-ids` to warn; it reads `refs/persistence`, so tests mock it with `(l/derived :persistence store)`.
|
||||
- `persistence-failed` emits a `save-failed` audit event (`:cause-type :code :retrying :queued-commits`) whenever the user is notified (`notify-failure?`, same rule as the toast), including delegated auth failures.
|
||||
- `start-undo-transaction` stamps the transaction with a fresh `:commit-group` (dropped from the history entry); `commit-changes` uses `(or undo-group (:undo-group tx) (:commit-group tx) (uuid/next))`, so all commits of one transaction are one rollback unit. Pre-setting `:undo-group` instead breaks entries whose group is set later (sync transactions).
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.repo :as rp]
|
||||
@ -529,6 +530,21 @@
|
||||
|
||||
;; --- EVENT: duplicate-file
|
||||
|
||||
;; Longest wait for the pending changes of a file to be saved before copying it.
|
||||
(def ^:private save-wait-timeout-ms 10000)
|
||||
|
||||
(defn- warn-unsaved-copy
|
||||
"Warns that the copy of `file-id` misses the changes not saved yet."
|
||||
[file-id]
|
||||
(ptk/reify ::warn-unsaved-copy
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (dps/unsaved-changes? state #{file-id})
|
||||
(rx/of (ntf/show {:content (tr "dashboard.duplicate-unsaved-changes")
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 8000}))))))
|
||||
|
||||
(defn duplicate-file
|
||||
[{:keys [id name] :as params}]
|
||||
(dm/assert! (uuid? id))
|
||||
@ -546,10 +562,14 @@
|
||||
(when (> copy-count 1)
|
||||
(str " " copy-count))))
|
||||
new-name (cfh/generate-unique-name name unames :suffix-fn suffix-fn)]
|
||||
(->> (rp/cmd! :duplicate-file {:file-id id :name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map file-created)
|
||||
(rx/catch on-error))))))
|
||||
;; The copy is made from the server's data: send the pending changes first.
|
||||
(rx/concat
|
||||
(dps/force-persist-files-and-wait #{id} save-wait-timeout-ms)
|
||||
(rx/of (warn-unsaved-copy id))
|
||||
(->> (rp/cmd! :duplicate-file {:file-id id :name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map file-created)
|
||||
(rx/catch on-error)))))))
|
||||
|
||||
;; --- EVENT: move-files
|
||||
|
||||
|
||||
@ -36,13 +36,13 @@
|
||||
(into #{} (keep :file-id) exports))
|
||||
|
||||
(defn- warn-unsaved-changes
|
||||
"Warns that the export may miss the changes of `file-ids` not saved yet."
|
||||
[file-ids]
|
||||
"Shows `message` when some changes of `file-ids` are not saved yet."
|
||||
[file-ids message]
|
||||
(ptk/reify ::warn-unsaved-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (dwp/unsaved-changes? state file-ids)
|
||||
(rx/of (ntf/show {:content (tr "workspace.export.unsaved-changes")
|
||||
(rx/of (ntf/show {:content message
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 8000}))))))
|
||||
@ -339,8 +339,13 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(case @target
|
||||
;; Drawn from the local data, which a rejected save may still change
|
||||
:client
|
||||
(rx/of (request-simple-export-wasm export))
|
||||
(rx/concat
|
||||
(dwp/force-persist-files-and-wait (export-file-ids [export]) save-wait-timeout-ms)
|
||||
(rx/of (warn-unsaved-changes (export-file-ids [export])
|
||||
(tr "workspace.export.unsaved-changes-included"))
|
||||
(request-simple-export-wasm export)))
|
||||
|
||||
:too-large
|
||||
(rx/of (ntf/error (tr "errors.export-too-large")))
|
||||
@ -353,7 +358,8 @@
|
||||
:is-wasm (wasm-export-enabled? state)})]
|
||||
(rx/concat
|
||||
(dwp/force-persist-files-and-wait (export-file-ids [export]) save-wait-timeout-ms)
|
||||
(rx/of (warn-unsaved-changes (export-file-ids [export])))
|
||||
(rx/of (warn-unsaved-changes (export-file-ids [export])
|
||||
(tr "workspace.export.unsaved-changes")))
|
||||
|
||||
(->> (rp/cmd! :export params)
|
||||
(rx/map (fn [{:keys [filename mtype uri]}]
|
||||
@ -407,7 +413,7 @@
|
||||
|
||||
(rx/concat
|
||||
(dwp/force-persist-files-and-wait file-ids save-wait-timeout-ms)
|
||||
(rx/of (warn-unsaved-changes file-ids))
|
||||
(rx/of (warn-unsaved-changes file-ids (tr "workspace.export.unsaved-changes")))
|
||||
(rx/merge
|
||||
;; Updates the export state with each progress message, until
|
||||
;; shortly after the export ends or fails.
|
||||
|
||||
@ -307,8 +307,16 @@
|
||||
(def ^:private check-file-restored-params
|
||||
(sm/check-fn schema:handle-file-restored))
|
||||
|
||||
(defn- restorer-fullname
|
||||
"Name of the user whose session restored the file, when known."
|
||||
[state session-id]
|
||||
(when-let [profile-id (dm/get-in state [:workspace-presence session-id :profile-id])]
|
||||
(dm/get-in state [:profiles profile-id :fullname])))
|
||||
|
||||
(defn handle-file-restored
|
||||
[{:keys [file-id vern] :as msg}]
|
||||
"Reloads the file a peer restored to an earlier version and tells the
|
||||
user why it changed."
|
||||
[{:keys [file-id vern session-id] :as msg}]
|
||||
|
||||
(assert (check-file-restored-params msg)
|
||||
"expected valid parameters")
|
||||
@ -320,9 +328,12 @@
|
||||
file (dsh/lookup-file state curr-file-id)
|
||||
curr-vern (:vern file)]
|
||||
|
||||
(when (and (= file-id curr-file-id)
|
||||
(not= vern curr-vern))
|
||||
(rx/of (ptk/event ::dw/reload-current-file)))))))
|
||||
(if (and (= file-id curr-file-id)
|
||||
(not= vern curr-vern))
|
||||
(rx/of (ptk/event ::dw/reload-current-file)
|
||||
(modal/show :file-restored
|
||||
{:fullname (restorer-fullname state session-id)}))
|
||||
(rx/empty))))))
|
||||
|
||||
(def ^:private schema:handle-library-change
|
||||
[:map {:title "handle-library-change"}
|
||||
|
||||
@ -1511,7 +1511,8 @@
|
||||
:element-type (cfh/get-selected-type objects ids)
|
||||
:moved moved-count}))))
|
||||
|
||||
(when (and (some? frame-id) (d/not-empty? changes))
|
||||
;; Shapes that already belong to the frame leave nothing to move
|
||||
(when (and (some? frame-id) (seq ids))
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwc/expand-collapse frame-id)))
|
||||
(when add-component-to-variant?
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
[app.main.ui.workspace.components-debugger :refer [components-debugger*]]
|
||||
[app.main.ui.workspace.context-menu :refer [context-menu*]]
|
||||
[app.main.ui.workspace.coordinates :as coordinates]
|
||||
[app.main.ui.workspace.file-restored-modal]
|
||||
[app.main.ui.workspace.libraries]
|
||||
[app.main.ui.workspace.nudge]
|
||||
[app.main.ui.workspace.palette :refer [palette*]]
|
||||
|
||||
44
frontend/src/app/main/ui/workspace/file_restored_modal.cljs
Normal file
44
frontend/src/app/main/ui/workspace/file_restored_modal.cljs
Normal file
@ -0,0 +1,44 @@
|
||||
;; 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.ui.workspace.file-restored-modal
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
|
||||
(mf/defc file-restored-modal*
|
||||
"Tells a peer that another user restored an earlier version of the open
|
||||
file, which was reloaded."
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :file-restored}
|
||||
[{:keys [fullname]}]
|
||||
(let [on-close (mf/use-fn #(st/emit! (modal/hide)))]
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)
|
||||
:role "dialog"
|
||||
:aria-modal true
|
||||
:aria-labelledby "file-restored-title"}
|
||||
[:> heading* {:id "file-restored-title"
|
||||
:level 2
|
||||
:typography t/title-medium
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "workspace.file-restored.title")]
|
||||
[:> text* {:as "p" :typography t/body-large}
|
||||
(if fullname
|
||||
(tr "workspace.file-restored.message" fullname)
|
||||
(tr "workspace.file-restored.message-anonymous"))]
|
||||
[:footer {:class (stl/css :modal-footer)}
|
||||
[:> button* {:on-click on-close
|
||||
:variant "primary"}
|
||||
(tr "ds.confirm-ok")]]]]))
|
||||
34
frontend/src/app/main/ui/workspace/file_restored_modal.scss
Normal file
34
frontend/src/app/main/ui/workspace/file_restored_modal.scss
Normal file
@ -0,0 +1,34 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend %modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend %modal-container-base;
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
max-width: px2rem(482); // FIXME: This should be a token
|
||||
|
||||
& > * {
|
||||
margin: 0; // FIXME: This should be in reset styles
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-block-start: var(--sp-m);
|
||||
justify-self: end;
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
@ -172,3 +173,28 @@
|
||||
(await (async/observe (ptk/watch (dd/initialize team-id) state stream)
|
||||
:on-next #(swap! processed conj (ptk/type %))))))
|
||||
(t/is (= 1 (count (filter #{::dcm/handle-organization-change-sso} @processed))))))
|
||||
|
||||
;; Scenario: the user duplicates a file whose workspace still has a change
|
||||
;; waiting to be saved. Proves: the copy is requested once the save is done,
|
||||
;; and the user is told the copy misses what could not be saved.
|
||||
(t/deftest ^:async duplicating-a-file-waits-for-its-pending-changes
|
||||
(let [file-id (uuid/next)
|
||||
commit-id (uuid/next)
|
||||
calls (atom [])
|
||||
store (ptk/store {:state {:files {}
|
||||
:persistence {:queue (conj #queue [] commit-id)
|
||||
:index {commit-id {:id commit-id :file-id file-id}}}}})]
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{dps/force-persist-files-and-wait (mock/stub (fn [file-ids _]
|
||||
(swap! calls conj [:wait file-ids])
|
||||
(rx/empty)))
|
||||
rp/cmd! (mock/stub (fn [cmd _]
|
||||
(swap! calls conj [cmd])
|
||||
(rx/of {:id (uuid/next)})))
|
||||
i18n/tr (mock/stub #(str "translated:" %))}
|
||||
(ptk/emit! store (dd/duplicate-file {:id file-id :name "File"}))
|
||||
(await (async/wait-for #(= 2 (count @calls)) "the copy is requested"))
|
||||
(t/is (= [[:wait #{file-id}] [:duplicate-file]] @calls))
|
||||
(t/is (= "translated:dashboard.duplicate-unsaved-changes"
|
||||
(get-in @store [:notification :content])))))))
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.exports.assets :as de]
|
||||
[app.main.data.exports.wasm :as wasm.exports]
|
||||
[app.main.data.persistence :as dwp]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.repo :as repo]
|
||||
@ -203,3 +204,39 @@
|
||||
(await (async/wait-for #(true? @downloaded) "the export runs"))
|
||||
(await (async/settle))
|
||||
(t/is (nil? (:notification @store)))))))
|
||||
|
||||
;; Scenario: a PDF is drawn in the browser while its file has a change not
|
||||
;; saved yet. Proves: the export waits for the save first, and the user is
|
||||
;; told the export includes changes the server does not have.
|
||||
(t/deftest ^:async client-export-waits-for-the-save-and-warns-of-unsaved-changes
|
||||
(let [file-id (uuid/next)
|
||||
calls (atom [])
|
||||
store (export-store (assoc (unsaved-state file-id) :features #{"render-wasm/v1"})
|
||||
(atom []))]
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{dwp/force-persist-files-and-wait (mock/stub (fn [file-ids _]
|
||||
(swap! calls conj [:wait file-ids])
|
||||
(rx/empty)))
|
||||
wasm.exports/export-pdf (mock/stub (fn [_] (swap! calls conj [:export])))
|
||||
i18n/tr (mock/stub #(str "translated:" %))}
|
||||
(ptk/emit! store (de/request-simple-export {:export (assoc export :file-id file-id :type :pdf)}))
|
||||
(await (async/wait-for #(= 2 (count @calls)) "the export runs"))
|
||||
(t/is (= [[:wait #{file-id}] [:export]] @calls)
|
||||
"the export starts once the save is done")
|
||||
(t/is (= "translated:workspace.export.unsaved-changes-included"
|
||||
(get-in @store [:notification :content])))))))
|
||||
|
||||
;; Scenario: a PDF is drawn in the browser with every change saved. Proves:
|
||||
;; no warning is shown.
|
||||
(t/deftest ^:async client-export-shows-no-warning-when-the-file-is-saved
|
||||
(let [exported (atom false)
|
||||
store (export-store (assoc (test-state) :features #{"render-wasm/v1"}) (atom []))]
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{dwp/force-persist-files-and-wait (mock/stub (fn [_ _] (rx/empty)))
|
||||
wasm.exports/export-pdf (mock/stub (fn [_] (reset! exported true)))}
|
||||
(ptk/emit! store (de/request-simple-export {:export (assoc export :file-id (uuid/next) :type :pdf)}))
|
||||
(await (async/wait-for #(true? @exported) "the export runs"))
|
||||
(await (async/settle))
|
||||
(t/is (nil? (:notification @store)))))))
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.data.workspace.notifications :as dwntf]
|
||||
[app.util.i18n :as i18n]
|
||||
[beicon.v2.core :as rx]
|
||||
@ -50,3 +52,62 @@
|
||||
(t/deftest ^:async ordinary-file-change-does-not-notify
|
||||
(let [events (await (watch-file-change (file-change-msg {})))]
|
||||
(t/is (= [::dch/commit] (mapv ptk/type events)))))
|
||||
|
||||
(defn- restored-events
|
||||
"Events the watch of `handle-file-restored` emits for `msg` on `state`."
|
||||
[state msg]
|
||||
(let [events (atom [])]
|
||||
(-> (async/observe (ptk/watch (dwntf/handle-file-restored msg) state nil)
|
||||
:on-next #(swap! events conj %))
|
||||
(.then (fn [_] @events)))))
|
||||
|
||||
(defn- shown-modal
|
||||
[events]
|
||||
(some #(when (= ::modal/show-modal (ptk/type %))
|
||||
(::modal/modal (ptk/update % {})))
|
||||
events))
|
||||
|
||||
;; Scenario: a peer restores an earlier version of the open file. The file
|
||||
;; reloads and a modal names the peer. Proves: the reload is not silent.
|
||||
(t/deftest ^:async a-peer-restore-reloads-and-names-the-peer
|
||||
(let [file-id (uuid/next)
|
||||
session-id (uuid/next)
|
||||
profile-id (uuid/next)
|
||||
state {:current-file-id file-id
|
||||
:files {file-id {:id file-id :vern 1}}
|
||||
:workspace-presence {session-id {:profile-id profile-id}}
|
||||
:profiles {profile-id {:id profile-id :fullname "Ada"}}}
|
||||
events (await (restored-events state {:type :file-restored
|
||||
:file-id file-id
|
||||
:session-id session-id
|
||||
:vern 2}))
|
||||
modal (shown-modal events)]
|
||||
(t/is (some #(= ::dw/reload-current-file (ptk/type %)) events))
|
||||
(t/is (= :file-restored (:type modal)))
|
||||
(t/is (= "Ada" (get-in modal [:props :fullname])))))
|
||||
|
||||
;; Scenario: the restoring session left before its message arrived, so it
|
||||
;; has no presence. Proves: the modal still shows, without a name.
|
||||
(t/deftest ^:async a-restore-from-an-unknown-session-shows-no-name
|
||||
(let [file-id (uuid/next)
|
||||
state {:current-file-id file-id
|
||||
:files {file-id {:id file-id :vern 1}}}
|
||||
events (await (restored-events state {:type :file-restored
|
||||
:file-id file-id
|
||||
:session-id (uuid/next)
|
||||
:vern 2}))
|
||||
modal (shown-modal events)]
|
||||
(t/is (= :file-restored (:type modal)))
|
||||
(t/is (nil? (get-in modal [:props :fullname])))))
|
||||
|
||||
;; Scenario: the file already shows the restored version. Proves: no reload
|
||||
;; and no modal for a version the user already sees.
|
||||
(t/deftest ^:async a-restore-of-the-loaded-version-does-nothing
|
||||
(let [file-id (uuid/next)
|
||||
state {:current-file-id file-id
|
||||
:files {file-id {:id file-id :vern 2}}}
|
||||
events (await (restored-events state {:type :file-restored
|
||||
:file-id file-id
|
||||
:session-id (uuid/next)
|
||||
:vern 2}))]
|
||||
(t/is (empty? events))))
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
;; 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 frontend-tests.logic.move-shapes-to-frame-test
|
||||
"Tests for the reparenting step that ends a move (`dwt/move-shapes-to-frame`)."
|
||||
(:require
|
||||
[app.common.test-helpers.compositions :as ctho]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each {:before cthi/reset-idmap!})
|
||||
|
||||
(defn- count-commits
|
||||
"Runs `event` on a store of `file` and calls `check` with the number of
|
||||
commits it made and the resulting file."
|
||||
[done file event check]
|
||||
(let [store (ths/setup-store file)
|
||||
commits (atom 0)
|
||||
sub (->> (ptk/input-stream store)
|
||||
(rx/filter dch/commit?)
|
||||
(rx/subs! (fn [_] (swap! commits inc))))]
|
||||
(ths/run-store store done [event]
|
||||
(fn [state]
|
||||
(rx/dispose! sub)
|
||||
(check @commits (ths/get-file-from-state state))))))
|
||||
|
||||
;; Scenario: a move ends over the frame the shape already belongs to.
|
||||
;; Proves: nothing changes parent, so no commit (and no save) is made.
|
||||
(t/deftest moving-a-shape-within-its-frame-commits-nothing
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1))
|
||||
rect (cths/get-shape file :rect1)]
|
||||
(count-commits done file
|
||||
(dwt/move-shapes-to-frame [(:id rect)] uuid/zero nil nil)
|
||||
(fn [commits _]
|
||||
(t/is (zero? commits)))))))
|
||||
|
||||
;; Scenario: a move ends over another board. Proves: the shape is moved into
|
||||
;; it with one commit.
|
||||
(t/deftest moving-a-shape-into-a-board-commits-the-new-parent
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-frame :frame1)
|
||||
(ctho/add-rect :rect1))
|
||||
rect (cths/get-shape file :rect1)
|
||||
frame (cths/get-shape file :frame1)]
|
||||
(count-commits done file
|
||||
(dwt/move-shapes-to-frame [(:id rect)] (:id frame) nil nil)
|
||||
(fn [commits file']
|
||||
(t/is (= 1 commits))
|
||||
(t/is (= (:id frame) (:parent-id (cths/get-shape file' :rect1)))))))))
|
||||
@ -34,6 +34,7 @@
|
||||
[frontend-tests.data.workspace-libraries-sync-test]
|
||||
[frontend-tests.data.workspace-mcp-test]
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
[frontend-tests.data.workspace-notifications-test]
|
||||
[frontend-tests.data.workspace-pages-test]
|
||||
[frontend-tests.data.workspace-path-edition-test]
|
||||
[frontend-tests.data.workspace-reflow-test]
|
||||
@ -55,6 +56,7 @@
|
||||
[frontend-tests.logic.copying-and-duplicating-test]
|
||||
[frontend-tests.logic.frame-guides-test]
|
||||
[frontend-tests.logic.groups-test]
|
||||
[frontend-tests.logic.move-shapes-to-frame-test]
|
||||
[frontend-tests.logic.nudge-selected-shapes-test]
|
||||
[frontend-tests.logic.pasting-in-containers-test]
|
||||
[frontend-tests.logic.path-actions-test]
|
||||
@ -190,6 +192,7 @@
|
||||
'frontend-tests.data.workspace-libraries-sync-test
|
||||
'frontend-tests.data.workspace-mcp-test
|
||||
'frontend-tests.data.workspace-media-test
|
||||
'frontend-tests.data.workspace-notifications-test
|
||||
'frontend-tests.data.workspace-pages-test
|
||||
'frontend-tests.data.workspace-path-edition-test
|
||||
'frontend-tests.data.workspace-reflow-test
|
||||
@ -211,6 +214,7 @@
|
||||
'frontend-tests.logic.copying-and-duplicating-test
|
||||
'frontend-tests.logic.frame-guides-test
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.logic.move-shapes-to-frame-test
|
||||
'frontend-tests.logic.nudge-selected-shapes-test
|
||||
'frontend-tests.logic.path-actions-test
|
||||
'frontend-tests.logic.path-clipboard-test
|
||||
|
||||
@ -437,6 +437,10 @@ msgstr "Duplicate"
|
||||
msgid "dashboard.duplicate-multi"
|
||||
msgstr "Duplicate %s files"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs
|
||||
msgid "dashboard.duplicate-unsaved-changes"
|
||||
msgstr "Some of your latest changes are not saved yet, so the copy will not include them."
|
||||
|
||||
#: src/app/main/ui/dashboard/files.cljs:233, src/app/main/ui/dashboard/projects.cljs:309
|
||||
msgid "dashboard.empty-placeholder-drafts-subtitle"
|
||||
msgstr "Once a project member creates a draft, it will be displayed here."
|
||||
@ -6450,6 +6454,22 @@ msgstr "Debugging tools"
|
||||
msgid "workspace.export.unsaved-changes"
|
||||
msgstr "Some of your latest changes are not saved yet, so this export may not include them."
|
||||
|
||||
#: src/app/main/data/exports/assets.cljs
|
||||
msgid "workspace.export.unsaved-changes-included"
|
||||
msgstr "Some of your latest changes are not saved yet. This export includes them."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.message"
|
||||
msgstr "%s restored an earlier version of this file. You are now seeing that version."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.message-anonymous"
|
||||
msgstr "A teammate restored an earlier version of this file. You are now seeing that version."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.title"
|
||||
msgstr "This file was restored"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs:907
|
||||
msgid "workspace.focus.focus-mode"
|
||||
msgstr "Focus mode"
|
||||
|
||||
@ -445,6 +445,10 @@ msgstr "Duplicar"
|
||||
msgid "dashboard.duplicate-multi"
|
||||
msgstr "Duplicar %s archivos"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs
|
||||
msgid "dashboard.duplicate-unsaved-changes"
|
||||
msgstr "Algunos de tus últimos cambios aún no se han guardado, así que la copia no los incluirá."
|
||||
|
||||
#: src/app/main/ui/dashboard/files.cljs:233, src/app/main/ui/dashboard/projects.cljs:309
|
||||
msgid "dashboard.empty-placeholder-drafts-subtitle"
|
||||
msgstr "Cuando un miembro del equipo cree algún borrador, este aparecerá aquí."
|
||||
@ -6516,6 +6520,22 @@ msgstr "Herramientas de depuración"
|
||||
msgid "workspace.export.unsaved-changes"
|
||||
msgstr "Algunos de tus últimos cambios aún no se han guardado, así que puede que esta exportación no los incluya."
|
||||
|
||||
#: src/app/main/data/exports/assets.cljs
|
||||
msgid "workspace.export.unsaved-changes-included"
|
||||
msgstr "Algunos de tus últimos cambios aún no se han guardado. Esta exportación los incluye."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.message"
|
||||
msgstr "%s ha restaurado una versión anterior de este archivo. Ahora ves esa versión."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.message-anonymous"
|
||||
msgstr "Un miembro del equipo ha restaurado una versión anterior de este archivo. Ahora ves esa versión."
|
||||
|
||||
#: src/app/main/ui/workspace/file_restored_modal.cljs
|
||||
msgid "workspace.file-restored.title"
|
||||
msgstr "Se ha restaurado este archivo"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs:907
|
||||
msgid "workspace.focus.focus-mode"
|
||||
msgstr "Modo foco"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user