diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index d0b911c06..6c96daa18 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -328,6 +328,9 @@ The list of objects are returned in tree traversal order, respecting the order of the children of each parent." + ([object parent-id objects] + (clone-object object parent-id objects (fn [object _] object) (fn [object _] object) nil false)) + ([object parent-id objects update-new-object] (clone-object object parent-id objects update-new-object (fn [object _] object) nil false)) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index a1162c388..311dc2b70 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -12,10 +12,14 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] + [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [beicon.core :as rx] @@ -140,7 +144,7 @@ (mapv #(get objects %))) parent-id (cph/get-parent-id objects (:id frame)) idx-in-parent (->> (:id frame) - (cph/get-position-on-parent objects) + (cph/get-position-on-parent objects) inc)] (-> (pcb/empty-changes it page-id) @@ -150,6 +154,46 @@ (pcb/change-parent parent-id children idx-in-parent) (pcb/remove-objects [(:id frame)])))) + +(defn- clone-component-shapes-changes + [changes shape objects] + (let [shape-parent-id (:parent-id shape) + new-shape-id (uuid/next) + [_ new-shapes _] + (ctst/clone-object shape + shape-parent-id + objects + (fn [object _] + (cond-> object + (= new-shape-id (:parent-id object)) + (assoc :parent-id shape-parent-id))) + (fn [object _] object) + new-shape-id + false) + + new-shapes (->> new-shapes + (filter #(not= (:id %) new-shape-id)))] + (reduce + (fn [changes shape] + (pcb/add-object changes shape)) + changes + new-shapes))) + +(defn remove-component-changes + [it page-id shape objects file-data file] + (let [page (ctpl/get-page file-data page-id) + components-v2 (dm/get-in file-data [:options :components-v2]) + ;; In order to ungroup a component, we first make a clone of its shapes, + ;; and then we delete it + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/with-library-data file-data) + (pcb/with-page page) + (clone-component-shapes-changes shape objects) + (dwsh/delete-shapes-changes file page objects [(:id shape)] it components-v2))] + ;; TODO: Should we call detach-comment-thread ? + changes)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GROUPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -173,13 +217,18 @@ (ptk/reify ::ungroup-selected ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + file-data (get state :workspace-data) + file (wsh/get-local-file state) prepare (fn [shape-id] (let [shape (get objects shape-id)] (cond + (ctk/main-instance? shape) + (remove-component-changes it page-id shape objects file-data file) + (or (cph/group-shape? shape) (cph/bool-shape? shape)) (remove-group-changes it page-id shape objects) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index f40393f4b..4414fc650 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -214,126 +214,140 @@ (real-delete-shapes file page objects ids-to-delete it components-v2) (rx/of (dwu/commit-undo-transaction undo-id)))))))) -(defn- real-delete-shapes - [file page objects ids it components-v2] - (let [lookup (d/getf objects) - - groups-to-unmask - (reduce (fn [group-ids id] +(defn- real-delete-shapes-changes + ([file page objects ids it components-v2] + (let [changes (-> (pcb/empty-changes it (:id page)) + (pcb/with-page page) + (pcb/with-objects objects) + (pcb/with-library-data file))] + (real-delete-shapes-changes changes file page objects ids it components-v2))) + ([changes file page objects ids _it components-v2] + (let [lookup (d/getf objects) + groups-to-unmask + (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, ;; the mask condition must be removed, and it must be ;; converted to a normal group. - (let [obj (lookup id) - parent (lookup (:parent-id obj))] - (if (and (:masked-group? parent) - (= id (first (:shapes parent)))) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids) + (let [obj (lookup id) + parent (lookup (:parent-id obj))] + (if (and (:masked-group? parent) + (= id (first (:shapes parent)))) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids) - interacting-shapes - (filter (fn [shape] + interacting-shapes + (filter (fn [shape] ;; If any of the deleted shapes is the destination of ;; some interaction, this must be deleted, too. - (let [interactions (:interactions shape)] - (some #(and (ctsi/has-destination %) - (contains? ids (:destination %))) - interactions))) - (vals objects)) + (let [interactions (:interactions shape)] + (some #(and (ctsi/has-destination %) + (contains? ids (:destination %))) + interactions))) + (vals objects)) ;; If any of the deleted shapes is a frame with guides - guides (into {} - (comp (map second) - (remove #(contains? ids (:frame-id %))) - (map (juxt :id identity))) - (dm/get-in page [:options :guides])) + guides (into {} + (comp (map second) + (remove #(contains? ids (:frame-id %))) + (map (juxt :id identity))) + (dm/get-in page [:options :guides])) - starting-flows - (filter (fn [flow] + starting-flows + (filter (fn [flow] ;; If any of the deleted is a frame that starts a flow, ;; this must be deleted, too. - (contains? ids (:starting-frame flow))) - (-> page :options :flows)) + (contains? ids (:starting-frame flow))) + (-> page :options :flows)) - all-parents - (reduce (fn [res id] + all-parents + (reduce (fn [res id] ;; All parents of any deleted shape must be resized. - (into res (cph/get-parent-ids objects id))) - (d/ordered-set) - ids) + (into res (cph/get-parent-ids objects id))) + (d/ordered-set) + ids) - all-children - (->> ids ;; Children of deleted shapes must be also deleted. - (reduce (fn [res id] - (into res (cph/get-children-ids objects id))) - []) - (reverse) - (into (d/ordered-set))) + all-children + (->> ids ;; Children of deleted shapes must be also deleted. + (reduce (fn [res id] + (into res (cph/get-children-ids objects id))) + []) + (reverse) + (into (d/ordered-set))) - find-all-empty-parents - (fn recursive-find-empty-parents [empty-parents] - (let [all-ids (into empty-parents ids) - contains? (partial contains? all-ids) - xform (comp (map lookup) - (filter cph/group-shape?) - (remove #(->> (:shapes %) (remove contains?) seq)) - (map :id)) - parents (into #{} xform all-parents)] - (if (= empty-parents parents) - empty-parents - (recursive-find-empty-parents parents)))) + find-all-empty-parents + (fn recursive-find-empty-parents [empty-parents] + (let [all-ids (into empty-parents ids) + contains? (partial contains? all-ids) + xform (comp (map lookup) + (filter cph/group-shape?) + (remove #(->> (:shapes %) (remove contains?) seq)) + (map :id)) + parents (into #{} xform all-parents)] + (if (= empty-parents parents) + empty-parents + (recursive-find-empty-parents parents)))) - empty-parents + empty-parents ;; Any parent whose children are all deleted, must be deleted too. - (into (d/ordered-set) (find-all-empty-parents #{})) + (into (d/ordered-set) (find-all-empty-parents #{})) - components-to-delete - (if components-v2 - (reduce (fn [components id] - (let [shape (get objects id)] - (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file - (:main-instance? shape)) ;; but check anyway - (conj components (:component-id shape)) - components))) - [] - (into ids all-children)) - []) + components-to-delete + (if components-v2 + (reduce (fn [components id] + (let [shape (get objects id)] + (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file + (:main-instance? shape)) ;; but check anyway + (conj components (:component-id shape)) + components))) + [] + (into ids all-children)) + []) - changes (-> (pcb/empty-changes it (:id page)) - (pcb/with-page page) - (pcb/with-objects objects) - (pcb/with-library-data file) - (pcb/set-page-option :guides guides)) + changes (-> changes + (pcb/set-page-option :guides guides)) - changes (reduce (fn [changes component-id] + changes (reduce (fn [changes component-id] ;; It's important to delete the component before the main instance, because we ;; need to store the instance position if we want to restore it later. - (pcb/delete-component changes component-id)) - changes - components-to-delete) + (pcb/delete-component changes component-id)) + changes + components-to-delete) - changes (-> changes - (pcb/remove-objects all-children {:ignore-touched true}) - (pcb/remove-objects ids) - (pcb/remove-objects empty-parents) - (pcb/resize-parents all-parents) - (pcb/update-shapes groups-to-unmask - (fn [shape] - (assoc shape :masked-group? false))) - (pcb/update-shapes (map :id interacting-shapes) - (fn [shape] - (d/update-when shape :interactions - (fn [interactions] - (into [] - (remove #(and (ctsi/has-destination %) - (contains? ids (:destination %)))) - interactions))))) - (cond-> (seq starting-flows) - (pcb/update-page-option :flows (fn [flows] - (->> (map :id starting-flows) - (reduce ctp/remove-flow flows)))))) - undo-id (js/Symbol)] + changes (-> changes + (pcb/remove-objects all-children {:ignore-touched true}) + (pcb/remove-objects ids) + (pcb/remove-objects empty-parents) + (pcb/resize-parents all-parents) + (pcb/update-shapes groups-to-unmask + (fn [shape] + (assoc shape :masked-group? false))) + (pcb/update-shapes (map :id interacting-shapes) + (fn [shape] + (d/update-when shape :interactions + (fn [interactions] + (into [] + (remove #(and (ctsi/has-destination %) + (contains? ids (:destination %)))) + interactions))))) + (cond-> (seq starting-flows) + (pcb/update-page-option :flows (fn [flows] + (->> (map :id starting-flows) + (reduce ctp/remove-flow flows))))))] + [changes all-parents]))) + + +(defn delete-shapes-changes + [changes file page objects ids it components-v2] + (let [[changes _all-parents] (real-delete-shapes-changes changes file page objects ids it components-v2)] + changes)) + + +(defn- real-delete-shapes + [file page objects ids it components-v2] + (let [[changes all-parents] (real-delete-shapes-changes file page objects ids it components-v2) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dc/detach-comment-thread ids) (dch/commit-changes changes) @@ -467,7 +481,7 @@ ;; We have change only the hidden behaviour, to hide only the ;; selected shape, block behaviour remains the same. ids (if (boolean? blocked) - (into ids (->> ids (mapcat #(cph/get-children-ids objects %)))) + (into ids (->> ids (mapcat #(cph/get-children-ids objects %)))) ids)] (rx/of (dch/update-shapes ids update-fn))))))