diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 68a34dad95..0a5c7b4cfe 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 6a11dfd988..817bc040da 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -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 diff --git a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs index 7939b903df..b096a96417 100644 --- a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs +++ b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs @@ -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))