diff --git a/CHANGES.md b/CHANGES.md index 97c409534..a9a180abe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,9 @@ ### :boom: Breaking changes ### :sparkles: New features ### :bug: Bugs fixed + +- Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221) + ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index e0c3b2715..8a0e5430b 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -32,18 +32,23 @@ (vary-meta changes assoc ::objects objects)) (defn add-obj - ([changes obj index] - (add-obj changes (assoc obj ::index index))) - ([changes obj] - (let [add-change - {:type :add-obj - :id (:id obj) - :page-id (::page-id (meta changes)) - :parent-id (:parent-id obj) - :frame-id (:frame-id obj) - :index (::index obj) - :obj (dissoc obj ::index :parent-id)} + (add-obj changes obj nil)) + + ([changes obj {:keys [index ignore-touched] :or {index ::undefined ignore-touched false}}] + (let [obj (cond-> obj + (not= index ::undefined) + (assoc :index index)) + + add-change + {:type :add-obj + :id (:id obj) + :page-id (::page-id (meta changes)) + :parent-id (:parent-id obj) + :frame-id (:frame-id obj) + :index (::index obj) + :ignore-touched ignore-touched + :obj (dissoc obj ::index :parent-id)} del-change {:type :del-obj @@ -201,3 +206,22 @@ :page-id page-id :option option-key :value old-val})))) + +(defn reg-objects + [chdata shape-ids] + (let [page-id (::page-id (meta chdata))] + (-> chdata + (update :redo-changes conj {:type :reg-objects :page-id page-id :shapes shape-ids})))) + ;; No need to do anything to undo + +(defn amend-last-change + "Modify the last redo-changes added with an update function." + [chdata f] + (update chdata :redo-changes + #(conj (pop %) (f (peek %))))) + +(defn amend-changes + "Modify all redo-changes with an update function." + [chdata f] + (update chdata :redo-changes #(mapv f %))) + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ad4339d28..a4e34b373 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1718,7 +1718,8 @@ ;; Analyze the rchange and replace staled media and ;; references to the new uploaded media-objects. (process-rchange [media-idx item] - (if (= :image (get-in item [:obj :type])) + (if (and (= (:type item) :add-obj) + (= :image (get-in item [:obj :type]))) (update-in item [:obj :metadata] (fn [{:keys [id] :as mdata}] (if-let [mobj (get media-idx id)] @@ -1812,57 +1813,49 @@ ;; Proceed with the standard shape paste process. (do-paste [it state mouse-pos media] - (let [page-objects (wsh/lookup-page-objects state) - media-idx (d/index-by :prev-id media) + (let [page (wsh/lookup-page state) + media-idx (d/index-by :prev-id media) ;; Calculate position for the pasted elements [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) - paste-objects (->> paste-objects - (d/mapm (fn [_ shape] - (-> shape - (assoc :frame-id frame-id) - (assoc :parent-id parent-id) + paste-objects (->> paste-objects + (d/mapm (fn [_ shape] + (-> shape + (assoc :frame-id frame-id) + (assoc :parent-id parent-id) - (cond-> - ;; if foreign instance, detach the shape - (foreign-instance? shape paste-objects state) - (dissoc :component-id - :component-file - :component-root? - :remote-synced? - :shape-ref - :touched)))))) + (cond-> + ;; if foreign instance, detach the shape + (foreign-instance? shape paste-objects state) + (dissoc :component-id + :component-file + :component-root? + :remote-synced? + :shape-ref + :touched)))))) - all-objects (merge page-objects paste-objects) + all-objects (merge (:objects page) paste-objects) - page-id (:current-page-id state) - unames (-> (wsh/lookup-page-objects state page-id) - (dwc/retrieve-used-names)) ;; TODO: move this calculation inside prepare-duplicate-changes? - - rchanges (->> (dws/prepare-duplicate-changes all-objects page-id unames selected delta) - (mapv (partial process-rchange media-idx)) - (mapv (partial change-add-obj-index paste-objects selected index))) - - uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) - (reverse rchanges)) + changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it) + (pcb/amend-changes (partial process-rchange media-idx)) + (pcb/amend-changes (partial change-add-obj-index paste-objects selected index))) ;; Adds a reg-objects operation so the groups are updated. We add all the new objects - new-objects-ids (->> rchanges (filter #(= (:type %) :add-obj)) (mapv :id)) + new-objects-ids (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (mapv :id)) - rchanges (conj rchanges {:type :reg-objects - :page-id page-id - :shapes new-objects-ids}) + changes (pcb/reg-objects changes new-objects-ids) - selected (->> rchanges + selected (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) (filter #(selected (:old-id %))) (map #(get-in % [:obj :id])) (into (d/ordered-set)))] - (rx/of (dch/commit-changes {:redo-changes rchanges - :undo-changes uchanges - :origin it}) + (rx/of (dch/commit-changes changes) (dwc/select-shapes selected))))] + (ptk/reify ::paste-shape ptk/WatchEvent (watch [it state _] diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 6c705cb61..e67346103 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -99,7 +99,7 @@ shape-id (:id boolean-data) changes (-> (cb/empty-changes it page-id) (cb/with-objects objects) - (cb/add-obj boolean-data index) + (cb/add-obj boolean-data {:index index}) (cb/change-parent shape-id shapes))] (rx/of (dch/commit-changes changes) (dwc/select-shapes (d/ordered-set shape-id))))))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 08ec41784..4905c3916 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -10,9 +10,11 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] [app.common.math :as mth] + [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.spec.interactions :as cti] + [app.common.spec.page :as ctp] [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] @@ -263,14 +265,133 @@ (declare prepare-duplicate-change) (declare prepare-duplicate-frame-change) (declare prepare-duplicate-shape-change) +(declare prepare-duplicate-flows) -(defn update-indices - "Fixes the indices for a set of changes after a duplication. We need to - fix the indices to take into the account the movement of indices. +(defn prepare-duplicate-changes + "Prepare objects to duplicate: generate new id, give them unique names, + move to the desired position, and recalculate parents and frames as needed." + [all-objects page ids delta it] + (let [shapes (map (d/getf all-objects) ids) + unames (volatile! (dwc/retrieve-used-names (:objects page))) + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) #{} ids) + ids-map (into {} (map #(vector % (uuid/next))) all-ids)] + (-> (reduce (fn [changes shape] + (prepare-duplicate-change changes all-objects page unames update-unames! ids-map shape delta)) + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/with-objects all-objects)) + shapes) + (prepare-duplicate-flows shapes page ids-map)))) - index-map is a map that goes from parent-id => vector([id index-in-parent])" - [changes index-map] - (let [inc-indices +(defn- prepare-duplicate-change + [changes objects page unames update-unames! ids-map shape delta] + (if (cph/frame-shape? shape) + (prepare-duplicate-frame-change changes objects page unames update-unames! ids-map shape delta) + (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map shape delta (:frame-id shape) (:parent-id shape)))) + +(defn- prepare-duplicate-frame-change + [changes objects page unames update-unames! ids-map obj delta] + (let [new-id (ids-map (:id obj)) + frame-name (dwc/generate-unique-name @unames (:name obj)) + _ (update-unames! frame-name) + + new-frame (-> obj + (assoc :id new-id + :name frame-name + :frame-id uuid/zero + :shapes []) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) + + changes (-> (pcb/add-obj changes new-frame) + (pcb/amend-last-change #(assoc % :old-id (:id obj)))) + + changes (reduce (fn [changes child] + (prepare-duplicate-shape-change changes + objects + page + unames + update-unames! + ids-map + child + delta + new-id + new-id)) + changes + (map (d/getf objects) (:shapes obj)))] + changes)) + +(defn- prepare-duplicate-shape-change + [changes objects page unames update-unames! ids-map obj delta frame-id parent-id] + (if (some? obj) + (let [new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) + name (dwc/generate-unique-name @unames (:name obj)) + _ (update-unames! name) + + new-obj (-> obj + (assoc :id new-id + :name name + :parent-id parent-id + :frame-id frame-id) + (dissoc :shapes) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) + + changes (pcb/add-obj changes new-obj {:ignore-touched true}) + changes (-> (pcb/add-obj changes new-obj {:ignore-touched true}) + (pcb/amend-last-change #(assoc % :old-id (:id obj))))] + + (reduce (fn [changes child] + (prepare-duplicate-shape-change changes + objects + page + unames + update-unames! + ids-map + child + delta + frame-id + new-id)) + changes + (map (d/getf objects) (:shapes obj)))) + changes)) + +(defn- prepare-duplicate-flows + [changes shapes page ids-map] + (let [flows (-> page :options :flows) + unames (volatile! (into #{} (map :name flows))) + frames-with-flow (->> shapes + (filter #(= (:type %) :frame)) + (filter #(some? (ctp/get-frame-flow flows (:id %)))))] + (if-not (empty? frames-with-flow) + (let [new-flows (reduce + (fn [flows frame] + (let [name (dwc/generate-unique-name @unames "Flow-1") + _ (vswap! unames conj name) + new-flow {:id (uuid/next) + :name name + :starting-frame (get ids-map (:id frame))}] + (ctp/add-flow flows new-flow))) + flows + frames-with-flow)] + (pcb/set-page-option changes :flows new-flows)) + changes))) + +(defn duplicate-changes-update-indices + "Updates the changes to correctly set the indexes of the duplicated objects, + depending on the index of the original object respect their parent." + [objects ids changes] + (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) + index-map (reduce (fn [index-map id] + (let [parent-id (get-in objects [id :parent-id]) + parent-index (cph/get-position-on-parent objects id)] + (update index-map parent-id (fnil conj []) [id parent-index]))) + {} + ids) + + inc-indices (fn [[offset result] [id index]] [(inc offset) (conj result [id (+ index offset)])]) @@ -282,118 +403,12 @@ (second) (into {}))) - objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge)) + objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] - update-change - (fn [change] - (let [index (get objects-indices (:old-id change))] - (-> change - (assoc :index index))))] - (mapv update-change changes))) - -(defn prepare-duplicate-changes - "Prepare objects to paste: generate new id, give them unique names, - move to the position of mouse pointer, and find in what frame they - fit." - [objects page-id unames ids delta] - (let [unames (volatile! unames) - update-unames! (fn [new-name] (vswap! unames conj new-name)) - all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids objects %2))) #{} ids) - ids-map (into {} (map #(vector % (uuid/next))) all-ids)] - (loop [ids (seq ids) - chgs []] - (if ids - (let [id (first ids) - result (prepare-duplicate-change objects page-id unames update-unames! ids-map id delta) - result (if (vector? result) result [result])] - (recur - (next ids) - (into chgs result))) - chgs)))) - -(defn duplicate-changes-update-indices - "Parses the change set when duplicating to set-up the appropriate indices" - [objects ids changes] - - (let [process-id - (fn [index-map id] - (let [parent-id (get-in objects [id :parent-id]) - parent-index (cph/get-position-on-parent objects id)] - (update index-map parent-id (fnil conj []) [id parent-index]))) - index-map (reduce process-id {} ids)] - (-> changes (update-indices index-map)))) - -(defn- prepare-duplicate-change - [objects page-id unames update-unames! ids-map id delta] - (let [obj (get objects id)] - (if (cph/frame-shape? obj) - (prepare-duplicate-frame-change objects page-id unames update-unames! ids-map obj delta) - (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj))))) - -(defn- prepare-duplicate-shape-change - [objects page-id unames update-unames! ids-map obj delta frame-id parent-id] - (when (some? obj) - (let [new-id (ids-map (:id obj)) - parent-id (or parent-id frame-id) - name (dwc/generate-unique-name @unames (:name obj)) - _ (update-unames! name) - - new-obj (-> obj - (assoc :id new-id - :name name - :frame-id frame-id) - (dissoc :shapes) - (geom/move delta) - (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) - - children-changes - (loop [result [] - cid (first (:shapes obj)) - cids (rest (:shapes obj))] - (if (nil? cid) - result - (let [obj (get objects cid) - changes (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta frame-id new-id)] - (recur - (into result changes) - (first cids) - (rest cids)))))] - - (into [{:type :add-obj - :id new-id - :page-id page-id - :old-id (:id obj) - :frame-id frame-id - :parent-id parent-id - :ignore-touched true - :obj new-obj}] - children-changes)))) - -(defn- prepare-duplicate-frame-change - [objects page-id unames update-unames! ids-map obj delta] - (let [new-id (ids-map (:id obj)) - frame-name (dwc/generate-unique-name @unames (:name obj)) - _ (update-unames! frame-name) - - sch (->> (map #(get objects %) (:shapes obj)) - (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map % delta new-id new-id))) - - new-frame (-> obj - (assoc :id new-id - :name frame-name - :frame-id uuid/zero - :shapes []) - (geom/move delta) - (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) - - fch {:type :add-obj - :old-id (:id obj) - :page-id page-id - :id new-id - :frame-id uuid/zero - :obj new-frame}] - - (into [fch] sch))) + (pcb/amend-changes + changes + (fn [change] + (assoc change :index (get objects-indices (:old-id change))))))) (defn clear-memorize-duplicated [] @@ -447,25 +462,22 @@ ptk/WatchEvent (watch [it state _] (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [page (wsh/lookup-page state) + objects (:objects page) selected (wsh/lookup-selected state) delta (if (and move-delta? (= (count selected) 1)) (let [obj (get objects (first selected))] (calc-duplicate-delta obj state objects)) (gpt/point 0 0)) - unames (dwc/retrieve-used-names objects) - - rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta) - (duplicate-changes-update-indices objects selected)) - - uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) - (reverse rchanges)) + changes (->> (prepare-duplicate-changes objects page selected delta it) + (duplicate-changes-update-indices objects selected)) id-original (when (= (count selected) 1) (first selected)) - selected (->> rchanges + selected (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) (filter #(selected (:old-id %))) (map #(get-in % [:obj :id])) (into (d/ordered-set))) @@ -473,9 +485,7 @@ id-duplicated (when (= (count selected) 1) (first selected))] (rx/of (select-shapes selected) - (dch/commit-changes {:redo-changes rchanges - :undo-changes uchanges - :origin it}) + (dch/commit-changes changes) (memorize-duplicated id-original id-duplicated))))))) (defn change-hover-state