diff --git a/.serena/memories/backend/graph-experiment.md b/.serena/memories/backend/graph-experiment.md index 07d63f7f6f..e4325feabf 100644 --- a/.serena/memories/backend/graph-experiment.md +++ b/.serena/memories/backend/graph-experiment.md @@ -88,7 +88,7 @@ - Formats graph column values. - Quotes reserved graph labels such as `Group` and `Boolean`. - Defines container tables and shape tables. -- Defines `IsChildOf`, `IsInstanceOf`, `RefersTo`, and `FillsSwapSlot`. +- Defines `IsChildOf`, `IsInstanceOf`, and `RefersTo`. ### `app.graph.schema.contract` @@ -175,10 +175,8 @@ - `link-shape-refs` creates `RefersTo` edges from `shape-ref`. - Ladybug limits multi-label relationship `MERGE` statements. - The transform emits one statement for each shape-table pair. -- `link-swap-slots` creates `FillsSwapSlot` edges. -- Swap slot IDs come from `swap-slot-` entries in `touched`. -- The transform removes swap slot entries from `touched` after edge creation. -- The transform order matters because it reads and then changes `touched`. +- A swapped copy head carries `swap-group` in `touched`; its `RefersTo` edge points at the main child it replaces, and its `IsInstanceOf` edge at the component it takes its content from. +- The transforms read disjoint columns, so their order does not matter. ### `app.graph.meta` diff --git a/.serena/memories/common/file-change-validation-migration-subtleties.md b/.serena/memories/common/file-change-validation-migration-subtleties.md index 7333faadc3..6f21754fcc 100644 --- a/.serena/memories/common/file-change-validation-migration-subtleties.md +++ b/.serena/memories/common/file-change-validation-migration-subtleties.md @@ -22,7 +22,7 @@ - Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details. - `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes. - `repair-file-until-valid` stops on: no errors; unchanged `:data` after apply (backend page/`PointerMap` `=` is pointer identity); stable error fingerprint `(frequencies (map #(dissoc % :shape) errors))` vs previous pass (check fingerprint before `repair-file`/`process-changes`); or `max-iterations`. Each `repair-error` builds from the original `file-data`, so N `:set`-based errors on one shape take N passes. -- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule. +- `:ref-not-in-near-main`: a nested copy head inside a copy must refer to a child of the near main of its parent (the content ref of the parent, so a swapped parent counts by its own component). A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync. Its repair pairs the head with the child that stands for the same deepest main child, else the free child at its index (adding `:swap-group` when the components differ), else detaches it. `:misplaced-swap-group`: `:swap-group` anywhere but a nested copy head inside a copy. `:misplaced-slot`: any swap slot (files are migrated to `:swap-group` before validation, migration 0033). ## Migrations diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 8a9106d591..bd29b91af6 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.features :as cfeat] + [app.common.files.comp-processors :as cfcp] [app.common.files.helpers :as cfh] [app.common.files.migrations :as fmg] [app.common.files.tokens :as cfo] @@ -787,6 +788,25 @@ ;; all of them, not only the applied (vary-meta dissoc ::fmg/migrated)))) +(defn process-imported-file + "Process a file read from a binfile. The swap slots whose near main is in a + library not linked yet are left for `convert-deferred-swaps!`." + [cfg {:keys [id] :as file}] + (let [file (binding [cfcp/*defer-unreachable-swaps* true] + (process-file cfg file))] + (when (::cfcp/deferred-swaps (meta (:data file))) + (vswap! *state* update :deferred-swaps (fnil conj #{}) id)) + file)) + +(defn convert-deferred-swaps! + "Convert the swap slots the import left in its files, once the libraries + of the files are linked." + [cfg] + (doseq [file-id (:deferred-swaps @*state*)] + (let [file (get-file cfg file-id :realize? true :migrate? false) + libs (get-resolved-file-libraries cfg file)] + (update-file! cfg (update file :data cfcp/convert-swap-slots libs))))) + (defn- encode-file [cfg {:keys [id features] :as file}] (let [file (if (and (contains? features "fdata/objects-map") diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 4480d6fece..53c1b126fd 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -479,6 +479,7 @@ (read-section options)))) [:v1/metadata :v1/files :v1/rels :v1/sobjects]) + (bfc/convert-deferred-swaps! cfg) (bfm/apply-pending-migrations! cfg) ;; Knowing that the ids of the created files are in index, @@ -558,7 +559,7 @@ (assoc :project-id project-id) (dissoc :thumbnails) (bfc/check-components-v2)) - file (bfc/process-file system file)] + file (bfc/process-imported-file system file)] ;; All features that are enabled and requires explicit migration are ;; added to the state for a posterior migration step. diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 85484cbbb0..e50388ed77 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -1029,7 +1029,7 @@ (dissoc :options) (dissoc :tokens-source) (bfc/check-components-v2)) - file (bfc/process-file cfg file) + file (bfc/process-imported-file cfg file) file (ctf/check-file file)] (bfm/register-pending-migrations! cfg file) @@ -1273,6 +1273,7 @@ (let [resolution (resolve-and-link-libraries cfg files-info)] + (bfc/convert-deferred-swaps! cfg) (bfm/apply-pending-migrations! cfg) {:file-ids file-ids :resolution resolution}))) diff --git a/backend/src/app/graph/projection/transforms.clj b/backend/src/app/graph/projection/transforms.clj index e37da3d477..85ef1386a9 100644 --- a/backend/src/app/graph/projection/transforms.clj +++ b/backend/src/app/graph/projection/transforms.clj @@ -70,61 +70,14 @@ "MERGE (s)-[:RefersTo]->(t) " "RETURN count(*);"))))) -(def ^:private swap-slot-prefix "swap-slot-") - -(def ^:private slot-uuid-expr - ;; Ladybug `substring` is 1-indexed; 36 = RFC 4122 UUID text length. - (str "substring(touched_key, " (inc (count swap-slot-prefix)) ", 36)")) - -(defn- link-swap-slots! - "`FillsSwapSlot` from a swapped-in shape to the slot it replaces. - - Penpot records a component sub-shape swap as a `swap-slot-` entry in - the *replacing* shape's `touched` set, where `` names the replaced - slot shape in the main instance. The entries are then stripped from - `touched`, as `app.common.types.component/normal-touched-groups` does, so a - reader of `touched` sees design edits rather than swap bookkeeping. - - Stripping makes this the one transform that writes a column another - transform could read. Anything reading `touched` has to run before it." - [^Connection conn] - (let [linked - (reduce - (fn [total statement] (+ total (run-scalar! conn statement))) - 0 - (shape-pair-statements - (fn [from to] - (str "MATCH (s:" (nodes/match-label from) ") " - "WHERE size(s.touched) > 0 " - "UNWIND s.touched AS touched_key " - "WITH s, touched_key " - "WHERE STARTS_WITH(touched_key, '" swap-slot-prefix "') " - "WITH s, CAST(" slot-uuid-expr ", 'UUID') AS slot_id " - "MATCH (t:" (nodes/match-label to) ") " - "WHERE t.id = slot_id AND s.id <> t.id " - "MERGE (s)-[r:FillsSwapSlot {slot_id: slot_id}]->(t) " - "RETURN count(r);"))))] - ;; Strip unconditionally: an entry may name a slot that was garbage - ;; collected, so "no edge created" does not mean "nothing to strip". - (doseq [table nodes/shape-tables] - (ladybug/exec-on-connection! - conn - [(str "MATCH (s:" (nodes/match-label table) ") " - "WHERE size(s.touched) > 0 " - "SET s.touched = list_filter(s.touched, x -> " - "NOT STARTS_WITH(x, '" swap-slot-prefix "'));")])) - linked)) - (def registry "Every transform this backend applies. `:id` names the transform in the ingest report and the log. `:rel` names - the relationship it produces. The three registered here read disjoint - columns, so the vector order is not load-bearing. The one ordering - constraint that exists is stated on `link-swap-slots!`." - [{:id "link-component-instances" :rel :IsInstanceOf :run link-component-instances!} - {:id "link-shape-refs" :rel :RefersTo :run link-shape-refs!} - {:id "link-swap-slots" :rel :FillsSwapSlot :run link-swap-slots!}]) + the relationship it produces. The ones registered here read disjoint + columns, so the vector order is not load-bearing." + [{:id "link-component-instances" :rel :IsInstanceOf :run link-component-instances!} + {:id "link-shape-refs" :rel :RefersTo :run link-shape-refs!}]) (defn apply-transforms! "Apply every registered transform to an already loaded graph. diff --git a/backend/src/app/graph/schema/nodes.clj b/backend/src/app/graph/schema/nodes.clj index be88fb7d7c..ae8c058287 100644 --- a/backend/src/app/graph/schema/nodes.clj +++ b/backend/src/app/graph/schema/nodes.clj @@ -29,7 +29,7 @@ [clojure.string :as str])) (def schema-version - "penpot-graph-slice-4") + "penpot-graph-slice-5") (def ^:private document-projection {:source ctf/schema:file @@ -329,15 +329,9 @@ [] (shape-to-shape-rel-ddl "RefersTo" nil)) -(defn fills-swap-slot-ddl - "Swapped-in shape → the slot shape it replaces." - [] - (shape-to-shape-rel-ddl "FillsSwapSlot" ["`slot_id` UUID"])) - (defn ddl-statements [] (-> (mapv create-node-table-ddl node-types) (conj (is-child-of-ddl)) (conj (is-instance-of-ddl)) - (conj (refers-to-ddl)) - (conj (fills-swap-slot-ddl)))) \ No newline at end of file + (conj (refers-to-ddl)))) \ No newline at end of file diff --git a/backend/test/backend_tests/binfile_test.clj b/backend/test/backend_tests/binfile_test.clj index e34a733aea..f0e3da36f2 100644 --- a/backend/test/backend_tests/binfile_test.clj +++ b/backend/test/backend_tests/binfile_test.clj @@ -15,8 +15,17 @@ [app.common.files.changes-builder :as pcb] [app.common.files.tokens :as cfo] [app.common.files.validate :as cfv] + [app.common.files.comp-processors :as cfcp] + [app.common.files.migrations :as fmg] + [app.common.files.swap-slots :as cfss] + [app.common.pprint :as pp] + [app.common.test-helpers.components :as cthc] + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] [app.common.thumbnails :as thc] [app.common.time :as ct] + [app.common.types.component :as ctk] [app.common.types.shape :as cts] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] @@ -3030,3 +3039,67 @@ (t/testing "a file with components/v2 is left as it is" (let [file (file #{"components/v2"} {:shape-ref (uuid/custom 2 2)} nil)] (t/is (= file (bfc/check-components-v2 file))))))) + +(defn- swap-slot-library + "A library, in the form files older than the feature components/swap-group + have, whose :page main holds a copy of :card with its star swapped for + :heart." + [] + (-> (cthf/sample-file :library) + (ctho/add-simple-component :star :star-main :star-rect) + (ctho/add-simple-component :heart :heart-main :heart-rect) + (ctho/add-simple-component :badge :badge-main :badge-rect) + (ctho/add-frame :card-main) + (cthc/instantiate-component :star :card-star :parent-label :card-main) + (cthc/make-component :card :card-main) + (ctho/add-frame :page-main) + (cthc/instantiate-component :card :inner-copy + :parent-label :page-main + :children-labels [:inner-star]) + (cthc/make-component :page :page-main) + (ctho/swap-component-in-shape :inner-star :heart :as-slot? true))) + +(t/deftest import-converts-deferred-swap-slots-once-libraries-are-linked + (cthi/reset-idmap!) + (let [profile (th/create-profile* 1) + project-id (:default-project-id profile) + old-migs (disj fmg/available-migrations "0034-convert-swap-slots-to-swap-groups") + library (swap-slot-library) + ;; a file with a copy of :page whose star is swapped for :badge + file (-> (cthf/sample-file :file1) + (cthc/instantiate-component :page :outer-copy + :library library + :children-labels [:outer-inner :outer-star]) + (ctho/swap-component-in-shape :outer-star :badge + :library library + :as-slot? true) + (assoc :project-id project-id) + (assoc :migrations old-migs) + (update :features disj "components/swap-group")) + page-id (-> file :data :pages first)] + + (db/tx-run! th/*system* + (fn [cfg] + (binding [bfc/*state* (volatile! {:index {}})] + (bfc/insert-file! cfg (-> library + (assoc :project-id project-id) + (update :data cfcp/convert-swap-slots {}))) + + ;; the library is not linked yet: the swap is left as it is + (let [imported (bfc/process-imported-file cfg file)] + (bfc/insert-file! cfg imported) + (t/is (= #{(:id file)} (:deferred-swaps @bfc/*state*))) + (t/is (contains? (:features imported) "components/swap-group")) + (t/is (some? (-> imported :data :pages-index (get page-id) :objects + (get (cthi/id :outer-star)) cfss/get-swap-slot)))) + + (db/insert! cfg :file-library-rel {:file-id (:id file) + :library-file-id (:id library)}) + (bfc/convert-deferred-swaps! cfg) + + (let [head (-> (bfc/get-file cfg (:id file) :migrate? false :realize? true) + :data :pages-index (get page-id) :objects + (get (cthi/id :outer-star)))] + (t/is (= (cthi/id :inner-star) (:shape-ref head))) + (t/is (ctk/swapped? head)) + (t/is (nil? (cfss/get-swap-slot head))))))))) diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc index dbd287d71c..9c4d9617fe 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -48,6 +48,7 @@ "fdata/shape-data-type" "fdata/path-data" "components/v2" + "components/swap-group" "styles/v2" "layout/grid" "plugins/runtime" @@ -67,6 +68,7 @@ "styles/v2" "layout/grid" "components/v2" + "components/swap-group" "plugins/runtime" "design-tokens/v1" "tokens/numeric-input" @@ -96,7 +98,8 @@ ;; or modifying a file or creating or modifying a team (def no-team-inheritable-features #{"fdata/path-data" - "fdata/shape-data-type"}) + "fdata/shape-data-type" + "components/swap-group"}) ;; This is a set of features that does not require an explicit ;; migration like components/v2 or the migration is not mandatory to @@ -107,6 +110,7 @@ "design-tokens/v1" "fdata/shape-data-type" "fdata/path-data" + "components/swap-group" "tokens/numeric-input" "variants/v1"} (into frontend-only-features) diff --git a/common/src/app/common/files/comp_processors.cljc b/common/src/app/common/files/comp_processors.cljc index 993a04c42d..7a47f51ad9 100644 --- a/common/src/app/common/files/comp_processors.cljc +++ b/common/src/app/common/files/comp_processors.cljc @@ -7,12 +7,112 @@ (ns app.common.files.comp-processors "Repair, migration or transformation utilities for components." (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.files.swap-slots :as cfss] + [app.common.geom.point :as gpt] [app.common.logging :as log] [app.common.types.component :as ctk] - [app.common.types.file :as ctf])) + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.shape.layout :as ctsl])) (log/set-level! :warn) +;; ---- Swap slots + +(defn find-swap-slot + "The deepest main child of the position the shape takes: its swap slot, + or the first one along its ref chain, or the last shape of the chain + before a main root." + ([shape container file libraries] + (find-swap-slot shape container file libraries #{})) + ([shape container file libraries viewed-ids] + (if (contains? viewed-ids (:id shape)) ;; prevent cycles + nil + (if-let [swap-slot (cfss/get-swap-slot shape)] + swap-slot + (let [ref-shape (ctf/find-ref-shape file + container + libraries + shape + :include-deleted? true + :with-context? true) + shape-meta (meta ref-shape) + ref-file (:file shape-meta) + ref-container (:container shape-meta)] + (when ref-shape + (if-let [swap-slot (cfss/get-swap-slot ref-shape)] + swap-slot + (if (ctk/main-instance? ref-shape) + (:id shape) + (find-swap-slot ref-shape ref-container ref-file libraries (conj viewed-ids (:id 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 (ctn/get-shape container (:parent-id shape)) + parent-ref-shape (when parent-shape + (ctf/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-> (ctn/get-shape ref-container near-match-id) + with-context? + (with-meta (meta parent-ref-shape))))] + near-match)) + +(defn- find-next-related-swap-shape-id + "Go up from the chain of references shapes that will eventually lead to the shape + with swap-slot-id as id. Returns the next shape on the chain" + [parent swap-slot-id libraries] + (let [container (ctf/get-component-container-from-head parent libraries) + objects (:objects container) + + children (cfh/get-children objects (:id parent)) + original-shape-id (->> children + (filter #(= swap-slot-id (:id %))) + first + :id)] + (if original-shape-id + ;; Return the children which id is the swap-slot-id + original-shape-id + ;; No children with swap-slot-id as id, go up + (let [referenced-shape (ctf/find-ref-shape nil container libraries parent) + ;; Recursive call that will get the id of the next shape on + ;; the chain that ends on a shape with swap-slot-id as id + next-shape-id (when referenced-shape + (find-next-related-swap-shape-id referenced-shape swap-slot-id libraries))] + ;; Return the children which shape-ref points to the next-shape-id + (->> children + (filter #(= next-shape-id (:shape-ref %))) + first + :id))))) + +(defn find-ref-id-for-swapped + "When a shape has a swap slot, find the original ref-id that the shape had + before the swap" + [shape container libraries] + (let [swap-slot (cfss/get-swap-slot shape) + objects (:objects container) + + parent (get objects (:parent-id shape)) + parent-head (ctn/get-head-shape objects parent) + parent-ref (ctf/find-ref-shape nil container libraries parent-head)] + + (when (and swap-slot parent-ref) + (find-next-related-swap-shape-id parent-ref swap-slot libraries)))) + +;; ---- Repairs + (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." @@ -77,11 +177,11 @@ shape container #(ctf/find-ref-shape file container libraries % :include-deleted? true)) near-match (when swapped? - (ctf/find-near-match file container libraries shape :include-deleted? true :with-context? false))] + (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))] + (nil? (cfss/get-swap-slot shape))) + (let [updated-shape (cfss/set-swap-slot shape (:id near-match))] (log/warn :hint "adding missing swap slot to shape" :shape-id (str (:id shape)) :shape-name (:name shape) @@ -108,7 +208,9 @@ (ctf/update-all-shapes file-data (fn [shape] - (if (and (ctk/subcopy-head? shape) (nil? (ctk/get-swap-slot shape))) + (if (and (ctk/subcopy-head? shape) + (nil? (cfss/get-swap-slot shape)) + (not (ctk/swapped? 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})] @@ -153,3 +255,148 @@ (> iteration 20)) ;; safety bound next-data (recur next-data (inc iteration))))))) + +(def ^:dynamic *defer-unreachable-swaps* + "When true, `convert-swap-slots` leaves as they are the swap slots whose + near main it cannot reach, for a later pass with more libraries." + false) + +(defn- find-swap-position + "Among the children of `parent-ref`, the near main of the parent of a + swapped head, the one that stands for the position `origin`: the deepest + main child of that position, as a swap slot holds it." + [parent-ref libraries origin] + (let [container (:container (meta parent-ref)) + file (:file (meta parent-ref))] + (d/seek #(or (= (:id %) origin) + (= origin (find-swap-slot % container file libraries))) + (keep #(ctn/get-shape container %) (:shapes parent-ref))))) + +(defn find-replaced-shape + "The child of the near main of the parent of the copy head `shape` whose + position the head takes: the one that stands for the deepest main child + the head refers to, or else the free one at the same index." + [file container libraries shape] + (let [parent (ctn/get-shape container (:parent-id shape)) + parent-ref (when parent + (ctf/find-content-ref-shape file container libraries parent + :include-deleted? true + :with-context? true))] + (when parent-ref + (or (find-swap-position parent-ref libraries (:shape-ref shape)) + (let [taken (into #{} (keep #(:shape-ref (ctn/get-shape container %))) (:shapes parent)) + index (d/index-of (:shapes parent) (:id shape)) + near-id (when index (get (:shapes parent-ref) index))] + (when (and (some? near-id) (not (contains? taken near-id))) + (ctn/get-shape (:container (meta parent-ref)) near-id))))))) + +(defn- swap-made-in-copy? + "Whether a copy head with the swap slot `slot` was swapped in its copy, + and not only inherited the slot from its main: it refers to the root of + its own component, not to a child of its near main. When the near main + cannot be reached, a head that refers to its slot inherited it, since a + swap made in the copy refers to the root of another component; otherwise + its own component tells, and when that cannot be reached either, the swap + counts as made in the copy, so that it is kept." + [shape slot parent-ref file libraries] + (cond + (some? parent-ref) + (not-any? #(= % (:shape-ref shape)) (:shapes parent-ref)) + + (= slot (:shape-ref shape)) + false + + :else + (let [component (ctf/resolve-component shape file libraries :include-deleted? true)] + (or (nil? component) + (= (:shape-ref shape) (:main-instance-id component)))))) + +(defn- away-from-position? + "Whether the swapped head sits elsewhere than the main child it replaces + would place it. Sync did not move a head with a swap slot, so the main may + have moved that child since the swap. A position a layout owns does not + count." + [file container libraries shape position] + (let [objects (:objects container) + ;; the head sync places it against: the top copy, or a swapped head + ;; around it, which syncs from its own component + root (d/seek #(or (ctk/instance-root? %) + (some? (cfss/get-swap-slot %)) + (ctk/swapped? %)) + (map #(get objects %) (cfh/get-parent-ids objects (:id shape)))) + root-main (when root + (ctf/find-content-ref-shape file container libraries root :include-deleted? true)) + point (fn [shape] + (gpt/point (dm/get-in shape [:selrect :x]) + (dm/get-in shape [:selrect :y])))] + (and (some? root-main) + (not (ctk/touched-group? shape :geometry-group)) + (or (not (ctsl/any-layout-immediate-child? objects shape)) + (ctsl/position-absolute? shape)) + (not (gpt/close? (point shape) + (gpt/add (point position) + (gpt/subtract (point root) (point root-main)))))))) + +(defn convert-swap-slots + "Store the swaps of the file as override groups. Files older than the + feature components/swap-group mark a swapped copy head with a swap slot + (a `swap-slot-` touched group) and point its :shape-ref at the root of + its own component. Now the head keeps the :shape-ref of the near main child + it replaces and carries the touched group :swap-group. + + A swap made in a copy is paired with the child of the near main that holds + the same position, as sync paired it. Its geometry is marked as touched when + it sits elsewhere than that child would place it, since sync now moves it + with that child. One with no such child, or whose near + main cannot be reached, refers to its slot, so validation reports it. Any + other slot (a swap in a main, or a slot a copy inherited) is dropped. + + Every pairing is found on the data as it was. With + `*defer-unreachable-swaps*`, a slot whose near main cannot be reached is + left, and the data carries `::deferred-swaps` in its metadata." + [file-data libraries] + (let [file {:id (:id file-data) :data file-data} + deferred? (volatile! false) + + convert-shape + (fn [container shape slot] + (let [parent (ctn/get-shape container (:parent-id shape)) + shape' (cfss/remove-swap-slot shape)] + (if-not (and (ctk/instance-head? shape) + (ctk/in-component-copy? parent)) + shape' + (let [parent-ref (ctf/find-content-ref-shape file container libraries parent + :include-deleted? true + :with-context? true)] + (cond + (not (swap-made-in-copy? shape slot parent-ref file libraries)) + shape' + + (and (nil? parent-ref) *defer-unreachable-swaps*) + (do (vreset! deferred? true) + shape) + + :else + (let [position (when parent-ref + (find-swap-position parent-ref libraries slot))] + (cond-> (-> shape' + (assoc :shape-ref (or (:id position) slot)) + (update :touched (fnil conj #{}) :swap-group)) + ;; keep it where the user sees it + (and (some? position) + (away-from-position? file container libraries shape position)) + (update :touched conj :geometry-group)))))))) + + convert-container + (fn [container] + (let [converted (into [] + (keep (fn [shape] + (when-let [slot (cfss/get-swap-slot shape)] + (convert-shape container shape slot)))) + (vals (:objects container)))] + (reduce #(assoc-in %1 [:objects (:id %2)] %2) container converted))) + + file-data (ctf/update-containers file-data convert-container)] + (cond-> file-data + @deferred? + (vary-meta assoc ::deferred-swaps true)))) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index de82f16f9e..3a725b5bb0 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -13,6 +13,7 @@ [app.common.files.comp-processors :as cfcp] [app.common.files.defaults :as cfd] [app.common.files.helpers :as cfh] + [app.common.files.swap-slots :as cfss] [app.common.files.tokens :as cfo] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] @@ -938,12 +939,12 @@ libraries {component-file {:id component-file :data data}} ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true}) ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape)) - shape-swap-slot (ctk/get-swap-slot shape) - ref-swap-slot (ctk/get-swap-slot ref-shape)] + shape-swap-slot (cfss/get-swap-slot shape) + ref-swap-slot (cfss/get-swap-slot ref-shape)] (if (and (some? shape-swap-slot) (= shape-swap-slot ref-swap-slot) (ctk/main-instance? ref-parent)) - (ctk/remove-swap-slot shape) + (cfss/remove-swap-slot shape) shape))) (update-page [page] @@ -954,10 +955,10 @@ (defmethod migrate-data "legacy-48" [data _] (letfn [(fix-shape [shape] - (let [swap-slot (ctk/get-swap-slot shape)] + (let [swap-slot (cfss/get-swap-slot shape)] (if (and (some? swap-slot) (not (ctk/subcopy-head? shape))) - (ctk/remove-swap-slot shape) + (cfss/remove-swap-slot shape) shape))) (update-page [page] @@ -1493,11 +1494,11 @@ (defmethod migrate-data "0010-fix-swap-slots-pointing-non-existent-shapes" [data _] (letfn [(fix-shape [page shape] - (if (ctk/get-swap-slot shape) + (if (cfss/get-swap-slot shape) (let [libs (some-> (:libs data) deref) - ref-id (when libs (ctf/find-ref-id-for-swapped shape page libs))] + ref-id (when libs (cfcp/find-ref-id-for-swapped shape page libs))] (if (nil? ref-id) - (ctk/remove-swap-slot shape) + (cfss/remove-swap-slot shape) shape)) shape)) @@ -1509,7 +1510,7 @@ (defmethod migrate-data "0011-fix-invalid-text-touched-flags" [data _] (letfn [(fix-shape [shape] - (let [touched-groups (ctk/normal-touched-groups shape) + (let [touched-groups (cfss/normal-touched-groups shape) content-touched? (touched-groups :content-group) text-touched? (or (touched-groups :text-content-text) (touched-groups :text-content-attribute) @@ -2296,6 +2297,14 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0034-convert-swap-slots-to-swap-groups" + [data _] + (some-> cfeat/*new* (swap! conj "components/swap-group")) + (let [libraries (if (:libs data) + (deref (:libs data)) + {})] + (cfcp/convert-swap-slots data libraries))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -2386,4 +2395,5 @@ "0030-remove-stroke-per-side-attr" "0031-migrate-stroke-width-token-attr" "0032-fix-children-not-in-parent" - "0033-normalize-main-instance-names"])) + "0033-normalize-main-instance-names" + "0034-convert-swap-slots-to-swap-groups"])) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index f0a52b788c..39e997c431 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -10,7 +10,9 @@ [app.common.data.macros :as dm] [app.common.files.changes :as cfc] [app.common.files.changes-builder :as pcb] + [app.common.files.comp-processors :as cfcp] [app.common.files.helpers :as cfh] + [app.common.files.swap-slots :as cfss] [app.common.files.tokens :as cfo] [app.common.logging :as log] [app.common.path-names :as cpn] @@ -591,80 +593,74 @@ (fn [shape] ;; Remove the swap slot (log/debug :hint " -> remove swap-slot") - (ctk/remove-swap-slot shape))] + (cfss/remove-swap-slot shape))] (log/debug :hint "repairing shape :misplaced-slot" :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 :duplicate-slot +(defmethod repair-error :misplaced-swap-group [_ {:keys [shape page-id] :as error} file-data _] - (let [page (ctpl/get-page file-data page-id) - childs (map #(get (:objects page) %) (:shapes shape)) - child-with-duplicate (let [result (reduce (fn [[seen duplicates] item] - (let [swap-slot (ctk/get-swap-slot item)] - (if (contains? seen swap-slot) - [seen (conj duplicates item)] - [(conj seen swap-slot) duplicates]))) - [#{} []] - childs)] - (second result)) - repair-shape - (fn [shape] - ;; Remove the swap slot - (log/debug :hint " -> remove swap-slot" :child-id (:id shape)) - (ctk/remove-swap-slot shape))] - - (log/debug :hint "repairing shape :duplicated-slot" :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 (map :id child-with-duplicate) repair-shape)))) - -(defmethod repair-error :component-duplicate-slot - [_ {:keys [shape] :as error} file-data _] - (let [main-shape (get-in shape [:objects (:main-instance-id shape)]) - childs (map #(get (:objects shape) %) (:shapes main-shape)) - childs-with-duplicate (let [result (reduce (fn [[seen duplicates] item] - (let [swap-slot (ctk/get-swap-slot item)] - (if (contains? seen swap-slot) - [seen (conj duplicates item)] - [(conj seen swap-slot) duplicates]))) - [#{} []] - childs)] - (second result)) - duplicated-ids (set (mapv :id childs-with-duplicate)) - repair-component - (fn [component] - (let [objects (reduce-kv (fn [acc k v] - (if (contains? duplicated-ids k) - (assoc acc k (ctk/remove-swap-slot v)) - (assoc acc k v))) - {} - (:objects component))] - (assoc component :objects objects)))] - - (log/debug :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape)) - (-> (pcb/empty-changes nil) - (pcb/with-library-data file-data) - (pcb/update-component (:id shape) repair-component)))) - -(defmethod repair-error :missing-slot - [_ {:keys [shape page-id args] :as error} file-data _] (let [repair-shape (fn [shape] - ;; Set the desired swap slot - (let [slot (:swap-slot args)] - (if (some? slot) - (do (log/debug :hint (str " -> set swap-slot to " slot)) - (ctk/set-swap-slot shape slot)) - shape)))] + (log/debug :hint " -> remove :swap-group") + (ctk/remove-swap-group shape))] - (log/debug :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id) + (log/debug :hint "repairing shape :misplaced-swap-group" :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-not-in-near-main + [_ {:keys [shape page-id] :as error} file-data libraries] + (let [page (ctpl/get-page file-data page-id) + replaced (cfcp/find-replaced-shape {:id (:id file-data) :data file-data} + (ctn/make-container page :page) + libraries + shape) + + reassign-shape + (fn [shape] + (log/debug :hint " -> reassign shape-ref to" :shape-ref (:id replaced)) + (cond-> (assoc shape :shape-ref (:id replaced)) + (or (not= (:component-id shape) (:component-id replaced)) + (not= (:component-file shape) (:component-file replaced))) + (update :touched (fnil conj #{}) :swap-group)))] + + (log/debug :hint "repairing shape :ref-not-in-near-main" :id (:id shape) :name (:name shape) :page-id page-id) + (if (some? replaced) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] reassign-shape)) + ;; no position to take: the copy keeps its content, detached + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes (cfh/get-children-ids-with-self (:objects page) (:id shape)) + ctk/detach-shape))))) + +(defmethod repair-error :duplicate-ref + [_ {:keys [shape page-id] :as error} file-data _] + (let [page (ctpl/get-page file-data page-id) + objects (:objects page) + ;; sync pairs the first child that refers to a main shape; the + ;; others have no position to take + extra (->> (:shapes shape) + (map #(get objects %)) + (reduce (fn [[seen extra] child] + (let [ref (:shape-ref child)] + (if (and (some? ref) (contains? seen ref)) + [seen (conj extra (:id child))] + [(cond-> seen (some? ref) (conj ref)) extra]))) + [#{} []]) + (second)) + ids (mapcat #(cfh/get-children-ids-with-self objects %) extra)] + (log/debug :hint "repairing shape :duplicate-ref" :id (:id shape) :name (:name shape) :page-id page-id) + (log/debug :hint " -> delete shapes" :shape-ids ids) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/remove-objects ids)))) + (defmethod repair-error :main-instance-not-a-variant [_ {:keys [shape page-id args]} file-data _] (let [repair-shape diff --git a/common/src/app/common/files/swap_slots.cljc b/common/src/app/common/files/swap_slots.cljc new file mode 100644 index 0000000000..1edd595684 --- /dev/null +++ b/common/src/app/common/files/swap_slots.cljc @@ -0,0 +1,49 @@ +;; 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 SUBSIDIARY SL + +(ns app.common.files.swap-slots + "Files older than the feature components/swap-group mark a swapped copy + head with a swap slot: a touched group `swap-slot-`, where the id is + the deepest main child of the position the head takes. Validation reports + them, and the migrations read and repair them until + `app.common.files.comp-processors/convert-swap-slots` turns them into + override groups." + (:require + [app.common.data :as d] + [app.common.types.component :as ctk] + [cuerdas.core :as str])) + +(defn swap-slot-group? + [group] + (str/starts-with? (name group) "swap-slot-")) + +(defn build-swap-slot-group + "Convert a swap slot into a :touched group." + [swap-slot] + (when swap-slot + (keyword (str "swap-slot-" swap-slot)))) + +(defn normal-touched-groups + "The touched groups of the shape that are not swap slots." + [shape] + (into #{} (remove swap-slot-group?) (:touched shape))) + +(defn get-swap-slot + "If the shape has a :touched group in the form :swap-slot-, get the id." + [shape] + (when-let [group (d/seek swap-slot-group? (:touched shape))] + (parse-uuid (subs (name group) 10)))) ;; 10 is the length of "swap-slot-" + +(defn set-swap-slot + "Add a touched group with a form :swap-slot-." + [shape swap-slot] + (cond-> shape + (some? swap-slot) + (update :touched ctk/set-touched-group (build-swap-slot-group swap-slot)))) + +(defn remove-swap-slot + [shape] + (update shape :touched #(into #{} (remove swap-slot-group?) %))) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index b683424a4a..d2fb46cce8 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.helpers :as cfh] + [app.common.files.swap-slots :as cfss] [app.common.files.variant :as cfv] [app.common.path-names :as cpn] [app.common.schema :as sm] @@ -34,11 +35,9 @@ :child-not-found :frame-not-found :invalid-frame - :component-duplicate-slot :component-not-main :component-main-external :component-not-found - :duplicate-slot :invalid-main-instance-id :invalid-main-instance-page :invalid-main-instance-name @@ -64,7 +63,9 @@ :instance-head-not-frame :invalid-text-touched :misplaced-slot - :missing-slot + :misplaced-swap-group + :ref-not-in-near-main + :duplicate-ref :shape-ref-cycle :main-instance-not-a-variant :main-instance-invalid-variant-id @@ -411,8 +412,7 @@ "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 (and (nil? (ctk/get-swap-slot shape)) - (not (ctk/swapped? shape))) + (when-not (ctk/swapped? shape) (when-let [ref-shape (find-ref-shape* file page libraries shape)] (when (or (not= (:component-id shape) (:component-id ref-shape)) (not= (:component-file shape) (:component-file ref-shape))) @@ -422,61 +422,60 @@ :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." +(defn- check-no-swap-slot + "Validate that this shape has no swap slot, the form older files stored + swaps in." [shape file page] - (when (some? (ctk/get-swap-slot shape)) + (when (some? (cfss/get-swap-slot shape)) (report-error :misplaced-slot "This shape should not have swap slot" shape file page))) -(defn- has-duplicate-swap-slot? - "Returns true if any two children of `shape` share the same swap slot. - Uses a single pass with early exit on the first duplicate, avoiding - the intermediate `frequencies` map allocation." - [shape container] - (let [objects (:objects container) - result (reduce (fn [seen child-id] - (let [slot (ctk/get-swap-slot (get objects child-id))] - (if (nil? slot) - seen - (if (contains? seen slot) - (reduced true) - (conj seen slot))))) - #{} - (:shapes shape))] - (true? result))) - -(defn- check-duplicate-swap-slot - "Validate that the children of this shape does not have duplicated slots." +(defn- check-not-swapped + "Validate that this shape is not marked as swapped: only a nested copy + head inside a copy can be." [shape file page] - (when (has-duplicate-swap-slot? shape page) - (report-error :duplicate-slot - "This shape has children with the same swap slot" + (when (ctk/swapped? shape) + (report-error :misplaced-swap-group + "Only a nested copy head inside a copy can be swapped" shape file page))) -(defn- check-required-swap-slot - "Validate that the shape has a swap slot if it's a subinstance head that has been - swapped (see `ctf/swapped-subhead?`)." +(defn- check-ref-in-near-main + "Validate that a nested copy head refers to a child of the near main of + its parent: the shape whose position it takes." [shape file page libraries] - ;; Guard first: if the shape already has a swap slot the invariant is satisfied - ;; and we can avoid the ref-shape lookups entirely. - (when (and (nil? (ctk/get-swap-slot shape)) - (ctf/swapped-subhead? shape page #(if (ctk/swapped? %) - (ctf/find-content-ref-shape file page libraries % - :include-deleted? true) - (find-ref-shape* file page libraries %)))) - (let [near-match (ctf/find-near-match file page libraries shape :include-deleted? true :with-context? false)] - (when (some? near-match) - (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))))))) + (when (ctf/swapped-subhead? shape page #(ctf/find-content-ref-shape file page libraries % + :include-deleted? true)) + (report-error :ref-not-in-near-main + (str/ffmt "Referenced shape % is not a child of the near main of the parent" (:shape-ref shape)) + shape file page))) + +(defn- duplicate-ref? + "Whether two children of the shape refer to the same main shape." + [shape page] + (let [objects (:objects page)] + (true? (reduce (fn [seen child-id] + (let [ref (:shape-ref (get objects child-id))] + (cond + (nil? ref) seen + (contains? seen ref) (reduced true) + :else (conj seen ref)))) + #{} + (:shapes shape))))) + +(defn- check-no-duplicate-ref + "Validate that no two children of a copy shape take the position of the + same main shape." + [shape file page] + (when (duplicate-ref? shape page) + (report-error :duplicate-ref + "Two children of this copy refer to the same main shape" + shape file page))) (defn- check-valid-touched "Validate that the text touched flags are coherent." [shape file page] - (let [touched-groups (ctk/normal-touched-groups shape) + (let [touched-groups (or (:touched shape) #{}) content-touched? (touched-groups :content-group) text-touched? (or (touched-groups :text-content-text) (touched-groups :text-content-attribute) @@ -498,8 +497,8 @@ (check-component-main-head shape file page libraries) (check-component-root shape file page) (check-component-not-ref shape file page) - (check-empty-swap-slot shape file page) - (check-duplicate-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (run! #(check-shape % file page libraries :context :main-top) (:shapes shape))) (defn- check-shape-main-root-nested @@ -511,7 +510,8 @@ (check-component-main-head shape file page libraries) (check-component-not-root shape file page) (check-component-not-ref shape file page) - (check-empty-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (run! #(check-shape % file page libraries :context :main-nested) (:shapes shape))) (defn- check-shape-copy-root-top @@ -527,9 +527,10 @@ (check-component-root shape file page) (check-component-ref shape file page libraries) (check-ref-is-head shape file page libraries) - (check-empty-swap-slot shape file page) - (check-duplicate-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (check-valid-touched shape file page) + (check-no-duplicate-ref shape file page) (run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape)))) (defn- check-shape-copy-root-nested @@ -542,12 +543,14 @@ (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) + (check-no-swap-slot shape file page) + (check-ref-in-near-main 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 (check-component-ref shape file page libraries) (check-ref-is-head shape file page libraries)) + (check-no-duplicate-ref shape file page) (run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape))) (defn- check-shape-main-not-root @@ -556,7 +559,8 @@ (check-component-not-main-not-head shape file page) (check-component-not-root shape file page) (check-component-not-ref shape file page) - (check-empty-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (run! #(check-shape % file page libraries :context :main-any) (:shapes shape))) (defn- check-shape-copy-not-root @@ -566,8 +570,10 @@ (check-component-not-root shape file page) (check-component-ref shape file page libraries) (check-ref-is-not-head shape file page libraries) - (check-empty-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (check-valid-touched shape file page) + (check-no-duplicate-ref shape file page) (run! #(check-shape % file page libraries :context :copy-any) (:shapes shape))) (defn- check-shape-not-component @@ -577,7 +583,8 @@ (check-component-not-main-not-head shape file page) (check-component-not-root shape file page) (check-component-not-ref shape file page) - (check-empty-swap-slot shape file page) + (check-no-swap-slot shape file page) + (check-not-swapped shape file page) (run! #(check-shape % file page libraries :context :not-component) (:shapes shape))) (defn- check-variant-container @@ -757,14 +764,6 @@ shape file page) (check-shape-not-component shape file page libraries))))))))) -(defn check-component-duplicate-swap-slot - [component file] - (let [shape (get-in component [:objects (:main-instance-id component)])] - (when (has-duplicate-swap-slot? shape component) - (report-error :component-duplicate-slot - "This deleted component has children with the same swap slot" - component file nil)))) - (defn check-ref-cycles [component file] (let [cycles-ids (-> (reduce-kv (fn [acc id shape] @@ -828,7 +827,6 @@ (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)) (when (ctk/is-variant? component) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 9752e5456b..8c5a7b17ef 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -357,19 +357,12 @@ (pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root") (assoc % :component-root true))) - (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 %))) - (ctk/swapped? shape) ; A swapped head left with no copy around it is a copy of its own component (generate-rehead-swapped container libraries shape) - (and (nil? (ctk/get-swap-slot shape)) - (not (ctk/swapped? shape))) + (not (ctk/swapped? shape)) ; Near shape-ref of shape and children need to be advanced one level - ; (except if the head is already swapped) (generate-advance-nesting-level nil container libraries (:id shape))) ;; Otherwise, detach the shape and all children @@ -439,9 +432,9 @@ (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 - ;; ref-shape must be merged into the current shape, because they refer to - ;; the new referenced shape. + ;; When advancing level, the touched groups of the ref-shape must be + ;; merged into the current shape, because they refer to the new + ;; referenced shape. (some? ref-shape) (pcb/update-shapes [(:id shape)] @@ -450,15 +443,7 @@ (log/trace :msg (str " (ref touched: " (:touched ref-shape) ")")) (assoc % :touched (set/union (:touched shape) - (inherited-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 (str " (got swap-slot " (ctk/get-swap-slot ref-shape) ")")) - (ctk/set-swap-slot % (ctk/get-swap-slot ref-shape)))) + (inherited-touched shape (or (:touched ref-shape) #{})))))) ;; If we can't get the ref-shape (e.g. it's in an external library not linked), ;: we can't do a suitable advance. So it's better to detach the shape and all its @@ -1012,9 +997,6 @@ children-inst children-main container - component-container - file - libraries only-inst only-main both @@ -1190,9 +1172,6 @@ children-inst children-main container - component-container - file - libraries only-inst only-main both @@ -1222,14 +1201,12 @@ a swapped child pairs that way too, and takes `swapped-cb` in a normal sync, `both-cb` in a reset, and does not pair in an inverse sync, so the main child it replaces is removed and the swapped child is added in its place." - [changes shape-inst children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset?] + [changes shape-inst children-inst children-main container-inst only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset?] (shape-log :trace (:id shape-inst) container-inst :msg "Compare children") (let [pair? (fn [child-main child-inst] - (or (and (ctk/is-main-of? child-main child-inst) - (not (and inverse? (ctk/swapped? child-inst)))) - (and (ctf/match-swap-slot? child-main child-inst container-inst container-main file libraries) - (not reset?)))) + (and (ctk/is-main-of? child-main child-inst) + (not (and inverse? (ctk/swapped? child-inst))))) synced-pair? (fn [child-main child-inst] @@ -2560,26 +2537,13 @@ (pcb/update-shapes [shape-copy-id] (fn [shape-copy objects] - (let [component-page - (ctf/get-component-page main-container main-component) - - component-swap-children - (->> shape-main - :shapes - (map #(get (:objects component-page) %)) - (filter #(some? (ctk/get-swap-slot %))) - (group-by ctk/get-swap-slot)) - - ids-map + (let [ids-map (into {} (comp (map #(get objects %)) (keep (fn [copy-shape] - (let [main-shape - (if (some? (ctk/get-swap-slot copy-shape)) - (first (get component-swap-children (ctk/get-swap-slot copy-shape))) - (ctf/get-ref-shape main-container main-component copy-shape))] + (let [main-shape (ctf/get-ref-shape main-container main-component copy-shape)] [(:id main-shape) (:id copy-shape)])))) (:shapes shape-copy)) @@ -2904,9 +2868,7 @@ ;; swapped children (we will keep them too) ignore-swapped-fn (if ignore-swapped? - #(let [child (get objects %)] - (or (some? (ctk/get-swap-slot child)) - (ctk/swapped? child))) + #(ctk/swapped? (get objects %)) (constantly false)) ;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the @@ -2941,37 +2903,14 @@ (maybe-sync sync-typographies? done? #(generate-sync-file % file-id :typographies asset-id library-id libraries current-file-id early-return?)))))) -(defn generate-sync-head - [changes file-full libraries container id reset?] - (let [shape-inst (ctn/get-shape container id) - objects (:objects container) - parent (get objects (:parent-id shape-inst)) - head (ctn/get-component-shape objects parent) - changes - (-> changes - (pcb/with-container container) - (pcb/with-objects (:objects container)) - (generate-sync-shape-direct file-full libraries container (:id head) reset?))] - changes)) - (defn generate-reset-component + "Reset the copy head to its near main. A swapped head turns back into the + component of the main shape it refers to, keeping its id." [changes file-full libraries container id] - (let [objects (:objects container) - swap-slot (-> (ctn/get-shape container id) - (ctk/get-swap-slot)) - changes - (-> changes - (pcb/with-container container) - (pcb/with-objects objects) - (generate-sync-shape-direct file-full libraries container id true))] - - ;; If the shape has been swapped, the reset also have to undo the swap. As the - ;; shape has been reset and it's not touched now, by the sync above, if we - ;; do another reset of the parent, the shape will be reset as it is in the - ;; near main component. - (cond-> changes - (some? swap-slot) - (generate-sync-head file-full libraries container id true)))) + (-> changes + (pcb/with-container container) + (pcb/with-objects (:objects container)) + (generate-sync-shape-direct file-full libraries container id true))) (defn generate-duplicate-flows [changes shapes page ids-map] @@ -3138,11 +3077,11 @@ (defn generate-duplicate-shape-change ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id] - (generate-duplicate-shape-change changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true nil)) + (generate-duplicate-shape-change changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false nil)) ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id variant-props] - (generate-duplicate-shape-change changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true variant-props)) + (generate-duplicate-shape-change changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false variant-props)) - ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot? variant-props] + ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? variant-props] (cond (nil? obj) changes @@ -3165,8 +3104,6 @@ (ctk/instance-root? obj)) duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) is-component-main? (ctk/main-instance? obj) - subinstance-head? (ctk/subinstance-head? obj) - instance-root? (ctk/instance-root? obj) into-component? (and duplicating-component? (ctn/in-any-component? objects parent)) @@ -3198,11 +3135,6 @@ :parent-id parent-id :frame-id frame-id) - (cond-> (and (not instance-root?) - subinstance-head? - remove-swap-slot?) - (ctk/remove-swap-slot)) - (dissoc :shapes :use-for-thumbnail) @@ -3282,11 +3214,6 @@ new-id duplicating-component? true - (and remove-swap-slot? - ;; only remove swap slot of children when the current shape - ;; is not a subinstance head nor a instance root - (not subinstance-head?) - (not instance-root?)) variant-props)) changes (map (d/getf objects) (:shapes obj))))))) @@ -3318,9 +3245,7 @@ (cfh/get-children-ids-with-self objects (:id shape)))) advance-copy (fn [objects shape] (if (and (ctk/instance-head? shape) (not (ctk/main-instance? shape))) - (let [level-delta (if (nil? (ctk/get-swap-slot shape)) - (ctn/get-nesting-level-delta (:objects page) shape uuid/zero) - 0)] + (let [level-delta (ctn/get-nesting-level-delta (:objects page) shape uuid/zero)] (cond (not (pos? level-delta)) objects diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 30d6067889..e2cca38f06 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -467,9 +467,6 @@ variant-shapes (filter ctk/is-variant? shapes) - component-main-parent - (ctn/find-component-main objects parent false) - groups-to-delete (loop [current-id (first parents) to-check (rest parents) @@ -573,14 +570,6 @@ (cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent)) (pcb/update-shapes ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))) - ;; Remove the swap slots if it is moving to a different component - (pcb/update-shapes - child-heads-ids - (fn [shape] - (cond-> shape - (not= component-main-parent (ctn/find-component-main objects shape false)) - (ctk/remove-swap-slot)))) - ;; Remove component-root property when moving a shape inside a component (cond-> (ctn/get-instance-root objects parent) (pcb/update-shapes children-ids #(dissoc % :component-root))) diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 5e07c49c2c..e9d550388f 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -73,9 +73,9 @@ "As part of the keep-touched process on a switch, given a child on the original copy that was swapped (orig-swapped-child), and its related shape on the new copy (related-shape-in-new), move the orig-swapped-child into the parent of - related-shape-in-new, fix its swap-slot if needed, and then delete - related-shape-in-new" - [changes related-shape-in-new orig-swapped-child ldata page swap-ref-id] + related-shape-in-new, make it take the place of related-shape-in-new, and + then delete related-shape-in-new" + [changes related-shape-in-new orig-swapped-child ldata page] (let [;; Before to the swap, temporary move the previous ;; shape to the root panel to avoid problems when ;; the previous parent is deleted. @@ -85,7 +85,6 @@ (pcb/change-parent uuid/zero [orig-swapped-child] 0 {:allow-altering-copies true})) objects (pcb/get-objects changes) - prev-swap-slot (ctk/get-swap-slot orig-swapped-child) current-parent (get objects (:parent-id related-shape-in-new)) pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))] @@ -94,27 +93,14 @@ ;; Move the previous shape to the new parent (pcb/change-parent (:parent-id related-shape-in-new) [orig-swapped-child] pos {:allow-altering-copies true}) - ;; We need to update the swap slot only when it pointed - ;; to the swap-ref-id. Oterwise this is a swapped item - ;; inside a nested copy, so we need to keep it. - (cond-> - (= prev-swap-slot swap-ref-id) - (pcb/update-shapes - [(:id orig-swapped-child)] - #(-> % - (ctk/remove-swap-slot) - (ctk/set-swap-slot (:shape-ref related-shape-in-new))))) - - ;; A swap stored as override takes the place of the related shape, - ;; and stays a swap while its component differs from that one - (cond-> - (ctk/swapped? orig-swapped-child) - (pcb/update-shapes - [(:id orig-swapped-child)] - #(cond-> (assoc % :shape-ref (:shape-ref related-shape-in-new)) - (and (= (:component-id %) (:component-id related-shape-in-new)) - (= (:component-file %) (:component-file related-shape-in-new))) - (ctk/remove-swap-group)))) + ;; Take the place of the related shape; it stays a swap while its + ;; component differs from that one + (pcb/update-shapes + [(:id orig-swapped-child)] + #(cond-> (assoc % :shape-ref (:shape-ref related-shape-in-new)) + (and (= (:component-id %) (:component-id related-shape-in-new)) + (= (:component-file %) (:component-file related-shape-in-new))) + (ctk/remove-swap-group))) ;; Delete new non-swapped item (cls/generate-delete-shapes ldata page objects (d/ordered-set (:id related-shape-in-new)) {:allow-altering-copies true}) @@ -131,7 +117,7 @@ ;; Ignore first and last (base-parent and shape) ancestors (when (and ancestors (<= 3 num-ancestors)) (subvec (vec ancestors) 1 (dec num-ancestors)))] - (some #(or (ctk/get-swap-slot %) (ctk/swapped? %)) ancestors))) + (some ctk/swapped? ancestors))) (defn- find-shape-ref-child-of "Get the shape referenced by the shape-ref of the near main of the shape, @@ -220,23 +206,14 @@ (let [;; The switched shape itself relates to the root of the new one root? (= (:id orig-child-touched) (:id original-shape)) - ;; If the orig-child-touched was swapped, get its swap-slot - swap-slot (when-not root? (ctk/get-swap-slot orig-child-touched)) - swapped? (and (not root?) - (or (some? swap-slot) (ctk/swapped? orig-child-touched))) + swapped? (and (not root?) (ctk/swapped? orig-child-touched)) ;; orig-child-touched is in a copy. Get the referenced shape on the main component - ;; If there is a swap slot, we will get the referenced shape in another way - orig-ref-shape (cond - root? orig-base-ref-shape - swap-slot nil - :else (find-shape-ref-child-of container libraries orig-child-touched (:id orig-base-ref-shape))) + orig-ref-shape (if root? + orig-base-ref-shape + (find-shape-ref-child-of container libraries orig-child-touched (:id orig-base-ref-shape))) - orig-ref-id (if swap-slot - ;; If there is a swap slot, find the referenced shape id - (ctf/find-ref-id-for-swapped orig-child-touched container libraries) - ;; If there is not a swap slot, get the id from the orig-ref-shape - (:id orig-ref-shape)) + orig-ref-id (:id orig-ref-shape) ;; Get the shape path of the referenced main shape-path (get o-ref-shapes-p-map orig-ref-id) @@ -254,7 +231,7 @@ (if swapped? ;; If the orig-child-touched was swapped, keep it (keep-swapped-item changes related-shape-in-new orig-child-touched - ldata page orig-ref-id) + ldata page) ;; If the orig-child-touched wasn't swapped, copy ;; the touched attributes into it (cll/update-attrs-on-switch diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index b21a4a7429..23716e91d1 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -9,7 +9,9 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] + [app.common.files.comp-processors :as cfcp] [app.common.files.helpers :as cfh] + [app.common.files.swap-slots :as cfss] [app.common.geom.point :as gpt] [app.common.logic.libraries :as cll] [app.common.path-names :as cpn] @@ -154,7 +156,7 @@ (let [page (thf/current-page file) objects (:objects page)] (when (ctn/in-any-component? objects (get objects (:parent-id shape))) - (ctf/find-swap-slot shape page file (assoc libraries (:id file) file))))) + (cfcp/find-swap-slot shape page file (assoc libraries (:id file) file))))) (defn store-swap-as-slot "Store the swap of the head `head-id` the way files older than the feature @@ -172,7 +174,7 @@ (ctk/remove-swap-group)) (some? slot) - (ctk/set-swap-slot slot))] + (cfss/set-swap-slot slot))] (update file :data ctpl/update-page (:id page) #(ctst/set-shape % head')))) (defn component-swap diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index 7c20f4002e..73427778e4 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -445,16 +445,16 @@ [changes nil]) + ;; the old form does not validate any more file' (cond-> (thf/apply-changes file changes :validate? (and (not propagate-fn) (not as-slot?))) as-slot? - (-> (thc/store-swap-as-slot libraries (:id new-shape) slot) - (cond-> (not propagate-fn) (thf/validate-file!))))] + (thc/store-swap-as-slot libraries (:id new-shape) slot))] (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') - (thf/validate-file!)) + (cond-> (propagate-fn file') + (not as-slot?) (thf/validate-file!)) file'))) (defn swap-component-in-shape diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc index 448e0d2562..47232de1f5 100644 --- a/common/src/app/common/test_helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -9,10 +9,10 @@ [app.common.data :as d] [app.common.features :as ffeat] [app.common.files.changes :as cfc] + [app.common.files.swap-slots :as cfss] [app.common.files.validate :as cfv] [app.common.pprint :refer [pprint]] [app.common.test-helpers.ids-map :as thi] - [app.common.types.component :as ctk] [app.common.types.file :as ctf] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] @@ -137,9 +137,9 @@ (defn- stringify-keys [m keys] (let [kv (-> (select-keys m keys) (assoc :swap-slot (when ((set keys) :swap-slot) - (ctk/get-swap-slot m))) + (cfss/get-swap-slot m))) (assoc :swap-slot-label (when ((set keys) :swap-slot-label) - (when-let [slot (ctk/get-swap-slot m)] + (when-let [slot (cfss/get-swap-slot m)] (thi/label slot)))) (d/without-nils)) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 09481efc68..a9d0d05e27 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -6,14 +6,12 @@ (ns app.common.types.component (:require - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.time :as-alias ct] [app.common.types.page :as ctp] [app.common.types.plugins :as ctpg] - [app.common.types.variant :as ctv] - [cuerdas.core :as str])) + [app.common.types.variant :as ctv])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMA @@ -291,62 +289,6 @@ (swapped? shape) (update :touched disj :swap-group))) -(defn build-swap-slot-group - "Convert a swap-slot into a :touched group" - [swap-slot] - (when swap-slot - (keyword (str "swap-slot-" swap-slot)))) - -(defn swap-slot? - [group] - (str/starts-with? (name group) "swap-slot-")) - -(def ^:private xf:normal-touched - "Transducer that removes swap-slot touched groups." - (remove swap-slot?)) - -(defn normal-touched-groups - "Gets all touched groups that are not swap slots. - Returns an empty set immediately when `:touched` is nil or empty, - avoiding an unnecessary `into #{}` allocation for the common case." - [shape] - (let [touched (:touched shape)] - (if (empty? touched) - #{} - (into #{} xf:normal-touched touched)))) - -(defn group->swap-slot - [group] - (parse-uuid (subs (name group) 10))) ;; 10 is the length of "swap-slot-" - -(defn get-swap-slot - "If the shape has a :touched group in the form :swap-slot-, get the id." - [shape] - (let [group (d/seek swap-slot? (:touched shape))] - (when group - (group->swap-slot group)))) - -(defn set-swap-slot - "Add a touched group with a form :swap-slot-." - [shape swap-slot] - (cond-> shape - (some? swap-slot) - (update :touched set-touched-group (build-swap-slot-group swap-slot)))) - -(defn match-swap-slot? - [shape-main shape-inst] - (let [slot-main (get-swap-slot shape-main) - slot-inst (get-swap-slot shape-inst)] - (when (some? slot-inst) - (or (= slot-main slot-inst) - (= (:id shape-main) slot-inst))))) - -(defn remove-swap-slot - [shape] - (update shape :touched - (fn [touched] - (into #{} (remove #(str/starts-with? (name %) "swap-slot-") touched))))) - (defn get-deleted-component-root [component] (get-in component [:objects (:main-instance-id component)])) @@ -426,6 +368,4 @@ [group] (ex/ignoring (or (contains? all-touched-groups group) - (= group :swap-group) - (and (swap-slot? group) - (some? (group->swap-slot group)))))) + (= group :swap-group)))) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 6a2473c3c1..9518444085 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -450,27 +450,6 @@ :include-deleted? include-deleted? :with-context? with-context?))) -(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 swapped-subhead? "Whether `shape` references outside its near main parent and needs a swap slot. Same-parent positional differences are synchronized as reorders." @@ -557,80 +536,6 @@ (let [ref-component (find-ref-component file page libraries shape :include-deleted? true)] (true? (= (:id component) (:id ref-component)))))) -(defn find-swap-slot - ([shape container file libraries] - (find-swap-slot shape container file libraries #{})) - ([shape container file libraries viewed-ids] - (if (contains? viewed-ids (:id shape)) ;; prevent cycles - nil - (if-let [swap-slot (ctk/get-swap-slot shape)] - swap-slot - (let [ref-shape (find-ref-shape file - container - libraries - shape - :include-deleted? true - :with-context? true) - shape-meta (meta ref-shape) - ref-file (:file shape-meta) - ref-container (:container shape-meta)] - (if ref-shape - (if-let [swap-slot (ctk/get-swap-slot ref-shape)] - swap-slot - (if (ctk/main-instance? ref-shape) - (:id shape) - (find-swap-slot ref-shape ref-container ref-file libraries (conj viewed-ids (:id shape))))) - (:id shape))))))) ;; If cannot locate ref-shape (e.g. in an unlinked external library), use what we have - -(defn match-swap-slot? - [shape-main shape-inst container-inst container-main file libraries] - (let [slot-main (find-swap-slot shape-main container-main file libraries) - slot-inst (find-swap-slot shape-inst container-inst file libraries)] - (when (some? slot-inst) - (or (= slot-main slot-inst) - (= (:id shape-main) slot-inst))))) - -(defn- find-next-related-swap-shape-id - "Go up from the chain of references shapes that will eventually lead to the shape - with swap-slot-id as id. Returns the next shape on the chain" - [parent swap-slot-id libraries] - (let [container (get-component-container-from-head parent libraries) - objects (:objects container) - - children (cfh/get-children objects (:id parent)) - original-shape-id (->> children - (filter #(= swap-slot-id (:id %))) - first - :id)] - (if original-shape-id - ;; Return the children which id is the swap-slot-id - original-shape-id - ;; No children with swap-slot-id as id, go up - (let [referenced-shape (find-ref-shape nil container libraries parent) - ;; Recursive call that will get the id of the next shape on - ;; the chain that ends on a shape with swap-slot-id as id - next-shape-id (when referenced-shape - (find-next-related-swap-shape-id referenced-shape swap-slot-id libraries))] - ;; Return the children which shape-ref points to the next-shape-id - (->> children - (filter #(= next-shape-id (:shape-ref %))) - first - :id))))) - -(defn find-ref-id-for-swapped - "When a shape has been swapped, find the original ref-id that the shape had - before the swap" - [shape container libraries] - (let [swap-slot (ctk/get-swap-slot shape) - objects (:objects container) - - parent (get objects (:parent-id shape)) - parent-head (ctn/get-head-shape objects parent) - parent-ref (find-ref-shape nil container libraries parent-head)] - - (when (and swap-slot parent-ref) - (find-next-related-swap-shape-id parent-ref swap-slot libraries)))) - (defn get-component-shapes "Retrieve all shapes of the component" [file-data component] @@ -1317,10 +1222,10 @@ (defn get-touched-from-ref-chain-until-target-ref "Returns a set with the :touched of all the items on the shape ref chain until target-ref, including itself" - [container libraries shape target-ref & {:keys [remove-slots?] :or {remove-slots? true}}] - (let [chain (get-ref-chain-until-target-ref container libraries shape target-ref) - more-touched (cond->> chain - :always (map :touched) - :always (reduce set/union #{}) - remove-slots? (remove ctk/swap-slot?))] - (set/union (or (:touched shape) #{}) (set more-touched)))) \ No newline at end of file + [container libraries shape target-ref] + (let [chain (get-ref-chain-until-target-ref container libraries shape target-ref) + more-touched (->> chain + (map :touched) + (remove nil?) + (apply set/union))] + (set/union (or (:touched shape) #{}) more-touched))) diff --git a/common/test/common_tests/files/comp_processors_test.cljc b/common/test/common_tests/files/comp_processors_test.cljc index 5438228e72..c911d2c956 100644 --- a/common/test/common_tests/files/comp_processors_test.cljc +++ b/common/test/common_tests/files/comp_processors_test.cljc @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.files.comp-processors :as cfcp] + [app.common.files.swap-slots :as cfss] [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])) @@ -332,7 +332,7 @@ {(thi/id :copy2-nested-head) {:touched [nil - #{(ctk/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}] + #{(cfss/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}] (t/is (= expected-diff diff)))) @@ -412,7 +412,7 @@ {(thi/id :nested-subhead2) {:touched [nil - #{(ctk/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}] + #{(cfss/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}] (t/is (= expected-diff diff)))) @@ -469,7 +469,7 @@ {(thi/id :copy2-nested-head) {:touched [nil - #{(ctk/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}] + #{(cfss/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}] (t/is (= expected-diff diff))))) @@ -862,3 +862,372 @@ (t/is (= expected-diff diff))))) +(t/deftest test-get-swap-slot + (let [s1 (ths/sample-shape :s1) + s2 (ths/sample-shape :s2 :touched #{:visibility-group}) + s3 (ths/sample-shape :s3 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) + s4 (ths/sample-shape :s4 :touched #{:fill-group + :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) + s5 (ths/sample-shape :s5 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f + :content-group + :geometry-group}) + s6 (ths/sample-shape :s6 :touched #{:swap-slot-9cc181fa})] + (t/is (nil? (cfss/get-swap-slot s1))) + (t/is (nil? (cfss/get-swap-slot s2))) + (t/is (= (cfss/get-swap-slot s3) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + (t/is (= (cfss/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + (t/is (= (cfss/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + (t/is (nil? (cfss/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 (cfcp/find-near-match file page {} frame1) + near-match2 (cfcp/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 (cfcp/find-near-match file page {} main-root) + near-main-child1 (cfcp/find-near-match file page {} main-child1) + near-main-child2 (cfcp/find-near-match file page {} main-child2) + near-main-child3 (cfcp/find-near-match file page {} main-child3) + near-copy-root (cfcp/find-near-match file page {} copy-root) + near-copy-child1 (cfcp/find-near-match file page {} copy-child1) + near-copy-child2 (cfcp/find-near-match file page {} copy-child2) + near-copy-child3 (cfcp/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 (cfcp/find-near-match file page {} main1-root) + near-main1-child (cfcp/find-near-match file page {} main1-child) + near-main2-root (cfcp/find-near-match file page {} main2-root) + near-nested-head (cfcp/find-near-match file page {} nested-head) + near-nested-child (cfcp/find-near-match file page {} nested-child) + near-copy2 (cfcp/find-near-match file page {} copy2) + near-copy2-nested-head (cfcp/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (cfcp/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 :as-slot? true)) + + 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 (cfcp/find-near-match file page {} main1-root) + near-main1-child (cfcp/find-near-match file page {} main1-child) + near-main2-root (cfcp/find-near-match file page {} main2-root) + near-nested-head (cfcp/find-near-match file page {} nested-head) + near-nested-child (cfcp/find-near-match file page {} nested-child) + near-copy2 (cfcp/find-near-match file page {} copy2) + near-copy2-nested-head (cfcp/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (cfcp/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 :as-slot? true)) + + 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 (cfcp/find-near-match file page {} main1-root) + near-main1-child (cfcp/find-near-match file page {} main1-child) + near-main2-root (cfcp/find-near-match file page {} main2-root) + near-nested2-head (cfcp/find-near-match file page {} nested2-head) + near-nested2-child (cfcp/find-near-match file page {} nested2-child) + near-main3-root (cfcp/find-near-match file page {} main3-root) + near-main3-child (cfcp/find-near-match file page {} main3-child) + near-main4-root (cfcp/find-near-match file page {} main4-root) + near-nested4-head (cfcp/find-near-match file page {} nested4-head) + near-nested4-child (cfcp/find-near-match file page {} nested4-child) + near-copy2 (cfcp/find-near-match file page {} copy2) + near-copy2-nested-head (cfcp/find-near-match file page {} copy2-nested-head) + near-copy2-nested4-head (cfcp/find-near-match file page {} copy2-nested4-head) + near-copy2-nested4-child (cfcp/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 :as-slot? true)) + + 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 (cfcp/find-near-match file page-file libraries main1-root) + near-main1-child (cfcp/find-near-match file page-file libraries main1-child) + near-main2-root (cfcp/find-near-match file page-file libraries main2-root) + near-nested2-head (cfcp/find-near-match library1 page-library1 libraries nested2-head) + near-nested2-child (cfcp/find-near-match library1 page-library1 libraries nested2-child) + near-main3-root (cfcp/find-near-match file page-file libraries main3-root) + near-main3-child (cfcp/find-near-match file page-file libraries main3-child) + near-main4-root (cfcp/find-near-match file page-file libraries main4-root) + near-nested4-head (cfcp/find-near-match library2 page-library2 libraries nested4-head) + near-nested4-child (cfcp/find-near-match library2 page-library2 libraries nested4-child) + near-copy2 (cfcp/find-near-match file page-file libraries copy2) + near-copy2-nested-head (cfcp/find-near-match file page-file libraries copy2-nested-head) + near-copy2-nested4-head (cfcp/find-near-match file page-file libraries copy2-nested4-head) + near-copy2-nested4-child (cfcp/find-near-match file page-file libraries copy2-nested4-child)] + + (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/common/test/common_tests/files/convert_swap_slots_test.cljc b/common/test/common_tests/files/convert_swap_slots_test.cljc new file mode 100644 index 0000000000..20cf2065ed --- /dev/null +++ b/common/test/common_tests/files/convert_swap_slots_test.cljc @@ -0,0 +1,283 @@ +;; 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 SUBSIDIARY SL + +(ns common-tests.files.convert-swap-slots-test + "The conversion of swap slots, the form files older than the feature + components/swap-group store swaps in, to override groups." + (:require + [app.common.features :as cfeat] + [app.common.files.changes-builder :as pcb] + [app.common.files.comp-processors :as cfcp] + [app.common.files.helpers :as cfh] + [app.common.files.migrations :as fmg] + [app.common.files.swap-slots :as cfss] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.logic.shapes :as cls] + [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.file :as ctf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- add-components + [file] + ;; {:star-main} [:name Frame1] # [Component :star] + ;; :star-rect [:name Rect1] + ;; {:heart-main} [:name Frame1] # [Component :heart] + ;; :heart-rect [:name Rect1] + ;; {:dot-main} [:name Frame1] # [Component :dot] + ;; :dot-rect [:name Rect1] + ;; {:badge-main} [:name Frame1] # [Component :badge] + ;; :badge-dot [:name Frame1] @--> [Component :dot] :dot-main + ;; {:card-main} [:name Frame1] # [Component :card] + ;; :card-star [:name Frame1] @--> [Component :star] :star-main + (-> file + (tho/add-simple-component :star :star-main :star-rect + :child-params {:fills (ths/sample-fills-color :fill-color "#111111")}) + (tho/add-simple-component :heart :heart-main :heart-rect + :child-params {:fills (ths/sample-fills-color :fill-color "#222222")}) + (tho/add-simple-component :dot :dot-main :dot-rect) + (tho/add-frame :badge-main) + (thc/instantiate-component :dot :badge-dot :parent-label :badge-main) + (thc/make-component :badge :badge-main) + (tho/add-frame :card-main) + (thc/instantiate-component :star :card-star :parent-label :card-main) + (thc/make-component :card :card-main))) + +(defn- setup + [] + ;; :copy [:name Frame1] #--> [Component :card] :card-main + ;; :copy-star [:name Frame1] @--> [Component :star] :card-star + (-> (thf/sample-file :file1) + (add-components) + (thc/instantiate-component :card :copy :children-labels [:copy-star]))) + +(defn- setup-two-levels + [file] + ;; {:page-main} [:name Frame1] # [Component :page] + ;; :inner-copy [:name Frame1] @--> [Component :card] :card-main + ;; :inner-star [:name Frame1] @--> [Component :star] :card-star + ;; :outer-copy [:name Frame1] #--> [Component :page] :page-main + ;; :outer-inner [:name Frame1] @--> [Component :card] :inner-copy + ;; :outer-star [:name Frame1] @--> [Component :star] :inner-star + (-> file + (tho/add-frame :page-main) + (thc/instantiate-component :card :inner-copy + :parent-label :page-main + :children-labels [:inner-star]) + (thc/make-component :page :page-main) + (thc/instantiate-component :page :outer-copy + :children-labels [:outer-inner :outer-star]))) + +(defn- convert + ([file] (convert file {})) + ([file libraries] + (ctf/update-file-data file #(cfcp/convert-swap-slots % libraries)))) + +(defn- swap-state + "What the conversion changes on a shape." + [file label] + (let [shape (ths/get-shape file label)] + {:shape-ref (:shape-ref shape) + :touched (into #{} (map #(if (cfss/swap-slot-group? %) :swap-slot %)) (:touched shape))})) + +(defn- first-child-id + [file label] + (first (:shapes (ths/get-shape file label)))) + +(t/deftest swap-in-a-copy-refers-to-the-replaced-child + (let [old (-> (setup) + (tho/swap-component-in-shape :copy-star :heart :as-slot? true)) + file (-> old + (convert) + (thf/validate-file!))] + (t/is (= {:shape-ref (thi/id :heart-main) :touched #{:swap-slot}} + (swap-state old :copy-star))) + (t/is (= {:shape-ref (thi/id :card-star) :touched #{:swap-group}} + (swap-state file :copy-star))) + (t/is (= "#333333" (-> file + (tho/update-color :heart-rect "#333333") + (tho/propagate-component-changes :heart) + (thf/validate-file!) + (tho/bottom-fill-color :copy)))))) + +(t/deftest swap-in-a-main-drops-the-slot + (let [old (-> (setup) + (tho/swap-component-in-shape :card-star :heart + :as-slot? true + :propagate-fn #(tho/propagate-component-changes % :card))) + file (-> old + (convert) + (thf/validate-file!))] + (t/is (= #{:swap-slot} (:touched (swap-state old :card-star)))) + (t/is (= {:shape-ref (thi/id :heart-main) :touched #{}} + (swap-state file :card-star))))) + +(t/deftest slot-inherited-from-the-main-is-dropped + (let [old (-> (setup) + (tho/swap-component-in-shape :card-star :heart + :as-slot? true + :propagate-fn #(tho/propagate-component-changes % :card)) + (thc/instantiate-component :card :copy2 :children-labels [:copy2-star])) + slot (cfss/get-swap-slot (ths/get-shape old :card-star)) + old (ths/update-shape old :copy2-star :touched #{(cfss/build-swap-slot-group slot)}) + file (-> old + (convert) + (thf/validate-file!))] + (t/is (= {:shape-ref (thi/id :card-star) :touched #{}} + (swap-state file :copy2-star))))) + +(t/deftest swaps-nested-in-swaps-are-paired-on-the-old-data + (let [old (-> (setup) + (setup-two-levels) + (tho/swap-component-in-shape :outer-star :badge :as-slot? true)) + _ (thi/set-id! :outer-dot (first-child-id old :outer-star)) + file (-> old + (tho/swap-component-in-shape :outer-dot :heart :as-slot? true) + (convert) + (thf/validate-file!))] + (t/is (= {:shape-ref (thi/id :inner-star) :touched #{:swap-group}} + (swap-state file :outer-star))) + (t/is (= {:shape-ref (thi/id :badge-dot) :touched #{:swap-group}} + (swap-state file :outer-dot))))) + +(defn- setup-library + [] + ;; The library holds the components, and a swap in a copy inside its + ;; :page main: :inner-star swapped for :heart. The file holds a copy of + ;; :page, whose star is swapped for :badge. + (let [library (-> (thf/sample-file :library) + (add-components) + (setup-two-levels) + (tho/swap-component-in-shape :inner-star :heart + :as-slot? true + :propagate-fn #(tho/propagate-component-changes % :page))) + file (-> (thf/sample-file :file1) + (thc/instantiate-component :page :outer-copy + :library library + :children-labels [:outer-inner :outer-star]) + (tho/swap-component-in-shape :outer-star :badge + :library library + :as-slot? true))] + [library file])) + +(t/deftest pairing-reads-a-library-converted-or-not + (let [[library file] (setup-library) + converted (convert library) + expected {:shape-ref (thi/id :inner-star) :touched #{:swap-group}}] + (t/is (= expected (swap-state (convert file {(:id library) library}) :outer-star))) + (t/is (= expected (swap-state (convert file {(:id converted) converted}) :outer-star))))) + +(t/deftest unreachable-near-main-refers-to-the-slot + (let [[_ file] (setup-library) + slot (cfss/get-swap-slot (ths/get-shape file :outer-star)) + file' (convert file)] + (t/is (= {:shape-ref slot :touched #{:swap-group}} + (swap-state file' :outer-star))))) + +(t/deftest unreachable-near-main-can-be-left-for-later + (let [[library file] (setup-library) + file' (binding [cfcp/*defer-unreachable-swaps* true] + (convert file))] + (t/is (= (swap-state file :outer-star) (swap-state file' :outer-star))) + (t/is (::cfcp/deferred-swaps (meta (:data file')))) + (t/is (= {:shape-ref (thi/id :inner-star) :touched #{:swap-group}} + (swap-state (convert file' {(:id library) library}) :outer-star))))) + +(t/deftest slot-inherited-from-an-unreachable-main-is-dropped + ;; the near main is in a library that is not loaded: the copy refers to + ;; its slot, the child of that main that was swapped there + (let [library (-> (thf/sample-file :library) + (add-components) + (tho/swap-component-in-shape :card-star :heart + :as-slot? true + :propagate-fn #(tho/propagate-component-changes % :card))) + slot (cfss/get-swap-slot (ths/get-shape library :card-star)) + file (-> (thf/sample-file :file1) + (thc/instantiate-component :card :copy + :library library + :children-labels [:copy-star]) + (ths/update-shape :copy-star :touched #{(cfss/build-swap-slot-group slot)}))] + (t/is (= (thi/id :card-star) slot (:shape-ref (ths/get-shape file :copy-star)))) + (t/is (= {:shape-ref (thi/id :card-star) :touched #{}} + (swap-state (convert file) :copy-star))) + (t/is (= {:shape-ref (thi/id :card-star) :touched #{}} + (swap-state (convert file {(:id library) library}) :copy-star))))) + +(t/deftest slot-matching-no-child-is-kept-as-its-ref + (let [slot (uuid/next) + file (-> (setup) + (tho/swap-component-in-shape :copy-star :heart :as-slot? true) + (ths/update-shape :copy-star :touched #{(cfss/build-swap-slot-group slot)}) + (convert))] + (t/is (= {:shape-ref slot :touched #{:swap-group}} + (swap-state file :copy-star))))) + +(t/deftest migration-converts-and-adds-the-feature + (let [old (-> (setup) + (tho/swap-component-in-shape :copy-star :heart :as-slot? true) + (update :features disj "components/swap-group") + (assoc :migrations (disj fmg/available-migrations + "0034-convert-swap-slots-to-swap-groups"))) + file (fmg/migrate-file old (delay {}))] + (t/is (contains? (:features file) "components/swap-group")) + (t/is (not (contains? (:features old) "components/swap-group"))) + (t/is (contains? cfeat/default-features "components/swap-group")) + (t/is (= {:shape-ref (thi/id :card-star) :touched #{:swap-group}} + (swap-state file :copy-star))))) + +(defn- move-shape + "Move the shape and its children by the given delta, with no sync." + [file label dx dy] + (let [page (thf/current-page file) + ids (set (cfh/get-children-ids-with-self (:objects page) (thi/id label))) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + ids + #(gsh/move % (gpt/point dx dy)) + (:objects page) + {})] + (thf/apply-changes file changes :validate? false))) + +(defn- position + [file label] + (let [shape (ths/get-shape file label)] + [(get-in shape [:selrect :x]) (get-in shape [:selrect :y])])) + +(t/deftest swap-away-from-its-position-keeps-it + ;; the main moved the replaced child after the swap; the swapped head did + ;; not follow, and it keeps its place + (let [file (-> (setup) + (tho/swap-component-in-shape :copy-star :heart :as-slot? true) + (move-shape :card-star 10 20) + (convert)) + head (ths/get-shape file :copy-star) + file' (-> file + (tho/update-color :card-star "#555555") + (tho/propagate-component-changes :card) + (thf/validate-file!))] + (t/is (= {:shape-ref (thi/id :card-star) :touched #{:swap-group :geometry-group}} + (swap-state file :copy-star))) + (t/is (= [(get-in head [:selrect :x]) (get-in head [:selrect :y])] + (position file' :copy-star))))) + +(t/deftest swap-in-its-position-follows-the-main + (let [file (-> (setup) + (tho/swap-component-in-shape :copy-star :heart :as-slot? true) + (convert) + (thf/validate-file!)) + [x y] (position file :copy-star) + file' (-> file + (move-shape :card-star 10 20) + (tho/propagate-component-changes :card) + (thf/validate-file!))] + (t/is (= #{:swap-group} (:touched (ths/get-shape file :copy-star)))) + (t/is (= [(+ x 10) (+ y 20)] (position file' :copy-star))))) diff --git a/common/test/common_tests/files/repair_components_test.cljc b/common/test/common_tests/files/repair_components_test.cljc index 3e19e13cbd..575813399a 100644 --- a/common/test/common_tests/files/repair_components_test.cljc +++ b/common/test/common_tests/files/repair_components_test.cljc @@ -16,6 +16,7 @@ (:require [app.common.files.changes-builder :as pcb] [app.common.files.repair :as cfr] + [app.common.files.swap-slots :as cfss] [app.common.files.validate :as cfv] [app.common.logic.shapes :as cls] [app.common.test-helpers.components :as thc] @@ -146,7 +147,7 @@ (defn- slot-group [id] - #{(ctk/build-swap-slot-group id)}) + #{(cfss/build-swap-slot-group id)}) ;; ---- Main heads @@ -247,7 +248,7 @@ (t/deftest ref-shape-not-found (let [[found file' errors] (repair (ths/update-shape (nested) :copy2-nested-head :shape-ref (uuid/next)))] - (t/is (= #{:ref-shape-not-found :missing-slot} found)) + (t/is (= #{:ref-shape-not-found :ref-not-in-near-main} found)) (t/is (nil? errors)) ;; no matching shape: the nested copy is detached (t/is (nil? (:shape-ref (ths/get-shape file' :copy2-nested-head)))))) @@ -381,35 +382,36 @@ (let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :touched (slot-group (uuid/next))))] (t/is (= #{:misplaced-slot} found)) (t/is (nil? errors)) - (t/is (nil? (ctk/get-swap-slot (ths/get-shape file' :copy-root)))))) + (t/is (nil? (cfss/get-swap-slot (ths/get-shape file' :copy-root)))))) -(t/deftest duplicate-slot - (let [file (-> (two-nested) - (ths/update-shape :copy-a :touched (slot-group (thi/id :nested-a))) - (ths/update-shape :copy-b :touched (slot-group (thi/id :nested-a)))) - [found file' errors] (repair file)] - (t/is (= #{:duplicate-slot} found)) +(t/deftest misplaced-swap-group + (let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :touched #{:swap-group}))] + (t/is (= #{:misplaced-swap-group} found)) (t/is (nil? errors)) - (t/is (= (thi/id :nested-a) (ctk/get-swap-slot (ths/get-shape file' :copy-a)))) - (t/is (nil? (ctk/get-swap-slot (ths/get-shape file' :copy-b)))))) + (t/is (not (ctk/swapped? (ths/get-shape file' :copy-root)))))) -(t/deftest component-duplicate-slot - (let [file (-> (two-nested) - (delete-main :main2-root) - (update-deleted-object :component2 :nested-a #(assoc % :touched (slot-group (thi/id :nested-a)))) - (update-deleted-object :component2 :nested-b #(assoc % :touched (slot-group (thi/id :nested-a))))) - [found file' errors] (repair file)] - (t/is (= #{:component-duplicate-slot} found)) - (t/is (nil? errors)) - (t/is (= (thi/id :nested-a) (ctk/get-swap-slot (deleted-object file' :component2 :nested-a)))) - (t/is (nil? (ctk/get-swap-slot (deleted-object file' :component2 :nested-b)))))) - -(t/deftest missing-slot +(t/deftest ref-not-in-near-main + ;; a swapped copy that lost its position: it refers to the root of its own + ;; component, as files older than components/swap-group did, with no slot (let [file (-> (two-nested) (tho/add-simple-component :component3 :main3-root :main3-child) (tho/swap-component-in-shape :copy-a :component3 :new-shape-label :swapped :as-slot? true) (ths/update-shape :swapped :touched nil)) - [found file' errors] (repair file)] - (t/is (= #{:missing-slot} found)) + [found file' errors] (repair file) + swapped (ths/get-shape file' :swapped)] + (t/is (= #{:ref-not-in-near-main} found)) (t/is (nil? errors)) - (t/is (= (thi/id :nested-a) (ctk/get-swap-slot (ths/get-shape file' :swapped)))))) + ;; it takes the free position at its index, as a swap + (t/is (= (thi/id :nested-a) (:shape-ref swapped))) + (t/is (ctk/swapped? swapped)))) + +(t/deftest duplicate-ref + ;; two children of the copy take the position of the same main shape + (let [file (ths/update-shape (two-nested) :copy-b :shape-ref (thi/id :nested-a)) + [found file' errors] (repair file) + children (:shapes (ths/get-shape file' :copy2-root))] + (t/is (contains? found :duplicate-ref)) + (t/is (nil? errors)) + ;; the first one is kept, the second one deleted with its children + (t/is (= [(thi/id :copy-a)] (filterv #{(thi/id :copy-a) (thi/id :copy-b)} children))) + (t/is (nil? (ths/get-shape file' :copy-b))))) diff --git a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc b/common/test/common_tests/logic/comp_move_swapped_copies_test.cljc similarity index 96% rename from common/test/common_tests/logic/comp_remove_swap_slots_test.cljc rename to common/test/common_tests/logic/comp_move_swapped_copies_test.cljc index 38b985c076..bda7baf1b8 100644 --- a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc +++ b/common/test/common_tests/logic/comp_move_swapped_copies_test.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS SUBSIDIARY SL -(ns common-tests.logic.comp-remove-swap-slots-test +(ns common-tests.logic.comp-move-swapped-copies-test (:require [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] @@ -53,7 +53,7 @@ (tho/add-frame :frame-b2) (thc/make-component :b2 :frame-b2))) -(t/deftest test-remove-swap-slot-relocating-blue1-to-root +(t/deftest test-relocating-blue1-to-root (let [;; ==== Setup file (setup-file) @@ -82,7 +82,7 @@ (t/is (some? blue1')) (t/is (not (ctk/swapped? blue1'))))) -(t/deftest test-remove-swap-slot-relocating-blue1-to-b2 +(t/deftest test-relocating-blue1-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -112,7 +112,7 @@ (t/is (some? blue1')) (t/is (not (ctk/swapped? blue1'))))) -(t/deftest test-remove-swap-slot-relocating-yellow-to-root +(t/deftest test-relocating-yellow-to-root (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -155,7 +155,7 @@ (t/is (some? blue1'')) (t/is (not (ctk/swapped? blue1''))))) -(t/deftest test-remove-swap-slot-relocating-yellow-to-b2 +(t/deftest test-relocating-yellow-to-b2 (let [;; ==== Setup file (setup-file) page (thf/current-page file) @@ -208,7 +208,7 @@ (str/starts-with? (thi/label (:id %)) " (thf/sample-file :file1) + (tho/add-simple-component :star :star-main :star-rect) + (tho/add-simple-component :heart :heart-main :heart-rect) + (tho/add-frame :card-main :layout :flex :layout-flex-dir :row) + (thc/instantiate-component :star :card-star :parent-label :card-main) + (thc/make-component :card :card-main) + (thc/instantiate-component :card :copy :children-labels [:copy-star]) + (tho/swap-component-in-shape :copy-star :heart) + (ths/update-shape :card-star :layout-item-margin margin) + (propagate :card) + (thf/validate-file!))] + (t/is (= margin (:layout-item-margin (ths/get-shape file :copy-star)))))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index d472226ad8..94a3fbde07 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -25,6 +25,7 @@ [common-tests.files-migrations-0026-test] [common-tests.files-migrations-test] [common-tests.files.comp-processors-test] + [common-tests.files.convert-swap-slots-test] [common-tests.files.helpers-test] [common-tests.files.repair-components-test] [common-tests.files.repair-test] @@ -63,7 +64,7 @@ [common-tests.logic.comp-detach-with-nested-test] [common-tests.logic.comp-flex-interactions-test] [common-tests.logic.comp-main-edit-breaks-copy-slots-test] - [common-tests.logic.comp-remove-swap-slots-test] + [common-tests.logic.comp-move-swapped-copies-test] [common-tests.logic.comp-reset-test] [common-tests.logic.comp-sync-test] [common-tests.logic.comp-touched-test] @@ -145,6 +146,7 @@ 'common-tests.files-migrations-0026-test 'common-tests.files-migrations-test 'common-tests.files.comp-processors-test + 'common-tests.files.convert-swap-slots-test 'common-tests.files.helpers-test 'common-tests.files.repair-components-test 'common-tests.files.repair-test @@ -183,7 +185,7 @@ 'common-tests.logic.comp-detach-with-nested-test 'common-tests.logic.comp-flex-interactions-test 'common-tests.logic.comp-main-edit-breaks-copy-slots-test - 'common-tests.logic.comp-remove-swap-slots-test + 'common-tests.logic.comp-move-swapped-copies-test 'common-tests.logic.comp-reset-test 'common-tests.logic.comp-sync-test 'common-tests.logic.comp-touched-test diff --git a/common/test/common_tests/types/components_test.cljc b/common/test/common_tests/types/components_test.cljc index 3c2396e92e..64dc475f7a 100644 --- a/common/test/common_tests/types/components_test.cljc +++ b/common/test/common_tests/types/components_test.cljc @@ -6,13 +6,8 @@ (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) @@ -20,383 +15,11 @@ (t/deftest test-valid-touched-group (t/is (ctk/valid-touched-group? :name-group)) (t/is (ctk/valid-touched-group? :geometry-group)) - (t/is (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f)) + (t/is (ctk/valid-touched-group? :swap-group)) (t/is (not (ctk/valid-touched-group? :this-is-not-a-group))) - (t/is (not (ctk/valid-touched-group? :swap-slot-))) - (t/is (not (ctk/valid-touched-group? :swap-slot-xxxxxx))) - (t/is (not (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004))) + (t/is (not (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f))) (t/is (not (ctk/valid-touched-group? nil)))) -(t/deftest test-get-swap-slot - (let [s1 (ths/sample-shape :s1) - s2 (ths/sample-shape :s2 :touched #{:visibility-group}) - s3 (ths/sample-shape :s3 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) - s4 (ths/sample-shape :s4 :touched #{:fill-group - :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) - s5 (ths/sample-shape :s5 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f - :content-group - :geometry-group}) - s6 (ths/sample-shape :s6 :touched #{:swap-slot-9cc181fa})] - (t/is (nil? (ctk/get-swap-slot s1))) - (t/is (nil? (ctk/get-swap-slot s2))) - (t/is (= (ctk/get-swap-slot s3) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) - (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 :as-slot? true)) - - 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 :as-slot? true)) - - 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 :as-slot? true)) - - 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)] - - (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)))))) - (t/deftest variant-main-and-formula-rename (let [container {:id (thi/new-id! :container) :is-variant-container true} diff --git a/common/test/common_tests/types/file_test.cljc b/common/test/common_tests/types/file_test.cljc index d8d029772d..ca3c1200ea 100644 --- a/common/test/common_tests/types/file_test.cljc +++ b/common/test/common_tests/types/file_test.cljc @@ -11,6 +11,8 @@ components." (:require [app.common.data :as d] + [app.common.files.comp-processors :as cfcp] + [app.common.files.swap-slots :as cfss] [app.common.files.validate :as cfv] [app.common.test-helpers.components :as thc] [app.common.test-helpers.composable.core :as tm] @@ -200,9 +202,9 @@ (defn- ref-id-for-swapped [file] - (ctf/find-ref-id-for-swapped (ths/get-shape file :swapped) - (thf/current-page file) - {(:id file) file})) + (cfcp/find-ref-id-for-swapped (ths/get-shape file :swapped) + (thf/current-page file) + {(:id file) file})) (t/deftest ref-id-for-swapped-goes-up-the-ref-chain (let [file (-> (thf/sample-file :file) @@ -212,7 +214,7 @@ :c3 :main3 :nested-head2 :nested-subhead2 :copy3) (swap-at-second-level))] - (t/is (= (thi/id :nested-head1) (ctk/get-swap-slot (ths/get-shape file :swapped)))) + (t/is (= (thi/id :nested-head1) (cfss/get-swap-slot (ths/get-shape file :swapped)))) (t/is (= (thi/id :nested-subhead2) (ref-id-for-swapped file))))) (t/deftest ref-id-for-swapped-with-mains-on-other-pages @@ -227,7 +229,7 @@ (thc/make-component :c3 :main3) (thc/instantiate-component :c3 :copy3) (swap-at-second-level))] - (t/is (= (thi/id :nested-head1) (ctk/get-swap-slot (ths/get-shape file :swapped)))) + (t/is (= (thi/id :nested-head1) (cfss/get-swap-slot (ths/get-shape file :swapped)))) (tm/known-failure "F56" (thi/id :nested-subhead2) nil (ref-id-for-swapped file)))) ;; ----- Deleted components @@ -329,9 +331,9 @@ (tho/add-simple-component-with-copy :component :main :main-child :copy :copy-root-params {:children-labels [:copy-child]})) - match (ctf/find-near-match file (thf/current-page file) {(:id file) file} - (ths/get-shape file :copy-child) - :with-context? true)] + match (cfcp/find-near-match file (thf/current-page file) {(:id file) file} + (ths/get-shape file :copy-child) + :with-context? true)] (t/is (= (thi/id :main-child) (:id match))) (t/is (= :page (:type (:container (meta match))))) (t/is (= (:id (thf/current-page file)) (:id (:container (meta match))))))) diff --git a/frontend/src/app/main/ui/workspace/components_debugger.cljs b/frontend/src/app/main/ui/workspace/components_debugger.cljs index 911ecb3cd5..95759f8f4e 100644 --- a/frontend/src/app/main/ui/workspace/components_debugger.cljs +++ b/frontend/src/app/main/ui/workspace/components_debugger.cljs @@ -111,7 +111,7 @@ (ctk/instance-root? shape) (ctk/main-instance? shape) ;; Swapped nested heads are not instance-roots, but still need a slot. - (some? (ctk/get-swap-slot shape))))) + (ctk/swapped? shape)))) (defn- box-thumbnail-slot? "A box reserves a thumbnail slot when it is the root of a panel or the @@ -231,12 +231,9 @@ Includes deleted components so the chain can surface them." [file page libraries objects shape] (when-let [source (near-match-source-shape objects shape)] - (when-let [ref-shape (or (ctf/find-near-match file page libraries source - :with-context? true - :include-deleted? true) - (ctf/find-ref-shape file page libraries source - :with-context? true - :include-deleted? true))] + (when-let [ref-shape (ctf/find-ref-shape file page libraries source + :with-context? true + :include-deleted? true)] (near-component-render-data (ref-shape->component-head ref-shape) ref-shape)))) @@ -279,27 +276,22 @@ :page-name (:name match-page) :page-id (:id match-page))))) -(defn- resolve-swap-slot-main-data - "When the highlighted shape has a swap-slot, return its component main tree." - [file page libraries objects highlight-id] +(defn- resolve-swap-main-data + "When the highlighted shape is swapped, return the main tree of its own + component, where its content comes from." + [file _page libraries objects highlight-id] (when-let [shape (get objects highlight-id)] - (when (ctk/get-swap-slot shape) - (when-let [component (ctf/find-ref-component file page libraries shape - :include-deleted? true)] - (let [head (ctn/get-head-shape objects shape) - component-file (ctf/find-component-file file libraries (:component-file head)) + (when (ctk/swapped? shape) + (when-let [component (ctf/resolve-component shape file libraries :include-deleted? true)] + (let [component-file (ctf/find-component-file file libraries (:component-file shape)) file-data (:data component-file) container (ctf/get-component-container file-data component) root-shape (ctf/get-component-root file-data component) - component-page (ctf/get-component-page file-data component) - highlight-main-id - (or (ctf/find-ref-id-for-swapped shape page libraries) - (some-> (ctf/get-ref-shape file-data component shape) :id) - (ctk/get-swap-slot shape))] + component-page (ctf/get-component-page file-data component)] (when root-shape {:root-shape root-shape :objects (:objects container) - :highlight-id highlight-main-id + :highlight-id (:id root-shape) :file component-file :deleted (boolean (:deleted component)) :file-name (resolve-file-name component-file libraries) @@ -403,14 +395,14 @@ [objects root-shape labels thumbnail-context highlight-id] (shape-box-height objects root-shape preview-width labels thumbnail-context highlight-id true)) -(defn- swap-slot-panel +(defn- swap-panel "Build a swap-main panel for the highlight at `column-idx`. The panel is drawn one column to the right (`(inc column-idx)`), so arrows pair it with `highlight-panels` via `(dec column-idx)`." [column-idx swap-row-y file page libraries objects highlight-id] (when-let [{:keys [root-shape objects highlight-id file deleted file-id page-id file-name page-name]} - (resolve-swap-slot-main-data file page libraries objects highlight-id)] + (resolve-swap-main-data file page libraries objects highlight-id)] {:column-idx (inc column-idx) :y swap-row-y :root-shape root-shape @@ -776,7 +768,7 @@ [shape file-id page-id enabled?] (let [shape-id (:id shape) tag (shape-thumbnail-tag shape) - swapped? (some? (ctk/get-swap-slot shape)) + swapped? (ctk/swapped? shape) object-id (mf/with-memo [file-id page-id shape-id tag] (thc/fmt-object-id file-id page-id shape-id tag)) thumb-ref (mf/with-memo [object-id] @@ -955,17 +947,17 @@ selection-swap-panel (when (and root-shape selected-id swap-row-y) - (swap-slot-panel 0 swap-row-y - file page libraries objects selected-id)) + (swap-panel 0 swap-row-y + file page libraries objects selected-id)) near-swap-panels (when (and near-component-chain swap-row-y) (keep-indexed ;; Near columns are 1-based (selection is column 0), matching - ;; swap-slot-panel's (inc column-idx) placement and arrow pairing. + ;; swap-panel's (inc column-idx) placement and arrow pairing. (fn [idx {:keys [highlight-id file page objects]}] - (swap-slot-panel (inc idx) swap-row-y - file page libraries objects highlight-id)) + (swap-panel (inc idx) swap-row-y + file page libraries objects highlight-id)) near-component-chain)) swap-panels* diff --git a/frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs b/frontend/test/frontend_tests/logic/comp_move_swapped_copies_test.cljs similarity index 96% rename from frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs rename to frontend/test/frontend_tests/logic/comp_move_swapped_copies_test.cljs index 41b485bc0e..2f650c0ba3 100644 --- a/frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs +++ b/frontend/test/frontend_tests/logic/comp_move_swapped_copies_test.cljs @@ -3,7 +3,7 @@ ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS SUBSIDIARY SL -(ns frontend-tests.logic.comp-remove-swap-slots-test +(ns frontend-tests.logic.comp-move-swapped-copies-test (:require [app.common.files.validate :as cfv] [app.common.test-helpers.components :as cthc] @@ -94,7 +94,7 @@ (str/starts-with? (cthi/label (:id %)) "