mirror of
https://github.com/penpot/penpot.git
synced 2026-09-22 20:06:16 +00:00
* ✨ Auto link tokens when adding external libraries (provisional) * 🔧 Refactor tokens-lib initialization * 🔧 Add separated TokenStatus to store status apart of TokensLib * 🔧 Make all status operations use the new data structure * 🔧 Normalize status helper functions and access token sets by id * 🔧 Rename :tokens-file to :tokens-source * 🎉 Allow the user to choose the tokens-source of a file * 🎉 Make tokens library readonly when it's in an external file * 🎉 Show tokens in library summaries * 🎉 Show source info in sidebar * 🔧 Fix integration tests * 🐛 Propagate changes of token values in external library * 🎉 Layout updates * 🔧 Refactor tokens source calculations * 🔧 Add harder checks for nil or empty values in everything * 🐛 Fix some integration tests * 🔧 Add integration tests for tokens in external libs * 🔧 Validate and repair missing tokens status * 🎉 Make ui changes optional with config flag * 🐛 Propagate tokens after synchronizing components in ext library * 🐛 Propagate tokens after creating new instances * 🐛 Propagate tokens after synchronizing tokens in ext library * 🐛 Add a tokens source icon to libraries section (#11439) * 🐛 Add a tokens source icon to libraries section * 🐛 Fix ellipsis on library names * ♻️ Remove code under flag on legacy component * 🐛 Fix token theme name on inspect tab * 🎉 Add changes notification (#11476) * 🎉 Add changes notification * ♻️ Change fn names * 🐛 Fix tokens source label truncation and missing translations (#11533) * 🐛 Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * 🐛 Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * 🐛 Fix small visual error * 🐛 Fix problem with plugins * 🐛 Fix playwright tests --------- Co-authored-by: Eva Marco <evamarcod@gmail.com> Co-authored-by: Eva Marco <eva.marco@kaleidos.net> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
827 lines
30 KiB
Clojure
827 lines
30 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 app.plugins.tokens
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.tokens :as cfo]
|
|
[app.common.json :as json]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.file :as ctf]
|
|
[app.common.types.token :as cto]
|
|
[app.common.types.tokens-lib :as ctob]
|
|
[app.common.types.tokens-status :as ctos]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.tokenscript :as ts]
|
|
[app.main.data.workspace.tokens.application :as dwta]
|
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
|
[app.main.store :as st]
|
|
[app.plugins.register :as r]
|
|
[app.plugins.system-events :as se]
|
|
[app.plugins.utils :as u]
|
|
[app.util.object :as obj]
|
|
[clojure.datafy :refer [datafy]]
|
|
[clojure.set :refer [map-invert]]))
|
|
|
|
;; === Token
|
|
|
|
;; Give more semantic names to the shape attributes that tokens can be applied to
|
|
(def ^:private map:token-attr->token-attr-plugin
|
|
{:r1 :border-radius-top-left
|
|
:r2 :border-radius-top-right
|
|
:r3 :border-radius-bottom-right
|
|
:r4 :border-radius-bottom-left
|
|
|
|
:p1 :padding-top
|
|
:p2 :padding-right
|
|
:p3 :padding-bottom
|
|
:p4 :padding-left
|
|
|
|
:m1 :margin-top
|
|
:m2 :margin-right
|
|
:m3 :margin-bottom
|
|
:m4 :margin-left
|
|
|
|
:font-family :font-families})
|
|
|
|
(def ^:private map:token-attr-plugin->token-attr
|
|
(merge
|
|
(map-invert map:token-attr->token-attr-plugin)
|
|
{:padding-top :p1
|
|
:padding-right :p2
|
|
:padding-bottom :p3
|
|
:padding-left :p4
|
|
:margin-top :m1
|
|
:margin-right :m2
|
|
:margin-bottom :m3
|
|
:margin-left :m4}))
|
|
|
|
(defn token-attr->token-attr-plugin
|
|
[k]
|
|
(get map:token-attr->token-attr-plugin k k))
|
|
|
|
(defn token-attr-plugin->token-attr
|
|
"Resolve a plugin-side token attribute reference to its canonical
|
|
internal keyword.
|
|
|
|
Accepts either a Clojure keyword (the canonical form, e.g. `:r1`,
|
|
`:fill`) or a string (the natural shape that arrives from a JS plugin
|
|
call such as `shape.applyToken(token, [\"fill\"])`). Converts strings
|
|
to keywords first, then maps verbose plugin-side aliases (e.g.
|
|
`:border-radius-top-left`) to their internal short form (e.g. `:r1`).
|
|
Inputs that are already in canonical form (`:r1`, `:fill`, `\"fill\"`,
|
|
…) pass through unchanged."
|
|
[k]
|
|
(let [k (cond-> k (string? k) keyword)]
|
|
(get map:token-attr-plugin->token-attr k k)))
|
|
|
|
(defn applied-tokens-plugin->applied-tokens
|
|
[value]
|
|
(into {}
|
|
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
|
|
value))
|
|
|
|
(defn token-attr?
|
|
[attr]
|
|
(cto/token-attr? (token-attr-plugin->token-attr attr)))
|
|
|
|
(defn- apply-token-to-shapes
|
|
[plugin-id file-id set-id id shape-ids attrs]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :applyToken "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(let [token (u/locate-token file-id set-id id)]
|
|
(if (some #(not (token-attr? %)) attrs)
|
|
(u/not-valid plugin-id :applyToSelected attrs)
|
|
(st/emit!
|
|
(-> (dwta/toggle-token {:token token
|
|
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
|
|
:shape-ids shape-ids
|
|
:expand-with-children false})
|
|
(se/add-event plugin-id)))))))
|
|
|
|
(defn- typography-resolved-value->js
|
|
"Converts a resolved typography composite (a Clojure map keyed by the
|
|
tokenscript field names) into the plugin's `TokenTypographyValue[]` shape: a
|
|
JS array with a single object using the public camelCase member names."
|
|
[m]
|
|
(when (map? m)
|
|
#js [#js {"fontFamilies" (clj->js (:font-family m))
|
|
"fontSizes" (:font-size m)
|
|
"fontWeights" (some-> (:font-weight m) str)
|
|
"letterSpacing" (:letter-spacing m)
|
|
"lineHeight" (:line-height m)
|
|
"textCase" (:text-case m)
|
|
"textDecoration" (:text-decoration m)}]))
|
|
|
|
(defn- shadow-key->camel
|
|
"Renames a shadow composite field name (kebab string) to its public camelCase
|
|
member name. The shadow schema is closed; offset-x/offset-y are its only
|
|
multi-word fields, so the rest (blur, spread, color, inset) pass through."
|
|
[k]
|
|
(case k
|
|
"offset-x" "offsetX"
|
|
"offset-y" "offsetY"
|
|
k))
|
|
|
|
(defn- shadow-entry->js
|
|
"Converts one resolved shadow entry (a JS Map of field name -> tokenscript
|
|
symbol) into a plain JS object using the public member names and the
|
|
unit-converted values."
|
|
[^js m]
|
|
(let [out #js {}]
|
|
(.forEach m (fn [sym k]
|
|
(obj/set! out (shadow-key->camel k)
|
|
(ts/tokenscript-symbols->penpot-unit sym))))
|
|
out))
|
|
|
|
(defn- shadow-resolved-value->js
|
|
"Converts a resolved shadow composite (a sequence of shadow entries) into the
|
|
plugin's `TokenShadowValue[]` shape."
|
|
[entries]
|
|
(when (some? entries)
|
|
(into-array (map shadow-entry->js entries))))
|
|
|
|
(defn- font-families-resolved-value->js
|
|
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
|
|
documented `string[]` shape rather than leaking the raw tokenscript structure."
|
|
[resolved-value]
|
|
(let [v (ts/tokenscript-symbols->penpot-unit resolved-value)]
|
|
(cond
|
|
(nil? v) nil
|
|
(sequential? v) (clj->js v)
|
|
:else #js [v])))
|
|
|
|
(defn- get-resolved-value
|
|
[token tokens-tree]
|
|
(let [resolved-tokens (ts/resolve-tokens tokens-tree)
|
|
resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])]
|
|
(cond
|
|
(= :font-family (:type token))
|
|
;; A fontFamilies token resolves to a list of families; expose it as the
|
|
;; documented `string[]` rather than the raw tokenscript list symbol.
|
|
(font-families-resolved-value->js resolved-value)
|
|
|
|
(= :typography (:type token))
|
|
;; A typography token resolves to a composite; expose it as the documented
|
|
;; `TokenTypographyValue[]` rather than the raw tokenscript structure.
|
|
(typography-resolved-value->js
|
|
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
|
|
|
(= :shadow (:type token))
|
|
;; A shadow token resolves to a list of composites whose entries the
|
|
;; tokenscript unit conversion leaves as raw symbols; expose them as the
|
|
;; documented `TokenShadowValue[]`.
|
|
(shadow-resolved-value->js
|
|
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
|
|
|
:else
|
|
(ts/tokenscript-symbols->penpot-unit resolved-value))))
|
|
|
|
(defn token-proxy? [p]
|
|
(obj/type-of? p "TokenProxy"))
|
|
|
|
;; Cannot use shape/shape-proxy? here because of circular dependency in applyToken in shape proxy
|
|
(defn shape-proxy? [s]
|
|
(obj/type-of? s "ShapeProxy"))
|
|
|
|
(defn token-proxy
|
|
[plugin-id file-id set-id id]
|
|
(obj/reify {:name "TokenProxy"
|
|
:on-error (u/handle-error plugin-id)}
|
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
|
:$file-id {:enumerable false :get (constantly file-id)}
|
|
:$set-id {:enumerable false :get (constantly set-id)}
|
|
:$id {:enumerable false :get (constantly id)}
|
|
|
|
:id
|
|
{:get #(dm/str id)}
|
|
|
|
:name
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)]
|
|
(ctob/get-name token)))
|
|
:schema (cfo/make-token-name-schema
|
|
(some-> (u/locate-tokens-lib file-id)
|
|
(ctob/get-tokens set-id)))
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(st/emit! (-> (dwtl/update-token set-id id {:name value})
|
|
(se/add-event plugin-id))))))}
|
|
|
|
:type
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)]
|
|
(-> (:type token) (cto/token-type->dtcg-token-type))))}
|
|
|
|
:value
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)]
|
|
(json/->js (:value token))))
|
|
:schema (let [token (u/locate-token file-id set-id id)
|
|
base (cfo/make-token-value-schema (:type token))]
|
|
;; plugin-types declares the fontFamilies value as
|
|
;; `string | string[]`, but the core schema only accepts a
|
|
;; vector/ref; also accept a plain string (normalized in :set).
|
|
(if (= :font-family (:type token))
|
|
[:or :string base]
|
|
base))
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :value "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [token (u/locate-token file-id set-id id)
|
|
value (cond-> value
|
|
(= :font-family (:type token))
|
|
(ctob/convert-dtcg-font-family))]
|
|
(st/emit! (dwtl/update-token set-id id {:value value}))))))}
|
|
|
|
:resolvedValue
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)
|
|
tokens-lib (u/locate-tokens-lib file-id)
|
|
tokens-status (u/locate-tokens-status file-id)
|
|
tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
|
|
(get-resolved-value token tokens-tree)))}
|
|
|
|
:resolvedValueString
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)
|
|
tokens-lib (u/locate-tokens-lib file-id)
|
|
tokens-status (u/locate-tokens-status file-id)
|
|
tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
|
|
(str (get-resolved-value token tokens-tree))))}
|
|
|
|
:description
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [token (u/locate-token file-id set-id id)]
|
|
(ctob/get-description token)))
|
|
:schema cfo/schema:token-description
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :description "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(st/emit! (-> (dwtl/update-token set-id id {:description value})
|
|
(se/add-event plugin-id))))))}
|
|
|
|
:duplicate
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
;; TODO:
|
|
;; - add function duplicate-token in tokens-lib, that allows to specify the new id
|
|
;; - use this function in dwtl/duplicate-token
|
|
;; - return the new token proxy using the locally forced id
|
|
;; - do the same with sets and themes
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [token (u/locate-token file-id set-id id)
|
|
token' (ctob/make-token (-> (datafy token)
|
|
(dissoc :id
|
|
:modified-at)))]
|
|
(st/emit! (-> (dwtl/create-token set-id token')
|
|
(se/add-event plugin-id)))
|
|
(token-proxy plugin-id file-id set-id (:id token'))))))
|
|
|
|
:remove
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(st/emit! (-> (dwtl/delete-token set-id id)
|
|
(se/add-event plugin-id))))))
|
|
|
|
:applyToShapes
|
|
{:enumerable false
|
|
:schema [:tuple
|
|
[:vector [:fn shape-proxy?]]
|
|
[:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]]
|
|
:fn (fn [shapes attrs]
|
|
(apply-token-to-shapes plugin-id file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
|
|
|
|
:applyToSelected
|
|
{:enumerable false
|
|
:schema [:tuple [:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]]
|
|
:fn (fn [attrs]
|
|
(let [selected (get-in @st/state [:workspace-local :selected])]
|
|
(apply-token-to-shapes plugin-id file-id set-id id selected attrs)))}))
|
|
|
|
;; === Token Set
|
|
|
|
(defn token-set-proxy? [p]
|
|
(obj/type-of? p "TokenSetProxy"))
|
|
|
|
(defn token-set-proxy
|
|
([plugin-id file-id id]
|
|
(token-set-proxy plugin-id file-id id nil))
|
|
([plugin-id file-id id initial-name]
|
|
(obj/reify {:name "TokenSetProxy"
|
|
:on-error (u/handle-error plugin-id)}
|
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
|
:$file-id {:enumerable false :get (constantly file-id)}
|
|
:$id {:enumerable false :get (constantly id)}
|
|
|
|
:id
|
|
{:get #(dm/str id)}
|
|
|
|
:name
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
;; Prefer the authoritative state lookup; fall back to initial-name
|
|
;; when the async state update from `catalog.addSet()` hasn't
|
|
;; propagated yet.
|
|
(let [set (u/locate-token-set file-id id)]
|
|
(if (some? set)
|
|
(ctob/get-name set)
|
|
initial-name)))
|
|
:schema (cfo/make-token-set-name-schema
|
|
(u/locate-tokens-lib file-id)
|
|
id)
|
|
:set
|
|
(fn [_ name]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [set (u/locate-token-set file-id id)]
|
|
(st/emit! (dwtl/rename-token-set set name))))))}
|
|
|
|
:active
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-status (u/locate-tokens-status file-id)]
|
|
(ctos/set-active? tokens-status id)))
|
|
:schema ::sm/boolean
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(st/emit! (dwtl/set-enabled-token-set id value))))} ;; This can be done even with tokens in an external library
|
|
|
|
:toggleActive
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(st/emit! (dwtl/toggle-token-set id))))
|
|
|
|
:tokens
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)]
|
|
(->> (ctob/get-tokens tokens-lib id)
|
|
(vals)
|
|
(map #(token-proxy plugin-id file-id id (:id %)))
|
|
(apply array))))}
|
|
|
|
:tokensByType
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
|
tokens (ctob/get-tokens tokens-lib id)]
|
|
(->> tokens
|
|
(vals)
|
|
(sort-by :name)
|
|
(group-by #(cto/token-type->dtcg-token-type (:type %)))
|
|
(into [])
|
|
(mapv (fn [[type tokens]]
|
|
#js [(name type)
|
|
(->> tokens
|
|
(map #(token-proxy plugin-id file-id id (:id %)))
|
|
(apply array))]))
|
|
(apply array))))}
|
|
|
|
:getTokenById
|
|
{:enumerable false
|
|
:schema [:tuple ::sm/uuid]
|
|
:fn (fn [token-id]
|
|
(let [token (u/locate-token file-id id token-id)]
|
|
(when (some? token)
|
|
(token-proxy plugin-id file-id id token-id))))}
|
|
|
|
:addToken
|
|
{:enumerable false
|
|
:schema (fn [args]
|
|
(let [tokens-tree (-> (u/locate-tokens-lib file-id)
|
|
(ctob/get-tokens id)
|
|
;; Convert to the adecuate format for schema
|
|
(ctob/tokens-tree))]
|
|
[:tuple (-> (cfo/make-token-schema
|
|
tokens-tree
|
|
(cto/dtcg-token-type->token-type (-> args (first) (get "type")))
|
|
nil)
|
|
;; Don't allow plugins to set the id
|
|
(sm/dissoc-key :id)
|
|
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
|
|
;; and set a converter that changes DTCG types to internal types (:decode/json).
|
|
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
|
|
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
|
|
:decode/options {:key-fn identity}
|
|
:fn (fn [attrs]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addToken "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
|
token (ctob/make-token attrs)
|
|
;; Resolve against all tokens in the library (including those
|
|
;; in inactive sets) so that references to structurally
|
|
;; existing tokens resolve even if their set is not active.
|
|
;; The target set's tokens take precedence over equally named
|
|
;; tokens in other sets, and the new token takes precedence
|
|
;; over all.
|
|
tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib)
|
|
(ctob/get-tokens tokens-lib id))
|
|
(assoc (:name token) token))
|
|
resolved-tokens (ts/resolve-tokens tokens-tree)
|
|
|
|
{:keys [errors resolved-value] :as resolved-token}
|
|
(get resolved-tokens (:name token))]
|
|
|
|
(if resolved-value
|
|
(do (st/emit! (-> (dwtl/create-token id token)
|
|
(se/add-event plugin-id)))
|
|
(token-proxy plugin-id file-id id (:id token)))
|
|
(do (u/not-valid plugin-id :addToken (str errors))
|
|
nil))))))}
|
|
|
|
:duplicate
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [id-ref (atom nil)]
|
|
(st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref}))
|
|
(when (some? @id-ref)
|
|
(token-set-proxy plugin-id file-id @id-ref))))))
|
|
|
|
:remove
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(st/emit! (dwtl/delete-token-set id))))))))
|
|
|
|
(defn token-theme-proxy? [p]
|
|
(obj/type-of? p "TokenThemeProxy"))
|
|
|
|
(defn- resolve-token-set
|
|
"Resolves an addSet/removeSet argument to a token set. A proxy is returned
|
|
as-is; an id is located in the file's token library."
|
|
[file-id set-arg]
|
|
(if (token-set-proxy? set-arg)
|
|
set-arg
|
|
(u/locate-token-set file-id set-arg)))
|
|
|
|
(defn- token-set-name
|
|
"Reads the name from a resolved token set, supporting both proxies (whose
|
|
getter falls back to the freshly-created name) and located sets."
|
|
[set]
|
|
(when (some? set)
|
|
(if (token-set-proxy? set)
|
|
(obj/get set "name")
|
|
(ctob/get-name set))))
|
|
|
|
(defn token-theme-proxy
|
|
[plugin-id file-id id]
|
|
(obj/reify {:name "TokenThemeProxy"
|
|
:on-error (u/handle-error plugin-id)}
|
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
|
:$file-id {:enumerable false :get (constantly file-id)}
|
|
:$id {:enumerable false :get (constantly id)}
|
|
|
|
:id
|
|
{:get #(dm/str id)}
|
|
|
|
:external-id
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [theme (u/locate-token-theme file-id id)]
|
|
(:external-id theme)))}
|
|
|
|
:group
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [theme (u/locate-token-theme file-id id)]
|
|
(:group theme)))
|
|
:schema (let [theme (u/locate-token-theme file-id id)]
|
|
(cfo/make-token-theme-group-schema
|
|
(u/locate-tokens-lib file-id)
|
|
(:name theme)
|
|
(:id theme)))
|
|
:set
|
|
(fn [_ group]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :group "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [theme (u/locate-token-theme file-id id)]
|
|
(st/emit! (dwtl/update-token-theme id (assoc theme :group group)))))))}
|
|
|
|
:name
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [theme (u/locate-token-theme file-id id)]
|
|
(:name theme)))
|
|
:schema (let [theme (u/locate-token-theme file-id id)]
|
|
(cfo/make-token-theme-name-schema
|
|
(u/locate-tokens-lib file-id)
|
|
(:id theme)
|
|
(:group theme)))
|
|
:set
|
|
(fn [_ name]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [theme (u/locate-token-theme file-id id)]
|
|
(when name
|
|
(st/emit! (dwtl/update-token-theme id (assoc theme :name name))))))))}
|
|
|
|
:active
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-status (u/locate-tokens-status file-id)]
|
|
(ctos/theme-active? tokens-status id)))
|
|
:schema ::sm/boolean
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(st/emit! (dwtl/set-token-theme-active id value))))}
|
|
|
|
:toggleActive
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(st/emit! (dwtl/toggle-token-theme-active id))))
|
|
|
|
:activeSets
|
|
{:this true
|
|
:get (fn [_]
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
|
theme (u/locate-token-theme file-id id)]
|
|
(->> theme
|
|
:sets
|
|
(map #(->> %
|
|
(ctob/get-set-by-name tokens-lib)
|
|
(ctob/get-id)
|
|
(token-set-proxy plugin-id file-id)))
|
|
(apply array))))}
|
|
|
|
:addSet
|
|
{:enumerable false
|
|
:schema [:tuple [:or [:fn token-set-proxy?] ::sm/uuid]]
|
|
:fn (fn [set-arg]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [set-name (token-set-name (resolve-token-set file-id set-arg))
|
|
theme (u/locate-token-theme file-id id)]
|
|
(when (and set-name theme)
|
|
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))))}
|
|
|
|
:removeSet
|
|
{:enumerable false
|
|
:schema [:tuple [:or [:fn token-set-proxy?] ::sm/uuid]]
|
|
:fn (fn [set-arg]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :removeSet "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [set-name (token-set-name (resolve-token-set file-id set-arg))
|
|
theme (u/locate-token-theme file-id id)]
|
|
(when (and set-name theme)
|
|
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))))}
|
|
|
|
:duplicate
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [theme (u/locate-token-theme file-id id)
|
|
theme' (ctob/make-token-theme (-> (datafy theme)
|
|
(dissoc :id
|
|
:modified-at)))]
|
|
(st/emit! (dwtl/create-token-theme theme'))
|
|
(token-theme-proxy plugin-id file-id (:id theme'))))))
|
|
|
|
:remove
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(st/emit! (dwtl/delete-token-theme id)))))))
|
|
|
|
(defn tokens-catalog
|
|
[plugin-id file-id]
|
|
(obj/reify {:name "TokensCatalog"
|
|
:on-error (u/handle-error plugin-id)}
|
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
|
:$id {:enumerable false :get (constantly file-id)}
|
|
|
|
:isEditableTokens
|
|
{:this false
|
|
:enumerable false
|
|
:get
|
|
(fn []
|
|
(let [file (u/locate-file file-id)]
|
|
(cfo/editable-tokens? (ctf/file-data file))))}
|
|
|
|
:themes
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
|
themes (when tokens-lib
|
|
(->> (ctob/get-themes tokens-lib)
|
|
(remove #(= (:id %) uuid/zero))))]
|
|
(apply array (map #(token-theme-proxy plugin-id file-id (ctob/get-id %)) themes))))}
|
|
|
|
:sets
|
|
{:this true
|
|
:enumerable false
|
|
:get
|
|
(fn [_]
|
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
|
sets (when tokens-lib
|
|
(ctob/get-sets tokens-lib))]
|
|
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
|
|
|
|
:addTheme
|
|
{:enumerable false
|
|
:schema (fn [args]
|
|
[:tuple (-> (cfo/make-token-theme-schema
|
|
(u/locate-tokens-lib file-id)
|
|
(get (first args) :group "")
|
|
(get (first args) :name "")
|
|
nil)
|
|
(sm/dissoc-key :id))]) ;; We don't allow plugins to set the id
|
|
:fn (fn [attrs]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addTheme "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [theme (ctob/make-token-theme attrs)]
|
|
(st/emit! (dwtl/create-token-theme theme))
|
|
(token-theme-proxy plugin-id file-id (:id theme))))))}
|
|
|
|
:addSet
|
|
{:enumerable false
|
|
:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
|
|
(u/locate-tokens-lib file-id)
|
|
nil))
|
|
(sm/dissoc-key :id) ;; We don't allow plugins to set the id
|
|
;; Allow an optional `active` flag so a plugin can create
|
|
;; an already-active set in a single call. Newly created
|
|
;; sets are inactive by default (only active sets affect
|
|
;; shapes and reference resolution). `active` is not part
|
|
;; of the token-set data model, so the :fn strips it and
|
|
;; applies it through the set-activation logic.
|
|
(sm/merge [:map [:active {:optional true} ::sm/boolean]]))]
|
|
|
|
:fn (fn [attrs]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(do
|
|
(u/check-editable-tokens file-id)
|
|
(let [active? (boolean (:active attrs))
|
|
attrs (-> attrs
|
|
(dissoc :active)
|
|
(update :name ctob/normalize-set-name))
|
|
set (ctob/make-token-set attrs)]
|
|
(st/emit! (dwtl/create-token-set set))
|
|
;; Newly created sets are inactive by default; activate it when
|
|
;; requested. Enabling only adds the set name to the hidden theme,
|
|
;; so it does not depend on the create event having propagated yet.
|
|
(when active?
|
|
(st/emit! (dwtl/set-enabled-token-set (ctob/get-id set) true)))
|
|
;; Pass the set name as `initial-name` so the proxy can resolve
|
|
;; it immediately, before the async `st/emit!` above propagates
|
|
;; the new set into `@st/state`.
|
|
(token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set))))))}
|
|
|
|
:getThemeById
|
|
{:enumerable false
|
|
:schema [:tuple ::sm/uuid]
|
|
:fn (fn [theme-id]
|
|
(let [theme (u/locate-token-theme file-id theme-id)]
|
|
(when (some? theme)
|
|
(token-theme-proxy plugin-id file-id theme-id))))}
|
|
|
|
:getSetById
|
|
{:enumerable false
|
|
:schema [:tuple ::sm/uuid]
|
|
:fn (fn [set-id]
|
|
(let [set (u/locate-token-set file-id set-id)]
|
|
(when (some? set)
|
|
(token-set-proxy plugin-id file-id set-id))))}))
|