diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 8e4bbb67d9..6c46d70c9d 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -6,6 +6,7 @@ (ns app.common.files.changes (:require + #?(:cljs [app.common.files.validate :as val]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -444,7 +445,14 @@ [:map {:title "SetTokensSource"} [:type [:= :set-tokens-source]] [:file-id ::sm/uuid] - [:library-id [:maybe ::sm/uuid]]]]]) + [:library-id [:maybe ::sm/uuid]]]] + + [:validate-shapes + [:map {:title "ValidateShapesChange"} + [:type [:= :validate-shapes]] + [:page-id ::sm/uuid] + [:shape-ids [:vector ::sm/uuid]] + [:context :string]]]]) (def schema:changes [:sequential {:gen/max 5 :gen/min 1} schema:change]) @@ -480,7 +488,7 @@ to the processor backend." nil) -(defmulti process-change (fn [_ change] (:type change))) +(defmulti process-change (fn [_ change _] (:type change))) (defmulti process-operation (fn [_ op] (:type op))) ;; Changes Processing Impl @@ -512,22 +520,25 @@ (defn process-changes ([data items] - (process-changes data items true)) + (process-changes data items true {})) ([data items verify?] + (process-changes data items verify? {})) + + ([data items verify? libraries] ;; When verify? false we spec the schema validation. Currently used ;; to make just 1 validation even if the changes are applied twice (when verify? (check-changes items)) (binding [*touched-changes* (volatile! #{})] - (let [result (reduce #(or (process-change %1 %2) %1) data items)] + (let [result (reduce #(or (process-change %1 %2 libraries) %1) data items)] (reduce process-touched-change result @*touched-changes*))))) ;; --- Comment Threads (defmethod process-change :set-comment-thread-position - [data {:keys [page-id comment-thread-id position frame-id]}] + [data {:keys [page-id comment-thread-id position frame-id]} _] (d/update-in-when data [:pages-index page-id] (fn [page] (if (and position frame-id) @@ -540,7 +551,7 @@ ;; --- Guides (defmethod process-change :set-guide - [data {:keys [page-id id params]}] + [data {:keys [page-id id params]} _] (if (nil? params) (d/update-in-when data [:pages-index page-id] (fn [page] @@ -556,7 +567,7 @@ ;; --- Flows (defmethod process-change :set-flow - [data {:keys [page-id id params]}] + [data {:keys [page-id id params]} _] (if (nil? params) (d/update-in-when data [:pages-index page-id] (fn [page] @@ -572,7 +583,7 @@ ;; --- Grids (defmethod process-change :set-default-grid - [data {:keys [page-id grid-type params]}] + [data {:keys [page-id grid-type params]} _] (if (nil? params) (d/update-in-when data [:pages-index page-id] (fn [page] @@ -609,7 +620,7 @@ (update state :media-refs into xform media-refs))) (defmethod process-change :add-obj - [data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}] + [data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]} _] ;; NOTE: we only perform hard validation on backend #?(:clj (validate-shape obj page-id)) @@ -644,7 +655,7 @@ objects)) (defmethod process-change :mod-obj - [data {:keys [page-id component-id] :as change}] + [data {:keys [page-id component-id] :as change} _] (if page-id (d/update-in-when data [:pages-index page-id :objects] process-operations change) (d/update-in-when data [:components component-id :objects] process-operations change))) @@ -674,19 +685,19 @@ objects)) (defmethod process-change :reorder-children - [data {:keys [page-id component-id] :as change}] + [data {:keys [page-id component-id] :as change} _] (if page-id (d/update-in-when data [:pages-index page-id :objects] process-children-reordering change) (d/update-in-when data [:components component-id :objects] process-children-reordering change))) (defmethod process-change :del-obj - [data {:keys [page-id component-id id ignore-touched]}] + [data {:keys [page-id component-id id ignore-touched]} _] (if page-id (d/update-in-when data [:pages-index page-id] ctst/delete-shape id ignore-touched) (d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched))) (defmethod process-change :fix-obj - [data {:keys [page-id component-id id] :as params}] + [data {:keys [page-id component-id id] :as params} _] (letfn [(fix-container [container] (case (:fix params :broken-children) :broken-children (ctst/fix-broken-children container id) @@ -698,7 +709,7 @@ (d/update-in-when data [:components component-id] fix-container)))) (defmethod process-change :reg-objects - [data {:keys [page-id component-id shapes]}] + [data {:keys [page-id component-id shapes]} _] ;; FIXME: Improve performance (letfn [(reg-objects [objects] (let [lookup (d/getf objects) @@ -755,7 +766,7 @@ (defmethod process-change :mov-objects ;; FIXME: ignore-touched is no longer used, so we can consider it deprecated - [data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]}] + [data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]} _] (letfn [(calculate-invalid-targets [objects shape-id] (let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))] (->> (get-in objects [shape-id :shapes]) @@ -870,7 +881,7 @@ (d/update-in-when data [:components component-id :objects] move-objects)))) (defmethod process-change :add-page - [data {:keys [id name page]}] + [data {:keys [id name page]} _] (when (and id name page) (ex/raise :type :conflict :hint "id+name or page should be provided, never both")) @@ -880,7 +891,7 @@ (ctpl/add-page data page))) (defmethod process-change :mod-page - [data {:keys [id] :as params}] + [data {:keys [id] :as params} _] (d/update-in-when data [:pages-index id] (fn [page] (let [name (get params :name) @@ -917,7 +928,7 @@ (dissoc :pixel-grid-opacity)))))) (defmethod process-change :set-plugin-data - [data {:keys [object-type object-id page-id namespace key value]}] + [data {:keys [object-type object-id page-id namespace key value]} _] (letfn [(update-fn [data] (if (some? value) (assoc-in data [:plugin-data namespace key] value) @@ -943,83 +954,83 @@ (d/update-in-when data [:components object-id] update-fn)))) (defmethod process-change :del-page - [data {:keys [id]}] + [data {:keys [id]} _] (ctpl/delete-page data id)) (defmethod process-change :mov-page - [data {:keys [id index]}] + [data {:keys [id index]} _] (update data :pages d/insert-at-index index [id])) (defmethod process-change :add-color - [data {:keys [color]}] + [data {:keys [color]} _] (ctl/add-color data color)) (defmethod process-change :mod-color - [data {:keys [color]}] + [data {:keys [color]} _] (ctl/set-color data color)) (defmethod process-change :del-color - [data {:keys [id]}] + [data {:keys [id]} _] (ctl/delete-color data id)) ;; -- Media (defmethod process-change :add-media - [data {:keys [object]}] + [data {:keys [object]} _] (update data :media assoc (:id object) object)) (defmethod process-change :mod-media - [data {:keys [object]}] + [data {:keys [object]} _] (d/update-in-when data [:media (:id object)] merge object)) (defmethod process-change :del-media - [data {:keys [id]}] + [data {:keys [id]} _] (d/update-when data :media dissoc id)) ;; -- Components (defmethod process-change :add-component - [data params] + [data params _] (ctkl/add-component data params)) (defmethod process-change :mod-component - [data params] + [data params _] (ctkl/mod-component data params)) (defmethod process-change :del-component - [data {:keys [id skip-undelete? delta]}] + [data {:keys [id skip-undelete? delta]} _] (ctf/delete-component data id skip-undelete? delta)) (defmethod process-change :restore-component - [data {:keys [id page-id]}] + [data {:keys [id page-id]} _] (ctf/restore-component data id page-id)) (defmethod process-change :purge-component - [data {:keys [id]}] + [data {:keys [id]} _] (ctf/purge-component data id)) ;; -- Typography (defmethod process-change :add-typography - [data {:keys [typography]}] + [data {:keys [typography]} _] (ctyl/add-typography data typography)) (defmethod process-change :mod-typography - [data {:keys [typography]}] + [data {:keys [typography]} _] (ctyl/update-typography data (:id typography) merge typography)) (defmethod process-change :del-typography - [data {:keys [id]}] + [data {:keys [id]} _] (ctyl/delete-typography data id)) ;; -- Design Tokens (defmethod process-change :set-tokens-lib - [data {:keys [tokens-lib]}] + [data {:keys [tokens-lib]} _] (assoc data :tokens-lib tokens-lib)) (defmethod process-change :set-token - [data {:keys [set-id token-id attrs]}] + [data {:keys [set-id token-id attrs]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib (fn [lib'] @@ -1036,7 +1047,7 @@ (ctob/make-token (merge prev-token attrs))))))))) (defmethod process-change :set-token-set - [data {:keys [id attrs]}] + [data {:keys [id attrs]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib (fn [lib'] @@ -1051,7 +1062,7 @@ (ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs)))))))) (defmethod process-change :set-token-theme - [data {:keys [id attrs]}] + [data {:keys [id attrs]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib (fn [lib'] @@ -1069,7 +1080,7 @@ (ctob/make-token-theme (merge prev-token-theme attrs))))))))) (defmethod process-change :set-tokens-status - [data {:keys [theme-ids set-ids]}] + [data {:keys [theme-ids set-ids]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-status ctos/set-tokens-status theme-ids set-ids))) @@ -1081,30 +1092,63 @@ :active-set-ids set-ids)))) (defmethod process-change :rename-token-set-group - [data {:keys [set-group-path set-group-fname]}] + [data {:keys [set-group-path set-group-fname]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib ctob/rename-set-group set-group-path set-group-fname))) (defmethod process-change :move-token-set - [data {:keys [from-path to-path before-path before-group] :as changes}] + [data {:keys [from-path to-path before-path before-group] :as changes} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib ctob/move-set from-path to-path before-path before-group))) (defmethod process-change :move-token-set-group - [data {:keys [from-path to-path before-path before-group]}] + [data {:keys [from-path to-path before-path before-group]} _] (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-lib ctob/move-set-group from-path to-path before-path before-group))) -;; === Design Tokens configuration +;; --- Design Tokens configuration (defmethod process-change :set-base-font-size - [data {:keys [base-font-size]}] + [data {:keys [base-font-size]} _] (ctf/set-base-font-size data base-font-size)) (defmethod process-change :set-tokens-source - [data {:keys [library-id]}] + [data {:keys [library-id]} _] (cfo/set-tokens-source data library-id)) +;; --- Validate Shapes + +#?(:clj + (defmethod process-change :validate-shapes + [data _ _] + data)) + +#?(:cljs + (defmethod process-change :validate-shapes + [data {:keys [page-id shape-ids context]} libraries] + (if libraries + (println "Validating shapes: \n" + " page-id:" (str page-id) "\n" + " shape-ids:" (str shape-ids) "\n" + " context:" context) + (let [file {:data data :id uuid/zero} + errors (reduce (fn [acc shape-id] + (if-let [page (ctpl/get-page data page-id)] + (let [page-errors (val/validate-shape shape-id file page libraries)] + (if (seq page-errors) + (into acc page-errors) + acc)) + acc)) + [] + shape-ids)] + (when (seq errors) + (ex/raise :type :validation + :code :referential-integrity + :hint (str "error on validating shapes: " context) + :details errors)) + data)) + data)) + ;; === Operations (def decode-shape-attrs diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 94f50fccef..17348bc952 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1254,7 +1254,6 @@ [changes] (::page-id (meta changes))) - (defn set-text-content [changes id content prev-content] (assert-page-id! changes) @@ -1275,3 +1274,12 @@ (-> changes (update :redo-changes conj redo-change) (update :undo-changes conj undo-change)))) + +;; Validate Shapes + +(defn validate-shapes + [changes page-id shape-ids context] + (update changes :redo-changes conj {:type :validate-shapes + :page-id page-id + :shape-ids (vec shape-ids) + :context context})) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index ac5a516156..1cc1a638f2 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -294,8 +294,11 @@ duplicated-parent? (->> ids-map vals (some #(= % (:parent-id first-shape)))) + grid-parent? + (and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?)) + changes - (if (and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?)) + (if grid-parent? (let [target-cell (-> position meta :cell) [row column] @@ -317,7 +320,19 @@ changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes - (rest new-shapes))] + (rest new-shapes)) + + ids-to-validate (cond-> [(:id first-shape)] + grid-parent? + (conj (:parent-id first-shape))) + + changes (if (seq ids-to-validate) + (pcb/validate-shapes changes + (:id page) + ids-to-validate + (str "generate-instantiate-component: " component-id + " under parent-id" (or parent-id " root"))) + changes)] [new-shape changes]))) @@ -3140,7 +3155,6 @@ ;; we calculate a new one because the components will have created new shapes. ids-map (into {} (map #(vector % (uuid/next))) all-ids) - ;; If there is an alt-duplication we change to root ;; For variants so the copy is made as a child of root ;; This is because inside a variant-container can't be a copy @@ -3152,7 +3166,6 @@ (assoc :parent-id uuid/zero :frame-id uuid/zero))) shapes) - changes (-> changes (pcb/with-page page) (pcb/with-objects all-objects) @@ -3182,7 +3195,20 @@ (comp (filter #(= :add-obj (:type %))) (map #(vector (:old-id %) (-> % :obj :id)))) - (:redo-changes changes))] + (:redo-changes changes)) + + copied-components + (ctn/get-all-instance-roots (:objects page) ids) + + ids-to-validate + (map #(get ids-map % %) copied-components) + + changes (if (seq ids-to-validate) + (pcb/validate-shapes changes + (:id page) + ids-to-validate + (cond-> (str "generate-duplicate-changes: " ids))) + changes)] (-> changes (generate-duplicate-flows shapes page ids-map) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 30d6067889..020f92d0a8 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -84,7 +84,7 @@ (reduce check-shape changes mod-obj-changes))) (defn generate-update-shapes - [changes ids update-fn objects {:keys [attrs changed-sub-attr changed-item-index ignore-tree ignore-touched with-objects? translation? skip-grid-reassignment?]}] + [changes ids update-fn objects {:keys [attrs changed-sub-attr changed-item-index ignore-tree ignore-touched with-objects? translation? skip-grid-reassignment? extra-context]}] (let [changes (reduce (fn [changes id] (let [opts {:attrs attrs @@ -105,7 +105,19 @@ (pcb/reorder-grid-children ids)) (not ignore-touched) - (generate-unapply-tokens objects changed-sub-attr changed-item-index))] + (generate-unapply-tokens objects changed-sub-attr changed-item-index)) + + page-id (pcb/get-page-id changes) + modified-components (ctn/get-all-instance-roots objects ids) + + changes (if (and page-id (seq modified-components)) + (pcb/validate-shapes changes + page-id + modified-components + (cond-> (str "generate-update-shapes: " ids " " attrs) + (some? extra-context) + (str " \n -> from " extra-context))) + changes)] changes)) (defn- generate-update-shape-flags @@ -257,8 +269,8 @@ page-id (pcb/get-page-id changes) page (or (pcb/get-page changes) (ctpl/get-page data page-id)) - ids (cfh/clean-loops objects ids) + in-component-copy? (fn [shape-id] ;; Look for shapes that are inside a component copy, but are @@ -267,7 +279,7 @@ ;; If we want to specifically allow altering the copies, this is ;; a special case, like a component swap, in which case we want ;; to delete the old shape - (let [shape (get objects shape-id)] + (let [shape (get objects shape-id)] (and (ctn/has-any-copy-parent? objects shape) (not allow-altering-copies)))) @@ -446,7 +458,19 @@ (into [] (remove #(and (ctsi/has-destination %) (id-to-delete? (:destination %)))) - interactions))))))] + interactions)))))) + + modified-components (ctn/get-all-instance-roots objects (disj all-parents uuid/zero)) + ;; There is no need to validate deleted objects. Probably also no need to validate hidden or unmasked objects, + ;; but we may think of it + + changes (if (seq modified-components) + (pcb/validate-shapes changes + page-id + modified-components + (str "generate-delete-shapes: " ids)) + changes)] + [all-parents changes]))) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index cc7ea9bd07..ffbaaef905 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -216,6 +216,62 @@ :else (get-instance-root objects (get objects (:parent-id shape))))) +(defn get-all-instance-roots + "Given a list of shape ids and an objects tree, returns a set with the + ids of the instance roots related to each of the shapes (see + ctk/instance-root?). + + For each shape, first look up: if the shape itself or any of its + ancestors is an instance root, only that root is added. If not, look + down the descendants of the shape with a depth-first search: each + instance root found is added, and its own subtree is skipped, while + the search continues with the other descendants." + [objects shape-ids] + (let [visited (volatile! #{}) + result (volatile! #{})] + (letfn [(search-up + ;; Returns true if the shape is already covered: it has an + ;; instance root at or above it (that is added to the + ;; result), or it was visited by a previous search. A shape + ;; is marked as visited only after a root has been found at + ;; or above it, so a failed search never blocks the + ;; downward search of the shapes below. + [shape-id] + (when-not (contains? @visited shape-id) + (let [shape (get objects shape-id)] + (cond + (nil? shape) + false + + (ctk/instance-root? shape) + (do (vswap! visited conj shape-id) + (vswap! result conj shape-id) + true) + + (cfh/root? shape) + false + + :else + (when (search-up (:parent-id shape)) + (vswap! visited conj shape-id) + true))))) + + (search-down + [shape-id] + (when-not (contains? @visited shape-id) + (vswap! visited conj shape-id) + (let [shape (get objects shape-id)] + (when-not (nil? shape) + (if (ctk/instance-root? shape) + (vswap! result conj shape-id) + (run! search-down (:shapes shape)))))))] + + (doseq [shape-id shape-ids] + (when-not (search-up shape-id) + (search-down shape-id))) + + @result))) + (defn find-component-main "If the shape is a component main instance or is inside one, return that instance. Uses an iterative loop with cycle detection to prevent stack overflow on circular diff --git a/common/test/common_tests/types/container_test.cljc b/common/test/common_tests/types/container_test.cljc index e00b1fb9c6..adefe438f1 100644 --- a/common/test/common_tests/types/container_test.cljc +++ b/common/test/common_tests/types/container_test.cljc @@ -154,3 +154,200 @@ shape-b (make-shape id-b :parent-id id-a) objects (objects-map shape-a shape-b)] (t/is (nil? (ctc/find-component-main objects shape-a false)))))) + +;; --------------------------------------------------------------------------- +;; Tests – get-all-instance-roots +;; --------------------------------------------------------------------------- + +(defn- make-tree + "Build an objects map from a seq of [id parent-id attrs] triples. The + :shapes of each parent are filled from the triples, in order. A nil + parent-id means that the shape has no parent in the map." + [specs] + (let [children (reduce (fn [acc [id parent-id]] + (cond-> acc + (some? parent-id) + (update parent-id (fnil conj []) id))) + {} + specs)] + (->> specs + (map (fn [[id parent-id attrs]] + (apply make-shape id + :parent-id parent-id + :shapes (get children id []) + (mapcat identity attrs)))) + (apply objects-map)))) + +(def ^:private root-attrs {:component-root true + :component-id (uuid/next)}) + +(t/deftest get-all-instance-roots-empty-and-missing + (t/testing "no ids returns an empty set" + (t/is (= #{} (ctc/get-all-instance-roots {} [])))) + + (t/testing "ids that are not in objects return an empty set" + (t/is (= #{} (ctc/get-all-instance-roots {} [(uuid/next)]))))) + +(t/deftest get-all-instance-roots-no-roots + (t/testing "returns an empty set when there is no instance root above or below" + (let [[a b c] (repeatedly 3 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b]])] + (doseq [id [a b c]] + (t/is (= #{} (ctc/get-all-instance-roots objects [id]))))))) + +(t/deftest get-all-instance-roots-self-is-root + (t/testing "a shape that is an instance root returns itself" + (let [[a] (repeatedly 1 uuid/next) + objects (make-tree [[a root-id root-attrs]])] + (t/is (= #{a} (ctc/get-all-instance-roots objects [a]))))) + + (t/testing "does not look down when the shape itself is an instance root" + (let [[a b] (repeatedly 2 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a root-attrs]])] + (t/is (= #{a} (ctc/get-all-instance-roots objects [a])))))) + +(t/deftest get-all-instance-roots-ancestor-is-root + (t/testing "finds the instance root among the ancestors" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a] + [c b] + [d c]])] + (doseq [id [b c d]] + (t/is (= #{a} (ctc/get-all-instance-roots objects [id])))))) + + (t/testing "returns the nearest ancestor root when there are several" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a root-attrs] + [c b] + [d c]])] + (t/is (= #{b} (ctc/get-all-instance-roots objects [d]))))) + + (t/testing "does not look down when an ancestor is an instance root" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a] + [c b root-attrs] + [d b root-attrs]])] + (t/is (= #{a} (ctc/get-all-instance-roots objects [b])))))) + +(t/deftest get-all-instance-roots-descendant-roots + (t/testing "finds an instance root below a shape without roots above" + (let [[a b c] (repeatedly 3 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b root-attrs]])] + (t/is (= #{c} (ctc/get-all-instance-roots objects [a]))) + (t/is (= #{c} (ctc/get-all-instance-roots objects [b]))))) + + (t/testing "finds a root that is a direct child of the starting shape" + (let [[a b] (repeatedly 2 uuid/next) + objects (make-tree [[a root-id] + [b a root-attrs]])] + (t/is (= #{b} (ctc/get-all-instance-roots objects [a]))))) + + (t/testing "finds roots in several branches" + (let [[a b c d e f] (repeatedly 6 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b root-attrs] + [d a root-attrs] + [e a] + [f e root-attrs]])] + (t/is (= #{c d f} (ctc/get-all-instance-roots objects [a]))))) + + (t/testing "skips the subtree of a found root and continues with the siblings" + (let [[a b c d e f] (repeatedly 6 uuid/next) + objects (make-tree [[a root-id] + [b a root-attrs] + [c b root-attrs] ; nested in b, must be skipped + [d c root-attrs] ; nested in c, must be skipped + [e a] + [f e root-attrs]])] + (t/is (= #{b f} (ctc/get-all-instance-roots objects [a]))))) + + (t/testing "skips a root nested in a non-root shape inside another root" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id] + [b a root-attrs] + [c b] + [d c root-attrs]])] + (t/is (= #{b} (ctc/get-all-instance-roots objects [a])))))) + +(t/deftest get-all-instance-roots-page-root + (t/testing "searching from the page root finds the top level instance roots" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (assoc (make-tree [[a root-id root-attrs] + [b a root-attrs] + [c root-id] + [d c root-attrs]]) + root-id + (assoc (root-shape) :shapes [a c]))] + (t/is (= #{a d} (ctc/get-all-instance-roots objects [root-id])))))) + +(t/deftest get-all-instance-roots-several-ids + (t/testing "returns the union of the roots of every id" + (let [[a b c d e] (repeatedly 5 uuid/next) + objects (make-tree [[a root-id root-attrs] ; root with up search + [b a] + [c root-id] ; no root above + [d c root-attrs] ; found down from c + [e root-id]])] ; nothing + (t/is (= #{a d} (ctc/get-all-instance-roots objects [b c e]))))) + + (t/testing "ids that overlap in the same tree do not duplicate or lose roots" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b root-attrs] + [d a root-attrs]])] + (t/is (= #{c d} (ctc/get-all-instance-roots objects [a b c d]))) + (t/is (= #{c d} (ctc/get-all-instance-roots objects [b a d c]))) + (t/is (= #{c d} (ctc/get-all-instance-roots objects [a a]))))) + + (t/testing "a shape below an already searched shape is not searched again" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b root-attrs] + [d a root-attrs]])] + (t/is (= #{c d} (ctc/get-all-instance-roots objects [a b]))) + (t/is (= #{c d} (ctc/get-all-instance-roots objects [b a]))))) + + (t/testing "a child of a shape with a root above does not search down" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a] + [c b root-attrs] + [d b]])] + (t/is (= #{a} (ctc/get-all-instance-roots objects [b d]))) + (t/is (= #{a} (ctc/get-all-instance-roots objects [d b]))))) + + (t/testing "siblings without a root above are both searched down" + (let [[a b c d e] (repeatedly 5 uuid/next) + objects (make-tree [[a root-id] + [b a] + [c b root-attrs] + [d a] + [e d root-attrs]])] + (t/is (= #{c e} (ctc/get-all-instance-roots objects [b d]))) + (t/is (= #{c e} (ctc/get-all-instance-roots objects [c e]))))) + + (t/testing "a shape inside a root and a shape outside any root" + (let [[a b c d] (repeatedly 4 uuid/next) + objects (make-tree [[a root-id root-attrs] + [b a] + [c root-id] + [d c]])] + (t/is (= #{a} (ctc/get-all-instance-roots objects [b d])))))) + +(t/deftest get-all-instance-roots-cycles + (t/testing "does not loop on circular children references" + (let [[a b] (repeatedly 2 uuid/next) + objects (objects-map (make-shape a :shapes [b]) + (make-shape b :shapes [a]))] + (t/is (= #{} (ctc/get-all-instance-roots objects [a])))))) diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index ee8eaa33f3..dace738d9d 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -105,12 +105,14 @@ (wasm.api/process-objects shapes) (wasm.api/request-render "sync-wasm-structural-changes")))))) -(defn- apply-changes-localy +(defn- apply-changes-locally [{:keys [file-id redo-changes ignore-wasm?] :as commit} pending] - (ptk/reify ::apply-changes-localy + (ptk/reify ::apply-changes-locally ptk/UpdateEvent (update [_ state] - (let [undo-changes + (let [libraries (dsh/lookup-libraries state) + + undo-changes (if pending (->> pending (map :undo-changes) @@ -128,8 +130,8 @@ apply-changes (fn [fdata] - (let [fdata (cpc/process-changes fdata undo-changes false) - fdata (cpc/process-changes fdata redo-changes false) + (let [fdata (cpc/process-changes fdata undo-changes false libraries) + fdata (cpc/process-changes fdata redo-changes false libraries) pids (into #{} xf:map-page-id redo-changes)] (reduce #(ctst/update-object-indices %1 %2) fdata pids)))] @@ -202,7 +204,7 @@ (let [pending (when-not local? (get-pending-commits state))] (rx/concat - (rx/of (apply-changes-localy commit pending)) + (rx/of (apply-changes-locally commit pending)) (if pending (rx/concat (->> (rx/from (reverse pending)) diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index df614dfe7a..dee6bb19e3 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -193,7 +193,8 @@ layout-initializer (get-layout-initializer type from-frame? calculate-params?)] (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/update-shapes [id] layout-initializer {:with-objects? true}) + (dwsh/update-shapes [id] layout-initializer {:with-objects? true + :extra-context (str "create-layout-from-id: " id)}) (dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v)) (ptk/data-event :layout/update {:ids [id]}) (dwu/commit-undo-transaction undo-id)))))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 587956c084..b11adf55cc 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -211,7 +211,7 @@ {:as props :keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects? changed-sub-attr changed-item-index translation? - skip-grid-reassignment? skip-component-sync? resize-ids] + skip-grid-reassignment? skip-component-sync? resize-ids extra-context] :or {reg-objects? false save-undo? true stack-undo? false @@ -251,7 +251,8 @@ :ignore-touched ignore-touched :with-objects? with-objects? :translation? translation? - :skip-grid-reassignment? skip-grid-reassignment?}) + :skip-grid-reassignment? skip-grid-reassignment? + :extra-context extra-context}) (cond-> undo-group (pcb/set-undo-group undo-group)) (pcb/set-translation? translation?)