🎉 Add natural sort on token names (#8672)

This commit is contained in:
Eva Marco
2026-03-23 11:24:59 +01:00
committed by GitHub
parent bfb331d230
commit 4345cfaec7
4 changed files with 94 additions and 3 deletions

View File

@@ -19,6 +19,7 @@
- Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498)
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
### :bug: Bugs fixed

View File

@@ -1126,6 +1126,51 @@
(let [value (format-precision value precision)]
(str value))))))
(defn- natural-sort-key
"Splits a string into a sequence of alternating string and number segments,
converting numeric segments to longs/ints so they compare by value rather
than lexicographically. e.g. \"size10b\" => (\"size\" 10 \"b\")"
[s]
(map (fn [part]
(if (re-matches #"\d+" part)
#?(:clj (Long/parseLong part)
:cljs (js/parseInt part))
part))
(re-seq #"\d+|\D+" s)))
(defn- natural-compare
"Comparator that orders strings naturally, sorting numeric segments by value
rather than lexicographically. Returns a negative number, zero, or positive
number when a is before, equal to, or after b respectively.
e.g. \"size2\" < \"size10\" instead of \"size10\" < \"size2\"."
[a b]
(loop [ka (natural-sort-key a)
kb (natural-sort-key b)]
(cond
(and (empty? ka) (empty? kb)) 0
(empty? ka) -1
(empty? kb) 1
:else
(let [pa (first ka)
pb (first kb)
result (cond
(and (number? pa) (number? pb)) (compare pa pb)
(and (string? pa) (string? pb)) (compare pa pb)
(number? pa) -1
:else 1)]
(if (zero? result)
(recur (rest ka) (rest kb))
result)))))
(defn natural-sort-by
"Sorts coll by extracting a string key with keyfn and ordering elements
using natural sort order, where embedded numbers are compared by value
rather than lexicographically.
e.g. (natural-sort-by :name [{:name \"size10\"} {:name \"size2\"}])
=> [{:name \"size2\"} {:name \"size10\"}]"
[key coll]
(sort-by key natural-compare coll))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Util protocols
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -137,3 +137,46 @@
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 1) 3))
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7))
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil)))
(t/deftest natural-sort-by-test
(t/is (= (d/natural-sort-by identity ["10" "2" "1" "11" "3" "30"])
["1" "2" "3" "10" "11" "30"]))
(t/is (= (d/natural-sort-by identity ["banana" "apple" "cherry"])
["apple" "banana" "cherry"]))
(t/is (= (d/natural-sort-by identity ["size10" "size2" "size1" "size20" "size3"])
["size1" "size2" "size3" "size10" "size20"]))
(t/is (= (d/natural-sort-by identity ["b1" "a2" "a10" "a1"])
["a1" "a2" "a10" "b1"]))
(t/is (= (d/natural-sort-by identity []) []))
(t/is (= (d/natural-sort-by identity ["solo"]) ["solo"]))
(t/is (= (d/natural-sort-by identity ["b" "a" "a" "c"])
["a" "a" "b" "c"]))
(t/is (= (d/natural-sort-by :name
[{:name "big"} {:name "small"} {:name "medium"}])
[{:name "big"} {:name "medium"} {:name "small"}]))
(t/is (= (d/natural-sort-by :name
[{:name "size10"} {:name "size2"} {:name "size1"}])
[{:name "size1"} {:name "size2"} {:name "size10"}]))
(t/is (= (d/natural-sort-by :name
[{:name "border-radius-10"}
{:name "border-radius-2"}
{:name "border-radius-1"}])
[{:name "border-radius-1"}
{:name "border-radius-2"}
{:name "border-radius-10"}]))
(t/is (= (d/natural-sort-by :name
[{:name "border-10-radius"}
{:name "border-2-radius"}
{:name "border-1-radius"}])
[{:name "border-1-radius"}
{:name "border-2-radius"}
{:name "border-10-radius"}]))
(t/is (= (d/natural-sort-by :name
[{:name "border-10-radius"}
{:name "border-2-extra"}
{:name "border-2-radius"}
{:name "border-1-radius"}])
[{:name "border-1-radius"}
{:name "border-2-extra"}
{:name "border-2-radius"}
{:name "border-10-radius"}])))

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.management.token-tree
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.library-edit :as dwtl]
@@ -69,8 +70,8 @@
[:div {:class (stl/css :folder-children-wrapper)
:id (str "folder-children-" (:path node))}
(when children-fn
(let [children (children-fn)]
(for [child children]
(let [sorted-children (d/natural-sort-by :name (children-fn))]
(for [child sorted-children]
(if (not (:leaf child))
[:ul {:class (stl/css :node-parent)
:key (:path child)}
@@ -127,7 +128,8 @@
tree (mf/use-memo
(mf/deps tokens)
(fn []
(cpn/build-tree-root tokens separator)))
(->> (cpn/build-tree-root tokens separator)
(d/natural-sort-by :name))))
can-edit? (:can-edit (deref refs/permissions))
on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu)