From 831839080fa934a04cd957df9e32808014df3598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 17 Nov 2022 15:31:25 +0100 Subject: [PATCH] :tada: When deleting a shape inside a component copy, just hide it --- common/src/app/common/types/component.cljc | 11 + frontend/src/app/main/data/workspace.cljs | 63 +--- .../src/app/main/data/workspace/shapes.cljs | 348 +++++++++++------- 3 files changed, 240 insertions(+), 182 deletions(-) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 6d20c9ef3..d84befc44 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -23,7 +23,13 @@ (or (= (:shape-ref shape-inst) (:id shape-main)) (= (:shape-ref shape-inst) (:shape-ref shape-main))))) +(defn main-instance? + "Check if this shape is the root of the main instance of some component." + [shape] + (some? (:main-instance? shape))) + (defn is-main-instance? + "Check if this shape is the root of the main instance of the given component." [shape-id page-id component] (and (= shape-id (:main-instance-id component)) (= page-id (:main-instance-page component)))) @@ -38,3 +44,8 @@ (and (some? (:component-id shape)) (= (:component-file shape) library-id))) +(defn in-component-instance? + "Check if the shape is inside a component instance." + [shape] + (some? (:shape-ref shape))) + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 4fb86995d..e927d1ec4 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -842,63 +842,6 @@ (rx/of (dch/update-shapes selected #(assoc % :proportion-lock true))) (rx/of (dch/update-shapes selected #(update % :proportion-lock not)))))))) -;; --- Update Shape Flags - -(defn update-shape-flags - [ids {:keys [blocked hidden] :as flags}] - (us/verify (s/coll-of ::us/uuid) ids) - (us/assert ::shape-attrs flags) - (ptk/reify ::update-shape-flags - ptk/WatchEvent - (watch [_ state _] - (let [update-fn - (fn [obj] - (cond-> obj - (boolean? blocked) (assoc :blocked blocked) - (boolean? hidden) (assoc :hidden hidden))) - objects (wsh/lookup-page-objects state) - ids (into ids (->> ids (mapcat #(cph/get-children-ids objects %))))] - (rx/of (dch/update-shapes ids update-fn)))))) - -(defn toggle-visibility-selected - [] - (ptk/reify ::toggle-visibility-selected - ptk/WatchEvent - (watch [_ state _] - (let [selected (wsh/lookup-selected state)] - (rx/of (dch/update-shapes selected #(update % :hidden not))))))) - -(defn toggle-lock-selected - [] - (ptk/reify ::toggle-lock-selected - ptk/WatchEvent - (watch [_ state _] - (let [selected (wsh/lookup-selected state)] - (rx/of (dch/update-shapes selected #(update % :blocked not))))))) - -(defn toggle-file-thumbnail-selected - [] - (ptk/reify ::toggle-file-thumbnail-selected - ptk/WatchEvent - (watch [_ state _] - (let [selected (wsh/lookup-selected state) - pages (-> state :workspace-data :pages-index vals) - get-frames (fn [{:keys [objects id] :as page}] - (->> (ctst/get-frames objects) - (sequence - (comp (filter :use-for-thumbnail?) - (map :id) - (remove selected) - (map (partial vector id))))))] - - (rx/concat - (rx/from - (->> (mapcat get-frames pages) - (d/group-by first second) - (map (fn [[page-id frame-ids]] - (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id}))))) - (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not)))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Navigation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1755,6 +1698,12 @@ (dm/export dwh/highlight-shape) (dm/export dwh/dehighlight-shape) +;; Shape flags +(dm/export dwsh/update-shape-flags) +(dm/export dwsh/toggle-visibility-selected) +(dm/export dwsh/toggle-lock-selected) +(dm/export dwsh/toggle-file-thumbnail-selected) + ;; Groups (dm/export dwg/mask-group) (dm/export dwg/unmask-group) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 93f7b8264..49bfe4b1c 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -13,6 +13,7 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.types.component :as ctk] [app.common.types.page :as ctp] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] @@ -133,6 +134,9 @@ (rx/of (dch/commit-changes changes)) (rx/empty)))))) +(declare real-delete-shapes) +(declare update-shape-flags) + (defn delete-shapes ([ids] (delete-shapes nil ids)) ([page-id ids] @@ -140,141 +144,175 @@ (ptk/reify ::delete-shapes ptk/WatchEvent (watch [it state _] - (let [file-id (:current-file-id state) - page-id (or page-id (:current-page-id state)) - file (wsh/get-file state file-id) - page (wsh/lookup-page state page-id) - objects (wsh/lookup-page-objects state page-id) - - ids (cph/clean-loops objects ids) - lookup (d/getf objects) - - layout-ids (->> ids - (mapcat (partial cph/get-parent-ids objects)) - (filter (partial ctl/layout? objects))) + (let [file-id (:current-file-id state) + page-id (or page-id (:current-page-id state)) + file (wsh/get-file state file-id) + page (wsh/lookup-page state page-id) + objects (wsh/lookup-page-objects state page-id) components-v2 (features/active-feature? state :components-v2) - 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) + ids (cph/clean-loops objects ids) - 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)) + in-component-copy? + (fn [shape-id] + ;; Look for shapes that are inside a component copy, but are + ;; not the root. In this case, they must not be deleted, + ;; but hidden (to be able to recover them more easily). + (let [shape (get objects shape-id) + component-shape (cph/get-component-shape objects shape)] + (and (ctk/in-component-instance? shape) + (not= shape component-shape) + (not (ctk/main-instance? component-shape))))) - ;; 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])) - - 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)) - - 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) - - 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)))) - - empty-parents - ;; Any parent whose children are all deleted, must be deleted too. - (into (d/ordered-set) (find-all-empty-parents #{})) - - components-to-delete + [ids-to-delete ids-to-hide] (if components-v2 - (reduce (fn [components id] - (let [shape (get objects id)] - (if (and (= (:component-file shape) file-id) ;; Main instances should exist only in local file - (:main-instance? shape)) ;; but check anyway - (conj components (:component-id shape)) - components))) - [] - (into ids all-children)) - []) + (loop [ids-seq (seq ids) + ids-to-delete [] + ids-to-hide []] + (let [id (first ids-seq)] + (if (nil? id) + [ids-to-delete ids-to-hide] + (if (in-component-copy? id) + (recur (rest ids-seq) + ids-to-delete + (conj ids-to-hide id)) + (recur (rest ids-seq) + (conj ids-to-delete id) + ids-to-hide))))) + [ids []])] - changes (-> (pcb/empty-changes it page-id) - (pcb/with-page page) - (pcb/with-objects objects) - (pcb/with-library-data file) - (pcb/set-page-option :guides guides)) + (rx/concat (rx/of (update-shape-flags ids-to-hide {:hidden true})) + (real-delete-shapes file page objects ids-to-delete it components-v2))))))) - 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 components-v2)) - changes - components-to-delete) +(defn- real-delete-shapes + [file page objects ids it components-v2] + (let [lookup (d/getf objects) - changes (-> changes - (pcb/remove-objects all-children) - (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))))))] + layout-ids (->> ids + (mapcat (partial cph/get-parent-ids objects)) + (filter (partial ctl/layout? objects))) - (rx/of (dc/detach-comment-thread ids) - (dwsul/update-layout-positions all-parents) - (dch/commit-changes changes) - (dwsul/update-layout-positions layout-ids))))))) + 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) + + 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)) + + ;; 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])) + + 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)) + + 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) + + 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)))) + + empty-parents + ;; Any parent whose children are all deleted, must be deleted too. + (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)) + []) + + 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 (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 components-v2)) + changes + components-to-delete) + + changes (-> changes + (pcb/remove-objects all-children) + (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))))))] + + (rx/of (dc/detach-comment-thread ids) + (dwsul/update-layout-positions all-parents) + (dch/commit-changes changes) + (dwsul/update-layout-positions layout-ids)))) (defn create-and-add-shape [type frame-x frame-y data] @@ -330,3 +368,63 @@ (add-shape shape) (move-shapes-into-frame (:id shape) selected) (dwu/commit-undo-transaction))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Shape Flags +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn update-shape-flags + [ids {:keys [blocked hidden] :as flags}] + (us/verify (s/coll-of ::us/uuid) ids) + (us/assert ::shape-attrs flags) + (ptk/reify ::update-shape-flags + ptk/WatchEvent + (watch [_ state _] + (let [update-fn + (fn [obj] + (cond-> obj + (boolean? blocked) (assoc :blocked blocked) + (boolean? hidden) (assoc :hidden hidden))) + objects (wsh/lookup-page-objects state) + ids (into ids (->> ids (mapcat #(cph/get-children-ids objects %))))] + (rx/of (dch/update-shapes ids update-fn)))))) + +(defn toggle-visibility-selected + [] + (ptk/reify ::toggle-visibility-selected + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state)] + (rx/of (dch/update-shapes selected #(update % :hidden not))))))) + +(defn toggle-lock-selected + [] + (ptk/reify ::toggle-lock-selected + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state)] + (rx/of (dch/update-shapes selected #(update % :blocked not))))))) + +(defn toggle-file-thumbnail-selected + [] + (ptk/reify ::toggle-file-thumbnail-selected + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state) + pages (-> state :workspace-data :pages-index vals) + get-frames (fn [{:keys [objects id] :as page}] + (->> (ctst/get-frames objects) + (sequence + (comp (filter :use-for-thumbnail?) + (map :id) + (remove selected) + (map (partial vector id))))))] + + (rx/concat + (rx/from + (->> (mapcat get-frames pages) + (d/group-by first second) + (map (fn [[page-id frame-ids]] + (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id}))))) + (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not)))))))) +