🎉 Make tokens library readonly when it's in an external file

This commit is contained in:
Andrés Moya 2026-07-27 17:52:25 +02:00
parent c63bf0a5e0
commit 1f793e9c15
15 changed files with 356 additions and 236 deletions

View File

@ -1682,10 +1682,10 @@
(conj $ library-id) (conj $ library-id)
(map #(load-library-file file-id %) $)))) (map #(load-library-file file-id %) $))))
(rx/catch (fn [cause] (rx/catch (fn [cause]
(let [error (ex-data cause)] (let [error (ex-data cause)]
(if (= (:code error) :circular-library-reference) (if (= (:code error) :circular-library-reference)
(rx/of (ntf/error (tr "errors.circular-library-reference"))) (rx/of (ntf/error (tr "errors.circular-library-reference")))
(rx/throw cause)))))) (rx/throw cause))))))
(rx/of (ptk/reify ::link-file-to-library-finished)) (rx/of (ptk/reify ::link-file-to-library-finished))
(when (pos? variants-count) (when (pos? variants-count)
(->> (rp/cmd! :get-library-usage {:file-id library-id}) (->> (rp/cmd! :get-library-usage {:file-id library-id})

View File

@ -47,6 +47,7 @@
(def permissions (mf/create-context nil)) (def permissions (mf/create-context nil))
(def can-edit? (mf/create-context nil)) (def can-edit? (mf/create-context nil))
(def can-edit-tokens? (mf/create-context nil))
(def active-tokens-by-type (def active-tokens-by-type
"Active tokens by type, used mainly for provide tokens data to the "Active tokens by type, used mainly for provide tokens data to the

View File

@ -19,6 +19,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.util.clipboard :as clipboard] [app.util.clipboard :as clipboard]
@ -346,31 +347,39 @@
(defn default-actions (defn default-actions
[{:keys [token selected-token-set-id on-delete-token errors]}] [{:keys [token selected-token-set-id on-delete-token errors]}]
(let [{:keys [modal]} (dwta/get-token-properties token) (let [{:keys [modal]} (dwta/get-token-properties token)
can-edit-tokens?
(mf/use-ctx ctx/can-edit-tokens?)
on-copy-name #(clipboard/to-clipboard (:name token)) on-copy-name #(clipboard/to-clipboard (:name token))
on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))] on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))]
[{:title (tr "workspace.tokens.edit") (concat
:no-selectable true []
:action (fn [event] (when can-edit-tokens?
(let [{:keys [key fields]} modal] [{:title (tr "workspace.tokens.edit")
(dom/stop-propagation event) :no-selectable true
(st/emit! (dwtl/assign-token-context-menu nil) :action (fn [event]
(modal/show key {:x (.-clientX ^js event) (let [{:keys [key fields]} modal]
:y (.-clientY ^js event) (dom/stop-propagation event)
:position :right (st/emit! (dwtl/assign-token-context-menu nil)
:fields fields (modal/show key {:x (.-clientX ^js event)
:initial-errors errors :y (.-clientY ^js event)
:action "edit" :position :right
:selected-token-set-id selected-token-set-id :fields fields
:token token}))))} :initial-errors errors
{:title (tr "workspace.tokens.duplicate") :action "edit"
:no-selectable true :selected-token-set-id selected-token-set-id
:action on-duplicate-token} :token token}))))}
{:title (tr "workspace.tokens.copy-name") {:title (tr "workspace.tokens.duplicate")
:no-selectable true :no-selectable true
:action on-copy-name} :action on-duplicate-token}])
{:title (tr "workspace.tokens.delete") [{:title (tr "workspace.tokens.copy-name")
:no-selectable true :no-selectable true
:action #(on-delete-token token)}])) :action on-copy-name}]
(when can-edit-tokens?
[{:title (tr "workspace.tokens.delete")
:no-selectable true
:action #(on-delete-token token)}]))))
(defn- allowed-shape-attributes (defn- allowed-shape-attributes
[shapes] [shapes]

View File

@ -92,8 +92,8 @@
(not (and (some? edition) (not (and (some? edition)
(= :text (:type (get objects edition)))))) (= :text (:type (get objects edition))))))
can-edit? can-edit-tokens?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit-tokens?)
is-selected-inside-layout (d/nilv is-selected-inside-layout false) is-selected-inside-layout (d/nilv is-selected-inside-layout false)
@ -177,7 +177,7 @@
:aria-controls (dm/str "token-tree-" (name type)) :aria-controls (dm/str "token-tree-" (name type))
:on-toggle-expand on-toggle-open-click :on-toggle-expand on-toggle-open-click
:icon (token-section-icon type)} :icon (token-section-icon type)}
(when can-edit? (when can-edit-tokens?
[:> icon-button* {:id (str "add-token-button-" title) [:> icon-button* {:id (str "add-token-button-" title)
:icon "add" :icon "add"
:aria-label (tr "workspace.tokens.add-token" title) :aria-label (tr "workspace.tokens.add-token" title)

View File

@ -140,14 +140,17 @@
on-pill-context-menu on-pill-context-menu
on-node-context-menu]}] on-node-context-menu]}]
(let [separator "." (let [separator "."
raw-tree (mf/with-memo [tokens] raw-tree
(cpn/build-tree-root tokens separator)) (mf/with-memo [tokens]
permissions (mf/use-ctx ctx/permissions) (cpn/build-tree-root tokens separator))
can-edit? (:can-edit permissions)
can-edit-file?
(mf/use-ctx ctx/can-edit?)
on-node-context-menu (mf/use-fn on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu) (mf/deps can-edit-file? on-node-context-menu)
(fn [event node] (fn [event node]
(when can-edit? (when can-edit-file?
(on-node-context-menu event node)))) (on-node-context-menu event node))))
ordered-nodes (mf/with-memo [raw-tree] ordered-nodes (mf/with-memo [raw-tree]

View File

@ -38,8 +38,8 @@
token-sets token-sets
(some-> tokens-lib (ctob/get-set-tree)) (some-> tokens-lib (ctob/get-set-tree))
can-edit? can-edit-tokens?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit-tokens?)
token-set-active? token-set-active?
(mf/use-fn (mf/use-fn
@ -55,17 +55,17 @@
on-reset-edition on-reset-edition
(mf/use-fn (mf/use-fn
(mf/deps can-edit?) (mf/deps can-edit-tokens?)
(fn [_] (fn [_]
(when can-edit? (when can-edit-tokens?
(st/emit! (dwtl/clear-token-set-edition) (st/emit! (dwtl/clear-token-set-edition)
(dwtl/clear-token-set-creation))))) (dwtl/clear-token-set-creation)))))
on-start-edition on-start-edition
(mf/use-fn (mf/use-fn
(mf/deps can-edit?) (mf/deps can-edit-tokens?)
(fn [id] (fn [id]
(when can-edit? (when can-edit-tokens?
(st/emit! (dwtl/start-token-set-edition id)))))] (st/emit! (dwtl/start-token-set-edition id)))))]
[:> controlled-sets-list* [:> controlled-sets-list*
@ -81,7 +81,6 @@
:edition-id edition-id :edition-id edition-id
:origin "set-panel" :origin "set-panel"
:can-edit can-edit?
:on-start-edition on-start-edition :on-start-edition on-start-edition
:on-reset-edition on-reset-edition :on-reset-edition on-reset-edition

View File

@ -86,8 +86,8 @@
(mf/defc inline-add-button* (mf/defc inline-add-button*
[] []
(let [can-edit? (mf/use-ctx ctx/can-edit?)] (let [can-edit-tokens? (mf/use-ctx ctx/can-edit-tokens?)]
(if can-edit? (if can-edit-tokens?
[:div {:class (stl/css :empty-sets-wrapper)} [:div {:class (stl/css :empty-sets-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.tokens.no-sets-yet")] (tr "workspace.tokens.no-sets-yet")]
@ -110,7 +110,10 @@
[{:keys [id label is-editing is-active is-selected is-draggable is-collapsed path depth index [{:keys [id label is-editing is-active is-selected is-draggable is-collapsed path depth index
on-toggle on-drop on-start-edition on-reset-edition on-edit-submit on-toggle-collapse]}] on-toggle on-drop on-start-edition on-reset-edition on-edit-submit on-toggle-collapse]}]
(let [can-edit? (let [can-edit-tokens?
(mf/use-ctx ctx/can-edit-tokens?)
can-edit-file?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit?)
label-id label-id
@ -118,11 +121,11 @@
on-context-menu on-context-menu
(mf/use-fn (mf/use-fn
(mf/deps is-editing id path can-edit?) (mf/deps is-editing id path can-edit-tokens?)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(when (and can-edit? (not is-editing)) (when (and can-edit-tokens? (not is-editing))
(st/emit! (dwtl/assign-token-set-context-menu (st/emit! (dwtl/assign-token-set-context-menu
{:position (dom/get-client-position event) {:position (dom/get-client-position event)
:is-group true :is-group true
@ -141,12 +144,12 @@
on-checkbox-click on-checkbox-click
(mf/use-fn (mf/use-fn
(mf/deps on-toggle path can-edit?) (mf/deps on-toggle path can-edit-file?)
#(on-toggle path)) #(on-toggle path))
on-edit-submit' on-edit-submit'
(mf/use-fn (mf/use-fn
(mf/deps path on-edit-submit can-edit?) (mf/deps path on-edit-submit can-edit-tokens?)
#(on-edit-submit path %)) #(on-edit-submit path %))
on-drop on-drop
@ -196,7 +199,7 @@
label] label]
[:> checkbox* [:> checkbox*
{:on-click on-checkbox-click {:on-click on-checkbox-click
:disabled (not can-edit?) :disabled (not can-edit-file?)
:checked (case is-active :checked (case is-active
:all true :all true
:partial "mixed" :partial "mixed"
@ -207,7 +210,11 @@
[{:keys [id set label is-editing is-active is-selected is-draggable is-new path depth index [{:keys [id set label is-editing is-active is-selected is-draggable is-new path depth index
on-select on-toggle on-drop on-start-edition on-reset-edition on-edit-submit]}] on-select on-toggle on-drop on-start-edition on-reset-edition on-edit-submit]}]
(let [can-edit? (mf/use-ctx ctx/can-edit?) (let [can-edit-tokens?
(mf/use-ctx ctx/can-edit-tokens?)
can-edit-file?
(mf/use-ctx ctx/can-edit?)
on-click on-click
(mf/use-fn (mf/use-fn
@ -220,11 +227,11 @@
on-context-menu on-context-menu
(mf/use-fn (mf/use-fn
(mf/deps is-editing id path can-edit?) (mf/deps is-editing id path can-edit-tokens?)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(when (and can-edit? (not is-editing)) (when (and can-edit-tokens? (not is-editing))
(st/emit! (dwtl/assign-token-set-context-menu (st/emit! (dwtl/assign-token-set-context-menu
{:position (dom/get-client-position event) {:position (dom/get-client-position event)
:is-group false :is-group false
@ -305,7 +312,7 @@
label] label]
[:> checkbox* [:> checkbox*
{:on-click on-checkbox-click {:on-click on-checkbox-click
:disabled (not can-edit?) :disabled (not can-edit-file?)
:arial-label (tr "workspace.tokens.select-set") :arial-label (tr "workspace.tokens.select-set")
:checked is-active}]])])) :checked is-active}]])]))
@ -455,12 +462,12 @@
(assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop") (assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop")
(assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop") (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop")
(let [theme-modal? (= origin "theme-modal") (let [theme-modal? (= origin "theme-modal")
can-edit? (mf/use-ctx ctx/can-edit?) can-edit-tokens? (mf/use-ctx ctx/can-edit-tokens?)
draggable? (and (not theme-modal?) can-edit?) draggable? (and (not theme-modal?) can-edit-tokens?)
empty-state? (and theme-modal? empty-state? (and theme-modal?
(empty? token-sets) (empty? token-sets)
(not new-path)) (not new-path))
;; NOTE: on-reset-edition and on-start-edition function can ;; NOTE: on-reset-edition and on-start-edition function can
;; come as nil, in this case we need to provide a safe ;; come as nil, in this case we need to provide a safe

View File

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.sidebar (ns app.main.ui.workspace.tokens.sidebar
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.files.tokens :as cfo]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.config :as cf] [app.config :as cf]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -62,9 +63,8 @@
{::mf/private true} {::mf/private true}
[{:keys [resize-height] :as props}] [{:keys [resize-height] :as props}]
(let [can-edit? (let [can-edit-tokens?
(mf/use-ctx ctx/can-edit?)] (mf/use-ctx ctx/can-edit-tokens?)]
[:* [:*
[:> token-set-context-menu*] [:> token-set-context-menu*]
[:section {:data-testid "token-management-sidebar" [:section {:data-testid "token-management-sidebar"
@ -73,7 +73,7 @@
[:> themes-header*] [:> themes-header*]
[:div {:class (stl/css :sidebar-header)} [:div {:class (stl/css :sidebar-header)}
[:> title-bar* {:title (tr "labels.sets")} [:> title-bar* {:title (tr "labels.sets")}
(when can-edit? (when can-edit-tokens?
[:> tsetslist/add-button*])]] [:> tsetslist/add-button*])]]
[:> token-sets-list* props]]])) [:> token-sets-list* props]]]))
@ -83,8 +83,8 @@
(let [show-menu* (mf/use-state false) (let [show-menu* (mf/use-state false)
show-menu? (deref show-menu*) show-menu? (deref show-menu*)
can-edit? can-edit-tokens?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit-tokens?)
open-menu open-menu
(mf/use-fn (mf/use-fn
@ -124,7 +124,7 @@
:on-close close-menu :on-close close-menu
:id "tokens-menu" :id "tokens-menu"
:class (stl/css :import-export-menu)} :class (stl/css :import-export-menu)}
(when can-edit? (when can-edit-tokens?
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-modal-show} :on-click on-modal-show}
[:div {:class (stl/css :import-menu-item)} [:div {:class (stl/css :import-menu-item)}
@ -133,8 +133,7 @@
:on-click on-export} :on-click on-export}
(tr "labels.export")]] (tr "labels.export")]]
(when (and can-edit-tokens? (contains? cf/flags :token-base-font-size))
(when (and can-edit? (contains? cf/flags :token-base-font-size))
[:> icon-button* {:variant "secondary" [:> icon-button* {:variant "secondary"
:icon i/settings :icon i/settings
:aria-label "Settings" :aria-label "Settings"
@ -146,18 +145,30 @@
on-lost-pointer-capture-pages :on-lost-pointer-capture on-lost-pointer-capture-pages :on-lost-pointer-capture
on-pointer-move-pages :on-pointer-move on-pointer-move-pages :on-pointer-move
size-pages-opened :size} size-pages-opened :size}
(use-resize-hook :tokens 200 38 "0.6" :y false nil)] (use-resize-hook :tokens 200 38 "0.6" :y false nil)
[:div {:class (stl/css :sidebar-wrapper)} current-file-data
[:> token-management-section* (mf/deref refs/workspace-data)
{:resize-height size-pages-opened
:tokens-lib tokens-lib}] can-edit-file?
[:article {:class (stl/css :tokens-section-wrapper) (mf/use-ctx ctx/can-edit?)
:data-testid "tokens-sidebar"}
[:div {:class (stl/css :resize-area-horiz) can-edit-tokens?
:on-pointer-down on-pointer-down-pages (mf/with-memo [can-edit-file? current-file-data]
:on-lost-pointer-capture on-lost-pointer-capture-pages (and can-edit-file?
:on-pointer-move on-pointer-move-pages} (cfo/editable-tokens? current-file-data)))]
[:div {:class (stl/css :resize-handle-horiz)}]]
[:> tokens-section* props]] [:> (mf/provider ctx/can-edit-tokens?) {:value can-edit-tokens?}
[:> import-export-button*]])) [:div {:class (stl/css :sidebar-wrapper)}
[:> token-management-section*
{:resize-height size-pages-opened
:tokens-lib tokens-lib}]
[:article {:class (stl/css :tokens-section-wrapper)
:data-testid "tokens-sidebar"}
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}
[:div {:class (stl/css :resize-handle-horiz)}]]
[:> tokens-section* props]]
[:> import-export-button*]]]))

View File

@ -23,8 +23,8 @@
(let [ordered-themes (let [ordered-themes
(mf/deref refs/workspace-token-themes-no-hidden) (mf/deref refs/workspace-token-themes-no-hidden)
can-edit? can-edit-tokens?
(mf/use-ctx ctx/can-edit?) (mf/use-ctx ctx/can-edit-tokens?)
open-modal open-modal
(mf/use-fn (mf/use-fn
@ -38,11 +38,11 @@
[:div {:class (stl/css :empty-theme-wrapper)} [:div {:class (stl/css :empty-theme-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.tokens.no-themes")] (tr "workspace.tokens.no-themes")]
(when can-edit? (when can-edit-tokens?
[:button {:on-click open-modal [:button {:on-click open-modal
:class (stl/css :create-theme-button)} :class (stl/css :create-theme-button)}
(tr "workspace.tokens.create-one")])] (tr "workspace.tokens.create-one")])]
(if can-edit? (if can-edit-tokens?
[:div {:class (stl/css :theme-selector-wrapper)} [:div {:class (stl/css :theme-selector-wrapper)}
[:> theme-selector*] [:> theme-selector*]
[:> button* {:variant "secondary" [:> button* {:variant "secondary"
@ -50,6 +50,6 @@
:class (stl/css :edit-theme-button) :class (stl/css :edit-theme-button)
:on-click open-modal} :on-click open-modal}
(tr "labels.edit")]] (tr "labels.edit")]]
[:div {:title (when-not can-edit? [:div {:title (when-not can-edit-tokens?
(tr "workspace.tokens.no-permission-themes"))} (tr "workspace.tokens.no-permission-themes"))}
[:> theme-selector*]]))])) [:> theme-selector*]]))]))

View File

@ -10,6 +10,7 @@
[app.common.files.tokens :as cfo] [app.common.files.tokens :as cfo]
[app.common.json :as json] [app.common.json :as json]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.file :as ctf]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos] [app.common.types.tokens-status :as ctos]
@ -217,8 +218,10 @@
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (-> (dwtl/update-token set-id id {:name value}) (do
(se/add-event plugin-id)))))} (u/check-editable-tokens file-id)
(st/emit! (-> (dwtl/update-token set-id id {:name value})
(se/add-event plugin-id))))))}
:type :type
{:this true {:this true
@ -248,11 +251,13 @@
(u/not-valid plugin-id :value "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :value "Plugin doesn't have 'content:write' permission")
:else :else
(let [token (u/locate-token file-id set-id id) (do
value (cond-> value (u/check-editable-tokens file-id)
(= :font-family (:type token)) (let [token (u/locate-token file-id set-id id)
(ctob/convert-dtcg-font-family))] value (cond-> value
(st/emit! (dwtl/update-token set-id id {:value value})))))} (= :font-family (:type token))
(ctob/convert-dtcg-font-family))]
(st/emit! (dwtl/update-token set-id id {:value value}))))))}
:resolvedValue :resolvedValue
{:this true {:this true
@ -290,8 +295,10 @@
(u/not-valid plugin-id :description "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :description "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (-> (dwtl/update-token set-id id {:description value}) (do
(se/add-event plugin-id)))))} (u/check-editable-tokens file-id)
(st/emit! (-> (dwtl/update-token set-id id {:description value})
(se/add-event plugin-id))))))}
:duplicate :duplicate
(fn [] (fn []
@ -305,13 +312,15 @@
;; - use this function in dwtl/duplicate-token ;; - use this function in dwtl/duplicate-token
;; - return the new token proxy using the locally forced id ;; - return the new token proxy using the locally forced id
;; - do the same with sets and themes ;; - do the same with sets and themes
(let [token (u/locate-token file-id set-id id) (do
token' (ctob/make-token (-> (datafy token) (u/check-editable-tokens file-id)
(dissoc :id (let [token (u/locate-token file-id set-id id)
:modified-at)))] token' (ctob/make-token (-> (datafy token)
(st/emit! (-> (dwtl/create-token set-id token') (dissoc :id
(se/add-event plugin-id))) :modified-at)))]
(token-proxy plugin-id file-id set-id (:id token'))))) (st/emit! (-> (dwtl/create-token set-id token')
(se/add-event plugin-id)))
(token-proxy plugin-id file-id set-id (:id token'))))))
:remove :remove
(fn [] (fn []
@ -320,8 +329,10 @@
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (-> (dwtl/delete-token set-id id) (do
(se/add-event plugin-id))))) (u/check-editable-tokens file-id)
(st/emit! (-> (dwtl/delete-token set-id id)
(se/add-event plugin-id))))))
:applyToShapes :applyToShapes
{:enumerable false {:enumerable false
@ -377,8 +388,10 @@
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else :else
(let [set (u/locate-token-set file-id id)] (do
(st/emit! (dwtl/rename-token-set set name)))))} (u/check-editable-tokens file-id)
(let [set (u/locate-token-set file-id id)]
(st/emit! (dwtl/rename-token-set set name))))))}
:active :active
{:this true {:this true
@ -395,7 +408,7 @@
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwtl/set-enabled-token-set id value))))} (st/emit! (dwtl/set-enabled-token-set id value))))} ;; This can be done even with tokens in an external library
:toggleActive :toggleActive
(fn [] (fn []
@ -468,28 +481,30 @@
(u/not-valid plugin-id :addToken "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :addToken "Plugin doesn't have 'content:write' permission")
:else :else
(let [tokens-lib (u/locate-tokens-lib file-id) (do
token (ctob/make-token attrs) (u/check-editable-tokens file-id)
;; Resolve against all tokens in the library (including those (let [tokens-lib (u/locate-tokens-lib file-id)
;; in inactive sets) so that references to structurally token (ctob/make-token attrs)
;; existing tokens resolve even if their set is not active. ;; Resolve against all tokens in the library (including those
;; The target set's tokens take precedence over equally named ;; in inactive sets) so that references to structurally
;; tokens in other sets, and the new token takes precedence ;; existing tokens resolve even if their set is not active.
;; over all. ;; The target set's tokens take precedence over equally named
tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib) ;; tokens in other sets, and the new token takes precedence
(ctob/get-tokens tokens-lib id)) ;; over all.
(assoc (:name token) token)) tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib)
resolved-tokens (ts/resolve-tokens tokens-tree) (ctob/get-tokens tokens-lib id))
(assoc (:name token) token))
resolved-tokens (ts/resolve-tokens tokens-tree)
{:keys [errors resolved-value] :as resolved-token} {:keys [errors resolved-value] :as resolved-token}
(get resolved-tokens (:name token))] (get resolved-tokens (:name token))]
(if resolved-value (if resolved-value
(do (st/emit! (-> (dwtl/create-token id token) (do (st/emit! (-> (dwtl/create-token id token)
(se/add-event plugin-id))) (se/add-event plugin-id)))
(token-proxy plugin-id file-id id (:id token))) (token-proxy plugin-id file-id id (:id token)))
(do (u/not-valid plugin-id :addToken (str errors)) (do (u/not-valid plugin-id :addToken (str errors))
nil)))))} nil))))))}
:duplicate :duplicate
(fn [] (fn []
@ -498,10 +513,12 @@
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
:else :else
(let [id-ref (atom nil)] (do
(st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref})) (u/check-editable-tokens file-id)
(when (some? @id-ref) (let [id-ref (atom nil)]
(token-set-proxy plugin-id file-id @id-ref))))) (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 :remove
(fn [] (fn []
@ -510,7 +527,9 @@
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwtl/delete-token-set id))))))) (do
(u/check-editable-tokens file-id)
(st/emit! (dwtl/delete-token-set id))))))))
(defn token-theme-proxy? [p] (defn token-theme-proxy? [p]
(obj/type-of? p "TokenThemeProxy")) (obj/type-of? p "TokenThemeProxy"))
@ -568,8 +587,10 @@
(u/not-valid plugin-id :group "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :group "Plugin doesn't have 'content:write' permission")
:else :else
(let [theme (u/locate-token-theme file-id id)] (do
(st/emit! (dwtl/update-token-theme id (assoc theme :group group))))))} (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 :name
{:this true {:this true
@ -589,9 +610,11 @@
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else :else
(let [theme (u/locate-token-theme file-id id)] (do
(when name (u/check-editable-tokens file-id)
(st/emit! (dwtl/update-token-theme id (assoc theme :name name)))))))} (let [theme (u/locate-token-theme file-id id)]
(when name
(st/emit! (dwtl/update-token-theme id (assoc theme :name name))))))))}
:active :active
{:this true {:this true
@ -641,10 +664,12 @@
(u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission")
:else :else
(let [set-name (token-set-name (resolve-token-set file-id set-arg)) (do
theme (u/locate-token-theme file-id id)] (u/check-editable-tokens file-id)
(when (and set-name theme) (let [set-name (token-set-name (resolve-token-set file-id set-arg))
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name)))))))} 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 :removeSet
{:enumerable false {:enumerable false
@ -655,10 +680,12 @@
(u/not-valid plugin-id :removeSet "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :removeSet "Plugin doesn't have 'content:write' permission")
:else :else
(let [set-name (token-set-name (resolve-token-set file-id set-arg)) (do
theme (u/locate-token-theme file-id id)] (u/check-editable-tokens file-id)
(when (and set-name theme) (let [set-name (token-set-name (resolve-token-set file-id set-arg))
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name)))))))} 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 :duplicate
(fn [] (fn []
@ -667,12 +694,14 @@
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
:else :else
(let [theme (u/locate-token-theme file-id id) (do
theme' (ctob/make-token-theme (-> (datafy theme) (u/check-editable-tokens file-id)
(dissoc :id (let [theme (u/locate-token-theme file-id id)
:modified-at)))] theme' (ctob/make-token-theme (-> (datafy theme)
(st/emit! (dwtl/create-token-theme theme')) (dissoc :id
(token-theme-proxy plugin-id file-id (:id theme'))))) :modified-at)))]
(st/emit! (dwtl/create-token-theme theme'))
(token-theme-proxy plugin-id file-id (:id theme'))))))
:remove :remove
(fn [] (fn []
@ -681,7 +710,9 @@
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwtl/delete-token-theme id)))))) (do
(u/check-editable-tokens file-id)
(st/emit! (dwtl/delete-token-theme id)))))))
(defn tokens-catalog (defn tokens-catalog
[plugin-id file-id] [plugin-id file-id]
@ -690,6 +721,14 @@
:$plugin {:enumerable false :get (constantly plugin-id)} :$plugin {:enumerable false :get (constantly plugin-id)}
:$id {:enumerable false :get (constantly file-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 :themes
{:this true {:this true
:enumerable false :enumerable false
@ -726,9 +765,11 @@
(u/not-valid plugin-id :addTheme "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :addTheme "Plugin doesn't have 'content:write' permission")
:else :else
(let [theme (ctob/make-token-theme attrs)] (do
(st/emit! (dwtl/create-token-theme theme)) (u/check-editable-tokens file-id)
(token-theme-proxy plugin-id file-id (:id theme)))))} (let [theme (ctob/make-token-theme attrs)]
(st/emit! (dwtl/create-token-theme theme))
(token-theme-proxy plugin-id file-id (:id theme))))))}
:addSet :addSet
{:enumerable false {:enumerable false
@ -750,21 +791,23 @@
(u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission")
:else :else
(let [active? (boolean (:active attrs)) (do
attrs (-> attrs (u/check-editable-tokens file-id)
(dissoc :active) (let [active? (boolean (:active attrs))
(update :name ctob/normalize-set-name)) attrs (-> attrs
set (ctob/make-token-set attrs)] (dissoc :active)
(st/emit! (dwtl/create-token-set set)) (update :name ctob/normalize-set-name))
;; Newly created sets are inactive by default; activate it when set (ctob/make-token-set attrs)]
;; requested. Enabling only adds the set name to the hidden theme, (st/emit! (dwtl/create-token-set set))
;; so it does not depend on the create event having propagated yet. ;; Newly created sets are inactive by default; activate it when
(when active? ;; requested. Enabling only adds the set name to the hidden theme,
(st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) true))) ;; so it does not depend on the create event having propagated yet.
;; Pass the set name as `initial-name` so the proxy can resolve (when active?
;; it immediately, before the async `st/emit!` above propagates (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) true)))
;; the new set into `@st/state`. ;; Pass the set name as `initial-name` so the proxy can resolve
(token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set)))))} ;; 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 :getThemeById
{:enumerable false {:enumerable false

View File

@ -9,6 +9,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.i18n :as i18n :refer [tr]] [app.common.i18n :as i18n :refer [tr]]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.messages :as csm] [app.common.schema.messages :as csm]
@ -67,13 +68,18 @@
(defn locate-tokens-lib (defn locate-tokens-lib
[file-id] [file-id]
(let [file (locate-file file-id)] (let [file (locate-file file-id)
(->> file :data :tokens-lib))) file-data (ctf/file-data file)
tokens-source-id (cfo/get-tokens-source file-data)
tokens-file (locate-file tokens-source-id)
tokens-file-data (ctf/file-data tokens-file)]
(cfo/get-tokens-lib tokens-file-data)))
(defn locate-tokens-status (defn locate-tokens-status
[file-id] [file-id]
(let [file (locate-file file-id)] (let [file (locate-file file-id)
(->> file :data :tokens-status))) file-data (ctf/file-data file)]
(cfo/get-tokens-status file-data)))
(defn locate-token-theme (defn locate-token-theme
[file-id id] [file-id id]
@ -271,6 +277,12 @@
(boolean (boolean
(dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering]))) (dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering])))
(defn check-editable-tokens
[file-id]
(let [file (locate-file file-id)]
(when-not (cfo/editable-tokens? (ctf/file-data file))
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Cannot modify tokens in an external library"))))))
(defn throw-validation-errors? (defn throw-validation-errors?
[plugin-id] [plugin-id]
(boolean (boolean

View File

@ -6,13 +6,15 @@
<div class="panel"> <div class="panel">
<div class="panel-heading"> <div class="panel-heading">
<p class="headline-m">THEMES</p> <p class="headline-m">THEMES</p>
<button @if (isEditableTokens) {
type="button" <button
data-appearance="secondary" type="button"
(click)="addTheme()" data-appearance="secondary"
> (click)="addTheme()"
+ >
</button> +
</button>
}
</div> </div>
<ul data-handler="themes-list"> <ul data-handler="themes-list">
@ -24,20 +26,22 @@
} }
{{ theme.name }} {{ theme.name }}
</span> </span>
<button @if (isEditableTokens) {
type="button" <button
data-appearance="secondary" type="button"
(click)="renameTheme(theme.id, theme.name)" data-appearance="secondary"
> (click)="renameTheme(theme.id, theme.name)"
🖊️ >
</button> 🖊️
<button </button>
type="button" <button
data-appearance="secondary" type="button"
(click)="deleteTheme(theme.id)" data-appearance="secondary"
> (click)="deleteTheme(theme.id)"
>
</button>
</button>
}
<div class="checkbox-container"> <div class="checkbox-container">
<input <input
class="checkbox-input" class="checkbox-input"
@ -55,9 +59,11 @@
<div class="panel"> <div class="panel">
<div class="panel-heading"> <div class="panel-heading">
<p class="headline-m">SETS</p> <p class="headline-m">SETS</p>
<button type="button" data-appearance="secondary" (click)="addSet()"> @if (isEditableTokens) {
+ <button type="button" data-appearance="secondary" (click)="addSet()">
</button> +
</button>
}
</div> </div>
<ul data-handler="sets-list"> <ul data-handler="sets-list">
@ -69,20 +75,22 @@
<span (click)="loadTokens(set.id)"> <span (click)="loadTokens(set.id)">
{{ set.name }} {{ set.name }}
</span> </span>
<button @if (isEditableTokens) {
type="button" <button
data-appearance="secondary" type="button"
(click)="renameSet(set.id, set.name)" data-appearance="secondary"
> (click)="renameSet(set.id, set.name)"
🖊️ >
</button> 🖊️
<button </button>
type="button" <button
data-appearance="secondary" type="button"
(click)="deleteSet(set.id)" data-appearance="secondary"
> (click)="deleteSet(set.id)"
>
</button>
</button>
}
<div class="checkbox-container"> <div class="checkbox-container">
<input <input
class="checkbox-input" class="checkbox-input"
@ -105,13 +113,15 @@
@for (group of tokenGroups; track group[0]) { @for (group of tokenGroups; track group[0]) {
<li class="body-m token-group"> <li class="body-m token-group">
<span>{{ group[0] }}</span> <span>{{ group[0] }}</span>
<button @if (isEditableTokens) {
type="button" <button
data-appearance="secondary" type="button"
(click)="addToken(group[0])" data-appearance="secondary"
> (click)="addToken(group[0])"
+ >
</button> +
</button>
}
</li> </li>
@for (token of group[1]; track token.id) { @for (token of group[1]; track token.id) {
<li <li
@ -125,20 +135,22 @@
> >
{{ token.name }} {{ token.name }}
</span> </span>
<button @if (isEditableTokens) {
type="button" <button
data-appearance="secondary" type="button"
(click)="renameToken(token.id, token.name)" data-appearance="secondary"
> (click)="renameToken(token.id, token.name)"
🖊️ >
</button> 🖊️
<button </button>
type="button" <button
data-appearance="secondary" type="button"
(click)="deleteToken(token.id)" data-appearance="secondary"
> (click)="deleteToken(token.id)"
>
</button>
</button>
}
</li> </li>
} }
} }

View File

@ -59,6 +59,7 @@ export class AppComponent {
), ),
); );
public isEditableTokens: boolean = false;
public themes: TokenTheme[] = []; public themes: TokenTheme[] = [];
public sets: TokenSet[] = []; public sets: TokenSet[] = [];
public tokenGroups: TokensGroup[] = []; public tokenGroups: TokensGroup[] = [];
@ -72,6 +73,8 @@ export class AppComponent {
this.#setSets(event.data.setsData); this.#setSets(event.data.setsData);
} else if (event.data.type === 'set-tokens') { } else if (event.data.type === 'set-tokens') {
this.#setTokens(event.data.tokenGroupsData); this.#setTokens(event.data.tokenGroupsData);
} else if (event.data.type === 'set-is-editable-tokens') {
this.#setIsEditableTokens(event.data.isEditableTokens);
} }
}); });
} }
@ -268,6 +271,10 @@ export class AppComponent {
parent.postMessage(message, '*'); parent.postMessage(message, '*');
} }
#setIsEditableTokens(isEditableTokens: boolean) {
this.isEditableTokens = isEditableTokens;
}
#setThemes(themes: TokenTheme[]) { #setThemes(themes: TokenTheme[]) {
this.themes = themes; this.themes = themes;
} }

View File

@ -58,6 +58,14 @@ function sendMessage(message: PluginMessageEvent) {
function loadLibrary() { function loadLibrary() {
const tokensCatalog = penpot.library.local.tokens; const tokensCatalog = penpot.library.local.tokens;
const isEditableTokens = tokensCatalog.isEditableTokens;
penpot.ui.sendMessage({
source: 'penpot',
type: 'set-is-editable-tokens',
isEditableTokens,
});
const themes = tokensCatalog.themes; const themes = tokensCatalog.themes;
const themesData = themes.map((theme) => { const themesData = themes.map((theme) => {

View File

@ -5220,6 +5220,14 @@ export type Token =
* Themes. * Themes.
*/ */
export interface TokenCatalog { export interface TokenCatalog {
/**
* Returns true if the tokens can be edited (it can't
* if the tokens of the file are in an external library).
* Note that even in this case, Themes and Sets status still
* can be changed and tokens can be applied to shapes.
*/
readonly isEditableTokens: boolean;
/** /**
* The list of themes in this catalog, in creation order. * The list of themes in this catalog, in creation order.
*/ */