0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-16 08:51:32 -05:00

🎉 Link with components of other files

This commit is contained in:
Andrés Moya 2020-09-10 15:42:11 +02:00
parent f837bad894
commit 1ad9a7f82f
11 changed files with 329 additions and 96 deletions

View file

@ -44,6 +44,9 @@
(integer? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def ::component-id uuid?)
(s/def ::component-file uuid?)
(s/def ::shape-ref uuid?)
(s/def ::safe-number
#(and
@ -216,7 +219,10 @@
(s/def ::shape
(s/and ::minimal-shape ::shape-attrs
(s/keys :opt-un [::id])))
(s/keys :opt-un [::id
::component-id
::component-file
::shape-ref])))
(s/def :internal.page/objects (s/map-of uuid? ::shape))
@ -363,7 +369,7 @@
(s/keys :req-un [::id]))
(defmethod change-spec :update-component [_]
(s/keys :req-un [::id ::shapes]))
(s/keys :req-un [::id ::name ::shapes]))
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change))
@ -777,50 +783,12 @@
[data {:keys [id]}]
(d/dissoc-in data [:components id]))
(declare sync-component-shape)
(defmethod process-change :update-component
[data {:keys [id shapes]}]
(let [sync-component
(fn [component]
(update component :objects
#(d/mapm (partial sync-component-shape shapes) %)))]
(update-in data [:components id] sync-component)))
(defn- sync-component-shape
[new-shapes _ component-shape]
(let [shape (d/seek #(= (:shape-ref %) (:id component-shape)) new-shapes)]
(if (nil? shape)
component-shape
(-> component-shape
(d/assoc-when :content (:content shape))
(d/assoc-when :fill-color (:fill-color shape))
(d/assoc-when :fill-color-ref-file (:fill-color-ref-file shape))
(d/assoc-when :fill-color-ref-id (:fill-color-ref-id shape))
(d/assoc-when :fill-opacity (:fill-opacity shape))
(d/assoc-when :font-family (:font-family shape))
(d/assoc-when :font-size (:font-size shape))
(d/assoc-when :font-style (:font-style shape))
(d/assoc-when :font-weight (:font-weight shape))
(d/assoc-when :letter-spacing (:letter-spacing shape))
(d/assoc-when :line-height (:line-height shape))
(d/assoc-when :proportion (:proportion shape))
(d/assoc-when :rx (:rx shape))
(d/assoc-when :ry (:ry shape))
(d/assoc-when :stroke-color (:stroke-color shape))
(d/assoc-when :stroke-color-ref-file (:stroke-color-ref-file shape))
(d/assoc-when :stroke-color-ref-id (:stroke-color-ref-id shape))
(d/assoc-when :stroke-opacity (:stroke-opacity shape))
(d/assoc-when :stroke-style (:stroke-style shape))
(d/assoc-when :stroke-width (:stroke-width shape))
(d/assoc-when :stroke-alignment (:stroke-alignment shape))
(d/assoc-when :text-align (:text-align shape))
(d/assoc-when :width (:width shape))
(d/assoc-when :height (:height shape))
(d/assoc-when :interactions (:interactions shape))
(d/assoc-when :selrect (:selrect shape))
(d/assoc-when :points (:points shape))))))
[data {:keys [id name shapes]}]
(update-in data [:components id]
#(assoc %
:name name
:objects (d/index-by :id shapes))))
(defmethod process-operation :set
[shape op]

View file

@ -174,6 +174,7 @@
Returns the cloned object, the list of all new objects (including
the cloned one), and possibly a list of original objects modified."
([object parent-id objects xf-new-object]
(clone-object object parent-id objects xf-new-object identity))

View file

@ -176,9 +176,7 @@
grid-auto-rows: 10vh;
.grid-cell {
background-color: transparent;
border: 1px solid $color-gray-40;
border-radius: 4px;
padding: $x-small;
& svg {
height: 10vh;

View file

@ -111,7 +111,7 @@
.element-list li.component {
.element-list-body {
.element-name {
span.element-name {
color: $color-component;
}
@ -120,7 +120,7 @@
}
&.selected {
.element-name {
span.element-name {
color: $color-component-highlight;
}
@ -132,7 +132,7 @@
&:hover {
background-color: $color-component-highlight;
.element-name {
span.element-name {
color: $color-gray-60;
}

View file

@ -28,7 +28,7 @@
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.colors :as dwl]
[app.main.data.colors :as mdc]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
@ -1130,8 +1130,14 @@
(ptk/reify ::show-context-menu
ptk/UpdateEvent
(update [_ state]
(let [mdata {:position position
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component (:id shape) objects)
root-shape (get objects root-id)
mdata {:position position
:shape shape
:root-shape root-shape
:selected (get-in state [:workspace-local :selected])}]
(-> state
(assoc-in [:workspace-local :context-menu] mdata))))
@ -1467,5 +1473,5 @@
"right" #(st/emit! (dwt/move-selected :right false))
"left" #(st/emit! (dwt/move-selected :left false))
"i" #(st/emit! (dwl/picker-for-selected-shape ))})
"i" #(st/emit! (mdc/picker-for-selected-shape ))})

View file

@ -23,6 +23,7 @@
[app.main.streams :as ms]
[app.util.color :as color]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
@ -147,6 +148,9 @@
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}]})
@ -164,6 +168,9 @@
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
@ -192,15 +199,14 @@
(defn delete-component
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
:id id}
{:type :sync-library
:id (get-in state [:workspace-file :id])}]
:id id}]
uchanges [{:type :add-component
:id id
@ -210,12 +216,15 @@
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn instantiate-component
[id]
(us/assert ::us/uuid id)
[file-id component-id]
(us/assert (s/nilable ::us/uuid) file-id)
(us/assert ::us/uuid component-id)
(ptk/reify ::instantiate-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
(let [component (if (nil? file-id)
(get-in state [:workspace-data :components component-id])
(get-in state [:workspace-libraries file-id :data :components component-id]))
component-shape (get-in component [:objects (:id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
@ -228,14 +237,16 @@
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
unames (dwc/retrieve-used-names objects)
unames (atom (dwc/retrieve-used-names objects))
all-frames (cph/select-frames objects)
xf-new-shape
(fn [new-shape original-shape]
(let [new-name ;; TODO: ojoooooooooo
(dwc/generate-unique-name unames (:name new-shape))]
(let [new-name
(dwc/generate-unique-name @unames (:name new-shape))]
(swap! unames conj new-name)
(cond-> new-shape
true
@ -249,7 +260,10 @@
(assoc $ :shape-ref (:id original-shape)))
(nil? (:parent-id original-shape))
(assoc :component-id (:id original-shape)))))
(assoc :component-id (:id original-shape))
(and (nil? (:parent-id original-shape)) (some? file-id))
(assoc :component-file file-id))))
[new-shape new-shapes _]
(cph/clone-object component-shape
@ -294,6 +308,9 @@
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
@ -306,6 +323,9 @@
:operations [{:type :set
:attr :component-id
:val (:component-id obj)}
{:type :set
:attr :component-file
:val (:component-file obj)}
{:type :set
:attr :shape-ref
:val (:shape-ref obj)}]})
@ -313,17 +333,51 @@
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state stream]
(let [file (get-in state [:workspace-libraries file-id])
pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams))))))
(declare generate-sync-file)
(declare generate-sync-page)
(declare generate-sync-shape-and-children)
(declare generate-sync-shape)
(declare remove-component-and-ref)
(declare remove-ref)
(declare update-attrs)
(declare sync-attrs)
(defn reset-component
[id]
[id]
(us/assert ::us/uuid id)
(ptk/reify ::reset-component
ptk/WatchEvent
(watch [_ state stream]
)))
(let [page-id (:current-page-id state)
page (get-in state [:workspace-data :pages-index page-id])
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects)
root-shape (get objects id)
file-id (get root-shape :component-file)
components
(if (nil? file-id)
(get-in state [:workspace-data :components])
(get-in state [:workspace-libraries file-id :data :components]))
[rchanges uchanges]
(generate-sync-shape-and-children root-shape page components)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn update-component
[id]
[id]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
@ -333,21 +387,202 @@
objects (dwc/lookup-page-objects state page-id)
root-id (cph/get-root-component id objects)
root-shape (get objects id)
component-id (get root-shape :component-id)
component-objs (dwc/lookup-component-objects state component-id)
component-obj (get component-objs component-id)
shapes (cph/get-object-with-children root-id objects)
;; Clone again the original shape and its children, maintaing
;; the ids of the cloned shapes. If the original shape has some
;; new child shapes, the cloned ones will have new generated ids.
xf-new-shape (fn [new-shape original-shape]
(cond-> new-shape
true
(assoc :frame-id nil)
(some? (:shape-ref original-shape))
(assoc :id (:shape-ref original-shape))))
[new-shape new-shapes _]
(cph/clone-object root-shape nil objects xf-new-shape)
rchanges [{:type :update-component
:id component-id
:shapes shapes}
{:type :sync-library
:id (get-in state [:workspace-file :id])}]
:name (:name new-shape)
:shapes new-shapes}]
uchanges [{:type :update-component
:id component-id
:name (:name component-obj)
:shapes (vals component-objs)}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn sync-file
[{:keys [file-id] :as params}]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file
ptk/WatchEvent
(watch [_ state stream]
(let [[rchanges uchanges] (generate-sync-file state file-id)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn- generate-sync-file
[state file-id]
(let [components
(if (nil? file-id)
(get-in state [:workspace-data :components])
(get-in state [:workspace-libraries file-id :data :components]))]
(loop [pages (seq (vals (get-in state [:workspace-data :pages-index])))
rchanges []
uchanges []]
(let [page (first pages)]
(if (nil? page)
[rchanges uchanges]
(let [[page-rchanges page-uchanges]
(generate-sync-page page components)]
(recur (next pages)
(concat rchanges page-rchanges)
(concat uchanges page-uchanges))))))))
(defn- generate-sync-page
[page components]
(let [linked-shapes
(cph/select-objects #(some? (:component-id %)) page)]
(loop [shapes (seq linked-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape-and-children shape page components)]
(recur (next shapes)
(concat rchanges shape-rchanges)
(concat uchanges shape-uchanges))))))))
(defn- generate-sync-shape-and-children
[root-shape page components]
(let [objects (get page :objects)
all-shapes (cph/get-object-with-children (:id root-shape) objects)
component (get components (:component-id root-shape))]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape shape page component)]
(recur (next shapes)
(concat rchanges shape-rchanges)
(concat uchanges shape-uchanges))))))))
(defn- generate-sync-shape
[shape page component]
(if (nil? component)
(remove-component-and-ref shape page)
(let [component-shape (get (:objects component) (:shape-ref shape))]
(if (nil? component-shape)
(remove-ref shape page)
(update-attrs shape component-shape page)))))
(defn- remove-component-and-ref
[shape page]
[[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]}]
[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :component-id
:val (:component-id shape)}
{:type :set
:attr :component-file
:val (:component-file shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref shape)}]}]])
(defn- remove-ref
[shape page]
[[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :shape-ref
:val nil}]}]
[{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations [{:type :set
:attr :shape-ref
:val (:shape-ref shape)}]}]])
(defn- update-attrs
[shape component-shape page]
(loop [attrs (seq sync-attrs)
roperations []
uoperations []]
(let [attr (first attrs)]
(if (nil? attr)
(let [rchanges [{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations roperations}]
uchanges [{:type :mod-obj
:page-id (:id page)
:id (:id shape)
:operations uoperations}]]
[rchanges uchanges])
(if-not (contains? shape attr)
(recur (next attrs)
roperations
uoperations)
(let [roperation {:type :set
:attr attr
:val (get component-shape attr)}
uoperation {:type :set
:attr attr
:val (get shape attr)}]
(recur (next attrs)
(conj roperations roperation)
(conj uoperations uoperation))))))))
(def sync-attrs [:content
:fill-color
:fill-color-ref-file
:fill-color-ref-id
:fill-opacity
:font-family
:font-size
:font-style
:font-weight
:letter-spacing
:line-height
:proportion
:rx
:ry
:stroke-color
:stroke-color-ref-file
:stroke-color-ref-id
:stroke-opacity
:stroke-style
:stroke-width
:stroke-alignment
:text-align
:width
:height
:interactions
:points])

View file

@ -46,6 +46,7 @@
[{:keys [mdata] :as props}]
(let [{:keys [id] :as shape} (:shape mdata)
selected (:selected mdata)
root-shape (:root-shape mdata)
do-duplicate #(st/emit! dw/duplicate-selected)
do-delete #(st/emit! dw/delete-selected)
@ -64,7 +65,11 @@
do-add-component #(st/emit! dwl/add-component)
do-detach-component #(st/emit! (dwl/detach-component id))
do-reset-component #(st/emit! (dwl/reset-component id))
do-update-component #(st/emit! (dwl/update-component id))]
do-update-component #(do
(st/emit! (dwl/update-component id))
(st/emit! (dwl/sync-file {:file-id nil})))
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
(:component-file root-shape)))]
[:*
[:& menu-entry {:title "Copy"
:shortcut "Ctrl + c"
@ -123,8 +128,11 @@
:on-click do-detach-component}]
[:& menu-entry {:title "Reset overrides"
:on-click do-reset-component}]
[:& menu-entry {:title "Update master component"
:on-click do-update-component}]])
(if (nil? (:component-file root-shape))
[:& menu-entry {:title "Update master component"
:on-click do-update-component}]
[:& menu-entry {:title "Go to master component file"
:on-click do-navigate-component-file}])])
[:& menu-separator]
[:& menu-entry {:title "Delete"

View file

@ -50,8 +50,8 @@
(mf/use-callback
(mf/deps state)
(fn []
(let [params {:id (:component-id @state)}]
(st/emit! (dwl/delete-component params)))))
(st/emit! (dwl/delete-component {:id (:component-id @state)}))
(st/emit! (dwl/sync-file {:file-id nil}))))
on-context-menu
(mf/use-callback
@ -70,7 +70,8 @@
on-drag-start
(mf/use-callback
(fn [component-id event]
(dnd/set-data! event "app/component" component-id)
(dnd/set-data! event "app/component" {:file-id (if local? nil file-id)
:component-id component-id})
(dnd/set-allowed-effect! event "move")))]
[:div.asset-group
@ -363,26 +364,26 @@
(mf/defc file-library
[{:keys [file local? open? filters locale] :as props}]
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
toggles (mf/use-state #{:graphics :colors})
toggles (mf/use-state #{:graphics :colors})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)
components-ref (mf/use-memo (mf/deps (:id file)) #(file-components-ref (:id file)))
components (apply-filters (mf/deref components-ref) filters)]
components-ref (mf/use-memo (mf/deps (:id file)) #(file-components-ref (:id file)))
components (apply-filters (mf/deref components-ref) filters)]
[:div.tool-window
[:div.tool-window-bar

View file

@ -297,6 +297,8 @@
:content
:parent-id
:component-id
:component-file
:shape-ref
:metadata])]
(persistent!
(reduce-kv (fn [res id obj]

View file

@ -495,8 +495,8 @@
(assoc :y final-y)))))
(dnd/has-type? event "app/component")
(let [component-id (dnd/get-data event "app/component")]
(st/emit! (dwl/instantiate-component component-id)))
(let [{:keys [component-id file-id]} (dnd/get-data event "app/component")]
(st/emit! (dwl/instantiate-component file-id component-id)))
(dnd/has-type? event "text/uri-list")
(let [data (dnd/get-data event "text/uri-list")

View file

@ -16,6 +16,7 @@
[potok.core :as ptk]
[reitit.core :as r]
[app.common.data :as d]
[app.config :as cfg]
[app.util.browser-history :as bhistory]
[app.util.timers :as ts])
(:import
@ -112,6 +113,19 @@
(def navigate nav)
(deftype NavigateNewWindow [id params qparams]
ptk/EffectEvent
(effect [_ state stream]
(let [router (:router state)
path (resolve router id params qparams)
uri (str cfg/public-uri "/#" path)]
(js/window.open uri "_blank"))))
(defn nav-new-window
([id] (nav-new-window id nil nil))
([id params] (nav-new-window id params nil))
([id params qparams] (NavigateNewWindow. id params qparams)))
;; --- History API
(defn initialize-history