From a44c70ef6962acbdca0cba5eb1e9a8103936af8c Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 26 Jun 2025 11:54:31 +0200 Subject: [PATCH] :sparkles: Keep the swapped childs if the copies when doing a variant switch --- .../src/app/common/files/changes_builder.cljc | 14 ++- common/src/app/common/files/helpers.cljc | 22 +++- common/src/app/common/logic/libraries.cljc | 17 ++- common/src/app/common/logic/shapes.cljc | 24 +++- common/src/app/common/logic/variants.cljc | 103 ++++++++++++++++-- .../app/common/test_helpers/components.cljc | 2 +- .../app/common/test_helpers/compositions.cljc | 3 +- common/src/app/common/types/file.cljc | 41 +++++++ .../app/main/data/workspace/libraries.cljs | 5 +- 9 files changed, 197 insertions(+), 34 deletions(-) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 83a64303eb..5cf97c7a62 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -473,12 +473,14 @@ (fn [undo-changes shape] (let [prev-sibling (cfh/get-prev-sibling objects (:id shape))] (conj undo-changes - {:type :mov-objects - :page-id (::page-id (meta changes)) - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :after-shape prev-sibling - :index 0}))) ; index is used in case there is no after-shape (moving bottom shapes) + (cond-> {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :after-shape prev-sibling + :index 0} ; index is used in case there is no after-shape (moving bottom shapes) + (:component-swap options) + (assoc :component-swap true))))) restore-touched-change {:type :mod-obj diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 360d8428d3..7f5c60f12f 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -152,12 +152,22 @@ (dm/get-prop shape :type)))) (defn get-children-ids - [objects id] - (letfn [(get-children-ids-rec [id processed] - (when (not (contains? processed id)) - (when-let [shapes (-> (get objects id) :shapes (some-> vec))] - (into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))] - (get-children-ids-rec id #{}))) + "Returns the ids of all the descendants of the shape identified + by the id. Optionally, you can pass an ignore function to indicate + when to ignore a descendant (and all its descendants)" + ([objects id] + (get-children-ids objects id {})) + ([objects id {:keys [ignore-children-fn] + ;;ignore-children-fn should receive a shape and return a boolean + :or {ignore-children-fn (constantly false)}}] + (letfn [(get-children-ids-rec [id processed] + (when-not (contains? processed id) + (when-let [shapes (as-> (get objects id) $ + (:shapes $) + (remove ignore-children-fn $) + (some-> $ vec))] + (into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))] + (get-children-ids-rec id #{})))) (defn get-children-ids-with-self [objects id] diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index b8bafa7152..582b8ab4cf 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -2257,10 +2257,21 @@ {}))])) (defn generate-component-swap - [changes objects shape file page libraries id-new-component index target-cell keep-props-values] - (let [[all-parents changes] + [changes objects shape file page libraries id-new-component + index target-cell keep-props-values keep-touched] + (let [;; When we keep the touched properties, we can't delete the + ;; swapped children (we will keep them too) + ignore-swapped-fn + (if keep-touched + #(-> (get objects %) + (ctk/get-swap-slot)) + (constantly false)) + + [all-parents changes] (-> changes - (cls/generate-delete-shapes file page objects (d/ordered-set (:id shape)) {:component-swap true})) + (cls/generate-delete-shapes + file page objects (d/ordered-set (:id shape)) + {:component-swap true :ignore-children-fn ignore-swapped-fn})) [new-shape changes] (-> changes (generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))] diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index f26b9f95a8..39fc560220 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -99,7 +99,14 @@ (pcb/with-library-data file)) ids options)) - ([changes ids {:keys [ignore-touched component-swap]}] + ([changes ids {:keys [ignore-touched + component-swap + ;; We will delete the shapes and its descendants. + ;; ignore-children-fn is used to ignore some descendants + ;; on the deletion process. It should receive a shape and + ;; return a boolean + ignore-children-fn] + :or {ignore-children-fn (constantly false)}}] (let [objects (pcb/get-objects changes) data (pcb/get-library-data changes) page-id (pcb/get-page-id changes) @@ -177,10 +184,15 @@ (d/ordered-set) (concat ids-to-delete ids-to-hide)) - all-children - (->> ids-to-delete ;; Children of deleted shapes must be also deleted. + ;; Descendants of deleted shapes must be also deleted, + ;; except the ignored ones by the function ignore-children-fn + descendants-to-delete + (->> ids-to-delete (reduce (fn [res id] - (into res (cfh/get-children-ids objects id))) + (into res (cfh/get-children-ids + objects + id + {:ignore-children-fn ignore-children-fn}))) []) (reverse) (into (d/ordered-set))) @@ -214,7 +226,7 @@ (conj components (:component-id shape)) components))) [] - (into ids-to-delete all-children)) + (into ids-to-delete descendants-to-delete)) ids-set (set ids-to-delete) @@ -241,7 +253,7 @@ changes (-> changes (generate-update-shape-flags ids-to-hide objects {:hidden true}) - (pcb/remove-objects all-children {:ignore-touched true}) + (pcb/remove-objects descendants-to-delete {:ignore-touched true}) (pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched}) (pcb/remove-objects empty-parents) (pcb/resize-parents all-parents) diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 0843983c30..054bf0103f 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -1,13 +1,17 @@ (ns app.common.logic.variants (:require + [app.common.data :as d] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.files.variant :as cfv] [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] [app.common.logic.variant-properties :as clvp] + [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] - [app.common.types.variant :as ctv])) + [app.common.types.variant :as ctv] + [app.common.uuid :as uuid])) (defn generate-add-new-variant [changes shape variant-id new-component-id new-shape-id prop-num] @@ -62,6 +66,59 @@ shapes)))) +(defn- keep-swapped-item + "As part of the keep-touched process on a switch, given a child on the original + copy that was swapped (orig-swapped-child), and its related shape on the new copy + (related-shape-in-new), move the orig-swapped-child into the parent of + related-shape-in-new, fix its swap-slot if needed, and then delete + related-shape-in-new" + [changes related-shape-in-new orig-swapped-child ldata page swap-ref-id] + (let [;; Before to the swap, temporary move the previous + ;; shape to the root panel to avoid problems when + ;; the previous parent is deleted. + before-changes (-> (pcb/empty-changes) + (pcb/with-page page) + (pcb/with-objects (:objects page)) + (pcb/change-parent uuid/zero [orig-swapped-child] 0 {:component-swap true})) + + objects (pcb/get-objects changes) + prev-swap-slot (ctk/get-swap-slot orig-swapped-child) + current-parent (get objects (:parent-id related-shape-in-new)) + pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))] + + + (-> (pcb/concat-changes before-changes changes) + + ;; Move the previous shape to the new parent + (pcb/change-parent (:parent-id related-shape-in-new) [orig-swapped-child] pos {:component-swap true}) + + ;; We need to update the swap slot only when it pointed + ;; to the swap-ref-id. Oterwise this is a swapped item + ;; inside a nested copy, so we need to keep it. + (cond-> + (= prev-swap-slot swap-ref-id) + (pcb/update-shapes + [(:id orig-swapped-child)] + #(ctk/set-swap-slot % (:shape-ref related-shape-in-new)))) + + ;; Delete new non-swapped item + (cls/generate-delete-shapes ldata page objects (d/ordered-set (:id related-shape-in-new)) {:component-swap true}) + second))) + +(defn- child-of-swapped? + "Check if any ancestor of a shape (between base-parent-id and shape) was swapped" + [shape objects base-parent-id] + (let [ancestors (->> (ctn/get-parent-heads objects shape) + ;; Ignore ancestors ahead of base-parent + (drop-while #(not= base-parent-id (:id %))) + seq) + num-ancestors (count ancestors) + ;; Ignore first and last (base-parent and shape) + ancestors (when (and ancestors (<= 3 num-ancestors)) + (subvec (vec ancestors) 1 (dec num-ancestors)))] + (some ctk/get-swap-slot ancestors))) + + (defn generate-keep-touched "This is used as part of the switch process, when you switch from an original-shape to a new-shape. It generate changes to @@ -71,12 +128,20 @@ * On the main components, both have the same name (the name on the copies are ignored) * Both has the same type of ancestors, on the same order (see generate-path for the translation of the types)" - [changes new-shape original-shape original-shapes page libraries] + [changes new-shape original-shape original-shapes page libraries ldata] (let [objects (pcb/get-objects changes) container (ctn/make-container page :page) + page-objects (:objects page) ;; Get the touched children of the original-shape - orig-touched (filter (comp seq :touched) original-shapes) + ;; Ignore children of swapped items, because + ;; they will be moved without change when + ;; managing their swapped ancestor + orig-touched (->> (filter (comp seq :touched) original-shapes) + (remove + #(child-of-swapped? % + page-objects + (:id original-shape)))) ;; Adds a :shape-path attribute to the children of the new-shape, ;; that contains the type of its ancestors and its name @@ -106,17 +171,37 @@ ;; Process each touched children of the original-shape (reduce (fn [changes orig-child-touched] - (let [;; orig-child-touched is in a copy. Get the referenced shape on the main component - orig-ref-shape (ctf/find-ref-shape nil container libraries orig-child-touched) + (let [;; If the orig-child-touched was swapped, get its swap-slot + swap-slot (ctk/get-swap-slot orig-child-touched) + + ;; orig-child-touched is in a copy. Get the referenced shape on the main component + ;; If there is a swap slot, we will get the referenced shape in another way + orig-ref-shape (when-not swap-slot + ;; TODO Maybe just get it from o-ref-shapes-wp + (ctf/find-ref-shape nil container libraries orig-child-touched)) + + orig-ref-id (if swap-slot + ;; If there is a swap slot, find the referenced shape id + (ctf/find-ref-id-for-swapped orig-child-touched container libraries) + ;; If there is not a swap slot, get the id from the orig-ref-shape + (:id orig-ref-shape)) + ;; Get the shape path of the referenced main - shape-path (get o-ref-shapes-p-map (:id orig-ref-shape)) + shape-path (get o-ref-shapes-p-map orig-ref-id) ;; Get its related shape in the children of new-shape: the one that ;; has the same shape-path related-shape-in-new (get new-shapes-map shape-path)] + ;; If there is a related shape, keep its data (if related-shape-in-new - ;; If there is a related shape, copy the touched attributes into it - (cll/update-attrs-on-switch - changes related-shape-in-new orig-child-touched new-shape original-shape orig-ref-shape container) + (if swap-slot + ;; If the orig-child-touched was swapped, keep it + (keep-swapped-item changes related-shape-in-new orig-child-touched + ldata page orig-ref-id) + ;; If the orig-child-touched wasn't swapped, copy + ;; the touched attributes into it + (cll/update-attrs-on-switch + changes related-shape-in-new orig-child-touched + new-shape original-shape orig-ref-shape container)) changes))) changes orig-touched))) diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index 687be91871..4e82acd647 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -156,7 +156,7 @@ [new_shape _ changes] (-> (pcb/empty-changes nil (:id page)) - (cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values)) + (cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values false)) file' (thf/apply-changes file changes)] diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index ca6bd064ba..bb9a658e80 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -291,7 +291,8 @@ :id) 0 nil - {}) + {} + false) file' (thf/apply-changes file changes)] (if propagate-fn diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index fafa4b7226..521915da96 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -397,6 +397,47 @@ (or (= slot-main slot-inst) (= (:id shape-main) slot-inst))))) +(defn- find-next-related-swap-shape-id + "Go up from the chain of references shapes that will eventually lead to the shape + with swap-slot-id as id. Returns the next shape on the chain" + [parent swap-slot-id libraries] + (let [container (get-component-container-from-head parent libraries) + objects (:objects container) + + children (cfh/get-children objects (:id parent)) + original-shape-id (->> children + (filter #(= swap-slot-id (:id %))) + first + :id)] + (if original-shape-id + ;; Return the children which id is the swap-slot-id + original-shape-id + ;; No children with swap-slot-id as id, go up + (let [referenced-shape (find-ref-shape nil container libraries parent) + ;; Recursive call that will get the id of the next shape on + ;; the chain that ends on a shape with swap-slot-id as id + next-shape-id (when referenced-shape + (find-next-related-swap-shape-id referenced-shape swap-slot-id libraries))] + ;; Return the children which shape-ref points to the next-shape-id + (->> children + (filter #(= next-shape-id (:shape-ref %))) + first + :id))))) + +(defn find-ref-id-for-swapped + "When a shape has been swapped, find the original ref-id that the shape had + before the swap" + [shape container libraries] + (let [swap-slot (ctk/get-swap-slot shape) + objects (:objects container) + + parent (get objects (:parent-id shape)) + parent-head (ctn/get-head-shape objects parent) + parent-ref (find-ref-shape nil container libraries parent-head)] + + (when (and swap-slot parent-ref) + (find-next-related-swap-shape-id parent-ref swap-slot libraries)))) + (defn get-component-shapes "Retrieve all shapes of the component" [file-data component] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 034bdd6b55..4d28a60c21 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -975,10 +975,11 @@ [new-shape all-parents changes] (-> (pcb/empty-changes it (:id page)) (pcb/set-undo-group undo-group) - (cll/generate-component-swap objects shape ldata page libraries id-new-component index target-cell keep-props-values)) + (cll/generate-component-swap objects shape ldata page libraries id-new-component + index target-cell keep-props-values keep-touched?)) changes (if keep-touched? - (clv/generate-keep-touched changes new-shape shape orig-shapes page libraries) + (clv/generate-keep-touched changes new-shape shape orig-shapes page libraries ldata) changes)] (rx/of