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 1/2] :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)" From 46053b6bbfda4533000060ec69856e2d9599bd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 31 Aug 2022 17:48:02 +0200 Subject: [PATCH 2/2] :tada: Import & export new components --- backend/src/app/rpc/commands/binfile.clj | 4 + common/src/app/common/file_builder.cljc | 56 ++++++++- common/src/app/common/pages/changes.cljc | 4 +- common/src/app/common/pages/changes_spec.cljc | 5 +- common/src/app/common/types/container.cljc | 82 +++++++------ common/src/app/common/types/file.cljc | 47 ++++---- common/src/app/common/types/shape_tree.cljc | 7 +- .../test/app/common/test_helpers/files.cljc | 3 +- .../data/workspace/libraries_helpers.cljs | 5 +- frontend/src/app/main/render.cljs | 27 +++-- .../src/app/main/ui/dashboard/import.cljs | 4 + frontend/src/app/main/ui/shapes/export.cljs | 10 +- frontend/src/app/util/import/parser.cljs | 6 +- frontend/src/app/worker/export.cljs | 37 ++++-- frontend/src/app/worker/import.cljs | 108 ++++++++++++++---- 15 files changed, 283 insertions(+), 122 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 764f0651e..48b2b04c6 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -750,6 +750,10 @@ (uuid? (:typography-ref-file form)) (update :typography-ref-file lookup-index) + ;; This covers the component instance links + (uuid? (:component-file form)) + (update :component-file lookup-index) + ;; This covers the shadows and grids (they have directly ;; the :file-id prop) (uuid? (:file-id form)) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index a924186b3..7194fdf10 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -9,12 +9,16 @@ (:require [app.common.data :as d] [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.pages.changes :as ch] [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] [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] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -516,10 +520,17 @@ [file data] (let [selrect cts/empty-selrect - name (:name data) - path (:path data) + name (:name data) + path (:path data) + main-instance-id (:main-instance-id data) + main-instance-page (:main-instance-page data) obj (-> (cts/make-minimal-group nil selrect name) (merge data) + (dissoc :path + :main-instance-id + :main-instance-page + :main-instance-x + :main-instance-y) (check-name file :group) (d/without-nils))] (-> file @@ -528,6 +539,8 @@ :id (:id obj) :name name :path path + :main-instance-id main-instance-id + :main-instance-page main-instance-page :shapes [obj]}) (assoc :last-id (:id obj)) @@ -546,7 +559,8 @@ (commit-change file {:type :del-component - :id component-id}) + :id component-id + :skip-undelete? true}) (:masked-group? component) (let [mask (first children)] @@ -586,6 +600,42 @@ (dissoc :current-component-id) (update :parent-stack pop)))) +(defn finish-deleted-component + [component-id page-id main-instance-x main-instance-y file] + (let [file (assoc file :current-component-id component-id) + page (ctpl/get-page (:data file) page-id) + component (ctkl/get-component (:data file) component-id) + main-instance-id (:main-instance-id component) + + ; To obtain a deleted component, we first create the component + ; and the main instance in the workspace, and then delete them. + [_ shapes] + (ctn/make-component-instance page + component + (:id file) + (gpt/point main-instance-x + main-instance-y) + {:main-instance? true + :force-id main-instance-id})] + (as-> file $ + (reduce #(commit-change %1 + {:type :add-obj + :id (:id %2) + :page-id (:id page) + :parent-id (:parent-id %2) + :frame-id (:frame-id %2) + :obj %2}) + $ + shapes) + (commit-change $ {:type :del-component + :id component-id}) + (reduce #(commit-change %1 {:type :del-obj + :page-id page-id + :id (:id %2)}) + $ + shapes) + (dissoc $ :current-component-id)))) + (defn delete-object [file id] (let [page-id (:current-page-id file)] diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 5d8f969da..511cd6714 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -370,8 +370,8 @@ (assoc :objects objects)))) (defmethod process-change :del-component - [data {:keys [id]}] - (ctf/delete-component data id)) + [data {:keys [id skip-undelete?]}] + (ctf/delete-component data id skip-undelete?)) (defmethod process-change :restore-component [data {:keys [id]}] diff --git a/common/src/app/common/pages/changes_spec.cljc b/common/src/app/common/pages/changes_spec.cljc index bd147cbb9..f8e1be45d 100644 --- a/common/src/app/common/pages/changes_spec.cljc +++ b/common/src/app/common/pages/changes_spec.cljc @@ -159,8 +159,11 @@ (s/keys :req-un [::id] :opt-un [::name :internal.changes.add-component/shapes])) +(s/def :internal.changes.del-component/skip-undelete? boolean?) + (defmethod change-spec :del-component [_] - (s/keys :req-un [::id])) + (s/keys :req-un [::id] + :opt-un [:internal.changes.del-component/skip-undelete?])) (defmethod change-spec :restore-component [_] (s/keys :req-un [::id])) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index b6ea7b917..104fd0f34 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -108,53 +108,59 @@ "Clone the shapes of the component, generating new names and ids, and linking each new shape to the corresponding one of the component. Place the new instance coordinates in the given position." - [container component component-file-id position main-instance?] - (let [component-shape (get-shape component (:id component)) + ([container component component-file-id position] + (make-component-instance container component component-file-id position {})) - orig-pos (gpt/point (:x component-shape) (:y component-shape)) - delta (gpt/subtract position orig-pos) + ([container component component-file-id position + {:keys [main-instance? force-id] :or {main-instance? false force-id nil}}] + (let [component-shape (get-shape component (:id component)) - objects (:objects container) - unames (volatile! (ctst/retrieve-used-names objects)) + orig-pos (gpt/point (:x component-shape) (:y component-shape)) + delta (gpt/subtract position orig-pos) - frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta)) + objects (:objects container) + unames (volatile! (ctst/retrieve-used-names objects)) - update-new-shape - (fn [new-shape original-shape] - (let [new-name (ctst/generate-unique-name @unames (:name new-shape))] + frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta)) - (when (nil? (:parent-id original-shape)) - (vswap! unames conj new-name)) + update-new-shape + (fn [new-shape original-shape] + (let [new-name (ctst/generate-unique-name @unames (:name new-shape))] - (cond-> new-shape - true - (as-> $ - (gsh/move $ delta) - (assoc $ :frame-id frame-id) - (assoc $ :parent-id - (or (:parent-id $) (:frame-id $))) - (dissoc $ :touched)) + (when (nil? (:parent-id original-shape)) + (vswap! unames conj new-name)) - (nil? (:shape-ref original-shape)) - (assoc :shape-ref (:id original-shape)) + (cond-> new-shape + true + (as-> $ + (gsh/move $ delta) + (assoc $ :frame-id frame-id) + (assoc $ :parent-id + (or (:parent-id $) (:frame-id $))) + (dissoc $ :touched)) - (nil? (:parent-id original-shape)) - (assoc :component-id (:id original-shape) - :component-file component-file-id - :component-root? true - :name new-name) + (nil? (:shape-ref original-shape)) + (assoc :shape-ref (:id original-shape)) - (and (nil? (:parent-id original-shape)) main-instance?) - (assoc :main-instance? true) + (nil? (:parent-id original-shape)) + (assoc :component-id (:id original-shape) + :component-file component-file-id + :component-root? true + :name new-name) - (some? (:parent-id original-shape)) - (dissoc :component-root?)))) + (and (nil? (:parent-id original-shape)) main-instance?) + (assoc :main-instance? true) - [new-shape new-shapes _] - (ctst/clone-object component-shape - nil - (get component :objects) - update-new-shape)] + (some? (:parent-id original-shape)) + (dissoc :component-root?)))) + + [new-shape new-shapes _] + (ctst/clone-object component-shape + nil + (get component :objects) + update-new-shape + (fn [object _] object) + force-id)] + + [new-shape new-shapes]))) - [new-shape new-shapes])) - diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 18881b8c4..bff027463 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -124,29 +124,32 @@ "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]) + ([file-data component-id] + (delete-component file-data component-id false)) - 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)))] + ([file-data component-id skip-undelete?] + (let [components-v2 (get-in file-data [:options :components-v2]) - (cond-> file-data - components-v2 - (add-to-deleted-components) + 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)))] - :always - (ctkl/delete-component component-id)))) + (cond-> file-data + (and components-v2 (not skip-undelete?)) + (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." @@ -253,7 +256,7 @@ component (:id file-data) position - true) + {:main-instance? true}) add-shapes (fn [page] @@ -317,7 +320,7 @@ component (:id file-data) position - true) + {:main-instance? true}) ; Add all shapes of the main instance to the library page add-main-instance-shapes diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index eba4c065e..728801fc0 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -284,10 +284,13 @@ the order of the children of each parent." ([object parent-id objects update-new-object] - (clone-object object parent-id objects update-new-object (fn [object _] object))) + (clone-object object parent-id objects update-new-object (fn [object _] object) nil)) ([object parent-id objects update-new-object update-original-object] - (let [new-id (uuid/next)] + (clone-object object parent-id objects update-new-object update-original-object nil)) + + ([object parent-id objects update-new-object update-original-object force-id] + (let [new-id (or force-id (uuid/next))] (loop [child-ids (seq (:shapes object)) new-direct-children [] new-children [] diff --git a/common/test/app/common/test_helpers/files.cljc b/common/test/app/common/test_helpers/files.cljc index ebac09ee6..3c3302cba 100644 --- a/common/test/app/common/test_helpers/files.cljc +++ b/common/test/app/common/test_helpers/files.cljc @@ -97,8 +97,7 @@ (ctn/make-component-instance (ctpl/get-page file-data page-id) (ctkl/get-component (:data library) component-id) (:id library) - (gpt/point 0 0) - false)] + (gpt/point 0 0))] (swap! idmap assoc label (:id instance-shape)) (-> file-data diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index f48b3ba45..b5ba25207 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -118,8 +118,7 @@ :name (:name new-component-shape) :objects (d/index-by :id new-component-shapes)} (:component-file main-instance-shape) - position - false))] + position))] [new-component-shape new-component-shapes new-instance-shape new-instance-shapes])) @@ -130,7 +129,7 @@ (let [component (cph/get-component libraries file-id component-id) [new-shape new-shapes] - (ctn/make-component-instance page component file-id position false) + (ctn/make-component-instance page component file-id position) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) (pcb/empty-changes it (:id page)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 66b8ee2c4..377da5147 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -220,7 +220,7 @@ :fill "none"} (when include-metadata? - [:& export/export-page {:options (:options data)}]) + [:& export/export-page {:id (:id data) :options (:options data)}]) (let [shapes (->> shapes (remove cph/frame-shape?) @@ -393,6 +393,11 @@ object (get objects id) selrect (:selrect object) + main-instance-id (:main-instance-id data) + main-instance-page (:main-instance-page data) + main-instance-x (:main-instance-x data) + main-instance-y (:main-instance-y data) + vbox (format-viewbox {:width (:width selrect) @@ -403,7 +408,13 @@ (mf/deps objects) (fn [] (group-wrapper-factory objects)))] - [:> "symbol" #js {:id (str id) :viewBox vbox "penpot:path" path} + [:> "symbol" #js {:id (str id) + :viewBox vbox + "penpot:path" path + "penpot:main-instance-id" main-instance-id + "penpot:main-instance-page" main-instance-page + "penpot:main-instance-x" main-instance-x + "penpot:main-instance-y" main-instance-y} [:title name] [:> shape-container {:shape object} [:& group-wrapper {:shape object :view-box vbox}]]])) @@ -414,7 +425,8 @@ (let [data (obj/get props "data") children (obj/get props "children") render-embed? (obj/get props "render-embed?") - include-metadata? (obj/get props "include-metadata?")] + include-metadata? (obj/get props "include-metadata?") + source (keyword (obj/get props "source" "components"))] [:& (mf/provider embed/context) {:value render-embed?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} [:svg {:version "1.1" @@ -424,7 +436,7 @@ :style {:display (when-not (some? children) "none")} :fill "none"} [:defs - (for [[id data] (:components data)] + (for [[id data] (source data)] [:& component-symbol {:id id :key (dm/str id) :data data}])] children]]])) @@ -482,9 +494,9 @@ (rds/renderToStaticMarkup elem))))))) (defn render-components - [data] + [data source] (let [;; Join all components objects into a single map - objects (->> (:components data) + objects (->> (source data) (vals) (map :objects) (reduce conj))] @@ -498,5 +510,6 @@ (rx/map (fn [data] (let [elem (mf/element components-sprite-svg - #js {:data data :render-embed? true :include-metadata? true})] + #js {:data data :render-embed? true :include-metadata? true + :source (name source)})] (rds/renderToStaticMarkup elem)))))))) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 7dc5fe0b6..1a29db2d2 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -13,6 +13,7 @@ [app.main.data.events :as ev] [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.icons :as i] @@ -247,6 +248,8 @@ :files (->> files (mapv #(assoc % :status :analyzing)))}) + components-v2 (features/use-feature :components-v2) + analyze-import (mf/use-callback (fn [files] @@ -268,6 +271,7 @@ :num-files (count files)})) (->> (uw/ask-many! {:cmd :import-files + :components-v2 components-v2 :project-id project-id :files files}) (rx/subs diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index b2453f7e0..a37a4a9df 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -54,7 +54,8 @@ ([props attr trfn] (let [val (get shape attr) val (if (keyword? val) (d/name val) val) - ns-attr (str "penpot:" (-> attr d/name))] + ns-attr (-> (str "penpot:" (-> attr d/name)) + (str/strip-suffix "?"))] (cond-> props (some? val) (obj/set! ns-attr (trfn val))))))) @@ -136,7 +137,8 @@ (add! :typography-ref-file) (add! :component-file) (add! :component-id) - (add! :component-root) + (add! :component-root?) + (add! :main-instance?) (add! :shape-ref)))) (defn prefix-keys [m] @@ -177,11 +179,11 @@ :axis (d/name axis)}])]) (mf/defc export-page - [{:keys [options]}] + [{:keys [id options]}] (let [saved-grids (get options :saved-grids) flows (get options :flows) guides (get options :guides)] - [:> "penpot:page" #js {} + [:> "penpot:page" #js {:id id} (when (d/not-empty? saved-grids) (let [parse-grid (fn [[type params]] {:type type :params params}) grids (->> saved-grids (mapv parse-grid))] diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index f04ba60c7..4f23b4821 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -400,7 +400,8 @@ component-id (get-meta node :component-id uuid/uuid) component-file (get-meta node :component-file uuid/uuid) shape-ref (get-meta node :shape-ref uuid/uuid) - component-root? (get-meta node :component-root str->bool)] + component-root? (get-meta node :component-root str->bool) + main-instance? (get-meta node :main-instance str->bool)] (cond-> props (some? stroke-color-ref-id) @@ -414,6 +415,9 @@ component-root? (assoc :component-root? component-root?) + main-instance? + (assoc :main-instance? main-instance?) + (some? shape-ref) (assoc :shape-ref shape-ref)))) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 1427ebd8a..0df2c23bb 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -40,17 +40,18 @@ (reduce format-page {}))] (-> manifest (assoc (str (:id file)) - {:name name - :shared is-shared - :pages pages - :pagesIndex index - :version current-version - :libraries (->> (:libraries file) (into #{}) (mapv str)) - :exportType (d/name export-type) - :hasComponents (d/not-empty? (get-in file [:data :components])) - :hasMedia (d/not-empty? (get-in file [:data :media])) - :hasColors (d/not-empty? (get-in file [:data :colors])) - :hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))] + {:name name + :shared is-shared + :pages pages + :pagesIndex index + :version current-version + :libraries (->> (:libraries file) (into #{}) (mapv str)) + :exportType (d/name export-type) + :hasComponents (d/not-empty? (get-in file [:data :components])) + :hasDeletedComponents (d/not-empty? (get-in file [:data :deleted-components])) + :hasMedia (d/not-empty? (get-in file [:data :media])) + :hasColors (d/not-empty? (get-in file [:data :colors])) + :hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))] (let [manifest {:teamId (str team-id) :fileId (str file-id) :files (->> (vals files) (reduce format-file {}))}] @@ -146,9 +147,14 @@ (defn parse-library-components [file] - (->> (r/render-components (:data file)) + (->> (r/render-components (:data file) :components) (rx/map #(vector (str (:id file) "/components.svg") %)))) +(defn parse-deleted-components + [file] + (->> (r/render-components (:data file) :deleted-components) + (rx/map #(vector (str (:id file) "/deleted-components.svg") %)))) + (defn fetch-file-with-libraries [file-id components-v2] (->> (rx/zip (rp/query :file {:id file-id :components-v2 components-v2}) (rp/query :file-libraries {:file-id file-id})) @@ -426,6 +432,12 @@ (rx/filter #(d/not-empty? (get-in % [:data :components]))) (rx/flat-map parse-library-components)) + deleted-components-stream + (->> files-stream + (rx/flat-map vals) + (rx/filter #(d/not-empty? (get-in % [:data :deleted-components]))) + (rx/flat-map parse-deleted-components)) + pages-stream (->> render-stream (rx/map collect-page))] @@ -441,6 +453,7 @@ manifest-stream pages-stream components-stream + deleted-components-stream media-stream colors-stream typographies-stream) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index b606ca3db..8f7582a26 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -46,14 +46,15 @@ ([context type id media] (let [file-id (:file-id context) path (case type - :manifest (str "manifest.json") - :page (str file-id "/" id ".svg") - :colors (str file-id "/colors.json") - :typographies (str file-id "/typographies.json") - :media-list (str file-id "/media.json") - :media (let [ext (cm/mtype->extension (:mtype media))] - (str/concat file-id "/media/" id ext)) - :components (str file-id "/components.svg")) + :manifest (str "manifest.json") + :page (str file-id "/" id ".svg") + :colors (str file-id "/colors.json") + :typographies (str file-id "/typographies.json") + :media-list (str file-id "/media.json") + :media (let [ext (cm/mtype->extension (:mtype media))] + (str/concat file-id "/media/" id ext)) + :components (str file-id "/components.svg") + :deleted-components (str file-id "/deleted-components.svg")) parse-svg? (and (not= type :media) (str/ends-with? path "svg")) parse-json? (and (not= type :media) (str/ends-with? path "json")) @@ -125,7 +126,7 @@ (defn create-file "Create a new file on the back-end" - [context] + [context components-v2] (let [resolve (:resolve context) file-id (resolve (:file-id context))] (rp/mutation :create-temp-file @@ -133,7 +134,9 @@ :name (:name context) :is-shared (:shared context) :project-id (:project-id context) - :data (-> ctf/empty-file-data (assoc :id file-id))}))) + :data (-> ctf/empty-file-data + (assoc :id file-id) + (assoc-in [:options :components-v2] components-v2))}))) (defn link-file-libraries "Create a new file on the back-end" @@ -380,18 +383,22 @@ (rx/map (comp fb/close-page setup-interactions)))))))) (defn import-component [context file node] - (let [resolve (:resolve context) - content (cip/find-node node :g) - file-id (:id file) - old-id (cip/get-id node) - id (resolve old-id) - path (get-in node [:attrs :penpot:path] "") - data (-> (cip/parse-data :group content) - (assoc :path path) - (assoc :id id)) + (let [resolve (:resolve context) + content (cip/find-node node :g) + file-id (:id file) + old-id (cip/get-id node) + id (resolve old-id) + path (get-in node [:attrs :penpot:path] "") + main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] ""))) + main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] ""))) + data (-> (cip/parse-data :group content) + (assoc :path path) + (assoc :id id) + (assoc :main-instance-id main-instance-id) + (assoc :main-instance-page main-instance-page)) - file (-> file (fb/start-component data)) - children (cip/node-seq node)] + file (-> file (fb/start-component data)) + children (cip/node-seq node)] (->> (rx/from children) (rx/filter cip/shape?) @@ -401,6 +408,43 @@ (rx/reduce (partial process-import-node context) file) (rx/map fb/finish-component)))) +(defn import-deleted-component [context file node] + (let [resolve (:resolve context) + content (cip/find-node node :g) + file-id (:id file) + old-id (cip/get-id node) + id (resolve old-id) + path (get-in node [:attrs :penpot:path] "") + main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] ""))) + main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] ""))) + main-instance-x (get-in node [:attrs :penpot:main-instance-x] "") + main-instance-y (get-in node [:attrs :penpot:main-instance-y] "") + + data (-> (cip/parse-data :group content) + (assoc :path path) + (assoc :id id) + (assoc :main-instance-id main-instance-id) + (assoc :main-instance-page main-instance-page) + (assoc :main-instance-x main-instance-x) + (assoc :main-instance-y main-instance-y)) + + file (-> file (fb/start-component data)) + component-id (:current-component-id file) + children (cip/node-seq node)] + + (->> (rx/from children) + (rx/filter cip/shape?) + (rx/skip 1) + (rx/skip-last 1) + (rx/mapcat (partial resolve-media context file-id)) + (rx/reduce (partial process-import-node context) file) + (rx/map fb/finish-component) + (rx/map (partial fb/finish-deleted-component + component-id + main-instance-page + main-instance-x + main-instance-y))))) + (defn process-pages [context file] (let [index (:pages-index context) @@ -486,6 +530,18 @@ (rx/concat-reduce (partial import-component context) file))) (rx/of file))) +(defn process-deleted-components + [context file] + (if (:has-deleted-components context) + (let [split-components + (fn [content] (->> (cip/node-seq content) + (filter #(= :symbol (:tag %)))))] + + (->> (get-file context :deleted-components) + (rx/flat-map split-components) + (rx/concat-reduce (partial import-deleted-component context) file))) + (rx/of file))) + (defn process-file [context file] @@ -502,18 +558,20 @@ (rx/flat-map (partial process-library-media context)) (rx/tap #(progress! context :process-components)) (rx/flat-map (partial process-library-components context)) + (rx/tap #(progress! context :process-deleted-components)) + (rx/flat-map (partial process-deleted-components context)) (rx/flat-map (partial send-changes context)) (rx/tap #(rx/end! progress-str)))])) (defn create-files - [context files] + [context files components-v2] (let [data (group-by :file-id files)] (rx/concat (->> (rx/from files) (rx/map #(merge context %)) (rx/flat-map (fn [context] - (->> (create-file context) + (->> (create-file context components-v2) (rx/map #(vector % (first (get data (:file-id context))))))))) (->> (rx/from files) @@ -564,7 +622,7 @@ (rx/catch #(rx/of {:uri (:uri file) :error (.-message %)})))))))) (defmethod impl/handler :import-files - [{:keys [project-id files]}] + [{:keys [project-id files components-v2]}] (let [context {:project-id project-id :resolve (resolve-factory)} @@ -572,7 +630,7 @@ binary-files (filter #(= "application/octet-stream" (:type %)) files)] (->> (rx/merge - (->> (create-files context zip-files) + (->> (create-files context zip-files components-v2) (rx/flat-map (fn [[file data]] (->> (uz/load-from-url (:uri data))