Save unfolded tokens path (#7949)

This commit is contained in:
Xaviju 2026-01-09 09:56:18 +01:00 committed by GitHub
parent 3f4506284b
commit 2240d93069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 139 additions and 67 deletions

View File

@ -11,10 +11,10 @@
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202) - 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 ### :bug: Bugs fixed
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations ### :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 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) - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
## 2.12.1 ## 2.12.1
### :bug: Bugs fixed ### :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 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) - Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0 ## 2.12.0
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@ -67,7 +65,6 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH. recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL #### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to 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 and makis it more modular, enabling the ability to configure SSO auth
provider dinamically. provider dinamically.
#### Changes on default docker compose #### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small We have updated the `docker/images/docker-compose.yaml` with a small

View File

@ -11,6 +11,7 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.logic.tokens :as clt] [app.common.logic.tokens :as clt]
[app.common.path-names :as cpn]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -22,6 +23,7 @@
[app.main.data.workspace.tokens.propagation :as dwtp] [app.main.data.workspace.tokens.propagation :as dwtp]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(declare set-selected-token-set-id) (declare set-selected-token-set-id)
@ -460,12 +462,35 @@
;; TOKEN UI OPS ;; TOKEN UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-token-type-section-open (defn clean-tokens-paths
[token-type open?] []
(ptk/reify ::set-token-type-section-open (ptk/reify ::clean-tokens-paths
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 (defn assign-token-context-menu
[{:keys [position] :as params}] [{:keys [position] :as params}]

View File

@ -152,7 +152,6 @@
(when path-set (when path-set
(ptk/data-event :expand-token-sets {:paths path-set})) (ptk/data-event :expand-token-sets {:paths path-set}))
(dwtl/set-selected-token-set-id id) (dwtl/set-selected-token-set-id id)
(dwtl/set-token-type-section-open :color true)
(let [{:keys [modal title]} (get dwta/token-properties :color) (let [{:keys [modal title]} (get dwta/token-properties :color)
window-size (dom/get-window-size) window-size (dom/get-window-size)
left-sidebar (dom/get-element "left-sidebar-aside") left-sidebar (dom/get-element "left-sidebar-aside")

View File

@ -16,12 +16,8 @@
[app.main.ui.workspace.tokens.management.group :refer [token-group*]] [app.main.ui.workspace.tokens.management.group :refer [token-group*]]
[app.util.array :as array] [app.util.array :as array]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf])) [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 (defn- get-sorted-token-groups
"Separate token-types into groups of `empty` or `filled` depending if "Separate token-types into groups of `empty` or `filled` depending if
tokens exist for that type. Sort each group alphabetically (by their type)." tokens exist for that type. Sort each group alphabetically (by their type)."
@ -82,7 +78,6 @@
[{:keys [tokens-lib active-tokens resolved-active-tokens]}] [{:keys [tokens-lib active-tokens resolved-active-tokens]}]
(let [objects (mf/deref refs/workspace-page-objects) (let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes) selected (mf/deref refs/selected-shapes)
open-status (mf/deref ref:token-type-open-status)
selected-shapes selected-shapes
(mf/with-memo [selected objects] (mf/with-memo [selected objects]
@ -123,10 +118,6 @@
tokens)] tokens)]
(ctob/group-by-type tokens))) (ctob/group-by-type tokens)))
[empty-group filled-group] [empty-group filled-group]
(mf/with-memo [tokens-by-type] (mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))] (get-sorted-token-groups tokens-by-type))]
@ -151,7 +142,6 @@
(let [tokens (get tokens-by-type type)] (let [tokens (get tokens-by-type type)]
[:> token-group* {:key (name type) [:> token-group* {:key (name type)
:tokens tokens :tokens tokens
:is-expanded (get open-status type false)
:type type :type type
:selected-ids selected :selected-ids selected
:selected-shapes selected-shapes :selected-shapes selected-shapes

View File

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.management.forms.generic-form (ns app.main.ui.workspace.tokens.management.forms.generic-form
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cft]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
@ -180,6 +181,7 @@
(mf/deps validate-token token tokens token-type value-subfield type active-tab) (mf/deps validate-token token tokens token-type value-subfield type active-tab)
(fn [form _event] (fn [form _event]
(let [name (get-in @form [:clean-data :name]) (let [name (get-in @form [:clean-data :name])
path (str (d/name token-type) "." name)
description (get-in @form [:clean-data :description]) description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value]) value (get-in @form [:clean-data :value])
value-for-validation (get-value-for-validator active-tab value value-subfield type)] value-for-validation (get-value-for-validator active-tab value value-subfield type)]
@ -220,6 +222,7 @@
{:name name {:name name
:value (:value valid-token) :value (:value valid-token)
:description description})) :description description}))
(dwtl/toggle-token-path path)
(dwtp/propagate-workspace-tokens) (dwtp/propagate-workspace-tokens)
(modal/hide!))))))))))] (modal/hide!))))))))))]

View File

@ -23,8 +23,11 @@
[app.main.ui.workspace.tokens.management.token-tree :refer [token-tree*]] [app.main.ui.workspace.tokens.management.token-tree :refer [token-tree*]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ref:unfolded-token-paths
(l/derived (l/key :unfolded-token-paths) refs/workspace-tokens))
(defn token-section-icon (defn token-section-icon
[type] [type]
@ -64,14 +67,16 @@
(mf/defc token-group* (mf/defc token-group*
{::mf/schema schema: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]} (let [{:keys [modal title]}
(get dwta/token-properties type) (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) editing-ref (mf/deref refs/workspace-editor-state)
not-editing? (empty? editing-ref) not-editing? (empty? editing-ref)
is-expanded (d/nilv is-expanded false)
can-edit? can-edit?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit?)
@ -95,24 +100,26 @@
on-toggle-open-click on-toggle-open-click
(mf/use-fn (mf/use-fn
(mf/deps is-expanded type) (mf/deps type expandable?)
#(st/emit! (dwtl/set-token-type-section-open type (not is-expanded)))) (fn []
(when expandable?
(st/emit! (dwtl/toggle-token-path (name type))))))
on-popover-open-click on-popover-open-click
(mf/use-fn (mf/use-fn
(mf/deps type title modal) (mf/deps type title modal)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dwtl/set-token-type-section-open type true) (st/emit!
(let [pos (dom/get-client-position event)] (let [pos (dom/get-client-position event)]
(modal/show (:key modal) (modal/show (:key modal)
{:x (:x pos) {:x (:x pos)
:y (:y pos) :y (:y pos)
:position :right :position :right
:fields (:fields modal) :fields (:fields modal)
:title title :title title
:action "create" :action "create"
:token-type type}))))) :token-type type})))))
on-token-pill-click on-token-pill-click
(mf/use-fn (mf/use-fn
@ -127,10 +134,10 @@
[:div {:class (stl/css :token-section-wrapper) [:div {:class (stl/css :token-section-wrapper)
:data-testid (dm/str "section-" (name type))} :data-testid (dm/str "section-" (name type))}
[:> layer-button* {:label title [:> layer-button* {:label title
:expanded is-expanded :expanded is-type-unfolded
:description (when expandable? (dm/str (count tokens))) :description (when expandable? (dm/str (count tokens)))
:is-expandable expandable? :is-expandable expandable?
:aria-expanded is-expanded :aria-expanded is-type-unfolded
:aria-controls (dm/str "token-tree-" (name type)) :aria-controls (dm/str "token-tree-" (name type))
:on-toggle-expand on-toggle-open-click :on-toggle-expand on-toggle-open-click
:icon (token-section-icon type)} :icon (token-section-icon type)}
@ -141,10 +148,12 @@
:variant "ghost" :variant "ghost"
:on-click on-popover-open-click :on-click on-popover-open-click
:class (stl/css :token-section-icon)}])] :class (stl/css :token-section-icon)}])]
(when is-expanded (when is-type-unfolded
[:> token-tree* {:tokens tokens [:> token-tree* {:tokens tokens
:type type
:id (dm/str "token-tree-" (name type)) :id (dm/str "token-tree-" (name type))
:tokens-lib tokens-lib :tokens-lib tokens-lib
:unfolded-token-paths unfolded-token-paths
:selected-shapes selected-shapes :selected-shapes selected-shapes
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:selected-token-set-id selected-token-set-id :selected-token-set-id selected-token-set-id

View File

@ -9,6 +9,8 @@
(:require (:require
[app.common.path-names :as cpn] [app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob] [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.ds.layers.layer-button :refer [layer-button*]]
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]] [app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -16,6 +18,8 @@
(def ^:private schema:folder-node (def ^:private schema:folder-node
[:map [:map
[:node :any] [:node :any]
[:type :keyword]
[:unfolded-token-paths {:optional true} [:vector :string]]
[:selected-shapes :any] [:selected-shapes :any]
[:is-selected-inside-layout {:optional true} :boolean] [:is-selected-inside-layout {:optional true} :boolean]
[:active-theme-tokens {:optional true} :any] [:active-theme-tokens {:optional true} :any]
@ -26,18 +30,32 @@
(mf/defc folder-node* (mf/defc folder-node*
{::mf/schema schema: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]}] [{:keys [node
(let [expanded* (mf/use-state false) type
expanded (deref expanded*) unfolded-token-paths
swap-folder-expanded #(swap! expanded* not)] 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)} [:li {:class (stl/css :folder-node)}
[:> layer-button* {:label (:name node) [:> layer-button* {:label (:name node)
:expanded expanded :expanded is-folder-expanded
:aria-expanded expanded :aria-expanded is-folder-expanded
:aria-controls (str "folder-children-" (:path node)) :aria-controls (str "folder-children-" (:path node))
:is-expandable (not (:leaf node)) :is-expandable (not (:leaf node))
:on-toggle-expand swap-folder-expanded}] :on-toggle-expand swap-folder-expanded}]
(when expanded (when is-folder-expanded
(let [children-fn (:children-fn node)] (let [children-fn (:children-fn node)]
[:div {:class (stl/css :folder-children-wrapper) [:div {:class (stl/css :folder-children-wrapper)
:id (str "folder-children-" (:path node))} :id (str "folder-children-" (:path node))}
@ -47,7 +65,9 @@
(if (not (:leaf child)) (if (not (:leaf child))
[:ul {:class (stl/css :node-parent)} [:ul {:class (stl/css :node-parent)}
[:> folder-node* {:key (:path child) [:> folder-node* {:key (:path child)
:type type
:node child :node child
:unfolded-token-paths unfolded-token-paths
:selected-shapes selected-shapes :selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
@ -69,6 +89,8 @@
(def ^:private schema:token-tree (def ^:private schema:token-tree
[:map [:map
[:tokens :any] [:tokens :any]
[:type :keyword]
[:unfolded-token-paths {:optional true} [:vector :string]]
[:selected-shapes :any] [:selected-shapes :any]
[:is-selected-inside-layout {:optional true} :boolean] [:is-selected-inside-layout {:optional true} :boolean]
[:active-theme-tokens {:optional true} :any] [:active-theme-tokens {:optional true} :any]
@ -79,7 +101,16 @@
(mf/defc token-tree* (mf/defc token-tree*
{::mf/schema schema: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 "." (let [separator "."
tree (mf/use-memo tree (mf/use-memo
(mf/deps tokens) (mf/deps tokens)
@ -87,24 +118,26 @@
(cpn/build-tree-root tokens separator)))] (cpn/build-tree-root tokens separator)))]
[:div {:class (stl/css :token-tree-wrapper)} [:div {:class (stl/css :token-tree-wrapper)}
(for [node tree] (for [node tree]
[:ul {:class (stl/css :node-parent) (if (:leaf node)
:key (:path node) (let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))]
:style {:--node-depth (inc (:depth node))}} [:> token-pill*
(if (:leaf node) {:token token
(let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))] :key (:id (:leaf node))
[:> token-pill* :selected-shapes selected-shapes
{:token token :is-selected-inside-layout is-selected-inside-layout
:selected-shapes selected-shapes :active-theme-tokens active-theme-tokens
:is-selected-inside-layout is-selected-inside-layout :on-click on-token-pill-click
:active-theme-tokens active-theme-tokens :on-context-menu on-context-menu}])
:on-click on-token-pill-click
:on-context-menu on-context-menu}])
;; Render segment folder ;; Render segment folder
[:ul {:class (stl/css :node-parent)
:key (:path node)}
[:> folder-node* {:node node [:> folder-node* {:node node
:type type
:unfolded-token-paths unfolded-token-paths
:selected-shapes selected-shapes :selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:on-token-pill-click on-token-pill-click :on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu :on-context-menu on-context-menu
:tokens-lib tokens-lib :tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}])])])) :selected-token-set-id selected-token-set-id}]]))]))

View File

@ -7,24 +7,41 @@
@use "ds/_borders.scss" as *; @use "ds/_borders.scss" as *;
.token-tree-wrapper { .token-tree-wrapper {
--node-spacing: var(--sp-s);
padding-block-end: 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-parent {
--node-spacing: var(--sp-l);
--node-depth: 0;
margin-block-end: 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); margin-inline-start: var(--sp-s);
padding-inline-start: var(--sp-s); padding-inline-start: var(--sp-s);
border-inline-start: $b-2 solid var(--color-background-quaternary); border-inline-start: $b-2 solid var(--color-background-quaternary);
display: flex;
flex-wrap: wrap; &:has(> button) {
column-gap: var(--sp-xs); display: flex;
flex-wrap: wrap;
gap: var(--sp-xs);
}
& .node-parent { & .node-parent {
flex: 1 0 100%; flex: 1 0 100%;