From fac72a5874319eede62f0d3bfbf8a9d07722217a Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 28 Sep 2023 12:47:01 +0200 Subject: [PATCH] :tada: Component swap --- common/src/app/common/pages/changes.cljc | 8 +- .../src/app/common/pages/changes_builder.cljc | 9 +- common/src/app/common/pages/helpers.cljc | 8 + common/src/app/common/types/component.cljc | 9 +- common/src/app/common/types/container.cljc | 19 + .../partials/sidebar-element-options.scss | 154 +++++ frontend/src/app/main/data/workspace.cljs | 9 +- .../src/app/main/data/workspace/groups.cljs | 2 +- .../app/main/data/workspace/libraries.cljs | 52 ++ .../app/main/data/workspace/selection.cljs | 23 +- .../src/app/main/data/workspace/shapes.cljs | 21 +- .../app/main/data/workspace/shortcuts.cljs | 5 +- .../data/workspace/specialized_panel.cljs | 31 + .../app/main/data/workspace/transforms.cljs | 7 +- frontend/src/app/main/refs.cljs | 3 + frontend/src/app/main/ui/hooks.cljs | 7 +- .../app/main/ui/workspace/context_menu.cljs | 2 +- .../main/ui/workspace/sidebar/layer_item.cljs | 22 +- .../main/ui/workspace/sidebar/options.cljs | 16 + .../sidebar/options/menus/component.cljs | 175 +++++- .../sidebar/options/shapes/frame.cljs | 7 +- .../sidebar/options/shapes/group.cljs | 5 +- .../main/ui/workspace/viewport/actions.cljs | 4 +- .../state_components_sync_test.cljs | 575 +++++++++--------- frontend/translations/en.po | 6 + frontend/translations/es.po | 6 + 26 files changed, 837 insertions(+), 348 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/specialized_panel.cljs diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 0133d2978..f0f773ec8 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -463,20 +463,22 @@ (d/update-in-when data [:components component-id :objects] reg-objects)))) (defmethod process-change :mov-objects - [data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape]}] + [data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape component-swap]}] (letfn [(calculate-invalid-targets [objects shape-id] (let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))] (->> (get-in objects [shape-id :shapes]) (reduce reduce-fn #{shape-id})))) ;; Avoid placing a shape as a direct or indirect child of itself, - ;; or inside its main component if it's in a copy. + ;; or inside its main component if it's in a copy, + ;; or inside a copy (is-valid-move? [objects shape-id] (let [invalid-targets (calculate-invalid-targets objects shape-id)] (and (contains? objects shape-id) (not (invalid-targets parent-id)) (not (cph/components-nesting-loop? objects shape-id parent-id)) - #_(cph/valid-frame-target? objects parent-id shape-id)))) + (or component-swap + (not (ctk/in-component-copy? (get objects parent-id))))))) ;; We don't want to change the structure of component copies (insert-items [prev-shapes index shapes] (let [prev-shapes (or prev-shapes [])] diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index a40dc9a4d..9a9754a5d 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -308,9 +308,12 @@ (defn change-parent ([changes parent-id shapes] - (change-parent changes parent-id shapes nil)) + (change-parent changes parent-id shapes nil {})) ([changes parent-id shapes index] + (change-parent changes parent-id shapes index {})) + + ([changes parent-id shapes index options] (assert-page-id! changes) (assert-objects! changes) (let [objects (lookup-objects changes) @@ -323,7 +326,9 @@ :shapes (->> shapes reverse (mapv :id))} (some? index) - (assoc :index index)) + (assoc :index index) + (:component-swap options) + (assoc :component-swap true)) mk-undo-change (fn [undo-changes shape] diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 4d79957b0..99a1562c5 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -531,6 +531,14 @@ (merge-path other-path item)))) [other-path last-item false])))) +(defn prev-path + "Remove the last item of the path." + [path] + (let [split (split-path path)] + (if (= 1 (count split)) + "" + (join-path (butlast split))))) + (defn compact-name "Append the first item of the path and the name." [path name] diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 0d1b19943..319d4fd00 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -134,12 +134,19 @@ [shape] (some? (:shape-ref shape))) +(defn in-component-copy-not-head? + "Check if the shape is inside a component non-main instance and + is not the head of a subinstance." + [shape] + (and (some? (:shape-ref shape)) + (nil? (:component-id shape)))) + (defn in-component-copy-not-root? "Check if the shape is inside a component non-main instance and is not the root shape." [shape] (and (some? (:shape-ref shape)) - (nil? (:component-id shape)))) + (nil? (:component-root shape)))) (defn main-instance-of? "Check if this shape is the root of the main instance of the given component." diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 8167a1d11..022595b94 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -344,3 +344,22 @@ (if (= parent-id uuid/zero) current-top (get-top-instance objects parent current-top)))) + + +(defn get-first-not-copy-parent + "Go trough the parents until we find a shape that is not a copy of a component." + [objects id] + (let [shape (get objects id)] + (if (ctk/in-component-copy? shape) + (get-first-not-copy-parent objects (:parent-id shape)) + shape))) + +(defn has-any-copy-parent? + "Check if the shape has any parent that is a copy of a component." + [objects shape] + (let [parent (get objects (:parent-id shape))] + (if (nil? parent) + false + (if (ctk/in-component-copy? parent) + true + (has-any-copy-parent? objects (:parent-id shape)))))) diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 9fa16aa28..e251a36f3 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -73,6 +73,18 @@ width: 100%; align-items: center; justify-content: space-between; + + svg { + height: 8px; + width: 8px; + fill: $color-gray-20; + margin-right: 1rem; + transform: rotate(180deg); + } + + &.back { + cursor: pointer; + } } } @@ -585,6 +597,26 @@ } } } + + &.copy { + flex-wrap: wrap; + border-radius: 8px; + border: 1px solid $color-gray-60; + padding: 0.5rem; + cursor: pointer; + + .component-name { + width: 80%; + color: $color-white; + } + .component-parent-name { + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + padding-left: calc(0.5rem + 16px); + color: $color-gray-40; + } + } } .grid-option .custom-select { @@ -2554,3 +2586,125 @@ } } } + +.component-swap { + .search-block { + margin: 0.7rem 0.5rem 0.2rem 0.2rem; + height: 2.1rem; + width: 100%; + } + + svg { + fill: $color-gray-20; + height: 0.7rem; + width: 0.7rem; + cursor: pointer; + } + + .search-block { + border: 1px solid $color-gray-30; + margin: 0.6rem 0.5rem 0.2rem 0.2rem; + padding: $size-1 $size-2; + display: flex; + align-items: center; + + &:hover { + border-color: $color-gray-20; + } + + &:focus-within { + border-color: $color-primary; + } + + & .search-input { + background-color: $color-gray-50; + border: none; + color: $color-gray-10; + font-size: $fs12; + margin: 0; + padding: 0; + flex-grow: 1; + + &:focus { + color: lighten($color-gray-10, 8%); + outline: none; + } + } + + & .search-icon { + display: flex; + align-items: center; + + svg { + fill: $color-gray-30; + height: 16px; + width: 16px; + } + + &.close { + transform: rotate(45deg); + cursor: pointer; + } + } + } + + .component-path { + display: flex; + margin: 0.4rem 0 0 0.4rem; + cursor: pointer; + svg { + height: 8px; + width: 8px; + margin-right: 0.5rem; + transform: rotate(180deg); + } + } + + .component-list { + margin: 0.7rem 0.5rem 0.5rem 0.5rem; + } + + .component-item { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + cursor: pointer; + svg { + background-color: $color-canvas; + border-radius: $br4; + border: 2px solid transparent; + height: 24px; + width: 24px; + margin-right: $size-2; + } + + .selected { + color: $color-primary; + } + + &:hover { + color: $color-primary; + } + + &.disabled { + cursor: auto; + color: $color-gray-30; + } + } + + .component-group { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + justify-content: space-between; + cursor: pointer; + height: 24px; + svg { + height: 8px; + width: 8px; + } + &:hover { + color: $color-primary; + } + } +} diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 0243c4e11..6d1706cc6 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -921,7 +921,7 @@ component-shape (ctn/get-component-shape objects shape) component-shape-parent (ctn/get-component-shape objects parent) - detach? (and (ctk/in-component-copy-not-root? shape) + detach? (and (ctk/in-component-copy-not-head? shape) (not= (:id component-shape) (:id component-shape-parent))) deroot? (and (ctk/instance-root? shape) @@ -1834,6 +1834,11 @@ ;; Calculate position for the pasted elements [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) + ;; We don't want to change the structure of component copies + ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent + parent-id (:id (ctn/get-first-not-copy-parent page-objects parent-id)) + frame-id (:id (ctn/get-first-not-copy-parent page-objects frame-id)) + process-shape (fn [_ shape] (let [parent (get page-objects parent-id) @@ -1841,7 +1846,7 @@ component-shape-parent (ctn/get-component-shape page-objects parent) ;; if foreign instance, or a shape belonging to another component, detach the shape detach? (or (foreign-instance? shape paste-objects state) - (and (ctk/in-component-copy-not-root? shape) + (and (ctk/in-component-copy-not-head? shape) (not= (:id component-shape) (:id component-shape-parent)))) assign-shapes? (and (or (cph/group-shape? shape) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 1b04a6f10..33cb56f59 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -101,7 +101,7 @@ ;; Shapes that are in a component, but are not root, must be detached, ;; because they will be now children of a non instance group. - shapes-to-detach (filter ctk/in-component-copy-not-root? shapes) + shapes-to-detach (filter ctk/in-component-copy-not-head? shapes) ;; Look at the `get-empty-groups-after-group-creation` ;; docstring to understand the real purpose of this code diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 96c357596..de4ae4870 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -32,6 +32,7 @@ [app.main.data.workspace.notifications :as-alias dwn] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] + [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.undo :as dwu] @@ -744,6 +745,57 @@ (rx/map #(update-component-sync (:id %) file-id (uuid/next)) (rx/from shapes)) (rx/of (dwu/commit-undo-transaction undo-id))))))) +(defn- find-shape-index + [objects id shape-id] + (let [object (get objects id)] + (when object + (let [shapes (:shapes object)] + (or (->> shapes + (map-indexed (fn [index shape] [shape index])) + (filter #(= shape-id (first %))) + first + second) + 0))))) + +(defn component-swap + "Swaps a component with another one" + [shape file-id id-new-component] + (dm/assert! (uuid? id-new-component)) + (dm/assert! (uuid? file-id)) + (ptk/reify ::component-swap + ptk/WatchEvent + (watch [it state _] + (let [page (wsh/lookup-page state) + libraries (wsh/get-libraries state) + + objects (:objects page) + index (find-shape-index objects (:parent-id shape) (:id shape)) + position (gpt/point (:x shape) (:y shape)) + changes (-> (pcb/empty-changes it (:id page)) + (pcb/with-objects objects)) + + [new-shape changes] + (dwlh/generate-instantiate-component changes + objects + file-id + id-new-component + position + page + libraries) + changes (pcb/change-parent changes (:parent-id shape) [new-shape] index {:component-swap true}) + undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update [(:id new-shape)]) + (dws/select-shapes (d/ordered-set (:id new-shape))) + (dwsh/delete-shapes nil (d/ordered-set (:id shape)) {:component-swap true}) + (dwu/commit-undo-transaction undo-id) + (dwsp/open-specialized-panel :component-swap [(assoc new-shape :parent-id (:parent-id shape))])))))) + + + + + (def valid-asset-types #{:colors :components :typographies}) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 6a463f2c6..6eb274b19 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -123,7 +123,8 @@ (update [_ state] (-> state (update-in [:workspace-local :selected] d/toggle-selection id toggle?) - (assoc-in [:workspace-local :last-selected] id))) + (assoc-in [:workspace-local :last-selected] id) + (dissoc :specialized-panel))) ptk/WatchEvent (watch [_ state _] @@ -188,7 +189,8 @@ (update [_ state] (-> state (update-in [:workspace-local :selected] disj id) - (update :workspace-local dissoc :last-selected))))) + (update :workspace-local dissoc :last-selected) + (dissoc :specialized-panel))))) (defn shift-select-shapes ([id] @@ -206,7 +208,8 @@ (-> state (assoc-in [:workspace-local :selected] (set/union selection append-to-selection)) - (update :workspace-local assoc :last-selected id))))))) + (update :workspace-local assoc :last-selected id) + (dissoc :specialized-panel))))))) (defn select-shapes [ids] @@ -223,7 +226,9 @@ ids (if (d/not-empty? focus) (cpf/filter-not-focus objects focus ids) ids)] - (assoc-in state [:workspace-local :selected] ids))) + (-> state + (assoc-in [:workspace-local :selected] ids) + (dissoc :specialized-panel)))) ptk/WatchEvent (watch [_ state _] @@ -277,7 +282,9 @@ (update :workspace-local #(-> % (assoc :selected (d/ordered-set)) - (dissoc :selected-frame)))))))) + (dissoc :selected-frame))) + :allways + (dissoc :specialized-panel)))))) ;; --- Select Shapes (By selrect) @@ -631,7 +638,11 @@ (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) (let [page (wsh/lookup-page state) objects (:objects page) - selected (wsh/lookup-selected state)] + selected (->> (wsh/lookup-selected state) + (map #(get objects %)) + (remove #(ctk/in-component-copy-not-root? %)) ;; We don't want to change the structure of component copies + (map :id) + set)] (when (seq selected) (let [obj (get objects (first selected)) delta (if move-delta? diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index f1e65650f..48b9d0d2b 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -12,7 +12,6 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.schema :as sm] - [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.page :as ctp] [app.common.types.shape :as cts] @@ -133,8 +132,9 @@ (declare update-shape-flags) (defn delete-shapes - ([ids] (delete-shapes nil ids)) - ([page-id ids] + ([ids] (delete-shapes nil ids {})) + ([page-id ids] (delete-shapes page-id ids {})) + ([page-id ids options] (dm/assert! (sm/set-of-uuid? ids)) (ptk/reify ::delete-shapes ptk/WatchEvent @@ -154,11 +154,11 @@ ;; 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 (ctn/get-component-shape objects shape)] - (and (ctk/in-component-copy? shape) - (not= shape component-shape) - (not (ctk/main-instance? component-shape))))) + ;; Unless we are doing a component swap, in which case we want + ;; to delete the old shape + (let [shape (get objects shape-id)] + (and (ctn/has-any-copy-parent? objects shape) + (not (:component-swap options))))) [ids-to-delete ids-to-hide] (if components-v2 @@ -347,6 +347,11 @@ frame-id (:parent-id base)) + ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent + parent-id (:id (ctn/get-first-not-copy-parent objects parent-id)) ;; We don't want to change the structure of component copies + frame-id (:id (ctn/get-first-not-copy-parent objects frame-id)) + + shape (cts/setup-shape (-> attrs (assoc :type type) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index dbacccfdd..f43223278 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -68,8 +68,9 @@ :cut {:tooltip (ds/meta "X") :command (ds/c-mod "x") :subsections [:edit] - :fn #(emit-when-no-readonly (dw/copy-selected) - (dw/delete-selected))} + :fn #(emit-when-no-readonly + (dw/copy-selected) + (dw/delete-selected))} :paste {:tooltip (ds/meta "V") :disabled true diff --git a/frontend/src/app/main/data/workspace/specialized_panel.cljs b/frontend/src/app/main/data/workspace/specialized_panel.cljs new file mode 100644 index 000000000..a6c9c480e --- /dev/null +++ b/frontend/src/app/main/data/workspace/specialized_panel.cljs @@ -0,0 +1,31 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.specialized-panel + (:require + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn interrupt? [e] (= e :interrupt)) + +(def clear-specialized-panel + (ptk/reify ::clear-specialized-panel + ptk/UpdateEvent + (update [_ state] + (dissoc state :specialized-panel)))) + +(defn open-specialized-panel + [type shapes] + (ptk/reify ::open-specialized-panel + ptk/UpdateEvent + (update [_ state] + (assoc state :specialized-panel {:type type :shapes shapes})) + ptk/WatchEvent + (watch [_ _ stream] + (->> stream + (rx/filter interrupt?) + (rx/take 1) + (rx/map (constantly clear-specialized-panel)))))) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 98fc79cd4..6c004325d 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -513,10 +513,11 @@ :else [move-vector nil]) - nesting-loop? (some #(cph/components-nesting-loop? objects (:id %) target-frame) shapes)] + nesting-loop? (some #(cph/components-nesting-loop? objects (:id %) target-frame) shapes) + is-component-copy? (ctk/in-component-copy? (get objects target-frame))] (cond-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) - (not nesting-loop?) + (and (not nesting-loop?) (not is-component-copy?)) (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data) :always (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis})))))) @@ -820,7 +821,7 @@ shape-ids-to-detach (reduce (fn [result shape] - (if (and (some? shape) (ctk/in-component-copy-not-root? shape)) + (if (and (some? shape) (ctk/in-component-copy-not-head? shape)) (let [shape-component (ctn/get-component-shape objects shape)] (if (= (:id frame-component) (:id shape-component)) result diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index e0c4c05ae..4eaf864e3 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -573,3 +573,6 @@ (defn workspace-preview-blend-by-id [id] (l/derived (l/key id) workspace-preview-blend =)) + +(def specialized-panel + (l/derived :specialized-panel st/state)) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 44a8595a9..7c845452b 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -118,7 +118,9 @@ on-drag-start (fn [event] (if (or disabled (not draggable?)) - (dom/prevent-default event) + (do + (dom/stop-propagation event) + (dom/prevent-default event)) (do (dom/stop-propagation event) (dnd/set-data! event data-type data) @@ -178,7 +180,8 @@ on-mount (fn [] (let [dom (mf/ref-val ref)] - (.setAttribute dom "draggable" draggable?) + (.setAttribute dom "draggable" true) ;; In firefox it needs to be draggable for problems with event handling. + ;; It will stop the drag operation in on-drag-start ;; Register all events in the (default) bubble mode, so that they ;; are captured by the most leaf item. The handler will stop diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 1f9c90f9a..1b160dc57 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -444,7 +444,7 @@ has-component? (some true? (map #(ctk/instance-head? %) shapes)) is-component? (and single? (-> shapes first :component-id some?)) - in-copy-not-root? (some true? (map #(ctk/in-component-copy-not-root? %) shapes)) + in-copy-not-root? (some true? (map #(ctk/in-component-copy-not-head? %) shapes)) objects (deref refs/workspace-page-objects) touched? (and single? (cph/component-touched? objects (:id (first shapes)))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 61bb1dbc3..539ee74cf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -10,6 +10,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] @@ -145,10 +147,15 @@ (mf/use-fn (mf/deps id index objects) (fn [side _data] - (if (= side :center) - (st/emit! (dw/relocate-selected-shapes id 0)) - (let [to-index (if (= side :top) (inc index) index) - parent-id (cph/get-parent-id objects id)] + (let [to-index (cond + (= side :center) 0 + (= side :top) (inc index) + :else index) + parent-id (if (= side :center) + id + (cph/get-parent-id objects id)) + parent (get objects parent-id)] + (when-not (ctk/in-component-copy? parent) ;; We don't want to change the structure of component copies (st/emit! (dw/relocate-selected-shapes parent-id to-index)))))) on-hold @@ -176,7 +183,10 @@ :data {:id (:id item) :index index :name (:name item)} - :draggable? (and sortable? (not read-only?))) + :draggable? (and + sortable? + (not read-only?) + (not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies ref (mf/use-ref) depth (+ depth 1) @@ -372,7 +382,7 @@ [:div.toggle-element {:class (when ^boolean hidden? "selected") :title (if (:hidden item) (tr "workspace.shape.menu.show") - (tr "workspace.shape.menu.hide")) + (tr "workspace.shape.menu.hide")) :on-click toggle-visibility} (if ^boolean hidden? i/eye-closed i/eye)] [:div.block-element {:class (when ^boolean blocked? "selected") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 96668717d..288d04610 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -21,6 +21,7 @@ [app.main.ui.viewer.inspect.right-sidebar :as hrs] [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] [app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] @@ -67,6 +68,13 @@ :page-id page-id :file-id file-id}]])) +(mf/defc specialized-panel + {::mf/wrap [mf/memo]} + [{:keys [panel]}] + (when (= (:type panel) :component-swap) + [:& component-menu {:shape (first (:shapes panel)) :swap-opened? true}])) + + (mf/defc options-content {::mf/wrap [mf/memo]} [{:keys [selected section shapes shapes-with-children page-id file-id on-change-section on-expand]}] @@ -76,6 +84,7 @@ shared-libs (mf/deref refs/workspace-libraries) edition (mf/deref refs/selected-edition) grid-edition (mf/deref refs/workspace-grid-edition) + sp-panel (mf/deref refs/specialized-panel) selected-shapes (into [] (keep (d/getf objects)) selected) first-selected-shape (first selected-shapes) @@ -116,6 +125,9 @@ {:ids [edition] :values (get objects edition)}] + (not (nil? sp-panel)) + [:& specialized-panel {:panel sp-panel}] + (d/not-empty? drawing) [:& shape-options {:shape (:object drawing) @@ -177,6 +189,10 @@ {:ids [edition] :values (get objects edition)}] + sp-panel + [:& specialized-panel {:panel sp-panel}] + + (d/not-empty? drawing) [:& shape-options {:shape (:object drawing) 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 bcb09fca4..f213c10ea 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 @@ -11,10 +11,13 @@ [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] + [app.common.uuid :as uuid] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.specialized-panel :as dwsp] [app.main.refs :as refs] + [app.main.render :refer [component-svg]] [app.main.store :as st] [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.dropdown :refer [dropdown]] @@ -23,6 +26,7 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -144,9 +148,153 @@ (when (or @editing? creating?) [:div.counter (str @size "/300")])]]))) +(mf/defc component-swap + [{:keys [shapes] :as props}] + (let [shape (first shapes) + new-css-system (mf/use-ctx ctx/new-css-system) + current-file-id (mf/use-ctx ctx/current-file-id) + workspace-file (deref refs/workspace-file) + workspace-data (deref refs/workspace-data) + workspace-libraries (deref refs/workspace-libraries) + objects (deref refs/workspace-page-objects) + libraries (assoc workspace-libraries current-file-id (assoc workspace-file :data workspace-data)) + filters* (mf/use-state + {:term "" + :file-id (:component-file shape) + :path (cph/prev-path (:name shape))}) + filters (deref filters*) + + components (-> (get-in libraries [(:file-id filters) :data :components]) + vals) + + components (if (str/empty? (:term filters)) + components + (filter #(str/includes? (str/lower (:name %)) (str/lower (:term filters))) components)) + + groups (->> (map :path components) + (filter #(= (cph/prev-path (:path %)) (:path filters))) + (remove str/empty?) + distinct + (map #(hash-map :name %))) + + components (filter #(= (:path %) (:path filters)) components) + + items (->> (concat groups components) + (sort-by :name)) + + ;; Get the ids of the components and its root-shapes that are parents of the current shape, to avoid loops + get-comps-ids (fn get-comps-ids [shape ids] + (if (uuid/zero? (:id shape)) + ids + (let [ids (if (ctk/instance-head? shape) + (conj ids (:id shape) (:component-id shape)) + ids)] + (get-comps-ids (get objects (:parent-id shape)) ids)))) + + parent-components (set (get-comps-ids (get objects (:parent-id shape)) [])) + + on-library-change + (mf/use-fn + (fn [event] + (let [value (or (-> (dom/get-target event) + (dom/get-value)) + (as-> (dom/get-current-target event) $ + (dom/get-attribute $ "data-test"))) + value (uuid/uuid value)] + (swap! filters* assoc :file-id value :term "" :path "")))) + + on-search-term-change + (mf/use-fn + (mf/deps new-css-system) + (fn [event] + ;; NOTE: When old-css-system is removed this function will recibe value and event + ;; Let won't be necessary any more + (let [value (if ^boolean new-css-system + event + (dom/get-target-val event))] + (swap! filters* assoc :term value)))) + + + on-search-clear-click + (mf/use-fn #(swap! filters* assoc :term "")) + + on-go-back + (mf/use-fn + (mf/deps (:path filters)) + #(swap! filters* assoc :path (cph/prev-path (:path filters)))) + + on-enter-group + (mf/use-fn #(swap! filters* assoc :path %)) + + handle-key-down + (mf/use-fn + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event) + node (dom/event->target event)] + + (when ^boolean enter? (dom/blur! node)) + (when ^boolean esc? (dom/blur! node)))))] + + [:div.component-swap + [:div.element-set-title + [:span (tr "workspace.options.component.swap")]] + [:div.component-swap-content + [:div.search-block + [:input.search-input + {:placeholder (str (tr "labels.search") " " (get-in libraries [(:file-id filters) :name])) + :type "text" + :value (:term filters) + :on-change on-search-term-change + :on-key-down handle-key-down}] + + (if ^boolean (str/empty? (:term filters)) + [:div.search-icon + i/search] + [:div.search-icon.close + {:on-click on-search-clear-click} + i/close])] + + [:select.input-select {:value (:file-id filters) + :data-mousetrap-dont-stop true + :on-change on-library-change} + (for [library (vals libraries)] + [:option {:key (:id library) :value (:id library)} (:name library)])] + + (when-not (str/empty? (:path filters)) + [:div.component-path {:on-click on-go-back} + [:span i/arrow-slide] + [:span (-> (cph/split-path (:path filters)) + last)]]) + [:div.component-list + (for [item items] + (if (:id item) + (let [data (get-in libraries [(:file-id filters) :data]) + container (ctf/get-component-page data item) + root-shape (ctf/get-component-root data item) + loop? (or (contains? parent-components (:main-instance-id item)) + (contains? parent-components (:id item)))] + [:div.component-item + {:class (stl/css-case :disabled loop?) + :key (:id item) + :on-click #(when-not loop? + (st/emit! + (dwl/component-swap shape (:file-id filters) (:id item))))} + [:& component-svg {:root-shape root-shape + :objects (:objects container)}] + [:span.component-name + {:class (stl/css-case :selected (= (:id item) (:component-id shape)))} + (:name item)]]) + [:div.component-group {:key (uuid/next) :on-click #(on-enter-group (:name item))} + [:span (:name item)] + [:span i/arrow-slide]]))]]])) + + + (mf/defc component-menu - [{:keys [ids values shape] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) + [{:keys [shape swap-opened?] :as props}] + (let [[ids values] [[(:id shape)] (select-keys shape component-attrs)] + new-css-system (mf/use-ctx ctx/new-css-system) current-file-id (mf/use-ctx ctx/current-file-id) components-v2 (mf/use-ctx ctx/components-v2) @@ -171,6 +319,7 @@ main-instance? (if components-v2 (ctk/main-instance? values) true) + can-swap? (and components-v2 (not main-instance?)) main-component? (:main-instance values) lacks-annotation? (nil? (:annotation values)) @@ -342,13 +491,20 @@ [:& component-annotation {:id id :values values :shape shape :component component}])])] [:div.element-set - [:div.element-set-title - [:span (tr "workspace.options.component")] + [:div.element-set-title {:class (stl/css-case :back swap-opened?) + :on-click #(when swap-opened? (st/emit! :interrupt))} + [:div + (when swap-opened? + [:span + i/arrow-slide]) + [:span (tr "workspace.options.component")]] [:span (if main-instance? (tr "workspace.options.component.main") (tr "workspace.options.component.copy"))]] [:div.element-set-content [:div.row-flex.component-row + {:class (stl/css-case :copy can-swap?) + :on-click #(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap [shape])))} (if main-instance? i/component i/component-copy) @@ -391,7 +547,14 @@ [(tr "workspace.shape.menu.reset-overrides") do-reset-component]) (when can-update-main? [(tr "workspace.shape.menu.update-main") do-update-remote-component]) - [(tr "workspace.shape.menu.go-main") do-navigate-component-file]])))}]]] + [(tr "workspace.shape.menu.go-main") do-navigate-component-file]])))}]] - (when components-v2 + (when can-swap? + [:div.component-parent-name + (cph/merge-path-item (:path component) (:name component))])] + + (when swap-opened? + [:& component-swap {:shapes [shape]}]) + + (when (and (not swap-opened?) components-v2) [:& component-annotation {:id id :values values :shape shape :component component}])]])))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 11e3fda65..7d900fd37 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -12,7 +12,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] - [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape fill-menu]] [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]] @@ -36,7 +36,6 @@ constraint-values (select-keys shape constraint-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) layout-item-values (select-keys shape layout-item-attrs) - [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)] ids (hooks/use-equal-memo ids) @@ -63,9 +62,7 @@ :values measure-values :type type :shape shape}] - [:& component-menu {:ids comp-ids - :values comp-values - :shape shape}] + [:& component-menu {:shape shape}] (when (or (not is-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 597d39a26..944323423 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -14,7 +14,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] - [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] @@ -66,7 +66,6 @@ [stroke-ids stroke-values] (get-attrs [shape] objects :stroke) [text-ids text-values] (get-attrs [shape] objects :text) [svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])] - [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)] [layout-item-ids layout-item-values] (get-attrs [shape] objects :layout-item)] @@ -74,7 +73,7 @@ :options true)} [:& layer-menu {:type type :ids layer-ids :values layer-values}] [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] - [:& component-menu {:ids comp-ids :values comp-values :shape shape}] ;;remove this in components-v2 + [:& component-menu {:shape shape}] ;;remove this in components-v2 [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index b65e6c279..1bc79a4dc 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -18,6 +18,7 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path :as dwdp] + [app.main.data.workspace.specialized-panel :as dwsp] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] @@ -84,7 +85,8 @@ left-click? (do - (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?)) + (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?) + dwsp/clear-specialized-panel) (when (and (not= edition id) (or text-editing? grid-editing?)) (st/emit! dw/clear-edition-mode)) diff --git a/frontend/test/frontend_tests/state_components_sync_test.cljs b/frontend/test/frontend_tests/state_components_sync_test.cljs index 0b1de3fff..fd66ca12d 100644 --- a/frontend/test/frontend_tests/state_components_sync_test.cljs +++ b/frontend/test/frontend_tests/state_components_sync_test.cljs @@ -90,63 +90,58 @@ (t/deftest test-touched-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] + store (the/prepare-store state done + (fn [new-state] ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 - ;; Rect 1 - ;; Rect 1* #--> Rect 1 - ;; #{:shapes-group} + ;; [Page: Page 1] + ;; Root Frame + ;; {Rect 1} + ;; Rect1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 ;; Circle 1 - ;; Rect 1 ---> Rect 1 ;; - ;; [Rect 1] - ;; page1 / Rect 1 - ;; - (let [[[group shape1 shape2] [c-group c-shape1] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] + ;; [Component: Rect 1] core.cljs:200:23 + ;; --> [Page 1] Rect 1 - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] + (t/is (= (:name group) "Rect 1")) + (t/is (nil? (:touched group))) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - :the/end)))) + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) ;; We cant't change the structure of component copies, so this operation will do nothing + :the/end)))) (t/deftest test-touched-children-delete (t/async done @@ -215,78 +210,78 @@ (t/deftest test-touched-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [group1' shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [group1' shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] + store (the/prepare-store state done + (fn [new-state] ;; Expected shape tree: - ;; - ;; [Page] - ;; Root Frame - ;; Component 1 - ;; Rect 1 - ;; Rect 2 - ;; Rect 3 - ;; Component 1* #--> Component 1 - ;; #{:shapes-group} - ;; Rect 2 ---> Rect 2 - ;; Rect 1 ---> Rect 1 - ;; Rect 3 ---> Rect 3 - ;; - ;; [Component 1] - ;; page1 / Component 1 - ;; - (let [[[group shape1 shape2 shape3] - [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] + ;; [Page: Page 1] + ;; Root Frame + ;; {Component 1} # + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; + ;; ========= Local library + ;; + ;; [Component: Component 1] + ;; --> [Page 1] Component 1 - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) + (let [[[group shape1 shape2 shape3] + [c-group c-shape1 c-shape2 c-shape3] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] + (t/is (= (:name group) "Component 1")) + (t/is (nil? (:touched group))) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (not= (:shape-ref shape3) nil)) - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) - :the/end)))) + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)) + (t/is (= (:name c-shape3) "Rect 3")) + (t/is (= (:touched c-shape3) nil)) + (t/is (= (:shape-ref c-shape3) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) ;; We cant't change the structure of component copies, so this operation will do nothing + :the/end)))) (t/deftest test-touched-from-lib (t/async @@ -1507,109 +1502,97 @@ (t/deftest test-update-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] + store (the/prepare-store state done + (fn [new-state] ;; Expected shape tree: ;; - ;; [Page] - ;; Root Frame - ;; Rect 1 + ;; [Page: Page 1] + ;; Root Frame + ;; {Rect 1} # + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 ;; Circle 1 - ;; Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; Rect 1 ---> Rect 1 - ;; Rect 1 #--> Rect 1 - ;; Circle 1 ---> Circle 1 - ;; Rect 1 ---> Rect 1 ;; - ;; [Rect 1] - ;; page1 / Rect 1 + ;; ========= Local library ;; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + ;; [Component: Rect 1] + ;; --> [Page 1] Rect 1 + ;; + (let [[[main1 shape1] + [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] + [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] + [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Circle 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 1")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) ;; We cant't change the structure of component copies, so this operation will do nothing + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-delete (t/async done @@ -1724,126 +1707,126 @@ (t/deftest test-update-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1' _shape2' _shape3'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1' _shape2' _shape3'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] + store (the/prepare-store state done + (fn [new-state] ;; Expected shape tree: ;; ;; [Page] ;; Root Frame ;; Component 1 - ;; Rect 2 ;; Rect 1 + ;; Rect 2 ;; Rect 3 ;; Component 1 #--> Component 1 - ;; Rect 2 ---> Rect 2 ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 ;; Rect 3 ---> Rect 3 ;; Component 1 #--> Component 1 - ;; Rect 2 ---> Rect 2 ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 ;; Rect 3 ---> Rect 3 ;; ;; [Component 1] ;; page1 / Component 1 ;; - (let [[[main1 shape1 shape2 shape3] - [c-main1 c-shape1 c-shape2 c-shape3] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + (let [[[main1 shape1 shape2 shape3] + [c-main1 c-shape1 c-shape2 c-shape3] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape4 shape5 shape6] - [c-instance1 c-shape4 c-shape5 c-shape6] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape4 shape5 shape6] + [c-instance1 c-shape4 c-shape5 c-shape6] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape7 shape8 shape9] - [c-instance2 c-shape7 c-shape8 c-shape9] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape7 shape8 shape9] + [c-instance2 c-shape7 c-shape8 c-shape9] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) nil)) + (t/is (= (:name main1) "Component 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) nil)) - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape2))) - (t/is (= (:name shape6) "Rect 3")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape3))) + (t/is (= (:name instance1) "Component 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape1))) + (t/is (= (:name shape5) "Rect 2")) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape2))) + (t/is (= (:name shape6) "Rect 3")) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape6) (:id c-shape3))) - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape7) "Rect 2")) - (t/is (= (:touched shape7) nil)) - (t/is (= (:shape-ref shape7) (:id c-shape1))) - (t/is (= (:name shape8) "Rect 1")) - (t/is (= (:touched shape8) nil)) - (t/is (= (:shape-ref shape8) (:id c-shape2))) - (t/is (= (:name shape9) "Rect 3")) - (t/is (= (:touched shape9) nil)) - (t/is (= (:shape-ref shape9) (:id c-shape3))) + (t/is (= (:name instance2) "Component 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape7) "Rect 1")) + (t/is (= (:touched shape7) nil)) + (t/is (= (:shape-ref shape7) (:id c-shape1))) + (t/is (= (:name shape8) "Rect 2")) + (t/is (= (:touched shape8) nil)) + (t/is (= (:shape-ref shape8) (:id c-shape2))) + (t/is (= (:name shape9) "Rect 3")) + (t/is (= (:touched shape9) nil)) + (t/is (= (:shape-ref shape9) (:id c-shape3))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-shape3 shape3)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape4 c-shape4)) - (t/is (= c-shape5 c-shape5)) - (t/is (= c-shape6 c-shape6)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape7 c-shape7)) - (t/is (= c-shape8 c-shape8)) - (t/is (= c-shape9 c-shape9)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-shape3 shape3)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape4 c-shape4)) + (t/is (= c-shape5 c-shape5)) + (t/is (= c-shape6 c-shape6)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape7 c-shape7)) + (t/is (= c-shape8 c-shape8)) + (t/is (= c-shape9 c-shape9)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-from-lib (t/async done diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 56ec34a5a..d70cb351d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1673,6 +1673,9 @@ msgstr "(you)" msgid "labels.your-account" msgstr "Your account" +msgid "labels.search" +msgstr "Search" + #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Loading image…" @@ -3590,6 +3593,9 @@ msgstr "Edit an annotation" msgid "workspace.options.component.main" msgstr "Main" + +msgid "workspace.options.component.swap" +msgstr "Swap component" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Constraints" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 3850af90f..3c096f801 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1725,6 +1725,9 @@ msgstr "(tú)" msgid "labels.your-account" msgstr "Tu cuenta" +msgid "labels.search" +msgstr "Buscar" + #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Cargando imagen…" @@ -3666,6 +3669,9 @@ msgstr "Editar una nota" msgid "workspace.options.component.main" msgstr "Principal" + +msgid "workspace.options.component.swap" +msgstr "Intercambiar componente" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Restricciones"