Add copy and paste for grid layout rows and columns via co… (#8498)

*  Add copy and paste for grid layout rows and columns via context menu

* 🔧 Use grid-id instead of grid in context menu deps

---------

Co-authored-by: bittoby <bittoby@users.noreply.github.com>
This commit is contained in:
BitToby
2026-03-18 17:19:15 +02:00
committed by GitHub
parent 2a09f30199
commit b876417d5b
4 changed files with 231 additions and 5 deletions

View File

@@ -874,6 +874,42 @@
(duplicate-cells :column index (inc index) ids-map)
(assign-cells objects))))
(defn duplicate-row-at
"Duplicate source row and insert the copy at target-index (0-indexed).
Like `duplicate-row` but inserts at an arbitrary position.
Note: after add-grid-row, if target <= source the source cells shift
by +1, so we must adjust the from-index for duplicate-cells."
[shape objects source-index target-index ids-map]
(let [value (dm/get-in shape [:layout-grid-rows source-index])
;; After inserting at target-index, cells at rows >= (inc target-index)
;; get shifted +1. If target <= source, the source row shifts.
adjusted-source (if (<= target-index source-index)
(inc source-index)
source-index)]
(-> shape
(remove-cell-areas-after :row source-index)
(add-grid-row value target-index)
(duplicate-cells :row adjusted-source target-index ids-map)
(assign-cells objects))))
(defn duplicate-column-at
"Duplicate source column and insert the copy at target-index (0-indexed).
Like `duplicate-column` but inserts at an arbitrary position.
Note: after add-grid-column, if target <= source the source cells shift
by +1, so we must adjust the from-index for duplicate-cells."
[shape objects source-index target-index ids-map]
(let [value (dm/get-in shape [:layout-grid-columns source-index])
;; After inserting at target-index, cells at columns >= (inc target-index)
;; get shifted +1. If target <= source, the source column shifts.
adjusted-source (if (<= target-index source-index)
(inc source-index)
source-index)]
(-> shape
(remove-cell-areas-after :column source-index)
(add-grid-column value target-index)
(duplicate-cells :column adjusted-source target-index ids-map)
(assign-cells objects))))
(defn make-remove-cell
[attr span-attr track-num]
(fn [[_ cell]]

View File

@@ -787,3 +787,135 @@
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [layout-id]})
(dwu/commit-undo-transaction undo-id))))))
(defn complete-rows?
"Check if the selected cells cover complete row(s) — all columns must be included."
[grid cells]
(let [{:keys [first-column last-column]} (ctl/cells-coordinates cells)
num-columns (count (:layout-grid-columns grid))]
(and (= first-column 1)
(= last-column num-columns))))
(defn complete-columns?
"Check if the selected cells cover complete column(s) — all rows must be included."
[grid cells]
(let [{:keys [first-row last-row]} (ctl/cells-coordinates cells)
num-rows (count (:layout-grid-rows grid))]
(and (= first-row 1)
(= last-row num-rows))))
(defn copy-grid-tracks
"Store the selected track indices for later paste. Works for both
complete rows and complete columns."
[grid-id type]
(assert (#{:row :column} type))
(ptk/reify ::copy-grid-tracks
ptk/UpdateEvent
(update [_ state]
(let [objects (dsh/lookup-page-objects state)
grid (get objects grid-id)
selected (get-in state [:workspace-grid-edition grid-id :selected])
cells (->> selected (map #(get-in grid [:layout-grid-cells %])))
{:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells)
;; Convert 1-indexed cell positions to 0-indexed track indices
track-indices (if (= type :row)
(vec (range (dec first-row) last-row))
(vec (range (dec first-column) last-column)))]
(assoc-in state [:workspace-grid-edition grid-id :copied-tracks]
{:track-indices track-indices
:type type
:grid-id grid-id})))))
(defn paste-grid-tracks
"Paste previously copied tracks at the end of the grid.
Each source track is duplicated and appended after the last
existing track. All operations are grouped in a single undo
transaction. Follows the same pattern as `duplicate-layout-track`."
[grid-id]
(ptk/reify ::paste-grid-tracks
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)
grid (get objects grid-id)
copied (get-in state [:workspace-grid-edition grid-id :copied-tracks])
track-indices (:track-indices copied)
type (:type copied)
undo-id (js/Symbol)]
(when (and (seq track-indices) (some? type))
(let [shapes-by-track-fn
(if (= type :row)
ctl/shapes-by-row
ctl/shapes-by-column)
;; Collect shapes from all source tracks
all-shapes
(->> track-indices
(mapcat #(shapes-by-track-fn grid % false))
(set))
;; Generate duplication changes for all shapes at once
changes
(-> (pcb/empty-changes it)
(cll/generate-duplicate-changes objects page all-shapes (gpt/point 0 0) libraries library-data file-id)
(cll/generate-duplicate-changes-update-indices objects all-shapes))
;; Build ids-map: old-shape-id -> new-shape-id
ids-map
(->> changes
:redo-changes
(filter #(= (:type %) :add-obj))
(filter #(all-shapes (:old-id %)))
(map #(vector (:old-id %) (get-in % [:obj :id])))
(into {}))
duplicate-at-fn
(if (= type :row)
ctl/duplicate-row-at
ctl/duplicate-column-at)
tracks-prop
(if (= type :row)
:layout-grid-rows
:layout-grid-columns)
;; Sort source indices ascending — we'll append each
;; copy at the end in order, preserving the original
;; track ordering in the appended block.
sorted-indices (vec (sort track-indices))
changes
(-> changes
(pcb/update-shapes
[grid-id]
(fn [shape objects]
;; Restore grid structure (duplication may have altered it)
(let [shape (merge shape (select-keys grid [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
;; Append each source track at the end.
;; Process in ascending order so the copies
;; appear in the same order as the originals.
;; Each insertion adds one track, so both the
;; target index and the source index (if it
;; comes after the target) shift by 1.
(reduce
(fn [s [offset src-idx]]
(let [;; Source tracks don't shift because we
;; append after them (target > source).
actual-src src-idx
;; Append at the end (which grows by
;; one with each iteration).
target-idx (+ (count (get grid tracks-prop)) offset)]
(duplicate-at-fn s objects actual-src target-idx ids-map)))
shape
(map-indexed vector sorted-indices))))
{:with-objects? true}))]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [grid-id]})
(dwu/commit-undo-transaction undo-id))))))))

View File

@@ -779,6 +779,7 @@
[{:keys [mdata]}]
(let [{:keys [grid cells]} mdata
grid-id (:id grid)
single? (= (count cells) 1)
can-merge?
@@ -786,17 +787,53 @@
(mf/deps cells)
#(ctl/valid-area-cells? cells))
can-copy-rows?
(mf/use-memo
(mf/deps grid cells)
#(dwsl/complete-rows? grid cells))
can-copy-columns?
(mf/use-memo
(mf/deps grid cells)
#(dwsl/complete-columns? grid cells))
grid-edition-ref
(mf/use-memo
(mf/deps grid-id)
#(refs/workspace-grid-edition-id grid-id))
grid-edition (mf/deref grid-edition-ref)
has-copied-tracks? (some? (:copied-tracks grid-edition))
do-merge-cells
(mf/use-fn
(mf/deps grid cells)
(mf/deps grid-id cells)
(fn []
(st/emit! (dwsl/merge-cells (:id grid) (map :id cells)))))
(st/emit! (dwsl/merge-cells grid-id (map :id cells)))))
do-create-board
(mf/use-fn
(mf/deps grid cells)
(mf/deps grid-id cells)
(fn []
(st/emit! (dwsl/create-cell-board (:id grid) (map :id cells)))))]
(st/emit! (dwsl/create-cell-board grid-id (map :id cells)))))
do-copy-rows
(mf/use-fn
(mf/deps grid-id)
(fn []
(st/emit! (dwsl/copy-grid-tracks grid-id :row))))
do-copy-columns
(mf/use-fn
(mf/deps grid-id)
(fn []
(st/emit! (dwsl/copy-grid-tracks grid-id :column))))
do-paste-tracks
(mf/use-fn
(mf/deps grid-id)
(fn []
(st/emit! (dwsl/paste-grid-tracks grid-id))))]
[:*
(when (not single?)
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.merge")
@@ -809,7 +846,19 @@
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.create-board")
:on-click do-create-board
:disabled (and (not single?) (not can-merge?))}]]))
:disabled (and (not single?) (not can-merge?))}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-rows")
:on-click do-copy-rows
:disabled (not can-copy-rows?)}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-columns")
:on-click do-copy-columns
:disabled (not can-copy-columns?)}]
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.paste-tracks")
:on-click do-paste-tracks
:disabled (not has-copied-tracks?)}]]))
;; FIXME: optimize because it is rendered always

View File

@@ -5589,6 +5589,15 @@ msgstr "Create board"
msgid "workspace.context-menu.grid-cells.merge"
msgstr "Merge cells"
msgid "workspace.context-menu.grid-cells.copy-rows"
msgstr "Copy rows"
msgid "workspace.context-menu.grid-cells.copy-columns"
msgstr "Copy columns"
msgid "workspace.context-menu.grid-cells.paste-tracks"
msgstr "Paste"
#: src/app/main/ui/workspace/context_menu.cljs:754
msgid "workspace.context-menu.grid-track.column.add-after"
msgstr "Add 1 column to the right"