diff --git a/common/src/app/common/files/comp_processors.cljc b/common/src/app/common/files/comp_processors.cljc new file mode 100644 index 0000000000..80e782e7cc --- /dev/null +++ b/common/src/app/common/files/comp_processors.cljc @@ -0,0 +1,115 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.comp-processors + "Repair, migration or transformation utilities for components." + (:require + [app.common.logging :as log] + [app.common.types.component :as ctk] + [app.common.types.file :as ctf])) + +(log/set-level! :warn) + +(defn remove-unneeded-objects-in-components + "Some components have an :objects attribute, despite not being deleted. This removes it. + It also adds an empty :objects if it's deleted and does not have it." + [file-data] + (ctf/update-components + file-data + (fn [component] + (if (:deleted component) + (if (nil? (:objects component)) + (do + (log/warn :msg "Adding empty :objects to deleted component" + :component-id (:id component) + :component-name (:name component) + :file-id (:id file-data)) + (assoc component :objects {})) + component) + (if (contains? component :objects) + (do + (log/warn :msg "Removing :objects from non-deleted component" + :component-id (:id component) + :component-name (:name component) + :file-id (:id file-data)) + (dissoc component :objects)) + component))))) + +(defn fix-missing-swap-slots + "Locate shapes that have been swapped (i.e. their shape-ref does not point to the near match) but + they don't have a swap slot. In this case, add one pointing to the near match." + [file-data libraries] + (ctf/update-all-shapes + file-data + (fn [shape] + (if (ctk/subcopy-head? shape) + (let [container (:container (meta shape)) + file {:id (:id file-data) :data file-data} + near-match (ctf/find-near-match file container libraries shape :include-deleted? true :with-context? false)] + (if (and (some? near-match) + (not= (:shape-ref shape) (:id near-match)) + (nil? (ctk/get-swap-slot shape))) + (let [updated-shape (ctk/set-swap-slot shape (:id near-match))] + (log/warn :msg "Adding missing swap slot to shape" + :shape-id (:id shape) + :shape-name (:name shape) + :swap-slot (:id near-match) + :file-id (:id file) + :container-id (:id container) + :container-type (:type container)) + {:result :update :updated-shape updated-shape}) + {:result :keep})) + {:result :keep})))) + +(defn sync-component-id-with-ref-shape + "Ensure that all copies heads have the same component id and file as the referenced shape. + There may be bugs that cause them to get out of sync." + [file-data libraries] + (letfn [(sync-one-iteration + [file-data libraries] + (ctf/update-all-shapes + file-data + (fn [shape] + (if (and (ctk/subcopy-head? shape) (nil? (ctk/get-swap-slot shape))) + (let [container (:container (meta shape)) + file {:id (:id file-data) :data file-data} + ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true :with-context? true})] + (if (and (some? ref-shape) + (or (not= (:component-id shape) (:component-id ref-shape)) + (not= (:component-file shape) (:component-file ref-shape)))) + (let [shape' (cond-> shape + (some? (:component-id ref-shape)) + (assoc :component-id (:component-id ref-shape)) + + (nil? (:component-id ref-shape)) + (dissoc :component-id) + + (some? (:component-file ref-shape)) + (assoc :component-file (:component-file ref-shape)) + + (nil? (:component-file ref-shape)) + (dissoc :component-file))] + (log/warn :msg "Syncing component id and file with ref shape" + :shape-id (:id shape) + :shape-name (:name shape) + :component-id (:component-id shape') + :component-file (:component-file shape') + :ref-shape-id (:id ref-shape) + :file-id (:id file) + :container-id (:id container) + :container-type (:type container)) + {:result :update :updated-shape shape'}) + {:result :keep})) + {:result :keep}))))] + ;; If a copy inside a main is updated, we need to repeat the process for the change to be + ;; propagated to all copies. + (loop [current-data file-data + iteration 0] + (let [next-data (sync-one-iteration current-data libraries)] + (if (or (= current-data next-data) + (> iteration 20)) ;; safety bound + next-data + (recur next-data (inc iteration))))))) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 3655f3ece5..eeb11e9067 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.features :as cfeat] [app.common.files.changes :as cpc] + [app.common.files.comp-processors :as cfcp] [app.common.files.defaults :as cfd] [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] @@ -1786,6 +1787,24 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0018-remove-unneeded-objects-from-components" + [data _] + (cfcp/remove-unneeded-objects-in-components data)) + +(defmethod migrate-data "0019-fix-missing-swap-slots" + [data _] + (let [libraries (if (:libs data) + (deref (:libs data)) + {})] + (cfcp/fix-missing-swap-slots data libraries))) + +(defmethod migrate-data "0020-sync-component-id-with-near-main" + [data _] + (let [libraries (if (:libs data) + (deref (:libs data)) + {})] + (cfcp/sync-component-id-with-ref-shape data libraries))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -1860,4 +1879,7 @@ "0015-fix-text-attrs-blank-strings" "0015-clean-shadow-color" "0016-copy-fills-from-position-data-to-text-node" - "0017-fix-layout-flex-dir"])) + "0017-fix-layout-flex-dir" + "0018-remove-unneeded-objects-from-components" + "0019-fix-missing-swap-slots" + "0020-sync-component-id-with-near-main"])) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index 454cc78e0a..29e6d4fdf5 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -334,6 +334,31 @@ (pcb/with-file-data file-data) (pcb/update-shapes [(:id shape)] repair-shape)))) +(defmethod repair-error :component-id-mismatch + [_ {:keys [shape page-id args] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set the component-id and component-file to the ones of the near main + (log/debug :hint (str " -> set component-id to " (:component-id args))) + (log/debug :hint (str " -> set component-file to " (:component-file args))) + (cond-> shape + (some? (:component-id args)) + (assoc :component-id (:component-id args)) + + (nil? (:component-id args)) + (dissoc :component-id) + + (some? (:component-file args)) + (assoc :component-file (:component-file args)) + + (nil? (:component-file args)) + (dissoc :component-file)))] + + (log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + (defmethod repair-error :ref-shape-is-head [_ {:keys [shape page-id args] :as error} file-data _] (let [repair-shape @@ -501,7 +526,7 @@ (pcb/update-shapes [(:id shape)] repair-shape)))) (defmethod repair-error :component-nil-objects-not-allowed - [_ {:keys [shape] :as error} file-data _] + [_ {component :shape} file-data _] ; in this error the :shape argument is the component (let [repair-component (fn [component] ;; Remove the objects key, or set it to {} if the component is deleted @@ -513,10 +538,26 @@ (log/debug :hint " -> remove :objects") (dissoc component :objects))))] - (log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape)) + (log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component)) (-> (pcb/empty-changes nil) (pcb/with-library-data file-data) - (pcb/update-component (:id shape) repair-component)))) + (pcb/update-component (:id component) repair-component)))) + +(defmethod repair-error :non-deleted-component-cannot-have-objects + [_ {component :shape} file-data _] ; in this error the :shape argument is the component + (let [repair-component + (fn [component] + ; Remove the :objects field + (if-not (:deleted component) + (do + (log/debug :hint " -> remove :objects") + (dissoc component :objects)) + component))] + + (log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component)) + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/update-component (:id component) repair-component)))) (defmethod repair-error :invalid-text-touched [_ {:keys [shape page-id] :as error} file-data _] diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 5b0e1d74d4..1c16c4dcbc 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -51,6 +51,7 @@ :ref-shape-is-head :ref-shape-is-not-head :shape-ref-in-main + :component-id-mismatch :root-main-not-allowed :nested-main-not-allowed :root-copy-not-allowed @@ -59,6 +60,7 @@ :not-head-copy-not-allowed :not-component-not-allowed :component-nil-objects-not-allowed + :non-deleted-component-cannot-have-objects :instance-head-not-frame :invalid-text-touched :misplaced-slot @@ -326,6 +328,20 @@ :component-file (:component-file ref-shape) :component-id (:component-id ref-shape))))) +(defn- check-ref-component-id + "Validate that if the copy has not been swapped, the component-id and component-file are + the same as in the referenced shape in the near main." + [shape file page libraries] + (when (nil? (ctk/get-swap-slot shape)) + (when-let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)] + (when (or (not= (:component-id shape) (:component-id ref-shape)) + (not= (:component-file shape) (:component-file ref-shape))) + (report-error :component-id-mismatch + "Nested copy component-id and component-file must be the same as the near main" + shape file page + :component-id (:component-id ref-shape) + :component-file (:component-file ref-shape)))))) + (defn- check-empty-swap-slot "Validate that this shape does not have any swap slot." [shape file page] @@ -350,6 +366,19 @@ "This shape has children with the same swap slot" shape file page))) +(defn- check-required-swap-slot + "Validate that the shape has swap-slot if it's a subinstance head and the ref shape is not the + matching shape by position in the near main." + [shape file page libraries] + (let [near-match (ctf/find-near-match file page libraries shape :include-deleted? true :with-context? false)] + (when (and (some? near-match) + (not= (:shape-ref shape) (:id near-match)) + (nil? (ctk/get-swap-slot shape))) + (report-error :missing-slot + "Shape has been swapped, should have swap slot" + shape file page + :swap-slot (or (ctk/get-swap-slot near-match) (:id near-match)))))) + (defn- check-valid-touched "Validate that the text touched flags are coherent." [shape file page] @@ -418,6 +447,8 @@ (check-component-not-main-head shape file page libraries) (check-component-not-root shape file page) (check-valid-touched shape file page) + (check-ref-component-id shape file page libraries) + (check-required-swap-slot shape file page libraries) ;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached ;; so we only validate the shape-ref if the ancestor is from a valid library (when library-exists @@ -458,8 +489,7 @@ (defn- check-variant-container "Shape is a variant container, so: -all its children should be variants with variant-id equals to the shape-id - -all the components should have the same properties - " + -all the components should have the same properties" [shape file page] (let [shape-id (:id shape) shapes (:shapes shape) @@ -648,6 +678,13 @@ "Component main not allowed inside other component" main-instance file component-page)))) +(defn- check-not-objects + [component file] + (when (d/not-empty? (:objects component)) + (report-error :non-deleted-component-cannot-have-objects + "A non-deleted component cannot have shapes inside" + component file nil))) + (defn- check-component "Validate semantic coherence of a component. Report all errors found." [component file] @@ -656,7 +693,8 @@ "Objects list cannot be nil" component file nil)) (when-not (:deleted component) - (check-main-inside-main component file)) + (check-main-inside-main component file) + (check-not-objects component file)) (when (:deleted component) (check-component-duplicate-swap-slot component file) (check-ref-cycles component file)) @@ -674,8 +712,6 @@ ;; PUBLIC API: VALIDATION FUNCTIONS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare check-swap-slots) - (defn validate-file "Validate full referential integrity and semantic coherence on file data. @@ -686,8 +722,6 @@ (doseq [page (filter :id (ctpl/pages-seq data))] (check-shape uuid/zero file page libraries) - (when (str/includes? (:name file) "check-swap-slot") - (check-swap-slots uuid/zero file page libraries)) (->> (get-orphan-shapes page) (run! #(check-shape % file page libraries)))) @@ -728,40 +762,3 @@ :hint "error on validating file referential integrity" :file-id (:id file) :details errors))) - -(declare compare-slots) - -;; Optional check to look for missing swap slots. -;; Search for copies that do not point the shape-ref to the near component but don't have swap slot -;; (looking for position relative to the parent, in the copy and the main). -;; -;; This check cannot be generally enabled, because files that have been migrated from components v1 -;; may have copies with shapes that do not match by position, but have not been swapped. So we enable -;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to -;; the name of the file. -(defn- check-swap-slots - [shape-id file page libraries] - (let [shape (ctst/get-shape page shape-id)] - (if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape)) - (let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true) - container (:container (meta ref-shape))] - (when (some? ref-shape) - (compare-slots shape ref-shape file page container))) - (doall (for [child-id (:shapes shape)] - (check-swap-slots child-id file page libraries)))))) - -(defn- compare-slots - [shape-copy shape-main file container-copy container-main] - (if (and (not= (:shape-ref shape-copy) (:id shape-main)) - (nil? (ctk/get-swap-slot shape-copy))) - (report-error :missing-slot - "Shape has been swapped, should have swap slot" - shape-copy file container-copy - :swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main))) - (when (nil? (ctk/get-swap-slot shape-copy)) - (let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))] - (doall (for [[child-copy-id child-main-id] children-id-pairs] - (let [child-copy (ctst/get-shape container-copy child-copy-id) - child-main (ctst/get-shape container-main child-main-id)] - (when (and (some? child-copy) (some? child-main)) - (compare-slots child-copy child-main file container-copy container-main))))))))) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 7e703385e7..35df16aa86 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -333,7 +333,7 @@ (pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root") (assoc % :component-root true))) - :always + (some? (ctk/get-swap-slot shape)) ; First level subinstances of a detached component can't have swap-slot (pcb/update-shapes [shape-id] #(do (log/trace :msg " -> remove swap-slot") (ctk/remove-swap-slot %))) @@ -364,7 +364,7 @@ (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] (cond-> changes (some? (:shape-ref ref-shape)) - (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (advanced)") + (pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (advanced to " (:shape-ref ref-shape) ")")) (assoc % :shape-ref (:shape-ref ref-shape)))) ;; When advancing level, the normal touched groups (not swap slots) of the @@ -374,16 +374,18 @@ (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (merge touched)") + (log/trace :msg (str " (ref-shape: " (:id ref-shape) ")")) + (log/trace :msg (str " (ref touched: " (:touched ref-shape) ")")) (assoc % :touched - (clojure.set/union (:touched shape) - (ctk/normal-touched-groups ref-shape))))) + (set/union (:touched shape) + (ctk/normal-touched-groups ref-shape))))) ;; Swap slot must also be copied if the current shape has not any, ;; except if this is the first level subcopy. (and (some? (ctk/get-swap-slot ref-shape)) (nil? (ctk/get-swap-slot shape)) (not= (:id shape) shape-id)) - (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (got swap-slot)") + (pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (got swap-slot " (ctk/get-swap-slot ref-shape) ")")) (ctk/set-swap-slot % (ctk/get-swap-slot ref-shape)))) ;; If we can't get the ref-shape (e.g. it's in an external library not linked), @@ -771,14 +773,6 @@ ;; is different than the one in the near component (Shape-2-2-1) ;; but it's not touched. -(defn- redirect-shaperef ;;Set the :shape-ref of a shape pointing to the :id of its remote-shape - ([container libraries shape] - (redirect-shaperef nil nil shape (ctf/find-remote-shape container libraries shape))) - ([_ _ shape remote-shape] - (if (some? (:shape-ref shape)) - (assoc shape :shape-ref (:id remote-shape)) - shape))) - (defn generate-sync-shape-direct "Generate changes to synchronize one shape that is the root of a component instance, and all its children, from the given component." @@ -790,18 +784,12 @@ component (ctkl/get-component library (:component-id shape-inst) true)] (if (and (ctk/in-component-copy? shape-inst) (or (ctf/direct-copy? shape-inst component container nil libraries) reset?)) ; In a normal sync, we don't want to sync remote mains, only direct/near - (let [redirect-shaperef (partial redirect-shaperef container libraries) - - shape-main (when component + (let [shape-main (when component (if reset? ;; the reset is against the ref-shape, not against the original shape of the component (ctf/find-ref-shape file container libraries shape-inst) (ctf/get-ref-shape library component shape-inst))) - shape-inst (if reset? - (redirect-shaperef shape-inst shape-main) - shape-inst) - initial-root? (:component-root shape-inst) root-inst shape-inst @@ -819,8 +807,8 @@ root-inst root-main reset? - initial-root? - redirect-shaperef) + initial-root?) + ;; If the component is not found, because the master component has been ;; deleted or the library unlinked, do nothing. changes)) @@ -844,7 +832,7 @@ nil)))))) (defn- generate-sync-shape-direct-recursive - [changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef] + [changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root?] (shape-log :debug (:id shape-inst) container :msg "Sync shape direct recursive" :shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst))) @@ -891,9 +879,6 @@ children-inst (vec (ctn/get-direct-children container shape-inst)) children-main (vec (ctn/get-direct-children component-container shape-main)) - children-inst (if reset? - (map #(redirect-shaperef %) children-inst) children-inst) - only-inst (fn [changes child-inst] (shape-log :trace (:id child-inst) container :msg "Only inst" @@ -942,8 +927,7 @@ root-inst root-main reset? - initial-root? - redirect-shaperef)) + initial-root?)) swapped (fn [changes child-inst child-main] (shape-log :trace (:id child-inst) container @@ -1008,16 +992,13 @@ the values in the shape and all its children." [changes file libraries container shape-id] (shape-log :debug shape-id container :msg "Sync shape inverse" :shape (str shape-id)) - (let [redirect-shaperef (partial redirect-shaperef container libraries) - shape-inst (ctn/get-shape container shape-id) + (let [shape-inst (ctn/get-shape container shape-id) library (dm/get-in libraries [(:component-file shape-inst) :data]) component (ctkl/get-component library (:component-id shape-inst)) shape-main (when component (ctf/find-remote-shape container libraries shape-inst)) - shape-inst (redirect-shaperef shape-inst shape-main) - initial-root? (:component-root shape-inst) root-inst shape-inst @@ -1038,12 +1019,11 @@ shape-main root-inst root-main - initial-root? - redirect-shaperef) + initial-root?) changes))) (defn- generate-sync-shape-inverse-recursive - [changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef] + [changes container shape-inst component library file libraries shape-main root-inst root-main initial-root?] (shape-log :trace (:id shape-inst) container :msg "Sync shape inverse recursive" :shape (str (:name shape-inst)) @@ -1100,8 +1080,6 @@ children-main (mapv #(ctn/get-shape component-container %) (:shapes shape-main)) - children-inst (map #(redirect-shaperef %) children-inst) - only-inst (fn [changes child-inst] (add-shape-to-main changes child-inst @@ -1130,8 +1108,7 @@ child-main root-inst root-main - initial-root? - redirect-shaperef)) + initial-root?)) swapped (fn [changes child-inst child-main] (shape-log :trace (:id child-inst) container @@ -1773,6 +1750,23 @@ (pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true}) changes)) +(defn- check-swapped-main + [changes dest-shape origin-shape] + ;; Only for direct updates (from main to copy). Check if the main shape + ;; has been swapped. If so, the new component-id and component-file must + ;; be put into the copy. + (if (and (= (:shape-ref dest-shape) (:id origin-shape)) + (ctk/instance-head? dest-shape) + (ctk/instance-head? origin-shape) + (or (not= (:component-id dest-shape) (:component-id origin-shape)) + (not= (:component-file dest-shape) (:component-file origin-shape)))) + (pcb/update-shapes changes [(:id dest-shape)] + #(assoc % + :component-id (:component-id origin-shape) + :component-file (:component-file origin-shape)) + {:ignore-touched true}) + changes)) + (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy attributes that have changed in the origin shape to the dest shape. @@ -1816,6 +1810,8 @@ :always (check-detached-main dest-shape origin-shape) :always + (check-swapped-main dest-shape origin-shape) + :always (generate-update-tokens container dest-shape origin-shape touched omit-touched? nil)) (let [sync-group diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index f5c9b5a1ca..83f12fa084 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -177,8 +177,11 @@ (thc/instantiate-component component-label copy-root-label copy-root-params))) (defn add-nested-component - [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label - & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}] + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head-params]}] ;; Generated shape tree: ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] ;; :main1-child-label [:name Rect1] @@ -204,8 +207,13 @@ component2-params))) (defn add-nested-component-with-copy - [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label - & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}] + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head-label + copy2-root-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head-params + copy2-root-params]}] ;; Generated shape tree: ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] ;; :main1-child-label [:name Rect1] @@ -232,6 +240,102 @@ :nested-head-params nested-head-params) (thc/instantiate-component component2-label copy2-root-label copy2-root-params))) +(defn add-two-levels-nested-component + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head1-label + component3-label main3-root-label nested-head2-label nested-subhead2-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head1-params + component3-params main3-root-params nested-head2-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; {:main3-root-label} [:name Frame3] # [Component :component3-label] + ;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label + ;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + (-> file + (add-simple-component component1-label + main1-root-label + main1-child-label + :component-params component1-params + :root-params root1-params + :child-params main1-child-params) + (add-frame main2-root-label (merge {:name "Frame2"} + main2-root-params)) + (thc/instantiate-component component1-label + nested-head1-label + (assoc nested-head1-params + :parent-label main2-root-label)) + (thc/make-component component2-label + main2-root-label + component2-params) + (add-frame main3-root-label (merge {:name "Frame3"} + main3-root-params)) + (thc/instantiate-component component2-label + nested-head2-label + (assoc nested-head2-params + :parent-label main3-root-label + :children-labels [nested-subhead2-label])) + (thc/make-component component3-label + main3-root-label + component3-params))) + +(defn add-two-levels-nested-component-with-copy + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head1-label + component3-label main3-root-label nested-head2-label nested-subhead2-label + copy2-root-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head1-params + component3-params main3-root-params nested-head2-params + copy2-root-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; {:main3-root-label} [:name Frame3] # [Component :component3-label] + ;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label + ;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; :copy2-label [:name Frame3] #--> [Component :component3-label] :main3-root-label + ;; [:name Frame2] @--> [Component :component2-label] :nested-head2-label + ;; [:name Frame1] @--> [Component :component1-label] :nested-subhead2-label + ;; [:name Rect1] ---> + (-> file + (add-two-levels-nested-component component1-label + main1-root-label + main1-child-label + component2-label + main2-root-label + nested-head1-label + component3-label + main3-root-label + nested-head2-label + nested-subhead2-label + :component1-params component1-params + :root1-params root1-params + :main1-child-params main1-child-params + :component2-params component2-params + :main2-root-params main2-root-params + :nested-head1-params nested-head1-params + :component3-params component3-params + :main3-root-params main3-root-params + :nested-head2-params nested-head2-params) + (thc/instantiate-component component3-label copy2-root-label copy2-root-params))) + ;; ----- Getters (defn bottom-shape-by-id @@ -274,15 +378,18 @@ file-id {file-id file} file-id))] - (thf/apply-changes file changes))) + (thf/apply-changes file changes :validate? false))) -(defn swap-component +(defn swap-component- "Swap the specified shape by the component specified by component-tag" - [file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}] + [file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}] (let [page (if page-label (thf/get-page file page-label) (thf/current-page file)) - libraries {(:id file) file} + libraries (cond-> {(:id file) file} + (some? library) + (assoc (:id library) library)) + library (or library file) orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape))) @@ -290,10 +397,10 @@ (cll/generate-component-swap (pcb/empty-changes) (:objects page) shape - (:data file) + (:data library) page libraries - (-> (thc/get-component file component-tag) + (-> (thc/get-component library component-tag) :id) 0 nil @@ -305,26 +412,36 @@ [changes nil]) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (when new-shape-label (thi/rm-id! (:id new-shape)) (thi/set-id! new-shape-label (:id new-shape))) (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) -(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}] - (swap-component file (ths/get-shape file shape-tag :page-label page-label) component-tag :page-label page-label :propagate-fn propagate-fn)) +(defn swap-component-in-shape + [file shape-tag component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}] + (swap-component- file (ths/get-shape file shape-tag :page-label page-label) + component-tag + :page-label page-label + :propagate-fn propagate-fn + :keep-touched? keep-touched? + :new-shape-label new-shape-label + :library library)) -(defn swap-component-in-first-child [file shape-tag component-tag & {:keys [page-label propagate-fn]}] +(defn swap-component-in-first-child + [file shape-tag component-tag & {:keys [page-label propagate-fn library]}] (let [first-child-id (->> (ths/get-shape file shape-tag :page-label page-label) :shapes first)] - (swap-component file - (ths/get-shape-by-id file first-child-id :page-label page-label) - component-tag - :page-label page-label - :propagate-fn propagate-fn))) + (swap-component- file + (ths/get-shape-by-id file first-child-id :page-label page-label) + component-tag + :page-label page-label + :propagate-fn propagate-fn + :library library))) (defn update-color "Update the first fill color for the shape identified by shape-tag" @@ -339,9 +456,10 @@ (assoc shape :fills (ths/sample-fills-color :fill-color color))) (:objects page) {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn update-bottom-color @@ -357,9 +475,10 @@ (assoc shape :fills (ths/sample-fills-color :fill-color color))) (:objects page) {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn reset-overrides [file shape & {:keys [page-label propagate-fn]}] @@ -374,9 +493,10 @@ {file-id file} (ctn/make-container container :page) (:id shape))) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}] @@ -398,9 +518,10 @@ #{(-> (ths/get-shape file shape-tag :page-label page-label) :id)} {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}] @@ -419,8 +540,9 @@ (:id file)) ;; file-id (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects #{(:id shape)})) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc index a80675b65a..6357ab555b 100644 --- a/common/src/app/common/test_helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -54,12 +54,14 @@ ([file] (validate-file! file {})) ([file libraries] (cfv/validate-file-schema! file) - (cfv/validate-file! file libraries))) + (cfv/validate-file! file libraries) + file)) (defn apply-changes - [file changes] + [file changes & {:keys [validate?] :or {validate? true}}] (let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))] - (validate-file! file') + (when validate? + (validate-file! file')) file')) (defn apply-undo-changes diff --git a/common/src/app/common/test_helpers/shapes.cljc b/common/src/app/common/test_helpers/shapes.cljc index b212984e06..d557c0501f 100644 --- a/common/src/app/common/test_helpers/shapes.cljc +++ b/common/src/app/common/test_helpers/shapes.cljc @@ -82,6 +82,18 @@ (:id page) #(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) +(defn update-shape-by-id + [file shape-id attr val & {:keys [page-label]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + shape (ctst/get-shape page shape-id)] + (update file :data + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) + (defn update-shape-text [file shape-label attr val & {:keys [page-label]}] (let [page (if page-label diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index d07ffaeb50..ecc6e30c65 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -163,11 +163,15 @@ Note that design tokens also are involved, although they go by an alternate route and thus they are not part of :sync-attrs. Also when detaching a nested copy it also needs to trigger a synchronization, - even though :shape-ref is not a synced attribute per se" + even though :shape-ref, :component-id or :component-file are not synced + attributes per se." [attr] (or (contains? sync-attrs attr) (= :shape-ref attr) - (= :applied-tokens attr))) + (= :applied-tokens attr) + (= :component-id attr) + (= :component-file attr) + (= :component-root attr))) (defn instance-root? "Check if this shape is the head of a top instance." diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index c4f3a66063..be92b16999 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -60,6 +60,9 @@ (some? objects) (assoc :objects objects) + (nil? objects) + (dissoc :objects) + (some? modified-at) (assoc :modified-at modified-at) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index df9ce86be2..4e6021dbb6 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -55,6 +55,10 @@ [page-or-component type] (assoc page-or-component :type type)) +(defn unmake-container + [container] + (dissoc container :type)) + (defn page? [container] (= (:type container) :page)) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 3733359a6c..974db477b3 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -204,7 +204,8 @@ (defn update-file-data [file f] - (update file :data f)) + (when file + (update file :data f))) (defn containers-seq "Generate a sequence of all pages and all components, wrapped as containers" @@ -225,6 +226,85 @@ (ctpl/update-page file-data (:id container) f) (ctkl/update-component file-data (:id container) f))) +(defn update-pages + "Update all pages inside the file" + [file-data f] + (update file-data :pages-index d/update-vals + (fn [page] + (-> page + (ctn/make-container :page) + (f) + (ctn/unmake-container))))) + +(defn update-components + "Update all components inside the file" + [file-data f] + (d/update-when file-data :components d/update-vals + (fn [component] + (-> component + (ctn/make-container :component) + (f) + (ctn/unmake-container))))) + +(defn update-containers + "Update all pages and components inside the file" + [file-data f] + (-> file-data + (update-pages f) + (update-components f))) + +(defn update-objects-tree + "Do a depth-first traversal of the shapes in a container, doing different kinds of updates. + The function f receives a shape with a context metadata with the container. + It must return a map with the following keys: + - :result -> :keep, :update or :remove + - :updated-shape -> the updated shape if result is :update" + [container f] + (letfn [(update-shape-recursive + [container shape-id] + (let [shape (ctst/get-shape container shape-id)] + (when (not shape) + (throw (ex-info "Shape not found" {:shape-id shape-id}))) + (let [shape (with-meta shape {:container container}) + + {:keys [result updated-shape]} (f shape) + + container' + (case result + :keep + container + + :update + (ctst/set-shape container updated-shape) + + :remove + (ctst/delete-shape container shape-id true) + + (throw (ex-info "Invalid result from update function" {:result result})))] + + (if (= result :remove) + container' + (reduce update-shape-recursive + container' + (:shapes shape))))))] + + (let [root-id (if (ctn/page? container) + uuid/zero + (:main-instance-id container))] + + (if-not (empty? (:objects container)) + (update-shape-recursive container root-id) + container)))) + +(defn update-all-shapes + "Update all shapes in the file data, using the update-objects-tree function for each container" + [file-data f] + (when file-data + (update-containers + file-data + (fn [container] + (update-objects-tree container f))))) + ;; Asset helpers (defn find-component-file [file libraries component-file] @@ -328,6 +408,27 @@ (get-ref-shape (:data component-file) component shape :with-context? with-context?))))] (some find-ref-shape-in-head (ctn/get-parent-heads (:objects container) shape)))) +(defn find-near-match + "Locate the shape that occupies the same position in the near main component. + This will be the ref-shape except if the shape is a copy subhead that has been + swapped. In this case, the near match will be the ref-shape that was before + the swap." + [file container libraries shape & {:keys [include-deleted? with-context?] :or {include-deleted? false with-context? false}}] + (let [parent-shape (ctst/get-shape container (:parent-id shape)) + parent-ref-shape (when parent-shape + (find-ref-shape file container libraries parent-shape :include-deleted? include-deleted? :with-context? true)) + ref-container (when parent-ref-shape + (:container (meta parent-ref-shape))) + shape-index (when parent-shape + (d/index-of (:shapes parent-shape) (:id shape))) + near-match-id (when (and parent-ref-shape shape-index) + (get (:shapes parent-ref-shape) shape-index)) + near-match (when near-match-id + (cond-> (ctst/get-shape ref-container near-match-id) + with-context? + (with-meta (meta parent-ref-shape))))] + near-match)) + (defn advance-shape-ref "Get the shape-ref of the near main of the shape, recursively repeated as many times as the given levels." diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 92732e18a1..3944d96afb 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -16,8 +16,6 @@ [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid])) - -;; FIXME: the order of arguments seems arbitrary, container should be a first artgument (defn add-shape "Insert a shape in the tree, at the given index below the given parent or frame. Update the parent as needed." diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 9b21f8f796..ed542d868b 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -60,8 +60,9 @@ :cljs (uuid (impl/v4)))) (defn custom - ([a] #?(:clj (UUID. 0 a) :cljs (uuid (impl/custom 0 a)))) - ([b a] #?(:clj (UUID. b a) :cljs (uuid (impl/custom b a))))) + "Generate a uuid using directly the given number (specified as one or two long integers)" + ([low] #?(:clj (UUID. 0 low) :cljs (uuid (impl/custom 0 low)))) + ([high low] #?(:clj (UUID. high low) :cljs (uuid (impl/custom high low))))) (def zero (uuid "00000000-0000-0000-0000-000000000000")) @@ -137,6 +138,22 @@ (+ (clojure.lang.Murmur3/hashLong a) (clojure.lang.Murmur3/hashLong b))))) +;; Fake uuids generator +(def ^:private fake-ids (atom 0)) + +(defn reset-fake! + "Reset the fake uuid counter to 0, for reproducible results across tests." + [] + (reset! fake-ids 0)) + +(defn next-fake + "When you need predictable uuids, for example when debugging a failing test, wrap the code with + (with-redefs [uuid/next uuid/next-fake] + ...tested code...)" + [] + (-> (swap! fake-ids inc) + (custom))) + ;; Commented code used for debug ;; #?(:cljs ;; (defn ^:export test-uuid diff --git a/common/test/common_tests/files/comp_processors_test.cljc b/common/test/common_tests/files/comp_processors_test.cljc new file mode 100644 index 0000000000..c1cabbbb72 --- /dev/null +++ b/common/test/common_tests/files/comp_processors_test.cljc @@ -0,0 +1,787 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.files.comp-processors-test + (:require + [app.common.data :as d] + [app.common.files.comp-processors :as cfcp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.file :as ctf] + [clojure.test :as t])) + +(t/deftest test-remove-unneeded-objects-in-components + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with non deleted components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with deleted components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (tho/delete-shape :frame1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with non deleted components with :objects nil should remove it" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/update-component :component1 {:objects nil})) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with non deleted components with :objects should remove it" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/update-component :component1 {:objects {:sample 777}})) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {:objects + [{:sample 777} nil]}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with deleted components without :objects should add an empty one" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (tho/delete-shape :frame1) + (ctf/update-file-data + (fn [file-data] + (ctkl/update-component file-data (thi/id :component1) #(dissoc % :objects))))) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {:objects + [nil {}]}}}}] + + (t/is (= expected-diff diff))))) + +(t/deftest test-fix-missing-swap-slots + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with nested not swapped components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a normally swapped copy should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2-root :component3)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a swapped nested copy in a main should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2))) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a swapped copy with broken slot should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; NO SWAP SLOT + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2-root :component3) + (ths/update-shape :copy2-nested-head :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with a swapped copy inside a main with broken slot has no effect since it cannot be distinguished" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; NO SWAP SLOT + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2)) + (ths/update-shape :nested-head :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a two levels nested copy in a main swapped with broken slot should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :main4-child [:name Rect4] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame4] @--> [Component :component4] :main4-root + ;; NO SWAP SLOT + ;; [:name Rect4] ---> :main4-child + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame4] @--> [Component :component4] :nested-subhead2 + ;; [:name Rect4] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + (tho/add-simple-component :component4 :main4-root :main4-child + :root-params {:name "Frame4"} + :child-params {:name "Rect4"}) + (tho/swap-component-in-shape :nested-subhead2 :component4 + :propagate-fn #(tho/propagate-component-changes % :component3)) + (ths/update-shape :nested-subhead2 :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :nested-subhead2) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "when components are in external libraries, the fix still works well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; NO SWAP SLOT + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (tho/swap-component-in-first-child :copy2 :component4 :library library2) + (ths/update-shape :copy2-nested-head :touched nil)) + + libraries {(:id library1) library1 + (:id library2) library2} + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % libraries)) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}] + + (t/is (= expected-diff diff))))) + +(t/deftest test-sync-component-id-with-ref-shape + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with valid normal components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame1] @--> [Component :component1] :nested-head1 + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + #_(thf/dump-file file') ;; Uncomment to debug + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with valid swapped components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + ;; + ;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy3-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head]) + (tho/swap-component-in-first-child :copy3-root :component3)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + #_(thf/dump-file file') ;; Uncomment to debug + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a non swapped copy with broken component id/file should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component ] :nested-head ## <- BAD component-id + ;; [:name Rect1] ---> + ;; + ;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy3-nested-head [:name Frame1] @--> [Component ] :nested-head ## <- BAD component-file + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head]) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy3-nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)]} + (thi/id :copy3-nested-head) + {:component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with a copy of a swapped main with broken component id/file should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component: ] :nested-head ## <- BAD component-id/file + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2)) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component3)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with multiple copies of same component should sync all" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/instantiate-component :component1 :copy1-root :children-labels [:copy1-child]) + (thc/instantiate-component :component1 :copy2-root :children-labels [:copy2-child]) + (ths/update-shape :copy1-child :component-id (thi/new-id! :wrong-id1)) + (ths/update-shape :copy2-child :component-id (thi/new-id! :wrong-id2))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file')] + + ;; Both copies should be corrected + (t/is (contains? diff :data)) + (t/is (contains? (get-in diff [:data :pages-index]) (thf/current-page-id file))))) + + (t/testing "file with a copy root with broken component id/file cannot be repaired. But it's propagated to copies." + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component ] :main1-root ## <- BAD component-id/file + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (ths/update-shape :nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :component1) (thi/id :some-other-id)] + :component-file + [(thi/id :file1) (thi/id :some-other-file)]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with a 2nd nested copy inside a main with broken component/id should have it repaired, and propagated to copies" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame1] @--> [Component ] :nested-head1 ## <- BAD component-id/file + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :nested-subhead2 :component-file (thi/new-id! :some-other-file))) + + copy2-root (ths/get-shape file :copy2-root) + copy2-root-child1 (ths/get-shape-by-id file (first (:shapes copy2-root))) + copy2-root-child2 (ths/get-shape-by-id file (first (:shapes copy2-root-child1))) + file (-> file + (ths/update-shape-by-id (:id copy2-root-child2) :component-id (thi/id :some-other-id)) + (ths/update-shape-by-id (:id copy2-root-child2) :component-file (thi/id :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :nested-subhead2) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]} + (:id copy2-root-child2) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "when components are in external libraries, the fix still works well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; :nested4-head [:name Frame3] @--> [Component: component3] :main3-root + ;; :nested4-child [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + library1 + (tho/swap-component-in-shape library1 :nested2-head :component4 :library library2) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component ] :main4-root ## <- BAD component-id/file + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file))) + + libraries {(:id library1) library1 + (:id library2) library2} + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % libraries)) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component4)] + :component-file + [(thi/id :some-other-file) (thi/id :library2)]}}}}}}] + + #_(thf/dump-file library2) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with several broken ids should propagate to all copies" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component ] :main2-root ## <- BAD component-id + ;; :nested-subhead2 [:name Frame1] @--> [Component ] :nested-head1 ## <- BAD component-id + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + ;; Corrupt both levels + (ths/update-shape :nested-head2 :component-id (thi/new-id! :wrong-comp2)) + (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :wrong-comp3))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + copy2-root (ths/get-shape file' :copy2-root) + copy2-root-child1 (ths/get-shape-by-id file' (first (:shapes copy2-root))) + copy2-root-child2 (ths/get-shape-by-id file' (first (:shapes copy2-root-child1))) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(:id copy2-root-child1) + {:component-id [(thi/id :component2) (thi/id :wrong-comp2)]} + (:id copy2-root-child2) + {:component-id [(thi/id :component1) (thi/id :wrong-comp3)]}}}}}}] + + (thf/dump-file file') ;; Uncomment to debug + (t/is (= expected-diff diff))))) + diff --git a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc index 9460a3b91c..143221a4d3 100644 --- a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc +++ b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc @@ -465,9 +465,10 @@ page {(:id file) file} (thi/id :nested-h-ellipse)) - file' (-> (thf/apply-changes file changes) + file' (-> (thf/apply-changes file changes :validate? false) (tho/propagate-component-changes :c-board-with-ellipse) - (tho/propagate-component-changes :c-big-board)) + (tho/propagate-component-changes :c-big-board) + (thf/validate-file!)) ;; ==== Get nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse) diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc index 23894cc398..649b25e757 100644 --- a/common/test/common_tests/logic/comp_reset_test.cljc +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -349,4 +349,73 @@ (t/is (= (:fill-color fill') "#FFFFFF")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)) - (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file + (t/is (= (:touched copy2-child') nil)))) + +(t/deftest test-reset-with-propagation-updates-copies + ;; When a nested copy inside a main component has an override and we + ;; reset it passing a propagate-fn, the reset must be propagated to + ;; all copies of that component so they reflect the canonical color. + (let [;; ==== Setup + file + (-> (thf/sample-file :file1) + ;; component1: main1-root / main1-child (fill "#aabbcc") + ;; component2: main2-root contains nested-head (instance of component1) + ;; copy2-root: copy of component2 + (tho/add-nested-component-with-copy + :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root + :main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")} + :copy2-root-params {:children-labels [:copy2-nested-head]})) + + propagate-fn (fn [f] + (-> f + (tho/propagate-component-changes :component1) + (tho/propagate-component-changes :component2))) + + ;; ==== Action – override the nested-head color, then reset it with propagation + file' + (-> file + (tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn) + (tho/reset-overrides (ths/get-shape file :nested-head) :propagate-fn propagate-fn)) + + ;; ==== Get + copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)] + + ;; ==== Check + ;; After reset + propagation the copy should mirror the canonical color + (t/is (= copy2-bottom-color "#aabbcc")))) + +(t/deftest test-reset-without-propagation-does-not-update-copies + ;; This is the regression test for the misplaced-parenthesis bug: when + ;; propagate-fn is NOT passed to reset-overrides the copies of the component + ;; must still hold the overridden value because the component sync never ran. + (let [;; ==== Setup + file + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy + :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root + :main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")} + :copy2-root-params {:children-labels [:copy2-nested-head]})) + + propagate-fn (fn [f] + (-> f + (tho/propagate-component-changes :component1) + (tho/propagate-component-changes :component2))) + + ;; ==== Action – override the nested-head color, then reset WITHOUT propagation + file' + (-> file + (tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn) + ;; Reset without propagate-fn: the component definition is updated but + ;; the change is never pushed to the copy. + (tho/reset-overrides (ths/get-shape file :nested-head))) + + ;; ==== Get + copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)] + + ;; ==== Check + ;; Without propagation the copy still reflects the overridden color + (t/is (= copy2-bottom-color "#fabada")))) \ No newline at end of file diff --git a/common/test/common_tests/logic/duplicated_pages_test.cljc b/common/test/common_tests/logic/duplicated_pages_test.cljc index d1bafb88d7..57dd490143 100644 --- a/common/test/common_tests/logic/duplicated_pages_test.cljc +++ b/common/test/common_tests/logic/duplicated_pages_test.cljc @@ -64,9 +64,8 @@ (reset-all-overrides [file] (-> file - (tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1) - (tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2) - (propagate-all-component-changes))) + (tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1 :propagate-fn propagate-all-component-changes) + (tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2 :propagate-fn propagate-all-component-changes))) (fill-colors [file] [(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1) diff --git a/common/test/common_tests/logic/multiple_nesting_levels_test.cljc b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc index 43b7c7ef0e..11276ceb85 100644 --- a/common/test/common_tests/logic/multiple_nesting_levels_test.cljc +++ b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc @@ -6,20 +6,11 @@ (ns common-tests.logic.multiple-nesting-levels-test (:require - [app.common.files.changes :as ch] - [app.common.files.changes-builder :as pcb] - [app.common.logic.libraries :as cll] - [app.common.logic.shapes :as cls] - [app.common.pprint :as pp] [app.common.test-helpers.components :as thc] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -56,10 +47,9 @@ (reset-all-overrides [file] (-> file - (tho/reset-overrides (ths/get-shape file :copy-simple-1)) - (tho/reset-overrides (ths/get-shape file :copy-frame-composed-1)) - (tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy)) - (propagate-all-component-changes))) + (tho/reset-overrides (ths/get-shape file :copy-simple-1) :propagate-fn propagate-all-component-changes) + (tho/reset-overrides (ths/get-shape file :copy-frame-composed-1) :propagate-fn propagate-all-component-changes) + (tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy) :propagate-fn propagate-all-component-changes))) (fill-colors [file] [(tho/bottom-fill-color file :frame-simple-1) diff --git a/common/test/common_tests/logic/swap_as_override_test.cljc b/common/test/common_tests/logic/swap_as_override_test.cljc index a4a1b5a632..519b24e48f 100644 --- a/common/test/common_tests/logic/swap_as_override_test.cljc +++ b/common/test/common_tests/logic/swap_as_override_test.cljc @@ -6,20 +6,12 @@ (ns common-tests.logic.swap-as-override-test (:require - [app.common.files.changes :as ch] - [app.common.files.changes-builder :as pcb] - [app.common.logic.libraries :as cll] - [app.common.logic.shapes :as cls] - [app.common.pprint :as pp] + [app.common.data :as d] [app.common.test-helpers.components :as thc] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -27,23 +19,40 @@ (defn- setup [] (-> (thf/sample-file :file1) - (tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")}) - (tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")}) - (tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")}) + (tho/add-simple-component :component-1 :frame-component-1 :child-component-1 + :root-params {:name "component-1"} + :child-params {:name "child-component-1" + :type :rect + :fills (ths/sample-fills-color :fill-color "#111111")}) + (tho/add-simple-component :component-2 :frame-component-2 :child-component-2 + :root-params {:name "component-2"} + :child-params {:name "child-component-2" + :type :rect + :fills (ths/sample-fills-color :fill-color "#222222")}) + (tho/add-simple-component :component-3 :frame-component-3 :child-component-3 + :root-params {:name "component-3"} + :child-params {:name "child-component-3" + :type :rect + :fills (ths/sample-fills-color :fill-color "#333333")}) - (tho/add-frame :frame-icon-and-text) - (thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text]) + (tho/add-frame :frame-icon-and-text :name "copy-component-1") + (thc/instantiate-component :component-1 :copy-component-1 + :parent-label :frame-icon-and-text + :children-labels [:component-1-icon-and-text]) (ths/add-sample-shape :text {:type :text :name "icon+text" :parent-label :frame-icon-and-text}) (thc/make-component :icon-and-text :frame-icon-and-text) - (tho/add-frame :frame-panel) - (thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel]) + (tho/add-frame :frame-panel :name "icon-and-text") + (thc/instantiate-component :icon-and-text :copy-icon-and-text + :parent-label :frame-panel + :children-labels [:icon-and-text-panel]) (thc/make-component :panel :frame-panel) - (thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel]))) + (thc/instantiate-component :panel :copy-panel + :children-labels [:copy-icon-and-text-panel]))) (defn- propagate-all-component-changes [file] (-> file diff --git a/common/test/common_tests/logic/swap_keeps_id_test.cljc b/common/test/common_tests/logic/swap_keeps_id_test.cljc index 6ecc3583b2..e6478beeb1 100644 --- a/common/test/common_tests/logic/swap_keeps_id_test.cljc +++ b/common/test/common_tests/logic/swap_keeps_id_test.cljc @@ -30,7 +30,7 @@ copy (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy :circle {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :circle {:new-shape-label :copy02 :keep-touched? true}) copy' (ths/get-shape file' :copy02)] ;; Both copies have the same id diff --git a/common/test/common_tests/logic/variants_switch_test.cljc b/common/test/common_tests/logic/variants_switch_test.cljc index f01da5f268..c991f35ab6 100644 --- a/common/test/common_tests/logic/variants_switch_test.cljc +++ b/common/test/common_tests/logic/variants_switch_test.cljc @@ -35,7 +35,7 @@ copy01 (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy01' (ths/get-shape file' :copy02)] (thf/dump-file file :keys [:width]) @@ -61,7 +61,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -100,7 +100,7 @@ copy01 (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy01' (ths/get-shape file' :copy02)] (thf/dump-file file :keys [:width]) @@ -137,7 +137,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -180,7 +180,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -257,25 +257,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -283,10 +277,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -387,25 +381,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -413,10 +401,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -515,25 +503,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -541,10 +523,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -645,25 +627,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -671,10 +647,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -774,14 +750,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -789,7 +763,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -797,9 +770,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -908,14 +881,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -923,7 +894,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -931,9 +901,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1038,14 +1008,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -1053,7 +1021,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -1061,9 +1028,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1169,14 +1136,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -1184,7 +1149,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -1192,9 +1156,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1290,7 +1254,6 @@ :children-labels [:copy-cp01])) page (thf/current-page file) - copy01 (ths/get-shape file :copy01) copy-cp01 (ths/get-shape file :copy-cp01) copy-cp01-rect-id (-> copy-cp01 :shapes first) @@ -1309,7 +1272,7 @@ ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02 (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape-by-id file' (-> copy02 :shapes first)) copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))] @@ -1337,17 +1300,16 @@ :children-labels [:copy-cp01])) copy01 (ths/get-shape file :copy01) - copy-cp01 (ths/get-shape file :copy-cp01) external02 (thc/get-component file :external02) ;; On :c01, swap the copy of :external01 for a copy of :external02 file (-> file - (tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) + (tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) copy-cp02 (ths/get-shape file :copy-cp02) ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02' (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape file' :copy-cp02)] @@ -1376,12 +1338,11 @@ page (thf/current-page file) copy01 (ths/get-shape file :copy01) - copy-cp01 (ths/get-shape file :copy-cp01) external02 (thc/get-component file :external02) ;; On :c01, swap the copy of :external01 for a copy of :external02 file (-> file - (tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) + (tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) copy-cp02 (ths/get-shape file :copy-cp02) copy-cp02-rect-id (-> copy-cp02 :shapes first) @@ -1396,7 +1357,7 @@ ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02' (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape file' :copy-cp02) @@ -1463,7 +1424,7 @@ ;; ==== Action - file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true}) + file' (tho/swap-component-in-shape file :c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true}) page' (thf/current-page file') c02-in-copy' (ths/get-shape file' :c02-in-copy) @@ -1515,7 +1476,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1564,7 +1525,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1613,7 +1574,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1660,7 +1621,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1714,7 +1675,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1763,7 +1724,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1812,7 +1773,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1859,7 +1820,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1910,7 +1871,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1956,7 +1917,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2023,7 +1984,7 @@ text01 (get-in page [:objects (:id text01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2055,7 +2016,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - Try to switch to a component with different shape type - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2098,7 +2059,7 @@ path01 (get-in page [:objects (:id path01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2146,7 +2107,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2190,7 +2151,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2243,7 +2204,7 @@ old-position-data (:position-data text01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2306,7 +2267,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2357,7 +2318,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2411,7 +2372,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2468,7 +2429,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2532,7 +2493,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2588,7 +2549,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2653,7 +2614,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2710,7 +2671,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) diff --git a/common/test/common_tests/types/components_test.cljc b/common/test/common_tests/types/components_test.cljc index 36394f29a2..684d45db99 100644 --- a/common/test/common_tests/types/components_test.cljc +++ b/common/test/common_tests/types/components_test.cljc @@ -6,9 +6,13 @@ (ns common-tests.types.components-test (:require + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] + [app.common.types.file :as ctf] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -39,3 +43,357 @@ (t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) (t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) (t/is (nil? (ctk/get-swap-slot s6))))) + +(t/deftest test-find-near-match + + (t/testing "shapes not in a component have no near match" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + page (thf/current-page file) + + frame1 (ths/get-shape file :frame1) + shape1 (ths/get-shape file :shape1) + + near-match1 (ctf/find-near-match file page {} frame1) + near-match2 (ctf/find-near-match file page {} shape1)] + + (t/is (nil? near-match1)) + (t/is (nil? near-match2)))) + + (t/testing "shapes in a copy get the ref-shape" + (let [file + ;; {:main-root} [:name Frame1] # [Component :component1] + ;; :main-child1 [:name Rect1] + ;; :main-child2 [:name Rect2] + ;; :main-child3 [:name Rect3] + ;; + ;; :copy-root [:name Frame1] #--> [Component :component1] :main-root + ;; [:name Rect1] ---> :main-child1 + ;; [:name Rect2] ---> :main-child2 + ;; [:name Rect3] ---> :main-child3 + (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root [:main-child1 :main-child2 :main-child3] + :copy-root)) + + page (thf/current-page file) + + main-root (ths/get-shape file :main-root) + main-child1 (ths/get-shape file :main-child1) + main-child2 (ths/get-shape file :main-child2) + main-child3 (ths/get-shape file :main-child3) + copy-root (ths/get-shape file :copy-root) + copy-child1 (ths/get-shape-by-id file (nth (:shapes copy-root) 0)) + copy-child2 (ths/get-shape-by-id file (nth (:shapes copy-root) 1)) + copy-child3 (ths/get-shape-by-id file (nth (:shapes copy-root) 2)) + + near-main-root (ctf/find-near-match file page {} main-root) + near-main-child1 (ctf/find-near-match file page {} main-child1) + near-main-child2 (ctf/find-near-match file page {} main-child2) + near-main-child3 (ctf/find-near-match file page {} main-child3) + near-copy-root (ctf/find-near-match file page {} copy-root) + near-copy-child1 (ctf/find-near-match file page {} copy-child1) + near-copy-child2 (ctf/find-near-match file page {} copy-child2) + near-copy-child3 (ctf/find-near-match file page {} copy-child3)] + + (t/is (nil? near-main-root)) + (t/is (nil? near-main-child1)) + (t/is (nil? near-main-child2)) + (t/is (nil? near-main-child3)) + (t/is (nil? near-copy-root)) + (t/is (= (:id near-copy-child1) (thi/id :main-child1))) + (t/is (= (:id near-copy-child2) (thi/id :main-child2))) + (t/is (= (:id near-copy-child3) (thi/id :main-child3))))) + + (t/testing "shapes in nested not swapped copies get the ref-shape" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested-child [:name Rect1] ---> :main1-child + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head + ;; :copy2-nested-child [:name Rect1] ---> :nested-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :nested-head-params {:children-labels [:nested-child]}) + (thc/instantiate-component :component2 :copy2 + :children-labels [:copy2-nested-head :copy2-nested-child])) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested-head (ths/get-shape file :nested-head) + nested-child (ths/get-shape file :nested-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested-child (ths/get-shape file :copy2-nested-child) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested-head (ctf/find-near-match file page {} nested-head) + near-nested-child (ctf/find-near-match file page {} nested-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested-head)) + (t/is (= (:id near-nested-child) (thi/id :main1-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested-head))) + (t/is (= (:id near-copy2-nested-child) (thi/id :nested-child))))) + + (t/testing "shapes in swapped copies get the swap slot" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested-child [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :nested-head-params {:children-labels [:nested-child]}) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2 :component3)) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested-head (ths/get-shape file :nested-head) + nested-child (ths/get-shape file :nested-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested-child (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested-head (ctf/find-near-match file page {} nested-head) + near-nested-child (ctf/find-near-match file page {} nested-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested-head)) + (t/is (= (:id near-nested-child) (thi/id :main1-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested-head))) + (t/is (= (:id near-copy2-nested-child) (thi/id :main3-child))))) + + (t/testing "shapes in second level nested copies under swapped get the shape in the new main" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]}) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]}) + (tho/swap-component-in-first-child :copy2 :component4)) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested2-head (ths/get-shape file :nested2-head) + nested2-child (ths/get-shape file :nested2-child) + main3-root (ths/get-shape file :main3-root) + main3-child (ths/get-shape file :main3-child) + main4-root (ths/get-shape file :main4-root) + nested4-head (ths/get-shape file :nested4-head) + nested4-child (ths/get-shape file :nested4-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head))) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested2-head (ctf/find-near-match file page {} nested2-head) + near-nested2-child (ctf/find-near-match file page {} nested2-child) + near-main3-root (ctf/find-near-match file page {} main3-root) + near-main3-child (ctf/find-near-match file page {} main3-child) + near-main4-root (ctf/find-near-match file page {} main4-root) + near-nested4-head (ctf/find-near-match file page {} nested4-head) + near-nested4-child (ctf/find-near-match file page {} nested4-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested4-head (ctf/find-near-match file page {} copy2-nested4-head) + near-copy2-nested4-child (ctf/find-near-match file page {} copy2-nested4-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested2-head)) + (t/is (= (:id near-nested2-child) (thi/id :main1-child))) + (t/is (nil? near-main3-root)) + (t/is (nil? near-main3-child)) + (t/is (nil? near-main4-root)) + (t/is (nil? near-nested4-head)) + (t/is (= (:id near-nested4-child) (thi/id :main3-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head))) + (t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head))) + (t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child))))) + + (t/testing "component in external libraries still work well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (tho/swap-component-in-first-child :copy2 :component4 :library library2)) + + page-library1 (thf/current-page library1) + page-library2 (thf/current-page library2) + page-file (thf/current-page file) + libraries {(:id library1) library1 + (:id library2) library2} + + main1-root (ths/get-shape library1 :main1-root) + main1-child (ths/get-shape library1 :main1-child) + main2-root (ths/get-shape library1 :main2-root) + nested2-head (ths/get-shape library1 :nested2-head) + nested2-child (ths/get-shape library1 :nested2-child) + main3-root (ths/get-shape library2 :main3-root) + main3-child (ths/get-shape library2 :main3-child) + main4-root (ths/get-shape library2 :main4-root) + nested4-head (ths/get-shape library2 :nested4-head) + nested4-child (ths/get-shape library2 :nested4-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head))) + + near-main1-root (ctf/find-near-match file page-file libraries main1-root) + near-main1-child (ctf/find-near-match file page-file libraries main1-child) + near-main2-root (ctf/find-near-match file page-file libraries main2-root) + near-nested2-head (ctf/find-near-match library1 page-library1 libraries nested2-head) + near-nested2-child (ctf/find-near-match library1 page-library1 libraries nested2-child) + near-main3-root (ctf/find-near-match file page-file libraries main3-root) + near-main3-child (ctf/find-near-match file page-file libraries main3-child) + near-main4-root (ctf/find-near-match file page-file libraries main4-root) + near-nested4-head (ctf/find-near-match library2 page-library2 libraries nested4-head) + near-nested4-child (ctf/find-near-match library2 page-library2 libraries nested4-child) + near-copy2 (ctf/find-near-match file page-file libraries copy2) + near-copy2-nested-head (ctf/find-near-match file page-file libraries copy2-nested-head) + near-copy2-nested4-head (ctf/find-near-match file page-file libraries copy2-nested4-head) + near-copy2-nested4-child (ctf/find-near-match file page-file libraries copy2-nested4-child)] + + (thf/dump-file library1 :keys [:name :swap-slot-label] :show-refs? true) + (t/is (some? main1-root)) + (t/is (some? main1-child)) + (t/is (some? main2-root)) + (t/is (some? nested2-head)) + (t/is (some? nested2-child)) + (t/is (some? main3-root)) + (t/is (some? main3-child)) + (t/is (some? main4-root)) + (t/is (some? nested4-head)) + (t/is (some? nested4-child)) + (t/is (some? copy2)) + (t/is (some? copy2-nested-head)) + (t/is (some? copy2-nested4-head)) + (t/is (some? copy2-nested4-child)) + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested2-head)) + (t/is (= (:id near-nested2-child) (thi/id :main1-child))) + (t/is (nil? near-main3-root)) + (t/is (nil? near-main3-child)) + (t/is (nil? near-main4-root)) + (t/is (nil? near-nested4-head)) + (t/is (= (:id near-nested4-child) (thi/id :main3-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head))) + (t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head))) + (t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child)))))) diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 042f943e2c..f529705d9f 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -23,10 +23,11 @@ [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module -(log/set-level! :info) +(log/set-level! :warn) (def page-change? #{:add-page :mod-page :del-page :mov-page}) + (def update-layout-attr? #{:hidden}) @@ -207,6 +208,7 @@ ;; Prevent commit changes by a viewer team member (it really should never happen) (when (:can-edit permissions) + (log/trace :hint "commit-changes" :redo-changes redo-changes) (let [selected (dm/get-in state [:workspace-local :selected])] (rx/of (-> params (assoc :undo-group undo-group)