mirror of
https://github.com/penpot/penpot.git
synced 2025-05-28 11:36:11 +02:00
414 lines
16 KiB
Clojure
414 lines
16 KiB
Clojure
;; 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) UXBOX Labs SL
|
|
|
|
(ns app.main.ui.workspace.header
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.math :as mth]
|
|
[app.config :as cf]
|
|
[app.main.data.events :as ev]
|
|
[app.main.data.messages :as dm]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.workspace :as dw]
|
|
[app.main.data.workspace.shortcuts :as sc]
|
|
[app.main.refs :as refs]
|
|
[app.main.repo :as rp]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
|
[app.main.ui.hooks.resize :as r]
|
|
[app.main.ui.icons :as i]
|
|
[app.main.ui.workspace.presence :refer [active-sessions]]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.router :as rt]
|
|
[beicon.core :as rx]
|
|
[okulary.core :as l]
|
|
[potok.core :as ptk]
|
|
[rumext.alpha :as mf]))
|
|
|
|
;; --- Zoom Widget
|
|
|
|
(def workspace-persistence-ref
|
|
(l/derived :workspace-persistence st/state))
|
|
|
|
(mf/defc persistence-state-widget
|
|
{::mf/wrap [mf/memo]}
|
|
[]
|
|
(let [data (mf/deref workspace-persistence-ref)]
|
|
[:div.persistence-status-widget
|
|
(cond
|
|
(= :pending (:status data))
|
|
[:div.pending
|
|
[:span.label (tr "workspace.header.unsaved")]]
|
|
|
|
(= :saving (:status data))
|
|
[:div.saving
|
|
[:span.icon i/toggle]
|
|
[:span.label (tr "workspace.header.saving")]]
|
|
|
|
(= :saved (:status data))
|
|
[:div.saved
|
|
[:span.icon i/tick]
|
|
[:span.label (tr "workspace.header.saved")]]
|
|
|
|
(= :error (:status data))
|
|
[:div.error {:title "There was an error saving the data. Please refresh if this persists."}
|
|
[:span.icon i/msg-warning]
|
|
[:span.label (tr "workspace.header.save-error")]])]))
|
|
|
|
(mf/defc zoom-widget-workspace
|
|
{::mf/wrap [mf/memo]}
|
|
[{:keys [zoom
|
|
on-increase
|
|
on-decrease
|
|
on-zoom-reset
|
|
on-zoom-fit
|
|
on-zoom-selected]
|
|
:as props}]
|
|
(let [show-dropdown? (mf/use-state false)]
|
|
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
|
|
[:span.label {} (str (mth/round (* 100 zoom)) "%")]
|
|
[:span.icon i/arrow-down]
|
|
[:& dropdown {:show @show-dropdown?
|
|
:on-close #(reset! show-dropdown? false)}
|
|
[:ul.dropdown
|
|
[:li.basic-zoom-bar
|
|
[:span.zoom-btns
|
|
[:button {:on-click (fn [event]
|
|
(dom/stop-propagation event)
|
|
(dom/prevent-default event)
|
|
(on-decrease))} "-"]
|
|
[:p.zoom-size {} (str (mth/round (* 100 zoom)) "%")]
|
|
[: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")]]
|
|
[:li.separator]
|
|
[:li {:on-click on-zoom-fit}
|
|
(tr "workspace.header.zoom-fit-all") [:span (sc/get-tooltip :fit-all)]]
|
|
[:li {:on-click on-zoom-selected}
|
|
(tr "workspace.header.zoom-selected") [:span (sc/get-tooltip :zoom-selected)]]]]]))
|
|
|
|
|
|
|
|
;; --- Header Users
|
|
|
|
(mf/defc menu
|
|
[{:keys [layout project file team-id page-id] :as props}]
|
|
(let [show-menu? (mf/use-state false)
|
|
show-sub-menu? (mf/use-state false)
|
|
editing? (mf/use-state false)
|
|
edit-input-ref (mf/use-ref nil)
|
|
frames (mf/deref refs/workspace-frames)
|
|
|
|
add-shared-fn
|
|
(st/emitf (dw/set-file-shared (:id file) true))
|
|
|
|
del-shared-fn
|
|
(st/emitf (dw/set-file-shared (:id file) false))
|
|
|
|
on-add-shared
|
|
(mf/use-fn
|
|
(mf/deps file)
|
|
(st/emitf (modal/show
|
|
{:type :confirm
|
|
:message ""
|
|
:title (tr "modals.add-shared-confirm.message" (:name file))
|
|
:hint (tr "modals.add-shared-confirm.hint")
|
|
:cancel-label :omit
|
|
:accept-label (tr "modals.add-shared-confirm.accept")
|
|
:accept-style :primary
|
|
:on-accept add-shared-fn})))
|
|
|
|
on-remove-shared
|
|
(mf/use-fn
|
|
(mf/deps file)
|
|
(st/emitf (modal/show
|
|
{:type :confirm
|
|
:message ""
|
|
:title (tr "modals.remove-shared-confirm.message" (:name file))
|
|
:hint (tr "modals.remove-shared-confirm.hint")
|
|
:cancel-label :omit
|
|
:accept-label (tr "modals.remove-shared-confirm.accept")
|
|
:on-accept del-shared-fn})))
|
|
|
|
|
|
handle-blur (fn [_]
|
|
(let [value (-> edit-input-ref mf/ref-val dom/get-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-file
|
|
(mf/use-callback
|
|
(mf/deps file team-id)
|
|
(fn [_]
|
|
(st/emit! (ptk/event ::ev/event {::ev/name "export-files"
|
|
::ev/origin "workspace"
|
|
:num-files 1}))
|
|
|
|
(->> (rx/of file)
|
|
(rx/flat-map
|
|
(fn [file]
|
|
(->> (rp/query :file-libraries {:file-id (:id file)})
|
|
(rx/map #(assoc file :has-libraries? (d/not-empty? %))))))
|
|
(rx/reduce conj [])
|
|
(rx/subs
|
|
(fn [files]
|
|
(st/emit!
|
|
(modal/show
|
|
{:type :export
|
|
:team-id team-id
|
|
:has-libraries? (->> files (some :has-libraries?))
|
|
:files files})))))))
|
|
|
|
on-export-frames
|
|
(mf/use-callback
|
|
(mf/deps file frames)
|
|
(fn [_]
|
|
(when (seq frames)
|
|
(let [filename (str (:name file) ".pdf")
|
|
frame-ids (mapv :id frames)]
|
|
(st/emit! (dm/info (tr "workspace.options.exporting-object")
|
|
{:timeout nil}))
|
|
(->> (rp/query! :export-frames
|
|
{:name (:name file)
|
|
:file-id (:id file)
|
|
:page-id page-id
|
|
:frame-ids frame-ids})
|
|
(rx/subs
|
|
(fn [body]
|
|
(dom/trigger-download filename body))
|
|
(fn [_error]
|
|
(st/emit! (dm/error (tr "errors.unexpected-error"))))
|
|
(st/emitf dm/hide)))))))
|
|
|
|
on-item-click
|
|
(mf/use-callback
|
|
(fn [item]
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(reset! show-sub-menu? item))))]
|
|
|
|
(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/navigate :dashboard-files {: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)}
|
|
[:span (tr "workspace.header.menu.option.file")]
|
|
[:span i/arrow-slide]]
|
|
[:li {:on-click (on-item-click :edit)}
|
|
[:span (tr "workspace.header.menu.option.edit")] [:span i/arrow-slide]]
|
|
[:li {:on-click (on-item-click :view)}
|
|
[:span (tr "workspace.header.menu.option.view")] [:span i/arrow-slide]]
|
|
[:li {:on-click (on-item-click :preferences)}
|
|
[:span (tr "workspace.header.menu.option.preferences")] [:span i/arrow-slide]]
|
|
(when (contains? @cf/flags :user-feedback)
|
|
[:*
|
|
[:li.separator]
|
|
[:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))}
|
|
[:span (tr "labels.give-feedback")]]])]]
|
|
|
|
[:& dropdown {:show (= @show-sub-menu? :file)
|
|
:on-close #(reset! show-sub-menu? false)}
|
|
[:ul.sub-menu.file
|
|
(if (:is-shared file)
|
|
[:li {:on-click on-remove-shared}
|
|
[:span (tr "dashboard.remove-shared")]]
|
|
[:li {:on-click on-add-shared}
|
|
[:span (tr "dashboard.add-shared")]])
|
|
[:li.export-file {:on-click on-export-file}
|
|
[:span (tr "dashboard.export-single")]]
|
|
(when (seq frames)
|
|
[:li.export-file {:on-click on-export-frames}
|
|
[:span (tr "dashboard.export-frames")]])]]
|
|
|
|
[:& dropdown {:show (= @show-sub-menu? :edit)
|
|
:on-close #(reset! show-sub-menu? false)}
|
|
[:ul.sub-menu.edit
|
|
[:li {:on-click #(st/emit! (dw/select-all))}
|
|
[:span (tr "workspace.header.menu.select-all")]
|
|
[:span.shortcut (sc/get-tooltip :select-all)]]
|
|
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :scale-text))}
|
|
[:span
|
|
(if (contains? layout :scale-text)
|
|
(tr "workspace.header.menu.disable-scale-text")
|
|
(tr "workspace.header.menu.enable-scale-text"))]
|
|
[:span.shortcut (sc/get-tooltip :toggle-scale-text)]]]]
|
|
|
|
[:& dropdown {:show (= @show-sub-menu? :view)
|
|
:on-close #(reset! show-sub-menu? false)}
|
|
[:ul.sub-menu.view
|
|
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :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 #(st/emit! (dw/toggle-layout-flags :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)]]
|
|
|
|
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :sitemap :layers))}
|
|
[:span
|
|
(if (or (contains? layout :sitemap) (contains? layout :layers))
|
|
(tr "workspace.header.menu.hide-layers")
|
|
(tr "workspace.header.menu.show-layers"))]
|
|
[:span.shortcut (sc/get-tooltip :toggle-layers)]]
|
|
|
|
[:li {:on-click (fn []
|
|
(r/set-resize-type! :bottom)
|
|
(st/emit! (dw/remove-layout-flags :textpalette)
|
|
(dw/toggle-layout-flags :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 []
|
|
(r/set-resize-type! :bottom)
|
|
(st/emit! (dw/remove-layout-flags :colorpalette)
|
|
(dw/toggle-layout-flags :textpalette)))}
|
|
[: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 #(st/emit! (dw/toggle-layout-flags :assets))}
|
|
[:span
|
|
(if (contains? layout :assets)
|
|
(tr "workspace.header.menu.hide-assets")
|
|
(tr "workspace.header.menu.show-assets"))]
|
|
[:span.shortcut (sc/get-tooltip :toggle-assets)]]
|
|
|
|
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :display-artboard-names))}
|
|
[:span
|
|
(if (contains? layout :display-artboard-names)
|
|
(tr "workspace.header.menu.hide-artboard-names")
|
|
(tr "workspace.header.menu.show-artboard-names"))]]]]
|
|
|
|
[:& dropdown {:show (= @show-sub-menu? :preferences)
|
|
:on-close #(reset! show-sub-menu? false)}
|
|
[:ul.sub-menu.preferences
|
|
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :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 #(st/emit! (dw/toggle-layout-flags :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 #(st/emit! (dw/toggle-layout-flags :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! (modal/show {:type :nudge-option}))}
|
|
[:span (tr "modals.nudge-title")]]]]]))
|
|
|
|
;; --- Header Component
|
|
|
|
(mf/defc header
|
|
[{:keys [file layout project page-id] :as props}]
|
|
(let [team-id (:team-id project)
|
|
zoom (mf/deref refs/selected-zoom)
|
|
params {:page-id page-id :file-id (:id file) :section "interactions"}
|
|
|
|
go-back
|
|
(mf/use-callback
|
|
(mf/deps project)
|
|
(st/emitf (dw/go-to-dashboard project)))
|
|
|
|
go-viewer
|
|
(mf/use-callback
|
|
(mf/deps file page-id)
|
|
(st/emitf (dw/go-to-viewer params)))]
|
|
|
|
[:header.workspace-header
|
|
[:div.left-area
|
|
[:div.main-icon
|
|
[:a {:on-click go-back} i/logo-icon]]
|
|
|
|
[:& menu {:layout layout
|
|
:project project
|
|
:file file
|
|
:team-id team-id
|
|
:page-id page-id}]]
|
|
|
|
[:div.center-area
|
|
[:div.users-section
|
|
[:& active-sessions]]]
|
|
|
|
[:div.right-area
|
|
[:div.options-section
|
|
[:& persistence-state-widget]
|
|
[:button.document-history
|
|
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
|
|
:class (when (contains? layout :document-history) "selected")
|
|
:on-click (st/emitf (dw/toggle-layout-flags :document-history))}
|
|
i/recent]]
|
|
|
|
[:div.options-section
|
|
[:& zoom-widget-workspace
|
|
{:zoom zoom
|
|
:on-increase #(st/emit! (dw/increase-zoom nil))
|
|
:on-decrease #(st/emit! (dw/decrease-zoom nil))
|
|
:on-zoom-reset #(st/emit! dw/reset-zoom)
|
|
:on-zoom-fit #(st/emit! dw/zoom-to-fit-all)
|
|
:on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}]
|
|
|
|
[:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
|
|
{:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
|
|
:on-click go-viewer}
|
|
i/play]]]]))
|
|
|