224 lines
8.4 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) KALEIDOS INC
(ns app.main.ui.workspace.sidebar.assets
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.components-list :as ctkl]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.assets :as dwa]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.assets.file-library :refer [file-library*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(mf/defc assets-libraries*
{::mf/wrap [mf/memo]
::mf/private true}
[{:keys [filters]}]
(let [file-id (mf/use-ctx ctx/current-file-id)
files (mf/deref refs/files)
libraries (mf/with-memo [files file-id]
(->> (refs/select-libraries files file-id)
(vals)
(remove :is-indirect)
(remove #(= file-id (:id %)))
(map (fn [file]
(update file :data dissoc :pages-index)))
(sort-by #(str/lower (:name %)))))]
(for [file libraries]
[:> file-library*
{:key (dm/str (:id file))
:file file
:is-local false
:is-default-open false
:filters filters}])))
(def ^:private ref:local-library
(l/derived (fn [file]
(update file :data dissoc :pages-index))
refs/file))
(mf/defc assets-local-library*
{::mf/private true}
[{:keys [filters]}]
(let [file (mf/deref ref:local-library)]
[:> file-library*
{:file file
:is-local true
:is-default-open true
:filters filters}]))
(defn- toggle-values
[v a b]
(if (= v a) b a))
;; Per-file, session-scoped (in-memory only) so the search term and section
;; filter survive switching between the Layers and Assets sidebar tabs without
;; leaking across files or persisting across reloads.
(defonce ^:private session-filters*
(atom {}))
(mf/defc assets-toolbox*
{::mf/wrap [mf/memo]}
[{:keys [size file-id]}]
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
filters* (mf/use-state
(fn []
(-> (or (get @session-filters* file-id)
{:term ""
:section "all"})
(assoc :ordering (dwa/get-current-assets-ordering)
:list-style (dwa/get-current-assets-list-style)
:open-menu false))))
filters (deref filters*)
term (:term filters)
list-style (:list-style filters)
menu-open? (:open-menu filters)
section (:section filters)
ordering (:ordering filters)
reverse-sort? (= :desc ordering)
libs (mf/deref refs/libraries)
num-libs (count libs)
file (get libs file-id)
shared? (:is-shared file)
components (mf/with-memo [file] (ctkl/components (:data file)))
toggle-ordering
(mf/use-fn
(mf/deps ordering)
(fn []
(let [new-value (toggle-values ordering :asc :desc)]
(swap! filters* assoc :ordering new-value)
(dwa/set-current-assets-ordering! new-value))))
toggle-list-style
(mf/use-fn
(mf/deps list-style)
(fn []
(let [new-value (toggle-values list-style :thumbs :list)]
(swap! filters* assoc :list-style new-value)
(dwa/set-current-assets-list-style! new-value))))
on-search-term-change
(mf/use-fn
(fn [event]
(st/emit! (dw/clear-assets-section-open))
(swap! filters* assoc :term event)))
on-section-filter-change
(mf/use-fn
(fn [event]
(let [value (or (-> (dom/get-target event)
(dom/get-value))
(as-> (dom/get-current-target event) $
(dom/get-attribute $ "data-testid")))]
(st/emit! (dw/clear-assets-section-open))
(swap! filters* assoc :section value :open-menu false))))
show-libraries-dialog
(mf/use-fn
(mf/deps file-id)
(fn []
(modal/show! :libraries-dialog {:file-id file-id})))
on-open-menu
(mf/use-fn #(swap! filters* update :open-menu not))
on-menu-close
(mf/use-fn #(swap! filters* assoc :open-menu false))
;; Memoize options to prevent infinite re-render loops when dev-tools are open.
;;
;; Problem: When dev-tools are open, they constantly monitor the application state,
;; triggering frequent updates to okulary refs. This causes the parent component to
;; re-render constantly, recreating the options array on every render.
;;
;; The context-menu* component has a mf/with-effect that depends on [options].
;; When options are recreated (even with identical content), the effect runs,
;; updating the internal state, which triggers another re-render, creating
;; an infinite loop: render -> new options -> effect -> state update -> render...
options
(mf/with-memo [on-section-filter-change]
[{:name (tr "workspace.assets.box-filter-all")
:id "all"
:handler on-section-filter-change}
{:name (tr "workspace.assets.components")
:id "components"
:handler on-section-filter-change}
{:name (tr "workspace.assets.colors")
:id "colors"
:handler on-section-filter-change}
{:name (tr "workspace.assets.typography")
:id "typographies"
:handler on-section-filter-change}])]
(mf/with-effect [file-id term section]
(swap! session-filters* assoc file-id {:term term :section section}))
[:article {:class (stl/css :assets-bar)}
[:div {:class (stl/css :assets-header)}
(when-not ^boolean read-only?
(if (and (= num-libs 1) (empty? components) (not shared?))
[:button {:class (stl/css :add-library-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
(tr "workspace.assets.add-library")]
[:button {:class (stl/css :libraries-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
(tr "workspace.assets.manage-library")]))
[:div {:class (stl/css :search-wrapper)}
[:> search-bar* {:on-change on-search-term-change
:value term
:placeholder (tr "workspace.assets.search")}
[:button
{:on-click on-open-menu
:title (tr "workspace.assets.filter")
:class (stl/css-case :section-button true
:opened menu-open?)}
deprecated-icon/filter-icon]]
[:> context-menu*
{:on-close on-menu-close
:selectable true
:selected section
:show menu-open?
:fixed true
:min-width true
:width size
:top 158
:left 18
:options options}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.assets.sort")
:on-click toggle-ordering
:icon (if reverse-sort? "asc-sort" "desc-sort")}]]]
[:& (mf/provider cmm/assets-filters) {:value filters}
[:& (mf/provider cmm/assets-toggle-ordering) {:value toggle-ordering}
[:& (mf/provider cmm/assets-toggle-list-style) {:value toggle-list-style}
[:*
[:> assets-local-library* {:filters filters}]
[:> assets-libraries* {:filters filters}]]]]]]))