From 95c172e7812a6bd232874fe2cb047b3da166cb84 Mon Sep 17 00:00:00 2001 From: Xavier Julian Date: Wed, 10 Dec 2025 15:08:50 +0100 Subject: [PATCH] :sparkles: Save unfolded tokens path --- common/src/app/common/path_names.cljc | 6 +- .../data/workspace/tokens/library_edit.cljs | 33 ++++++++- .../workspace/colorpicker/color_tokens.cljs | 1 - .../sidebar/options/menus/component.cljs | 4 +- .../main/ui/workspace/tokens/management.cljs | 10 --- .../tokens/management/forms/generic_form.cljs | 2 + .../ui/workspace/tokens/management/group.cljs | 45 ++++++----- .../tokens/management/token_tree.cljs | 74 +++++++++++++------ .../tokens/management/token_tree.scss | 33 +++++++-- 9 files changed, 141 insertions(+), 67 deletions(-) diff --git a/common/src/app/common/path_names.cljc b/common/src/app/common/path_names.cljc index 74774c0044..6fab97e66e 100644 --- a/common/src/app/common/path_names.cljc +++ b/common/src/app/common/path_names.cljc @@ -99,11 +99,11 @@ Some naming conventions: (defn butlast-path "Remove the last item of the path." - [path] - (let [split (split-path path)] + [path separator] + (let [split (split-path path :separator separator)] (if (= 1 (count split)) "" - (join-path (butlast split))))) + (join-path (butlast split) :separator separator)))) (defn butlast-path-with-dots "Remove the last item of the path." diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 63b883a48f..f7d0a7f6a6 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -11,6 +11,7 @@ [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.logic.tokens :as clt] + [app.common.path-names :as cpn] [app.common.types.shape :as cts] [app.common.types.tokens-lib :as ctob] [app.common.uuid :as uuid] @@ -22,6 +23,7 @@ [app.main.data.workspace.tokens.propagation :as dwtp] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] + [cuerdas.core :as str] [potok.v2.core :as ptk])) (declare set-selected-token-set-id) @@ -460,12 +462,35 @@ ;; TOKEN UI OPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn set-token-type-section-open - [token-type open?] - (ptk/reify ::set-token-type-section-open +(defn clean-paths + [] + (ptk/reify ::clean-paths ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-tokens :open-status-by-type] assoc token-type open?)))) + (assoc-in state [:workspace-tokens :unfolded-token-paths] [])))) + +(defn toggle-path + [path] + (ptk/reify ::toggle-path + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-tokens :unfolded-token-paths] + (fn [paths] + (let [paths (or paths [])] + (if (some #(= % path) paths) + (vec (remove #(or (= % path) + (str/starts-with? % (str path "."))) + paths)) + (let [split-path (cpn/split-path path :separator ".") + partial-paths (reduce + (fn [acc segment] + (let [new-acc (if (empty? acc) + segment + (str (last acc) "." segment))] + (conj acc new-acc))) + [] + split-path)] + (into paths partial-paths))))))))) (defn assign-token-context-menu [{:keys [position] :as params}] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs index 61871dfcdd..dfef56e0ac 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs @@ -152,7 +152,6 @@ (when path-set (ptk/data-event :expand-token-sets {:paths path-set})) (dwtl/set-selected-token-set-id id) - (dwtl/set-token-type-section-open :color true) (let [{:keys [modal title]} (get dwta/token-properties :color) window-size (dom/get-window-size) left-sidebar (dom/get-element "left-sidebar-aside") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index e016bc354f..f7ced38f8d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -716,7 +716,7 @@ (remove str/empty?) (remove nil?) (distinct) - (filter #(= (cpn/butlast-path %) (:path filters)))) + (filter #(= (cpn/butlast-path % "/") (:path filters)))) groups (when-not search? (->> (sort (sequence xform components)) @@ -762,7 +762,7 @@ on-go-back (mf/use-fn (mf/deps (:path filters)) - #(swap! filters* assoc :path (cpn/butlast-path (:path filters)))) + #(swap! filters* assoc :path (cpn/butlast-path (:path filters) "/"))) on-enter-group (mf/use-fn #(swap! filters* assoc :path %)) diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 846c112bdb..57028b1bd1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -16,12 +16,8 @@ [app.main.ui.workspace.tokens.management.group :refer [token-group*]] [app.util.array :as array] [app.util.i18n :refer [tr]] - [okulary.core :as l] [rumext.v2 :as mf])) -(def ref:token-type-open-status - (l/derived (l/key :open-status-by-type) refs/workspace-tokens)) - (defn- get-sorted-token-groups "Separate token-types into groups of `empty` or `filled` depending if tokens exist for that type. Sort each group alphabetically (by their type)." @@ -82,7 +78,6 @@ [{:keys [tokens-lib active-tokens resolved-active-tokens]}] (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) - open-status (mf/deref ref:token-type-open-status) selected-shapes (mf/with-memo [selected objects] @@ -123,10 +118,6 @@ tokens)] (ctob/group-by-type tokens))) - - - - [empty-group filled-group] (mf/with-memo [tokens-by-type] (get-sorted-token-groups tokens-by-type))] @@ -151,7 +142,6 @@ (let [tokens (get tokens-by-type type)] [:> token-group* {:key (name type) :tokens tokens - :is-expanded (get open-status type false) :type type :selected-ids selected :selected-shapes selected-shapes diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index cb1f3a1902..a5738c9c55 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -181,6 +181,7 @@ (mf/deps validate-token token tokens token-type value-subfield type active-tab) (fn [form _event] (let [name (get-in @form [:clean-data :name]) + path (str (clojure.core/name token-type) "." name) description (get-in @form [:clean-data :description]) value (get-in @form [:clean-data :value]) value-for-validation (get-value-for-validator active-tab value value-subfield type)] @@ -202,6 +203,7 @@ {:name name :value (:value valid-token) :description description})) + (dwtl/toggle-path path) (dwtp/propagate-workspace-tokens) (modal/hide))))))))] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index 0d038a2324..4261b1925a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -23,8 +23,11 @@ [app.main.ui.workspace.tokens.management.token-tree :refer [token-tree*]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [okulary.core :as l] [rumext.v2 :as mf])) +(def ref:unfolded-token-paths + (l/derived (l/key :unfolded-token-paths) refs/workspace-tokens)) (defn token-section-icon [type] @@ -64,14 +67,16 @@ (mf/defc token-group* {::mf/schema schema:token-group} - [{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib is-expanded selected-ids]}] + [{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib selected-ids]}] (let [{:keys [modal title]} (get dwta/token-properties type) + + unfolded-token-paths (mf/deref ref:unfolded-token-paths) + is-type-unfolded (contains? (set unfolded-token-paths) (name type)) + editing-ref (mf/deref refs/workspace-editor-state) not-editing? (empty? editing-ref) - is-expanded (d/nilv is-expanded false) - can-edit? (mf/use-ctx ctx/can-edit?) @@ -95,24 +100,26 @@ on-toggle-open-click (mf/use-fn - (mf/deps is-expanded type) - #(st/emit! (dwtl/set-token-type-section-open type (not is-expanded)))) + (mf/deps type expandable?) + (fn [] + (when expandable? + (st/emit! (dwtl/toggle-path (name type)))))) on-popover-open-click (mf/use-fn (mf/deps type title modal) (fn [event] (dom/stop-propagation event) - (st/emit! (dwtl/set-token-type-section-open type true) - (let [pos (dom/get-client-position event)] - (modal/show (:key modal) - {:x (:x pos) - :y (:y pos) - :position :right - :fields (:fields modal) - :title title - :action "create" - :token-type type}))))) + (st/emit! + (let [pos (dom/get-client-position event)] + (modal/show (:key modal) + {:x (:x pos) + :y (:y pos) + :position :right + :fields (:fields modal) + :title title + :action "create" + :token-type type}))))) on-token-pill-click (mf/use-fn @@ -127,10 +134,10 @@ [:div {:class (stl/css :token-section-wrapper) :data-testid (dm/str "section-" (name type))} [:> layer-button* {:label title - :expanded is-expanded + :expanded is-type-unfolded :description (when expandable? (dm/str (count tokens))) :is-expandable expandable? - :aria-expanded is-expanded + :aria-expanded is-type-unfolded :aria-controls (dm/str "token-tree-" (name type)) :on-toggle-expand on-toggle-open-click :icon (token-section-icon type)} @@ -141,10 +148,12 @@ :variant "ghost" :on-click on-popover-open-click :class (stl/css :token-section-icon)}])] - (when is-expanded + (when is-type-unfolded [:> token-tree* {:tokens tokens + :type type :id (dm/str "token-tree-" (name type)) :tokens-lib tokens-lib + :unfolded-token-paths unfolded-token-paths :selected-shapes selected-shapes :active-theme-tokens active-theme-tokens :selected-token-set-id selected-token-set-id diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs index 5a31dbcd54..97ca77168e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs @@ -9,6 +9,8 @@ (:require [app.common.path-names :as cpn] [app.common.types.tokens-lib :as ctob] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.store :as st] [app.main.ui.ds.layers.layer-button :refer [layer-button*]] [app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]] [rumext.v2 :as mf])) @@ -16,6 +18,8 @@ (def ^:private schema:folder-node [:map [:node :any] + [:type :keyword] + [:unfolded-token-paths {:optional true} [:vector :string]] [:selected-shapes :any] [:is-selected-inside-layout {:optional true} :boolean] [:active-theme-tokens {:optional true} :any] @@ -26,18 +30,32 @@ (mf/defc folder-node* {::mf/schema schema:folder-node} - [{:keys [node selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib on-token-pill-click on-context-menu]}] - (let [expanded* (mf/use-state false) - expanded (deref expanded*) - swap-folder-expanded #(swap! expanded* not)] + [{:keys [node + type + unfolded-token-paths + selected-shapes + is-selected-inside-layout + active-theme-tokens + selected-token-set-id + tokens-lib + on-token-pill-click + on-context-menu]}] + (let [full-path (str (name type) "." (:path node)) + is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path) + + swap-folder-expanded (mf/use-fn + (mf/deps (:path node) type) + (fn [] + (let [path (str (name type) "." (:path node))] + (st/emit! (dwtl/toggle-path path)))))] [:li {:class (stl/css :folder-node)} [:> layer-button* {:label (:name node) - :expanded expanded - :aria-expanded expanded + :expanded is-folder-expanded + :aria-expanded is-folder-expanded :aria-controls (str "folder-children-" (:path node)) :is-expandable (not (:leaf node)) :on-toggle-expand swap-folder-expanded}] - (when expanded + (when is-folder-expanded (let [children-fn (:children-fn node)] [:div {:class (stl/css :folder-children-wrapper) :id (str "folder-children-" (:path node))} @@ -47,7 +65,9 @@ (if (not (:leaf child)) [:ul {:class (stl/css :node-parent)} [:> folder-node* {:key (:path child) + :type type :node child + :unfolded-token-paths unfolded-token-paths :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens @@ -69,6 +89,8 @@ (def ^:private schema:token-tree [:map [:tokens :any] + [:type :keyword] + [:unfolded-token-paths {:optional true} [:vector :string]] [:selected-shapes :any] [:is-selected-inside-layout {:optional true} :boolean] [:active-theme-tokens {:optional true} :any] @@ -79,7 +101,16 @@ (mf/defc token-tree* {::mf/schema schema:token-tree} - [{:keys [tokens selected-shapes is-selected-inside-layout active-theme-tokens tokens-lib selected-token-set-id on-token-pill-click on-context-menu]}] + [{:keys [tokens + type + unfolded-token-paths + selected-shapes + is-selected-inside-layout + active-theme-tokens + tokens-lib + selected-token-set-id + on-token-pill-click + on-context-menu]}] (let [separator "." tree (mf/use-memo (mf/deps tokens) @@ -87,24 +118,25 @@ (cpn/build-tree-root tokens separator)))] [:div {:class (stl/css :token-tree-wrapper)} (for [node tree] - [:ul {:class (stl/css :node-parent) - :key (:path node) - :style {:--node-depth (inc (:depth node))}} - (if (:leaf node) - (let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))] - [:> token-pill* - {:token token - :selected-shapes selected-shapes - :is-selected-inside-layout is-selected-inside-layout - :active-theme-tokens active-theme-tokens - :on-click on-token-pill-click - :on-context-menu on-context-menu}]) + (if (:leaf node) + (let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))] + [:> token-pill* + {:token token + :selected-shapes selected-shapes + :is-selected-inside-layout is-selected-inside-layout + :active-theme-tokens active-theme-tokens + :on-click on-token-pill-click + :on-context-menu on-context-menu}]) ;; Render segment folder + [:ul {:class (stl/css :node-parent) + :key (:path node)} [:> folder-node* {:node node + :type type + :unfolded-token-paths unfolded-token-paths :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-token-pill-click on-token-pill-click :on-context-menu on-context-menu :tokens-lib tokens-lib - :selected-token-set-id selected-token-set-id}])])])) + :selected-token-set-id selected-token-set-id}]]))])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss index 3320379d04..7b1ea0244e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss @@ -7,24 +7,41 @@ @use "ds/_borders.scss" as *; .token-tree-wrapper { + --node-spacing: var(--sp-s); + padding-block-end: var(--sp-s); + display: flex; + flex-wrap: wrap; + gap: var(--sp-s); + padding-inline-start: calc(var(--node-spacing)); + + & .node-parent { + flex: 1 0 100%; + + &:last-of-type + * { + margin-block-end: var(--sp-s); + } + } + + & .token-pill { + flex: 0 0 auto; + } } .node-parent { - --node-spacing: var(--sp-l); - --node-depth: 0; - margin-block-end: 0; - padding-inline-start: calc(var(--node-spacing) * var(--node-depth)); } -.folder-children-wrapper:has(> button) { +.folder-children-wrapper { margin-inline-start: var(--sp-s); padding-inline-start: var(--sp-s); border-inline-start: $b-2 solid var(--color-background-quaternary); - display: flex; - flex-wrap: wrap; - column-gap: var(--sp-xs); + + &:has(> button) { + display: flex; + flex-wrap: wrap; + gap: var(--sp-xs); + } & .node-parent { flex: 1 0 100%;