From 2240d930690cfcc9dfe0211264e2d06c2a5b4db7 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Fri, 9 Jan 2026 09:56:18 +0100 Subject: [PATCH] :sparkles: Save unfolded tokens path (#7949) --- CHANGES.md | 6 +- .../data/workspace/tokens/library_edit.cljs | 33 +++++++- .../workspace/colorpicker/color_tokens.cljs | 1 - .../main/ui/workspace/tokens/management.cljs | 10 --- .../tokens/management/forms/generic_form.cljs | 3 + .../ui/workspace/tokens/management/group.cljs | 45 ++++++----- .../tokens/management/token_tree.cljs | 75 +++++++++++++------ .../tokens/management/token_tree.scss | 33 ++++++-- 8 files changed, 139 insertions(+), 67 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 17fe6a2897..3f86081827 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,10 +11,10 @@ ### :sparkles: New features & Enhancements - Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202) +- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966) ### :bug: Bugs fixed - ## 2.13.0 (Unreleased) ### :boom: Breaking changes & Deprecations @@ -46,7 +46,6 @@ - Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959) - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865) - ## 2.12.1 ### :bug: Bugs fixed @@ -55,7 +54,6 @@ - Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935) - Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917) - ## 2.12.0 ### :boom: Breaking changes & Deprecations @@ -67,7 +65,6 @@ The backend RPC API URLS are changed from `/api/rpc/command/` to compatibility; however, if you are a user of this API, it is strongly recommended that you adapt your code to use the new PATH. - #### Updated SSO Callback URL The OAuth / Single Sign-On (SSO) callback endpoint has changed to @@ -100,7 +97,6 @@ This update standardizes all authentication flows under the single URL and makis it more modular, enabling the ability to configure SSO auth provider dinamically. - #### Changes on default docker compose We have updated the `docker/images/docker-compose.yaml` with a small 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..25fc451cf8 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-tokens-paths + [] + (ptk/reify ::clean-tokens-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-token-path + [path] + (ptk/reify ::toggle-token-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/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 55b2933a77..c5ab6a428a 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 @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.management.forms.generic-form (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.files.tokens :as cft] [app.common.schema :as sm] [app.common.types.token :as cto] @@ -180,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 (d/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)] @@ -220,6 +222,7 @@ {:name name :value (:value valid-token) :description description})) + (dwtl/toggle-token-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..b3450e8665 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-token-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..e7da275b39 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-token-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,26 @@ (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 + :key (:id (:leaf node)) + :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%;