mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Add token source to plugins (#12183)
* ✨ Add token source to plugins * ✨ Changes after review * ✨ Move plugins code to library-edit
This commit is contained in:
parent
01aacde0a8
commit
02e10a005a
@ -523,7 +523,9 @@
|
||||
(effective-tokens-source? file-data (:id file-data))))
|
||||
|
||||
(defn editable-tokens?
|
||||
"Returns true if the file-data is its own tokens source."
|
||||
"Returns true if the file-data is its own tokens source.
|
||||
Call only with the current local file's data. This predicate does not check
|
||||
which file is open; callers must reject connected libraries first."
|
||||
[file-data]
|
||||
(effective-tokens-source? file-data (:id file-data)))
|
||||
|
||||
@ -810,4 +812,4 @@
|
||||
{:active-theme-ids active-theme-ids
|
||||
:active-set-ids active-set-ids})
|
||||
{:active-theme-ids #{}
|
||||
:active-set-ids #{}}))
|
||||
:active-set-ids #{}}))
|
||||
|
||||
@ -15,15 +15,34 @@
|
||||
|
||||
;; Tokens source
|
||||
|
||||
(defn- own-tokens-status
|
||||
"The current tokens status of `file-data`, keeping only the themes and sets
|
||||
that exist in its own tokens lib."
|
||||
[file-data]
|
||||
(let [tokens-status (cfo/get-tokens-status file-data)
|
||||
tokens-lib (cfo/get-tokens-lib file-data)]
|
||||
(if (and tokens-status tokens-lib)
|
||||
(cfo/sync-tokens-status-with-lib tokens-status tokens-lib)
|
||||
(ctos/make-tokens-status))))
|
||||
|
||||
(defn generate-set-tokens-source
|
||||
"Create changes for setting the tokens source of a file to `library`,
|
||||
copying the library's tokens status. If the library is nil, the tokens source
|
||||
is removed and the tokens status is cleared."
|
||||
copying the library's tokens status. If the library is the file itself,
|
||||
the themes and sets that don't exist in its own tokens lib are deactivated.
|
||||
If the library is nil, the tokens source is removed and the tokens status
|
||||
is cleared."
|
||||
[changes library]
|
||||
(let [library-id (:id library)
|
||||
library-tokens-status (if library
|
||||
(-> library ctf/file-data cfo/get-tokens-status)
|
||||
(ctos/make-tokens-status))]
|
||||
file-data (pcb/get-library-data changes)
|
||||
library-tokens-status (cond
|
||||
(nil? library)
|
||||
(ctos/make-tokens-status)
|
||||
|
||||
(= library-id (:id file-data))
|
||||
(own-tokens-status file-data)
|
||||
|
||||
:else
|
||||
(-> library ctf/file-data cfo/get-tokens-status))]
|
||||
(-> changes
|
||||
(pcb/set-tokens-source library-id)
|
||||
(pcb/set-tokens-status library-tokens-status))))
|
||||
|
||||
@ -177,7 +177,60 @@
|
||||
(t/is (= (ctos/get-active-theme-ids file-tokens-status)
|
||||
(ctos/get-active-theme-ids undo-tokens-status)))
|
||||
(t/is (= (ctos/get-active-set-ids file-tokens-status)
|
||||
(ctos/get-active-set-ids undo-tokens-status))))))
|
||||
(ctos/get-active-set-ids undo-tokens-status)))))
|
||||
|
||||
(t/testing "if the library is the same file, themes and sets of the previous source are deactivated"
|
||||
(let [set-id (uuid/next)
|
||||
external-set-id (uuid/next)
|
||||
external-theme-id (uuid/next)
|
||||
library-id (uuid/next)
|
||||
file (-> (tht/sample-file-with-tokens
|
||||
:file-id :file1
|
||||
:lib-fn #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :id set-id
|
||||
:name "one set")))
|
||||
:status-fn #(ctos/set-tokens-status % #{external-theme-id}
|
||||
#{set-id external-set-id}))
|
||||
(assoc-in [:data :tokens-source] library-id))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (ctf/file-data file))
|
||||
(clt/generate-set-tokens-source file))
|
||||
redo (thf/apply-changes file changes)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
file-tokens-status (tht/get-tokens-status file)
|
||||
redo-tokens-source (tht/get-tokens-source redo)
|
||||
redo-tokens-status (tht/get-tokens-status redo)
|
||||
undo-tokens-source (tht/get-tokens-source undo)
|
||||
undo-tokens-status (tht/get-tokens-status undo)]
|
||||
|
||||
;; Redo: only the themes and sets of the file's own lib stay active
|
||||
(t/is (nil? redo-tokens-source))
|
||||
(t/is (= #{} (ctos/get-active-theme-ids redo-tokens-status)))
|
||||
(t/is (= #{set-id} (ctos/get-active-set-ids redo-tokens-status)))
|
||||
|
||||
;; Undo: source and status restored to the original state
|
||||
(t/is (= library-id undo-tokens-source))
|
||||
(t/is (= (ctos/get-active-theme-ids file-tokens-status)
|
||||
(ctos/get-active-theme-ids undo-tokens-status)))
|
||||
(t/is (= (ctos/get-active-set-ids file-tokens-status)
|
||||
(ctos/get-active-set-ids undo-tokens-status)))))
|
||||
|
||||
(t/testing "if the library is the same file and it has no own tokens lib, the tokens status is cleared"
|
||||
(let [set-id (uuid/next)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(update :data dissoc :tokens-lib)
|
||||
(assoc-in [:data :tokens-source] (uuid/next))
|
||||
(assoc-in [:data :tokens-status] (ctos/set-tokens-status (ctos/make-tokens-status)
|
||||
#{} #{set-id})))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (ctf/file-data file))
|
||||
(clt/generate-set-tokens-source file))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-tokens-status (tht/get-tokens-status redo)]
|
||||
|
||||
(t/is (nil? (tht/get-tokens-source redo)))
|
||||
(t/is (= #{} (ctos/get-active-theme-ids redo-tokens-status)))
|
||||
(t/is (= #{} (ctos/get-active-set-ids redo-tokens-status))))))
|
||||
|
||||
;; Tokens lib
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.main.data.workspace.tokens.library-edit
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.tokens :as cfo]
|
||||
@ -48,6 +49,28 @@
|
||||
;; Helpers
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn editable-tokens-status?
|
||||
"True when file-id is the current file. Its activation status is editable
|
||||
even when its tokens come from an external source."
|
||||
[state file-id]
|
||||
(= file-id (:current-file-id state)))
|
||||
|
||||
(defn editable-tokens?
|
||||
"True when file-id is the current file and uses its own tokens source.
|
||||
Use this predicate when the file may be a connected library."
|
||||
[state file-id]
|
||||
(and (editable-tokens-status? state file-id)
|
||||
(cfo/editable-tokens? (dsh/lookup-file-data state file-id))))
|
||||
|
||||
(defn- check-editable-tokens!
|
||||
"Throws when the tokens of the current file come from an external library,
|
||||
so they can't be modified from this file."
|
||||
[state]
|
||||
(when-not (editable-tokens? state (:current-file-id state))
|
||||
(ex/raise :type :assertion
|
||||
:code :tokens-not-editable
|
||||
:hint "cannot modify the tokens of an external library")))
|
||||
|
||||
;; TODO HYMA: Copied over from workspace.cljs
|
||||
(defn update-shape
|
||||
[id attrs]
|
||||
@ -268,6 +291,7 @@
|
||||
(ptk/reify ::create-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (dsh/lookup-tokens-lib state)]
|
||||
(if (and tokens-lib (ctob/get-theme tokens-lib (ctob/get-id token-theme)))
|
||||
@ -286,6 +310,7 @@
|
||||
(ptk/reify ::update-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (dsh/lookup-tokens-lib state)]
|
||||
(if (and (not= id (ctob/get-id token-theme))
|
||||
@ -322,7 +347,7 @@
|
||||
(ptk/reify ::toggle-token-theme-active
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-tokens-source-data state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-status (dsh/lookup-tokens-status state)
|
||||
tokens-lib (dsh/lookup-tokens-lib state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
@ -338,6 +363,7 @@
|
||||
(ptk/reify ::delete-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
@ -352,6 +378,7 @@
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
@ -366,6 +393,7 @@
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
@ -377,7 +405,8 @@
|
||||
[set-group-path set-group-fname]
|
||||
(ptk/reify ::rename-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [it _state _]
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/rename-token-set-group set-group-path set-group-fname))]
|
||||
(rx/of
|
||||
@ -390,6 +419,7 @@
|
||||
(ptk/reify ::duplicate-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (dsh/lookup-tokens-lib state)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")]
|
||||
@ -458,6 +488,7 @@
|
||||
(ptk/reify ::import-tokens-lib
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
status (cfo/make-tokens-status-from-lib lib)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
@ -486,6 +517,7 @@
|
||||
(ptk/reify ::delete-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
@ -498,6 +530,7 @@
|
||||
(ptk/reify ::delete-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
@ -528,6 +561,7 @@
|
||||
(ptk/reify ::drop-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(try
|
||||
(when-let [changes (clo/generate-move-token-set-group (pcb/empty-changes it) (dsh/lookup-tokens-lib state) drop-opts)]
|
||||
(rx/of
|
||||
@ -544,6 +578,7 @@
|
||||
(ptk/reify ::drop-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(try
|
||||
(let [tokens-lib (dsh/lookup-tokens-lib state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
@ -559,6 +594,7 @@
|
||||
(ptk/reify ::create-token-and-set
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data
|
||||
(dsh/lookup-file-data state)
|
||||
|
||||
@ -594,6 +630,7 @@
|
||||
(ptk/reify ::create-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(if-let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))]
|
||||
@ -621,6 +658,7 @@
|
||||
(ptk/reify ::bulk-create-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [token-set (lookup-token-set state set-id)
|
||||
data (dsh/lookup-file-data state)
|
||||
changes (reduce (fn [changes token-id]
|
||||
@ -650,6 +688,7 @@
|
||||
(ptk/reify ::update-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))
|
||||
@ -678,6 +717,7 @@
|
||||
(ptk/reify ::bulk-update-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))
|
||||
@ -708,6 +748,7 @@
|
||||
(ptk/reify ::delete-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (dsh/lookup-tokens-lib state)
|
||||
token-set (if set-id
|
||||
@ -731,6 +772,7 @@
|
||||
(ptk/reify ::bulk-delete-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(check-editable-tokens! state)
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (reduce (fn [changes token-id]
|
||||
(pcb/set-token changes set-id token-id nil))
|
||||
@ -746,6 +788,7 @@
|
||||
(ptk/reify ::duplicate-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(check-editable-tokens! state)
|
||||
(when-let [token-set (lookup-token-set state)]
|
||||
(when-let [tokens-lib (dsh/lookup-tokens-lib state)]
|
||||
(when-let [token (ctob/get-token tokens-lib
|
||||
|
||||
@ -489,9 +489,10 @@
|
||||
(tr "common.publish")])]
|
||||
|
||||
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
has-tokens? (and (has-tokens? library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
can-import-tokens? (and (has-tokens? library)
|
||||
(cfo/editable-tokens? local-library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
[:div {:class (stl/css :section-list-item-legacy)
|
||||
:key (dm/str id)
|
||||
:data-testid "library-item"}
|
||||
@ -508,7 +509,7 @@
|
||||
[:span ")"]])])]]
|
||||
|
||||
[:div {:class (stl/css :library-actions)}
|
||||
(when ^boolean has-tokens?
|
||||
(when ^boolean can-import-tokens?
|
||||
[:> icon-button*
|
||||
{:type "button"
|
||||
:aria-label (tr "workspace.tokens.import-tokens")
|
||||
@ -865,9 +866,10 @@
|
||||
:title-class (stl/css :connected-libraries-title)}]
|
||||
(if (seq linked-libraries)
|
||||
(for [{:keys [id name data connected-to nested? parent-name] :as library} linked-libraries]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
has-tokens? (and (has-tokens? library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
can-import-tokens? (and (has-tokens? library)
|
||||
(cfo/editable-tokens? local-library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
[:div {:class (stl/css-case :section-list-item true
|
||||
:section-list-item-nested nested?)
|
||||
:key (dm/str id)
|
||||
@ -893,7 +895,7 @@
|
||||
:data-library-id (dm/str id)
|
||||
:on-click set-as-tokens-source}
|
||||
(tr "workspace.libraries.set-as-tokens-source")])
|
||||
(when ^boolean has-tokens?
|
||||
(when ^boolean can-import-tokens?
|
||||
[:> icon-button*
|
||||
{:type "button"
|
||||
:aria-label (tr "workspace.tokens.import-tokens")
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
@ -22,6 +23,7 @@
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.repo :as rp]
|
||||
@ -1300,6 +1302,37 @@
|
||||
(let [libraries (get @st/state :files)]
|
||||
(apply array (->> libraries keys (map (partial library-proxy plugin-id))))))}
|
||||
|
||||
:tokensSource
|
||||
{:get
|
||||
(fn []
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
file-data (ctf/file-data (u/locate-file file-id))]
|
||||
(library-proxy plugin-id (cfo/get-effective-tokens-source file-data))))
|
||||
:set
|
||||
(fn [value]
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
library-id (when (library-proxy? value) (obj/get value "$id"))
|
||||
library (some-> library-id u/locate-file)
|
||||
library-data (some-> library ctf/file-data)
|
||||
file-data (ctf/file-data (u/locate-file file-id))]
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :tokensSource "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (library-proxy? value))
|
||||
(u/not-valid plugin-id :tokensSource value)
|
||||
|
||||
(nil? library)
|
||||
(u/not-valid plugin-id :tokensSource "Library is not connected")
|
||||
|
||||
(and (not= library-id file-id)
|
||||
(not (cfo/tokens-provider? library-data)))
|
||||
(u/not-valid plugin-id :tokensSource "Library must have its own tokens")
|
||||
|
||||
(not (cfo/effective-tokens-source? file-data library-id))
|
||||
(st/emit! (-> (dwtl/set-tokens-source library-id)
|
||||
(se/add-event plugin-id))))))}
|
||||
|
||||
:availableLibraries
|
||||
(fn []
|
||||
(let [team-id (:current-team-id @st/state)]
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
[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]
|
||||
@ -512,8 +511,11 @@
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (dwtl/editable-tokens-status? @st/state file-id))
|
||||
(u/not-valid plugin-id :active "Cannot change activation through a connected library; use penpot.library.local.tokens")
|
||||
|
||||
:else
|
||||
(st/emit! (dwtl/set-enabled-token-set id value))))} ;; This can be done even with tokens in an external library
|
||||
(st/emit! (dwtl/set-enabled-token-set id value))))}
|
||||
|
||||
:toggleActive
|
||||
(fn []
|
||||
@ -521,6 +523,9 @@
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (dwtl/editable-tokens-status? @st/state file-id))
|
||||
(u/not-valid plugin-id :toggleActive "Cannot change activation through a connected library; use penpot.library.local.tokens")
|
||||
|
||||
:else
|
||||
(st/emit! (dwtl/toggle-token-set id))))
|
||||
|
||||
@ -737,6 +742,9 @@
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (dwtl/editable-tokens-status? @st/state file-id))
|
||||
(u/not-valid plugin-id :active "Cannot change activation through a connected library; use penpot.library.local.tokens")
|
||||
|
||||
:else
|
||||
(st/emit! (dwtl/set-token-theme-active id value))))}
|
||||
|
||||
@ -746,6 +754,9 @@
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (dwtl/editable-tokens-status? @st/state file-id))
|
||||
(u/not-valid plugin-id :toggleActive "Cannot change activation through a connected library; use penpot.library.local.tokens")
|
||||
|
||||
:else
|
||||
(st/emit! (dwtl/toggle-token-theme-active id))))
|
||||
|
||||
@ -839,8 +850,7 @@
|
||||
:enumerable false
|
||||
:get
|
||||
(fn []
|
||||
(let [file (u/locate-file file-id)]
|
||||
(cfo/editable-tokens? (ctf/file-data file))))}
|
||||
(dwtl/editable-tokens? @st/state file-id))}
|
||||
|
||||
:themes
|
||||
{:this true
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]))
|
||||
@ -288,9 +289,8 @@
|
||||
|
||||
(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"))))))
|
||||
(when-not (dwtl/editable-tokens? @st/state file-id)
|
||||
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Cannot modify tokens in an external library")))))
|
||||
|
||||
(defn throw-validation-errors?
|
||||
[plugin-id]
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
;; profile renderer preference instead of the global feature flag alone.
|
||||
:profile {:props {:renderer :wasm}}})
|
||||
|
||||
(defn- on-error
|
||||
(defn- report-error
|
||||
[cause]
|
||||
(js/console.error "STORE ERROR" (.-stack cause))
|
||||
(t/do-report {:type :error :message "Store error" :actual cause})
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
(defn setup-store
|
||||
([file] (setup-store file nil))
|
||||
([file {:keys [renderer libraries] :as _opts}]
|
||||
([file {:keys [renderer libraries on-error] :or {on-error report-error} :as _opts}]
|
||||
(let [state (-> initial-state
|
||||
(assoc :current-file-id (:id file)
|
||||
:current-page-id (cthf/current-page-id file)
|
||||
|
||||
@ -18,17 +18,21 @@
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.library :as library]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.tokens :as ptok]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as tha]
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[frontend-tests.helpers.async :as async]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[frontend-tests.tokens.helpers.state :as tohs]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
@ -71,7 +75,7 @@
|
||||
#js {:strokeColor "#000000" :strokeWidth 1}])
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(obj/set! (aget (.-strokes rect) index) property value)
|
||||
(await (tha/settle))
|
||||
(await (async/settle))
|
||||
(t/is (= (apply dissoc tokens removed) (get-in @store path)))
|
||||
(t/is (= value (obj/get (aget (.-strokes rect) index) property))))))))
|
||||
|
||||
@ -97,15 +101,344 @@
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(let [^js gradient (obj/get (aget (.-strokes rect) 0) "strokeColorGradient")]
|
||||
(set! (.-startX gradient) 0.5)
|
||||
(await (tha/settle))
|
||||
(await (async/settle))
|
||||
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
|
||||
(t/is (= 0.5 (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "startX")))
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(obj/set! (aget (.-stops gradient) 0) "color" "#ff0000")
|
||||
(await (tha/settle))
|
||||
(await (async/settle))
|
||||
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
|
||||
(t/is (= "#ff0000" (obj/get (aget (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "stops") 0) "color"))))))
|
||||
|
||||
(defn- token-source-file
|
||||
[file-label]
|
||||
(let [set-id (uuid/next)]
|
||||
(ctht/sample-file-with-tokens
|
||||
:file-id file-label
|
||||
:lib-fn #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :id set-id :name "Core"))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "Light"
|
||||
:group "Mode"
|
||||
:sets #{"Core"}))
|
||||
(ctob/add-token set-id (ctob/make-token :name "color.primary"
|
||||
:type :color
|
||||
:value "#FF0000")))
|
||||
:status-fn #(ctos/set-tokens-status % #{} #{set-id}))))
|
||||
|
||||
(defn- with-active-theme
|
||||
"The tokens status of `file` with its first visible theme activated."
|
||||
[file]
|
||||
(let [tokens-lib (ctht/get-tokens-lib file)
|
||||
theme-id (:id (first (remove ctob/hidden-theme? (ctob/get-themes tokens-lib))))]
|
||||
(cfo/activate-theme (ctht/get-tokens-status file) tokens-lib theme-id)))
|
||||
|
||||
(defn- tokens-unchanged?
|
||||
"True when the token attributes of `file` in the store are the original ones."
|
||||
[store file]
|
||||
(let [attrs [:tokens-lib :tokens-status :tokens-source]]
|
||||
(= (select-keys (:data file) attrs)
|
||||
(select-keys (get-in @store [:files (:id file) :data]) attrs))))
|
||||
|
||||
(defn- record-not-valid
|
||||
"A `u/not-valid` stub that collects each call into `errors`."
|
||||
[errors]
|
||||
(mock/stub (fn [plugin-id code value] (swap! errors conj [plugin-id code value]))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-switches-to-connected-library
|
||||
(let [file (token-source-file :local)
|
||||
source (token-source-file :source)
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
^js context (library/library-subcontext (str uuid/zero))
|
||||
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(t/is (= (str (:id file)) (.. context -tokensSource -id)))
|
||||
(set! (.-tokensSource context) source-proxy)
|
||||
(await (async/wait-for #(= (:id source) (get-in @store [:files (:id file) :data :tokens-source]))
|
||||
"library token source"))
|
||||
(t/is (= (str (:id source)) (.. context -tokensSource -id)))
|
||||
(t/is (= (datafy (ctht/get-tokens-status source))
|
||||
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
|
||||
(t/is (= (ctht/get-tokens-lib source) (u/locate-tokens-lib (:id file))))
|
||||
(t/is (false? (.. context -local -tokens -isEditableTokens)))
|
||||
(t/is (= (ctht/get-tokens-lib file)
|
||||
(get-in @store [:files (:id file) :data :tokens-lib])))
|
||||
(t/is (tokens-unchanged? store source))))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-restores-local-tokens
|
||||
(let [source (token-source-file :source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] (with-active-theme source)))
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
^js context (library/library-subcontext (str uuid/zero))]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(set! (.-tokensSource context) (.-local context))
|
||||
(await (async/wait-for #(nil? (get-in @store [:files (:id file) :data :tokens-source]))
|
||||
"local token source"))
|
||||
(let [^js catalog (.. context -local -tokens)]
|
||||
(t/is (= (str (:id file)) (.. context -tokensSource -id)))
|
||||
(t/is (true? (.-isEditableTokens catalog)))
|
||||
(t/is (= (ctht/get-tokens-lib file) (u/locate-tokens-lib (:id file))))
|
||||
(t/is (= #{} (ctos/get-active-set-ids (get-in @store [:files (:id file) :data :tokens-status]))))
|
||||
(t/is (not-any? #(.-active ^js %) (.-themes catalog))))))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-supports-undo-and-redo
|
||||
(let [file (token-source-file :local)
|
||||
source (token-source-file :source)
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
^js context (library/library-subcontext (str uuid/zero))
|
||||
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(st/emit! (tohs/watch-undo-stack))
|
||||
(set! (.-tokensSource context) source-proxy)
|
||||
(await (async/wait-for #(seq (get-in @store [:workspace-undo :items]))
|
||||
"token source undo entry"))
|
||||
(st/emit! dwu/undo)
|
||||
(await (async/wait-for #(nil? (get-in @store [:files (:id file) :data :tokens-source]))
|
||||
"undo token source"))
|
||||
(t/is (= (datafy (ctht/get-tokens-status file))
|
||||
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
|
||||
(st/emit! dwu/redo)
|
||||
(await (async/wait-for #(= (:id source) (get-in @store [:files (:id file) :data :tokens-source]))
|
||||
"redo token source"))
|
||||
(t/is (= (datafy (ctht/get-tokens-status source))
|
||||
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-keeps-status-when-source-is-unchanged
|
||||
(let [source (token-source-file :source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source)))
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
^js context (library/library-subcontext (str uuid/zero))
|
||||
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(set! (.-tokensSource context) source-proxy)
|
||||
(await (async/settle))
|
||||
(t/is (tokens-unchanged? store file))))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-rejects-invalid-libraries
|
||||
(let [file (token-source-file :local)
|
||||
empty-lib (ctht/sample-file-with-tokens :file-id :empty)
|
||||
indirect (-> (token-source-file :indirect)
|
||||
(assoc-in [:data :tokens-source] (uuid/next)))
|
||||
store (ths/setup-store file {:libraries [empty-lib indirect]})
|
||||
plugin-id (str uuid/zero)
|
||||
^js context (library/library-subcontext plugin-id)
|
||||
errors (atom [])]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store
|
||||
st/stream (ptk/input-stream store)
|
||||
u/not-valid (record-not-valid errors)}
|
||||
(doseq [[label value message]
|
||||
[["not a library" "invalid" "invalid"]
|
||||
["not connected" (library/library-proxy plugin-id (uuid/next)) "Library is not connected"]
|
||||
["without own tokens" (library/library-proxy plugin-id (:id empty-lib)) "Library must have its own tokens"]
|
||||
["with another tokens source" (library/library-proxy plugin-id (:id indirect)) "Library must have its own tokens"]]]
|
||||
(t/testing label
|
||||
(reset! errors [])
|
||||
(set! (.-tokensSource context) value)
|
||||
(t/is (= [[plugin-id :tokensSource message]] @errors))))
|
||||
(await (async/settle))
|
||||
(t/is (tokens-unchanged? store file))))))
|
||||
|
||||
(t/deftest ^:async external-catalogs-reject-tokens-changes
|
||||
(let [source (token-source-file :source)
|
||||
other (token-source-file :other)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source)))
|
||||
store (ths/setup-store file {:libraries [source other]})
|
||||
plugin-id (str uuid/zero)]
|
||||
(swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true)
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(doseq [[catalog-label ^js catalog]
|
||||
[["local catalog" (.. (library/library-subcontext plugin-id) -local -tokens)]
|
||||
["source catalog" (.. (library/library-subcontext plugin-id) -tokensSource -tokens)]
|
||||
["other connected catalog" (.-tokens (library/library-proxy plugin-id (:id other)))]]]
|
||||
(t/testing catalog-label
|
||||
(let [^js token-set (aget (.-sets catalog) 0)
|
||||
^js token (aget (.-tokens token-set) 0)
|
||||
^js theme (aget (.-themes catalog) 0)]
|
||||
(t/is (false? (.-isEditableTokens catalog)))
|
||||
(doseq [[label mutate]
|
||||
[["create set" #(.addSet catalog #js {:name "New"})]
|
||||
["rename set" #(set! (.-name token-set) "Renamed")]
|
||||
["duplicate set" #(.duplicate token-set)]
|
||||
["remove set" #(.remove token-set)]
|
||||
["create token" #(.addToken token-set #js {:name "color.new" :type "color" :value "#00FF00"})]
|
||||
["rename token" #(set! (.-name token) "color.renamed")]
|
||||
["change token value" #(set! (.-value token) "#00FF00")]
|
||||
["change token description" #(set! (.-description token) "New description")]
|
||||
["duplicate token" #(.duplicate token)]
|
||||
["remove token" #(.remove token)]
|
||||
["create theme" #(.addTheme catalog #js {:name "Dark" :group "Mode"})]
|
||||
["rename theme" #(set! (.-name theme) "Dark")]
|
||||
["change theme group" #(set! (.-group theme) "Other")]
|
||||
["add set to theme" #(.addSet theme token-set)]
|
||||
["remove set from theme" #(.removeSet theme token-set)]
|
||||
["duplicate theme" #(.duplicate theme)]
|
||||
["remove theme" #(.remove theme)]]]
|
||||
(t/testing label
|
||||
(t/is (thrown-with-msg? js/Error #"Cannot modify tokens in an external library"
|
||||
(mutate)))
|
||||
(await (async/settle))
|
||||
(t/is (tokens-unchanged? store file))
|
||||
(t/is (tokens-unchanged? store source))
|
||||
(t/is (tokens-unchanged? store other)))))))))))
|
||||
|
||||
(t/deftest ^:async external-catalogs-reject-status-changes
|
||||
(let [source (token-source-file :source)
|
||||
other (token-source-file :other)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] (with-active-theme source)))
|
||||
store (ths/setup-store file {:libraries [source other]})
|
||||
plugin-id (str uuid/zero)
|
||||
message "Cannot change activation through a connected library; use penpot.library.local.tokens"
|
||||
errors (atom [])]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store
|
||||
st/stream (ptk/input-stream store)
|
||||
u/not-valid (record-not-valid errors)}
|
||||
(let [^js context (library/library-subcontext plugin-id)]
|
||||
(t/is (true? (.-active ^js (aget (.. context -local -tokens -themes) 0))))
|
||||
(doseq [[catalog-label ^js catalog]
|
||||
[["source catalog" (.. context -tokensSource -tokens)]
|
||||
["other connected catalog" (.-tokens (library/library-proxy plugin-id (:id other)))]]]
|
||||
(t/testing catalog-label
|
||||
(let [^js token-set (aget (.-sets catalog) 0)
|
||||
^js theme (aget (.-themes catalog) 0)]
|
||||
(t/testing "reports the status of its own file"
|
||||
(t/is (true? (.-active token-set)))
|
||||
(t/is (false? (.-active theme))))
|
||||
(reset! errors [])
|
||||
(set! (.-active token-set) false)
|
||||
(.toggleActive token-set)
|
||||
(set! (.-active theme) true)
|
||||
(.toggleActive theme)
|
||||
(await (async/settle))
|
||||
(t/is (= [[plugin-id :active message]
|
||||
[plugin-id :toggleActive message]
|
||||
[plugin-id :active message]
|
||||
[plugin-id :toggleActive message]]
|
||||
@errors))
|
||||
(t/is (tokens-unchanged? store file))
|
||||
(t/is (tokens-unchanged? store source))
|
||||
(t/is (tokens-unchanged? store other))))))))))
|
||||
|
||||
(t/deftest ^:async external-token-source-allows-set-activation
|
||||
(let [source (token-source-file :source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source)))
|
||||
store (ths/setup-store file {:libraries [source]})]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
|
||||
^js token-set (aget (.-sets catalog) 0)]
|
||||
(t/is (true? (.-active token-set)))
|
||||
(set! (.-active token-set) false)
|
||||
(await (async/wait-for #(false? (.-active token-set)) "deactivate set"))
|
||||
(.toggleActive token-set)
|
||||
(await (async/wait-for #(true? (.-active token-set)) "toggle set"))
|
||||
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
|
||||
(t/is (tokens-unchanged? store source)))))))
|
||||
|
||||
(t/deftest ^:async external-token-source-allows-theme-activation
|
||||
(let [source (token-source-file :source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source)))
|
||||
store (ths/setup-store file {:libraries [source]})]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
|
||||
^js theme (aget (.-themes catalog) 0)]
|
||||
(t/is (false? (.-active theme)))
|
||||
(set! (.-active theme) true)
|
||||
(await (async/wait-for #(true? (.-active theme)) "activate theme"))
|
||||
(.toggleActive theme)
|
||||
(await (async/wait-for #(false? (.-active theme)) "toggle theme"))
|
||||
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
|
||||
(t/is (tokens-unchanged? store source)))))))
|
||||
|
||||
(t/deftest ^:async external-token-source-theme-toggle-supports-undo
|
||||
(let [source (token-source-file :source)
|
||||
status (with-active-theme source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] status))
|
||||
store (ths/setup-store file {:libraries [source]})]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
|
||||
^js theme (aget (.-themes catalog) 0)]
|
||||
(st/emit! (tohs/watch-undo-stack))
|
||||
(.toggleActive theme)
|
||||
(await (async/wait-for #(seq (get-in @store [:workspace-undo :items]))
|
||||
"theme toggle undo entry"))
|
||||
(t/is (false? (.-active theme)))
|
||||
(st/emit! dwu/undo)
|
||||
(await (async/wait-for #(= (datafy status)
|
||||
(datafy (get-in @store [:files (:id file) :data :tokens-status])))
|
||||
"undo external theme toggle"))
|
||||
(t/is (tokens-unchanged? store source)))))))
|
||||
|
||||
(t/deftest ^:async external-token-source-allows-applying-tokens-to-shapes
|
||||
(let [source (token-source-file :source)
|
||||
file (-> (token-source-file :local)
|
||||
(assoc-in [:data :tokens-source] (:id source))
|
||||
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source))
|
||||
(ctho/add-rect :rect1)
|
||||
(ctho/add-rect :rect2)
|
||||
(ctho/add-rect :rect3))
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
page-id (cthf/current-page-id file)]
|
||||
(swap! store assoc-in [:workspace-local :selected] #{(cthi/id :rect3)})
|
||||
(await
|
||||
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
|
||||
(let [^js context (api/create-context (str uuid/zero))
|
||||
^js page (.-currentPage context)
|
||||
^js catalog (.. context -library -local -tokens)
|
||||
^js token-set (aget (.-sets catalog) 0)
|
||||
^js token (aget (.-tokens token-set) 0)
|
||||
^js rect1 (.getShapeById page (str (cthi/id :rect1)))
|
||||
^js rect2 (.getShapeById page (str (cthi/id :rect2)))]
|
||||
(.applyToken rect1 token #js ["fill"])
|
||||
(.applyToShapes token #js [rect2] #js ["fill"])
|
||||
(.applyToSelected token #js ["fill"])
|
||||
(doseq [label [:rect1 :rect2 :rect3]]
|
||||
(let [shape-path [:files (:id file) :data :pages-index page-id :objects (cthi/id label)]]
|
||||
(await (async/wait-for #(= "color.primary" (get-in @store (conj shape-path :applied-tokens :fill)))
|
||||
(str "apply external token to " label)))
|
||||
(await (async/wait-for #(= "#ff0000" (get-in @store (conj shape-path :fills 0 :fill-color)))
|
||||
(str "resolve external token on " label)))))
|
||||
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
|
||||
(t/is (= (datafy (ctht/get-tokens-status file))
|
||||
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
|
||||
(t/is (tokens-unchanged? store source)))))))
|
||||
|
||||
(t/deftest ^:async library-tokens-source-requires-content-write-permission
|
||||
(let [file (token-source-file :local)
|
||||
source (token-source-file :source)
|
||||
plugin-id "read-only-plugin"
|
||||
store (ths/setup-store file {:libraries [source]})
|
||||
^js context (library/library-subcontext plugin-id)
|
||||
source-proxy (library/library-proxy plugin-id (:id source))
|
||||
errors (atom [])]
|
||||
(await
|
||||
(mock/with-mocks* {st/state store
|
||||
st/stream (ptk/input-stream store)
|
||||
u/not-valid (record-not-valid errors)}
|
||||
(set! (.-tokensSource context) source-proxy)
|
||||
(t/is (= [[plugin-id :tokensSource "Plugin doesn't have 'content:write' permission"]] @errors))
|
||||
(await (async/settle))
|
||||
(t/is (tokens-unchanged? store file))))))
|
||||
|
||||
;; Regression coverage for issue #9162.
|
||||
;;
|
||||
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
|
||||
|
||||
@ -15,10 +15,12 @@
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as async]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.tokens.helpers.state :as tohs]
|
||||
[frontend-tests.tokens.helpers.tokens :as toht]))
|
||||
[frontend-tests.tokens.helpers.tokens :as toht]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
@ -895,3 +897,64 @@
|
||||
(t/testing "Tokens lib has been imported into file without existing lib"
|
||||
(t/is (some? tokens-lib'))
|
||||
(t/is (some? (ctob/get-set-by-name tokens-lib' "Imported Set"))))))))))
|
||||
|
||||
(defn- setup-tokens-source-library
|
||||
[]
|
||||
(ctho/sample-file-with-tokens
|
||||
:file-id :library
|
||||
:lib-fn #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :source-set)
|
||||
:name "Source"))
|
||||
(ctob/add-token (cthi/id :source-set)
|
||||
(ctob/make-token :id (cthi/new-id! :source-token)
|
||||
:name "color.source"
|
||||
:type :color
|
||||
:value "#FF0000"))
|
||||
(ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :source-theme)
|
||||
:name "Light"
|
||||
:group "Mode"
|
||||
:sets #{"Source"})))
|
||||
:status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :source-set)})))
|
||||
|
||||
(t/deftest ^:async external-tokens-source-rejects-tokens-lib-changes
|
||||
(let [source (setup-tokens-source-library)
|
||||
file (-> (setup-file-with-token-lib)
|
||||
(assoc-in [:data :tokens-source] (:id source)))
|
||||
errors (atom [])
|
||||
store (ths/setup-store file {:libraries [source]
|
||||
:on-error #(swap! errors conj %)})
|
||||
tokens-lib (ctho/get-tokens-lib source)
|
||||
set-id (cthi/id :source-set)
|
||||
token-id (cthi/id :source-token)
|
||||
theme-id (cthi/id :source-theme)]
|
||||
(doseq [[label event]
|
||||
[["create theme" (dwtl/create-token-theme (ctob/make-token-theme :name "Dark" :group "Mode"))]
|
||||
["update theme" (dwtl/update-token-theme theme-id (ctob/rename (ctob/get-theme tokens-lib theme-id) "Dark"))]
|
||||
["delete theme" (dwtl/delete-token-theme theme-id)]
|
||||
["create set" (dwtl/create-token-set (ctob/make-token-set :name "New"))]
|
||||
["rename set" (dwtl/rename-token-set (ctob/get-set tokens-lib set-id) "Renamed")]
|
||||
["rename set group" (dwtl/rename-token-set-group ["Source"] "Renamed")]
|
||||
["duplicate set" (dwtl/duplicate-token-set set-id)]
|
||||
["delete set" (dwtl/delete-token-set set-id)]
|
||||
["delete set group" (dwtl/delete-token-set-group ["Source"])]
|
||||
["drop set group" (dwtl/drop-token-set-group {:from-index 0 :to-index 0 :position :top})]
|
||||
["drop set" (dwtl/drop-token-set {:from-index 0 :to-index 0 :position :top})]
|
||||
["import tokens lib" (dwtl/import-tokens-lib (ctob/make-tokens-lib))]
|
||||
["create token" (dwtl/create-token set-id (ctob/make-token :name "color.new"
|
||||
:type :color
|
||||
:value "#00FF00"))]
|
||||
["bulk create tokens" (dwtl/bulk-create-tokens set-id [token-id] :color {:depth 0} "brand")]
|
||||
["update token" (dwtl/update-token set-id token-id {:value "#00FF00"})]
|
||||
["bulk update tokens" (dwtl/bulk-update-tokens set-id [token-id] :color "color" "brand")]
|
||||
["delete token" (dwtl/delete-token set-id token-id)]
|
||||
["bulk delete tokens" (dwtl/bulk-delete-tokens set-id [token-id])]
|
||||
["duplicate token" (dwtl/duplicate-token token-id)]]]
|
||||
(t/testing label
|
||||
(reset! errors [])
|
||||
(ptk/emit! store event)
|
||||
(await (async/wait-for #(seq @errors) label))
|
||||
(t/is (= [{:type :assertion :code :tokens-not-editable}]
|
||||
(map #(select-keys (ex-data %) [:type :code]) @errors)))))
|
||||
(await (async/settle))
|
||||
(t/is (= file (get-in @store [:files (:id file)])))
|
||||
(t/is (= source (get-in @store [:files (:id source)])))))
|
||||
|
||||
@ -22426,10 +22426,12 @@ TokenCatalog:
|
||||
readonly isEditableTokens: boolean
|
||||
```
|
||||
|
||||
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.
|
||||
Returns true if the tokens in this catalog can be edited. Only the
|
||||
catalog of the current file (`penpot.library.local.tokens`) can be
|
||||
edited, and only when the file is its own tokens source.
|
||||
When the tokens come from an external library, the local catalog
|
||||
can still change the active themes and sets, and its tokens can
|
||||
still be applied to shapes.
|
||||
themes: |-
|
||||
```
|
||||
readonly themes: TokenTheme[]
|
||||
@ -22564,7 +22566,8 @@ TokenSet:
|
||||
active: boolean
|
||||
```
|
||||
|
||||
Indicates if the set is currently active.
|
||||
Indicates if the set is active in the file that owns this catalog.
|
||||
Only sets from `penpot.library.local.tokens` can be changed.
|
||||
tokens: |-
|
||||
```
|
||||
readonly tokens: Token[]
|
||||
@ -22584,6 +22587,7 @@ TokenSet:
|
||||
```
|
||||
|
||||
Toggles the active status of this set.
|
||||
Only sets from `penpot.library.local.tokens` can be changed.
|
||||
|
||||
Returns void
|
||||
getTokenById: |-
|
||||
@ -22707,7 +22711,8 @@ TokenTheme:
|
||||
active: boolean
|
||||
```
|
||||
|
||||
Indicates if the theme is currently active.
|
||||
Indicates if the theme is active in the file that owns this catalog.
|
||||
Only themes from `penpot.library.local.tokens` can be changed.
|
||||
activeSets: |-
|
||||
```
|
||||
readonly activeSets: TokenSet[]
|
||||
@ -22721,6 +22726,7 @@ TokenTheme:
|
||||
```
|
||||
|
||||
Toggles the active status of this theme.
|
||||
Only themes from `penpot.library.local.tokens` can be changed.
|
||||
|
||||
Returns void
|
||||
addSet: |-
|
||||
@ -23297,6 +23303,7 @@ LibraryContext:
|
||||
type LibraryContext = {
|
||||
local: Library;
|
||||
connected: Library[];
|
||||
tokensSource: Library;
|
||||
availableLibraries(): Promise<LibrarySummary[]>;
|
||||
connectLibrary(libraryId: string): Promise<Library>;
|
||||
}
|
||||
@ -23327,6 +23334,33 @@ LibraryContext:
|
||||
```
|
||||
const connectedLibraries = libraryContext.connected;
|
||||
```
|
||||
tokensSource: |-
|
||||
```
|
||||
tokensSource: Library
|
||||
```
|
||||
|
||||
The library that supplies tokens to the current file.
|
||||
Defaults to the local library.
|
||||
|
||||
Assign a connected library that has its own tokens to use it as the
|
||||
source. This copies its active themes and sets to the current file.
|
||||
Assign `local` to use the file's own tokens again. Themes and sets
|
||||
that don't exist in the local tokens are deactivated.
|
||||
Setting the source requires `content:write` permission and supports undo.
|
||||
Assigning the current source again keeps the active themes and sets.
|
||||
|
||||
The catalog of an external `tokensSource` is read-only and reports the
|
||||
library's own active themes and sets. To change which ones are active
|
||||
in the current file, use `penpot.library.local.tokens`, which lists the
|
||||
tokens of the source.
|
||||
|
||||
Example
|
||||
```
|
||||
const source = await penpot.library.connectLibrary('library-id');
|
||||
penpot.library.tokensSource = source;
|
||||
// Restore local tokens:
|
||||
penpot.library.tokensSource = penpot.library.local;
|
||||
```
|
||||
Methods:
|
||||
availableLibraries: |-
|
||||
```
|
||||
|
||||
@ -63,6 +63,8 @@ class PenpotAPIContentMarkdownConverter(MarkdownConverter):
|
||||
if node.name == "pre":
|
||||
for button in soup.find_all("button"):
|
||||
button.decompose()
|
||||
for br in soup.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
return f"\n```\n{soup.get_text().strip()}\n```\n\n"
|
||||
|
||||
# convert tsd-signature elements to code blocks, converting <br> to newlines
|
||||
|
||||
31
mcp/types-generator/test_prepare_api_docs.py
Normal file
31
mcp/types-generator/test_prepare_api_docs.py
Normal file
@ -0,0 +1,31 @@
|
||||
import unittest
|
||||
|
||||
from prepare_api_docs import PenpotAPIContentMarkdownConverter
|
||||
|
||||
|
||||
class PenpotAPIContentMarkdownConverterTest(unittest.TestCase):
|
||||
def test_preserves_line_breaks_in_code_examples(self):
|
||||
html = (
|
||||
'<pre><code>'
|
||||
'const source = await penpot.library.connectLibrary("library-id");<br>'
|
||||
'penpot.library.tokensSource = source;<br>'
|
||||
'// Restore local tokens:<br>'
|
||||
'penpot.library.tokensSource = penpot.library.local;'
|
||||
'</code><button>Copy</button></pre>'
|
||||
)
|
||||
|
||||
result = PenpotAPIContentMarkdownConverter().convert(html).strip()
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
'```\n'
|
||||
'const source = await penpot.library.connectLibrary("library-id");\n'
|
||||
'penpot.library.tokensSource = source;\n'
|
||||
'// Restore local tokens:\n'
|
||||
'penpot.library.tokensSource = penpot.library.local;\n'
|
||||
'```',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
- **plugin-types:** Added `LibraryComponent.restore()` to restore a deleted component and its main instance without changing the current page, selection, zoom, or viewport. Requires `library:write` permission.
|
||||
- **plugin-types:** Added `Shape.updateMain()`, `LibraryComponent.duplicate()`, writable component `annotation`, and `Library.deletedComponents`. Declared the existing `LibraryComponent.remove()` method.
|
||||
- **plugin-types:** Added `LibraryContext.tokensSource` to read or set the current file's token source to a connected library, or restore local tokens.
|
||||
- **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently.
|
||||
- **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs.
|
||||
- **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished
|
||||
|
||||
@ -301,7 +301,8 @@
|
||||
"availableLibraries",
|
||||
"connectLibrary",
|
||||
"connected",
|
||||
"local"
|
||||
"local",
|
||||
"tokensSource"
|
||||
],
|
||||
"LibraryElement": ["id", "libraryId", "name", "path"],
|
||||
"LibrarySummary": [
|
||||
@ -544,6 +545,7 @@
|
||||
"addTheme",
|
||||
"getSetById",
|
||||
"getThemeById",
|
||||
"isEditableTokens",
|
||||
"sets",
|
||||
"themes"
|
||||
],
|
||||
@ -5318,6 +5320,12 @@
|
||||
"type": "Library",
|
||||
"array": true
|
||||
},
|
||||
"tokensSource": {
|
||||
"decl": "LibraryContext",
|
||||
"kind": "getset",
|
||||
"type": "Library",
|
||||
"array": false
|
||||
},
|
||||
"availableLibraries": {
|
||||
"decl": "LibraryContext",
|
||||
"kind": "method",
|
||||
@ -9394,6 +9402,12 @@
|
||||
}
|
||||
},
|
||||
"TokenCatalog": {
|
||||
"isEditableTokens": {
|
||||
"decl": "TokenCatalog",
|
||||
"kind": "get",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"themes": {
|
||||
"decl": "TokenCatalog",
|
||||
"kind": "get",
|
||||
|
||||
44
plugins/libs/plugin-types/index.d.ts
vendored
44
plugins/libs/plugin-types/index.d.ts
vendored
@ -2892,6 +2892,32 @@ export type LibraryContext = {
|
||||
*/
|
||||
readonly connected: Library[];
|
||||
|
||||
/**
|
||||
* The library that supplies tokens to the current file.
|
||||
* Defaults to the local library.
|
||||
*
|
||||
* Assign a connected library that has its own tokens to use it as the
|
||||
* source. This copies its active themes and sets to the current file.
|
||||
* Assign `local` to use the file's own tokens again. Themes and sets
|
||||
* that don't exist in the local tokens are deactivated.
|
||||
* Setting the source requires `content:write` permission and supports undo.
|
||||
* Assigning the current source again keeps the active themes and sets.
|
||||
*
|
||||
* The catalog of an external `tokensSource` is read-only and reports the
|
||||
* library's own active themes and sets. To change which ones are active
|
||||
* in the current file, use `penpot.library.local.tokens`, which lists the
|
||||
* tokens of the source.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const source = await penpot.library.connectLibrary('library-id');
|
||||
* penpot.library.tokensSource = source;
|
||||
* // Restore local tokens:
|
||||
* penpot.library.tokensSource = penpot.library.local;
|
||||
* ```
|
||||
*/
|
||||
tokensSource: Library;
|
||||
|
||||
/**
|
||||
* Retrieves a summary of available libraries that can be connected to.
|
||||
* @return Returns a promise that resolves to an array of `LibrarySummary` objects representing available libraries.
|
||||
@ -5293,10 +5319,12 @@ export type Token =
|
||||
*/
|
||||
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.
|
||||
* Returns true if the tokens in this catalog can be edited. Only the
|
||||
* catalog of the current file (`penpot.library.local.tokens`) can be
|
||||
* edited, and only when the file is its own tokens source.
|
||||
* When the tokens come from an external library, the local catalog
|
||||
* can still change the active themes and sets, and its tokens can
|
||||
* still be applied to shapes.
|
||||
*/
|
||||
readonly isEditableTokens: boolean;
|
||||
|
||||
@ -5370,7 +5398,8 @@ export interface TokenSet {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Indicates if the set is currently active.
|
||||
* Indicates if the set is active in the file that owns this catalog.
|
||||
* Only sets from `penpot.library.local.tokens` can be changed.
|
||||
*/
|
||||
active: boolean;
|
||||
|
||||
@ -5386,6 +5415,7 @@ export interface TokenSet {
|
||||
|
||||
/**
|
||||
* Toggles the active status of this set.
|
||||
* Only sets from `penpot.library.local.tokens` can be changed.
|
||||
*/
|
||||
toggleActive(): void;
|
||||
|
||||
@ -5470,12 +5500,14 @@ export interface TokenTheme {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Indicates if the theme is currently active.
|
||||
* Indicates if the theme is active in the file that owns this catalog.
|
||||
* Only themes from `penpot.library.local.tokens` can be changed.
|
||||
*/
|
||||
active: boolean;
|
||||
|
||||
/**
|
||||
* Toggles the active status of this theme.
|
||||
* Only themes from `penpot.library.local.tokens` can be changed.
|
||||
*/
|
||||
toggleActive(): void;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user