Compare commits

...

4 Commits

Author SHA1 Message Date
Xavier Julian d999762ad4 Remove tokens tree node 2026-01-08 23:03:45 +01:00
Xavier Julian 316bb4561d ♻️ Cleanup code 2026-01-08 10:32:47 +01:00
Xavier Julian 7b25924e14 📎 Add to CHANGES 2026-01-07 23:20:06 +01:00
Xavier Julian 95c172e781 Save unfolded tokens path 2026-01-07 22:40:50 +01:00
9 changed files with 184 additions and 79 deletions

View File

@ -10,8 +10,9 @@
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
### :bug: Bugs fixed
## 2.13.0 (Unreleased)
@ -37,7 +38,6 @@
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
## 2.12.1
### :bug: Bugs fixed
@ -46,7 +46,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
@ -58,7 +57,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
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
@ -91,7 +89,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

View File

@ -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}]

View File

@ -8,7 +8,9 @@
(:require-macros
[app.main.style :as stl])
(:require
[app.main.refs :as refs]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(def ^:private schema:layer-button
@ -19,17 +21,19 @@
[:expandable {:optional true} :boolean]
[:expanded {:optional true} :boolean]
[:icon {:optional true} :string]
[:on-toggle-expand fn?]])
[:on-toggle-expand {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
(mf/defc layer-button*
{::mf/schema schema:layer-button}
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
[{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}]
(let [button-props (mf/spread-props props
{:class [class (stl/css-case :layer-button true
:layer-button--expandable is-expandable
:layer-button--expanded expanded)]
:type "button"
:on-click on-toggle-expand})]
:on-click on-toggle-expand
:on-context-menu on-context-menu})]
[:div {:class (stl/css :layer-button-wrapper)}
[:> "button" button-props
[:div {:class (stl/css :layer-button-content)}

View File

@ -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")

View File

@ -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

View File

@ -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-token-path path)
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]

View File

@ -10,6 +10,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pprint :as pp]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
@ -23,8 +24,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 +68,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?)
@ -83,7 +89,7 @@
expandable? (d/nilv (seq tokens) false)
on-context-menu
on-pill-context-menu
(mf/use-fn
(fn [event token]
(dom/prevent-default event)
@ -93,26 +99,39 @@
:errors (:errors token)
:token-id (:id token)}))))
on-node-context-menu
(mf/use-fn
(fn [event node]
(dom/prevent-default event)
(pp/pprint node)
#_(st/emit! (dwtl/assign-token-context-menu
{:type :token
:position (dom/get-client-position event)
:errors (:errors token)
:token-id (:id token)}))))
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 +146,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,13 +160,16 @@
: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
:is-selected-inside-layout is-selected-inside-layout
:on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu}])]))
:on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu}])]))

View File

@ -9,35 +9,62 @@
(: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.refs :as refs]
[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*]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(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]
[:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
[:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(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-pill-context-menu
on-node-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)))))
node-context-menu-prep (mf/use-fn
(mf/deps on-node-context-menu node)
(fn [event]
(when on-node-context-menu
(on-node-context-menu event node))))]
[: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
:on-toggle-expand swap-folder-expanded
:on-context-menu node-context-menu-prep}]
(when is-folder-expanded
(let [children-fn (:children-fn node)]
[:div {:class (stl/css :folder-children-wrapper)
:id (str "folder-children-" (:path node))}
@ -47,12 +74,14 @@
(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
:on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu
:on-node-context-menu on-node-context-menu
:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]]
(let [id (:id (:leaf child))
@ -64,47 +93,67 @@
: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}])))))]))]))
:on-context-menu on-pill-context-menu}])))))]))]))
(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]
[:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
[:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(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-pill-context-menu
on-node-context-menu]}]
(let [separator "."
tree (mf/use-memo
(mf/deps tokens)
(fn []
(cpn/build-tree-root tokens separator)))]
(cpn/build-tree-root tokens separator)))
can-edit? (:can-edit (deref refs/permissions))
on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu)
(fn [event node]
(when can-edit?
(on-node-context-menu event node))))]
[: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-pill-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
:on-node-context-menu on-node-context-menu
: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 *;
.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%;