🐛 Block grid track duplication and paste in component copies

This commit is contained in:
Pablo Alba 2026-11-11 12:36:23 +01:00 committed by Pablo Alba
parent 3eebe92606
commit bb672a3723
3 changed files with 223 additions and 46 deletions

View File

@ -436,6 +436,14 @@
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))
(defn grid-in-copy?
"Check if the grid is a component copy or lives inside one. Copies
must keep the structure of their main, so tracks that add shapes are
not allowed there."
[objects grid]
(or (ctc/in-component-copy? grid)
(ctn/has-any-copy-parent? objects grid)))
(defn duplicate-layout-track
[ids type index]
(assert (#{:row :column} type))
@ -443,53 +451,53 @@
(ptk/reify ::duplicate-layout-track
ptk/WatchEvent
(watch [it state _]
(let [file-id (:current-file-id state)
page (dsh/lookup-page state)
objects (:objects page)
libraries (dsh/lookup-libraries state)
library-data (dsh/lookup-file state file-id)
shape-id (first ids)
base-shape (get objects shape-id)
(let [objects (dsh/lookup-page-objects state)
base-shape (get objects (first ids))]
(when-not (grid-in-copy? objects base-shape)
(let [file-id (:current-file-id state)
page (dsh/lookup-page state)
libraries (dsh/lookup-libraries state)
library-data (dsh/lookup-file state file-id)
shapes-by-track
(if (= type :column)
(ctl/shapes-by-column base-shape index false)
(ctl/shapes-by-row base-shape index false))
shapes-by-track
(if (= type :column)
(ctl/shapes-by-column base-shape index false)
(ctl/shapes-by-row base-shape index false))
;; Change to set in order to use auxiliary functions
selected (set shapes-by-track)
;; Change to set in order to use auxiliary functions
selected (set shapes-by-track)
changes
(-> (pcb/empty-changes it)
(cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id)
(cll/generate-duplicate-changes-update-indices objects selected))
changes
(-> (pcb/empty-changes it)
(cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id)
(cll/generate-duplicate-changes-update-indices objects selected))
;; Creates a map with shape-id => duplicated-shape-id
ids-map
(->> changes
:redo-changes
(filter #(= (:type %) :add-obj))
(filter #(selected (:old-id %)))
(map #(vector (:old-id %) (get-in % [:obj :id])))
(into {}))
;; Creates a map with shape-id => duplicated-shape-id
ids-map
(->> changes
:redo-changes
(filter #(= (:type %) :add-obj))
(filter #(selected (:old-id %)))
(map #(vector (:old-id %) (get-in % [:obj :id])))
(into {}))
changes
(-> changes
(pcb/update-shapes
ids
(fn [shape objects]
;; The duplication could have altered the grid so we restore the values, we'll calculate the good ones now
(let [shape (merge shape (select-keys base-shape [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
(case type
:row (ctl/duplicate-row shape objects index ids-map)
:column (ctl/duplicate-column shape objects index ids-map))))
{:with-objects? true}))
changes
(-> changes
(pcb/update-shapes
ids
(fn [shape objects]
;; The duplication could have altered the grid so we restore the values, we'll calculate the good ones now
(let [shape (merge shape (select-keys base-shape [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
(case type
:row (ctl/duplicate-row shape objects index ids-map)
:column (ctl/duplicate-column shape objects index ids-map))))
{:with-objects? true}))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))))
(defn reorder-layout-track
[ids type from-index to-index move-content?]
@ -918,7 +926,9 @@
type (:type copied)
undo-id (js/Symbol)]
(when (and (seq track-indices) (some? type))
(when (and (seq track-indices)
(some? type)
(not (grid-in-copy? objects grid)))
(let [shapes-by-track-fn
(if (= type :row)
ctl/shapes-by-row

View File

@ -873,6 +873,10 @@
{::mf/private true}
[{:keys [mdata]}]
(let [{:keys [type index grid-id]} mdata
objects (deref refs/workspace-page-objects)
in-copy? (dwsl/grid-in-copy? objects (get objects grid-id))
do-delete-track
(mf/use-fn
(mf/deps grid-id type index)
@ -905,14 +909,14 @@
(if (= type :column)
[:*
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.duplicate") :on-click do-duplicate-track}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.duplicate") :on-click do-duplicate-track :disabled in-copy?}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.add-before") :on-click do-add-track-before}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.add-after") :on-click do-add-track-after}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.delete") :on-click do-delete-track}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.column.delete-shapes") :on-click do-delete-track-shapes}]]
[:*
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.duplicate") :on-click do-duplicate-track}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.duplicate") :on-click do-duplicate-track :disabled in-copy?}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.add-before") :on-click do-add-track-before}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.add-after") :on-click do-add-track-after}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.delete") :on-click do-delete-track}]
@ -949,6 +953,9 @@
grid-edition (mf/deref grid-edition-ref)
has-copied-tracks? (some? (:copied-tracks grid-edition))
objects (deref refs/workspace-page-objects)
in-copy? (dwsl/grid-in-copy? objects (get objects grid-id))
do-merge-cells
(mf/use-fn
(mf/deps grid-id cells)
@ -1002,7 +1009,7 @@
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.paste-tracks")
:on-click do-paste-tracks
:disabled (not has-copied-tracks?)}]]))
:disabled (or (not has-copied-tracks?) in-copy?)}]]))
(def guide-color-presets

View File

@ -12,16 +12,19 @@
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.pages :as dwp]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]))
[frontend-tests.helpers.state :as ths]
[potok.v2.core :as ptk]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
@ -653,3 +656,160 @@
"Pasted text shapes have nil position-data"))))))
done)))
;; Grid tracks in component copies
(defn- add-grid-with-child
"Adds a grid board with one rect child and assigns the child to a cell."
[file grid-label rect-label & {:keys [parent-label params]}]
(let [file (-> file
(ctho/add-frame grid-label
(merge {:name "grid"
:parent-label parent-label
:layout :grid
:layout-grid-dir :row
:layout-grid-columns []
:layout-grid-rows []
:layout-grid-cells {}}
params))
(cths/add-sample-shape rect-label :parent-label grid-label))
grid (->> (cths/get-shape file grid-label)
(#(ctl/assign-cells % (:objects (cthf/current-page file)))))]
(reduce (fn [file attr] (cths/update-shape file grid-label attr (get grid attr)))
file
[:layout-grid-columns :layout-grid-rows :layout-grid-cells])))
(defn- grid-root-file
"Main component whose root board is the grid, and a copy of it."
[]
(-> (cthf/sample-file :file1 :page-label :page-1)
(add-grid-with-child :grid-main :rect-main)
(cthc/make-component :comp :grid-main)
(cthc/instantiate-component :comp :grid-copy :children-labels [:rect-copy])))
(defn- grid-nested-file
"Main component with a wrapper board that contains the grid, and a copy."
[]
(-> (cthf/sample-file :file1 :page-label :page-1)
(ctho/add-frame :wrapper-main :name "wrapper")
(add-grid-with-child :grid-main :rect-main :parent-label :wrapper-main)
(cthc/make-component :comp :wrapper-main)
(cthc/instantiate-component :comp :wrapper-copy :children-labels [:grid-copy :rect-copy])))
(defn- grid-user-added-in-copy-file
"Copy of a wrapper board, with a plain grid (no :shape-ref) added inside
the copy. Only its parent is a copy."
[]
(let [file (-> (grid-nested-file)
(add-grid-with-child :grid-user :rect-user :parent-label :wrapper-copy))]
(assert (nil? (:shape-ref (cths/get-shape file :grid-user))))
file))
(defn- grid-variant-file
"Variant main whose root board is the grid. The root has :variant-id and
no :shape-ref."
[]
(let [file (-> (cthf/sample-file :file1 :page-label :page-1)
(cths/add-sample-shape :variant :type :frame :is-variant-container true))
variant-id (cthi/id :variant)]
(-> file
(add-grid-with-child :grid-main :rect-main
:parent-label :variant
:params {:variant-id variant-id :variant-name "Value1"})
(cthc/make-component :comp :grid-main)
(cthc/update-component :comp {:variant-id variant-id
:variant-properties [{:name "Property 1" :value "Value1"}]}))))
(defn- copied-tracks-event
[grid-id]
(ptk/reify ::set-copied-tracks
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-grid-edition grid-id :copied-tracks]
{:track-indices [0] :type :column :grid-id grid-id}))))
(defn- run-grid-track-events
[file make-events check-fn]
(t/async done
(mock/with-mocks {uuid/next cthi/next-uuid}
(fn [done']
(let [store (ths/setup-store file)
events (make-events file)]
(ths/run-store
store done' events
(fn [new-state]
(check-fn (cthf/current-page file)
(ths/get-file-from-state new-state))))))
done)))
(defn- duplicate-events
[grid-label]
(fn [file]
[(dwsl/duplicate-layout-track [(:id (cths/get-shape file grid-label))] :column 0)]))
(defn- paste-events
[grid-label]
(fn [file]
(let [grid-id (:id (cths/get-shape file grid-label))]
[(copied-tracks-event grid-id)
(dwsl/paste-grid-tracks grid-id)])))
(defn- objects-of
[file]
(:objects (cthf/current-page file)))
(defn- column-count
[file label]
(count (:layout-grid-columns (cths/get-shape file label))))
(defn- check-blocked
[file make-events grid-label]
(run-grid-track-events
file make-events
(fn [page file']
(t/is (= (:objects page) (objects-of file')))
(t/is (= (column-count file grid-label) (column-count file' grid-label))))))
(defn- check-works
[file make-events grid-label]
(run-grid-track-events
file make-events
(fn [page file']
(t/is (> (count (objects-of file')) (count (:objects page))))
(t/is (> (column-count file' grid-label) (column-count file grid-label))))))
(t/deftest duplicate-track-blocked-in-copy-root-grid
(check-blocked (grid-root-file) (duplicate-events :grid-copy) :grid-copy))
(t/deftest paste-tracks-blocked-in-copy-root-grid
(check-blocked (grid-root-file) (paste-events :grid-copy) :grid-copy))
(t/deftest duplicate-track-blocked-in-nested-copy-grid
(check-blocked (grid-nested-file) (duplicate-events :grid-copy) :grid-copy))
(t/deftest paste-tracks-blocked-in-nested-copy-grid
(check-blocked (grid-nested-file) (paste-events :grid-copy) :grid-copy))
(t/deftest duplicate-track-blocked-in-user-added-grid-inside-copy
(check-blocked (grid-user-added-in-copy-file) (duplicate-events :grid-user) :grid-user))
(t/deftest paste-tracks-blocked-in-user-added-grid-inside-copy
(check-blocked (grid-user-added-in-copy-file) (paste-events :grid-user) :grid-user))
(t/deftest duplicate-track-works-in-main-grid
(check-works (grid-root-file) (duplicate-events :grid-main) :grid-main))
(t/deftest paste-tracks-works-in-main-grid
(check-works (grid-root-file) (paste-events :grid-main) :grid-main))
(t/deftest duplicate-track-works-in-main-nested-grid
(check-works (grid-nested-file) (duplicate-events :grid-main) :grid-main))
(t/deftest paste-tracks-works-in-main-nested-grid
(check-works (grid-nested-file) (paste-events :grid-main) :grid-main))
(t/deftest duplicate-track-works-in-variant-main-grid
(check-works (grid-variant-file) (duplicate-events :grid-main) :grid-main))
(t/deftest paste-tracks-works-in-variant-main-grid
(check-works (grid-variant-file) (paste-events :grid-main) :grid-main))