From 92f89c6cc1192bbd785cb25f395e4ce1ad71091f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Fri, 15 Oct 2021 11:51:37 +0200 Subject: [PATCH] :sparkles: Enhance duplicating prototype connections --- CHANGES.md | 1 + common/src/app/common/pages.cljc | 2 + common/src/app/common/pages/helpers.cljc | 7 ++ common/src/app/common/types/interactions.cljc | 16 +++++ frontend/src/app/main/data/workspace.cljs | 54 ++++++++------- .../app/main/data/workspace/selection.cljs | 65 +++++++++++-------- .../sidebar/options/menus/interactions.cljs | 4 +- .../ui/workspace/viewport/interactions.cljs | 8 +-- 8 files changed, 99 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 19efdd1aa..337e5a8d2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ ### :bug: Bugs fixed +- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093). - Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165). - Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216). - Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186). diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index accb623ad..00725454a 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -44,6 +44,7 @@ (d/export helpers/is-shape-grouped) (d/export helpers/get-parent) (d/export helpers/get-parents) +(d/export helpers/get-frame) (d/export helpers/clean-loops) (d/export helpers/calculate-invalid-targets) (d/export helpers/valid-frame-target) @@ -67,6 +68,7 @@ (d/export helpers/merge-path-item) (d/export helpers/compact-path) (d/export helpers/compact-name) +(d/export helpers/unframed-shape?) ;; Indices (d/export indices/calculate-z-index) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 596e5a9f4..909908820 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -484,3 +484,10 @@ (let [children (get-object-with-children frame-id objects)] (or (some cti/flow-origin? (map :interactions children)) (some #(cti/flow-to? % frame-id) (map :interactions (vals objects)))))) + +(defn unframed-shape? + "Checks if it's a non-frame shape in the top level." + [shape] + (and (not= (:type shape) :frame) + (= (:frame-id shape) uuid/zero))) + diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc index 15f1a15c3..42967c283 100644 --- a/common/src/app/common/types/interactions.cljc +++ b/common/src/app/common/types/interactions.cljc @@ -6,6 +6,7 @@ (ns app.common.types.interactions (:require + [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.spec :as us] [clojure.spec.alpha :as s])) @@ -340,6 +341,21 @@ [interactions index update-fn] (update interactions index update-fn)) +(defn remap-interactions + "Update all interactions whose destination points to a shape in the + map to the new id. And remove the ones whose destination does not exist + in the map nor in the objects tree." + [interactions ids-map objects] + (when (some? interactions) + (->> interactions + (filterv (fn [interaction] + (let [destination (:destination interaction)] + (or (nil? destination) + (contains? ids-map destination) + (contains? objects destination))))) + (mapv (fn [interaction] + (d/update-when interaction :destination #(get ids-map % %))))))) + (defn actionable? "Check if there is any interaction that is clickable by the user" [interactions] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ff080c04e..9219ab1f9 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1557,8 +1557,12 @@ (= :frame (get-in objects [(first selected) :type]))))) (defn- paste-shape - [{:keys [selected objects images] :as data} in-viewport?] ;; TODO: perhaps rename 'objects' to 'shapes', because it contains only - (letfn [;; Given a file-id and img (part generated by the ;; the shapes to paste, not the whole page tree of shapes + [{selected :selected + paste-objects :objects ;; rename this because here comes only the clipboard shapes, + images :images ;; not the whole page tree of shapes. + :as data} + in-viewport?] + (letfn [;; Given a file-id and img (part generated by the ;; copy-selected event), uploads the new media. (upload-media [file-id imgpart] (->> (http/send! {:uri (:file-data imgpart) @@ -1590,7 +1594,7 @@ (calculate-paste-position [state mouse-pos in-viewport?] (let [page-objects (wsh/lookup-page-objects state) - selected-objs (map #(get objects %) selected) + selected-objs (map #(get paste-objects %) selected) has-frame? (d/seek #(= (:type %) :frame) selected-objs) page-selected (wsh/lookup-selected state) wrapper (gsh/selection-rect selected-objs) @@ -1617,12 +1621,12 @@ [frame-id parent-id delta index])))) ;; Change the indexes if the paste is done with an element selected - (change-add-obj-index [objects selected index change] + (change-add-obj-index [paste-objects selected index change] (let [set-index (fn [[result index] id] [(assoc result id index) (inc index)]) map-ids (when index - (->> (vals objects) + (->> (vals paste-objects) (filter #(not (selected (:parent-id %)))) (map :id) (reduce set-index [{} (inc index)]) @@ -1634,8 +1638,8 @@ ;; Check if the shape is an instance whose master is defined in a ;; library that is not linked to the current file - (foreign-instance? [shape objects state] - (let [root (cph/get-root-shape shape objects) + (foreign-instance? [shape paste-objects state] + (let [root (cph/get-root-shape shape paste-objects) root-file-id (:component-file root)] (and (some? root) (not= root-file-id (:current-file-id state)) @@ -1643,34 +1647,36 @@ ;; Procceed with the standard shape paste procediment. (do-paste [it state mouse-pos media] - (let [media-idx (d/index-by :prev-id media) + (let [page-objects (wsh/lookup-page-objects state) + all-objects (merge page-objects paste-objects) + media-idx (d/index-by :prev-id media) ;; Calculate position for the pasted elements [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) - objects (->> objects - (d/mapm (fn [_ shape] - (-> shape - (assoc :frame-id frame-id) - (assoc :parent-id parent-id) + paste-objects (->> paste-objects + (d/mapm (fn [_ shape] + (-> shape + (assoc :frame-id frame-id) + (assoc :parent-id parent-id) - (cond-> - ;; if foreign instance, detach the shape - (foreign-instance? shape objects state) - (dissoc :component-id - :component-file - :component-root? - :remote-synced? - :shape-ref - :touched)))))) + (cond-> + ;; if foreign instance, detach the shape + (foreign-instance? shape paste-objects state) + (dissoc :component-id + :component-file + :component-root? + :remote-synced? + :shape-ref + :touched)))))) page-id (:current-page-id state) unames (-> (wsh/lookup-page-objects state page-id) (dwc/retrieve-used-names)) ;; TODO: move this calculation inside prepare-duplcate-changes? - rchanges (->> (dws/prepare-duplicate-changes objects page-id unames selected delta) + rchanges (->> (dws/prepare-duplicate-changes all-objects page-id unames selected delta) (mapv (partial process-rchange media-idx)) - (mapv (partial change-add-obj-index objects selected index))) + (mapv (partial change-add-obj-index paste-objects selected index))) uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) (reverse rchanges)) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 3c2e7df94..ce5ff4160 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -12,6 +12,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.types.interactions :as cti] [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] @@ -288,12 +289,17 @@ fit." [objects page-id unames ids delta] (let [unames (volatile! unames) - update-unames! (fn [new-name] (vswap! unames conj new-name))] + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce (fn [ids-set id] + (into ids-set (cons id (cp/get-children id objects)))) + #{} + ids) + ids-map (into {} (map #(vector % (uuid/next)) all-ids))] (loop [ids (seq ids) chgs []] (if ids (let [id (first ids) - result (prepare-duplicate-change objects page-id unames update-unames! id delta) + result (prepare-duplicate-change objects page-id unames update-unames! ids-map id delta) result (if (vector? result) result [result])] (recur (next ids) @@ -313,22 +319,27 @@ (-> changes (update-indices index-map)))) (defn- prepare-duplicate-change - [objects page-id unames update-unames! id delta] + [objects page-id unames update-unames! ids-map id delta] (let [obj (get objects id)] (if (= :frame (:type obj)) - (prepare-duplicate-frame-change objects page-id unames update-unames! obj delta) - (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta (:frame-id obj) (:parent-id obj))))) + (prepare-duplicate-frame-change objects page-id unames update-unames! ids-map obj delta) + (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj))))) (defn- prepare-duplicate-shape-change - [objects page-id unames update-unames! obj delta frame-id parent-id] + [objects page-id unames update-unames! ids-map obj delta frame-id parent-id] (when (some? obj) - (let [id (uuid/next) + (let [new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) name (dwc/generate-unique-name @unames (:name obj)) _ (update-unames! name) - renamed-obj (assoc obj :id id :name name) - moved-obj (geom/move renamed-obj delta) - parent-id (or parent-id frame-id) + new-obj (-> obj + (assoc :id new-id + :name name + :frame-id frame-id) + (dissoc :shapes) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) children-changes (loop [result [] @@ -337,47 +348,45 @@ (if (nil? cid) result (let [obj (get objects cid) - changes (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta frame-id id)] + changes (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta frame-id new-id)] (recur (into result changes) (first cids) - (rest cids))))) + (rest cids)))))] - reframed-obj (-> moved-obj - (assoc :frame-id frame-id) - (dissoc :shapes))] (into [{:type :add-obj - :id id + :id new-id :page-id page-id :old-id (:id obj) :frame-id frame-id :parent-id parent-id :ignore-touched true - :obj (dissoc reframed-obj :shapes)}] + :obj new-obj}] children-changes)))) (defn- prepare-duplicate-frame-change - [objects page-id unames update-unames! obj delta] - (let [frame-id (uuid/next) + [objects page-id unames update-unames! ids-map obj delta] + (let [new-id (ids-map (:id obj)) frame-name (dwc/generate-unique-name @unames (:name obj)) _ (update-unames! frame-name) sch (->> (map #(get objects %) (:shapes obj)) - (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! % delta frame-id frame-id))) + (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map % delta new-id new-id))) - frame (-> obj - (assoc :id frame-id) - (assoc :name frame-name) - (assoc :frame-id uuid/zero) - (assoc :shapes []) - (geom/move delta)) + new-frame (-> obj + (assoc :id new-id + :name frame-name + :frame-id uuid/zero + :shapes []) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) fch {:type :add-obj :old-id (:id obj) :page-id page-id - :id frame-id + :id new-id :frame-id uuid/zero - :obj frame}] + :obj new-frame}] (into [fch] sch))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 74a2d08c9..881b95236 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -377,7 +377,7 @@ [:& page-flows {:flows flows}]) [:div.element-set.interactions-options - (when (and shape (not= (:frame-id shape) uuid/zero)) + (when (and shape (not (cp/unframed-shape? shape))) [:div.element-set-title [:span (tr "workspace.options.interactions")] [:div.add-page {:on-click add-interaction} @@ -385,7 +385,7 @@ [:div.element-set-content (when (= (count interactions) 0) [:* - (when (and shape (not= (:frame-id shape) uuid/zero)) + (when (and shape (not (cp/unframed-shape? shape))) [:* [:div.interactions-help-icon i/plus] [:div.interactions-help.separator (tr "workspace.options.add-interaction")]]) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 3072bc40a..ed2300b65 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -8,9 +8,8 @@ "Visually show shape interactions in workspace" (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.pages :as cp] [app.common.types.interactions :as cti] - [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] @@ -210,7 +209,7 @@ (st/emit! (dw/start-move-overlay-pos index)))] (when dest-shape - (let [orig-frame (cph/get-frame orig-shape objects) + (let [orig-frame (cp/get-frame orig-shape objects) marker-x (+ (:x orig-frame) (:x position)) marker-y (+ (:y orig-frame) (:y position)) width (:width dest-shape) @@ -320,7 +319,8 @@ :position (:overlay-position interaction) :objects objects :hover-disabled? hover-disabled?}]))]))) - (when (and (not= (:frame-id shape) uuid/zero) + (when (and shape + (not (cp/unframed-shape? shape)) (not (#{:move :rotate} current-transform))) [:& interaction-handle {:key (:id shape) :index nil