From 251e7eada29bca572218d7092a19729c26d950a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 29 Aug 2022 09:23:51 +0200 Subject: [PATCH] :tada: Allow to restore deleted components --- common/src/app/common/pages/changes.cljc | 19 ++++--- .../src/app/common/pages/changes_builder.cljc | 32 +++++++++--- common/src/app/common/pages/changes_spec.cljc | 6 +++ common/src/app/common/types/colors_list.cljc | 4 ++ .../src/app/common/types/components_list.cljc | 4 ++ common/src/app/common/types/file.cljc | 50 ++++++++++++++++++- common/src/app/common/types/pages_list.cljc | 6 +++ .../app/common/types/typographies_list.cljc | 4 ++ .../app/main/data/workspace/libraries.cljs | 42 +++++++++++++++- .../src/app/main/data/workspace/shapes.cljs | 18 ++++--- .../app/main/ui/workspace/context_menu.cljs | 26 ++++++++-- .../sidebar/options/menus/component.cljs | 26 ++++++++-- frontend/translations/en.po | 3 ++ frontend/translations/es.po | 3 ++ 14 files changed, 209 insertions(+), 34 deletions(-) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 152612769..5d8f969da 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -19,6 +19,7 @@ [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.colors-list :as ctcl] + [app.common.types.file :as ctf] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] @@ -302,9 +303,7 @@ (defmethod process-change :del-page [data {:keys [id]}] - (-> data - (update :pages (fn [pages] (filterv #(not= % id) pages))) - (update :pages-index dissoc id))) + (ctpl/delete-page data id)) (defmethod process-change :mov-page [data {:keys [id index]}] @@ -320,7 +319,7 @@ (defmethod process-change :del-color [data {:keys [id]}] - (update data :colors dissoc id)) + (ctcl/delete-color data id)) (defmethod process-change :add-recent-color [data {:keys [color]}] @@ -372,7 +371,15 @@ (defmethod process-change :del-component [data {:keys [id]}] - (d/dissoc-in data [:components id])) + (ctf/delete-component data id)) + +(defmethod process-change :restore-component + [data {:keys [id]}] + (ctf/restore-component data id)) + +(defmethod process-change :purge-component + [data {:keys [id]}] + (ctf/purge-component data id)) ;; -- Typography @@ -386,7 +393,7 @@ (defmethod process-change :del-typography [data {:keys [id]}] - (update data :typographies dissoc id)) + (ctyl/delete-typography data id)) ;; === Operations diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 671e6a636..e37d9cd79 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -617,18 +617,34 @@ changes))) (defn delete-component - [changes id] + [changes id components-v2] (assert-library changes) (let [library-data (::library-data (meta changes)) prev-component (get-in library-data [:components id])] (-> changes (update :redo-changes conj {:type :del-component :id id}) - (update :undo-changes d/preconj {:type :add-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :main-instance-id (:main-instance-id prev-component) - :main-instance-page (:main-instance-page prev-component) - :shapes (vals (:objects prev-component))})))) + (update :undo-changes + (fn [undo-changes] + (cond-> undo-changes + components-v2 + (d/preconj {:type :purge-component + :id id}) + :always + (d/preconj {:type :add-component + :id id + :name (:name prev-component) + :path (:path prev-component) + :main-instance-id (:main-instance-id prev-component) + :main-instance-page (:main-instance-page prev-component) + :shapes (vals (:objects prev-component))}))))))) + +(defn restore-component + [changes id] + (assert-library changes) + (-> changes + (update :redo-changes conj {:type :restore-component + :id id}) + (update :undo-changes d/preconj {:type :del-component + :id id}))) diff --git a/common/src/app/common/pages/changes_spec.cljc b/common/src/app/common/pages/changes_spec.cljc index d53249e06..bd147cbb9 100644 --- a/common/src/app/common/pages/changes_spec.cljc +++ b/common/src/app/common/pages/changes_spec.cljc @@ -162,6 +162,12 @@ (defmethod change-spec :del-component [_] (s/keys :req-un [::id])) +(defmethod change-spec :restore-component [_] + (s/keys :req-un [::id])) + +(defmethod change-spec :purge-component [_] + (s/keys :req-un [::id])) + (defmethod change-spec :add-typography [_] (s/keys :req-un [::ctt/typography])) diff --git a/common/src/app/common/types/colors_list.cljc b/common/src/app/common/types/colors_list.cljc index 7c1b7ba0a..6a1143471 100644 --- a/common/src/app/common/types/colors_list.cljc +++ b/common/src/app/common/types/colors_list.cljc @@ -22,3 +22,7 @@ [file-data color-id f] (update-in file-data [:colors color-id] f)) +(defn delete-color + [file-data color-id] + (update file-data :colors dissoc color-id)) + diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index d2bbff58f..64edcab1f 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -35,3 +35,7 @@ [file-data component-id f] (update-in file-data [:components component-id] f)) +(defn delete-component + [file-data component-id] + (update file-data :components dissoc component-id)) + diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 7653d9954..18881b8c4 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -120,6 +120,54 @@ ;; Asset helpers +(defn delete-component + "Delete a component and store it to be able to be recovered later. + + Remember also the position of the main instance." + [file-data component-id] + (let [components-v2 (get-in file-data [:options :components-v2]) + + add-to-deleted-components + (fn [file-data] + (let [component (ctkl/get-component file-data component-id)] + (if (some? component) + (let [page (ctpl/get-page file-data (:main-instance-page component)) + main-instance (ctn/get-shape page (:main-instance-id component)) + component (assoc component + :main-instance-x (:x main-instance) ; An instance root is always a group, + :main-instance-y (:y main-instance))] ; so it will have :x and :y + (when (nil? main-instance) + (throw (ex-info "Cannot delete the main instance before the component" {:component-id component-id}))) + (assoc-in file-data [:deleted-components component-id] component)) + file-data)))] + + (cond-> file-data + components-v2 + (add-to-deleted-components) + + :always + (ctkl/delete-component component-id)))) + +(defn get-deleted-component + "Retrieve a component that has been deleted but still is in the safe store." + [file-data component-id] + (get-in file-data [:deleted-components component-id])) + +(defn restore-component + "Recover a deleted component and put it again in place." + [file-data component-id] + (let [component (-> (get-in file-data [:deleted-components component-id]) + (dissoc :main-instance-x :main-instance-y))] + (cond-> file-data + (some? component) + (-> (assoc-in [:components component-id] component) + (d/dissoc-in [:deleted-components component-id]))))) + +(defn purge-component + "Remove permanently a component." + [file-data component-id] + (d/dissoc-in file-data [:deleted-components component-id])) + (defmulti uses-asset? "Checks if a shape uses the given asset." (fn [asset-type _ _ _] asset-type)) @@ -185,7 +233,7 @@ (defn migrate-to-components-v2 "If there is any component in the file library, add a new 'Library backup' and generate - main instances for all components there. Mark the file with the :comonents-v2 option." + main instances for all components there. Mark the file with the :components-v2 option." [file-data] (let [components (ctkl/components-seq file-data)] (if (or (empty? components) diff --git a/common/src/app/common/types/pages_list.cljc b/common/src/app/common/types/pages_list.cljc index 3ff669b0c..b9394c476 100644 --- a/common/src/app/common/types/pages_list.cljc +++ b/common/src/app/common/types/pages_list.cljc @@ -37,3 +37,9 @@ [file-data page-id f] (update-in file-data [:pages-index page-id] f)) +(defn delete-page + [file-data page-id] + (-> file-data + (update :pages (fn [pages] (filterv #(not= % page-id) pages))) + (update :pages-index dissoc page-id))) + diff --git a/common/src/app/common/types/typographies_list.cljc b/common/src/app/common/types/typographies_list.cljc index 1e7dace0d..bda59c4a8 100644 --- a/common/src/app/common/types/typographies_list.cljc +++ b/common/src/app/common/types/typographies_list.cljc @@ -22,3 +22,7 @@ [file-data typography-id f] (update-in file-data [:typographies typography-id] f)) +(defn delete-typography + [file-data typography-id] + (update file-data :typographies dissoc typography-id)) + diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 3987fef61..85b6b0010 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -18,6 +18,7 @@ [app.common.types.color :as ctc] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] @@ -393,13 +394,50 @@ (ptk/reify ::delete-component ptk/WatchEvent (watch [it state _] - (let [data (get state :workspace-data) + (let [data (get state :workspace-data) + components-v2 (features/active-feature? state :components-v2) changes (-> (pcb/empty-changes it) (pcb/with-library-data data) - (pcb/delete-component id))] + (pcb/delete-component id components-v2))] (rx/of (dch/commit-changes changes)))))) +(defn restore-component + "Restore a deleted component, with the given id, on the current file library." + [id] + (us/assert ::us/uuid id) + (ptk/reify ::restore-component + ptk/WatchEvent + (watch [it state _] + (let [data (get state :workspace-data) + component (ctf/get-deleted-component data id) + page (ctpl/get-page data (:main-instance-page component)) + + ; Make a new main instance, with the same id of the original + [_main-instance shapes] + (ctn/make-component-instance page + component + (:id data) + (gpt/point (:main-instance-x component) + (:main-instance-y component)) + {:main-instance? true + :force-id (:main-instance-id component)}) + + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/with-page page)) + + changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) + changes + shapes) + + ; restore-component change needs to be done after add main instance + ; because when undo changes, the orden is inverse + changes (pcb/restore-component changes id)] + + (rx/of (dch/commit-changes changes)))))) + + (defn instantiate-component "Create a new shape in the current page, from the component with the given id in the given file library. Then selects the newly created instance." diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index f0be0d18e..3932073b1 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -233,7 +233,16 @@ (pcb/with-page page) (pcb/with-objects objects) (pcb/with-library-data file) - (pcb/set-page-option :guides guides) + (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) @@ -252,12 +261,7 @@ (cond-> (seq starting-flows) (pcb/update-page-option :flows (fn [flows] (->> (map :id starting-flows) - (reduce ctp/remove-flow flows)))))) - - changes (reduce (fn [changes component-id] - (pcb/delete-component changes component-id)) - changes - components-to-delete)] + (reduce ctp/remove-flow flows))))))] (rx/of (dc/detach-comment-thread ids) (dwsl/update-layout-positions all-parents) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index b1b9e7b44..404836014 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] + [app.common.types.components-list :as ctkl] [app.common.types.page :as ctp] [app.main.data.events :as ev] [app.main.data.modal :as modal] @@ -18,6 +19,7 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shortcuts :as sc] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] @@ -373,9 +375,19 @@ component-file (-> shapes first :component-file) component-shapes (filter #(contains? % :component-id) shapes) + components-v2 (features/use-feature :components-v2) + current-file-id (mf/use-ctx ctx/current-file-id) local-component? (= component-file current-file-id) + local-library (when local-component? + ;; Not needed to subscribe to changes because it's not expected + ;; to change while context menu is open + (deref refs/workspace-local-library)) + + main-component (when local-component? + (ctkl/get-component local-library (:component-id (first shapes)))) + do-add-component #(st/emit! (dwl/add-component)) do-detach-component #(st/emit! (dwl/detach-component shape-id)) do-detach-component-in-bulk #(st/emit! dwl/detach-selected-components) @@ -384,6 +396,7 @@ do-navigate-component-file #(st/emit! (dwl/nav-to-component-file component-file)) do-update-component #(st/emit! (dwl/update-component-sync shape-id component-file)) do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk component-shapes component-file)) + do-restore-component #(st/emit! (dwl/restore-component component-id)) do-update-remote-component #(st/emit! (modal/show @@ -436,11 +449,14 @@ (if local-component? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.update-main") - :on-click do-update-component}] - [:& menu-entry {:title (tr "workspace.shape.menu.show-main") - :on-click do-show-component}]] + (if (and (nil? main-component) components-v2) + [:& menu-entry {:title (tr "workspace.shape.menu.restore-main") + :on-click do-restore-component}] + [:* + [:& menu-entry {:title (tr "workspace.shape.menu.update-main") + :on-click do-update-component}] + [:& menu-entry {:title (tr "workspace.shape.menu.show-main") + :on-click do-show-component}]]) [:* [:& menu-entry {:title (tr "workspace.shape.menu.go-main") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 34e044ae9..3ce071101 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -6,9 +6,11 @@ (ns app.main.ui.workspace.sidebar.options.menus.component (:require + [app.common.types.components-list :as ctkl] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.context :as ctx] @@ -34,6 +36,15 @@ (:main-instance? values) true) + local-component? (= library-id current-file-id) + local-library (when local-component? + ;; Not needed to subscribe to changes because it's not expected + ;; to change while context menu is open + (deref refs/workspace-local-library)) + + main-component (when local-component? + (ctkl/get-component local-library component-id)) + on-menu-click (mf/use-callback (fn [event] @@ -54,6 +65,9 @@ do-update-component #(st/emit! (dwl/update-component-sync id library-id)) + do-restore-component + #(st/emit! (dwl/restore-component component-id)) + do-update-remote-component #(st/emit! (modal/show {:type :confirm @@ -85,11 +99,13 @@ ;; app/main/ui/workspace/context_menu.cljs [:& context-menu {:on-close on-menu-close :show (:menu-open @local) - :options (if (= library-id current-file-id) - [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - [(tr "workspace.shape.menu.reset-overrides") do-reset-component] - [(tr "workspace.shape.menu.update-main") do-update-component] - [(tr "workspace.shape.menu.show-main") do-show-component]] + :options (if local-component? + (if (and (nil? main-component) components-v2) + [[(tr "workspace.shape.menu.restore-main") do-restore-component]] + [[(tr "workspace.shape.menu.detach-instance") do-detach-component] + [(tr "workspace.shape.menu.reset-overrides") do-reset-component] + [(tr "workspace.shape.menu.update-main") do-update-component] + [(tr "workspace.shape.menu.show-main") do-show-component]]) [[(tr "workspace.shape.menu.detach-instance") do-detach-component] [(tr "workspace.shape.menu.reset-overrides") do-reset-component] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 26a399377..3e55a545f 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4303,6 +4303,9 @@ msgstr "Update main components" msgid "workspace.shape.menu.update-main" msgstr "Update main component" +msgid "workspace.shape.menu.restore-main" +msgstr "Restore main component" + #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.sidebar.history" msgstr "History (%s)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index f7b53fd83..023384c87 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -4498,6 +4498,9 @@ msgstr "Actualizar componentes" msgid "workspace.shape.menu.update-main" msgstr "Actualizar componente principal" +msgid "workspace.shape.menu.restore-main" +msgstr "Restaurar componente principal" + #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.sidebar.history" msgstr "Historial (%s)"