♻️ Refactor context-menu component

This commit is contained in:
Andrey Antukh 2024-10-15 17:51:29 +02:00
parent 782d733bc9
commit 88d85706ad
12 changed files with 460 additions and 449 deletions

View file

@ -9,90 +9,107 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as tm] [app.util.timers :as tm]
[goog.object :as gobj]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn generate-ids-group (def ^:private xf:options
[options parent-name] (comp
(let [ids (->> options (map :id)
(map :id) (filter some?)))
(filter some?))]
(if parent-name
(cons "go-back-sub-option" ids)
ids)))
(mf/defc context-menu-a11y-item (defn- generate-ids-group
{::mf/wrap-props false} [options has-parents?]
[props] (let [ids (sequence xf:options options)
ids (if has-parents?
(cons "go-back-sub-option" ids)
ids)]
(vec ids)))
(let [children (gobj/get props "children") (def ^:private schema:option
on-click (gobj/get props "on-click") [:schema {:registry
on-key-down (gobj/get props "on-key-down") {::option
id (gobj/get props "id") [:or
klass (gobj/get props "class") :nil
key-index (gobj/get props "key-index") [:map [:name [:= :separator]]]
data-testid (gobj/get props "data-testid")] [:and
[:li {:id id [:map
:class klass [:name :string]
:tab-index "0" [:id :string]
:on-key-down on-key-down [:handler {:optional true} fn?]
:on-click on-click [:options {:optional true}
:key key-index [:sequential [:ref ::option]]]]
:role "menuitem" [::sm/contains-any #{:handler :options}]]]}}
:data-testid data-testid} [:ref ::option]])
children]))
(def ^:private valid-option?
(sm/lazy-validator schema:option))
(mf/defc context-menu*
{::mf/props :obj}
[{:keys [show on-close options selectable selected
top left fixed min-width origin width]
:as props}]
(assert (every? valid-option? options) "expected valid options")
(assert (fn? on-close) "missing `on-close` prop")
(assert (boolean? show) "missing `show` prop")
(assert (vector? options) "missing `options` prop")
(let [width (d/nilv width "initial")
min-width (d/nilv min-width false)
left (d/nilv left 0)
top (d/nilv top 0)
(mf/defc context-menu-a11y'
{::mf/wrap-props false}
[props]
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop")
(let [open? (gobj/get props "show")
on-close (gobj/get props "on-close")
options (gobj/get props "options")
is-selectable (gobj/get props "selectable")
selected (gobj/get props "selected")
top (gobj/get props "top" 0)
left (gobj/get props "left" 0)
fixed? (gobj/get props "fixed?" false)
min-width? (gobj/get props "min-width?" false)
origin (gobj/get props "origin")
route (mf/deref refs/route) route (mf/deref refs/route)
in-dashboard? (= :dashboard-projects (:name (:data route))) in-dashboard? (= :dashboard-projects (:name (:data route)))
local (mf/use-state {:offset-y 0
:offset-x 0
:levels nil})
width (gobj/get props "width" "initial")
state* (mf/use-state
#(-> {:offset-y 0
:offset-x 0
:levels nil}))
state (deref state*)
offset-x (get state :offset-x)
offset-y (get state :offset-y)
levels (get state :levels)
on-local-close on-local-close
(mf/use-callback (mf/use-fn
(mf/deps on-close)
(fn [] (fn []
(swap! local assoc :levels [{:parent-option nil (swap! state* assoc :levels [{:parent nil
:options options}]) :options options}])
(on-close))) (on-close)))
props (obj/merge props #js {:on-close on-local-close}) props
(mf/spread props :on-close on-local-close)
ids
(mf/with-memo [levels]
(let [last-level (last levels)]
(generate-ids-group (:options last-level)
(:parent last-level))))
ids (generate-ids-group (:options (last (:levels @local))) (:parent-option (last (:levels @local))))
check-menu-offscreen check-menu-offscreen
(mf/use-callback (mf/use-fn
(mf/deps top (:offset-y @local) left (:offset-x @local)) (mf/deps top left offset-x offset-y)
(fn [node] (fn [node]
(when (some? node) (when (some? node)
(let [bounding_rect (dom/get-bounding-rect node) (let [bounding-rect (dom/get-bounding-rect node)
window_size (dom/get-window-size) window-size (dom/get-window-size)
{node-height :height node-width :width} bounding_rect node-height (dm/get-prop bounding-rect :height)
{window-height :height window-width :width} window_size node-width (dm/get-prop bounding-rect :width)
window-height (get window-size :height)
window-width (get window-size :width)
target-offset-y (if (> (+ top node-height) window-height) target-offset-y (if (> (+ top node-height) window-height)
(- node-height) (- node-height)
0) 0)
@ -100,74 +117,86 @@
(- node-width) (- node-width)
0)] 0)]
(when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local))) (when (or (not= target-offset-y offset-y)
(swap! local assoc :offset-y target-offset-y :offset-x target-offset-x)))))) (not= target-offset-x offset-x))
(swap! state* assoc
:offset-y target-offset-y
:offset-x target-offset-x))))))
;; NOTE: this function is used for build navigation callbacks
;; so we don't really need to use the use-fn here. It is not
;; an efficient approach but this manages a reasonable small
;; list of objects, so doing it this way has no real
;; implications on performance but facilitates a lot the
;; implementation
enter-submenu enter-submenu
(mf/use-callback (fn [name options]
(mf/deps options) (fn [event]
(fn [option-name sub-options] (dom/stop-propagation event)
(fn [event] (swap! state* update :levels conj {:parent name
(dom/stop-propagation event) :options options})))
(swap! local update :levels on-submenu-exit
conj {:parent-option option-name (mf/use-fn
:options sub-options}))))
exit-submenu
(mf/use-callback
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! local update :levels pop))) (swap! state* update :levels pop)))
;; NOTE: this function is used for build navigation callbacks
;; so we don't really need to use the use-fn here. It is not
;; an efficient approach but this manages a reasonable small
;; list of objects, so doing it this way has no real
;; implications on performance but facilitates a lot the
;; implementation
on-key-down on-key-down
(fn [options-original parent-original] (fn [options-original parent-original]
(fn [event] (fn [event]
(let [ids (generate-ids-group options-original parent-original) (let [ids (generate-ids-group options-original
first-id (dom/get-element (first ids)) parent-original)
first-element (dom/get-element first-id) first-id (dom/get-element (first ids))
len (count ids) first-element (dom/get-element first-id)
parent (dom/get-target event) len (count ids)
parent-id (dom/get-attribute parent "id")
option (first (filter #(= parent-id (:id %)) options-original)) parent (dom/get-target event)
sub-options (:sub-options option) parent-id (dom/get-attribute parent "id")
has-suboptions? (some? (:sub-options option))
option-handler (:option-handler option) option (d/seek #(= parent-id (:id %)) options-original)
is-back-option (= "go-back-sub-option" parent-id)] sub-options (not-empty (:options option))
handler (:handler option)
is-back-option? (= "go-back-sub-option" parent-id)]
(when (kbd/home? event) (when (kbd/home? event)
(when first-element (when first-element
(dom/focus! first-element))) (dom/focus! first-element)))
(when (kbd/enter? event) (when (kbd/enter? event)
(if is-back-option (if is-back-option?
(exit-submenu event) (on-submenu-exit event)
(if has-suboptions? (if sub-options
(do (do
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! local update :levels (swap! state* update :levels conj {:parent (:name option)
conj {:parent-option (:option-name option) :options sub-options}))
:options sub-options}))
(do (do
(dom/stop-propagation event) (dom/stop-propagation event)
(option-handler event))))) (handler event)))))
(when (and is-back-option (when (and is-back-option? (kbd/left-arrow? event))
(kbd/left-arrow? event)) (on-submenu-exit event))
(exit-submenu event))
(when (and has-suboptions? (kbd/right-arrow? event)) (when (and sub-options (kbd/right-arrow? event))
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! local update :levels (swap! state* update :levels conj {:parent (:name option)
conj {:parent-option (:option-name option) :options sub-options}))
:options sub-options}))
(when (kbd/up-arrow? event) (when (kbd/up-arrow? event)
(let [actual-selected (dom/get-active) (let [actual-selected (dom/get-active)
actual-id (dom/get-attribute actual-selected "id") actual-id (dom/get-attribute actual-selected "id")
actual-index (d/index-of ids actual-id) actual-index (d/index-of ids actual-id)
previous-id (if (= 0 actual-index) previous-id (if (= 0 actual-index)
(last ids) (last ids)
(nth ids (- actual-index 1)))] (nth ids (- actual-index 1)))]
(dom/focus! (dom/get-element previous-id)))) (dom/focus! (dom/get-element previous-id))))
(when (kbd/down-arrow? event) (when (kbd/down-arrow? event)
@ -180,98 +209,87 @@
(dom/focus! (dom/get-element next-id)))) (dom/focus! (dom/get-element next-id))))
(when (or (kbd/esc? event) (kbd/tab? event)) (when (or (kbd/esc? event) (kbd/tab? event))
(on-close) (on-close event)
(dom/focus! (dom/get-element origin))))))] (dom/focus! (dom/get-element origin))))))]
(mf/with-effect [options] (mf/with-effect [options]
(swap! local assoc :levels [{:parent-option nil (swap! state* assoc :levels [{:parent nil
:options options}])) :options options}]))
(mf/with-effect [ids] (mf/with-effect [ids]
(tm/schedule-on-idle (tm/schedule-on-idle
#(dom/focus! (dom/get-element (first ids))))) #(dom/focus! (dom/get-element (first ids)))))
(when (and open? (some? (:levels @local))) (when (and show (some? levels))
[:> dropdown' props [:> dropdown' props
(let [level (-> @local :levels peek) (let [level (peek levels)
original-options (:options level) options (:options level)
parent-original (:parent-option level)] parent (:parent level)]
[:div {:class (stl/css-case :is-selectable is-selectable
:context-menu true [:div {:class (stl/css-case
:is-open open? :is-selectable selectable
:fixed fixed?) :context-menu true
:style {:top (+ top (:offset-y @local)) :is-open show
:left (+ left (:offset-x @local))} :fixed fixed)
:on-key-down (on-key-down original-options parent-original)} :style {:top (+ top offset-y)
(let [level (-> @local :levels peek)] :left (+ left offset-x)}
[:ul {:class (stl/css-case :min-width min-width? :on-key-down (on-key-down options parent)}
:context-menu-items true)
:style {:width width} [:ul {:class (stl/css-case :min-width min-width
:role "menu" :context-menu-items true)
:ref check-menu-offscreen} :style {:width width}
(when-let [parent-option (:parent-option level)] :role "menu"
[:* :ref check-menu-offscreen}
[:& context-menu-a11y-item
{:id "go-back-sub-option" (when-let [parent (:parent level)]
:class (stl/css :context-menu-item) [:*
:tab-index "0" [:li {:id "go-back-sub-option"
:on-key-down (fn [event] :class (stl/css :context-menu-item)
(dom/prevent-default event))} :role "menuitem"
[:button {:class (stl/css :context-menu-action :submenu-back) :tab-index "0"
:on-key-down dom/prevent-default}
[:button {:class (stl/css :context-menu-action :submenu-back)
:data-no-close true
:on-click on-submenu-exit}
[:span {:class (stl/css :submenu-icon-back)} i/arrow]
parent]]
[:li {:class (stl/css :separator)}]])
(for [[index option] (d/enumerate (:options level))]
(let [name (:name option)
id (:id option)
sub-options (:options option)
handler (:handler option)]
(when name
(if (= name :separator)
[:li {:key (dm/str "context-item-" index)
:class (stl/css :separator)}]
[:li {:id id
:key id
:class (stl/css-case
:is-selected (and selected (= name selected))
:selected (and selected (= id selected))
:context-menu-item true)
:tab-index "0"
:role "menuitem"
:on-key-down dom/prevent-default}
(if-not sub-options
[:a {:class (stl/css :context-menu-action)
:on-click #(do (dom/stop-propagation %)
(on-close %)
(handler %))
:data-testid id}
(if (and in-dashboard? (= name "Default"))
(tr "dashboard.default-team-name")
name)
(when (and selected (= id selected))
[:span {:class (stl/css :selected-icon)} i/tick])]
[:a {:class (stl/css :context-menu-action :submenu)
:data-no-close true :data-no-close true
:on-click exit-submenu} :on-click (enter-submenu name sub-options)
[:span {:class (stl/css :submenu-icon-back)} i/arrow] :data-testid id}
parent-option]] name
[:span {:class (stl/css :submenu-icon)} i/arrow]])]))))]])])))
[:li {:class (stl/css :separator)}]])
(for [[index option] (d/enumerate (:options level))]
(let [option-name (:option-name option)
id (:id option)
sub-options (:sub-options option)
option-handler (:option-handler option)
data-testid (:data-testid option)]
(when option-name
(if (= option-name :separator)
[:li {:key (dm/str "context-item-" index)
:class (stl/css :separator)}]
[:& context-menu-a11y-item
{:id id
:key id
:class (stl/css-case
:is-selected (and selected (= option-name selected))
:selected (and selected (= data-testid selected))
:context-menu-item true)
:key-index (dm/str "context-item-" index)
:tab-index "0"
:on-key-down (fn [event]
(dom/prevent-default event))}
(if-not sub-options
[:a {:class (stl/css :context-menu-action)
:on-click #(do (dom/stop-propagation %)
(on-close)
(option-handler %))
:data-testid data-testid}
(if (and in-dashboard? (= option-name "Default"))
(tr "dashboard.default-team-name")
option-name)
(when (and selected (= data-testid selected))
[:span {:class (stl/css :selected-icon)} i/tick])]
[:a {:class (stl/css :context-menu-action :submenu)
:data-no-close true
:on-click (enter-submenu option-name sub-options)
:data-testid data-testid}
option-name
[:span {:class (stl/css :submenu-icon)} i/arrow]])]))))])])])))
(mf/defc context-menu-a11y
{::mf/wrap-props false}
[props]
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop")
(when (gobj/get props "show")
(mf/element context-menu-a11y' props)))

View file

@ -14,7 +14,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -221,113 +221,118 @@
(reset! teams %))))))) (reset! teams %)))))))
(when current-team (when current-team
(let [sub-options (concat (vec (for [project current-projects] (let [sub-options
{:option-name (get-project-name project) (concat
:id (get-project-id project) (for [project current-projects]
:option-handler (on-move (:id current-team) {:name (get-project-name project)
(:id project))})) :id (get-project-id project)
(when (seq other-teams) :handler (on-move (:id current-team)
[{:option-name (tr "dashboard.move-to-other-team") (:id project))})
:id "move-to-other-team" (when (seq other-teams)
:sub-options [{:name (tr "dashboard.move-to-other-team")
(for [team other-teams] :id "move-to-other-team"
{:option-name (get-team-name team) :options
:id (get-project-id team) (for [team other-teams]
:sub-options {:name (get-team-name team)
(for [sub-project (:projects team)] :id (get-project-id team)
{:option-name (get-project-name sub-project) :options
:id (get-project-id sub-project) (for [sub-project (:projects team)]
:option-handler (on-move (:id team) {:name (get-project-name sub-project)
(:id sub-project))})})}])) :id (get-project-id sub-project)
:handler (on-move (:id team)
(:id sub-project))})})}]))
options (if multi? options
[(when-not you-viewer? (if multi?
{:option-name (tr "dashboard.duplicate-multi" file-count) [(when-not you-viewer?
:id "file-duplicate-multi" {:name (tr "dashboard.duplicate-multi" file-count)
:option-handler on-duplicate :id "duplicate-multi"
:data-testid "duplicate-multi"}) :handler on-duplicate})
(when (and (or (seq current-projects) (seq other-teams))
(not you-viewer?))
{:option-name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:sub-options sub-options
:data-testid "move-to-multi"})
{:option-name (tr "dashboard.export-binary-multi" file-count)
:id "file-binari-export-multi"
:option-handler on-export-binary-files}
{:option-name (tr "dashboard.export-standard-multi" file-count)
:id "file-standard-export-multi"
:option-handler on-export-standard-files}
(when (and (:is-shared file)
(not you-viewer?))
{:option-name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:option-handler on-del-shared
:data-testid "file-del-shared"})
(when (and (not is-lib-page?)
(not you-viewer?))
{:option-name :separator}
{:option-name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:option-handler on-delete
:data-testid "delete-multi-files"})]
[{:option-name (tr "dashboard.open-in-new-tab") (when (and (or (seq current-projects) (seq other-teams))
:id "file-open-new-tab" (not you-viewer?))
:option-handler on-new-tab} {:name (tr "dashboard.move-to-multi" file-count)
(when (and (not is-search-page?) :id "file-move-multi"
(not you-viewer?)) :options sub-options})
{:option-name (tr "labels.rename")
:id "file-rename"
:option-handler on-edit
:data-testid "file-rename"})
(when (and (not is-search-page?)
(not you-viewer?))
{:option-name (tr "dashboard.duplicate")
:id "file-duplicate"
:option-handler on-duplicate
:data-testid "file-duplicate"})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
(not you-viewer?))
{:option-name (tr "dashboard.move-to")
:id "file-move-to"
:sub-options sub-options
:data-testid "file-move-to"})
(when (and (not is-search-page?)
(not you-viewer?))
(if (:is-shared file)
{:option-name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:option-handler on-del-shared
:data-testid "file-del-shared"}
{:option-name (tr "dashboard.add-shared")
:id "file-add-shared"
:option-handler on-add-shared
:data-testid "file-add-shared"}))
{:option-name :separator}
{:option-name (tr "dashboard.download-binary-file")
:id "file-download-binary"
:option-handler on-export-binary-files
:data-testid "download-binary-file"}
{:option-name (tr "dashboard.download-standard-file")
:id "file-download-standard"
:option-handler on-export-standard-files
:data-testid "download-standard-file"}
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
{:option-name :separator}
{:option-name (tr "labels.delete")
:id "file-delete"
:option-handler on-delete
:data-testid "file-delete"})])]
[:& context-menu-a11y {:on-close on-menu-close {:name (tr "dashboard.export-binary-multi" file-count)
:show show? :id "file-binari-export-multi"
:fixed? (or (not= top 0) (not= left 0)) :handler on-export-binary-files}
:min-width? true
:top top {:name (tr "dashboard.export-standard-multi" file-count)
:left left :id "file-standard-export-multi"
:options options :handler on-export-standard-files}
:origin parent-id
:workspace? false}])))) (when (and (:is-shared file)
(not you-viewer?))
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (not is-lib-page?)
(not you-viewer?))
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:handler on-delete})]
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?)
(not you-viewer?))
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
(when (and (not is-search-page?)
(not you-viewer?))
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
(not you-viewer?))
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-search-page?)
(not you-viewer?))
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:handler on-del-shared}
{:name (tr "dashboard.add-shared")
:id "file-add-shared"
:handler on-add-shared}))
{:name :separator}
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files}
{:name (tr "dashboard.download-standard-file")
:id "download-standard-file"
:handler on-export-standard-files}
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})])]
[:> context-menu*
{:on-close on-menu-close
:show show?
:fixed (or (not= top 0) (not= left 0))
:min-width true
:top top
:left left
:options options
:origin parent-id}]))))

View file

@ -14,7 +14,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -250,21 +250,20 @@
::mf/private true} ::mf/private true}
[{:keys [is-open on-close on-edit on-delete]}] [{:keys [is-open on-close on-edit on-delete]}]
(let [options (mf/with-memo [on-edit on-delete] (let [options (mf/with-memo [on-edit on-delete]
[{:option-name (tr "labels.edit") [{:name (tr "labels.edit")
:id "font-edit" :id "font-edit"
:option-handler on-edit} :handler on-edit}
{:option-name (tr "labels.delete") {:name (tr "labels.delete")
:id "font-delete" :id "font-delete"
:option-handler on-delete}])] :handler on-delete}])]
[:& context-menu-a11y [:> context-menu*
{:on-close on-close {:on-close on-close
:show is-open :show is-open
:fixed? false :fixed false
:min-width? true :min-width true
:top -15 :top -15
:left -115 :left -115
:options options :options options}]))
:workspace? false}]))
(mf/defc installed-font (mf/defc installed-font
{::mf/props :obj {::mf/props :obj

View file

@ -11,7 +11,7 @@
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.dashboard.import :as udi] [app.main.ui.dashboard.import :as udi]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -81,53 +81,50 @@
(fn [] (fn []
(when (fn? on-import) (on-import)))) (when (fn? on-import) (on-import))))
options [(when-not (:is-default project) options
{:option-name (tr "labels.rename") [(when-not (:is-default project)
:id "project-menu-rename" {:name (tr "labels.rename")
:option-handler on-edit :id "project-rename"
:data-testid "project-rename"}) :handler on-edit})
(when-not (:is-default project) (when-not (:is-default project)
{:option-name (tr "dashboard.duplicate") {:name (tr "dashboard.duplicate")
:id "project-menu-duplicated" :id "project-duplicate"
:option-handler on-duplicate :handler on-duplicate})
:data-testid "project-duplicate"}) (when-not (:is-default project)
(when-not (:is-default project) {:name (tr "dashboard.pin-unpin")
{:option-name (tr "dashboard.pin-unpin") :id "project-pin"
:id "project-menu-pin" :handler toggle-pin})
:option-handler toggle-pin})
(when (and (seq teams) (not (:is-default project))) (when (and (seq teams) (not (:is-default project)))
{:option-name (tr "dashboard.move-to") {:name (tr "dashboard.move-to")
:id "project-menu-move-to" :id "project-move-to"
:sub-options (for [team teams] :options (for [team teams]
{:option-name (:name team) {:name (:name team)
:id (:name team) :id (str "move-to-" (:id team))
:option-handler (on-move (:id team))}) :handler (on-move (:id team))})})
:data-testid "project-move-to"})
(when (some? on-import) (when (some? on-import)
{:option-name (tr "dashboard.import") {:name (tr "dashboard.import")
:id "project-menu-import" :id "file-import"
:option-handler on-import-files :handler on-import-files})
:data-testid "file-import"}) (when-not (:is-default project)
(when-not (:is-default project) {:name :separator})
{:option-name :separator}) (when-not (:is-default project)
(when-not (:is-default project) {:name (tr "labels.delete")
{:option-name (tr "labels.delete") :id "project-delete"
:id "project-menu-delete" :handler on-delete})]]
:option-handler on-delete
:data-testid "project-delete"})]]
[:* [:*
[:& context-menu-a11y [:> context-menu*
{:on-close on-menu-close {:on-close on-menu-close
:show show? :show show?
:fixed? (or (not= top 0) (not= left 0)) :fixed (or (not= top 0) (not= left 0))
:min-width? true :min-width true
:top top :top top
:left left :left left
:options options :options options}]
:workspace false}]
[:& udi/import-form {:ref file-input [:& udi/import-form {:ref file-input
:project-id (:id project) :project-id (:id project)
:on-finish-import on-finish-import}]])) :on-finish-import on-finish-import}]]))

View file

@ -12,7 +12,7 @@
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -195,9 +195,9 @@
(let [local (mf/use-state {:menu-open false}) (let [local (mf/use-state {:menu-open false})
show? (:menu-open @local) show? (:menu-open @local)
options (mf/with-memo [on-delete] options (mf/with-memo [on-delete]
[{:option-name (tr "labels.delete") [{:name (tr "labels.delete")
:id "access-token-delete" :id "access-token-delete"
:option-handler on-delete}]) :handler on-delete}])
menu-ref (mf/use-ref) menu-ref (mf/use-ref)
@ -224,11 +224,11 @@
:on-click on-menu-click :on-click on-menu-click
:on-key-down on-keydown} :on-key-down on-keydown}
menu-icon menu-icon
[:& context-menu-a11y [:> context-menu*
{:on-close on-menu-close {:on-close on-menu-close
:show show? :show show?
:fixed? true :fixed true
:min-width? true :min-width true
:top "auto" :top "auto"
:left "auto" :left "auto"
:options options}]])) :options options}]]))

View file

@ -13,7 +13,7 @@
[app.main.data.workspace.assets :as dwa] [app.main.data.workspace.assets :as dwa]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -130,32 +130,26 @@
on-menu-close on-menu-close
(mf/use-fn #(swap! filters* assoc :open-menu false)) (mf/use-fn #(swap! filters* assoc :open-menu false))
options (into [] (remove nil? options
[{:option-name (tr "workspace.assets.box-filter-all") [{:name (tr "workspace.assets.box-filter-all")
:id "section-all" :id "section-all"
:option-handler on-section-filter-change :handler on-section-filter-change}
:data-testid "all"} {:name (tr "workspace.assets.components")
:id "section-components"
:handler on-section-filter-change}
{:option-name (tr "workspace.assets.components") (when (not components-v2)
:id "section-components" {:name (tr "workspace.assets.graphics")
:option-handler on-section-filter-change :id "section-graphics"
:data-testid "components"} :handler on-section-filter-change})
(when (not components-v2) {:name (tr "workspace.assets.colors")
{:option-name (tr "workspace.assets.graphics") :id "section-colors"
:id "section-graphics" :handler on-section-filter-change}
:option-handler on-section-filter-change
:data-testid "graphics"})
{:option-name (tr "workspace.assets.colors") {:name (tr "workspace.assets.typography")
:id "section-color" :id "section-typographies"
:option-handler on-section-filter-change :handler on-section-filter-change}]]
:data-testid "colors"}
{:option-name (tr "workspace.assets.typography")
:id "section-typography"
:option-handler on-section-filter-change
:data-testid "typographies"}]))]
[:article {:class (stl/css :assets-bar)} [:article {:class (stl/css :assets-bar)}
[:div {:class (stl/css :assets-header)} [:div {:class (stl/css :assets-header)}
@ -177,18 +171,17 @@
:class (stl/css-case :section-button true :class (stl/css-case :section-button true
:opened menu-open?)} :opened menu-open?)}
i/filter-icon]] i/filter-icon]]
[:& context-menu-a11y [:> context-menu*
{:on-close on-menu-close {:on-close on-menu-close
:selectable true :selectable true
:selected section :selected section
:show menu-open? :show menu-open?
:fixed? true :fixed true
:min-width? true :min-width true
:width size :width size
:top 158 :top 158
:left 18 :left 18
:options options :options options}]
:workspace? true}]
[:button {:class (stl/css :sort-button) [:button {:class (stl/css :sort-button)
:title (tr "workspace.assets.sort") :title (tr "workspace.assets.sort")
:on-click toggle-ordering} :on-click toggle-ordering}

View file

@ -240,21 +240,21 @@
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [(when-not (or multi-colors? multi-assets?) :options [(when-not (or multi-colors? multi-assets?)
{:option-name (tr "workspace.assets.rename") {:name (tr "workspace.assets.rename")
:id "assets-rename-color" :id "assets-rename-color"
:option-handler rename-color-clicked}) :handler rename-color-clicked})
(when-not (or multi-colors? multi-assets?) (when-not (or multi-colors? multi-assets?)
{:option-name (tr "workspace.assets.edit") {:name (tr "workspace.assets.edit")
:id "assets-edit-color" :id "assets-edit-color"
:option-handler edit-color-clicked}) :handler edit-color-clicked})
{:option-name (tr "workspace.assets.delete") {:name (tr "workspace.assets.delete")
:id "assets-delete-color" :id "assets-delete-color"
:option-handler delete-color} :handler delete-color}
(when-not multi-assets? (when-not multi-assets?
{:option-name (tr "workspace.assets.group") {:name (tr "workspace.assets.group")
:id "assets-group-color" :id "assets-group-color"
:option-handler (on-group (:id color))})]}]) :handler (on-group (:id color))})]}])
(when ^boolean dragging? (when ^boolean dragging?
[:div {:class (stl/css :dragging)}])])) [:div {:class (stl/css :dragging)}])]))

View file

@ -22,7 +22,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.render :refer [component-svg component-svg-thumbnail]] [app.main.render :refer [component-svg component-svg-thumbnail]]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -111,14 +111,13 @@
(mf/defc assets-context-menu (mf/defc assets-context-menu
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [options state on-close]}] [{:keys [options state on-close]}]
[:& context-menu-a11y [:> context-menu*
{:show (:open? state) {:show (:open? state)
:fixed? (or (not= (:top state) 0) (not= (:left state) 0)) :fixed (or (not= (:top state) 0) (not= (:left state) 0))
:on-close on-close :on-close on-close
:top (:top state) :top (:top state)
:left (:left state) :left (:left state)
:options options :options options}])
:workspace? true}])
(mf/defc section-icon (mf/defc section-icon
{::mf/wrap-props false} {::mf/wrap-props false}

View file

@ -559,26 +559,26 @@
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [(when (and local? (not (or multi-components? multi-assets? read-only?))) :options [(when (and local? (not (or multi-components? multi-assets? read-only?)))
{:option-name (tr "workspace.assets.rename") {:name (tr "workspace.assets.rename")
:id "assets-rename-component" :id "assets-rename-component"
:option-handler on-rename}) :handler on-rename})
(when (and local? (not (or multi-assets? read-only?))) (when (and local? (not (or multi-assets? read-only?)))
{:option-name (if components-v2 {:name (if components-v2
(tr "workspace.assets.duplicate-main") (tr "workspace.assets.duplicate-main")
(tr "workspace.assets.duplicate")) (tr "workspace.assets.duplicate"))
:id "assets-duplicate-component" :id "assets-duplicate-component"
:option-handler on-duplicate}) :handler on-duplicate})
(when (and local? (not read-only?)) (when (and local? (not read-only?))
{:option-name (tr "workspace.assets.delete") {:name (tr "workspace.assets.delete")
:id "assets-delete-component" :id "assets-delete-component"
:option-handler on-delete}) :handler on-delete})
(when (and local? (not (or multi-assets? read-only?))) (when (and local? (not (or multi-assets? read-only?)))
{:option-name (tr "workspace.assets.group") {:name (tr "workspace.assets.group")
:id "assets-group-component" :id "assets-group-component"
:option-handler on-group}) :handler on-group})
(when (and components-v2 (not multi-assets?)) (when (and components-v2 (not multi-assets?))
{:option-name (tr "workspace.shape.menu.show-main") {:name (tr "workspace.shape.menu.show-main")
:id "assets-show-main-component" :id "assets-show-main-component"
:option-handler on-show-main})]}]]])) :handler on-show-main})]}]]]))

View file

@ -418,13 +418,13 @@
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [(when-not (or multi-objects? multi-assets?) :options [(when-not (or multi-objects? multi-assets?)
{:option-name (tr "workspace.assets.rename") {:name (tr "workspace.assets.rename")
:id "assets-rename-graphics" :id "assets-rename-graphics"
:option-handler on-rename}) :handler on-rename})
{:option-name (tr "workspace.assets.delete") {:name (tr "workspace.assets.delete")
:id "assets-delete-graphics" :id "assets-delete-graphics"
:option-handler on-delete} :handler on-delete}
(when-not multi-assets? (when-not multi-assets?
{:option-name (tr "workspace.assets.group") {:name (tr "workspace.assets.group")
:id "assets-group-graphics" :id "assets-group-graphics"
:option-handler on-group})]}])]])) :handler on-group})]}])]]))

View file

@ -59,12 +59,12 @@
[:& cmm/assets-context-menu [:& cmm/assets-context-menu
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [{:option-name (tr "workspace.assets.rename") :options [{:name (tr "workspace.assets.rename")
:id "assets-rename-group" :id "assets-rename-group"
:option-handler #(on-rename % path last-path)} :handler #(on-rename % path last-path)}
{:option-name (tr "workspace.assets.ungroup") {:name (tr "workspace.assets.ungroup")
:id "assets-ungroup-group" :id "assets-ungroup-group"
:option-handler #(on-ungroup path)}]}]]))) :handler #(on-ungroup path)}]}]])))
(defn group-assets (defn group-assets
"Convert a list of assets in a nested structure like this: "Convert a list of assets in a nested structure like this:

View file

@ -434,27 +434,27 @@
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [(when-not (or multi-typographies? multi-assets?) :options [(when-not (or multi-typographies? multi-assets?)
{:option-name (tr "workspace.assets.rename") {:name (tr "workspace.assets.rename")
:id "assets-rename-typography" :id "assets-rename-typography"
:option-handler handle-rename-typography-clicked}) :handler handle-rename-typography-clicked})
(when-not (or multi-typographies? multi-assets?) (when-not (or multi-typographies? multi-assets?)
{:option-name (tr "workspace.assets.edit") {:name (tr "workspace.assets.edit")
:id "assets-edit-typography" :id "assets-edit-typography"
:option-handler handle-edit-typography-clicked}) :handler handle-edit-typography-clicked})
{:option-name (tr "workspace.assets.delete") {:name (tr "workspace.assets.delete")
:id "assets-delete-typography" :id "assets-delete-typography"
:option-handler handle-delete-typography} :handler handle-delete-typography}
(when-not multi-assets? (when-not multi-assets?
{:option-name (tr "workspace.assets.group") {:name (tr "workspace.assets.group")
:id "assets-group-typography" :id "assets-group-typography"
:option-handler on-group})]}] :handler on-group})]}]
[:& cmm/assets-context-menu [:& cmm/assets-context-menu
{:on-close on-close-menu {:on-close on-close-menu
:state @menu-state :state @menu-state
:options [{:option-name "show info" :options [{:name "show info"
:id "assets-rename-typography" :id "assets-rename-typography"
:option-handler handle-edit-typography-clicked}]}])]]])) :handler handle-edit-typography-clicked}]}])]]]))