diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index ffdc5a61a3..69ef62b13e 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1095,3 +1095,11 @@ (defn get-objects [changes] (dm/get-in (::file-data (meta changes)) [:pages-index uuid/zero :objects])) + +(defn get-page + [changes] + (::page (meta changes))) + +(defn get-page-id + [changes] + (::page-id (meta changes))) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 7016f61759..ca64e55f3d 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -13,6 +13,7 @@ [app.common.logic.variant-properties :as clvp] [app.common.types.component :as ctk] [app.common.types.container :as ctn] + [app.common.types.pages-list :as ctpl] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.types.token :as cto] @@ -80,161 +81,167 @@ (pcb/update-shapes ids update-fn {:attrs #{:blocked :hidden}})))) (defn generate-delete-shapes - [changes file page objects ids {:keys [ignore-touched component-swap]}] - (let [ids (cfh/clean-loops objects ids) + ([changes file page objects ids options] + (generate-delete-shapes (-> changes + (pcb/with-page page) + (pcb/with-objects objects) + (pcb/with-library-data file)) + ids + options)) + ([changes ids {:keys [ignore-touched component-swap]}] + (let [objects (pcb/get-objects changes) + data (pcb/get-library-data changes) + page-id (pcb/get-page-id changes) + page (or (pcb/get-page changes) + (ctpl/get-page data page-id)) - in-component-copy? - (fn [shape-id] + ids (cfh/clean-loops objects ids) + in-component-copy? + (fn [shape-id] ;; Look for shapes that are inside a component copy, but are ;; not the root. In this case, they must not be deleted, ;; but hidden (to be able to recover them more easily). ;; Unless we are doing a component swap, in which case we want ;; to delete the old shape - (let [shape (get objects shape-id)] - (and (ctn/has-any-copy-parent? objects shape) - (not component-swap)))) + (let [shape (get objects shape-id)] + (and (ctn/has-any-copy-parent? objects shape) + (not component-swap)))) - [ids-to-delete ids-to-hide] - (loop [ids-seq (seq ids) - ids-to-delete [] - ids-to-hide []] - (let [id (first ids-seq)] - (if (nil? id) - [ids-to-delete ids-to-hide] - (if (in-component-copy? id) - (recur (rest ids-seq) - ids-to-delete - (conj ids-to-hide id)) - (recur (rest ids-seq) - (conj ids-to-delete id) - ids-to-hide))))) + [ids-to-delete ids-to-hide] + (loop [ids-seq (seq ids) + ids-to-delete [] + ids-to-hide []] + (let [id (first ids-seq)] + (if (nil? id) + [ids-to-delete ids-to-hide] + (if (in-component-copy? id) + (recur (rest ids-seq) + ids-to-delete + (conj ids-to-hide id)) + (recur (rest ids-seq) + (conj ids-to-delete id) + ids-to-hide))))) + lookup (d/getf objects) - changes (-> changes - (pcb/with-page page) - (pcb/with-objects objects) - (pcb/with-library-data file)) - - lookup (d/getf objects) - - groups-to-unmask - (reduce (fn [group-ids id] + groups-to-unmask + (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, ;; the mask condition must be removed, and it must be ;; converted to a normal group. - (let [obj (lookup id) - parent (lookup (:parent-id obj))] - (if (and (:masked-group parent) - (= id (first (:shapes parent)))) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids-to-delete) + (let [obj (lookup id) + parent (lookup (:parent-id obj))] + (if (and (:masked-group parent) + (= id (first (:shapes parent)))) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids-to-delete) - interacting-shapes - (filter (fn [shape] + interacting-shapes + (filter (fn [shape] ;; If any of the deleted shapes is the destination of ;; some interaction, this must be deleted, too. - (let [interactions (:interactions shape)] - (some #(and (ctsi/has-destination %) - (contains? ids-to-delete (:destination %))) - interactions))) - (vals objects)) + (let [interactions (:interactions shape)] + (some #(and (ctsi/has-destination %) + (contains? ids-to-delete (:destination %))) + interactions))) + (vals objects)) - changes - (reduce (fn [changes {:keys [id] :as flow}] - (if (contains? ids-to-delete (:starting-frame flow)) - (pcb/set-flow changes id nil) - changes)) - changes - (:flows page)) + changes + (reduce (fn [changes {:keys [id] :as flow}] + (if (contains? ids-to-delete (:starting-frame flow)) + (pcb/set-flow changes id nil) + changes)) + changes + (:flows page)) - all-parents - (reduce (fn [res id] + all-parents + (reduce (fn [res id] ;; All parents of any deleted shape must be resized. - (into res (cfh/get-parent-ids objects id))) - (d/ordered-set) - (concat ids-to-delete ids-to-hide)) + (into res (cfh/get-parent-ids objects id))) + (d/ordered-set) + (concat ids-to-delete ids-to-hide)) - all-children - (->> ids-to-delete ;; Children of deleted shapes must be also deleted. - (reduce (fn [res id] - (into res (cfh/get-children-ids objects id))) - []) - (reverse) - (into (d/ordered-set))) + all-children + (->> ids-to-delete ;; Children of deleted shapes must be also deleted. + (reduce (fn [res id] + (into res (cfh/get-children-ids objects id))) + []) + (reverse) + (into (d/ordered-set))) - find-all-empty-parents - (fn recursive-find-empty-parents [empty-parents] - (let [all-ids (into empty-parents ids-to-delete) - contains? (partial contains? all-ids) - xform (comp (map lookup) - (filter #(or (cfh/group-shape? %) (cfh/bool-shape? %))) - (remove #(->> (:shapes %) (remove contains?) seq)) - (map :id)) - parents (into #{} xform all-parents)] - (if (= empty-parents parents) - empty-parents - (recursive-find-empty-parents parents)))) + find-all-empty-parents + (fn recursive-find-empty-parents [empty-parents] + (let [all-ids (into empty-parents ids-to-delete) + contains? (partial contains? all-ids) + xform (comp (map lookup) + (filter #(or (cfh/group-shape? %) (cfh/bool-shape? %) (ctk/is-variant-container? %))) + (remove #(->> (:shapes %) (remove contains?) seq)) + (map :id)) + parents (into #{} xform all-parents)] + (if (= empty-parents parents) + empty-parents + (recursive-find-empty-parents parents)))) - empty-parents + empty-parents ;; Any parent whose children are all deleted, must be deleted too. ;; Unless we are during a component swap: in this case we are replacing a shape by ;; other one, so must not delete empty parents. - (if-not component-swap - (into (d/ordered-set) (find-all-empty-parents #{})) - #{}) + (if-not component-swap + (into (d/ordered-set) (find-all-empty-parents #{})) + #{}) - components-to-delete - (reduce (fn [components id] - (let [shape (get objects id)] - (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file - (:main-instance shape)) ;; but check anyway - (conj components (:component-id shape)) - components))) - [] - (into ids-to-delete all-children)) + components-to-delete + (reduce (fn [components id] + (let [shape (get objects id)] + (if (and (= (:component-file shape) (:id data)) ;; Main instances should exist only in local file + (:main-instance shape)) ;; but check anyway + (conj components (:component-id shape)) + components))) + [] + (into ids-to-delete all-children)) - ids-set (set ids-to-delete) + ids-set (set ids-to-delete) - guides-to-delete - (->> (:guides page) - (vals) - (filter #(contains? ids-set (:frame-id %))) - (map :id)) + guides-to-delete + (->> (:guides page) + (vals) + (filter #(contains? ids-set (:frame-id %))) + (map :id)) - changes (reduce (fn [changes guide-id] - (pcb/set-flow changes guide-id nil)) - changes - guides-to-delete) + changes (reduce (fn [changes guide-id] + (pcb/set-flow changes guide-id nil)) + changes + guides-to-delete) - changes (reduce (fn [changes component-id] + changes (reduce (fn [changes component-id] ;; It's important to delete the component before the main instance, because we ;; need to store the instance position if we want to restore it later. - (pcb/delete-component changes component-id (:id page))) - changes - components-to-delete) + (pcb/delete-component changes component-id (:id page))) + changes + components-to-delete) - changes (-> changes - (generate-update-shape-flags ids-to-hide objects {:hidden true}) - (pcb/remove-objects all-children {:ignore-touched true}) - (pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched}) - (pcb/remove-objects empty-parents) - (pcb/resize-parents all-parents) - (pcb/update-shapes groups-to-unmask - (fn [shape] - (assoc shape :masked-group false))) - (pcb/update-shapes (map :id interacting-shapes) - (fn [shape] - (d/update-when shape :interactions - (fn [interactions] - (into [] - (remove #(and (ctsi/has-destination %) - (contains? ids-to-delete (:destination %)))) - interactions))))))] - [all-parents changes])) + changes (-> changes + (generate-update-shape-flags ids-to-hide objects {:hidden true}) + (pcb/remove-objects all-children {:ignore-touched true}) + (pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched}) + (pcb/remove-objects empty-parents) + (pcb/resize-parents all-parents) + (pcb/update-shapes groups-to-unmask + (fn [shape] + (assoc shape :masked-group false))) + (pcb/update-shapes (map :id interacting-shapes) + (fn [shape] + (d/update-when shape :interactions + (fn [interactions] + (into [] + (remove #(and (ctsi/has-destination %) + (contains? ids-to-delete (:destination %)))) + interactions))))))] + [all-parents changes]))) (defn generate-relocate @@ -336,7 +343,19 @@ (map :id))) index-cell-data (when to-index (ctl/get-cell-by-index parent to-index)) - cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))] + cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)])) + + + ;; Parents that are a variant-container that becomes empty + empty-variant-cont (reduce + (fn [to-delete parent-id] + (let [parent (get objects parent-id)] + (if (and (ctk/is-variant-container? parent) + (empty? (remove (set ids) (:shapes parent)))) + (conj to-delete (:id parent)) + to-delete))) + #{} + (remove #(= % parent-id) all-parents))] (-> changes ;; Remove layout-item properties when moving a shape outside a layout @@ -444,7 +463,11 @@ (pcb/update-shapes ids #(assoc % :blocked true))) ;; Resize parent containers that need to - (pcb/resize-parents parents)))) + (pcb/resize-parents parents) + + ;; Remove parents when are a variant-container that becomes empty + (cond-> (seq empty-variant-cont) + (#(second (generate-delete-shapes % empty-variant-cont {}))))))) (defn change-show-in-viewer [shape hide?] diff --git a/common/test/common_tests/logic/variants_test.cljc b/common/test/common_tests/logic/variants_test.cljc index 22971d3078..98d40253be 100644 --- a/common/test/common_tests/logic/variants_test.cljc +++ b/common/test/common_tests/logic/variants_test.cljc @@ -9,6 +9,7 @@ [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] [app.common.logic.variant-properties :as clvp] [app.common.test-helpers.components :as thc] [app.common.test-helpers.files :as thf] @@ -234,3 +235,33 @@ (t/is (= (count (:components data')) 4)) (t/is (= (count objects) 4)) (t/is (= (count objects') 7)))) + + +(t/deftest test-delete-variant + ;; When a variant container becomes empty, it id automatically deleted + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant-two-properties :v01 :c01 :m01 :c02 :m02)) + container (ths/get-shape file :v01) + m01-id (-> (ths/get-shape file :m01) :id) + m02-id (-> (ths/get-shape file :m02) :id) + + page (thf/current-page file) + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (#(second (cls/generate-delete-shapes % #{m01-id m02-id} {})))) + + file' (thf/apply-changes file changes) + + ;; ==== Get + container' (ths/get-shape file' :v01)] + + ;; ==== Check + ;; The variant containew was not nil before the deletion + (t/is (not (nil? container))) + ;; The variant containew is nil after the deletion + (t/is (nil? container'))))