mirror of
https://github.com/penpot/penpot.git
synced 2026-03-26 13:20:51 +01:00
🎉 Add natural sort on token names (#8672)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -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"}])))
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user