0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Merge pull request #283 from tokens-studio/refactor-types-2

Refactor types 2
This commit is contained in:
Florian Schrödl 2024-09-17 09:44:00 +02:00 committed by GitHub
commit 0697e69888
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1332 additions and 280 deletions

View file

@ -57,6 +57,54 @@
#?(:cljs (instance? lkm/LinkedMap o)
:clj (instance? LinkedMap o)))
(defn oassoc
[o & kvs]
(apply assoc (or o (ordered-map)) kvs))
(defn oassoc-in
[o [k & ks] v]
(if ks
(oassoc o k (oassoc-in (get o k) ks v))
(oassoc o k v)))
(defn oupdate-in
[m ks f & args]
(let [up (fn up [m ks f args]
(let [[k & ks] ks]
(if ks
(oassoc m k (up (get m k) ks f args))
(oassoc m k (apply f (get m k) args)))))]
(up m ks f args)))
(declare index-of)
(defn oassoc-before
"Assoc a k v pair, in the order position just before the other key"
[o before-k k v]
(if-let [index (index-of (keys o) before-k)]
(-> (ordered-map)
(into (take index o))
(assoc k v)
(into (drop index o)))
(oassoc o k v)))
(defn oassoc-in-before
[o [before-k & before-ks] [k & ks] v]
(if-let [index (index-of (keys o) before-k)]
(let [new-v (if ks
(oassoc-in-before (get o k) before-ks ks v)
v)]
(if (= k before-k)
(-> (ordered-map)
(into (take index o))
(assoc k new-v)
(into (drop (inc index) o)))
(-> (ordered-map)
(into (take index o))
(assoc k new-v)
(into (drop index o)))))
(oassoc-in o (cons k ks) v)))
(defn vec2
"Creates a optimized vector compatible type of length 2 backed
internally with MapEntry impl because it has faster access method
@ -564,6 +612,7 @@
new-elems
(remove p? after))))
;; TODO: remove this
(defn addm-at-index
"Insert an element in an ordered map at an arbitrary index"
[coll index key element]

View file

@ -263,7 +263,8 @@
[:delete-temporary-token-theme
[:map {:title "DeleteTemporaryTokenThemeChange"}
[:type [:= :delete-temporary-token-theme]]
[:id ::sm/uuid]]]
[:id ::sm/uuid]
[:name :string]]]
[:add-token-theme
[:map {:title "AddTokenThemeChange"}
@ -274,12 +275,14 @@
[:map {:title "ModTokenThemeChange"}
[:type [:= :mod-token-theme]]
[:id ::sm/uuid]
[:name :string]
[:token-theme ::ctot/token-theme]]]
[:del-token-theme
[:map {:title "DelTokenThemeChange"}
[:type [:= :del-token-theme]]
[:id ::sm/uuid]]]
[:id ::sm/uuid]
[:name :string]]]
[:add-token-set
[:map {:title "AddTokenSetChange"}
@ -822,29 +825,70 @@
set-name
name)))))
(defn- set-ids->names
[data sets]
(let [lib-sets (:token-sets-index data)
set-id->name
(fn [set-id]
(dm/get-in lib-sets [set-id :name]))]
(map set-id->name sets)))
(defmethod process-change :add-temporary-token-theme
[data {:keys [token-theme]}]
(ctotl/add-temporary-token-theme data token-theme))
(-> data
(ctotl/add-temporary-token-theme token-theme)
(update :tokens-lib
#(-> %
(ctob/ensure-tokens-lib)
(ctob/add-theme (-> token-theme
(update :sets (partial set-ids->names data))
(ctob/make-token-theme)))))))
(defmethod process-change :update-active-token-themes
[data {:keys [theme-ids]}]
(ctotl/assoc-active-token-themes data theme-ids))
(defmethod process-change :delete-temporary-token-theme
[data {:keys [id]}]
(ctotl/delete-temporary-token-theme data id))
[data {:keys [id group name]}]
(-> data
(ctotl/delete-temporary-token-theme id)
(update :tokens-lib
#(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-theme group name)))))
(defmethod process-change :add-token-theme
[data {:keys [token-theme]}]
(ctotl/add-token-theme data token-theme))
(-> data
(ctotl/add-token-theme token-theme)
(update :tokens-lib
#(-> %
(ctob/ensure-tokens-lib)
(ctob/add-theme (-> token-theme
(update :sets (partial set-ids->names data))
(ctob/make-token-theme)))))))
(defmethod process-change :mod-token-theme
[data {:keys [id token-theme]}]
(ctotl/update-token-theme data id merge token-theme))
[data {:keys [id name group token-theme]}]
(-> data
(ctotl/update-token-theme id merge token-theme)
(update :tokens-lib
#(-> %
(ctob/ensure-tokens-lib)
(ctob/update-theme name group
(fn [prev-theme]
(merge prev-theme
(-> token-theme
(update :sets (partial set-ids->names data))))))))))
(defmethod process-change :del-token-theme
[data {:keys [id]}]
(ctotl/delete-token-theme data id))
[data {:keys [id group name]}]
(-> data
(ctotl/delete-token-theme id)
(update :tokens-lib
#(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-theme group name)))))
(defmethod process-change :add-token-set
[data {:keys [token-set]}]

View file

@ -699,7 +699,7 @@
[changes token-theme]
(-> changes
(update :redo-changes conj {:type :add-temporary-token-theme :token-theme token-theme})
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme)})
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme) :name (:name token-theme)})
(apply-changes-local)))
(defn update-active-token-themes
@ -713,14 +713,14 @@
[changes token-theme]
(-> changes
(update :redo-changes conj {:type :add-token-theme :token-theme token-theme})
(update :undo-changes conj {:type :del-token-theme :id (:id token-theme)})
(update :undo-changes conj {:type :del-token-theme :id (:id token-theme) :name (:name token-theme)})
(apply-changes-local)))
(defn update-token-theme
[changes token-theme prev-token-theme]
(-> changes
(update :redo-changes conj {:type :mod-token-theme :id (:id token-theme) :token-theme token-theme})
(update :undo-changes conj {:type :mod-token-theme :id (:id token-theme) :token-theme (or prev-token-theme token-theme)})
(update :redo-changes conj {:type :mod-token-theme :id (:id token-theme) :name (:name prev-token-theme) :token-theme token-theme})
(update :undo-changes conj {:type :mod-token-theme :id (:id token-theme) :name (:name token-theme) :token-theme (or prev-token-theme token-theme)})
(apply-changes-local)))
(defn delete-token-theme
@ -729,7 +729,7 @@
(let [library-data (::library-data (meta changes))
prev-token-theme (get-in library-data [:token-themes-index token-theme-id])]
(-> changes
(update :redo-changes conj {:type :del-token-theme :id token-theme-id})
(update :redo-changes conj {:type :del-token-theme :id token-theme-id :name (:name prev-token-theme)})
(update :undo-changes conj {:type :add-token-theme :token-theme prev-token-theme})
(apply-changes-local))))

View file

@ -16,6 +16,7 @@
java.time.OffsetDateTime
java.util.List
linked.map.LinkedMap
linked.set.LinkedSet
org.fressian.Reader
org.fressian.StreamingWriter
org.fressian.Writer
@ -275,7 +276,12 @@
{:name "clj/seq"
:class clojure.lang.ISeq
:wfn write-list-like
:rfn (comp sequence read-object!)})
:rfn (comp sequence read-object!)}
{:name "linked/set"
:class LinkedSet
:wfn write-list-like
:rfn (comp #(into (d/ordered-set) %) read-object!)})
;; --- PUBLIC API

View file

@ -12,9 +12,88 @@
[app.common.time :as dt]
[app.common.transit :as t]
[app.common.types.token :as cto]
[cuerdas.core :as str]
#?(:clj [app.common.fressian :as fres])))
;; #?(:clj (set! *warn-on-reflection* true))
;; === Groups handling
(def schema:groupable-item
[:map {:title "Groupable item"}
[:name :string]])
(def valid-groupable-item?
(sm/validator schema:groupable-item))
(defn split-path
"Decompose a string in the form 'one.two.three' into a vector of strings, removing spaces."
[path separator]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path separator)
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector."
[path separator]
(str/join separator path))
(defn group-item
"Add a group to the item name, in the form group.name."
[item group-name separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(update item :name #(str group-name separator %)))
(defn ungroup-item
"Remove the first group from the item name."
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(update item :name #(-> %
(split-path separator)
(rest)
(join-path separator))))
(defn get-path
"Get the groups part of the name as a vector. E.g. group.subgroup.name -> ['group' 'subrgoup']"
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(split-path (:name item) separator))
(defn get-groups-str
"Get the groups part of the name. E.g. group.subgroup.name -> group.subrgoup"
[item separator]
(-> (get-path item separator)
(butlast)
(join-path separator)))
(defn get-final-name
"Get the final part of the name. E.g. group.subgroup.name -> name"
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(-> (:name item)
(split-path separator)
(last)))
(defn group?
"Check if a node of the grouping tree is a group or a final item."
[item]
(d/ordered-map? item))
(defn get-children
"Get all children of a group of a grouping tree. Each child is
a tuple [name item], where item "
[group]
(dm/assert!
"expected group node"
(group? group))
(seq group))
;; === Token
@ -23,10 +102,10 @@
(def schema:token
[:and
[:map {:title "Token"}
[:name cto/token-name-ref] ;; not necessary to have uuid
[:name cto/token-name-ref] ;; not necessary to have uuid
[:type [::sm/one-of cto/token-types]]
[:value :any]
[:description [:maybe :string]] ;; defrecord always have the attributes, even with nil value
[:description [:maybe :string]] ;; defrecord always have the attributes, even with nil value
[:modified-at ::sm/inst]]
[:fn (partial instance? Token)]])
@ -56,7 +135,8 @@
(defprotocol ITokenSet
(add-token [_ token] "add a token at the end of the list")
(update-token [_ token-name f] "update a token in the list")
(delete-token [_ token-name] "delete a token from the list"))
(delete-token [_ token-name] "delete a token from the list")
(get-tokens [_] "return an ordered sequence of all tokens in the set"))
(defrecord TokenSet [name description modified-at tokens]
ITokenSet
@ -77,24 +157,27 @@
(dt/now)
(if (= (:name token) (:name token'))
(assoc tokens (:name token') token')
(let [index (d/index-of (keys tokens) (:name token))]
(-> tokens
(dissoc (:name token))
(d/addm-at-index index (:name token') token'))))))
(-> tokens
(d/oassoc-before (:name token) (:name token') token')
(dissoc (:name token))))))
this))
(delete-token [_ token-name]
(TokenSet. name
description
(dt/now)
(dissoc tokens token-name))))
(dissoc tokens token-name)))
(get-tokens [_]
(vals tokens)))
(def schema:token-set
[:and [:map {:title "TokenSet"}
[:name :string]
[:description [:maybe :string]]
[:modified-at ::sm/inst]
[:tokens [:map-of {:gen/max 5} :string ::token]]]
[:tokens [:and [:map-of {:gen/max 5} :string ::token]
[:fn d/ordered-map?]]]]
[:fn (partial instance? TokenSet)]])
(sm/register! ::token-set schema:token-set)
@ -119,6 +202,16 @@
token-set))
;; === TokenSetGroup
(defrecord TokenSetGroup [attr1 attr2])
;; TODO schema, validators, etc.
(defn make-token-set-group
[]
(TokenSetGroup. "one" "two"))
;; === TokenSets (collection)
(defprotocol ITokenSets
@ -126,14 +219,24 @@
(update-set [_ set-name f] "modify a set in the ilbrary")
(delete-set [_ set-name] "delete a set in the library")
(set-count [_] "get the total number if sets in the library")
(get-set-tree [_] "get a nested tree of all sets in the library")
(get-sets [_] "get an ordered sequence of all sets in the library")
(get-set [_ set-name] "get one set looking for name")
(validate [_]))
(get-set-group [_ set-group-path] "get the attributes of a set group"))
(def schema:token-set-node
[:schema {:registry {::node [:or ::token-set
[:and
[:map-of {:gen/max 5} :string [:ref ::node]]
[:fn d/ordered-map?]]]}}
[:ref ::node]])
(sm/register! ::token-set-node schema:token-set-node)
(def schema:token-sets
[:and
[:map-of {:title "TokenSets"}
:string ::token-set]
:string ::token-set-node]
[:fn d/ordered-map?]])
(sm/register! ::token-sets schema:token-sets)
@ -144,10 +247,83 @@
(def check-token-sets!
(sm/check-fn ::token-sets))
;; === TokenTheme
(defprotocol ITokenTheme
(toggle-set [_ set-name] "togle a set used / not used in the theme"))
(defrecord TokenTheme [name group description is-source modified-at sets]
ITokenTheme
(toggle-set [_ set-name]
(TokenTheme. name
group
description
is-source
(dt/now)
(if (sets set-name)
(disj sets set-name)
(conj sets set-name)))))
(def schema:token-theme
[:and [:map {:title "TokenTheme"}
[:name :string]
[:group :string]
[:description [:maybe :string]]
[:is-source :boolean]
[:modified-at ::sm/inst]
[:sets [:and [:set {:gen/max 5} :string]
[:fn d/ordered-set?]]]]
[:fn (partial instance? TokenTheme)]])
(sm/register! ::token-theme schema:token-theme)
(def valid-token-theme?
(sm/validator schema:token-theme))
(def check-token-theme!
(sm/check-fn ::token-theme))
(defn make-token-theme
[& {:keys [] :as params}]
(let [params (-> params
(dissoc :id)
(update :group #(or % ""))
(update :is-source #(or % false))
(update :modified-at #(or % (dt/now)))
(update :sets #(into (d/ordered-set) %)))
token-theme (map->TokenTheme params)]
(dm/assert!
"expected valid token theme"
(check-token-theme! token-theme))
token-theme))
;; === TokenThemes (collection)
(defprotocol ITokenThemes
(add-theme [_ token-theme] "add a theme to the library, at the end")
(update-theme [_ group name f] "modify a theme in the ilbrary")
(delete-theme [_ group name] "delete a theme in the library")
(theme-count [_] "get the total number if themes in the library")
(get-theme-tree [_] "get a nested tree of all themes in the library")
(get-themes [_] "get an ordered sequence of all themes in the library")
(get-theme [_ group name] "get one theme looking for name"))
(def schema:token-themes
[:and
[:map-of {:title "TokenThemes"}
:string [:and [:map-of :string ::token-theme]
[:fn d/ordered-map?]]]
[:fn d/ordered-map?]])
(sm/register! ::token-themes schema:token-themes)
(def valid-token-themes?
(constantly true))
(sm/validator schema:token-themes))
(def check-token-themes!
(sm/check-fn ::token-themes))
;; === Tokens Lib
@ -155,62 +331,122 @@
"A library of tokens, sets and themes."
(add-token-in-set [_ set-name token] "add token to a set")
(update-token-in-set [_ set-name token-name f] "update a token in a set")
(delete-token-from-set [_ set-name token-name] "delete a token from a set"))
(delete-token-from-set [_ set-name token-name] "delete a token from a set")
(toggle-set-in-theme [_ group-name theme-name set-name] "toggle a set used / not used in a theme")
(validate [_]))
(deftype TokensLib [sets themes]
(deftype TokensLib [sets set-groups themes]
;; NOTE: This is only for debug purposes, pending to properly
;; implement the toString and alternative printing.
#?@(:clj [clojure.lang.IDeref
(deref [_] {:sets sets :themes themes})]
(deref [_] {:sets sets :set-groups set-groups :themes themes})]
:cljs [cljs.core/IDeref
(-deref [_] {:sets sets :themes themes})])
(-deref [_] {:sets sets :set-groups set-groups :themes themes})])
#?@(:cljs [cljs.core/IEncodeJS
(-clj->js [_] (js-obj "sets" (clj->js sets)
"set-groups" (clj->js set-groups)
"themes" (clj->js themes)))])
ITokenSets
(add-set [_ token-set]
(dm/assert! "expected valid token set" (check-token-set! token-set))
(TokensLib. (assoc sets (:name token-set) token-set)
themes))
(let [path (get-path token-set "/")
groups-str (get-groups-str token-set "/")]
(TokensLib. (d/oassoc-in sets path token-set)
(cond-> set-groups
(not (str/empty? groups-str))
(assoc groups-str (make-token-set-group)))
themes)))
(update-set [this set-name f]
(if-let [set (get sets set-name)]
(let [set' (-> (make-token-set (f set))
(assoc :modified-at (dt/now)))]
(check-token-set! set')
(TokensLib. (if (= (:name set) (:name set'))
(assoc sets (:name set') set')
(let [index (d/index-of (keys sets) (:name set))]
(let [path (split-path set-name "/")
set (get-in sets path)]
(if set
(let [set' (-> (make-token-set (f set))
(assoc :modified-at (dt/now)))
path' (get-path set' "/")]
(check-token-set! set')
(TokensLib. (if (= (:name set) (:name set'))
(d/oassoc-in sets path set')
(-> sets
(dissoc (:name set))
(d/addm-at-index index (:name set') set'))))
themes))
this))
(d/oassoc-in-before path path' set')
(d/dissoc-in path)))
set-groups ;; TODO update set-groups as needed
themes))
this)))
(delete-set [_ set-name]
(TokensLib. (dissoc sets set-name)
themes))
(let [path (split-path set-name "/")]
(TokensLib. (d/dissoc-in sets path)
set-groups ;; TODO remove set-group if needed
themes)))
(validate [_]
(and (valid-token-sets? sets)
(valid-token-themes? themes)))
(set-count [_]
(count sets))
(get-set-tree [_]
sets)
(get-sets [_]
(vals sets))
(->> (tree-seq d/ordered-map? vals sets)
(filter (partial instance? TokenSet))))
(set-count [this]
(count (get-sets this)))
(get-set [_ set-name]
(get sets set-name))
(let [path (split-path set-name "/")]
(get-in sets path)))
(get-set-group [_ set-group-path]
(get set-groups set-group-path))
ITokenThemes
(add-theme [_ token-theme]
(dm/assert! "expected valid token theme" (check-token-theme! token-theme))
(TokensLib. sets
set-groups
(update themes (:group token-theme) d/oassoc (:name token-theme) token-theme)))
(update-theme [this group name f]
(let [theme (dm/get-in themes [group name])]
(if theme
(let [theme' (-> (make-token-theme (f theme))
(assoc :modified-at (dt/now)))
group' (:group theme')
name' (:name theme')]
(check-token-theme! theme')
(TokensLib. sets
set-groups
(if (and (= group group') (= name name'))
(update themes group' assoc name' theme')
(-> themes
(d/oassoc-in-before [group name] [group' name'] theme')
(d/dissoc-in [group name])))))
this)))
(delete-theme [_ group name]
(TokensLib. sets
set-groups
(d/dissoc-in themes [group name])))
(get-theme-tree [_]
themes)
(get-themes [_]
(->> (tree-seq d/ordered-map? vals themes)
(filter (partial instance? TokenTheme))))
(theme-count [this]
(count (get-themes this)))
(get-theme [_ group name]
(dm/get-in themes [group name]))
ITokensLib
(add-token-in-set [this set-name token]
(dm/assert! "expected valid token instance" (check-token! token))
(if (contains? sets set-name)
(TokensLib. (update sets set-name add-token token)
set-groups
themes)
this))
@ -218,6 +454,7 @@
(if (contains? sets set-name)
(TokensLib. (update sets set-name
#(update-token % token-name f))
set-groups
themes)
this))
@ -225,8 +462,21 @@
(if (contains? sets set-name)
(TokensLib. (update sets set-name
#(delete-token % token-name))
set-groups
themes)
this)))
this))
(toggle-set-in-theme [this theme-group theme-name set-name]
(if-let [_theme (get-in themes theme-group theme-name)]
(TokensLib. sets
set-groups
(d/oupdate-in themes [theme-group theme-name]
#(toggle-set % set-name)))
this))
(validate [_]
(and (valid-token-sets? sets) ;; TODO: validate set-groups
(valid-token-themes? themes))))
(defn valid-tokens-lib?
[o]
@ -248,10 +498,11 @@
;; structure the data and the order separately as we already do
;; with pages and pages-index.
(make-tokens-lib :sets (d/ordered-map)
:set-groups {}
:themes (d/ordered-map)))
([& {:keys [sets themes]}]
(let [tokens-lib (TokensLib. sets themes)]
([& {:keys [sets set-groups themes]}]
(let [tokens-lib (TokensLib. sets set-groups themes)]
(dm/assert!
"expected valid tokens lib"
@ -269,7 +520,7 @@
(sm/register! ::tokens-lib type:tokens-lib)
;; === Serialization handlers for RPC API and database
;; === Serialization handlers for RPC API (transit) and database (fressian)
(t/add-handlers!
{:id "penpot/tokens-lib"
@ -281,6 +532,11 @@
:class TokenSet
:wfn #(into {} %)
:rfn #(make-token-set %)}
{:id "penpot/token-theme"
:class TokenTheme
:wfn #(into {} %)
:rfn #(make-token-theme %)}
{:id "penpot/token"
:class Token
@ -307,13 +563,24 @@
(let [obj (fres/read-object! r)]
(map->TokenSet obj)))}
{:name "penpot/token-theme/v1"
:class TokenTheme
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenTheme obj)))}
{:name "penpot/tokens-lib/v1"
:class TokensLib
:wfn (fn [n w o]
(fres/write-tag! w n 2)
(fres/write-tag! w n 3)
(fres/write-object! w (.-sets o))
(fres/write-object! w (.-set-groups o))
(fres/write-object! w (.-themes o)))
:rfn (fn [r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)]
(->TokensLib sets themes)))}))
(let [sets (fres/read-object! r)
set-groups (fres/read-object! r)
themes (fres/read-object! r)]
(->TokensLib sets set-groups themes)))}))

View file

@ -19,16 +19,17 @@
(assoc file-data :token-active-themes theme-ids))
(defn add-temporary-token-theme
[file-data {:keys [id] :as token-theme}]
[file-data {:keys [id name] :as token-theme}]
(-> file-data
(d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])
(assoc :token-theme-temporary-id id)
(assoc :token-theme-temporary-name name)
(update :token-themes-index assoc id token-theme)))
(defn delete-temporary-token-theme
[file-data token-theme-id]
(cond-> file-data
(= (:token-theme-temporary-id file-data) token-theme-id) (dissoc :token-theme-temporary-id)
(= (:token-theme-temporary-id file-data) token-theme-id) (dissoc :token-theme-temporary-id :token-theme-temporary-name)
:always (d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])))
(defn add-token-theme

File diff suppressed because it is too large Load diff

View file

@ -115,10 +115,9 @@
(ptk/reify ::update-token-theme
ptk/WatchEvent
(watch [it state _]
(let [prev-token-theme (wtts/get-workspace-token-theme state (:id token-theme))
(let [prev-token-theme (wtts/get-workspace-token-theme (:id token-theme) state)
changes (-> (pcb/empty-changes it)
(pcb/update-token-theme token-theme prev-token-theme))]
(js/console.log "changes" changes)
(rx/of
(dch/commit-changes changes))))))

View file

@ -69,11 +69,11 @@
(defn group-assets
"Convert a list of assets in a nested structure like this:
{'': [{assetA} {assetB}]
'group1': {'': [{asset1A} {asset1B}]
'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]}
'subgroup12': {'': [{asset12A}]}}
'group2': {'subgroup21': {'': [{asset21A}}}}
{'': [assetA assetB]
'group1': {'': [asset1A asset1B]
'subgroup11': {'': [asset11A asset11B asset11C]}
'subgroup12': {'': [asset12A]}}
'group2': {'subgroup21': {'': [asset21A]}}}
"
[assets reverse-sort?]
(when-not (empty? assets)