diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index fba45543b..7f4b64114 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -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] diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc index 1754665ff..b11a93cc2 100644 --- a/common/app/common/pages_helpers.cljc +++ b/common/app/common/pages_helpers.cljc @@ -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)) diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 2324c5ade..d4d6ff4ed 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -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; diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index 9a53caeb6..ea8e23fd4 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -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; } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 4bf7debbd..6db575380 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 ))}) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 0fb5d891b..7fddac187 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -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]) + diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 7669046bc..487708bee 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -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" diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 832394835..79a51e1da 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 77fcd0e24..41c928852 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -297,6 +297,8 @@ :content :parent-id :component-id + :component-file + :shape-ref :metadata])] (persistent! (reduce-kv (fn [res id obj] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 224eecda4..72a64ed16 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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") diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index 8d6d8707e..067c44e5f 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -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