mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
* 🐛 Fix token tree collapsing repeated path segments Tokens whose path contains the same segment name twice in a row (e.g. test.pointer.pointer) collapsed to a single level in the token tree. build-tree-node marked a node as a leaf when the current segment name matched the first segment of every remaining path. Because group-by-first-segment rewrites nested paths into the segment name, a still-nested remaining path was indistinguishable from a token ending at the current node, so test.pointer.pointer merged into one leaf. Mark rewritten segments with an internal ::remaining? marker and strip it when the rest-path is fully consumed; is-leaf? only accepts unmarked remaining segments as leaves. Closes #11588 AI-assisted-by: glm-5.3-flash Signed-off-by: Junsoo Choi <junsoo1172@gmail.com> * 🐛 Preserve ancestor tokens in path trees Keep a node's token and descendants independently when imported paths contain both an ancestor and a child. Render both in the token tree and include the ancestor token in existing path-based group operations. Cover accepted same-set imports, leaf IDs, repeated segments, and internal marker removal without changing import or collision validation. AI-assisted-by: GPT-6 Signed-off-by: makesomethingshit <junsoo1172@gmail.com> * ♻️ Rework token tree path building Carry the segments left to descend as a ::remaining-path vector on each segment and advance it by subvec, instead of rewriting the segment :name and re-splitting it later. With that, build-tree-node finds the leaf token with a predicate over ::remaining-path and takes children from the segments that still have one, so the pre-grouping children-first sort no longer changes the result. Drop sort-children-first and its segment-has-children? helper, and document inside-path?. AI-assisted-by: deepseek-v4.1-flash * 🐛 Fix and relocate the token tree tests The ancestor-and-descendant test called a function that does not exist in app.common.types.tokens-lib, so the namespace failed to load and the whole common suite errored. Use make-tokens-status-from-lib and the two-argument get-tokens-in-active-sets instead. Move the build-tree-root tests to path-names-tree-test so path-names-test stays a pure split/join unit test. AI-assisted-by: deepseek-v4.1-flash * ♻️ Refactor the token tree rendering and share path helpers Extract the token pill into a leaf-node* component and treat a node as a leaf only when it holds a token and has no children, so a node can carry both a token and its children. Make token-separator public and reuse it from the token tree and the management panel instead of local copies. Both management path filters inlined the same ancestor-or-descendant rule; extract it as token-in-path? and cover it with a unit test. AI-assisted-by: deepseek-v4.1-flash --------- Signed-off-by: Junsoo Choi <junsoo1172@gmail.com> Signed-off-by: makesomethingshit <junsoo1172@gmail.com> Co-authored-by: luisddm <luis.dedios@kaieldos.net>
98 lines
4.7 KiB
Clojure
98 lines
4.7 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 SUBSIDIARY SL
|
|
|
|
(ns common-tests.path-names-tree-test
|
|
(:require
|
|
[app.common.files.tokens :as cfo]
|
|
[app.common.path-names :as cpn]
|
|
[app.common.types.tokens-lib :as ctob]
|
|
[clojure.test :as t]))
|
|
|
|
(defn- single-chain-terminal
|
|
"Follows the first child down a single-token tree to its terminal node."
|
|
[root]
|
|
(loop [node root]
|
|
(if (:children node)
|
|
(recur (first (:children node)))
|
|
node)))
|
|
|
|
(defn- tree-payloads
|
|
"Collects every node and :leaf map in a tree for marker-leak checks."
|
|
[node]
|
|
(cons node
|
|
(concat (when (:leaf node) [(:leaf node)])
|
|
(mapcat tree-payloads (:children node)))))
|
|
|
|
(defn- tree-leaves
|
|
"Collects every token leaf from a tree."
|
|
[nodes]
|
|
(mapcat (fn [node]
|
|
(concat (when (:leaf node) [(:leaf node)])
|
|
(tree-leaves (:children node))))
|
|
nodes))
|
|
|
|
(t/deftest build-tree-root-preserves-repeated-segments
|
|
(t/testing "#11588: adjacent repeated path segments keep their depth"
|
|
(let [tree (cpn/build-tree-root [{:name "test.pointer.pointer" :id "t1"}] ".")
|
|
terminal (single-chain-terminal (first tree))]
|
|
(t/is (= "test" (-> tree first :name)))
|
|
(t/is (= "pointer" (-> tree first :children first :name)))
|
|
(t/is (nil? (-> tree first :children first :leaf)))
|
|
(t/is (= "pointer" (:name terminal)))
|
|
(t/is (= "test.pointer.pointer" (:path terminal)))
|
|
(t/is (= {:name "pointer" :id "t1"} (:leaf terminal)))
|
|
(t/is (nil? (:children terminal)))
|
|
(t/is (every? #(not (contains? % :app.common.path-names/remaining-path))
|
|
(tree-payloads (first tree))))))
|
|
(t/testing "control case without repeated segments is unchanged"
|
|
(let [tree (cpn/build-tree-root [{:name "test.thing.other-thing" :id "t2"}] ".")
|
|
terminal (single-chain-terminal (first tree))]
|
|
(t/is (= "test" (-> tree first :name)))
|
|
(t/is (= "thing" (-> tree first :children first :name)))
|
|
(t/is (= "other-thing" (:name terminal)))
|
|
(t/is (= {:name "other-thing" :id "t2"} (:leaf terminal)))))
|
|
(t/testing "repeated and non-repeated siblings coexist"
|
|
(let [tree (cpn/build-tree-root [{:name "test.pointer.pointer" :id "t1"}
|
|
{:name "test.thing.other-thing" :id "t2"}] ".")
|
|
children (-> tree first :children)]
|
|
(t/is (= ["pointer" "thing"] (mapv :name children)))
|
|
(t/is (= {:name "pointer" :id "t1"} (-> children first :children first :leaf)))))
|
|
(t/testing "longer repeated runs keep every depth"
|
|
(doseq [[path id] [["test.test.pointer" "t3"] ["test.test.test" "t4"]
|
|
["a.a" "t5"] ["a.b.b" "t6"] ["a.a.a" "t8"]]]
|
|
(let [segments (cpn/split-path path :separator ".")
|
|
root (first (cpn/build-tree-root [{:name path :id id}] "."))
|
|
terminal (single-chain-terminal root)]
|
|
(t/is (= (last segments) (:name terminal)) path)
|
|
(t/is (= (inc (count segments)) (count (tree-payloads root))) path)
|
|
(t/is (= {:name (last segments) :id id} (:leaf terminal)) path)))))
|
|
|
|
(t/deftest build-tree-root-preserves-active-set-ancestor-and-descendant
|
|
(t/testing "#11588: an ancestor token and its descendant in the active set are both kept"
|
|
(let [tokens-lib (ctob/read-multi-set-dtcg
|
|
{"Global" {"a.a" {"$value" "#aaa" "$type" "color"}
|
|
"a.a.a" {"$value" "#bbb" "$type" "color"}}
|
|
"$themes" []
|
|
"$metadata" {"tokenSetOrder" ["Global"]
|
|
"activeSets" ["Global"]}})
|
|
tokens-status (cfo/make-tokens-status-from-lib tokens-lib)
|
|
active-tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)
|
|
tree (cpn/build-tree-root (vec (vals active-tokens)) ".")
|
|
collision (-> tree first :children first)
|
|
leaves (tree-leaves tree)]
|
|
(t/is (= #{"a.a" "a.a.a"} (set (keys active-tokens))))
|
|
(t/is (= 2 (count leaves)))
|
|
(t/is (= #{(get-in active-tokens ["a.a" :id])
|
|
(get-in active-tokens ["a.a.a" :id])}
|
|
(set (map :id leaves))))
|
|
(t/is (= "a.a" (:path collision)))
|
|
(t/is (= (get-in active-tokens ["a.a" :id])
|
|
(get-in collision [:leaf :id])))
|
|
(t/is (= (get-in active-tokens ["a.a.a" :id])
|
|
(get-in collision [:children 0 :leaf :id])))
|
|
(t/is (every? #(not (contains? % :app.common.path-names/remaining-path))
|
|
(mapcat tree-payloads tree))))))
|