diff --git a/backend/tests/app/tests/test_common_pages.clj b/backend/tests/app/tests/test_common_pages.clj index b8a220d484..f6b84cc674 100644 --- a/backend/tests/app/tests/test_common_pages.clj +++ b/backend/tests/app/tests/test_common_pages.clj @@ -360,7 +360,7 @@ (t/is (= [rect-a-id rect-e-id rect-d-id] (get-in objects [group-b-id :shapes])))))) - (t/testing "Move elements and delete the empty group" + (t/testing "Move all elements from a group" (let [changes [{:type :mov-objects :page-id page-id :parent-id group-a-id @@ -368,9 +368,9 @@ res (cp/process-changes data changes)] (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id rect-e-id] + (t/is (= [group-a-id group-b-id rect-e-id] (get-in objects [frame-a-id :shapes]))) - (t/is (nil? (get-in objects [group-b-id])))))) + (t/is (empty? (get-in objects [group-b-id :shapes])))))) (t/testing "Move elements to a group with different frame" (let [changes [{:type :mov-objects @@ -727,11 +727,11 @@ ;; After - (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id] + (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id] (get-in res [:pages-index page-id :objects cp/root :shapes]))) - (t/is (= nil - (get-in res [:pages-index page-id :objects group-1-id]))) + (t/is (not= nil + (get-in res [:pages-index page-id :objects group-1-id]))) )) diff --git a/common/app/common/pages/changes.cljc b/common/app/common/pages/changes.cljc index 681c40a26c..3ff82791ea 100644 --- a/common/app/common/pages/changes.cljc +++ b/common/app/common/pages/changes.cljc @@ -36,8 +36,20 @@ (when verify? (us/verify ::spec/changes items)) - (->> items - (reduce #(or (process-change %1 %2) %1) data)))) + (let [pages (into #{} (map :page-id) items) + result (->> items + (reduce #(or (process-change %1 %2) %1) data))] + + ;; Validate result shapes (only on the backend) + #?(:clj + (doseq [page-id pages] + (let [page (get-in result [:pages-index page-id])] + (doseq [[id shape] (:objects page)] + (if-not (= shape (get-in data [:pages-index page-id :objects id])) + ;; If object has change verify is correct + (us/verify ::spec/shape shape)))))) + + result))) (defmethod process-change :set-option [data {:keys [page-id option value]}] @@ -94,7 +106,6 @@ (let [update-fn (fn [objects] (if-let [obj (get objects id)] (let [result (reduce process-operation obj operations)] - #?(:clj (us/verify ::spec/shape result)) (assoc objects id result)) objects))] (if page-id @@ -142,16 +153,25 @@ (map :id) (distinct)) shapes))) + (set-mask-selrect [group children] + (let [mask (first children)] + (-> group + (merge (select-keys mask [:selrect :points])) + (assoc :x (-> mask :selrect :x) + :y (-> mask :selrect :y) + :width (-> mask :selrect :width) + :height (-> mask :selrect :height))))) (update-group [group objects] (let [children (->> group :shapes (map #(get objects %)))] - (if (:masked-group? group) - (let [mask (first children)] - (-> group - (merge (select-keys mask [:selrect :points])) - (assoc :x (-> mask :selrect :x) - :y (-> mask :selrect :y) - :width (-> mask :selrect :width) - :height (-> mask :selrect :height)))) + (cond + ;; If the group is empty we don't make any changes. Should be removed by a later process + (empty? children) + group + + (:masked-group? group) + (set-mask-selrect group children) + + :else (gsh/update-group-selrect group children))))] (if page-id @@ -206,23 +226,17 @@ pid prev-parent-id objects objects] (let [obj (get objects pid)] - (if (and (= 1 (count (:shapes obj))) - (= sid (first (:shapes obj))) - (= :group (:type obj))) - (recur pid - (:parent-id obj) - (dissoc objects pid)) - (cond-> objects - true - (update-in [pid :shapes] strip-id sid) + (cond-> objects + true + (update-in [pid :shapes] strip-id sid) - (and (:shape-ref obj) - (= (:type obj) :group) - (not ignore-touched)) - (-> - (update-in [pid :touched] - cph/set-touched-group :shapes-group) - (d/dissoc-in [pid :remote-synced?]))))))))) + (and (:shape-ref obj) + (= (:type obj) :group) + (not ignore-touched)) + (-> + (update-in [pid :touched] + cph/set-touched-group :shapes-group) + (d/dissoc-in [pid :remote-synced?])))))))) (update-parent-id [objects id] (assoc-in objects [id :parent-id] parent-id)) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 8ab42f5326..f76b8974ce 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -808,6 +808,168 @@ ;; --- Change Shape Order (D&D Ordering) +(defn relocate-shapes-changes [objects parents parent-id page-id to-index ids groups-to-delete groups-to-unmask shapes-to-detach shapes-to-reroot shapes-to-deroot] + (let [;; Changes to the shapes that are being move + r-mov-change + [{:type :mov-objects + :parent-id parent-id + :page-id page-id + :index to-index + :shapes (vec (reverse ids))}] + + u-mov-change + (map (fn [id] + (let [obj (get objects id)] + {:type :mov-objects + :parent-id (:parent-id obj) + :page-id page-id + :index (cp/position-on-parent id objects) + :shapes [id]})) + (reverse ids)) + + ;; Changes deleting empty groups + r-del-change + (map (fn [group-id] + {:type :del-obj + :page-id page-id + :id group-id}) + groups-to-delete) + + u-del-change + (d/concat + [] + ;; Create the groups + (map (fn [group-id] + (let [group (get objects group-id)] + {:type :add-obj + :page-id page-id + :parent-id parent-id + :frame-id (:frame-id group) + :id group-id + :obj (-> group + (assoc :shapes []))})) + groups-to-delete) + ;; Creates the hierarchy + (map (fn [group-id] + (let [group (get objects group-id)] + {:type :mov-objects + :page-id page-id + :parent-id (:id group) + :shapes (:shapes group)})) + groups-to-delete)) + + ;; Changes removing the masks from the groups without mask shape + r-mask-change + (map (fn [group-id] + {:type :mod-obj + :page-id page-id + :id group-id + :operations [{:type :set + :attr :masked-group? + :val false}]}) + groups-to-unmask) + + u-mask-change + (map (fn [group-id] + (let [group (get objects group-id)] + {:type :mod-obj + :page-id page-id + :id group-id + :operations [{:type :set + :attr :masked-group? + :val (:masked-group? group)}]})) + groups-to-unmask) + + ;; Changes to the components metadata + + detach-keys [:component-id :component-file :component-root? :remote-synced? :shape-ref :touched] + + r-detach-change + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations (mapv #(hash-map :type :set :attr % :val nil) detach-keys)}) + shapes-to-detach) + + u-detach-change + (map (fn [id] + (let [obj (get objects id)] + {:type :mod-obj + :page-id page-id + :id id + :operations (mapv #(hash-map :type :set :attr % :val (get obj %)) detach-keys)})) + shapes-to-detach) + + r-deroot-change + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :component-root? + :val nil}]}) + shapes-to-deroot) + + u-deroot-change + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :component-root? + :val true}]}) + shapes-to-deroot) + + r-reroot-change + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :component-root? + :val true}]}) + shapes-to-reroot) + + u-reroot-change + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :component-root? + :val nil}]}) + shapes-to-reroot) + + r-reg-change + [{:type :reg-objects + :page-id page-id + :shapes (vec parents)}] + + u-reg-change + [{:type :reg-objects + :page-id page-id + :shapes (vec parents)}] + + rchanges (d/concat [] + r-mov-change + r-del-change + r-mask-change + r-detach-change + r-deroot-change + r-reroot-change + r-reg-change) + + uchanges (d/concat [] + u-del-change + u-reroot-change + u-deroot-change + u-detach-change + u-mask-change + u-mov-change + u-reg-change)] + [rchanges uchanges])) + (defn relocate-shapes [ids parent-id to-index] (us/verify (s/coll-of ::us/uuid) ids) @@ -826,13 +988,37 @@ ;; If we try to move a parent into a child we remove it ids (filter #(not (cp/is-parent? objects parent-id %)) ids) - parents (loop [res #{parent-id} - ids (seq ids)] - (if (nil? ids) - (vec res) - (recur - (conj res (cp/get-parent (first ids) objects)) - (next ids)))) + parents (reduce (fn [result id] + (conj result (cp/get-parent id objects))) + #{parent-id} ids) + + groups-to-delete + (loop [current-id (first parents) + to-check (rest parents) + removed-id? (set ids) + result #{}] + + (if-not current-id + ;; Base case, no next element + result + + (let [group (get objects current-id)] + (if (and (not= uuid/zero current-id) + (not= current-id parent-id) + (empty? (remove removed-id? (:shapes group)))) + + ;; Adds group to the remove and check its parent + (let [to-check (d/concat [] to-check [(cp/get-parent current-id objects)]) ] + (recur (first to-check) + (rest to-check) + (conj removed-id? current-id) + (conj result current-id))) + + ;; otherwise recur + (recur (first to-check) + (rest to-check) + removed-id? + result))))) groups-to-unmask (reduce (fn [group-ids id] @@ -849,6 +1035,10 @@ #{} ids) + ;; Sets the correct components metadata for the moved shapes + ;; `shapes-to-detach` Detach from a component instance a shape that was inside a component and is moved outside + ;; `shapes-to-deroot` Removes the root flag from a component instance moved inside another component + ;; `shapes-to-reroot` Adds a root flag when a nested component instance is moved outside [shapes-to-detach shapes-to-deroot shapes-to-reroot] (reduce (fn [[shapes-to-detach shapes-to-deroot shapes-to-reroot] id] (let [shape (get objects id) @@ -876,131 +1066,18 @@ [[] [] []] ids) - rchanges (d/concat - [{:type :mov-objects - :parent-id parent-id - :page-id page-id - :index to-index - :shapes (vec (reverse ids))} - {:type :reg-objects - :page-id page-id - :shapes parents}] - (map (fn [group-id] - {:type :mod-obj - :page-id page-id - :id group-id - :operations [{:type :set - :attr :masked-group? - :val false}]}) - groups-to-unmask) - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-id - :val nil} - {:type :set - :attr :component-file - :val nil} - {:type :set - :attr :component-root? - :val nil} - {:type :set - :attr :remote-synced? - :val nil} - {:type :set - :attr :shape-ref - :val nil} - {:type :set - :attr :touched - :val nil}]}) - shapes-to-detach) - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-root? - :val nil}]}) - shapes-to-deroot) - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-root? - :val true}]}) - shapes-to-reroot)) - - uchanges (d/concat - (reduce (fn [res id] - (let [obj (get objects id)] - (conj res - {:type :mov-objects - :parent-id (:parent-id obj) - :page-id page-id - :index (cp/position-on-parent id objects) - :shapes [id]}))) - [] (reverse ids)) - [{:type :reg-objects - :page-id page-id - :shapes parents}] - (map (fn [group-id] - {:type :mod-obj - :page-id page-id - :id group-id - :operations [{:type :set - :attr :masked-group? - :val true}]}) - groups-to-unmask) - (map (fn [id] - (let [obj (get objects id)] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-id - :val (:component-id obj)} - {:type :set - :attr :component-file - :val (:component-file obj)} - {:type :set - :attr :component-root? - :val (:component-root? obj)} - {:type :set - :attr :remote-synced? - :val (:remote-synced? obj)} - {:type :set - :attr :shape-ref - :val (:shape-ref obj)} - {:type :set - :attr :touched - :val (:touched obj)}]})) - shapes-to-detach) - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-root? - :val true}]}) - shapes-to-deroot) - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :component-root? - :val nil}]}) - shapes-to-reroot))] - - ;; (println "================ rchanges") - ;; (cljs.pprint/pprint rchanges) - ;; (println "================ uchanges") - ;; (cljs.pprint/pprint uchanges) - (rx/of (dwc/commit-changes rchanges uchanges - {:commit-local? true}) + [rchanges uchanges] (relocate-shapes-changes objects + parents + parent-id + page-id + to-index + ids + groups-to-delete + groups-to-unmask + shapes-to-detach + shapes-to-reroot + shapes-to-deroot)] + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (dwc/expand-collapse parent-id)))))) (defn relocate-selected-shapes diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index d77c9ce8c0..b4b8a128fb 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -198,7 +198,7 @@ (defn retrieve-used-names [objects] - (into #{} (map :name) (vals objects))) + (into #{} (comp (map :name) (remove nil?)) (vals objects))) (defn generate-unique-name