Refactor state management of workspace header

This commit is contained in:
Andrey Antukh 2023-05-15 13:34:18 +02:00 committed by Alejandro Alonso
parent fcc4f4eed8
commit bdb0e24c40
3 changed files with 524 additions and 355 deletions

View file

@ -60,8 +60,8 @@
(c/update ::modal merge options))))) (c/update ::modal merge options)))))
(defn show! (defn show!
[type props] ([props] (st/emit! (show props)))
(st/emit! (show type props))) ([type props] (st/emit! (show type props))))
(defn update-props! (defn update-props!
[type props] [type props]

View file

@ -37,7 +37,7 @@
[potok.core :as ptk] [potok.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def workspace-persistence-ref (def ref:workspace-persistence
(l/derived :workspace-persistence st/state)) (l/derived :workspace-persistence st/state))
;; --- Persistence state Widget ;; --- Persistence state Widget
@ -45,57 +45,71 @@
(mf/defc persistence-state-widget (mf/defc persistence-state-widget
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[] []
(let [data (mf/deref workspace-persistence-ref)] (let [{:keys [status]} (mf/deref ref:workspace-persistence)]
[:div.persistence-status-widget [:div.persistence-status-widget
(cond (case status
(= :pending (:status data)) :pending
[:div.pending [:div.pending
[:span.label (tr "workspace.header.unsaved")]] [:span.label (tr "workspace.header.unsaved")]]
(= :saving (:status data)) :saving
[:div.saving [:div.saving
[:span.icon i/toggle] [:span.icon i/toggle]
[:span.label (tr "workspace.header.saving")]] [:span.label (tr "workspace.header.saving")]]
(= :saved (:status data)) :saved
[:div.saved [:div.saved
[:span.icon i/tick] [:span.icon i/tick]
[:span.label (tr "workspace.header.saved")]] [:span.label (tr "workspace.header.saved")]]
(= :error (:status data)) :error
[:div.error {:title "There was an error saving the data. Please refresh if this persists."} [:div.error {:title "There was an error saving the data. Please refresh if this persists."}
[:span.icon i/msg-warning] [:span.icon i/msg-warning]
[:span.label (tr "workspace.header.save-error")]])])) [:span.label (tr "workspace.header.save-error")]]
nil)]))
;; --- Zoom Widget ;; --- Zoom Widget
(mf/defc zoom-widget-workspace (mf/defc zoom-widget-workspace
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]
[{:keys [zoom ::mf/wrap-props false}
[{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}]
(let [open* (mf/use-state false)
open? (deref open*)
open-dropdown
(mf/use-fn #(reset! open* true))
close-dropdown
(mf/use-fn #(reset! open* false))
on-increase on-increase
(mf/use-fn
(mf/deps on-increase)
(fn [event]
(dom/stop-propagation event)
(on-increase)))
on-decrease on-decrease
on-zoom-reset (mf/use-fn
on-zoom-fit (mf/deps on-decrease)
on-zoom-selected] (fn [event]
:as props}] (dom/stop-propagation event)
(let [show-dropdown? (mf/use-state false)] (on-decrease)))
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
[:span.label (fmt/format-percent zoom {:precision 0})] zoom (fmt/format-percent zoom {:precision 0})]
[:div.zoom-widget {:on-click open-dropdown}
[:span.label zoom]
[:span.icon i/arrow-down] [:span.icon i/arrow-down]
[:& dropdown {:show @show-dropdown? [:& dropdown {:show open? :on-close close-dropdown}
:on-close #(reset! show-dropdown? false)}
[:ul.dropdown [:ul.dropdown
[:li.basic-zoom-bar [:li.basic-zoom-bar
[:span.zoom-btns [:span.zoom-btns
[:button {:on-click (fn [event] [:button {:on-click on-decrease} "-"]
(dom/stop-propagation event) [:p.zoom-size zoom]
(dom/prevent-default event) [:button {:on-click on-increase} "+"]]
(on-decrease))} "-"]
[:p.zoom-size {} (fmt/format-percent zoom {:precision 0})]
[:button {:on-click (fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(on-increase))} "+"]]
[:button.reset-btn {:on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]] [:button.reset-btn {:on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]]
[:li.separator] [:li.separator]
[:li {:on-click on-zoom-fit} [:li {:on-click on-zoom-fit}
@ -103,71 +117,276 @@
[:li {:on-click on-zoom-selected} [:li {:on-click on-zoom-selected}
(tr "workspace.header.zoom-selected") [:span (sc/get-tooltip :zoom-selected)]]]]])) (tr "workspace.header.zoom-selected") [:span (sc/get-tooltip :zoom-selected)]]]]]))
;; --- Header Users ;; --- Header Users
;; FIXME: refactor & optimizations (mf/defc help-info-menu
(mf/defc menu {::mf/wrap-props false
[{:keys [layout project file team-id] :as props}] ::mf/wrap [mf/memo]}
(let [show-menu? (mf/use-state false) [{:keys [layout on-close]}]
show-sub-menu? (mf/use-state false) (let [nav-to-helpc-center
editing? (mf/use-state false) (mf/use-fn #(dom/open-new-window "https://help.penpot.app"))
edit-input-ref (mf/use-ref nil)
nav-to-community
(mf/use-fn #(dom/open-new-window "https://community.penpot.app"))
nav-to-youtube
(mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot"))
nav-to-templates
(mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates"))
nav-to-github
(mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot"))
nav-to-terms
(mf/use-fn #(dom/open-new-window "https://penpot.app/terms"))
nav-to-feedback
(mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback})))
show-shortcuts
(mf/use-fn
(mf/deps layout)
(fn []
(when (contains? layout :collapse-left-sidebar)
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(st/emit!
(-> (dw/toggle-layout-flag :shortcuts)
(vary-meta assoc ::ev/origin "workspace-header")))))
show-release-notes
(mf/use-fn
(fn [event]
(let [version (:main @cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding}))
(st/emit! (modal/show {:type :release-notes :version version}))))))
]
[:& dropdown {:show true :on-close on-close}
[:ul.sub-menu.help-info
[:li {:on-click nav-to-helpc-center}
[:span (tr "labels.help-center")]]
[:li {:on-click nav-to-community}
[:span (tr "labels.community")]]
[:li {:on-click nav-to-youtube}
[:span (tr "labels.tutorials")]]
[:li {:on-click show-release-notes}
[:span (tr "labels.release-notes")]]
[:li.separator {:on-click nav-to-templates}
[:span (tr "labels.libraries-and-templates")]]
[:li {:on-click nav-to-github}
[:span (tr "labels.github-repo")]]
[:li {:on-click nav-to-terms}
[:span (tr "auth.terms-of-service")]]
[:li.separator {:on-click show-shortcuts}
[:span (tr "label.shortcuts")]
[:span.shortcut (sc/get-tooltip :show-shortcuts)]]
(when (contains? @cf/flags :user-feedback)
[:*
[:li.feedback {:on-click nav-to-feedback}
[:span (tr "labels.give-feedback")]]])]]))
(mf/defc preferences-menu
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[{:keys [layout toggle-flag on-close]}]
(let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))]
[:& dropdown {:show true :on-close on-close}
[:ul.sub-menu.preferences
[:li {:on-click toggle-flag
:data-flag "scale-text"}
[:span
(if (contains? layout :scale-text)
(tr "workspace.header.menu.disable-scale-content")
(tr "workspace.header.menu.enable-scale-content"))]
[:span.shortcut (sc/get-tooltip :toggle-scale-text)]]
[:li {:on-click toggle-flag
:data-flag "snap-guides"}
[:span
(if (contains? layout :snap-guides)
(tr "workspace.header.menu.disable-snap-guides")
(tr "workspace.header.menu.enable-snap-guides"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-guide)]]
[:li {:on-click toggle-flag
:data-flag "snap-grid"}
[:span
(if (contains? layout :snap-grid)
(tr "workspace.header.menu.disable-snap-grid")
(tr "workspace.header.menu.enable-snap-grid"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
[:li {:on-click toggle-flag
:data-flag "dynamic-alignment"}
[:span
(if (contains? layout :dynamic-alignment)
(tr "workspace.header.menu.disable-dynamic-alignment")
(tr "workspace.header.menu.enable-dynamic-alignment"))]
[:span.shortcut (sc/get-tooltip :toggle-alignment)]]
[:li {:on-click toggle-flag
:data-flag "snap-pixel-grid"}
[:span
(if (contains? layout :snap-pixel-grid)
(tr "workspace.header.menu.disable-snap-pixel-grid")
(tr "workspace.header.menu.enable-snap-pixel-grid"))]
[:span.shortcut (sc/get-tooltip :snap-pixel-grid)]]
[:li {:on-click show-nudge-options}
[:span (tr "modals.nudge-title")]]]]))
(mf/defc view-menu
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[{:keys [layout toggle-flag on-close]}]
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
toggle-color-palette
(mf/use-fn
(fn []
(r/set-resize-type! :bottom)
(st/emit! (dw/remove-layout-flag :textpalette)
(-> (dw/toggle-layout-flag :colorpalette)
(vary-meta assoc ::ev/origin "workspace-menu")))))
toggle-text-palette
(mf/use-fn
(fn []
(r/set-resize-type! :bottom)
(st/emit! (dw/remove-layout-flag :colorpalette)
(-> (dw/toggle-layout-flag :textpalette)
(vary-meta assoc ::ev/origin "workspace-menu")))))]
[:& dropdown {:show true :on-close on-close}
[:ul.sub-menu.view
[:li {:on-click toggle-flag
:data-flag "rules"}
[:span
(if (contains? layout :rules)
(tr "workspace.header.menu.hide-rules")
(tr "workspace.header.menu.show-rules"))]
[:span.shortcut (sc/get-tooltip :toggle-rules)]]
[:li {:on-click toggle-flag
:data-flag "display-grid"}
[:span
(if (contains? layout :display-grid)
(tr "workspace.header.menu.hide-grid")
(tr "workspace.header.menu.show-grid"))]
[:span.shortcut (sc/get-tooltip :toggle-grid)]]
(when-not ^boolean read-only?
[:*
[:li {:on-click toggle-color-palette}
[:span
(if (contains? layout :colorpalette)
(tr "workspace.header.menu.hide-palette")
(tr "workspace.header.menu.show-palette"))]
[:span.shortcut (sc/get-tooltip :toggle-colorpalette)]]
[:li {:on-click toggle-text-palette}
[:span
(if (contains? layout :textpalette)
(tr "workspace.header.menu.hide-textpalette")
(tr "workspace.header.menu.show-textpalette"))]
[:span.shortcut (sc/get-tooltip :toggle-textpalette)]]])
[:li {:on-click toggle-flag
:data-flag "display-artboard-names"}
[:span
(if (contains? layout :display-artboard-names)
(tr "workspace.header.menu.hide-artboard-names")
(tr "workspace.header.menu.show-artboard-names"))]]
[:li {:on-click toggle-flag
:data-flag "show-pixel-grid"}
[:span
(if (contains? layout :show-pixel-grid)
(tr "workspace.header.menu.hide-pixel-grid")
(tr "workspace.header.menu.show-pixel-grid"))]
[:span.shortcut (sc/get-tooltip :show-pixel-grid)]]
[:li {:on-click toggle-flag
:data-flag "hide-ui"}
[:span
(tr "workspace.shape.menu.hide-ui")]
[:span.shortcut (sc/get-tooltip :hide-ui)]]]]))
(mf/defc edit-menu
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[{:keys [on-close]}]
(let [select-all (mf/use-fn #(st/emit! (dw/select-all)))
undo (mf/use-fn #(st/emit! dwc/undo))
redo (mf/use-fn #(st/emit! dwc/redo))]
[:& dropdown {:show true :on-close on-close}
[:ul.sub-menu.edit
[:li {:on-click select-all}
[:span (tr "workspace.header.menu.select-all")]
[:span.shortcut (sc/get-tooltip :select-all)]]
[:li {:on-click undo}
[:span (tr "workspace.header.menu.undo")]
[:span.shortcut (sc/get-tooltip :undo)]]
[:li {:on-click redo}
[:span (tr "workspace.header.menu.redo")]
[:span.shortcut (sc/get-tooltip :redo)]]]]))
(mf/defc file-menu
{::mf/wrap-props false}
[{:keys [on-close file team-id]}]
(let [file-id (:id file)
file-name (:name file)
shared? (:is-shared file)
objects (mf/deref refs/workspace-page-objects) objects (mf/deref refs/workspace-page-objects)
frames (->> (cph/get-immediate-children objects uuid/zero) frames (->> (cph/get-immediate-children objects uuid/zero)
(filterv cph/frame-shape?)) (filterv cph/frame-shape?))
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
add-shared-fn add-shared-fn
#(st/emit! (dwl/set-file-shared (:id file) true)) (mf/use-fn
(mf/deps file-id)
#(st/emit! (dwl/set-file-shared file-id true)))
on-add-shared on-add-shared
(mf/use-fn (mf/use-fn
(mf/deps file) (mf/deps file-name add-shared-fn)
#(st/emit! (modal/show #(modal/show! {:type :confirm
{:type :confirm
:message "" :message ""
:title (tr "modals.add-shared-confirm.message" (:name file)) :title (tr "modals.add-shared-confirm.message" file-name)
:hint (tr "modals.add-shared-confirm.hint") :hint (tr "modals.add-shared-confirm.hint")
:cancel-label :omit :cancel-label :omit
:accept-label (tr "modals.add-shared-confirm.accept") :accept-label (tr "modals.add-shared-confirm.accept")
:accept-style :primary :accept-style :primary
:on-accept add-shared-fn}))) :on-accept add-shared-fn}))
on-remove-shared on-remove-shared
(mf/use-fn (mf/use-fn
(mf/deps file) (mf/deps file-id)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (modal/show (modal/show!
{:type :delete-shared-libraries {:type :delete-shared-libraries
:origin :unpublish :origin :unpublish
:ids #{(:id file)} :ids #{file-id}
:on-accept #(st/emit! (dwl/set-file-shared (:id file) false)) :on-accept #(st/emit! (dwl/set-file-shared file-id false))
:count-libraries 1})))) :count-libraries 1})))
handle-blur
(fn [_]
(let [value (str/trim (-> edit-input-ref mf/ref-val dom/get-value))]
(when (not= value "")
(st/emit! (dw/rename-file (:id file) value))))
(reset! editing? false))
handle-name-keydown (fn [event]
(when (kbd/enter? event)
(handle-blur event)))
start-editing-name (fn [event]
(dom/prevent-default event)
(reset! editing? true))
on-export-shapes on-export-shapes
(mf/use-callback (mf/use-fn #(st/emit! (de/show-workspace-export-dialog)))
(fn [_]
(st/emit! (de/show-workspace-export-dialog))))
on-export-file on-export-file
(mf/use-fn
(mf/deps file)
(fn [event-name binary?] (fn [event-name binary?]
(st/emit! (ptk/event ::ev/event {::ev/name event-name (st/emit! (ptk/event ::ev/event {::ev/name event-name
::ev/origin "workspace" ::ev/origin "workspace"
@ -181,8 +400,7 @@
(rx/reduce conj []) (rx/reduce conj [])
(rx/subs (rx/subs
(fn [files] (fn [files]
(st/emit! (modal/show!
(modal/show
{:type :export {:type :export
:team-id team-id :team-id team-id
:has-libraries? (->> files (some :has-libraries?)) :has-libraries? (->> files (some :has-libraries?))
@ -190,103 +408,24 @@
:binary? binary?})))))) :binary? binary?}))))))
on-export-binary-file on-export-binary-file
(mf/use-callback (mf/use-fn
(mf/deps file team-id) (mf/deps on-export-file)
(fn [_] (partial on-export-file "export-binary-files" true))
(on-export-file "export-binary-files" true)))
on-export-standard-file on-export-standard-file
(mf/use-callback (mf/use-fn
(mf/deps file team-id) (mf/deps on-export-file)
(fn [_] (partial on-export-file "export-standard-files" false))
(on-export-file "export-standard-files" false)))
on-export-frames on-export-frames
(mf/use-callback (mf/use-fn
(mf/deps file frames) (mf/deps frames)
(fn [_] (fn [_]
(st/emit! (de/show-workspace-export-frames-dialog (reverse frames))))) (st/emit! (de/show-workspace-export-frames-dialog (reverse frames)))))]
on-item-hover [:& dropdown {:show true :on-close on-close}
(mf/use-callback
(fn [item]
(fn [event]
(dom/stop-propagation event)
(reset! show-sub-menu? item))))
on-item-click
(mf/use-callback
(fn [item]
(fn [event]
(dom/stop-propagation event)
(reset! show-sub-menu? item))))
toggle-flag
(mf/use-callback
(fn [flag]
(-> (dw/toggle-layout-flag flag)
(vary-meta assoc ::ev/origin "workspace-menu"))))
show-release-notes
(mf/use-callback
(fn [event]
(let [version (:main @cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding}))
(st/emit! (modal/show {:type :release-notes :version version}))))))]
(mf/use-effect
(mf/deps @editing?)
#(when @editing?
(dom/select-text! (mf/ref-val edit-input-ref))))
[:div.menu-section
[:div.btn-icon-dark.btn-small {:on-click #(reset! show-menu? true)} i/actions]
[:div.project-tree {:alt (tr "workspace.sitemap")}
[:span.project-name
{:on-click #(st/emit! (rt/nav-new-window* {:rname :dashboard-files
:path-params {:team-id team-id
:project-id (:project-id file)}}))}
(:name project) " /"]
(if @editing?
[:input.file-name
{:type "text"
:ref edit-input-ref
:on-blur handle-blur
:on-key-down handle-name-keydown
:auto-focus true
:default-value (:name file "")}]
[:span
{:on-double-click start-editing-name}
(:name file)])]
(when (:is-shared file)
[:div.shared-badge i/library])
[:& dropdown {:show @show-menu?
:on-close #(reset! show-menu? false)}
[:ul.menu
[:li {:on-click (on-item-click :file)
:on-pointer-enter (on-item-hover :file)}
[:span (tr "workspace.header.menu.option.file")]
[:span i/arrow-slide]]
[:li {:on-click (on-item-click :edit)
:on-pointer-enter (on-item-hover :edit)}
[:span (tr "workspace.header.menu.option.edit")] [:span i/arrow-slide]]
[:li {:on-click (on-item-click :view)
:on-pointer-enter (on-item-hover :view)}
[:span (tr "workspace.header.menu.option.view")] [:span i/arrow-slide]]
[:li {:on-click (on-item-click :preferences)
:on-pointer-enter (on-item-hover :preferences)}
[:span (tr "workspace.header.menu.option.preferences")] [:span i/arrow-slide]]
[:li.info {:on-click (on-item-click :help-info)
:on-pointer-enter (on-item-hover :help-info)}
[:span (tr "workspace.header.menu.option.help-info")] [:span i/arrow-slide]]]]
[:& dropdown {:show (= @show-sub-menu? :file)
:on-close #(reset! show-sub-menu? false)}
[:ul.sub-menu.file [:ul.sub-menu.file
(if (:is-shared file) (if ^boolean shared?
[:li {:on-click on-remove-shared} [:li {:on-click on-remove-shared}
[:span (tr "dashboard.unpublish-shared")]] [:span (tr "dashboard.unpublish-shared")]]
[:li {:on-click on-add-shared} [:li {:on-click on-add-shared}
@ -300,187 +439,217 @@
[:span (tr "dashboard.download-standard-file")]] [:span (tr "dashboard.download-standard-file")]]
(when (seq frames) (when (seq frames)
[:li.separator.export-file {:on-click on-export-frames} [:li.separator.export-file {:on-click on-export-frames}
[:span (tr "dashboard.export-frames")]])]] [:span (tr "dashboard.export-frames")]])]]))
[:& dropdown {:show (= @show-sub-menu? :edit) (mf/defc menu
:on-close #(reset! show-sub-menu? false)} {::mf/wrap-props false}
[:ul.sub-menu.edit [{:keys [layout file team-id]}]
[:li {:on-click #(st/emit! (dw/select-all))} (let [show-menu* (mf/use-state false)
[:span (tr "workspace.header.menu.select-all")] show-menu? (deref show-menu*)
[:span.shortcut (sc/get-tooltip :select-all)]] sub-menu* (mf/use-state false)
sub-menu (deref sub-menu*)
[:li {:on-click #(st/emit! dwc/undo)} open-menu (mf/use-fn #(reset! show-menu* true))
[:span (tr "workspace.header.menu.undo")] close-menu (mf/use-fn #(reset! show-menu* false))
[:span.shortcut (sc/get-tooltip :undo)]] close-sub-menu (mf/use-fn #(reset! sub-menu* nil))
[:li {:on-click #(st/emit! dwc/redo)} on-menu-click
[:span (tr "workspace.header.menu.redo")] (mf/use-fn
[:span.shortcut (sc/get-tooltip :redo)]]]] (fn [event]
(dom/stop-propagation event)
(let [menu (-> (dom/get-target event)
(dom/get-data "menu")
(keyword))]
(reset! sub-menu* menu))))
[:& dropdown {:show (= @show-sub-menu? :view) toggle-flag
:on-close #(reset! show-sub-menu? false)} (mf/use-fn
[:ul.sub-menu.view (fn [event]
[:li {:on-click #(st/emit! (toggle-flag :rules))} (let [flag (-> (dom/get-target event)
[:span (dom/get-data :flag)
(if (contains? layout :rules) (keyword))]
(tr "workspace.header.menu.hide-rules") (st/emit!
(tr "workspace.header.menu.show-rules"))] (-> (dw/toggle-layout-flag flag)
[:span.shortcut (sc/get-tooltip :toggle-rules)]] (vary-meta assoc ::ev/origin "workspace-menu"))))))]
[:li {:on-click #(st/emit! (toggle-flag :display-grid))}
[:span
(if (contains? layout :display-grid)
(tr "workspace.header.menu.hide-grid")
(tr "workspace.header.menu.show-grid"))]
[:span.shortcut (sc/get-tooltip :toggle-grid)]]
(when-not workspace-read-only?
[:* [:*
[:li {:on-click (fn [] [:div.btn-icon-dark.btn-small {:on-click open-menu} i/actions]
(r/set-resize-type! :bottom)
(st/emit! (dw/remove-layout-flag :textpalette)
(toggle-flag :colorpalette)))}
[:span
(if (contains? layout :colorpalette)
(tr "workspace.header.menu.hide-palette")
(tr "workspace.header.menu.show-palette"))]
[:span.shortcut (sc/get-tooltip :toggle-colorpalette)]]
[:li {:on-click (fn [] [:& dropdown {:show show-menu? :on-close close-menu}
(r/set-resize-type! :bottom) [:ul.menu
(st/emit! (dw/remove-layout-flag :colorpalette) [:li {:on-click on-menu-click
(toggle-flag :textpalette)))} :on-pointer-enter on-menu-click
[:span :data-menu "file"}
(if (contains? layout :textpalette) [:span (tr "workspace.header.menu.option.file")]
(tr "workspace.header.menu.hide-textpalette") [:span i/arrow-slide]]
(tr "workspace.header.menu.show-textpalette"))] [:li {:on-click on-menu-click
[:span.shortcut (sc/get-tooltip :toggle-textpalette)]]]) :on-pointer-enter on-menu-click
:data-menu "edit"}
[:span (tr "workspace.header.menu.option.edit")]
[:span i/arrow-slide]]
[:li {:on-click on-menu-click
:on-pointer-enter on-menu-click
:data-menu :view}
[:span (tr "workspace.header.menu.option.view")]
[:span i/arrow-slide]]
[:li {:on-click on-menu-click
:on-pointer-enter on-menu-click
:data-menu "preferences"}
[:span (tr "workspace.header.menu.option.preferences")]
[:span i/arrow-slide]]
[:li.info {:on-click on-menu-click
:on-pointer-enter on-menu-click
:data-menu "help-info"}
[:span (tr "workspace.header.menu.option.help-info")]
[:span i/arrow-slide]]]]
[:li {:on-click #(st/emit! (toggle-flag :display-artboard-names))} (case sub-menu
[:span :file
(if (contains? layout :display-artboard-names) [:& file-menu
(tr "workspace.header.menu.hide-artboard-names") {:file file
(tr "workspace.header.menu.show-artboard-names"))]] :team-id team-id
:on-close close-sub-menu}]
[:li {:on-click #(st/emit! (toggle-flag :show-pixel-grid))} :edit
[:span [:& edit-menu
(if (contains? layout :show-pixel-grid) {:on-close close-sub-menu}]
(tr "workspace.header.menu.hide-pixel-grid")
(tr "workspace.header.menu.show-pixel-grid"))]
[:span.shortcut (sc/get-tooltip :show-pixel-grid)]]
[:li {:on-click #(st/emit! (-> (toggle-flag :hide-ui) :view
(vary-meta assoc ::ev/origin "workspace-menu")))} [:& view-menu
[:span {:layout layout
(tr "workspace.shape.menu.hide-ui")] :toggle-flag toggle-flag
[:span.shortcut (sc/get-tooltip :hide-ui)]]]] :on-close close-sub-menu}]
[:& dropdown {:show (= @show-sub-menu? :preferences) :preferences
:on-close #(reset! show-sub-menu? false)} [:& preferences-menu
[:ul.sub-menu.preferences {:layout layout
[:li {:on-click #(st/emit! (toggle-flag :scale-text))} :toggle-flag toggle-flag
[:span :on-close close-sub-menu}]
(if (contains? layout :scale-text)
(tr "workspace.header.menu.disable-scale-content")
(tr "workspace.header.menu.enable-scale-content"))]
[:span.shortcut (sc/get-tooltip :toggle-scale-text)]]
[:li {:on-click #(st/emit! (toggle-flag :snap-guides))} :help-info
[:span [:& help-info-menu
(if (contains? layout :snap-guides) {:layout layout
(tr "workspace.header.menu.disable-snap-guides") :on-close close-sub-menu}]
(tr "workspace.header.menu.enable-snap-guides"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-guide)]]
[:li {:on-click #(st/emit! (toggle-flag :snap-grid))} nil)]))
[:span
(if (contains? layout :snap-grid)
(tr "workspace.header.menu.disable-snap-grid")
(tr "workspace.header.menu.enable-snap-grid"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
[:li {:on-click #(st/emit! (toggle-flag :dynamic-alignment))}
[:span
(if (contains? layout :dynamic-alignment)
(tr "workspace.header.menu.disable-dynamic-alignment")
(tr "workspace.header.menu.enable-dynamic-alignment"))]
[:span.shortcut (sc/get-tooltip :toggle-alignment)]]
[:li {:on-click #(st/emit! (toggle-flag :snap-pixel-grid))}
[:span
(if (contains? layout :snap-pixel-grid)
(tr "workspace.header.menu.disable-snap-pixel-grid")
(tr "workspace.header.menu.enable-snap-pixel-grid"))]
[:span.shortcut (sc/get-tooltip :snap-pixel-grid)]]
[:li {:on-click #(st/emit! (modal/show {:type :nudge-option}))}
[:span (tr "modals.nudge-title")]]]]
[:& dropdown {:show (= @show-sub-menu? :help-info)
:on-close #(reset! show-sub-menu? false)}
[:ul.sub-menu.help-info
[:li {:on-click #(dom/open-new-window "https://help.penpot.app")}
[:span (tr "labels.help-center")]]
[:li {:on-click #(dom/open-new-window "https://community.penpot.app")}
[:span (tr "labels.community")]]
[:li {:on-click #(dom/open-new-window "https://www.youtube.com/c/Penpot")}
[:span (tr "labels.tutorials")]]
[:li {:on-click show-release-notes}
[:span (tr "labels.release-notes")]]
[:li.separator {:on-click #(dom/open-new-window "https://penpot.app/libraries-templates")}
[:span (tr "labels.libraries-and-templates")]]
[:li {:on-click #(dom/open-new-window "https://github.com/penpot/penpot")}
[:span (tr "labels.github-repo")]]
[:li {:on-click #(dom/open-new-window "https://penpot.app/terms")}
[:span (tr "auth.terms-of-service")]]
[:li.separator {:on-click #(st/emit! (when (contains? layout :collapse-left-sidebar) (dw/toggle-layout-flag :collapse-left-sidebar))
(-> (dw/toggle-layout-flag :shortcuts)
(vary-meta assoc ::ev/origin "workspace-header")))}
[:span (tr "label.shortcuts")]
[:span.shortcut (sc/get-tooltip :show-shortcuts)]]
(when (contains? @cf/flags :user-feedback)
[:*
[:li.feedback {:on-click #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))}
[:span (tr "labels.give-feedback")]]])]]]))
;; --- Header Component ;; --- Header Component
(mf/defc header (mf/defc header
[{:keys [file layout project page-id] :as props}] {::mf/wrap-props false}
(let [team-id (:team-id project) [{:keys [file layout project page-id]}]
(let [file-id (:id file)
file-name (:name file)
project-id (:id project)
team-id (:team-id project)
shared? (:is-shared file)
zoom (mf/deref refs/selected-zoom) zoom (mf/deref refs/selected-zoom)
params {:page-id page-id :file-id (:id file) :section "interactions"} read-only? (mf/use-ctx ctx/workspace-read-only?)
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil)))
on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil)))
on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom))
on-zoom-fit (mf/use-fn #(st/emit! dw/zoom-to-fit-all))
on-zoom-selected (mf/use-fn #(st/emit! dw/zoom-to-selected-shape))
editing* (mf/use-state false)
editing? (deref editing*)
input-ref (mf/use-ref nil)
handle-blur
(mf/use-fn
(mf/deps file-id)
(fn [_]
(let [value (str/trim (-> input-ref mf/ref-val dom/get-value))]
(when (not= value "")
(st/emit! (dw/rename-file file-id value)))
(reset! editing* false))))
handle-name-keydown
(mf/use-fn
(mf/deps handle-blur)
(fn [event]
(when (kbd/enter? event)
(handle-blur event))))
start-editing-name
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(reset! editing* true)))
close-modals close-modals
(mf/use-callback (mf/use-fn
(fn [] #(st/emit! (dc/stop-picker)
(st/emit! (dc/stop-picker)) (modal/hide)))
(st/emit! (modal/hide!))))
go-back go-back
(mf/use-callback (mf/use-fn
(mf/deps project) (mf/deps project)
(fn [] (fn []
(close-modals) (close-modals)
(st/emit! (dw/go-to-dashboard project)))) (st/emit! (dw/go-to-dashboard project))))
go-viewer nav-to-viewer
(mf/use-callback (mf/use-fn
(mf/deps file page-id) (mf/deps file-id page-id)
#(st/emit! (dw/go-to-viewer params)))] (fn []
(let [params {:page-id page-id
:file-id file-id
:section "interactions"}]
(st/emit! (dw/go-to-viewer params)))))
nav-to-project
(mf/use-fn
(mf/deps team-id project-id)
#(st/emit! (rt/nav-new-window* {:rname :dashboard-files
:path-params {:team-id team-id
:project-id (:project-id project-id)}})))
toggle-history
(mf/use-fn
#(st/emit! (-> (dw/toggle-layout-flag :document-history)
(vary-meta assoc ::ev/origin "workspace-header"))))]
(mf/with-effect [editing?]
(when ^boolean editing?
(dom/select-text! (mf/ref-val input-ref))))
[:header.workspace-header [:header.workspace-header
[:div.left-area [:div.left-area
[:div.main-icon [:div.main-icon
[:a {:on-click go-back} i/logo-icon]] [:a {:on-click go-back} i/logo-icon]]
[:div.menu-section
[:& menu {:layout layout [:& menu {:layout layout
:project project
:file file :file file
:read-only? read-only?
:team-id team-id :team-id team-id
:page-id page-id}]] :page-id page-id}]
[:div.project-tree {:alt (tr "workspace.sitemap")}
[:span.project-name
{:on-click nav-to-project}
(:name project) " /"]
(if ^boolean editing?
[:input.file-name
{:type "text"
:ref input-ref
:on-blur handle-blur
:on-key-down handle-name-keydown
:auto-focus true
:default-value (:name file "")}]
[:span
{:on-double-click start-editing-name}
file-name])]
(when ^boolean shared?
[:div.shared-badge i/library])]]
[:div.center-area [:div.center-area
[:div.users-section [:div.users-section
@ -490,26 +659,25 @@
[:div.options-section [:div.options-section
[:& persistence-state-widget] [:& persistence-state-widget]
[:& export-progress-widget] [:& export-progress-widget]
(when-not workspace-read-only? (when-not ^boolean read-only?
[:button.document-history [:button.document-history
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
:aria-label (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) :aria-label (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
:class (when (contains? layout :document-history) "selected") :class (when (contains? layout :document-history) "selected")
:on-click #(st/emit! (-> (dw/toggle-layout-flag :document-history) :on-click toggle-history}
(vary-meta assoc ::ev/origin "workspace-header")))}
i/recent])] i/recent])]
[:div.options-section [:div.options-section
[:& zoom-widget-workspace [:& zoom-widget-workspace
{:zoom zoom {:zoom zoom
:on-increase #(st/emit! (dw/increase-zoom nil)) :on-increase on-increase
:on-decrease #(st/emit! (dw/decrease-zoom nil)) :on-decrease on-decrease
:on-zoom-reset #(st/emit! dw/reset-zoom) :on-zoom-reset on-zoom-reset
:on-zoom-fit #(st/emit! dw/zoom-to-fit-all) :on-zoom-fit on-zoom-fit
:on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}] :on-zoom-selected on-zoom-selected}]
[:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
{:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
:on-click go-viewer} :on-click nav-to-viewer}
i/play]]]])) i/play]]]]))

View file

@ -303,6 +303,7 @@
:options options}]) :options options}])
(mf/defc asset-section (mf/defc asset-section
{::mf/wrap-props false}
[{:keys [children file-id title section assets-count open?]}] [{:keys [children file-id title section assets-count open?]}]
(let [children (->> (if (array? children) children [children]) (let [children (->> (if (array? children) children [children])
(filter some?)) (filter some?))
@ -310,12 +311,12 @@
title-buttons (filter #(= (get-role %) :title-button) children) title-buttons (filter #(= (get-role %) :title-button) children)
content (filter #(= (get-role %) :content) children)] content (filter #(= (get-role %) :content) children)]
[:div.asset-section [:div.asset-section
[:div.asset-title {:class (when (not open?) "closed")} [:div.asset-title {:class (when (not ^boolean open?) "closed")}
[:span {:on-click #(st/emit! (dwl/set-assets-section-open file-id section (not open?)))} [:span {:on-click #(st/emit! (dwl/set-assets-section-open file-id section (not open?)))}
i/arrow-slide title] i/arrow-slide title]
[:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space
title-buttons] title-buttons]
(when open? (when ^boolean open?
content)])) content)]))
(mf/defc asset-section-block (mf/defc asset-section-block