mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 14:09:17 +00:00
⏪ Backport MCP from staging (part 1)
This commit is contained in:
parent
69e505a6a2
commit
3a39676969
@ -90,6 +90,8 @@
|
|||||||
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
|
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
|
||||||
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
|
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
|
||||||
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
|
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
|
||||||
|
- [MCP server] Integrations section [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
||||||
|
- [Access Tokens] Look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
|||||||
@ -463,8 +463,10 @@
|
|||||||
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
|
||||||
|
|
||||||
{:name "0145-fix-plugins-uri-on-profile"
|
{:name "0145-fix-plugins-uri-on-profile"
|
||||||
:fn mg0145/migrate}])
|
:fn mg0145/migrate}
|
||||||
|
|
||||||
|
{:name "0146-mod-access-token-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}])
|
||||||
|
|
||||||
(defn apply-migrations!
|
(defn apply-migrations!
|
||||||
[pool name migrations]
|
[pool name migrations]
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE access_token
|
||||||
|
ADD COLUMN type text NULL;
|
||||||
@ -23,7 +23,7 @@
|
|||||||
(dissoc row :perms))
|
(dissoc row :perms))
|
||||||
|
|
||||||
(defn create-access-token
|
(defn create-access-token
|
||||||
[{:keys [::db/conn] :as cfg} profile-id name expiration]
|
[{:keys [::db/conn] :as cfg} profile-id name expiration type]
|
||||||
(let [token-id (uuid/next)
|
(let [token-id (uuid/next)
|
||||||
expires-at (some-> expiration (ct/in-future))
|
expires-at (some-> expiration (ct/in-future))
|
||||||
created-at (ct/now)
|
created-at (ct/now)
|
||||||
@ -36,6 +36,7 @@
|
|||||||
{:id token-id
|
{:id token-id
|
||||||
:name name
|
:name name
|
||||||
:token token
|
:token token
|
||||||
|
:type type
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:created-at created-at
|
:created-at created-at
|
||||||
:updated-at created-at
|
:updated-at created-at
|
||||||
@ -50,17 +51,18 @@
|
|||||||
(def ^:private schema:create-access-token
|
(def ^:private schema:create-access-token
|
||||||
[:map {:title "create-access-token"}
|
[:map {:title "create-access-token"}
|
||||||
[:name [:string {:max 250 :min 1}]]
|
[:name [:string {:max 250 :min 1}]]
|
||||||
[:expiration {:optional true} ::ct/duration]])
|
[:expiration {:optional true} ::ct/duration]
|
||||||
|
[:type {:optional true} :string]])
|
||||||
|
|
||||||
(sv/defmethod ::create-access-token
|
(sv/defmethod ::create-access-token
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
::sm/params schema:create-access-token}
|
::sm/params schema:create-access-token}
|
||||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
[cfg {:keys [::rpc/profile-id name expiration type]}]
|
||||||
|
|
||||||
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||||
::quotes/profile-id profile-id})
|
::quotes/profile-id profile-id})
|
||||||
|
|
||||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
(db/tx-run! cfg create-access-token profile-id name expiration type))
|
||||||
|
|
||||||
(def ^:private schema:delete-access-token
|
(def ^:private schema:delete-access-token
|
||||||
[:map {:title "delete-access-token"}
|
[:map {:title "delete-access-token"}
|
||||||
@ -83,5 +85,22 @@
|
|||||||
(->> (db/query pool :access-token
|
(->> (db/query pool :access-token
|
||||||
{:profile-id profile-id}
|
{:profile-id profile-id}
|
||||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||||
:columns [:id :name :perms :created-at :updated-at :expires-at]})
|
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
|
||||||
(mapv decode-row)))
|
(mapv decode-row)))
|
||||||
|
|
||||||
|
(def ^:private schema:get-current-mcp-token
|
||||||
|
[:map {:title "get-current-mcp-token"}])
|
||||||
|
|
||||||
|
(sv/defmethod ::get-current-mcp-token
|
||||||
|
{::doc/added "2.15"
|
||||||
|
::sm/params schema:get-current-mcp-token}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
|
||||||
|
(->> (db/query pool :access-token
|
||||||
|
{:profile-id profile-id
|
||||||
|
:type "mcp"}
|
||||||
|
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||||
|
:columns [:token :expires-at]})
|
||||||
|
(remove #(and (some? (:expires-at %))
|
||||||
|
(ct/is-after? request-at (:expires-at %))))
|
||||||
|
(map decode-row)
|
||||||
|
(first)))
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
(def schema:props
|
(def schema:props
|
||||||
[:map {:title "ProfileProps"}
|
[:map {:title "ProfileProps"}
|
||||||
[:plugins {:optional true} schema:plugin-registry]
|
[:plugins {:optional true} schema:plugin-registry]
|
||||||
|
[:mcp-enabled {:optional true} ::sm/boolean]
|
||||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||||
[:newsletter-news {:optional true} ::sm/boolean]
|
[:newsletter-news {:optional true} ::sm/boolean]
|
||||||
[:onboarding-team-id {:optional true} ::sm/uuid]
|
[:onboarding-team-id {:optional true} ::sm/uuid]
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
(t/deftest access-token-authz
|
(t/deftest access-token-authz
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil nil)
|
||||||
handler (#'app.http.access-token/wrap-authz identity th/*system*)]
|
handler (#'app.http.access-token/wrap-authz identity th/*system*)]
|
||||||
|
|
||||||
(let [response (handler nil)]
|
(let [response (handler nil)]
|
||||||
|
|||||||
@ -107,4 +107,18 @@
|
|||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [results (:result out)]
|
(let [results (:result out)]
|
||||||
(t/is (= 2 (count results))))))))
|
(t/is (= 2 (count results))))))
|
||||||
|
|
||||||
|
(t/testing "get mcp token"
|
||||||
|
(let [_ (th/command! {::th/type :create-access-token
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:type "mcp"
|
||||||
|
:name "token 1"
|
||||||
|
:perms ["get-profile"]})
|
||||||
|
{:keys [error result]}
|
||||||
|
(th/command! {::th/type :get-current-mcp-token
|
||||||
|
::rpc/profile-id (:id prof)})]
|
||||||
|
;; (th/print-result! result)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (string? (:token result)))))))
|
||||||
|
|
||||||
|
|||||||
@ -152,7 +152,9 @@
|
|||||||
:redis-cache
|
:redis-cache
|
||||||
|
|
||||||
;; Activates the nitrate module
|
;; Activates the nitrate module
|
||||||
:nitrate})
|
:nitrate
|
||||||
|
|
||||||
|
:mcp})
|
||||||
|
|
||||||
(def all-flags
|
(def all-flags
|
||||||
(set/union email login varia))
|
(set/union email login varia))
|
||||||
|
|||||||
@ -13,3 +13,10 @@
|
|||||||
unit tests or backend code for logs or error messages."
|
unit tests or backend code for logs or error messages."
|
||||||
[key & _args]
|
[key & _args]
|
||||||
key)
|
key)
|
||||||
|
|
||||||
|
(defn c
|
||||||
|
"This function will be monkeypatched at runtime with the real function in frontend i18n.
|
||||||
|
Here it just returns the key passed as argument. This way the result can be used in
|
||||||
|
unit tests or backend code for logs or error messages."
|
||||||
|
[x]
|
||||||
|
x)
|
||||||
|
|||||||
105
common/src/app/common/schema/messages.cljc
Normal file
105
common/src/app/common/schema/messages.cljc
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.common.schema.messages
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.i18n :as i18n :refer [tr]]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[malli.core :as m]))
|
||||||
|
|
||||||
|
;; --- Handlers Helpers
|
||||||
|
|
||||||
|
(defn- translate-code
|
||||||
|
[code]
|
||||||
|
(if (vector? code)
|
||||||
|
(tr (nth code 0) (i18n/c (nth code 1)))
|
||||||
|
(tr code)))
|
||||||
|
|
||||||
|
(defn- handle-error-fn
|
||||||
|
[props problem]
|
||||||
|
(let [v-fn (:error/fn props)
|
||||||
|
result (v-fn problem)]
|
||||||
|
(if (string? result)
|
||||||
|
{:message result}
|
||||||
|
{:message (or (some-> (get result :code)
|
||||||
|
(translate-code))
|
||||||
|
(get result :message)
|
||||||
|
(tr "errors.invalid-data"))})))
|
||||||
|
|
||||||
|
(defn- handle-error-message
|
||||||
|
[props]
|
||||||
|
{:message (get props :error/message)})
|
||||||
|
|
||||||
|
(defn- handle-error-code
|
||||||
|
[props]
|
||||||
|
(let [code (get props :error/code)]
|
||||||
|
{:message (translate-code code)}))
|
||||||
|
|
||||||
|
(defn interpret-schema-problem
|
||||||
|
[acc {:keys [schema in value type] :as problem}]
|
||||||
|
(let [props (m/properties schema)
|
||||||
|
tprops (m/type-properties schema)
|
||||||
|
field (or (:error/field props)
|
||||||
|
in)
|
||||||
|
field (if (vector? field)
|
||||||
|
field
|
||||||
|
[field])]
|
||||||
|
|
||||||
|
(if (and (= 1 (count field))
|
||||||
|
(contains? acc (first field)))
|
||||||
|
acc
|
||||||
|
(cond
|
||||||
|
(or (nil? field)
|
||||||
|
(empty? field))
|
||||||
|
acc
|
||||||
|
|
||||||
|
(or (= type :malli.core/missing-key)
|
||||||
|
(nil? value))
|
||||||
|
(assoc-in acc field {:message (tr "errors.field-missing")})
|
||||||
|
|
||||||
|
;; --- CHECK on schema props
|
||||||
|
(contains? props :error/fn)
|
||||||
|
(assoc-in acc field (handle-error-fn props problem))
|
||||||
|
|
||||||
|
(contains? props :error/message)
|
||||||
|
(assoc-in acc field (handle-error-message props))
|
||||||
|
|
||||||
|
(contains? props :error/code)
|
||||||
|
(assoc-in acc field (handle-error-code props))
|
||||||
|
|
||||||
|
;; --- CHECK on type props
|
||||||
|
(contains? tprops :error/fn)
|
||||||
|
(assoc-in acc field (handle-error-fn tprops problem))
|
||||||
|
|
||||||
|
(contains? tprops :error/message)
|
||||||
|
(assoc-in acc field (handle-error-message tprops))
|
||||||
|
|
||||||
|
(contains? tprops :error/code)
|
||||||
|
(assoc-in acc field (handle-error-code tprops))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(assoc-in acc field {:message (tr "errors.invalid-data")})))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn- apply-validators
|
||||||
|
[validators state errors]
|
||||||
|
(reduce (fn [errors validator-fn]
|
||||||
|
(merge errors (validator-fn errors (:data state))))
|
||||||
|
errors
|
||||||
|
validators))
|
||||||
|
|
||||||
|
(defn collect-schema-errors
|
||||||
|
[schema validators state]
|
||||||
|
(let [explain (sm/explain schema (:data state))
|
||||||
|
errors (->> (reduce interpret-schema-problem {} (:errors explain))
|
||||||
|
(apply-validators validators state))]
|
||||||
|
|
||||||
|
(-> (:errors state)
|
||||||
|
(merge errors)
|
||||||
|
(d/without-nils)
|
||||||
|
(not-empty))))
|
||||||
@ -242,17 +242,19 @@
|
|||||||
(update-token- [this token-id f]
|
(update-token- [this token-id f]
|
||||||
(assert (uuid? token-id) "expected uuid for `token-id`")
|
(assert (uuid? token-id) "expected uuid for `token-id`")
|
||||||
(if-let [token (get-token- this token-id)]
|
(if-let [token (get-token- this token-id)]
|
||||||
(let [token' (-> (make-token (f token))
|
(let [token' (f token)]
|
||||||
(assoc :modified-at (ct/now)))]
|
(if (not= token token')
|
||||||
(TokenSet. id
|
(let [token' (assoc token' :modified-at (ct/now))]
|
||||||
name
|
(TokenSet. id
|
||||||
description
|
name
|
||||||
(ct/now)
|
description
|
||||||
(if (= (:name token) (:name token'))
|
(ct/now)
|
||||||
(assoc tokens (:name token') token')
|
(if (= (:name token) (:name token'))
|
||||||
(-> tokens
|
(assoc tokens (:name token') token')
|
||||||
(d/oassoc-before (:name token) (:name token') token')
|
(-> tokens
|
||||||
(dissoc (:name token))))))
|
(d/oassoc-before (:name token) (:name token') token')
|
||||||
|
(dissoc (:name token))))))
|
||||||
|
this))
|
||||||
this))
|
this))
|
||||||
|
|
||||||
(delete-token- [this token-id]
|
(delete-token- [this token-id]
|
||||||
@ -303,6 +305,35 @@
|
|||||||
(-clj->js [this]
|
(-clj->js [this]
|
||||||
(clj->js (datafy this)))))
|
(clj->js (datafy this)))))
|
||||||
|
|
||||||
|
(def ^:private set-prefix "S-")
|
||||||
|
|
||||||
|
(def ^:private set-group-prefix "G-")
|
||||||
|
|
||||||
|
(def ^:private set-separator "/")
|
||||||
|
|
||||||
|
(defn get-set-path
|
||||||
|
[token-set]
|
||||||
|
(cpn/split-path (get-name token-set) :separator set-separator))
|
||||||
|
|
||||||
|
(defn split-set-name
|
||||||
|
[name]
|
||||||
|
(cpn/split-path name :separator set-separator))
|
||||||
|
|
||||||
|
(defn join-set-path [path]
|
||||||
|
(cpn/join-path path :separator set-separator :with-spaces? false))
|
||||||
|
|
||||||
|
(defn normalize-set-name
|
||||||
|
"Normalize a set name (ensure that there are no extra spaces, like ' group / set' -> 'group/set').
|
||||||
|
|
||||||
|
If `relative-to` is provided, the normalized name will preserve the same group prefix as reference name."
|
||||||
|
([name]
|
||||||
|
(-> (split-set-name (str name))
|
||||||
|
(cpn/join-path :separator set-separator :with-spaces? false)))
|
||||||
|
([name relative-to]
|
||||||
|
(-> (concat (butlast (split-set-name relative-to))
|
||||||
|
(split-set-name (str name)))
|
||||||
|
(cpn/join-path :separator set-separator :with-spaces? false))))
|
||||||
|
|
||||||
(defn token-set?
|
(defn token-set?
|
||||||
[o]
|
[o]
|
||||||
(instance? TokenSet o))
|
(instance? TokenSet o))
|
||||||
@ -357,6 +388,7 @@
|
|||||||
(def check-token-set
|
(def check-token-set
|
||||||
(sm/check-fn schema:token-set :hint "expected valid token set"))
|
(sm/check-fn schema:token-set :hint "expected valid token set"))
|
||||||
|
|
||||||
|
|
||||||
(defn map->token-set
|
(defn map->token-set
|
||||||
[& {:as attrs}]
|
[& {:as attrs}]
|
||||||
(TokenSet. (:id attrs)
|
(TokenSet. (:id attrs)
|
||||||
@ -372,38 +404,10 @@
|
|||||||
(update :modified-at #(or % (ct/now)))
|
(update :modified-at #(or % (ct/now)))
|
||||||
(update :tokens #(into (d/ordered-map) %))
|
(update :tokens #(into (d/ordered-map) %))
|
||||||
(update :description d/nilv "")
|
(update :description d/nilv "")
|
||||||
|
(update :name normalize-set-name)
|
||||||
(check-token-set-attrs)
|
(check-token-set-attrs)
|
||||||
(map->token-set)))
|
(map->token-set)))
|
||||||
|
|
||||||
(def ^:private set-prefix "S-")
|
|
||||||
|
|
||||||
(def ^:private set-group-prefix "G-")
|
|
||||||
|
|
||||||
(def ^:private set-separator "/")
|
|
||||||
|
|
||||||
(defn get-set-path
|
|
||||||
[token-set]
|
|
||||||
(cpn/split-path (get-name token-set) :separator set-separator))
|
|
||||||
|
|
||||||
(defn split-set-name
|
|
||||||
[name]
|
|
||||||
(cpn/split-path name :separator set-separator))
|
|
||||||
|
|
||||||
(defn join-set-path [path]
|
|
||||||
(cpn/join-path path :separator set-separator :with-spaces? false))
|
|
||||||
|
|
||||||
(defn normalize-set-name
|
|
||||||
"Normalize a set name (ensure that there are no extra spaces, like ' group / set' -> 'group/set').
|
|
||||||
|
|
||||||
If `relative-to` is provided, the normalized name will preserve the same group prefix as reference name."
|
|
||||||
([name]
|
|
||||||
(-> (split-set-name name)
|
|
||||||
(cpn/join-path :separator set-separator :with-spaces? false)))
|
|
||||||
([name relative-to]
|
|
||||||
(-> (concat (butlast (split-set-name relative-to))
|
|
||||||
(split-set-name name))
|
|
||||||
(cpn/join-path :separator set-separator :with-spaces? false))))
|
|
||||||
|
|
||||||
(defn normalized-set-name?
|
(defn normalized-set-name?
|
||||||
"Check if a set name is normalized (no extra spaces)."
|
"Check if a set name is normalized (no extra spaces)."
|
||||||
[name]
|
[name]
|
||||||
@ -609,7 +613,7 @@
|
|||||||
is-source
|
is-source
|
||||||
external-id
|
external-id
|
||||||
(ct/now)
|
(ct/now)
|
||||||
set-names))
|
(into #{} (filter some?) set-names)))
|
||||||
|
|
||||||
(enable-set [this set-name]
|
(enable-set [this set-name]
|
||||||
(set-sets this (conj sets set-name)))
|
(set-sets this (conj sets set-name)))
|
||||||
@ -630,14 +634,9 @@
|
|||||||
|
|
||||||
(update-set-name [this prev-set-name set-name]
|
(update-set-name [this prev-set-name set-name]
|
||||||
(if (get sets prev-set-name)
|
(if (get sets prev-set-name)
|
||||||
(TokenTheme. id
|
(let [sets (-> (disj sets prev-set-name)
|
||||||
name
|
(conj set-name))]
|
||||||
group
|
(set-sets this sets))
|
||||||
description
|
|
||||||
is-source
|
|
||||||
external-id
|
|
||||||
(ct/now)
|
|
||||||
(conj (disj sets prev-set-name) set-name))
|
|
||||||
this))
|
this))
|
||||||
|
|
||||||
(theme-matches-group-name [this group name]
|
(theme-matches-group-name [this group name]
|
||||||
@ -722,7 +721,8 @@
|
|||||||
(update :is-source d/nilv false)
|
(update :is-source d/nilv false)
|
||||||
(update :external-id #(or % (str new-id)))
|
(update :external-id #(or % (str new-id)))
|
||||||
(update :modified-at #(or % (ct/now)))
|
(update :modified-at #(or % (ct/now)))
|
||||||
(update :sets set)
|
(update :sets #(into #{} (comp (filter some?)
|
||||||
|
(map normalize-set-name)) %))
|
||||||
(check-token-theme-attrs)
|
(check-token-theme-attrs)
|
||||||
(map->TokenTheme))))
|
(map->TokenTheme))))
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ localhost:3449 {
|
|||||||
header -Strict-Transport-Security
|
header -Strict-Transport-Security
|
||||||
}
|
}
|
||||||
|
|
||||||
http://localhost:3450 {
|
:3450 {
|
||||||
# For subpath test
|
# For subpath test
|
||||||
# handle_path /penpot/* {
|
# handle_path /penpot/* {
|
||||||
# reverse_proxy localhost:4449
|
# reverse_proxy localhost:4449
|
||||||
|
|||||||
@ -5,6 +5,9 @@ EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh;
|
|||||||
export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
|
export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
|
||||||
export CARGO_HOME="/home/penpot/.cargo"
|
export CARGO_HOME="/home/penpot/.cargo"
|
||||||
|
|
||||||
|
export PENPOT_MCP_PLUGIN_SERVER_HOST=0.0.0.0
|
||||||
|
export PENPOT_MCP_SERVER_HOST=0.0.0.0
|
||||||
|
|
||||||
alias l='ls --color -GFlh'
|
alias l='ls --color -GFlh'
|
||||||
alias ll='ls --color -GFlh'
|
alias ll='ls --color -GFlh'
|
||||||
alias rm='rm -rf'
|
alias rm='rm -rf'
|
||||||
|
|||||||
@ -126,6 +126,12 @@ http {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /plugins {
|
||||||
|
autoindex on;
|
||||||
|
alias /home/penpot/penpot/plugins/dist/apps;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
location /mcp/ws {
|
location /mcp/ws {
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
|
|||||||
@ -5,7 +5,8 @@ ENV LANG=en_US.UTF-8 \
|
|||||||
LC_ALL=en_US.UTF-8 \
|
LC_ALL=en_US.UTF-8 \
|
||||||
NODE_VERSION=v22.21.1 \
|
NODE_VERSION=v22.21.1 \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
PATH=/opt/node/bin:$PATH
|
PATH=/opt/node/bin:$PATH \
|
||||||
|
PENPOT_MCP_SERVER_HOST=0.0.0.0
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@penpot/draft-js": "workspace:./packages/draft-js",
|
"@penpot/draft-js": "workspace:./packages/draft-js",
|
||||||
"@penpot/mousetrap": "workspace:./packages/mousetrap",
|
"@penpot/mousetrap": "workspace:./packages/mousetrap",
|
||||||
"@penpot/plugins-runtime": "link:../plugins/dist/plugins-runtime",
|
"@penpot/plugins-runtime": "link:../plugins/libs/plugins-runtime",
|
||||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||||
"@penpot/text-editor": "workspace:./text-editor",
|
"@penpot/text-editor": "workspace:./text-editor",
|
||||||
"@penpot/tokenscript": "workspace:./packages/tokenscript",
|
"@penpot/tokenscript": "workspace:./packages/tokenscript",
|
||||||
|
|||||||
4
frontend/pnpm-lock.yaml
generated
4
frontend/pnpm-lock.yaml
generated
@ -20,8 +20,8 @@ importers:
|
|||||||
specifier: workspace:./packages/mousetrap
|
specifier: workspace:./packages/mousetrap
|
||||||
version: link:packages/mousetrap
|
version: link:packages/mousetrap
|
||||||
'@penpot/plugins-runtime':
|
'@penpot/plugins-runtime':
|
||||||
specifier: link:../plugins/dist/plugins-runtime
|
specifier: link:../plugins/libs/plugins-runtime
|
||||||
version: link:../plugins/dist/plugins-runtime
|
version: link:../plugins/libs/plugins-runtime
|
||||||
'@penpot/svgo':
|
'@penpot/svgo':
|
||||||
specifier: penpot/svgo#v3.2
|
specifier: penpot/svgo#v3.2
|
||||||
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
||||||
|
|||||||
@ -36,7 +36,7 @@ popd
|
|||||||
pushd ../mcp;
|
pushd ../mcp;
|
||||||
rm -rf node_modules;
|
rm -rf node_modules;
|
||||||
./scripts/setup
|
./scripts/setup
|
||||||
WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build:multi-user
|
WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build
|
||||||
popd;
|
popd;
|
||||||
|
|
||||||
pnpm run build:app:main $EXTRA_PARAMS;
|
pnpm run build:app:main $EXTRA_PARAMS;
|
||||||
|
|||||||
@ -172,6 +172,10 @@
|
|||||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||||
(obj/get location "origin"))))
|
(obj/get location "origin"))))
|
||||||
|
|
||||||
|
(def mcp-ws-uri
|
||||||
|
(or (some-> (obj/get global "penpotMcpServerURI") u/uri)
|
||||||
|
(u/join public-uri "mcp/ws")))
|
||||||
|
|
||||||
(def rasterizer-uri
|
(def rasterizer-uri
|
||||||
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
||||||
public-uri))
|
public-uri))
|
||||||
@ -205,6 +209,9 @@
|
|||||||
(let [f (obj/get global "initializeExternalConfigInfo")]
|
(let [f (obj/get global "initializeExternalConfigInfo")]
|
||||||
(when (fn? f) (f))))
|
(when (fn? f) (f))))
|
||||||
|
|
||||||
|
(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp/stream") str))
|
||||||
|
(def mcp-help-center-uri "https://help.penpot.app/mcp/")
|
||||||
|
|
||||||
;; --- Helper Functions
|
;; --- Helper Functions
|
||||||
|
|
||||||
(defn ^boolean check-browser? [candidate]
|
(defn ^boolean check-browser? [candidate]
|
||||||
|
|||||||
@ -57,5 +57,6 @@
|
|||||||
[type data]
|
[type data]
|
||||||
(ptk/reify ::event
|
(ptk/reify ::event
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ _ _]
|
(effect [_ state _]
|
||||||
(emit! type data))))
|
(let [session-id (get state :session-id)]
|
||||||
|
(emit! session-id type data)))))
|
||||||
|
|||||||
@ -7,12 +7,14 @@
|
|||||||
(ns app.main.data.plugins
|
(ns app.main.data.plugins
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.changes-builder :as pcb]
|
[app.common.files.changes-builder :as pcb]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.main.data.changes :as dch]
|
[app.main.data.changes :as dch]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
|
[app.main.errors :as errors]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.flags :as pflag]
|
[app.plugins.flags :as pflag]
|
||||||
[app.plugins.register :as preg]
|
[app.plugins.register :as preg]
|
||||||
@ -20,7 +22,8 @@
|
|||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(defn save-plugin-permissions-peek
|
(defn save-plugin-permissions-peek
|
||||||
[id permissions]
|
[id permissions]
|
||||||
@ -52,27 +55,47 @@
|
|||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||||
|
|
||||||
|
(defn start-plugin!
|
||||||
|
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
|
||||||
|
(-> (.ɵloadPlugin
|
||||||
|
^js ug/global
|
||||||
|
#js {:pluginId plugin-id
|
||||||
|
:name name
|
||||||
|
:version version
|
||||||
|
:description description
|
||||||
|
:host host
|
||||||
|
:code code
|
||||||
|
:allowBackground (boolean allow-background)
|
||||||
|
:permissions (apply array permissions)}
|
||||||
|
nil
|
||||||
|
extensions)
|
||||||
|
|
||||||
|
(p/catch (fn [cause]
|
||||||
|
(ex/print-throwable cause :prefix "Plugin Error")
|
||||||
|
(errors/flash :cause cause :type :handled)))))
|
||||||
|
|
||||||
(defn- load-plugin!
|
(defn- load-plugin!
|
||||||
[{:keys [plugin-id name description host code icon permissions]}]
|
[{:keys [plugin-id name version description host code icon permissions]}]
|
||||||
(try
|
(st/emit! (pflag/clear plugin-id)
|
||||||
(st/emit! (pflag/clear plugin-id)
|
(save-current-plugin plugin-id))
|
||||||
(save-current-plugin plugin-id))
|
|
||||||
|
|
||||||
(.ɵloadPlugin
|
(-> (.ɵloadPlugin
|
||||||
^js ug/global
|
^js ug/global
|
||||||
#js {:pluginId plugin-id
|
#js {:pluginId plugin-id
|
||||||
:name name
|
:name name
|
||||||
:description description
|
:description description
|
||||||
:host host
|
:version version
|
||||||
:code code
|
:host host
|
||||||
:icon icon
|
:code code
|
||||||
:permissions (apply array permissions)}
|
:icon icon
|
||||||
(fn []
|
:permissions (apply array permissions)}
|
||||||
(st/emit! (remove-current-plugin plugin-id))))
|
(fn []
|
||||||
|
(st/emit! (remove-current-plugin plugin-id))))
|
||||||
|
|
||||||
(catch :default e
|
(p/catch (fn [cause]
|
||||||
(st/emit! (remove-current-plugin plugin-id))
|
(st/emit! (remove-current-plugin plugin-id))
|
||||||
(.error js/console "Error" e))))
|
(ex/print-throwable cause :prefix "Plugin Error")
|
||||||
|
(errors/flash :cause cause :type :handled)))))
|
||||||
|
|
||||||
(defn open-plugin!
|
(defn open-plugin!
|
||||||
[{:keys [url] :as manifest} user-can-edit?]
|
[{:keys [url] :as manifest} user-can-edit?]
|
||||||
|
|||||||
@ -498,4 +498,3 @@
|
|||||||
(->> (rp/cmd! :delete-access-token params)
|
(->> (rp/cmd! :delete-access-token params)
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
[app.main.data.workspace.layers :as dwly]
|
[app.main.data.workspace.layers :as dwly]
|
||||||
[app.main.data.workspace.layout :as layout]
|
[app.main.data.workspace.layout :as layout]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
|
[app.main.data.workspace.mcp :as mcp]
|
||||||
[app.main.data.workspace.notifications :as dwn]
|
[app.main.data.workspace.notifications :as dwn]
|
||||||
[app.main.data.workspace.pages :as dwpg]
|
[app.main.data.workspace.pages :as dwpg]
|
||||||
[app.main.data.workspace.path :as dwdp]
|
[app.main.data.workspace.path :as dwdp]
|
||||||
@ -211,8 +212,11 @@
|
|||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(rx/of (dp/check-open-plugin)
|
(rx/merge
|
||||||
(fdf/fix-deleted-fonts-for-local-library file-id)))))
|
(rx/of (dp/check-open-plugin)
|
||||||
|
(fdf/fix-deleted-fonts-for-local-library file-id))
|
||||||
|
(when (contains? cf/flags :mcp)
|
||||||
|
(rx/of (mcp/init)))))))
|
||||||
|
|
||||||
(defn- bundle-fetched
|
(defn- bundle-fetched
|
||||||
[{:keys [file file-id thumbnails] :as bundle}]
|
[{:keys [file file-id thumbnails] :as bundle}]
|
||||||
@ -304,163 +308,169 @@
|
|||||||
:team-id (dm/str team-id)
|
:team-id (dm/str team-id)
|
||||||
:file-id (dm/str file-id))
|
:file-id (dm/str file-id))
|
||||||
|
|
||||||
(->> (rx/merge
|
(rx/concat
|
||||||
(rx/concat
|
(->> (rx/merge
|
||||||
;; Fetch all essential data that should be loaded before the file
|
(rx/concat
|
||||||
(rx/merge
|
;; Fetch all essential data that should be loaded before the file
|
||||||
(if ^boolean render-wasm?
|
(rx/merge
|
||||||
(->> (rx/from @wasm/module)
|
(if ^boolean render-wasm?
|
||||||
(rx/filter true?)
|
(->> (rx/from @wasm/module)
|
||||||
(rx/tap (fn [_]
|
(rx/filter true?)
|
||||||
(let [event (ug/event "penpot:wasm:loaded")]
|
(rx/tap (fn [_]
|
||||||
(ug/dispatch! event))))
|
(let [event (ug/event "penpot:wasm:loaded")]
|
||||||
(rx/ignore))
|
(ug/dispatch! event))))
|
||||||
(rx/empty))
|
(rx/ignore))
|
||||||
|
(rx/empty))
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/ignore))
|
(rx/ignore))
|
||||||
|
|
||||||
(rx/of (ntf/hide)
|
(rx/of (ntf/hide)
|
||||||
(dcmt/retrieve-comment-threads file-id)
|
(dcmt/retrieve-comment-threads file-id)
|
||||||
(dcmt/fetch-profiles)
|
(dcmt/fetch-profiles)
|
||||||
(df/fetch-fonts team-id)))
|
(df/fetch-fonts team-id))
|
||||||
|
|
||||||
;; Once the essential data is fetched, lets proceed to
|
(when (contains? cf/flags :mcp)
|
||||||
;; fetch teh file bunldle
|
(rx/of (du/fetch-access-tokens))))
|
||||||
(rx/of (fetch-bundle file-id features)))
|
|
||||||
|
|
||||||
(->> stream
|
;; Once the essential data is fetched, lets proceed to
|
||||||
(rx/filter (ptk/type? ::bundle-fetched))
|
;; fetch teh file bunldle
|
||||||
(rx/take 1)
|
(rx/of (fetch-bundle file-id features)))
|
||||||
(rx/map deref)
|
|
||||||
(rx/mapcat
|
|
||||||
(fn [{:keys [file]}]
|
|
||||||
(log/debug :hint "bundle fetched"
|
|
||||||
:team-id (dm/str team-id)
|
|
||||||
:file-id (dm/str file-id))
|
|
||||||
|
|
||||||
(rx/of (dpj/initialize-project (:project-id file))
|
(->> stream
|
||||||
(dwn/initialize team-id file-id)
|
(rx/filter (ptk/type? ::bundle-fetched))
|
||||||
(dwsl/initialize-shape-layout)
|
(rx/take 1)
|
||||||
(fetch-libraries file-id features)
|
(rx/map deref)
|
||||||
(-> (workspace-initialized file-id)
|
(rx/mapcat
|
||||||
(with-meta {:team-id team-id
|
(fn [{:keys [file]}]
|
||||||
:file-id file-id}))))))
|
(log/debug :hint "bundle fetched"
|
||||||
|
:team-id (dm/str team-id)
|
||||||
|
:file-id (dm/str file-id))
|
||||||
|
|
||||||
;; Install dev perf observers once the workspace is ready
|
(rx/of (dpj/initialize-project (:project-id file))
|
||||||
(when (contains? cf/flags :perf-logs)
|
(dwn/initialize team-id file-id)
|
||||||
(->> stream
|
(dwsl/initialize-shape-layout)
|
||||||
(rx/filter (ptk/type? ::workspace-initialized))
|
(fetch-libraries file-id features)
|
||||||
(rx/take 1)
|
(-> (workspace-initialized file-id)
|
||||||
(rx/tap (fn [_] (perf/setup)))))
|
(with-meta {:team-id team-id
|
||||||
|
:file-id file-id}))))))
|
||||||
|
|
||||||
(->> stream
|
;; Install dev perf observers once the workspace is ready
|
||||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
(when (contains? cf/flags :perf-logs)
|
||||||
(rx/take 1)
|
(->> stream
|
||||||
(rx/map dwc/set-workspace-visited))
|
(rx/filter (ptk/type? ::workspace-initialized))
|
||||||
|
(rx/take 1)
|
||||||
|
(rx/tap (fn [_] (perf/setup)))))
|
||||||
|
|
||||||
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
(->> stream
|
||||||
(->> stream
|
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||||
(rx/filter (ptk/type? ::workspace-initialized))
|
(rx/take 1)
|
||||||
(rx/observe-on :async)
|
(rx/map dwc/set-workspace-visited))
|
||||||
(rx/take 1)
|
|
||||||
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
|
||||||
|
|
||||||
(when (:board-id rparams)
|
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
(rx/filter (ptk/type? ::workspace-initialized))
|
||||||
(rx/take 1)
|
(rx/observe-on :async)
|
||||||
(rx/map zoom-to-frame)))
|
(rx/take 1)
|
||||||
|
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
||||||
|
|
||||||
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
(when (:board-id rparams)
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::workspace-initialized))
|
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||||
(rx/observe-on :async)
|
(rx/take 1)
|
||||||
(rx/take 1)
|
(rx/map zoom-to-frame)))
|
||||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
|
||||||
|
|
||||||
(when render-wasm?
|
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter dch/commit?)
|
(rx/filter (ptk/type? ::workspace-initialized))
|
||||||
(rx/map deref)
|
(rx/observe-on :async)
|
||||||
(rx/mapcat
|
(rx/take 1)
|
||||||
(fn [{:keys [redo-changes]}]
|
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||||
(let [added (->> redo-changes
|
|
||||||
(filter #(= (:type %) :add-obj))
|
|
||||||
(map :id))]
|
|
||||||
(->> (rx/from added)
|
|
||||||
(rx/map process-wasm-object)))))))
|
|
||||||
|
|
||||||
(when render-wasm?
|
(when render-wasm?
|
||||||
(let [local-commits-s
|
(->> stream
|
||||||
(->> stream
|
(rx/filter dch/commit?)
|
||||||
(rx/filter dch/commit?)
|
(rx/map deref)
|
||||||
(rx/map deref)
|
(rx/mapcat
|
||||||
(rx/filter #(and (= :local (:source %))
|
(fn [{:keys [redo-changes]}]
|
||||||
(not (contains? (:tags %) :position-data))))
|
(let [added (->> redo-changes
|
||||||
(rx/filter (complement empty?)))
|
(filter #(= (:type %) :add-obj))
|
||||||
|
(map :id))]
|
||||||
|
(->> (rx/from added)
|
||||||
|
(rx/map process-wasm-object)))))))
|
||||||
|
|
||||||
notifier-s
|
(when render-wasm?
|
||||||
(rx/merge
|
(let [local-commits-s
|
||||||
(->> local-commits-s (rx/debounce 1000))
|
(->> stream
|
||||||
(->> stream (rx/filter dps/force-persist?)))
|
(rx/filter dch/commit?)
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/filter #(and (= :local (:source %))
|
||||||
|
(not (contains? (:tags %) :position-data))))
|
||||||
|
(rx/filter (complement empty?)))
|
||||||
|
|
||||||
objects-s
|
notifier-s
|
||||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
(rx/merge
|
||||||
|
(->> local-commits-s (rx/debounce 1000))
|
||||||
|
(->> stream (rx/filter dps/force-persist?)))
|
||||||
|
|
||||||
current-page-id-s
|
objects-s
|
||||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||||
|
|
||||||
(->> local-commits-s
|
current-page-id-s
|
||||||
(rx/buffer-until notifier-s)
|
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||||
(rx/with-latest-from objects-s)
|
|
||||||
(rx/map
|
|
||||||
(fn [[commits objects]]
|
|
||||||
(->> commits
|
|
||||||
(mapcat :redo-changes)
|
|
||||||
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
|
||||||
(filter #(cfh/text-shape? objects (:id %)))
|
|
||||||
(map #(vector
|
|
||||||
(:id %)
|
|
||||||
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
|
||||||
|
|
||||||
(rx/with-latest-from current-page-id-s)
|
(->> local-commits-s
|
||||||
(rx/map
|
(rx/buffer-until notifier-s)
|
||||||
(fn [[text-position-data page-id]]
|
(rx/with-latest-from objects-s)
|
||||||
(let [changes
|
(rx/map
|
||||||
(->> text-position-data
|
(fn [[commits objects]]
|
||||||
(mapv (fn [[id position-data]]
|
(->> commits
|
||||||
{:type :mod-obj
|
(mapcat :redo-changes)
|
||||||
:id id
|
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||||
:page-id page-id
|
(filter #(cfh/text-shape? objects (:id %)))
|
||||||
:operations
|
(map #(vector
|
||||||
[{:type :set
|
(:id %)
|
||||||
:attr :position-data
|
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||||
:val position-data
|
|
||||||
:ignore-touched true
|
|
||||||
:ignore-geometry true}]})))]
|
|
||||||
(when (d/not-empty? changes)
|
|
||||||
(dch/commit-changes
|
|
||||||
{:redo-changes changes :undo-changes []
|
|
||||||
:save-undo? false
|
|
||||||
:tags #{:position-data}})))))
|
|
||||||
(rx/take-until stoper-s))))
|
|
||||||
|
|
||||||
(->> stream
|
(rx/with-latest-from current-page-id-s)
|
||||||
(rx/filter dch/commit?)
|
(rx/map
|
||||||
(rx/map deref)
|
(fn [[text-position-data page-id]]
|
||||||
(rx/mapcat
|
(let [changes
|
||||||
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
|
(->> text-position-data
|
||||||
(if (and save-undo? (seq undo-changes))
|
(mapv (fn [[id position-data]]
|
||||||
(let [entry {:undo-changes undo-changes
|
{:type :mod-obj
|
||||||
:redo-changes redo-changes
|
:id id
|
||||||
:undo-group undo-group
|
:page-id page-id
|
||||||
:tags tags}]
|
:operations
|
||||||
(rx/of (dwu/append-undo entry stack-undo?)))
|
[{:type :set
|
||||||
(rx/empty))))))
|
:attr :position-data
|
||||||
(rx/take-until stoper-s))))
|
:val position-data
|
||||||
|
:ignore-touched true
|
||||||
|
:ignore-geometry true}]})))]
|
||||||
|
(when (d/not-empty? changes)
|
||||||
|
(dch/commit-changes
|
||||||
|
{:redo-changes changes :undo-changes []
|
||||||
|
:save-undo? false
|
||||||
|
:tags #{:position-data}})))))
|
||||||
|
(rx/take-until stoper-s))))
|
||||||
|
|
||||||
|
(->> stream
|
||||||
|
(rx/filter dch/commit?)
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/mapcat
|
||||||
|
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
|
||||||
|
(if (and save-undo? (seq undo-changes))
|
||||||
|
(let [entry {:undo-changes undo-changes
|
||||||
|
:redo-changes redo-changes
|
||||||
|
:undo-group undo-group
|
||||||
|
:tags tags}]
|
||||||
|
(rx/of (dwu/append-undo entry stack-undo?)))
|
||||||
|
(rx/empty))))))
|
||||||
|
(rx/take-until stoper-s))
|
||||||
|
|
||||||
|
(rx/of (mcp/notify-other-tabs-disconnect)))))
|
||||||
|
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ _ _]
|
(effect [_ _ _]
|
||||||
|
|||||||
292
frontend/src/app/main/data/workspace/mcp.cljs
Normal file
292
frontend/src/app/main/data/workspace/mcp.cljs
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.data.workspace.mcp
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as log]
|
||||||
|
[app.common.uri :as u]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.main.broadcast :as mbc]
|
||||||
|
[app.main.data.event :as ev]
|
||||||
|
[app.main.data.notifications :as ntf]
|
||||||
|
[app.main.data.plugins :as dp]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.plugins.register :refer [mcp-plugin-id]]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[app.util.timers :as ts]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(def retry-interval 10000)
|
||||||
|
|
||||||
|
(log/set-level! :info)
|
||||||
|
|
||||||
|
(def ^:private default-manifest
|
||||||
|
{:code "plugin.js"
|
||||||
|
:name "Penpot MCP Plugin"
|
||||||
|
:version 2
|
||||||
|
:plugin-id mcp-plugin-id
|
||||||
|
:description "This plugin enables interaction with the Penpot MCP server"
|
||||||
|
:allow-background true
|
||||||
|
:permissions
|
||||||
|
#{"library:read" "library:write"
|
||||||
|
"comment:read" "comment:write"
|
||||||
|
"content:write" "content:read"}})
|
||||||
|
|
||||||
|
(defonce interval-sub (atom nil))
|
||||||
|
|
||||||
|
(defn finalize-workspace?
|
||||||
|
[event]
|
||||||
|
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
|
||||||
|
|
||||||
|
(defn set-mcp-active
|
||||||
|
[value]
|
||||||
|
(ptk/reify ::set-mcp-active
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(assoc-in state [:mcp :active] value))))
|
||||||
|
|
||||||
|
(defn start-reconnect-watcher!
|
||||||
|
[]
|
||||||
|
(st/emit! (set-mcp-active true))
|
||||||
|
(when (nil? @interval-sub)
|
||||||
|
(reset!
|
||||||
|
interval-sub
|
||||||
|
(ts/interval
|
||||||
|
retry-interval
|
||||||
|
(fn []
|
||||||
|
;; Try to reconnect if active and not connected
|
||||||
|
(when-not (contains? #{"connecting" "connected"}
|
||||||
|
(-> @st/state :mcp :connection-status))
|
||||||
|
(.log js/console "Reconnecting to MCP...")
|
||||||
|
(st/emit! (ptk/data-event ::connect))))))))
|
||||||
|
|
||||||
|
(defn stop-reconnect-watcher!
|
||||||
|
[]
|
||||||
|
(st/emit! (set-mcp-active false))
|
||||||
|
(when @interval-sub
|
||||||
|
(rx/dispose! @interval-sub)
|
||||||
|
(reset! interval-sub nil)))
|
||||||
|
|
||||||
|
(declare manage-mcp-notification)
|
||||||
|
|
||||||
|
(defn handle-pong
|
||||||
|
[{:keys [id data]}]
|
||||||
|
(ptk/reify ::handle-pong
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [mcp-state (get state :mcp)]
|
||||||
|
(cond
|
||||||
|
(= "connected" (:connection-status data))
|
||||||
|
(update state :mcp assoc :connected-tab id)
|
||||||
|
|
||||||
|
(and (= "disconnected" (:connection-status data))
|
||||||
|
(= id (:connection-status mcp-state)))
|
||||||
|
(update state :mcp dissoc :connected-tab)
|
||||||
|
|
||||||
|
:else
|
||||||
|
state)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of (manage-mcp-notification)))))
|
||||||
|
|
||||||
|
;; This event will arrive when a new workspace is open in another tab
|
||||||
|
(defn handle-ping
|
||||||
|
[]
|
||||||
|
(ptk/reify ::handle-ping
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [conn-status (get-in state [:mcp :connection-status])]
|
||||||
|
(rx/of (mbc/event :mcp/pong {:connection-status conn-status}))))))
|
||||||
|
|
||||||
|
(defn notify-other-tabs-disconnect
|
||||||
|
[]
|
||||||
|
(ptk/reify ::notify-other-tabs-disconnect
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of (mbc/event :mcp/pong {:connection-status "disconnected"})))))
|
||||||
|
|
||||||
|
;; This event will arrive when the mcp is enabled in the dashboard
|
||||||
|
(defn update-mcp-status
|
||||||
|
[value]
|
||||||
|
(ptk/reify ::update-mcp-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update-in state [:profile :props] assoc :mcp-enabled value))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/merge
|
||||||
|
(rx/of (manage-mcp-notification))
|
||||||
|
(case value
|
||||||
|
true (rx/of (ptk/data-event ::connect))
|
||||||
|
false (rx/of (ptk/data-event ::disconnect))
|
||||||
|
nil)))))
|
||||||
|
|
||||||
|
(defn update-mcp-connection-status
|
||||||
|
[value]
|
||||||
|
(ptk/reify ::update-mcp-plugin-connection
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :mcp assoc :connection-status value))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of (manage-mcp-notification)
|
||||||
|
(mbc/event :mcp/pong {:connection-status value})))))
|
||||||
|
|
||||||
|
(defn connect-mcp
|
||||||
|
[]
|
||||||
|
(ptk/reify ::connect-mcp
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :mcp assoc :connected-tab (:session-id state)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of (mbc/event :mcp/force-disconect {})
|
||||||
|
(ptk/data-event ::connect)))))
|
||||||
|
|
||||||
|
;; This event will arrive when the user selects disconnect on the menu
|
||||||
|
;; or there is a broadcast message for disconnection
|
||||||
|
(defn user-disconnect-mcp
|
||||||
|
[]
|
||||||
|
(ptk/reify ::user-disconnect-mcp
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of (ptk/data-event ::disconnect)
|
||||||
|
(update-mcp-connection-status "disconnected")))
|
||||||
|
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ _ _]
|
||||||
|
(stop-reconnect-watcher!))))
|
||||||
|
|
||||||
|
(defn- manage-mcp-notification
|
||||||
|
[]
|
||||||
|
(ptk/reify ::manage-mcp-notification
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [mcp-state (get state :mcp)
|
||||||
|
|
||||||
|
mcp-enabled? (-> state :profile :props :mcp-enabled)
|
||||||
|
|
||||||
|
current-tab-id (get state :session-id)
|
||||||
|
connected-tab-id (get mcp-state :connected-tab)]
|
||||||
|
|
||||||
|
(if mcp-enabled?
|
||||||
|
(if (= connected-tab-id current-tab-id)
|
||||||
|
(rx/of (ntf/hide))
|
||||||
|
(rx/of (ntf/dialog
|
||||||
|
{:content (tr "notifications.mcp.active-in-another-tab")
|
||||||
|
:cancel {:label (tr "labels.dismiss")
|
||||||
|
:callback #(st/emit! (ntf/hide)
|
||||||
|
(ev/event {::ev/name "confirm-mcp-tab-switch"
|
||||||
|
::ev/origin "workspace-notification"}))}
|
||||||
|
:accept {:label (tr "labels.switch")
|
||||||
|
:callback #(st/emit! (connect-mcp)
|
||||||
|
(ev/event {::ev/name "dismiss-mcp-tab-switch"
|
||||||
|
::ev/origin "workspace-notification"}))}})))
|
||||||
|
(rx/of (ntf/hide)))))))
|
||||||
|
|
||||||
|
(defn init-mcp
|
||||||
|
[stream]
|
||||||
|
(->> (rp/cmd! :get-current-mcp-token)
|
||||||
|
(rx/tap
|
||||||
|
(fn [{:keys [token]}]
|
||||||
|
(when token
|
||||||
|
(dp/start-plugin!
|
||||||
|
(assoc default-manifest
|
||||||
|
:url (str (u/join cf/public-uri "plugins/mcp/manifest.json"))
|
||||||
|
:host (str (u/join cf/public-uri "plugins/mcp/")))
|
||||||
|
|
||||||
|
;; API extension for MCP server
|
||||||
|
#js {:mcp
|
||||||
|
#js
|
||||||
|
{:getToken (constantly token)
|
||||||
|
:getServerUrl #(str cf/mcp-ws-uri)
|
||||||
|
:setMcpStatus
|
||||||
|
(fn [status]
|
||||||
|
(when (= status "connected")
|
||||||
|
(start-reconnect-watcher!))
|
||||||
|
(st/emit! (update-mcp-connection-status status))
|
||||||
|
(log/info :hint "MCP STATUS" :status status))
|
||||||
|
|
||||||
|
:on
|
||||||
|
(fn [event cb]
|
||||||
|
(when-let [event
|
||||||
|
(case event
|
||||||
|
"disconnect" ::disconnect
|
||||||
|
"connect" ::connect
|
||||||
|
nil)]
|
||||||
|
|
||||||
|
(let [stopper (rx/filter finalize-workspace? stream)]
|
||||||
|
(->> stream
|
||||||
|
(rx/filter (ptk/type? event))
|
||||||
|
(rx/take-until stopper)
|
||||||
|
(rx/subs! #(cb))))))}}))))
|
||||||
|
(rx/ignore)))
|
||||||
|
|
||||||
|
(defn init
|
||||||
|
[]
|
||||||
|
(ptk/reify ::init
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :mcp assoc :connected-tab (:session-id state) :active true))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [stoper-s (rx/merge
|
||||||
|
(rx/filter (ptk/type? :app.main.data.workspace/finalize-workspace) stream)
|
||||||
|
(rx/filter (ptk/type? ::init) stream))
|
||||||
|
session-id (get state :session-id)
|
||||||
|
enabled? (-> state :profile :props :mcp-enabled)]
|
||||||
|
|
||||||
|
(->> (rx/merge
|
||||||
|
(if enabled?
|
||||||
|
(rx/merge
|
||||||
|
(init-mcp stream)
|
||||||
|
|
||||||
|
(rx/of (mbc/event :mcp/ping {}))
|
||||||
|
|
||||||
|
(->> mbc/stream
|
||||||
|
(rx/filter (mbc/type? :mcp/ping))
|
||||||
|
(rx/filter (fn [{:keys [id]}]
|
||||||
|
(not= session-id id)))
|
||||||
|
(rx/map handle-ping))
|
||||||
|
|
||||||
|
(->> mbc/stream
|
||||||
|
(rx/filter (mbc/type? :mcp/pong))
|
||||||
|
(rx/filter (fn [{:keys [id]}]
|
||||||
|
(not= session-id id)))
|
||||||
|
(rx/map handle-pong))
|
||||||
|
|
||||||
|
(->> mbc/stream
|
||||||
|
(rx/filter (mbc/type? :mcp/force-disconect))
|
||||||
|
(rx/filter (fn [{:keys [id]}]
|
||||||
|
(not= session-id id)))
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/map (fn [] (user-disconnect-mcp)))))
|
||||||
|
(rx/empty))
|
||||||
|
|
||||||
|
(->> mbc/stream
|
||||||
|
(rx/filter (mbc/type? :mcp/enable))
|
||||||
|
(rx/mapcat (fn [_]
|
||||||
|
;; NOTE: we don't need an explicit
|
||||||
|
;; connect because the plugin has
|
||||||
|
;; auto-connect
|
||||||
|
(rx/of (update-mcp-status true)
|
||||||
|
(init)))))
|
||||||
|
|
||||||
|
(->> mbc/stream
|
||||||
|
(rx/filter (mbc/type? :mcp/disable))
|
||||||
|
(rx/mapcat (fn [_]
|
||||||
|
(rx/of (update-mcp-status false)
|
||||||
|
(init)
|
||||||
|
(user-disconnect-mcp))))))
|
||||||
|
|
||||||
|
(rx/take-until stoper-s))))))
|
||||||
@ -214,6 +214,7 @@
|
|||||||
(update state :workspace-presence dissoc session-id)
|
(update state :workspace-presence dissoc session-id)
|
||||||
(update state :workspace-presence update-presence))))))
|
(update state :workspace-presence update-presence))))))
|
||||||
|
|
||||||
|
|
||||||
(defn handle-pointer-update
|
(defn handle-pointer-update
|
||||||
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]
|
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]
|
||||||
(ptk/reify ::handle-pointer-update
|
(ptk/reify ::handle-pointer-update
|
||||||
|
|||||||
@ -49,14 +49,14 @@
|
|||||||
|
|
||||||
;; (note that dwsh/update-shapes function returns an event)
|
;; (note that dwsh/update-shapes function returns an event)
|
||||||
|
|
||||||
(defn update-shape-radius-all
|
(defn update-shape-radius
|
||||||
([value shape-ids attributes] (update-shape-radius-all value shape-ids attributes nil))
|
([value shape-ids attributes] (update-shape-radius value shape-ids attributes nil))
|
||||||
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
|
([value shape-ids attributes page-id]
|
||||||
(when (number? value)
|
(when (number? value)
|
||||||
(let [value (max 0 value)]
|
(let [value (max 0 value)]
|
||||||
(dwsh/update-shapes shape-ids
|
(dwsh/update-shapes shape-ids
|
||||||
(fn [shape]
|
(fn [shape]
|
||||||
(ctsr/set-radius-to-all-corners shape value))
|
(ctsr/set-radius-for-corners shape attributes value))
|
||||||
{:reg-objects? true
|
{:reg-objects? true
|
||||||
:ignore-touched true
|
:ignore-touched true
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
@ -531,7 +531,7 @@
|
|||||||
|
|
||||||
(some attributes #{:r1 :r2 :r3 :r4})
|
(some attributes #{:r1 :r2 :r3 :r4})
|
||||||
(conj #(if (= attributes #{:r1 :r2 :r3 :r4})
|
(conj #(if (= attributes #{:r1 :r2 :r3 :r4})
|
||||||
(update-shape-radius-all value shape-ids attributes page-id)
|
(update-shape-radius value shape-ids attributes page-id)
|
||||||
(update-shape-radius-for-corners
|
(update-shape-radius-for-corners
|
||||||
value shape-ids
|
value shape-ids
|
||||||
(set (filter attributes #{:r1 :r2 :r3 :r4}))
|
(set (filter attributes #{:r1 :r2 :r3 :r4}))
|
||||||
@ -607,6 +607,46 @@
|
|||||||
:state state})]
|
:state state})]
|
||||||
(apply rx/of (map #(%) actions)))))))))
|
(apply rx/of (map #(%) actions)))))))))
|
||||||
|
|
||||||
|
(def attributes->shape-update
|
||||||
|
"Maps each attribute-set to the update function that applies it to a shape.
|
||||||
|
Used both here (to resolve the correct update fn when explicit attrs are
|
||||||
|
passed to toggle-token) and in propagation.cljs (re-exported from there)."
|
||||||
|
{ctt/border-radius-keys update-shape-radius-for-corners
|
||||||
|
ctt/color-keys update-fill-stroke
|
||||||
|
ctt/stroke-width-keys update-stroke-width
|
||||||
|
ctt/sizing-keys apply-dimensions-token
|
||||||
|
ctt/opacity-keys update-opacity
|
||||||
|
ctt/rotation-keys update-rotation
|
||||||
|
|
||||||
|
;; Typography
|
||||||
|
ctt/font-family-keys update-font-family
|
||||||
|
ctt/font-size-keys update-font-size
|
||||||
|
ctt/font-weight-keys update-font-weight
|
||||||
|
ctt/letter-spacing-keys update-letter-spacing
|
||||||
|
ctt/text-case-keys update-text-case
|
||||||
|
ctt/text-decoration-keys update-text-decoration
|
||||||
|
ctt/typography-token-keys update-typography
|
||||||
|
ctt/shadow-keys update-shadow
|
||||||
|
ctt/line-height-keys update-line-height
|
||||||
|
|
||||||
|
;; Layout
|
||||||
|
#{:x :y} update-shape-position
|
||||||
|
#{:p1 :p2 :p3 :p4} update-layout-padding
|
||||||
|
#{:m1 :m2 :m3 :m4} update-layout-item-margin
|
||||||
|
#{:column-gap :row-gap} update-layout-gap
|
||||||
|
#{:width :height} apply-dimensions-token
|
||||||
|
#{:layout-item-min-w :layout-item-min-h
|
||||||
|
:layout-item-max-w :layout-item-max-h} update-layout-sizing-limits})
|
||||||
|
|
||||||
|
;; Flattened per-individual-key version of attributes->shape-update.
|
||||||
|
;; Allows O(1) lookup of the update function for any single attribute.
|
||||||
|
(def ^:private attr->shape-update
|
||||||
|
(reduce
|
||||||
|
(fn [acc [attr-set update-fn]]
|
||||||
|
(into acc (map (fn [k] [k update-fn]) attr-set)))
|
||||||
|
{}
|
||||||
|
attributes->shape-update))
|
||||||
|
|
||||||
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
||||||
|
|
||||||
(defn apply-token
|
(defn apply-token
|
||||||
@ -620,65 +660,73 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
;; We do not allow to apply tokens while text editor is open.
|
;; We do not allow to apply tokens while text editor is open.
|
||||||
(if (empty? (get state :workspace-editor-state))
|
;; The classic text editor sets :workspace-editor-state; the WASM text editor
|
||||||
(let [attributes-to-remove
|
;; does not, so we also check :workspace-local :edition for text shapes.
|
||||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
(let [edition (get-in state [:workspace-local :edition])
|
||||||
(cond
|
objects (dsh/lookup-page-objects state)
|
||||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
text-editing? (and (some? edition)
|
||||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
(= :text (:type (get objects edition))))]
|
||||||
:else attributes-to-remove)]
|
(if (and (empty? (get state :workspace-editor-state))
|
||||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
(not text-editing?))
|
||||||
(get :tokens-lib)
|
(let [attributes-to-remove
|
||||||
(ctob/get-tokens-in-active-sets))]
|
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||||
(->> (if (contains? cf/flags :tokenscript)
|
(cond
|
||||||
(rx/of (ts/resolve-tokens tokens))
|
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||||
(sd/resolve-tokens tokens))
|
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||||
(rx/mapcat
|
:else attributes-to-remove)]
|
||||||
(fn [resolved-tokens]
|
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||||
(let [undo-id (js/Symbol)
|
(get :tokens-lib)
|
||||||
objects (dsh/lookup-page-objects state)
|
(ctob/get-tokens-in-active-sets))]
|
||||||
selected-shapes (select-keys objects shape-ids)
|
(->> (if (contains? cf/flags :tokenscript)
|
||||||
|
(rx/of (ts/resolve-tokens tokens))
|
||||||
|
(sd/resolve-tokens tokens))
|
||||||
|
(rx/mapcat
|
||||||
|
(fn [resolved-tokens]
|
||||||
|
(let [undo-id (js/Symbol)
|
||||||
|
objects (dsh/lookup-page-objects state)
|
||||||
|
selected-shapes (select-keys objects shape-ids)
|
||||||
|
|
||||||
shapes (->> selected-shapes
|
shapes (->> selected-shapes
|
||||||
(filter (fn [[_ shape]]
|
(filter (fn [[_ shape]]
|
||||||
(or
|
(or
|
||||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||||
(some ctt/spacing-margin-keys attributes))
|
(some ctt/spacing-margin-keys attributes))
|
||||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||||
shape-ids (d/nilv (keys shapes) [])
|
shape-ids (d/nilv (keys shapes) [])
|
||||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||||
|
|
||||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||||
resolved-value (if (contains? cf/flags :tokenscript)
|
resolved-value (if (contains? cf/flags :tokenscript)
|
||||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||||
resolved-value)
|
resolved-value)
|
||||||
tokenized-attributes (cfo/attributes-map attributes token)
|
tokenized-attributes (cfo/attributes-map attributes token)
|
||||||
type (:type token)]
|
type (:type token)]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of
|
(rx/of
|
||||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||||
:type type
|
:type type
|
||||||
:applied-to attributes
|
:applied-to attributes
|
||||||
:applied-to-variant any-variant?}))
|
:applied-to-variant any-variant?}))
|
||||||
(dwu/start-undo-transaction undo-id)
|
(dwu/start-undo-transaction undo-id)
|
||||||
(dwsh/update-shapes shape-ids (fn [shape]
|
(dwsh/update-shapes shape-ids (fn [shape]
|
||||||
(cond-> shape
|
(cond-> shape
|
||||||
attributes-to-remove
|
attributes-to-remove
|
||||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||||
:always
|
:always
|
||||||
(update :applied-tokens merge tokenized-attributes)))))
|
(update :applied-tokens merge tokenized-attributes)))))
|
||||||
(when on-update-shape
|
(when on-update-shape
|
||||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||||
;; Composed updates return observables and need to be executed differently
|
;; Composed updates return observables and need to be executed differently
|
||||||
(if (rx/observable? res)
|
(if (rx/observable? res)
|
||||||
res
|
res
|
||||||
(rx/of res))))
|
(rx/of res))))
|
||||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))
|
(rx/of (dwu/commit-undo-transaction undo-id)))))))))
|
||||||
(rx/of (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
|
||||||
:type :toast
|
(rx/of (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
||||||
:level :warning
|
:type :toast
|
||||||
:timeout 3000}))))))
|
:level :warning
|
||||||
|
:timeout 3000})))))))
|
||||||
|
|
||||||
(defn apply-spacing-token-separated
|
(defn apply-spacing-token-separated
|
||||||
"Handles edge-case for spacing token when applying token via toggle button.
|
"Handles edge-case for spacing token when applying token via toggle button.
|
||||||
@ -744,10 +792,16 @@
|
|||||||
{:keys [attributes all-attributes on-update-shape]}
|
{:keys [attributes all-attributes on-update-shape]}
|
||||||
(get token-properties (:type token))
|
(get token-properties (:type token))
|
||||||
|
|
||||||
|
on-update-shape
|
||||||
|
(if (seq attrs)
|
||||||
|
(or (get attr->shape-update (first attrs)) on-update-shape)
|
||||||
|
on-update-shape)
|
||||||
|
|
||||||
unapply-tokens?
|
unapply-tokens?
|
||||||
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||||
|
|
||||||
shape-ids (map :id shapes)]
|
shape-ids
|
||||||
|
(map :id shapes)]
|
||||||
|
|
||||||
(if unapply-tokens?
|
(if unapply-tokens?
|
||||||
(rx/of
|
(rx/of
|
||||||
@ -808,7 +862,7 @@
|
|||||||
:border-radius
|
:border-radius
|
||||||
{:title "Border Radius"
|
{:title "Border Radius"
|
||||||
:attributes ctt/border-radius-keys
|
:attributes ctt/border-radius-keys
|
||||||
:on-update-shape update-shape-radius-all
|
:on-update-shape update-shape-radius
|
||||||
:modal {:key :tokens/border-radius
|
:modal {:key :tokens/border-radius
|
||||||
:fields [{:label "Border Radius"
|
:fields [{:label "Border Radius"
|
||||||
:key :border-radius}]}}
|
:key :border-radius}]}}
|
||||||
|
|||||||
@ -613,7 +613,7 @@
|
|||||||
vec))
|
vec))
|
||||||
|
|
||||||
(defn combine-as-variants
|
(defn combine-as-variants
|
||||||
[ids {:keys [page-id trigger]}]
|
[ids {:keys [page-id trigger variant-id]}]
|
||||||
(ptk/reify ::combine-as-variants
|
(ptk/reify ::combine-as-variants
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
@ -647,7 +647,7 @@
|
|||||||
:shapes
|
:shapes
|
||||||
count
|
count
|
||||||
inc)
|
inc)
|
||||||
variant-id (uuid/next)
|
variant-id (or variant-id (uuid/next))
|
||||||
undo-id (js/Symbol)]
|
undo-id (js/Symbol)]
|
||||||
|
|
||||||
(rx/concat
|
(rx/concat
|
||||||
|
|||||||
@ -33,6 +33,16 @@
|
|||||||
;; Will contain last uncaught exception
|
;; Will contain last uncaught exception
|
||||||
(def last-exception nil)
|
(def last-exception nil)
|
||||||
|
|
||||||
|
(defn is-plugin-error?
|
||||||
|
"This is a placeholder that always return false. It will be
|
||||||
|
overwritten when plugin system is initialized. This works this way
|
||||||
|
because we can't import plugins here because plugins requries full
|
||||||
|
DOM.
|
||||||
|
|
||||||
|
This placeholder is set on app.plugins/initialize event"
|
||||||
|
[_]
|
||||||
|
false)
|
||||||
|
|
||||||
;; Re-entrancy guard: prevents on-error from calling itself recursively.
|
;; Re-entrancy guard: prevents on-error from calling itself recursively.
|
||||||
;; If an error occurs while we are already handling an error (e.g. the
|
;; If an error occurs while we are already handling an error (e.g. the
|
||||||
;; notification emit itself throws), we log it and bail out immediately
|
;; notification emit itself throws), we log it and bail out immediately
|
||||||
@ -206,6 +216,16 @@
|
|||||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||||
(flash :cause cause :type :unhandled))))
|
(flash :cause cause :type :unhandled))))
|
||||||
|
|
||||||
|
(defmethod ptk/handle-error :wasm-error
|
||||||
|
[error]
|
||||||
|
(when-let [cause (::instance error)]
|
||||||
|
(ex/print-throwable cause)
|
||||||
|
(let [code (get error :code)]
|
||||||
|
(if (or (= code :panic)
|
||||||
|
(= code :webgl-context-lost))
|
||||||
|
(st/emit! (rt/assign-exception error))
|
||||||
|
(flash :type :handled :cause cause)))))
|
||||||
|
|
||||||
;; We receive a explicit authentication error; If the uri is for
|
;; We receive a explicit authentication error; If the uri is for
|
||||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||||
;; for show the error page. Otherwise this explicitly clears all
|
;; for show the error page. Otherwise this explicitly clears all
|
||||||
@ -420,6 +440,15 @@
|
|||||||
(and (string? stack)
|
(and (string? stack)
|
||||||
(str/includes? stack "posthog"))))
|
(str/includes? stack "posthog"))))
|
||||||
|
|
||||||
|
;; Check if the error is marked as originating from plugin code.
|
||||||
|
;; The plugin runtime tracks plugin errors in a WeakMap, which works
|
||||||
|
;; even in SES hardened environments where error objects may be frozen.
|
||||||
|
(from-plugin? [cause]
|
||||||
|
(try
|
||||||
|
(is-plugin-error? cause)
|
||||||
|
(catch :default _
|
||||||
|
false)))
|
||||||
|
|
||||||
(is-ignorable-exception? [cause]
|
(is-ignorable-exception? [cause]
|
||||||
(let [message (ex-message cause)]
|
(let [message (ex-message cause)]
|
||||||
(or (from-extension? cause)
|
(or (from-extension? cause)
|
||||||
@ -447,32 +476,55 @@
|
|||||||
(on-unhandled-error [event]
|
(on-unhandled-error [event]
|
||||||
(.preventDefault ^js event)
|
(.preventDefault ^js event)
|
||||||
(when-let [cause (unchecked-get event "error")]
|
(when-let [cause (unchecked-get event "error")]
|
||||||
(when-not (is-ignorable-exception? cause)
|
(cond
|
||||||
(if (stale-asset-error? cause)
|
(stale-asset-error? cause)
|
||||||
(cf/throttled-reload :reason (ex-message cause))
|
(cf/throttled-reload :reason (ex-message cause))
|
||||||
(let [data (ex-data cause)
|
|
||||||
type (get data :type)]
|
;; Plugin errors: log to console and ignore
|
||||||
(set! last-exception cause)
|
(from-plugin? cause)
|
||||||
(if (= :wasm-error type)
|
(ex/print-throwable cause :prefix "Plugin Error")
|
||||||
(on-error cause)
|
|
||||||
(do
|
;; Other ignorable exceptions: ignore silently
|
||||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
(is-ignorable-exception? cause)
|
||||||
(ts/asap #(flash :cause cause :type :unhandled)))))))))
|
nil
|
||||||
|
|
||||||
|
;; All other errors: show exception page
|
||||||
|
:else
|
||||||
|
|
||||||
|
(let [data (ex-data cause)
|
||||||
|
type (get data :type)]
|
||||||
|
(set! last-exception cause)
|
||||||
|
(if (= :wasm-error type)
|
||||||
|
(on-error cause)
|
||||||
|
(do
|
||||||
|
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||||
|
(ts/asap #(flash :cause cause :type :unhandled))))))))
|
||||||
|
|
||||||
(on-unhandled-rejection [event]
|
(on-unhandled-rejection [event]
|
||||||
(.preventDefault ^js event)
|
(.preventDefault ^js event)
|
||||||
(when-let [cause (unchecked-get event "reason")]
|
(when-let [cause (unchecked-get event "reason")]
|
||||||
(when-not (is-ignorable-exception? cause)
|
(cond
|
||||||
(if (stale-asset-error? cause)
|
(stale-asset-error? cause)
|
||||||
(cf/throttled-reload :reason (ex-message cause))
|
(cf/throttled-reload :reason (ex-message cause))
|
||||||
(let [data (ex-data cause)
|
|
||||||
type (get data :type)]
|
;; Plugin errors: log to console and ignore
|
||||||
(set! last-exception cause)
|
(from-plugin? cause)
|
||||||
(if (= :wasm-error type)
|
(ex/print-throwable cause :prefix "Plugin Error")
|
||||||
(on-error cause)
|
|
||||||
(do
|
;; Other ignorable exceptions: ignore silently
|
||||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
(is-ignorable-exception? cause)
|
||||||
(ts/asap #(flash :cause cause :type :unhandled)))))))))]
|
nil
|
||||||
|
|
||||||
|
;; All other errors: show exception page
|
||||||
|
:else
|
||||||
|
(let [data (ex-data cause)
|
||||||
|
type (get data :type)]
|
||||||
|
(set! last-exception cause)
|
||||||
|
(if (= :wasm-error type)
|
||||||
|
(on-error cause)
|
||||||
|
(do
|
||||||
|
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||||
|
(ts/asap #(flash :cause cause :type :unhandled))))))))]
|
||||||
|
|
||||||
(.addEventListener g/window "error" on-unhandled-error)
|
(.addEventListener g/window "error" on-unhandled-error)
|
||||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||||
|
|||||||
@ -150,6 +150,9 @@
|
|||||||
(def workspace-global
|
(def workspace-global
|
||||||
(l/derived :workspace-global st/state))
|
(l/derived :workspace-global st/state))
|
||||||
|
|
||||||
|
(def mcp
|
||||||
|
(l/derived :mcp st/state))
|
||||||
|
|
||||||
(def workspace-drawing
|
(def workspace-drawing
|
||||||
(l/derived :workspace-drawing st/state))
|
(l/derived :workspace-drawing st/state))
|
||||||
|
|
||||||
|
|||||||
@ -190,7 +190,7 @@
|
|||||||
:settings-options
|
:settings-options
|
||||||
:settings-feedback
|
:settings-feedback
|
||||||
:settings-subscription
|
:settings-subscription
|
||||||
:settings-access-tokens
|
:settings-integrations
|
||||||
:settings-notifications)
|
:settings-notifications)
|
||||||
(let [params (get params :query)
|
(let [params (get params :query)
|
||||||
error-report-id (some-> params :error-report-id uuid/parse*)]
|
error-report-id (some-> params :error-report-id uuid/parse*)]
|
||||||
|
|||||||
@ -9,14 +9,17 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.keyboard :as k]
|
[app.util.keyboard :as k]
|
||||||
[goog.events :as events]
|
[goog.events :as events]
|
||||||
[rumext.v2 :as mf])
|
[rumext.v2 :as mf])
|
||||||
(:import goog.events.EventType))
|
(:import
|
||||||
|
goog.events.EventType))
|
||||||
|
|
||||||
(mf/defc confirm-dialog
|
(mf/defc confirm-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
@ -68,8 +71,11 @@
|
|||||||
[:div {:class (stl/css :modal-container)}
|
[:div {:class (stl/css :modal-container)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h2 {:class (stl/css :modal-title)} title]
|
[:h2 {:class (stl/css :modal-title)} title]
|
||||||
[:button {:class (stl/css :modal-close-btn)
|
[:div {:class (stl/css :modal-close-btn)}
|
||||||
:on-click cancel-fn} deprecated-icon/close]]
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click cancel-fn
|
||||||
|
:icon i/close}]]]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
(when (and (string? message) (not= message ""))
|
(when (and (string? message) (not= message ""))
|
||||||
@ -87,24 +93,19 @@
|
|||||||
[:ul {:class (stl/css :component-list)}
|
[:ul {:class (stl/css :component-list)}
|
||||||
(for [item items]
|
(for [item items]
|
||||||
[:li {:class (stl/css :modal-item-element)}
|
[:li {:class (stl/css :modal-item-element)}
|
||||||
[:span {:class (stl/css :modal-component-icon)}
|
[:> icon* {:icon-id i/component
|
||||||
deprecated-icon/component]
|
:class (stl/css :modal-component-icon)
|
||||||
|
:size "s"}]
|
||||||
[:span {:class (stl/css :modal-component-name)}
|
[:span {:class (stl/css :modal-component-name)}
|
||||||
(:name item)]])]])]
|
(:name item)]])]])]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
[:div {:class (stl/css :modal-footer)}
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
(when-not (= cancel-label :omit)
|
(when-not (= cancel-label :omit)
|
||||||
[:input
|
[:> button* {:variant "secondary"
|
||||||
{:class (stl/css :cancel-button)
|
:on-click cancel-fn}
|
||||||
:type "button"
|
cancel-label])
|
||||||
:value cancel-label
|
[:> button* {:variant (cond (= accept-style :danger) "destructive"
|
||||||
:on-click cancel-fn}])
|
(= accept-style :primary) "primary")
|
||||||
|
:on-click accept-fn}
|
||||||
[:input
|
accept-label]]]]]))
|
||||||
{:class (stl/css-case :accept-btn true
|
|
||||||
:danger (= accept-style :danger)
|
|
||||||
:primary (= accept-style :primary))
|
|
||||||
:type "button"
|
|
||||||
:value accept-label
|
|
||||||
:on-click accept-fn}]]]]]))
|
|
||||||
|
|||||||
@ -15,10 +15,9 @@
|
|||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
@extend .modal-container-base;
|
@extend .modal-container-base;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.modal-header {
|
gap: var(--sp-xxl);
|
||||||
margin-bottom: deprecated.$s-24;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
@ -27,12 +26,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-btn {
|
.modal-close-btn {
|
||||||
@extend .modal-close-btn-base;
|
position: absolute;
|
||||||
|
top: var(--sp-m);
|
||||||
|
right: var(--sp-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@include deprecated.bodyLargeTypography;
|
@include deprecated.bodyLargeTypography;
|
||||||
margin-bottom: deprecated.$s-24;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-item-element {
|
.modal-item-element {
|
||||||
@ -41,32 +41,18 @@
|
|||||||
|
|
||||||
.modal-component-icon {
|
.modal-component-icon {
|
||||||
@include deprecated.flexCenter;
|
@include deprecated.flexCenter;
|
||||||
height: deprecated.$s-16;
|
color: var(--color-foreground-secondary);
|
||||||
width: deprecated.$s-16;
|
|
||||||
svg {
|
|
||||||
@extend .button-icon-small;
|
|
||||||
stroke: var(--color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-component-name {
|
.modal-component-name {
|
||||||
@include deprecated.bodyLargeTypography;
|
@include deprecated.bodyLargeTypography;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@extend .modal-action-btns;
|
@extend .modal-action-btns;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-button {
|
|
||||||
@extend .modal-cancel-btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accept-btn {
|
|
||||||
@extend .modal-accept-btn;
|
|
||||||
&.danger {
|
|
||||||
@extend .modal-danger-btn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-scd-msg,
|
.modal-scd-msg,
|
||||||
.modal-subtitle,
|
.modal-subtitle,
|
||||||
.modal-msg {
|
.modal-msg {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ $sz-32: px2rem(32);
|
|||||||
$sz-36: px2rem(36);
|
$sz-36: px2rem(36);
|
||||||
$sz-40: px2rem(40);
|
$sz-40: px2rem(40);
|
||||||
$sz-48: px2rem(48);
|
$sz-48: px2rem(48);
|
||||||
|
$sz-64: px2rem(64);
|
||||||
$sz-88: px2rem(88);
|
$sz-88: px2rem(88);
|
||||||
$sz-96: px2rem(96);
|
$sz-96: px2rem(96);
|
||||||
$sz-120: px2rem(120);
|
$sz-120: px2rem(120);
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.main.constants :refer [max-input-length]]
|
[app.main.constants :refer [max-input-length]]
|
||||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||||
@ -52,10 +51,11 @@
|
|||||||
:has-hint has-hint
|
:has-hint has-hint
|
||||||
:hint-type hint-type
|
:hint-type hint-type
|
||||||
:variant variant})]
|
:variant variant})]
|
||||||
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
|
|
||||||
:variant-dense (= variant "dense")
|
[:div {:class [class (stl/css-case :input-wrapper true
|
||||||
:variant-comfortable (= variant "comfortable")
|
:variant-dense (= variant "dense")
|
||||||
:has-hint has-hint))}
|
:variant-comfortable (= variant "comfortable")
|
||||||
|
:has-hint has-hint)]}
|
||||||
(when has-label
|
(when has-label
|
||||||
[:> label* {:for id :is-optional is-optional} label])
|
[:> label* {:for id :is-optional is-optional} label])
|
||||||
[:> input-field* props]
|
[:> input-field* props]
|
||||||
@ -64,4 +64,3 @@
|
|||||||
:class hint-class
|
:class hint-class
|
||||||
:message hint-message
|
:message hint-message
|
||||||
:type hint-type}])]))
|
:type hint-type}])]))
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
[app.main.ui.ds.controls.input :refer [input*]]
|
[app.main.ui.ds.controls.input :refer [input*]]
|
||||||
|
[app.main.ui.ds.controls.select :refer [select*]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
[app.util.keyboard :as k]
|
[app.util.keyboard :as k]
|
||||||
@ -47,6 +48,23 @@
|
|||||||
|
|
||||||
[:> input* props]))
|
[:> input* props]))
|
||||||
|
|
||||||
|
(mf/defc form-select*
|
||||||
|
[{:keys [name] :as props}]
|
||||||
|
(let [select-name name
|
||||||
|
form (mf/use-ctx context)
|
||||||
|
value (get-in @form [:data select-name] "")
|
||||||
|
|
||||||
|
handle-change
|
||||||
|
(fn [event]
|
||||||
|
(let [value (if (string? event) event (dom/get-target-val event))]
|
||||||
|
(fm/on-input-change form select-name value)))
|
||||||
|
|
||||||
|
props
|
||||||
|
(mf/spread-props props {:on-change handle-change
|
||||||
|
:value value})]
|
||||||
|
|
||||||
|
[:> select* props]))
|
||||||
|
|
||||||
(mf/defc form-submit*
|
(mf/defc form-submit*
|
||||||
[{:keys [disabled on-submit] :rest props}]
|
[{:keys [disabled on-submit] :rest props}]
|
||||||
(let [form (mf/use-ctx context)
|
(let [form (mf/use-ctx context)
|
||||||
@ -79,4 +97,4 @@
|
|||||||
(when (fn? on-submit)
|
(when (fn? on-submit)
|
||||||
(on-submit form event))))]
|
(on-submit form event))))]
|
||||||
[:> (mf/provider context) {:value form}
|
[:> (mf/provider context) {:value form}
|
||||||
[:form {:class class :on-submit on-submit'} children]]))
|
[:form {:class class :on-submit on-submit'} children]]))
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
["/feedback" :settings-feedback]
|
["/feedback" :settings-feedback]
|
||||||
["/options" :settings-options]
|
["/options" :settings-options]
|
||||||
["/subscriptions" :settings-subscription]
|
["/subscriptions" :settings-subscription]
|
||||||
["/access-tokens" :settings-access-tokens]
|
["/integrations" :settings-integrations]
|
||||||
["/notifications" :settings-notifications]]
|
["/notifications" :settings-notifications]]
|
||||||
|
|
||||||
["/frame-preview" :frame-preview]
|
["/frame-preview" :frame-preview]
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.modal :refer [modal-container*]]
|
[app.main.ui.modal :refer [modal-container*]]
|
||||||
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
|
|
||||||
[app.main.ui.settings.change-email]
|
[app.main.ui.settings.change-email]
|
||||||
[app.main.ui.settings.delete-account]
|
[app.main.ui.settings.delete-account]
|
||||||
[app.main.ui.settings.feedback :refer [feedback-page*]]
|
[app.main.ui.settings.feedback :refer [feedback-page*]]
|
||||||
|
[app.main.ui.settings.integrations :refer [integrations-page*]]
|
||||||
[app.main.ui.settings.notifications :refer [notifications-page*]]
|
[app.main.ui.settings.notifications :refer [notifications-page*]]
|
||||||
[app.main.ui.settings.options :refer [options-page]]
|
[app.main.ui.settings.options :refer [options-page]]
|
||||||
[app.main.ui.settings.password :refer [password-page]]
|
[app.main.ui.settings.password :refer [password-page]]
|
||||||
@ -73,8 +73,8 @@
|
|||||||
:settings-subscription
|
:settings-subscription
|
||||||
[:> subscription-page* {:profile profile}]
|
[:> subscription-page* {:profile profile}]
|
||||||
|
|
||||||
:settings-access-tokens
|
:settings-integrations
|
||||||
[:& access-tokens-page]
|
[:> integrations-page*]
|
||||||
|
|
||||||
:settings-notifications
|
:settings-notifications
|
||||||
[:& notifications-page* {:profile profile}])]]]]))
|
[:& notifications-page* {:profile profile}])]]]]))
|
||||||
|
|||||||
@ -1,291 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.main.ui.settings.access-tokens
|
|
||||||
(:require-macros [app.main.style :as stl])
|
|
||||||
(:require
|
|
||||||
[app.common.schema :as sm]
|
|
||||||
[app.common.time :as ct]
|
|
||||||
[app.main.data.modal :as modal]
|
|
||||||
[app.main.data.notifications :as ntf]
|
|
||||||
[app.main.data.profile :as du]
|
|
||||||
[app.main.store :as st]
|
|
||||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
|
||||||
[app.main.ui.components.forms :as fm]
|
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
|
||||||
[app.util.clipboard :as clipboard]
|
|
||||||
[app.util.dom :as dom]
|
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
|
||||||
[app.util.keyboard :as kbd]
|
|
||||||
[okulary.core :as l]
|
|
||||||
[rumext.v2 :as mf]))
|
|
||||||
|
|
||||||
(def ^:private clipboard-icon
|
|
||||||
(deprecated-icon/icon-xref :clipboard (stl/css :clipboard-icon)))
|
|
||||||
|
|
||||||
(def ^:private close-icon
|
|
||||||
(deprecated-icon/icon-xref :close (stl/css :close-icon)))
|
|
||||||
|
|
||||||
(def ^:private menu-icon
|
|
||||||
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
|
|
||||||
|
|
||||||
(def tokens-ref
|
|
||||||
(l/derived :access-tokens st/state))
|
|
||||||
|
|
||||||
(def token-created-ref
|
|
||||||
(l/derived :access-token-created st/state))
|
|
||||||
|
|
||||||
(def ^:private schema:form
|
|
||||||
[:map {:title "AccessTokenForm"}
|
|
||||||
[:name [::sm/text {:max 250}]]
|
|
||||||
[:expiration-date [::sm/text {:max 250}]]])
|
|
||||||
|
|
||||||
(def initial-data
|
|
||||||
{:name "" :expiration-date "never"})
|
|
||||||
|
|
||||||
(mf/defc access-token-modal
|
|
||||||
{::mf/register modal/components
|
|
||||||
::mf/register-as :access-token}
|
|
||||||
[]
|
|
||||||
(let [form (fm/use-form
|
|
||||||
:initial initial-data
|
|
||||||
:schema schema:form)
|
|
||||||
|
|
||||||
created (mf/deref token-created-ref)
|
|
||||||
created? (mf/use-state false)
|
|
||||||
|
|
||||||
on-success
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps created)
|
|
||||||
(fn [_]
|
|
||||||
(let [message (tr "dashboard.access-tokens.create.success")]
|
|
||||||
(st/emit! (du/fetch-access-tokens)
|
|
||||||
(ntf/success message)
|
|
||||||
(reset! created? true)))))
|
|
||||||
|
|
||||||
on-close
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps created)
|
|
||||||
(fn [_]
|
|
||||||
(reset! created? false)
|
|
||||||
(st/emit! (modal/hide))))
|
|
||||||
|
|
||||||
on-error
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [_]
|
|
||||||
(st/emit! (ntf/error (tr "errors.generic"))
|
|
||||||
(modal/hide))))
|
|
||||||
|
|
||||||
on-submit
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [form]
|
|
||||||
(let [cdata (:clean-data @form)
|
|
||||||
mdata {:on-success (partial on-success form)
|
|
||||||
:on-error (partial on-error form)}
|
|
||||||
expiration (:expiration-date cdata)
|
|
||||||
params (cond-> {:name (:name cdata)
|
|
||||||
:perms (:perms cdata)}
|
|
||||||
(not= "never" expiration) (assoc :expiration expiration))]
|
|
||||||
(st/emit! (du/create-access-token
|
|
||||||
(with-meta params mdata))))))
|
|
||||||
|
|
||||||
copy-token
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps created)
|
|
||||||
(fn [event]
|
|
||||||
(dom/prevent-default event)
|
|
||||||
(clipboard/to-clipboard (:token created))
|
|
||||||
(st/emit! (ntf/show {:level :info
|
|
||||||
:type :toast
|
|
||||||
:content (tr "dashboard.access-tokens.copied-success")
|
|
||||||
:timeout 7000}))))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
|
||||||
[:div {:class (stl/css :modal-container)}
|
|
||||||
[:& fm/form {:form form :on-submit on-submit}
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-header)}
|
|
||||||
[:h2 {:class (stl/css :modal-title)} (tr "modals.create-access-token.title")]
|
|
||||||
|
|
||||||
[:button {:class (stl/css :modal-close-btn)
|
|
||||||
:on-click on-close}
|
|
||||||
close-icon]]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
|
||||||
[:& fm/input {:type "text"
|
|
||||||
:auto-focus? true
|
|
||||||
:form form
|
|
||||||
:name :name
|
|
||||||
:disabled @created?
|
|
||||||
:label (tr "modals.create-access-token.name.label")
|
|
||||||
:show-success? true
|
|
||||||
:placeholder (tr "modals.create-access-token.name.placeholder")}]]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
|
||||||
[:div {:class (stl/css :select-title)}
|
|
||||||
(tr "modals.create-access-token.expiration-date.label")]
|
|
||||||
[:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"}
|
|
||||||
{:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"}
|
|
||||||
{:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"}
|
|
||||||
{:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"}
|
|
||||||
{:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}]
|
|
||||||
:default "never"
|
|
||||||
:disabled @created?
|
|
||||||
:name :expiration-date}]
|
|
||||||
(when @created?
|
|
||||||
[:span {:class (stl/css :token-created-info)}
|
|
||||||
(if (:expires-at created)
|
|
||||||
(tr "dashboard.access-tokens.token-will-expire" (ct/format-inst (:expires-at created) "PPP"))
|
|
||||||
(tr "dashboard.access-tokens.token-will-not-expire"))])]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
|
||||||
(when @created?
|
|
||||||
[:div {:class (stl/css :custon-input-wrapper)}
|
|
||||||
[:input {:type "text"
|
|
||||||
:value (:token created "")
|
|
||||||
:class (stl/css :custom-input-token)
|
|
||||||
:read-only true}]
|
|
||||||
[:button {:title (tr "modals.create-access-token.copy-token")
|
|
||||||
:class (stl/css :copy-btn)
|
|
||||||
:on-click copy-token}
|
|
||||||
clipboard-icon]])
|
|
||||||
#_(when @created?
|
|
||||||
[:button {:class (stl/css :copy-btn)
|
|
||||||
:title (tr "modals.create-access-token.copy-token")
|
|
||||||
:on-click copy-token}
|
|
||||||
[:span {:class (stl/css :token-value)} (:token created "")]
|
|
||||||
[:span {:class (stl/css :icon)}
|
|
||||||
i/clipboard]])]]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
|
||||||
[:div {:class (stl/css :action-buttons)}
|
|
||||||
|
|
||||||
(if @created?
|
|
||||||
[:input {:class (stl/css :cancel-button)
|
|
||||||
:type "button"
|
|
||||||
:value (tr "labels.close")
|
|
||||||
:on-click modal/hide!}]
|
|
||||||
[:*
|
|
||||||
[:input {:class (stl/css :cancel-button)
|
|
||||||
:type "button"
|
|
||||||
:value (tr "labels.cancel")
|
|
||||||
:on-click modal/hide!}]
|
|
||||||
[:> fm/submit-button*
|
|
||||||
{:large? false :label (tr "modals.create-access-token.submit-label")}]])]]]]]))
|
|
||||||
|
|
||||||
(mf/defc access-tokens-hero
|
|
||||||
[]
|
|
||||||
(let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))]
|
|
||||||
[:div {:class (stl/css :access-tokens-hero)}
|
|
||||||
[:h2 {:class (stl/css :hero-title)} (tr "dashboard.access-tokens.personal")]
|
|
||||||
[:p {:class (stl/css :hero-desc)} (tr "dashboard.access-tokens.personal.description")]
|
|
||||||
|
|
||||||
[:button {:class (stl/css :hero-btn)
|
|
||||||
:on-click on-click}
|
|
||||||
(tr "dashboard.access-tokens.create")]]))
|
|
||||||
|
|
||||||
(mf/defc access-token-actions
|
|
||||||
[{:keys [on-delete]}]
|
|
||||||
(let [local (mf/use-state {:menu-open false})
|
|
||||||
show? (:menu-open @local)
|
|
||||||
options (mf/with-memo [on-delete]
|
|
||||||
[{:name (tr "labels.delete")
|
|
||||||
:id "access-token-delete"
|
|
||||||
:handler on-delete}])
|
|
||||||
|
|
||||||
menu-ref (mf/use-ref)
|
|
||||||
|
|
||||||
on-menu-close
|
|
||||||
(mf/use-fn #(swap! local assoc :menu-open false))
|
|
||||||
|
|
||||||
on-menu-click
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [event]
|
|
||||||
(dom/prevent-default event)
|
|
||||||
(swap! local assoc :menu-open true)))
|
|
||||||
|
|
||||||
on-keydown
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps on-menu-click)
|
|
||||||
(fn [event]
|
|
||||||
(when (kbd/enter? event)
|
|
||||||
(dom/stop-propagation event)
|
|
||||||
(on-menu-click event))))]
|
|
||||||
|
|
||||||
[:button {:class (stl/css :menu-btn)
|
|
||||||
:tab-index "0"
|
|
||||||
:ref menu-ref
|
|
||||||
:on-click on-menu-click
|
|
||||||
:on-key-down on-keydown}
|
|
||||||
menu-icon
|
|
||||||
[:> context-menu*
|
|
||||||
{:on-close on-menu-close
|
|
||||||
:show show?
|
|
||||||
:fixed true
|
|
||||||
:min-width true
|
|
||||||
:top "auto"
|
|
||||||
:left "auto"
|
|
||||||
:options options}]]))
|
|
||||||
|
|
||||||
(mf/defc access-token-item
|
|
||||||
{::mf/wrap [mf/memo]}
|
|
||||||
[{:keys [token] :as props}]
|
|
||||||
(let [expires-at (:expires-at token)
|
|
||||||
expires-txt (some-> expires-at (ct/format-inst "PPP"))
|
|
||||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
|
||||||
|
|
||||||
delete-fn
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps token)
|
|
||||||
(fn []
|
|
||||||
(let [params {:id (:id token)}
|
|
||||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
|
||||||
(st/emit! (du/delete-access-token (with-meta params mdata))))))
|
|
||||||
|
|
||||||
on-delete
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps delete-fn)
|
|
||||||
(fn []
|
|
||||||
(st/emit! (modal/show
|
|
||||||
{:type :confirm
|
|
||||||
:title (tr "modals.delete-acces-token.title")
|
|
||||||
:message (tr "modals.delete-acces-token.message")
|
|
||||||
:accept-label (tr "modals.delete-acces-token.accept")
|
|
||||||
:on-accept delete-fn}))))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :table-row)}
|
|
||||||
[:div {:class (stl/css :table-field :field-name)}
|
|
||||||
(str (:name token))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css-case :expiration-date true
|
|
||||||
:expired expired?)}
|
|
||||||
(cond
|
|
||||||
(nil? expires-at) (tr "dashboard.access-tokens.no-expiration")
|
|
||||||
expired? (tr "dashboard.access-tokens.expired-on" expires-txt)
|
|
||||||
:else (tr "dashboard.access-tokens.expires-on" expires-txt))]
|
|
||||||
[:div {:class (stl/css :table-field :actions)}
|
|
||||||
[:& access-token-actions
|
|
||||||
{:on-delete on-delete}]]]))
|
|
||||||
|
|
||||||
(mf/defc access-tokens-page
|
|
||||||
[]
|
|
||||||
(let [tokens (mf/deref tokens-ref)]
|
|
||||||
(mf/with-effect []
|
|
||||||
(dom/set-html-title (tr "title.settings.access-tokens"))
|
|
||||||
(st/emit! (du/fetch-access-tokens)))
|
|
||||||
|
|
||||||
[:div {:class (stl/css :dashboard-access-tokens)}
|
|
||||||
[:& access-tokens-hero]
|
|
||||||
(if (empty? tokens)
|
|
||||||
[:div {:class (stl/css :access-tokens-empty)}
|
|
||||||
[:div (tr "dashboard.access-tokens.empty.no-access-tokens")]
|
|
||||||
[:div (tr "dashboard.access-tokens.empty.add-one")]]
|
|
||||||
[:div {:class (stl/css :dashboard-table)}
|
|
||||||
[:div {:class (stl/css :table-rows)}
|
|
||||||
(for [token tokens]
|
|
||||||
[:& access-token-item {:token token :key (:id token)}])]])]))
|
|
||||||
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
//
|
|
||||||
// Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
|
||||||
|
|
||||||
// ACCESS TOKENS PAGE
|
|
||||||
.dashboard-access-tokens {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
margin: deprecated.$s-80 auto deprecated.$s-120 auto;
|
|
||||||
gap: deprecated.$s-32;
|
|
||||||
width: deprecated.$s-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hero
|
|
||||||
.access-tokens-hero {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto auto 1fr;
|
|
||||||
gap: deprecated.$s-32;
|
|
||||||
width: deprecated.$s-500;
|
|
||||||
font-size: deprecated.$fs-14;
|
|
||||||
margin: deprecated.$s-16 auto 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
@include deprecated.bigTitleTipography;
|
|
||||||
color: var(--title-foreground-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-desc {
|
|
||||||
color: var(--title-foreground-color);
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: deprecated.$fs-14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-btn {
|
|
||||||
@extend .button-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
// table empty
|
|
||||||
.access-tokens-empty {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
align-content: center;
|
|
||||||
height: deprecated.$s-156;
|
|
||||||
max-width: deprecated.$s-1000;
|
|
||||||
width: 100%;
|
|
||||||
padding: deprecated.$s-32;
|
|
||||||
border: deprecated.$s-1 solid var(--panel-border-color);
|
|
||||||
border-radius: deprecated.$br-8;
|
|
||||||
color: var(--dashboard-list-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access tokens table
|
|
||||||
.dashboard-table {
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-rows {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-rows: deprecated.$s-64;
|
|
||||||
gap: deprecated.$s-16;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-width: deprecated.$s-1000;
|
|
||||||
margin-top: deprecated.$s-16;
|
|
||||||
color: var(--title-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 43% 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
height: deprecated.$s-64;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 deprecated.$s-16;
|
|
||||||
border-radius: deprecated.$br-8;
|
|
||||||
background-color: var(--dashboard-list-background-color);
|
|
||||||
color: var(--dashboard-list-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-name {
|
|
||||||
@include deprecated.textEllipsis;
|
|
||||||
display: grid;
|
|
||||||
width: 43%;
|
|
||||||
min-width: deprecated.$s-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expiration-date {
|
|
||||||
@include deprecated.flexCenter;
|
|
||||||
min-width: deprecated.$s-76;
|
|
||||||
width: fit-content;
|
|
||||||
height: deprecated.$s-24;
|
|
||||||
border-radius: deprecated.$br-8;
|
|
||||||
color: var(--dashboard-list-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expired {
|
|
||||||
@include deprecated.headlineSmallTypography;
|
|
||||||
padding: 0 deprecated.$s-6;
|
|
||||||
color: var(--pill-foreground-color);
|
|
||||||
background-color: var(--status-widget-background-color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.menu-icon {
|
|
||||||
@extend .button-icon;
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-btn {
|
|
||||||
@include deprecated.buttonStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create access token modal
|
|
||||||
.modal-overlay {
|
|
||||||
@extend .modal-overlay-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
@extend .modal-container-base;
|
|
||||||
min-width: deprecated.$s-408;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
margin-bottom: deprecated.$s-24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
@include deprecated.uppercaseTitleTipography;
|
|
||||||
color: var(--modal-title-foreground-color);
|
|
||||||
}
|
|
||||||
.modal-close-btn {
|
|
||||||
@extend .modal-close-btn-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
@include deprecated.flexColumn;
|
|
||||||
gap: deprecated.$s-24;
|
|
||||||
@include deprecated.bodySmallTypography;
|
|
||||||
margin-bottom: deprecated.$s-24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-title {
|
|
||||||
@include deprecated.bodySmallTypography;
|
|
||||||
color: var(--modal-title-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custon-input-wrapper {
|
|
||||||
@include deprecated.flexRow;
|
|
||||||
border-radius: deprecated.$br-8;
|
|
||||||
height: deprecated.$s-32;
|
|
||||||
background-color: var(--input-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-input-token {
|
|
||||||
@extend .input-element;
|
|
||||||
@include deprecated.bodySmallTypography;
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border: deprecated.$s-1 solid var(--input-border-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-value {
|
|
||||||
@include deprecated.textEllipsis;
|
|
||||||
@include deprecated.bodySmallTypography;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn {
|
|
||||||
@include deprecated.flexCenter;
|
|
||||||
@extend .button-secondary;
|
|
||||||
height: deprecated.$s-28;
|
|
||||||
width: deprecated.$s-28;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clipboard-icon {
|
|
||||||
@extend .button-icon-small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-created-info {
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
@extend .modal-action-btns;
|
|
||||||
button {
|
|
||||||
@extend .modal-accept-btn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-button {
|
|
||||||
@extend .modal-cancel-btn;
|
|
||||||
}
|
|
||||||
635
frontend/src/app/main/ui/settings/integrations.cljs
Normal file
635
frontend/src/app/main/ui/settings/integrations.cljs
Normal file
@ -0,0 +1,635 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.settings.integrations
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.time :as ct]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.main.broadcast :as mbc]
|
||||||
|
[app.main.data.event :as ev]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.data.notifications :as ntf]
|
||||||
|
[app.main.data.profile :as du]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.controls.input :refer [input*]]
|
||||||
|
[app.main.ui.ds.controls.switch :refer [switch*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||||
|
[app.main.ui.ds.foundations.typography :as t]
|
||||||
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
|
[app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]]
|
||||||
|
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||||
|
[app.main.ui.forms :as fc]
|
||||||
|
[app.util.clipboard :as clipboard]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.forms :as fm]
|
||||||
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[okulary.core :as l]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def tokens-ref
|
||||||
|
(l/derived :access-tokens st/state))
|
||||||
|
|
||||||
|
(def token-created-ref
|
||||||
|
(l/derived :access-token-created st/state))
|
||||||
|
|
||||||
|
(def notification-timeout 7000)
|
||||||
|
|
||||||
|
(def ^:private schema:form-access-token
|
||||||
|
[:map
|
||||||
|
[:name [::sm/text {:max 250}]]
|
||||||
|
[:expiration-date [::sm/text {:max 250}]]])
|
||||||
|
|
||||||
|
(def ^:private schema:form-mcp-key
|
||||||
|
[:map
|
||||||
|
[:expiration-date [::sm/text {:max 250}]]])
|
||||||
|
|
||||||
|
(def form-initial-data-access-token
|
||||||
|
{:name ""
|
||||||
|
:expiration-date "never"})
|
||||||
|
|
||||||
|
(def form-initial-data-mcp-key
|
||||||
|
{:expiration-date "never"})
|
||||||
|
|
||||||
|
(mf/defc input-copy*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [value on-copy-to-clipboard]}]
|
||||||
|
[:div {:class (stl/css :input-copy)}
|
||||||
|
[:> input* {:type "text"
|
||||||
|
:default-value value
|
||||||
|
:read-only true}]
|
||||||
|
[:div {:class (stl/css :input-copy-button-wrapper)}
|
||||||
|
[:> icon-button* {:variant "secondary"
|
||||||
|
:class (stl/css :input-copy-button)
|
||||||
|
:aria-label (tr "integrations.copy-to-clipboard")
|
||||||
|
:on-click on-copy-to-clipboard
|
||||||
|
:icon i/clipboard}]]])
|
||||||
|
|
||||||
|
(mf/defc token-created*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [title mcp-key?]}]
|
||||||
|
(let [token-created (mf/deref token-created-ref)
|
||||||
|
|
||||||
|
on-copy-to-clipboard
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps token-created)
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(clipboard/to-clipboard (:token token-created))
|
||||||
|
(st/emit! (ntf/show {:level :info
|
||||||
|
:type :toast
|
||||||
|
:content (if mcp-key?
|
||||||
|
(tr "integrations.notification.success.mcp-key-copied")
|
||||||
|
(tr "integrations.notification.success.token-copied"))
|
||||||
|
:timeout notification-timeout}))))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-form)}
|
||||||
|
[:> text* {:as "h2"
|
||||||
|
:typography t/headline-large
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
title]
|
||||||
|
|
||||||
|
[:> notification-pill* {:level :info
|
||||||
|
:type :context}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(if mcp-key?
|
||||||
|
(tr "integrations.mcp-key.info.non-recuperable")
|
||||||
|
(tr "integrations.token.info.non-recuperable"))]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> input-copy* {:value (:token token-created "")
|
||||||
|
:on-copy-to-clipboard on-copy-to-clipboard}]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
(if (:expires-at token-created)
|
||||||
|
(if mcp-key?
|
||||||
|
(tr "integrations.mcp-key.will-expire" (ct/format-inst (:expires-at token-created) "PPP"))
|
||||||
|
(tr "integrations.token.will-expire" (ct/format-inst (:expires-at token-created) "PPP")))
|
||||||
|
(if mcp-key?
|
||||||
|
(tr "integrations.mcp-key.will-not-expire")
|
||||||
|
(tr "integrations.token.will-not-expire")))]]
|
||||||
|
|
||||||
|
(when mcp-key?
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.info.mcp-client-config")]
|
||||||
|
[:textarea {:class (stl/css :textarea)
|
||||||
|
:wrap "off"
|
||||||
|
:rows 7
|
||||||
|
:read-only true}
|
||||||
|
(dm/str
|
||||||
|
"{\n"
|
||||||
|
" \"mcpServers\": {\n"
|
||||||
|
" \"penpot\": {\n"
|
||||||
|
" \"url\": \"" cf/mcp-server-url "?userToken=" (:token token-created "") "\"\n"
|
||||||
|
" }\n"
|
||||||
|
" }"
|
||||||
|
"\n}")]])
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-footer)}
|
||||||
|
[:> button* {:variant "secondary"
|
||||||
|
:on-click modal/hide!}
|
||||||
|
(tr "labels.close")]]]))
|
||||||
|
|
||||||
|
(mf/defc create-token*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [title info mcp-key? on-created]}]
|
||||||
|
(let [form (fm/use-form
|
||||||
|
:initial (if mcp-key?
|
||||||
|
form-initial-data-mcp-key
|
||||||
|
form-initial-data-access-token)
|
||||||
|
:schema (if mcp-key?
|
||||||
|
schema:form-mcp-key
|
||||||
|
schema:form-access-token))
|
||||||
|
|
||||||
|
on-error
|
||||||
|
(mf/use-fn
|
||||||
|
#(st/emit! (ntf/error (tr "errors.generic"))
|
||||||
|
(modal/hide)))
|
||||||
|
|
||||||
|
on-success
|
||||||
|
(mf/use-fn
|
||||||
|
#(st/emit! (du/fetch-access-tokens)
|
||||||
|
(ntf/success (tr "integrations.notification.success.created"))
|
||||||
|
(on-created)))
|
||||||
|
|
||||||
|
on-submit
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [form]
|
||||||
|
(let [cdata (:clean-data @form)
|
||||||
|
mdata {:on-success (partial on-success form)
|
||||||
|
:on-error (partial on-error form)}
|
||||||
|
expiration (:expiration-date cdata)
|
||||||
|
params (cond-> {:name (:name cdata)
|
||||||
|
:perms (:perms cdata)}
|
||||||
|
(not= "never" expiration) (assoc :expiration expiration)
|
||||||
|
(true? mcp-key?) (assoc :type "mcp"
|
||||||
|
:name "MCP key"))]
|
||||||
|
(st/emit! (du/create-access-token (with-meta params mdata))))))]
|
||||||
|
|
||||||
|
[:> fc/form* {:form form
|
||||||
|
:class (stl/css :modal-form)
|
||||||
|
:on-submit on-submit}
|
||||||
|
|
||||||
|
[:> text* {:as "h2"
|
||||||
|
:typography t/headline-large
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
title]
|
||||||
|
|
||||||
|
(when (some? info)
|
||||||
|
[:> notification-pill* {:level :info
|
||||||
|
:type :context}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
info]])
|
||||||
|
|
||||||
|
(if mcp-key?
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
(tr "integrations.info.mcp-server")]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> fc/form-input* {:type "text"
|
||||||
|
:auto-focus? true
|
||||||
|
:form form
|
||||||
|
:name :name
|
||||||
|
:label (tr "integrations.name.label")
|
||||||
|
:placeholder (tr "integrations.name.placeholder")}]])
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> text* {:as "label"
|
||||||
|
:typography t/body-small
|
||||||
|
:for :expiration-date
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.expiration-date.label")]
|
||||||
|
[:> fc/form-select* {:options [{:label (tr "integrations.expiration-never") :value "never" :id "never"}
|
||||||
|
{:label (tr "integrations.expiration-30-days") :value "720h" :id "720h"}
|
||||||
|
{:label (tr "integrations.expiration-60-days") :value "1440h" :id "1440h"}
|
||||||
|
{:label (tr "integrations.expiration-90-days") :value "2160h" :id "2160h"}
|
||||||
|
{:label (tr "integrations.expiration-180-days") :value "4320h" :id "4320h"}]
|
||||||
|
:default-selected "never"
|
||||||
|
:name :expiration-date}]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-footer)}
|
||||||
|
[:> button* {:variant "secondary"
|
||||||
|
:on-click modal/hide!}
|
||||||
|
(tr "labels.cancel")]
|
||||||
|
[:> fc/form-submit* {:variant "primary"}
|
||||||
|
title]]]))
|
||||||
|
|
||||||
|
(mf/defc create-access-token-modal
|
||||||
|
{::mf/register modal/components
|
||||||
|
::mf/register-as :create-access-token}
|
||||||
|
[]
|
||||||
|
(let [created? (mf/use-state false)
|
||||||
|
|
||||||
|
on-close
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(reset! created? false)
|
||||||
|
(st/emit! (modal/hide))))
|
||||||
|
|
||||||
|
on-created
|
||||||
|
(mf/use-fn
|
||||||
|
#(reset! created? true))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:div {:class (stl/css :modal-close-button)}
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click on-close
|
||||||
|
:icon i/close}]]
|
||||||
|
|
||||||
|
(if @created?
|
||||||
|
[:> token-created* {:title (tr "integrations.create-access-token.title.created")}]
|
||||||
|
[:> create-token* {:title (tr "integrations.create-access-token.title")
|
||||||
|
:on-created on-created}])]]))
|
||||||
|
|
||||||
|
(mf/defc generate-mcp-key-modal
|
||||||
|
{::mf/register modal/components
|
||||||
|
::mf/register-as :generate-mcp-key}
|
||||||
|
[]
|
||||||
|
(let [created? (mf/use-state false)
|
||||||
|
|
||||||
|
on-close
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(reset! created? false)
|
||||||
|
(st/emit! (modal/hide))))
|
||||||
|
|
||||||
|
on-created
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (du/update-profile-props {:mcp-enabled true})
|
||||||
|
(ev/event {::ev/name "generate-mcp-key"
|
||||||
|
::ev/origin "integrations"})
|
||||||
|
(ev/event {::ev/name "enable-mcp"
|
||||||
|
::ev/origin "integrations"
|
||||||
|
:source "key-creation"})
|
||||||
|
(mbc/event :mcp/enable {}))
|
||||||
|
(reset! created? true)))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:div {:class (stl/css :modal-close-button)}
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click on-close
|
||||||
|
:icon i/close}]]
|
||||||
|
|
||||||
|
(if @created?
|
||||||
|
[:> token-created* {:title (tr "integrations.generate-mcp-key.title.created")
|
||||||
|
:mcp-key? true}]
|
||||||
|
[:> create-token* {:title (tr "integrations.generate-mcp-key.title")
|
||||||
|
:mcp-key? true
|
||||||
|
:on-created on-created}])]]))
|
||||||
|
|
||||||
|
(mf/defc regenerate-mcp-key-modal
|
||||||
|
{::mf/register modal/components
|
||||||
|
::mf/register-as :regenerate-mcp-key}
|
||||||
|
[]
|
||||||
|
(let [created? (mf/use-state false)
|
||||||
|
|
||||||
|
tokens (mf/deref tokens-ref)
|
||||||
|
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||||
|
mcp-key-id (:id mcp-key)
|
||||||
|
|
||||||
|
on-close
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(reset! created? false)
|
||||||
|
(st/emit! (modal/hide))))
|
||||||
|
|
||||||
|
on-created
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (du/delete-access-token {:id mcp-key-id})
|
||||||
|
(du/update-profile-props {:mcp-enabled true})
|
||||||
|
(ev/event {::ev/name "regenerate-mcp-key"
|
||||||
|
::ev/origin "integrations"})
|
||||||
|
(mbc/event :mcp/enable {}))
|
||||||
|
(reset! created? true)))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:div {:class (stl/css :modal-close-button)}
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click on-close
|
||||||
|
:icon i/close}]]
|
||||||
|
|
||||||
|
(if @created?
|
||||||
|
[:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created")
|
||||||
|
:mcp-key? true}]
|
||||||
|
[:> create-token* {:title (tr "integrations.regenerate-mcp-key.title")
|
||||||
|
:info (tr "integrations.regenerate-mcp-key.info")
|
||||||
|
:mcp-key? true
|
||||||
|
:on-created on-created}])]]))
|
||||||
|
|
||||||
|
(mf/defc token-item*
|
||||||
|
{::mf/private true
|
||||||
|
::mf/wrap [mf/memo]}
|
||||||
|
[{:keys [name expires-at on-delete]}]
|
||||||
|
(let [expires-txt (some-> expires-at (ct/format-inst "PPP"))
|
||||||
|
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||||
|
|
||||||
|
menu-open* (mf/use-state false)
|
||||||
|
menu-open? (deref menu-open*)
|
||||||
|
|
||||||
|
handle-menu-close
|
||||||
|
(mf/use-fn
|
||||||
|
#(reset! menu-open* false))
|
||||||
|
|
||||||
|
handle-menu-click
|
||||||
|
(mf/use-fn
|
||||||
|
#(reset! menu-open* (not menu-open?)))
|
||||||
|
|
||||||
|
handle-open-confirm-modal
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-delete)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (modal/show {:type :confirm
|
||||||
|
:title (tr "integrations.delete-token.title")
|
||||||
|
:message (tr "integrations.delete-token.message")
|
||||||
|
:accept-label (tr "integrations.delete-token.accept")
|
||||||
|
:on-accept on-delete}))))
|
||||||
|
|
||||||
|
options
|
||||||
|
(mf/with-memo [on-delete]
|
||||||
|
[{:name (tr "labels.delete")
|
||||||
|
:id "token-delete"
|
||||||
|
:handler handle-open-confirm-modal}])]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :item)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:title name
|
||||||
|
:class (stl/css :item-title)}
|
||||||
|
name]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css-case :item-subtitle true
|
||||||
|
:warning expired?)}
|
||||||
|
(cond
|
||||||
|
(nil? expires-at) (tr "integrations.no-expiration")
|
||||||
|
expired? (tr "integrations.expired-on" expires-txt)
|
||||||
|
:else (tr "integrations.expires-on" expires-txt))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :item-actions)}
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:class (stl/css :item-button)
|
||||||
|
:aria-pressed menu-open?
|
||||||
|
:aria-label (tr "labels.options")
|
||||||
|
:on-click handle-menu-click
|
||||||
|
:icon i/menu}]
|
||||||
|
[:> context-menu* {:on-close handle-menu-close
|
||||||
|
:show menu-open?
|
||||||
|
:min-width true
|
||||||
|
:top -10
|
||||||
|
:left -138
|
||||||
|
:options options}]]]))
|
||||||
|
|
||||||
|
(mf/defc mcp-server-section*
|
||||||
|
{::mf/private true}
|
||||||
|
[]
|
||||||
|
(let [tokens (mf/deref tokens-ref)
|
||||||
|
profile (mf/deref refs/profile)
|
||||||
|
|
||||||
|
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||||
|
mcp-enabled? (true? (-> profile :props :mcp-enabled))
|
||||||
|
|
||||||
|
expires-at (:expires-at mcp-key)
|
||||||
|
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||||
|
|
||||||
|
tooltip-id
|
||||||
|
(mf/use-id)
|
||||||
|
|
||||||
|
handle-mcp-change
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [value]
|
||||||
|
(st/emit! (du/update-profile-props {:mcp-enabled value})
|
||||||
|
(ntf/show {:level :info
|
||||||
|
:type :toast
|
||||||
|
:content (if (true? value)
|
||||||
|
(tr "integrations.notification.success.mcp-server-enabled")
|
||||||
|
(tr "integrations.notification.success.mcp-server-disabled"))
|
||||||
|
:timeout notification-timeout})
|
||||||
|
(ev/event {::ev/name (if (true? value) "enable-mcp" "disable-mcp")
|
||||||
|
::ev/origin "integrations"
|
||||||
|
:source "toggle"})
|
||||||
|
(if value
|
||||||
|
(mbc/event :mcp/enable {})
|
||||||
|
(mbc/event :mcp/disable {})))))
|
||||||
|
|
||||||
|
handle-generate-mcp-key
|
||||||
|
(mf/use-fn
|
||||||
|
#(st/emit! (modal/show {:type :generate-mcp-key})))
|
||||||
|
|
||||||
|
handle-regenerate-mcp-key
|
||||||
|
(mf/use-fn
|
||||||
|
#(st/emit! (modal/show {:type :regenerate-mcp-key})))
|
||||||
|
|
||||||
|
handle-delete
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps mcp-key)
|
||||||
|
(fn []
|
||||||
|
(let [params {:id (:id mcp-key)}
|
||||||
|
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||||
|
(st/emit! (du/delete-access-token (with-meta params mdata))
|
||||||
|
(du/update-profile-props {:mcp-enabled false})
|
||||||
|
(mbc/event :mcp/disable {})))))
|
||||||
|
|
||||||
|
on-copy-to-clipboard
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(clipboard/to-clipboard cf/mcp-server-url)
|
||||||
|
(st/emit! (ntf/show {:level :info
|
||||||
|
:type :toast
|
||||||
|
:content (tr "integrations.notification.success.copied-link")
|
||||||
|
:timeout notification-timeout})
|
||||||
|
(ev/event {::ev/name "copy-mcp-url"
|
||||||
|
::ev/origin "integrations"}))))]
|
||||||
|
|
||||||
|
[:section {:class (stl/css :mcp-server-section)}
|
||||||
|
[:div
|
||||||
|
[:div {:class (stl/css :title)}
|
||||||
|
[:> heading* {:level 2
|
||||||
|
:typography t/title-medium
|
||||||
|
:class (stl/css :color-primary :mcp-server-title)}
|
||||||
|
(tr "integrations.mcp-server.title")]
|
||||||
|
[:> text* {:as "span"
|
||||||
|
:typography t/body-small
|
||||||
|
:class (stl/css :beta)}
|
||||||
|
(tr "integrations.mcp-server.title.beta")]]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
(tr "integrations.mcp-server.description")]]
|
||||||
|
|
||||||
|
[:div
|
||||||
|
[:> text* {:as "h3"
|
||||||
|
:typography t/headline-small
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.mcp-server.status")]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :mcp-server-block)}
|
||||||
|
(when expired?
|
||||||
|
[:> notification-pill* {:level :error
|
||||||
|
:type :context}
|
||||||
|
[:div {:class (stl/css :mcp-server-notification)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.mcp-server.status.expired.0")]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.mcp-server.status.expired.1")]]])
|
||||||
|
|
||||||
|
[:div {:class (stl/css :mcp-server-switch)}
|
||||||
|
[:> switch* {:label (if mcp-enabled?
|
||||||
|
(tr "integrations.mcp-server.status.enabled")
|
||||||
|
(tr "integrations.mcp-server.status.disabled"))
|
||||||
|
:default-checked mcp-enabled?
|
||||||
|
:on-change handle-mcp-change}]
|
||||||
|
(when (and (false? mcp-enabled?) (nil? mcp-key))
|
||||||
|
[:div {:class (stl/css :mcp-server-switch-cover)
|
||||||
|
:on-click handle-generate-mcp-key}])]]]
|
||||||
|
|
||||||
|
(when (some? mcp-key)
|
||||||
|
[:div {:class (stl/css :mcp-server-key)}
|
||||||
|
[:> text* {:as "h3"
|
||||||
|
:typography t/headline-small
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.mcp-server.mcp-keys.title")]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :mcp-server-block)}
|
||||||
|
[:div {:class (stl/css :mcp-server-regenerate)}
|
||||||
|
[:> button* {:variant "primary"
|
||||||
|
:class (stl/css :fit-content)
|
||||||
|
:on-click handle-regenerate-mcp-key}
|
||||||
|
(tr "integrations.mcp-server.mcp-keys.regenerate")]
|
||||||
|
[:> tooltip* {:content (tr "integrations.mcp-server.mcp-keys.tootip")
|
||||||
|
:id tooltip-id}
|
||||||
|
[:> icon* {:icon-id i/info
|
||||||
|
:class (stl/css :color-secondary)}]]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :list)}
|
||||||
|
[:> token-item* {:key (:id mcp-key)
|
||||||
|
:name (:name mcp-key)
|
||||||
|
:expires-at (:expires-at mcp-key)
|
||||||
|
:on-delete handle-delete}]]]])
|
||||||
|
|
||||||
|
[:> notification-pill* {:level :default
|
||||||
|
:type :context}
|
||||||
|
[:div {:class (stl/css :mcp-server-notification)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
(tr "integrations.mcp-server.mcp-keys.info")]
|
||||||
|
|
||||||
|
[:> input-copy* {:value (dm/str cf/mcp-server-url "?userToken=")
|
||||||
|
:on-copy-to-clipboard on-copy-to-clipboard}]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
[:a {:href cf/mcp-help-center-uri
|
||||||
|
:target "_blank"
|
||||||
|
:rel "noopener noreferrer"
|
||||||
|
:class (stl/css :mcp-server-notification-link)}
|
||||||
|
(tr "integrations.mcp-server.mcp-keys.help") [:> icon* {:icon-id i/open-link}]]]]]]))
|
||||||
|
|
||||||
|
(mf/defc access-tokens-section*
|
||||||
|
{::mf/private true}
|
||||||
|
[]
|
||||||
|
(let [tokens (mf/deref tokens-ref)
|
||||||
|
|
||||||
|
handle-click
|
||||||
|
(mf/use-fn
|
||||||
|
#(st/emit! (modal/show {:type :create-access-token})))
|
||||||
|
|
||||||
|
handle-delete
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [token-id]
|
||||||
|
(let [params {:id token-id}
|
||||||
|
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||||
|
(st/emit! (du/delete-access-token (with-meta params mdata))))))]
|
||||||
|
|
||||||
|
[:section {:class (stl/css :access-tokens-section)}
|
||||||
|
[:> heading* {:level 2
|
||||||
|
:typography t/title-medium
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.access-tokens.personal")]
|
||||||
|
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary)}
|
||||||
|
(tr "integrations.access-tokens.personal.description")]
|
||||||
|
|
||||||
|
[:> button* {:variant "primary"
|
||||||
|
:class (stl/css :fit-content)
|
||||||
|
:on-click handle-click}
|
||||||
|
(tr "integrations.access-tokens.create")]
|
||||||
|
|
||||||
|
(if (empty? tokens)
|
||||||
|
[:div {:class (stl/css :frame)}
|
||||||
|
[:> text* {:as "div"
|
||||||
|
:typography t/body-medium
|
||||||
|
:class (stl/css :color-secondary :text-center)}
|
||||||
|
[:div (tr "integrations.access-tokens.empty.no-access-tokens")]
|
||||||
|
[:div (tr "integrations.access-tokens.empty.add-one")]]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :list)}
|
||||||
|
(for [token tokens]
|
||||||
|
(when (nil? (:type token))
|
||||||
|
[:> token-item* {:key (:id token)
|
||||||
|
:name (:name token)
|
||||||
|
:expires-at (:expires-at token)
|
||||||
|
:on-delete (partial handle-delete (:id token))}]))])]))
|
||||||
|
|
||||||
|
(mf/defc integrations-page*
|
||||||
|
[]
|
||||||
|
(mf/with-effect []
|
||||||
|
(dom/set-html-title (tr "title.settings.integrations"))
|
||||||
|
(st/emit! (du/fetch-access-tokens)))
|
||||||
|
|
||||||
|
[:div {:class (stl/css :integrations)}
|
||||||
|
[:> heading* {:level 1
|
||||||
|
:typography t/title-large
|
||||||
|
:class (stl/css :color-primary)}
|
||||||
|
(tr "integrations.title")]
|
||||||
|
|
||||||
|
(when (contains? cf/flags :mcp)
|
||||||
|
[:> mcp-server-section*])
|
||||||
|
|
||||||
|
(when (and (contains? cf/flags :mcp)
|
||||||
|
(contains? cf/flags :access-tokens))
|
||||||
|
[:hr {:class (stl/css :separator)}])
|
||||||
|
|
||||||
|
(when (contains? cf/flags :access-tokens)
|
||||||
|
[:> access-tokens-section*])])
|
||||||
239
frontend/src/app/main/ui/settings/integrations.scss
Normal file
239
frontend/src/app/main/ui/settings/integrations.scss
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
|
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/mixins.scss" as *;
|
||||||
|
@use "ds/spacing.scss" as *;
|
||||||
|
@use "ds/typography.scss" as t;
|
||||||
|
|
||||||
|
.color-primary {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-secondary {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fit-content {
|
||||||
|
inline-size: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta {
|
||||||
|
color: var(--color-accent-primary);
|
||||||
|
border: $b-1 solid var(--color-accent-primary);
|
||||||
|
inline-size: fit-content;
|
||||||
|
padding: var(--sp-xxs) var(--sp-s);
|
||||||
|
border-radius: $br-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
@extend .modal-overlay-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
@extend .modal-container-base;
|
||||||
|
inline-size: $sz-400;
|
||||||
|
max-block-size: fit-content;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xxxl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--sp-s);
|
||||||
|
right: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-copy {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-copy-button-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-copy-button {
|
||||||
|
border-radius: 0 $br-8 $br-8 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integrations {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
margin: $sz-88 auto $sz-120 auto;
|
||||||
|
gap: $sz-32;
|
||||||
|
inline-size: $sz-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-tokens-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-key {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-notification {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
padding-right: var(--sp-xxl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-notification-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-accent-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-title {
|
||||||
|
margin: var(--sp-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-regenerate {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-switch {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-server-switch-cover {
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
border: $b-1 solid var(--color-background-quaternary);
|
||||||
|
margin: var(--sp-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
border: $b-1 solid var(--color-background-quaternary);
|
||||||
|
padding: var(--sp-m);
|
||||||
|
border-radius: $br-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: $sz-64;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 45% 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
border-radius: $br-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
@include textEllipsis;
|
||||||
|
align-content: center;
|
||||||
|
block-size: $sz-64;
|
||||||
|
padding: 0 var(--sp-l);
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-subtitle {
|
||||||
|
align-content: center;
|
||||||
|
block-size: $sz-64;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
padding: var(--sp-s) var(--sp-m);
|
||||||
|
block-size: fit-content;
|
||||||
|
inline-size: fit-content;
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
background-color: var(--color-background-warning);
|
||||||
|
border: $b-1 solid var(--color-accent-warning);
|
||||||
|
border-radius: $br-8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-button {
|
||||||
|
block-size: $sz-64;
|
||||||
|
inline-size: $sz-48;
|
||||||
|
border-radius: 0 var(--sp-s) var(--sp-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
@include t.use-typography("body-small");
|
||||||
|
border-radius: $br-8;
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
padding: var(--sp-xs) var(--sp-s);
|
||||||
|
border: 0;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: $b-1 solid var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,8 +43,8 @@
|
|||||||
(def ^:private go-settings-subscription
|
(def ^:private go-settings-subscription
|
||||||
#(st/emit! (rt/nav :settings-subscription)))
|
#(st/emit! (rt/nav :settings-subscription)))
|
||||||
|
|
||||||
(def ^:private go-settings-access-tokens
|
(def ^:private go-settings-integrations
|
||||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
#(st/emit! (rt/nav :settings-integrations)))
|
||||||
|
|
||||||
(def ^:private go-settings-notifications
|
(def ^:private go-settings-notifications
|
||||||
#(st/emit! (rt/nav :settings-notifications)))
|
#(st/emit! (rt/nav :settings-notifications)))
|
||||||
@ -66,7 +66,7 @@
|
|||||||
options? (= section :settings-options)
|
options? (= section :settings-options)
|
||||||
feedback? (= section :settings-feedback)
|
feedback? (= section :settings-feedback)
|
||||||
subscription? (= section :settings-subscription)
|
subscription? (= section :settings-subscription)
|
||||||
access-tokens? (= section :settings-access-tokens)
|
integrations? (= section :settings-integrations)
|
||||||
notifications? (= section :settings-notifications)
|
notifications? (= section :settings-notifications)
|
||||||
team-id (or (dtm/get-last-team-id)
|
team-id (or (dtm/get-last-team-id)
|
||||||
(:default-team-id profile))
|
(:default-team-id profile))
|
||||||
@ -115,12 +115,13 @@
|
|||||||
:data-testid "settings-subscription"}
|
:data-testid "settings-subscription"}
|
||||||
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
|
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
|
||||||
|
|
||||||
(when (contains? cf/flags :access-tokens)
|
(when (or (contains? cf/flags :access-tokens)
|
||||||
[:li {:class (stl/css-case :current access-tokens?
|
(contains? cf/flags :mcp))
|
||||||
|
[:li {:class (stl/css-case :current integrations?
|
||||||
:settings-item true)
|
:settings-item true)
|
||||||
:on-click go-settings-access-tokens
|
:on-click go-settings-integrations
|
||||||
:data-testid "settings-access-tokens"}
|
:data-testid "settings-integrations"}
|
||||||
[:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
|
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]])
|
||||||
|
|
||||||
[:hr {:class (stl/css :sidebar-separator)}]
|
[:hr {:class (stl/css :sidebar-separator)}]
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.context :as ctx]
|
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
[app.main.ui.icons :as deprecated-icon]
|
||||||
[app.main.ui.workspace.main-menu :as main-menu]
|
[app.main.ui.workspace.main-menu :as main-menu]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
@ -27,12 +26,10 @@
|
|||||||
;; --- Header Component
|
;; --- Header Component
|
||||||
|
|
||||||
(mf/defc left-header*
|
(mf/defc left-header*
|
||||||
[{:keys [file layout project page-id class]}]
|
[{:keys [file layout project class]}]
|
||||||
(let [profile (mf/deref refs/profile)
|
(let [file-id (:id file)
|
||||||
file-id (:id file)
|
|
||||||
file-name (:name file)
|
file-name (:name file)
|
||||||
project-id (:id project)
|
project-id (:id project)
|
||||||
team-id (:team-id project)
|
|
||||||
shared? (:is-shared file)
|
shared? (:is-shared file)
|
||||||
persistence
|
persistence
|
||||||
(mf/deref refs/persistence)
|
(mf/deref refs/persistence)
|
||||||
@ -40,8 +37,6 @@
|
|||||||
persistence-status
|
persistence-status
|
||||||
(get persistence :status)
|
(get persistence :status)
|
||||||
|
|
||||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
|
||||||
|
|
||||||
editing* (mf/use-state false)
|
editing* (mf/use-state false)
|
||||||
editing? (deref editing*)
|
editing? (deref editing*)
|
||||||
input-ref (mf/use-ref nil)
|
input-ref (mf/use-ref nil)
|
||||||
@ -137,10 +132,5 @@
|
|||||||
(when ^boolean shared?
|
(when ^boolean shared?
|
||||||
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
|
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
|
||||||
[:div {:class (stl/css :menu-section)}
|
[:div {:class (stl/css :menu-section)}
|
||||||
[:& main-menu/menu
|
[:> main-menu/menu* {:layout layout
|
||||||
{:layout layout
|
:file file}]]]))
|
||||||
:file file
|
|
||||||
:profile profile
|
|
||||||
:read-only? read-only?
|
|
||||||
:team-id team-id
|
|
||||||
:page-id page-id}]]]))
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4,125 +4,178 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS INC
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "ds/typography.scss" as t;
|
||||||
|
@use "ds/z-index.scss" as *;
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
|
.base-menu {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
padding: var(--sp-xs);
|
||||||
|
border-radius: $br-8;
|
||||||
|
z-index: var(--z-index-dropdown);
|
||||||
|
background-color: var(--menu-background-color);
|
||||||
|
border: $b-2 solid var(--panel-border-color);
|
||||||
|
box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
@extend .menu-dropdown;
|
top: $sz-48;
|
||||||
top: deprecated.$s-48;
|
left: calc(var(--right-sidebar-width) - $sz-40);
|
||||||
left: calc(var(--right-sidebar-width, deprecated.$s-256) - deprecated.$s-16);
|
inline-size: $sz-192;
|
||||||
width: deprecated.$s-192;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
@extend .menu-item-base;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.open-arrow {
|
|
||||||
@include deprecated.flexCenter;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend .button-icon;
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--menu-foreground-color-hover);
|
|
||||||
|
|
||||||
.open-arrow {
|
|
||||||
svg {
|
|
||||||
stroke: var(--menu-foreground-color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-key {
|
|
||||||
color: var(--menu-shortcut-foreground-color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
border-top: deprecated.$s-1 solid var(--color-background-quaternary);
|
|
||||||
height: deprecated.$s-4;
|
|
||||||
left: calc(-1 * deprecated.$s-4);
|
|
||||||
margin-top: deprecated.$s-8;
|
|
||||||
position: relative;
|
|
||||||
width: calc(100% + deprecated.$s-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut {
|
|
||||||
@extend .shortcut-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-key {
|
|
||||||
@extend .shortcut-key-base;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-menu {
|
.sub-menu {
|
||||||
@extend .menu-dropdown;
|
left: calc(var(--right-sidebar-width) + $sz-154);
|
||||||
left: calc(var(--right-sidebar-width, deprecated.$s-256) + deprecated.$s-180);
|
min-width: $sz-284;
|
||||||
width: deprecated.$s-192;
|
width: 115%;
|
||||||
min-width: calc(deprecated.$s-272 - deprecated.$s-2);
|
|
||||||
width: 110%;
|
|
||||||
|
|
||||||
.submenu-item {
|
&.pos-1 {
|
||||||
@extend .menu-item-base;
|
top: calc($sz-16 + $sz-32);
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--menu-foreground-color-hover);
|
|
||||||
|
|
||||||
.shortcut-key {
|
|
||||||
color: var(--menu-shortcut-foreground-color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-disabled {
|
&.pos-2 {
|
||||||
color: var(--color-foreground-secondary);
|
top: calc($sz-16 + (2 * $sz-32));
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: default;
|
|
||||||
color: var(--color-foreground-secondary);
|
|
||||||
background-color: var(--menu-background-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.file {
|
&.pos-3 {
|
||||||
top: deprecated.$s-48;
|
top: calc($sz-16 + (3 * $sz-32));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.edit {
|
&.pos-4 {
|
||||||
top: deprecated.$s-76;
|
top: calc($sz-16 + (4 * $sz-32));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.view {
|
&.pos-5 {
|
||||||
top: deprecated.$s-116;
|
top: calc($sz-16 + (5 * $sz-32));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.preferences {
|
&.pos-6 {
|
||||||
top: deprecated.$s-148;
|
top: calc($sz-16 + (6 * $sz-32));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pos-final-5 {
|
||||||
|
top: calc($sz-32 + (5 * $sz-32));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pos-final-6 {
|
||||||
|
top: calc($sz-32 + (6 * $sz-32));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pos-final-7 {
|
||||||
|
top: calc($sz-32 + (7 * $sz-32));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.plugins {
|
&.plugins {
|
||||||
top: deprecated.$s-180;
|
max-height: calc(100vh - $sz-200);
|
||||||
max-height: calc(100vh - deprecated.$s-180);
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.help-info {
|
.base-menu-item {
|
||||||
top: deprecated.$s-232;
|
@include t.use-typography("body-small");
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto $sz-16 $sz-16;
|
||||||
|
grid-template-areas: "name indicator arrow";
|
||||||
|
block-size: $sz-28;
|
||||||
|
inline-size: 100%;
|
||||||
|
padding: $sz-6;
|
||||||
|
border-radius: $br-8;
|
||||||
|
color: var(--menu-foreground-color);
|
||||||
|
background-color: var(--menu-background-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--menu-foreground-color: var(--menu-foreground-color-hover);
|
||||||
|
--menu-background-color: var(--menu-background-color-hover);
|
||||||
|
--menu-shortcut-foreground-color: var(--menu-shortcut-foreground-color-hover);
|
||||||
|
--menu-icon-foreground-color: var(--menu-foreground-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.help-info-old {
|
&.disabled {
|
||||||
top: deprecated.$s-192;
|
--menu-foreground-color: var(--color-foreground-secondary);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto $sz-16 $sz-16;
|
||||||
|
grid-template-areas: "name indicator arrow";
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
grid-area: name;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-indicator {
|
||||||
|
--menu-indicator-color: var(--color-foreground-secondary);
|
||||||
|
grid-area: indicator;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
inline-size: px2rem(8);
|
||||||
|
block-size: px2rem(8);
|
||||||
|
border-radius: $br-circle;
|
||||||
|
background-color: var(--menu-indicator-color);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
--menu-indicator-color: var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.failed {
|
||||||
|
--menu-indicator-color: var(--color-foreground-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
grid-area: arrow;
|
||||||
|
color: var(--menu-icon-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
.item-icon {
|
.item-icon {
|
||||||
svg {
|
color: var(--menu-icon-foreground-color);
|
||||||
@extend .button-icon;
|
display: flex;
|
||||||
stroke: var(--icon-foreground);
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
position: relative;
|
||||||
|
block-size: var(--sp-xs);
|
||||||
|
inline-size: calc(100% + var(--sp-s));
|
||||||
|
border-top: $b-1 solid var(--color-background-quaternary);
|
||||||
|
left: calc(-1 * var(--sp-xs));
|
||||||
|
margin-top: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--sp-xxs);
|
||||||
|
color: var(--menu-shortcut-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-key {
|
||||||
|
@include t.use-typography("body-small");
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: px2rem(20);
|
||||||
|
padding: var(--sp-xxs) px2rem(6);
|
||||||
|
border-radius: $br-6;
|
||||||
|
background-color: var(--menu-shortcut-background-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
(mf/defc left-sidebar*
|
(mf/defc left-sidebar*
|
||||||
{::mf/memo true}
|
{::mf/memo true}
|
||||||
[{:keys [layout file page-id tokens-lib active-tokens resolved-active-tokens]}]
|
[{:keys [layout file tokens-lib active-tokens resolved-active-tokens]}]
|
||||||
(let [options-mode (mf/deref refs/options-mode-global)
|
(let [options-mode (mf/deref refs/options-mode-global)
|
||||||
project (mf/deref refs/project)
|
project (mf/deref refs/project)
|
||||||
file-id (get file :id)
|
file-id (get file :id)
|
||||||
@ -185,12 +185,10 @@
|
|||||||
:class aside-class
|
:class aside-class
|
||||||
:style {:--left-sidebar-width (dm/str width "px")}}
|
:style {:--left-sidebar-width (dm/str width "px")}}
|
||||||
|
|
||||||
[:> left-header*
|
[:> left-header* {:file file
|
||||||
{:file file
|
:layout layout
|
||||||
:layout layout
|
:project project
|
||||||
:project project
|
:class (stl/css :left-header)}]
|
||||||
:page-id page-id
|
|
||||||
:class (stl/css :left-header)}]
|
|
||||||
|
|
||||||
[:div {:on-pointer-down on-pointer-down
|
[:div {:on-pointer-down on-pointer-down
|
||||||
:on-lost-pointer-capture on-lost-pointer-capture
|
:on-lost-pointer-capture on-lost-pointer-capture
|
||||||
|
|||||||
@ -291,7 +291,7 @@
|
|||||||
:r4 "Bottom Left"
|
:r4 "Bottom Left"
|
||||||
:r3 "Bottom Right"}
|
:r3 "Bottom Right"}
|
||||||
:hint (tr "workspace.tokens.radius")
|
:hint (tr "workspace.tokens.radius")
|
||||||
:on-update-shape-all dwta/update-shape-radius-all
|
:on-update-shape-all dwta/update-shape-radius
|
||||||
:on-update-shape update-shape-radius-for-corners})
|
:on-update-shape update-shape-radius-for-corners})
|
||||||
shadow (partial generic-attribute-actions #{:shadow} "Shadow")]
|
shadow (partial generic-attribute-actions #{:shadow} "Shadow")]
|
||||||
{:border-radius border-radius
|
{:border-radius border-radius
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"RPC for plugins runtime."
|
"RPC for plugins runtime."
|
||||||
(:require
|
(:require
|
||||||
["@penpot/plugins-runtime" :as runtime]
|
["@penpot/plugins-runtime" :as runtime]
|
||||||
|
[app.main.errors :as errors]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.api :as api]
|
[app.plugins.api :as api]
|
||||||
@ -30,6 +31,8 @@
|
|||||||
(ptk/reify ::initialize
|
(ptk/reify ::initialize
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ stream]
|
(watch [_ _ stream]
|
||||||
|
(set! errors/is-plugin-error? runtime/isPluginError)
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::features/initialize))
|
(rx/filter (ptk/type? ::features/initialize))
|
||||||
(rx/observe-on :async)
|
(rx/observe-on :async)
|
||||||
|
|||||||
@ -14,9 +14,11 @@
|
|||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.color :as ctc]
|
[app.common.types.color :as ctc]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.changes :as ch]
|
[app.main.data.changes :as ch]
|
||||||
[app.main.data.common :as dcm]
|
[app.main.data.common :as dcm]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
@ -26,6 +28,7 @@
|
|||||||
[app.main.data.workspace.groups :as dwg]
|
[app.main.data.workspace.groups :as dwg]
|
||||||
[app.main.data.workspace.media :as dwm]
|
[app.main.data.workspace.media :as dwm]
|
||||||
[app.main.data.workspace.selection :as dws]
|
[app.main.data.workspace.selection :as dws]
|
||||||
|
[app.main.data.workspace.variants :as dwv]
|
||||||
[app.main.data.workspace.wasm-text :as dwwt]
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.fonts :refer [fetch-font-css]]
|
[app.main.fonts :refer [fetch-font-css]]
|
||||||
@ -82,6 +85,10 @@
|
|||||||
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
||||||
|
|
||||||
;; Public properties
|
;; Public properties
|
||||||
|
:version
|
||||||
|
{:this true
|
||||||
|
:get (constantly (:base cf/version))}
|
||||||
|
|
||||||
:root
|
:root
|
||||||
{:this true
|
{:this true
|
||||||
:get #(.getRoot ^js %)}
|
:get #(.getRoot ^js %)}
|
||||||
@ -110,7 +117,7 @@
|
|||||||
(fn [_ shapes]
|
(fn [_ shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :selection shapes)
|
(u/not-valid plugin-id :selection shapes)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into (d/ordered-set) (map #(obj/get % "$id")) shapes)]
|
(let [ids (into (d/ordered-set) (map #(obj/get % "$id")) shapes)]
|
||||||
@ -175,7 +182,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :shapesColors-shapes shapes)
|
(u/not-valid plugin-id :shapesColors-shapes shapes)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [objects (u/locate-objects)
|
(let [objects (u/locate-objects)
|
||||||
@ -195,13 +202,13 @@
|
|||||||
new-color (parser/parse-color-data new-color)]
|
new-color (parser/parse-color-data new-color)]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :replaceColor-shapes shapes)
|
(u/not-valid plugin-id :replaceColor-shapes shapes)
|
||||||
|
|
||||||
(not (sm/validate ctc/schema:color old-color))
|
(not (sm/validate ctc/schema:color old-color))
|
||||||
(u/display-not-valid :replaceColor-oldColor old-color)
|
(u/not-valid plugin-id :replaceColor-oldColor old-color)
|
||||||
|
|
||||||
(not (sm/validate ctc/schema:color new-color))
|
(not (sm/validate ctc/schema:color new-color))
|
||||||
(u/display-not-valid :replaceColor-newColor new-color)
|
(u/not-valid plugin-id :replaceColor-newColor new-color)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file-id (:current-file-id @st/state)
|
(let [file-id (:current-file-id @st/state)
|
||||||
@ -254,10 +261,10 @@
|
|||||||
(fn [name url]
|
(fn [name url]
|
||||||
(cond
|
(cond
|
||||||
(not (string? name))
|
(not (string? name))
|
||||||
(u/display-not-valid :uploadMedia-name name)
|
(u/not-valid plugin-id :uploadMedia-name name)
|
||||||
|
|
||||||
(not (string? url))
|
(not (string? url))
|
||||||
(u/display-not-valid :uploadMedia-url url)
|
(u/not-valid plugin-id :uploadMedia-url url)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file-id (:current-file-id @st/state)]
|
(let [file-id (:current-file-id @st/state)]
|
||||||
@ -288,7 +295,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :group-shapes shapes)
|
(u/not-valid plugin-id :group-shapes shapes)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file-id (:current-file-id @st/state)
|
(let [file-id (:current-file-id @st/state)
|
||||||
@ -303,10 +310,10 @@
|
|||||||
(fn [group & rest]
|
(fn [group & rest]
|
||||||
(cond
|
(cond
|
||||||
(not (shape/shape-proxy? group))
|
(not (shape/shape-proxy? group))
|
||||||
(u/display-not-valid :ungroup group)
|
(u/not-valid plugin-id :ungroup group)
|
||||||
|
|
||||||
(and (some? rest) (not (every? shape/shape-proxy? rest)))
|
(and (some? rest) (not (every? shape/shape-proxy? rest)))
|
||||||
(u/display-not-valid :ungroup rest)
|
(u/not-valid plugin-id :ungroup rest)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shapes (concat [group] rest)
|
(let [shapes (concat [group] rest)
|
||||||
@ -346,7 +353,7 @@
|
|||||||
(fn [text]
|
(fn [text]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? text)) (empty? text))
|
(or (not (string? text)) (empty? text))
|
||||||
(u/display-not-valid :createText text)
|
(u/not-valid plugin-id :createText text)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [page (dsh/lookup-page @st/state)
|
(let [page (dsh/lookup-page @st/state)
|
||||||
@ -377,7 +384,7 @@
|
|||||||
(fn [svg-string]
|
(fn [svg-string]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? svg-string)) (empty? svg-string))
|
(or (not (string? svg-string)) (empty? svg-string))
|
||||||
(u/display-not-valid :createShapeFromSvg svg-string)
|
(u/not-valid plugin-id :createShapeFromSvg svg-string)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (uuid/next)
|
(let [id (uuid/next)
|
||||||
@ -394,7 +401,7 @@
|
|||||||
(cond
|
(cond
|
||||||
(or (not (string? svg-string)) (empty? svg-string))
|
(or (not (string? svg-string)) (empty? svg-string))
|
||||||
(do
|
(do
|
||||||
(u/display-not-valid :createShapeFromSvg "Svg not valid")
|
(u/not-valid plugin-id :createShapeFromSvg "Svg not valid")
|
||||||
(reject "Svg not valid"))
|
(reject "Svg not valid"))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
@ -412,10 +419,10 @@
|
|||||||
(let [bool-type (keyword bool-type)]
|
(let [bool-type (keyword bool-type)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? cts/bool-types bool-type))
|
(not (contains? cts/bool-types bool-type))
|
||||||
(u/display-not-valid :createBoolean-boolType bool-type)
|
(u/not-valid plugin-id :createBoolean-boolType bool-type)
|
||||||
|
|
||||||
(or (not (array? shapes)) (empty? shapes) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (empty? shapes) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :createBoolean-shapes shapes)
|
(u/not-valid plugin-id :createBoolean-shapes shapes)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
|
||||||
@ -429,10 +436,10 @@
|
|||||||
(let [type (d/nilv (obj/get options "type") "html")]
|
(let [type (d/nilv (obj/get options "type") "html")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :generateMarkup-shapes shapes)
|
(u/not-valid plugin-id :generateMarkup-shapes shapes)
|
||||||
|
|
||||||
(and (some? type) (not (contains? #{"html" "svg"} type)))
|
(and (some? type) (not (contains? #{"html" "svg"} type)))
|
||||||
(u/display-not-valid :generateMarkup-type type)
|
(u/not-valid plugin-id :generateMarkup-type type)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [resolved-code
|
(let [resolved-code
|
||||||
@ -464,16 +471,16 @@
|
|||||||
children? (d/nilv (obj/get options "includeChildren") true)]
|
children? (d/nilv (obj/get options "includeChildren") true)]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :generateStyle-shapes shapes)
|
(u/not-valid plugin-id :generateStyle-shapes shapes)
|
||||||
|
|
||||||
(and (some? type) (not (contains? #{"css"} type)))
|
(and (some? type) (not (contains? #{"css"} type)))
|
||||||
(u/display-not-valid :generateStyle-type type)
|
(u/not-valid plugin-id :generateStyle-type type)
|
||||||
|
|
||||||
(and (some? prelude?) (not (boolean? prelude?)))
|
(and (some? prelude?) (not (boolean? prelude?)))
|
||||||
(u/display-not-valid :generateStyle-withPrelude prelude?)
|
(u/not-valid plugin-id :generateStyle-withPrelude prelude?)
|
||||||
|
|
||||||
(and (some? children?) (not (boolean? children?)))
|
(and (some? children?) (not (boolean? children?)))
|
||||||
(u/display-not-valid :generateStyle-includeChildren children?)
|
(u/not-valid plugin-id :generateStyle-includeChildren children?)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [resolved-styles
|
(let [resolved-styles
|
||||||
@ -546,7 +553,7 @@
|
|||||||
:else nil)
|
:else nil)
|
||||||
new-window (if (boolean? new-window) new-window false)]
|
new-window (if (boolean? new-window) new-window false)]
|
||||||
(if (nil? id)
|
(if (nil? id)
|
||||||
(u/display-not-valid :openPage "Expected a Page object or a page UUID string")
|
(u/not-valid plugin-id :openPage "Expected a Page object or a page UUID string")
|
||||||
(st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window)))))
|
(st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window)))))
|
||||||
|
|
||||||
:alignHorizontal
|
:alignHorizontal
|
||||||
@ -558,10 +565,10 @@
|
|||||||
nil)]
|
nil)]
|
||||||
(cond
|
(cond
|
||||||
(nil? dir)
|
(nil? dir)
|
||||||
(u/display-not-valid :alignHorizontal-direction "Direction not valid")
|
(u/not-valid plugin-id :alignHorizontal-direction "Direction not valid")
|
||||||
|
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :alignHorizontal-shapes "Not valid shapes")
|
(u/not-valid plugin-id :alignHorizontal-shapes "Not valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||||
@ -576,10 +583,10 @@
|
|||||||
nil)]
|
nil)]
|
||||||
(cond
|
(cond
|
||||||
(nil? dir)
|
(nil? dir)
|
||||||
(u/display-not-valid :alignVertical-direction "Direction not valid")
|
(u/not-valid plugin-id :alignVertical-direction "Direction not valid")
|
||||||
|
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :alignVertical-shapes "Not valid shapes")
|
(u/not-valid plugin-id :alignVertical-shapes "Not valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||||
@ -589,7 +596,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :distributeHorizontal-shapes "Not valid shapes")
|
(u/not-valid plugin-id :distributeHorizontal-shapes "Not valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||||
@ -599,7 +606,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :distributeVertical-shapes "Not valid shapes")
|
(u/not-valid plugin-id :distributeVertical-shapes "Not valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||||
@ -609,8 +616,41 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||||
(u/display-not-valid :flatten-shapes "Not valid shapes")
|
(u/not-valid plugin-id :flatten-shapes "Not valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||||
(st/emit! (dw/convert-selected-to-path ids)))))))
|
(st/emit! (dw/convert-selected-to-path ids)))))
|
||||||
|
|
||||||
|
:createVariantFromComponents
|
||||||
|
(fn [shapes]
|
||||||
|
(cond
|
||||||
|
(or (not (seq shapes))
|
||||||
|
(not (every? u/is-main-component-proxy? shapes)))
|
||||||
|
(u/not-valid plugin-id :shapes shapes)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [file-id (obj/get (first shapes) "$file")
|
||||||
|
page-id (obj/get (first shapes) "$page")
|
||||||
|
ids (->> shapes
|
||||||
|
(map #(obj/get % "$id"))
|
||||||
|
(into #{}))
|
||||||
|
|
||||||
|
;; Check that every component is:
|
||||||
|
;; - in the same page
|
||||||
|
;; - not already a variant
|
||||||
|
valid?
|
||||||
|
(every?
|
||||||
|
(fn [id]
|
||||||
|
(let [shape (u/locate-shape file-id page-id id)
|
||||||
|
component (u/locate-library-component file-id (:component-id shape))]
|
||||||
|
(not (ctk/is-variant? component))))
|
||||||
|
ids)]
|
||||||
|
(if valid?
|
||||||
|
(let [variant-id (uuid/next)]
|
||||||
|
(st/emit! (dwv/combine-as-variants
|
||||||
|
ids
|
||||||
|
{:trigger "plugin:combine-as-variants" :variant-id variant-id}))
|
||||||
|
(shape/shape-proxy plugin-id variant-id))
|
||||||
|
|
||||||
|
(u/not-valid plugin-id :shapes "One of the components is not on the same page or is already a variant")))))))
|
||||||
|
|||||||
@ -60,13 +60,13 @@
|
|||||||
(let [profile (:profile @st/state)]
|
(let [profile (:profile @st/state)]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? content)) (empty? content))
|
(or (not (string? content)) (empty? content))
|
||||||
(u/display-not-valid :content "Not valid")
|
(u/not-valid plugin-id :content "Not valid")
|
||||||
|
|
||||||
(not= (:id profile) (:owner-id data))
|
(not= (:id profile) (:owner-id data))
|
||||||
(u/display-not-valid :content "Cannot change content from another user's comments")
|
(u/not-valid plugin-id :content "Cannot change content from another user's comments")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :content "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :content "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(->> (rp/cmd! :update-comment {:id (:id data) :content content})
|
(->> (rp/cmd! :update-comment {:id (:id data) :content content})
|
||||||
@ -81,7 +81,7 @@
|
|||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(do
|
(do
|
||||||
(u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
|
||||||
(reject "Plugin doesn't have 'comment:write' permission"))
|
(reject "Plugin doesn't have 'comment:write' permission"))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
@ -120,10 +120,10 @@
|
|||||||
(cond
|
(cond
|
||||||
(or (not (sm/valid-safe-number? (:x position)))
|
(or (not (sm/valid-safe-number? (:x position)))
|
||||||
(not (sm/valid-safe-number? (:y position))))
|
(not (sm/valid-safe-number? (:y position))))
|
||||||
(u/display-not-valid :position "Not valid point")
|
(u/not-valid plugin-id :position "Not valid point")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :position "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :position "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)]))
|
(do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)]))
|
||||||
@ -137,10 +137,10 @@
|
|||||||
(fn [is-resolved]
|
(fn [is-resolved]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? is-resolved))
|
(not (boolean? is-resolved))
|
||||||
(u/display-not-valid :resolved "Not a boolean type")
|
(u/not-valid plugin-id :resolved "Not a boolean type")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :resolved "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :resolved "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved)))
|
(do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved)))
|
||||||
@ -153,7 +153,7 @@
|
|||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "comment:read"))
|
(not (r/check-permission plugin-id "comment:read"))
|
||||||
(do
|
(do
|
||||||
(u/display-not-valid :findComments "Plugin doesn't have 'comment:read' permission")
|
(u/not-valid plugin-id :findComments "Plugin doesn't have 'comment:read' permission")
|
||||||
(reject "Plugin doesn't have 'comment:read' permission"))
|
(reject "Plugin doesn't have 'comment:read' permission"))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
@ -169,10 +169,10 @@
|
|||||||
(fn [content]
|
(fn [content]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :reply "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :reply "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
(or (not (string? content)) (empty? content))
|
(or (not (string? content)) (empty? content))
|
||||||
(u/display-not-valid :reply "Not valid")
|
(u/not-valid plugin-id :reply "Not valid")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
@ -186,10 +186,10 @@
|
|||||||
owner (dsh/lookup-profile @st/state (:owner-id data))]
|
owner (dsh/lookup-profile @st/state (:owner-id data))]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
(not= (:id profile) owner)
|
(not= (:id profile) owner)
|
||||||
(u/display-not-valid :remove "Cannot change content from another user's comments")
|
(u/not-valid plugin-id :remove "Cannot change content from another user's comments")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
|
|||||||
@ -45,10 +45,10 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :label "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :label "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(or (not (string? value)) (empty? value))
|
(or (not (string? value)) (empty? value))
|
||||||
(u/display-not-valid :label value)
|
(u/not-valid plugin-id :label value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (swap! data assoc :label value :created-by "user")
|
(do (swap! data assoc :label value :created-by "user")
|
||||||
@ -145,7 +145,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getPluginData-key key)
|
(u/not-valid plugin-id :getPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file id)]
|
(let [file (u/locate-file id)]
|
||||||
@ -155,13 +155,13 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? key)) (empty? key))
|
(or (not (string? key)) (empty? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data id :file (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data id :file (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -175,10 +175,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getSharedPluginData-key key)
|
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file id)]
|
(let [file (u/locate-file id)]
|
||||||
@ -188,16 +188,16 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? namespace)) (empty? namespace))
|
(or (not (string? namespace)) (empty? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(or (not (string? key)) (empty? key))
|
(or (not (string? key)) (empty? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data id :file (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data id :file (keyword "shared" namespace) key value))))
|
||||||
@ -206,7 +206,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginDataKeys namespace)
|
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file id)]
|
(let [file (u/locate-file id)]
|
||||||
@ -216,7 +216,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :createPage "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :createPage "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [page-id (uuid/next)]
|
(let [page-id (uuid/next)]
|
||||||
|
|||||||
@ -6,17 +6,11 @@
|
|||||||
|
|
||||||
(ns app.plugins.flags
|
(ns app.plugins.flags
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.utils :as u]
|
[app.plugins.utils :as u]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(defn natural-child-ordering?
|
|
||||||
[plugin-id]
|
|
||||||
(boolean
|
|
||||||
(dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering])))
|
|
||||||
|
|
||||||
(defn clear
|
(defn clear
|
||||||
[id]
|
[id]
|
||||||
(ptk/reify ::reset
|
(ptk/reify ::reset
|
||||||
@ -37,13 +31,27 @@
|
|||||||
:naturalChildOrdering
|
:naturalChildOrdering
|
||||||
{:this false
|
{:this false
|
||||||
:get
|
:get
|
||||||
(fn [] (natural-child-ordering? plugin-id))
|
(fn [] (u/natural-child-ordering? plugin-id))
|
||||||
|
|
||||||
:set
|
:set
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :naturalChildOrdering value)
|
(u/not-valid plugin-id :naturalChildOrdering value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (set-flag plugin-id :natural-child-ordering value))))}))
|
(st/emit! (set-flag plugin-id :natural-child-ordering value))))}
|
||||||
|
|
||||||
|
:throwValidationErrors
|
||||||
|
{:this false
|
||||||
|
:get
|
||||||
|
(fn [] (u/throw-validation-errors? plugin-id))
|
||||||
|
|
||||||
|
:set
|
||||||
|
(fn [value]
|
||||||
|
(cond
|
||||||
|
(not (boolean? value))
|
||||||
|
(u/not-valid plugin-id :throwValidationErrors value)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(st/emit! (set-flag plugin-id :throw-validation-errors value))))}))
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
[app.main.data.workspace.shape-layout :as dwsl]
|
[app.main.data.workspace.shape-layout :as dwsl]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.flags :refer [natural-child-ordering?]]
|
|
||||||
[app.plugins.register :as r]
|
[app.plugins.register :as r]
|
||||||
[app.plugins.utils :as u]
|
[app.plugins.utils :as u]
|
||||||
[app.util.object :as obj]))
|
[app.util.object :as obj]))
|
||||||
@ -39,10 +38,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/flex-direction-types value))
|
(not (contains? ctl/flex-direction-types value))
|
||||||
(u/display-not-valid :dir value)
|
(u/not-valid plugin-id :dir value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :dir "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-flex-dir value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-flex-dir value})))))}
|
||||||
@ -55,10 +54,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/wrap-types value))
|
(not (contains? ctl/wrap-types value))
|
||||||
(u/display-not-valid :wrap value)
|
(u/not-valid plugin-id :wrap value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :wrap "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :wrap "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-wrap-type value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-wrap-type value})))))}
|
||||||
@ -71,10 +70,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/align-items-types value))
|
(not (contains? ctl/align-items-types value))
|
||||||
(u/display-not-valid :alignItems value)
|
(u/not-valid plugin-id :alignItems value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
||||||
@ -87,10 +86,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/align-content-types value))
|
(not (contains? ctl/align-content-types value))
|
||||||
(u/display-not-valid :alignContent value)
|
(u/not-valid plugin-id :alignContent value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
||||||
@ -103,10 +102,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/justify-items-types value))
|
(not (contains? ctl/justify-items-types value))
|
||||||
(u/display-not-valid :justifyItems value)
|
(u/not-valid plugin-id :justifyItems value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
||||||
@ -119,10 +118,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/justify-content-types value))
|
(not (contains? ctl/justify-content-types value))
|
||||||
(u/display-not-valid :justifyContent value)
|
(u/not-valid plugin-id :justifyContent value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
||||||
@ -134,10 +133,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :rowGap value)
|
(u/not-valid plugin-id :rowGap value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
||||||
@ -149,10 +148,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :columnGap value)
|
(u/not-valid plugin-id :columnGap value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
||||||
@ -164,10 +163,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :verticalPadding value)
|
(u/not-valid plugin-id :verticalPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
||||||
@ -179,10 +178,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :horizontalPadding value)
|
(u/not-valid plugin-id :horizontalPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
||||||
@ -195,10 +194,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :topPadding value)
|
(u/not-valid plugin-id :topPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
||||||
@ -210,10 +209,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :rightPadding value)
|
(u/not-valid plugin-id :rightPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rightPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rightPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
||||||
@ -225,10 +224,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :bottomPadding value)
|
(u/not-valid plugin-id :bottomPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
||||||
@ -240,10 +239,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :leftPadding value)
|
(u/not-valid plugin-id :leftPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
||||||
@ -256,13 +255,13 @@
|
|||||||
(fn [child]
|
(fn [child]
|
||||||
(cond
|
(cond
|
||||||
(not (shape-proxy? child))
|
(not (shape-proxy? child))
|
||||||
(u/display-not-valid :appendChild child)
|
(u/not-valid plugin-id :appendChild child)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [child-id (obj/get child "$id")
|
(let [child-id (obj/get child "$id")
|
||||||
shape (u/locate-shape file-id page-id id)
|
shape (u/locate-shape file-id page-id id)
|
||||||
index
|
index
|
||||||
(if (and (natural-child-ordering? plugin-id) (not (ctl/reverse? shape)))
|
(if (and (u/natural-child-ordering? plugin-id) (not (ctl/reverse? shape)))
|
||||||
0
|
0
|
||||||
(count (:shapes shape)))]
|
(count (:shapes shape)))]
|
||||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index)))))
|
(st/emit! (dwsh/relocate-shapes #{child-id} id index)))))
|
||||||
@ -275,10 +274,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/item-h-sizing-types value))
|
(not (contains? ctl/item-h-sizing-types value))
|
||||||
(u/display-not-valid :horizontalSizing value)
|
(u/not-valid plugin-id :horizontalSizing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
||||||
@ -291,10 +290,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/item-v-sizing-types value))
|
(not (contains? ctl/item-v-sizing-types value))
|
||||||
(u/display-not-valid :verticalSizing value)
|
(u/not-valid plugin-id :verticalSizing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}))
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}))
|
||||||
@ -317,10 +316,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :absolute value)
|
(u/not-valid plugin-id :absolute value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :absolute "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :absolute "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-absolute value}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-absolute value}))))}
|
||||||
@ -332,10 +331,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(sm/valid-safe-int? value)
|
(sm/valid-safe-int? value)
|
||||||
(u/display-not-valid :zIndex value)
|
(u/not-valid plugin-id :zIndex value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :zIndex "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :zIndex "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-z-index value}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-z-index value}))))}
|
||||||
@ -348,10 +347,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/item-h-sizing-types value))
|
(not (contains? ctl/item-h-sizing-types value))
|
||||||
(u/display-not-valid :horizontalPadding value)
|
(u/not-valid plugin-id :horizontalPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
|
||||||
@ -364,10 +363,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/item-v-sizing-types value))
|
(not (contains? ctl/item-v-sizing-types value))
|
||||||
(u/display-not-valid :verticalSizing value)
|
(u/not-valid plugin-id :verticalSizing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}
|
||||||
@ -380,10 +379,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/item-align-self-types value))
|
(not (contains? ctl/item-align-self-types value))
|
||||||
(u/display-not-valid :alignSelf value)
|
(u/not-valid plugin-id :alignSelf value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-align-self value})))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-align-self value})))))}
|
||||||
@ -395,10 +394,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :verticalMargin value)
|
(u/not-valid plugin-id :verticalMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))}
|
||||||
@ -410,10 +409,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :horizontalMargin value)
|
(u/not-valid plugin-id :horizontalMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))}
|
||||||
@ -425,10 +424,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :topMargin value)
|
(u/not-valid plugin-id :topMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :topMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :topMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))}
|
||||||
@ -440,10 +439,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :rightMargin value)
|
(u/not-valid plugin-id :rightMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rightMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rightMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))}
|
||||||
@ -455,10 +454,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :bottomMargin value)
|
(u/not-valid plugin-id :bottomMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :bottomMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :bottomMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))}
|
||||||
@ -470,10 +469,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :leftMargin value)
|
(u/not-valid plugin-id :leftMargin value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :leftMargin "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :leftMargin "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))}
|
||||||
@ -485,10 +484,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :maxWidth value)
|
(u/not-valid plugin-id :maxWidth value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :maxWidth "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :maxWidth "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-w value}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-w value}))))}
|
||||||
@ -500,10 +499,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :minWidth value)
|
(u/not-valid plugin-id :minWidth value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :minWidth "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :minWidth "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-w value}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-w value}))))}
|
||||||
@ -515,10 +514,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :maxHeight value)
|
(u/not-valid plugin-id :maxHeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :maxHeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :maxHeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-h value}))))}
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-h value}))))}
|
||||||
@ -530,10 +529,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :minHeight value)
|
(u/not-valid plugin-id :minHeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :minHeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :minHeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-h value}))))}))
|
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-h value}))))}))
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
(obj/type-of? p "FontProxy"))
|
(obj/type-of? p "FontProxy"))
|
||||||
|
|
||||||
(defn font-proxy
|
(defn font-proxy
|
||||||
[{:keys [id family name variants] :as font}]
|
[plugin-id {:keys [id family name variants] :as font}]
|
||||||
(when (some? font)
|
(when (some? font)
|
||||||
(let [default-variant (fonts/get-default-variant font)]
|
(let [default-variant (fonts/get-default-variant font)]
|
||||||
(obj/reify {:name "FontProxy"}
|
(obj/reify {:name "FontProxy"}
|
||||||
@ -55,10 +55,10 @@
|
|||||||
(fn [text variant]
|
(fn [text variant]
|
||||||
(cond
|
(cond
|
||||||
(not (shape/shape-proxy? text))
|
(not (shape/shape-proxy? text))
|
||||||
(u/display-not-valid :applyToText text)
|
(u/not-valid plugin-id :applyToText text)
|
||||||
|
|
||||||
(not (r/check-permission (obj/get text "$plugin") "content:write"))
|
(not (r/check-permission (obj/get text "$plugin") "content:write"))
|
||||||
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get text "$id")
|
(let [id (obj/get text "$id")
|
||||||
@ -73,10 +73,10 @@
|
|||||||
(fn [range variant]
|
(fn [range variant]
|
||||||
(cond
|
(cond
|
||||||
(not (text/text-range-proxy? range))
|
(not (text/text-range-proxy? range))
|
||||||
(u/display-not-valid :applyToRange range)
|
(u/not-valid plugin-id :applyToRange range)
|
||||||
|
|
||||||
(not (r/check-permission (obj/get range "$plugin") "content:write"))
|
(not (r/check-permission (obj/get range "$plugin") "content:write"))
|
||||||
(u/display-not-valid :applyToRange "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :applyToRange "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get range "$id")
|
(let [id (obj/get range "$id")
|
||||||
@ -98,53 +98,53 @@
|
|||||||
{:get
|
{:get
|
||||||
(fn []
|
(fn []
|
||||||
(format/format-array
|
(format/format-array
|
||||||
font-proxy
|
(partial font-proxy plugin-id)
|
||||||
(vals @fonts/fontsdb)))}
|
(vals @fonts/fontsdb)))}
|
||||||
|
|
||||||
:findById
|
:findById
|
||||||
(fn [id]
|
(fn [id]
|
||||||
(cond
|
(cond
|
||||||
(not (string? id))
|
(not (string? id))
|
||||||
(u/display-not-valid :findbyId id)
|
(u/not-valid plugin-id :findbyId id)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(->> (vals @fonts/fontsdb)
|
(->> (vals @fonts/fontsdb)
|
||||||
(d/seek #(str/includes? (str/lower (:id %)) (str/lower id)))
|
(d/seek #(str/includes? (str/lower (:id %)) (str/lower id)))
|
||||||
(font-proxy))))
|
(font-proxy plugin-id))))
|
||||||
|
|
||||||
:findByName
|
:findByName
|
||||||
(fn [name]
|
(fn [name]
|
||||||
(cond
|
(cond
|
||||||
(not (string? name))
|
(not (string? name))
|
||||||
(u/display-not-valid :findByName name)
|
(u/not-valid plugin-id :findByName name)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(->> (vals @fonts/fontsdb)
|
(->> (vals @fonts/fontsdb)
|
||||||
(d/seek #(str/includes? (str/lower (:name %)) (str/lower name)))
|
(d/seek #(str/includes? (str/lower (:name %)) (str/lower name)))
|
||||||
(font-proxy))))
|
(font-proxy plugin-id))))
|
||||||
|
|
||||||
:findAllById
|
:findAllById
|
||||||
(fn [id]
|
(fn [id]
|
||||||
(cond
|
(cond
|
||||||
(not (string? id))
|
(not (string? id))
|
||||||
(u/display-not-valid :findAllById name)
|
(u/not-valid plugin-id :findAllById name)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(format/format-array
|
(format/format-array
|
||||||
(fn [font]
|
(fn [font]
|
||||||
(when (str/includes? (str/lower (:id font)) (str/lower id))
|
(when (str/includes? (str/lower (:id font)) (str/lower id))
|
||||||
(font-proxy font)))
|
(font-proxy plugin-id font)))
|
||||||
(vals @fonts/fontsdb))))
|
(vals @fonts/fontsdb))))
|
||||||
|
|
||||||
:findAllByName
|
:findAllByName
|
||||||
(fn [name]
|
(fn [name]
|
||||||
(cond
|
(cond
|
||||||
(not (string? name))
|
(not (string? name))
|
||||||
(u/display-not-valid :findAllByName name)
|
(u/not-valid plugin-id :findAllByName name)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(format/format-array
|
(format/format-array
|
||||||
(fn [font]
|
(fn [font]
|
||||||
(when (str/includes? (str/lower (:name font)) (str/lower name))
|
(when (str/includes? (str/lower (:name font)) (str/lower name))
|
||||||
(font-proxy font)))
|
(font-proxy plugin-id font)))
|
||||||
(vals @fonts/fontsdb))))))
|
(vals @fonts/fontsdb))))))
|
||||||
|
|||||||
@ -598,3 +598,10 @@
|
|||||||
(case axis
|
(case axis
|
||||||
:y "horizontal"
|
:y "horizontal"
|
||||||
:x "vertical"))
|
:x "vertical"))
|
||||||
|
|
||||||
|
(defn format-geom-rect
|
||||||
|
[{:keys [x y width height]}]
|
||||||
|
#js {:x x
|
||||||
|
:y y
|
||||||
|
:width width
|
||||||
|
:height height})
|
||||||
|
|||||||
@ -40,10 +40,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-direction-types value))
|
(not (contains? ctl/grid-direction-types value))
|
||||||
(u/display-not-valid :dir value)
|
(u/not-valid plugin-id :dir value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :dir "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value})))))}
|
||||||
@ -64,10 +64,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/align-items-types value))
|
(not (contains? ctl/align-items-types value))
|
||||||
(u/display-not-valid :alignItems value)
|
(u/not-valid plugin-id :alignItems value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
||||||
@ -80,10 +80,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/align-content-types value))
|
(not (contains? ctl/align-content-types value))
|
||||||
(u/display-not-valid :alignContent value)
|
(u/not-valid plugin-id :alignContent value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
||||||
@ -96,10 +96,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/justify-items-types value))
|
(not (contains? ctl/justify-items-types value))
|
||||||
(u/display-not-valid :justifyItems value)
|
(u/not-valid plugin-id :justifyItems value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
||||||
@ -112,10 +112,10 @@
|
|||||||
(let [value (keyword value)]
|
(let [value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/justify-content-types value))
|
(not (contains? ctl/justify-content-types value))
|
||||||
(u/display-not-valid :justifyContent value)
|
(u/not-valid plugin-id :justifyContent value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
||||||
@ -127,10 +127,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :rowGap value)
|
(u/not-valid plugin-id :rowGap value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
||||||
@ -142,10 +142,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :columnGap value)
|
(u/not-valid plugin-id :columnGap value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
||||||
@ -157,10 +157,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :verticalPadding value)
|
(u/not-valid plugin-id :verticalPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
||||||
@ -172,10 +172,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :horizontalPadding value)
|
(u/not-valid plugin-id :horizontalPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
||||||
@ -187,10 +187,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :topPadding value)
|
(u/not-valid plugin-id :topPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
||||||
@ -202,10 +202,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :rightPadding value)
|
(u/not-valid plugin-id :rightPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :righPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :righPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
||||||
@ -217,10 +217,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :bottomPadding value)
|
(u/not-valid plugin-id :bottomPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
||||||
@ -232,10 +232,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :leftPadding value)
|
(u/not-valid plugin-id :leftPadding value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
||||||
@ -245,14 +245,14 @@
|
|||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :addRow-type type)
|
(u/not-valid plugin-id :addRow-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :addRow-value value)
|
(u/not-valid plugin-id :addRow-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addRow "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addRow "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value})))))
|
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value})))))
|
||||||
@ -262,17 +262,17 @@
|
|||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :addRowAtIndex-index index)
|
(u/not-valid plugin-id :addRowAtIndex-index index)
|
||||||
|
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :addRowAtIndex-type type)
|
(u/not-valid plugin-id :addRowAtIndex-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :addRowAtIndex-value value)
|
(u/not-valid plugin-id :addRowAtIndex-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addRowAtIndex "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addRowAtIndex "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index)))))
|
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index)))))
|
||||||
@ -282,14 +282,14 @@
|
|||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :addColumn-type type)
|
(u/not-valid plugin-id :addColumn-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :lex type))
|
(and (or (= :percent type) (= :flex type) (= :lex type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :addColumn-value value)
|
(u/not-valid plugin-id :addColumn-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addColumn "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addColumn "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value})))))
|
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value})))))
|
||||||
@ -298,17 +298,17 @@
|
|||||||
(fn [index type value]
|
(fn [index type value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :addColumnAtIndex-index index)
|
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||||
|
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :addColumnAtIndex-type type)
|
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :addColumnAtIndex-value value)
|
(u/not-valid plugin-id :addColumnAtIndex-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
@ -318,10 +318,10 @@
|
|||||||
(fn [index]
|
(fn [index]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :removeRow index)
|
(u/not-valid plugin-id :removeRow index)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :removeRow "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
|
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
|
||||||
@ -330,10 +330,10 @@
|
|||||||
(fn [index]
|
(fn [index]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :removeColumn index)
|
(u/not-valid plugin-id :removeColumn index)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :removeColumn "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
|
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
|
||||||
@ -343,17 +343,17 @@
|
|||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :setColumn-index index)
|
(u/not-valid plugin-id :setColumn-index index)
|
||||||
|
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :setColumn-type type)
|
(u/not-valid plugin-id :setColumn-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :setColumn-value value)
|
(u/not-valid plugin-id :setColumn-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setColumn "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setColumn "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/change-layout-track #{id} :column index (d/without-nils {:type type :value value}))))))
|
(st/emit! (dwsl/change-layout-track #{id} :column index (d/without-nils {:type type :value value}))))))
|
||||||
@ -363,17 +363,17 @@
|
|||||||
(let [type (keyword type)]
|
(let [type (keyword type)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :setRow-index index)
|
(u/not-valid plugin-id :setRow-index index)
|
||||||
|
|
||||||
(not (contains? ctl/grid-track-types type))
|
(not (contains? ctl/grid-track-types type))
|
||||||
(u/display-not-valid :setRow-type type)
|
(u/not-valid plugin-id :setRow-type type)
|
||||||
|
|
||||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||||
(not (sm/valid-safe-number? value)))
|
(not (sm/valid-safe-number? value)))
|
||||||
(u/display-not-valid :setRow-value value)
|
(u/not-valid plugin-id :setRow-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setRow "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setRow "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/change-layout-track #{id} :row index (d/without-nils {:type type :value value}))))))
|
(st/emit! (dwsl/change-layout-track #{id} :row index (d/without-nils {:type type :value value}))))))
|
||||||
@ -382,7 +382,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :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! (dwsl/remove-layout #{id}))))
|
(st/emit! (dwsl/remove-layout #{id}))))
|
||||||
@ -391,16 +391,16 @@
|
|||||||
(fn [child row column]
|
(fn [child row column]
|
||||||
(cond
|
(cond
|
||||||
(not (shape-proxy? child))
|
(not (shape-proxy? child))
|
||||||
(u/display-not-valid :appendChild-child child)
|
(u/not-valid plugin-id :appendChild-child child)
|
||||||
|
|
||||||
(or (< row 0) (not (sm/valid-safe-int? row)))
|
(or (< row 0) (not (sm/valid-safe-int? row)))
|
||||||
(u/display-not-valid :appendChild-row row)
|
(u/not-valid plugin-id :appendChild-row row)
|
||||||
|
|
||||||
(or (< column 0) (not (sm/valid-safe-int? column)))
|
(or (< column 0) (not (sm/valid-safe-int? column)))
|
||||||
(u/display-not-valid :appendChild-column column)
|
(u/not-valid plugin-id :appendChild-column column)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [child-id (obj/get child "$id")]
|
(let [child-id (obj/get child "$id")]
|
||||||
@ -432,13 +432,13 @@
|
|||||||
shape (u/proxy->shape self)]
|
shape (u/proxy->shape self)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :row-value value)
|
(u/not-valid plugin-id :row-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :row-cell "cell not found")
|
(u/not-valid plugin-id :row-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :row "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :row "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row value})))))}
|
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row value})))))}
|
||||||
@ -452,13 +452,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :rowSpan-value value)
|
(u/not-valid plugin-id :rowSpan-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :rowSpan-cell "cell not found")
|
(u/not-valid plugin-id :rowSpan-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rowSpan "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rowSpan "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row-span value})))))}
|
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row-span value})))))}
|
||||||
@ -472,13 +472,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :column-value value)
|
(u/not-valid plugin-id :column-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :column-cell "cell not found")
|
(u/not-valid plugin-id :column-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :column "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :column "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column value})))))}
|
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column value})))))}
|
||||||
@ -492,13 +492,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :columnSpan-value value)
|
(u/not-valid plugin-id :columnSpan-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :columnSpan-cell "cell not found")
|
(u/not-valid plugin-id :columnSpan-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :columnSpan "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :columnSpan "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column-span value})))))}
|
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column-span value})))))}
|
||||||
@ -512,13 +512,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :areaName-value value)
|
(u/not-valid plugin-id :areaName-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :areaName-cell "cell not found")
|
(u/not-valid plugin-id :areaName-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :areaName "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :areaName "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:area-name value})))))}
|
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:area-name value})))))}
|
||||||
@ -533,13 +533,13 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-position-types value))
|
(not (contains? ctl/grid-position-types value))
|
||||||
(u/display-not-valid :position-value value)
|
(u/not-valid plugin-id :position-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :position-cell "cell not found")
|
(u/not-valid plugin-id :position-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :position "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/change-cells-mode (:parent-id shape) #{(:id cell)} value)))))}
|
(st/emit! (dwsl/change-cells-mode (:parent-id shape) #{(:id cell)} value)))))}
|
||||||
@ -554,13 +554,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-cell-align-self-types value))
|
(not (contains? ctl/grid-cell-align-self-types value))
|
||||||
(u/display-not-valid :alignSelf-value value)
|
(u/not-valid plugin-id :alignSelf-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :alignSelf-cell "cell not found")
|
(u/not-valid plugin-id :alignSelf-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:align-self value})))))}
|
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:align-self value})))))}
|
||||||
@ -575,13 +575,13 @@
|
|||||||
cell (locate-cell self)]
|
cell (locate-cell self)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctl/grid-cell-justify-self-types value))
|
(not (contains? ctl/grid-cell-justify-self-types value))
|
||||||
(u/display-not-valid :justifySelf-value value)
|
(u/not-valid plugin-id :justifySelf-value value)
|
||||||
|
|
||||||
(nil? cell)
|
(nil? cell)
|
||||||
(u/display-not-valid :justifySelf-cell "cell not found")
|
(u/not-valid plugin-id :justifySelf-cell "cell not found")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :justifySelf "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :justifySelf "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:justify-self value})))))})))
|
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:justify-self value})))))})))
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (js/Symbol)]
|
(let [id (js/Symbol)]
|
||||||
@ -35,10 +35,10 @@
|
|||||||
(fn [block-id]
|
(fn [block-id]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(not block-id)
|
(not block-id)
|
||||||
(u/display-not-valid :undoBlockFinish block-id)
|
(u/not-valid plugin-id :undoBlockFinish block-id)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwu/commit-undo-transaction block-id))))))
|
(st/emit! (dwu/commit-undo-transaction block-id))))))
|
||||||
|
|||||||
@ -60,10 +60,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (u/proxy->library-color self)
|
(let [color (u/proxy->library-color self)
|
||||||
@ -77,10 +77,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :path value)
|
(u/not-valid plugin-id :path value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (-> (u/proxy->library-color self)
|
(let [color (-> (u/proxy->library-color self)
|
||||||
@ -94,10 +94,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (clr/valid-hex-color? value)))
|
(or (not (string? value)) (not (clr/valid-hex-color? value)))
|
||||||
(u/display-not-valid :color value)
|
(u/not-valid plugin-id :color value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :color "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :color "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (-> (u/proxy->library-color self)
|
(let [color (-> (u/proxy->library-color self)
|
||||||
@ -111,10 +111,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (number? value)) (< value 0) (> value 1))
|
(or (not (number? value)) (< value 0) (> value 1))
|
||||||
(u/display-not-valid :opacity value)
|
(u/not-valid plugin-id :opacity value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :opacity "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :opacity "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (-> (u/proxy->library-color self)
|
(let [color (-> (u/proxy->library-color self)
|
||||||
@ -129,10 +129,10 @@
|
|||||||
(let [value (parser/parse-gradient value)]
|
(let [value (parser/parse-gradient value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate clr/schema:gradient value))
|
(not (sm/validate clr/schema:gradient value))
|
||||||
(u/display-not-valid :gradient value)
|
(u/not-valid plugin-id :gradient value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :gradient "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :gradient "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (-> (u/proxy->library-color self)
|
(let [color (-> (u/proxy->library-color self)
|
||||||
@ -147,10 +147,10 @@
|
|||||||
(let [value (parser/parse-image-data value)]
|
(let [value (parser/parse-image-data value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate clr/schema:image value))
|
(not (sm/validate clr/schema:image value))
|
||||||
(u/display-not-valid :image value)
|
(u/not-valid plugin-id :image value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :image "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :image "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (-> (u/proxy->library-color self)
|
(let [color (-> (u/proxy->library-color self)
|
||||||
@ -161,7 +161,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwl/delete-color {:id id}))))
|
(st/emit! (dwl/delete-color {:id id}))))
|
||||||
@ -170,7 +170,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :clone "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color-id (uuid/next)
|
(let [color-id (uuid/next)
|
||||||
@ -207,7 +207,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getPluginData-key key)
|
(u/not-valid plugin-id :getPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (u/locate-library-color file-id id)]
|
(let [color (u/locate-library-color file-id id)]
|
||||||
@ -217,16 +217,16 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :color id (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :color id (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -240,10 +240,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getSharedPluginData-key key)
|
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (u/locate-library-color file-id id)]
|
(let [color (u/locate-library-color file-id id)]
|
||||||
@ -253,19 +253,19 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :color id (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :color id (keyword "shared" namespace) key value))))
|
||||||
@ -274,7 +274,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color (u/locate-library-color file-id id)]
|
(let [color (u/locate-library-color file-id id)]
|
||||||
@ -301,10 +301,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (u/proxy->library-typography self)
|
(let [typo (u/proxy->library-typography self)
|
||||||
@ -318,10 +318,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :path value)
|
(u/not-valid plugin-id :path value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -335,10 +335,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontId value)
|
(u/not-valid plugin-id :fontId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontId "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontId "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -352,10 +352,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontFamily value)
|
(u/not-valid plugin-id :fontFamily value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontFamily "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -369,10 +369,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontVariantId value)
|
(u/not-valid plugin-id :fontVariantId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontVariantId "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -386,10 +386,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontSize value)
|
(u/not-valid plugin-id :fontSize value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontSize "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -403,10 +403,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontWeight value)
|
(u/not-valid plugin-id :fontWeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontWeight "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -420,10 +420,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontStyle value)
|
(u/not-valid plugin-id :fontStyle value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :fontStyle "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -437,10 +437,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :lineHeight value)
|
(u/not-valid plugin-id :lineHeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :lineHeight "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -454,10 +454,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :letterSpacing value)
|
(u/not-valid plugin-id :letterSpacing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :letterSpacing "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -471,10 +471,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :textTransform value)
|
(u/not-valid plugin-id :textTransform value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :textTransform "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo (-> (u/proxy->library-typography self)
|
(let [typo (-> (u/proxy->library-typography self)
|
||||||
@ -485,7 +485,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwl/delete-typography {:id id}))))
|
(st/emit! (dwl/delete-typography {:id id}))))
|
||||||
@ -494,7 +494,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :clone "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typo-id (uuid/next)
|
(let [typo-id (uuid/next)
|
||||||
@ -507,10 +507,10 @@
|
|||||||
(fn [shape]
|
(fn [shape]
|
||||||
(cond
|
(cond
|
||||||
(not (shape/shape-proxy? shape))
|
(not (shape/shape-proxy? shape))
|
||||||
(u/display-not-valid :applyToText shape)
|
(u/not-valid plugin-id :applyToText shape)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape-id (obj/get shape "$id")
|
(let [shape-id (obj/get shape "$id")
|
||||||
@ -521,10 +521,10 @@
|
|||||||
(fn [range]
|
(fn [range]
|
||||||
(cond
|
(cond
|
||||||
(not (text/text-range-proxy? range))
|
(not (text/text-range-proxy? range))
|
||||||
(u/display-not-valid :applyToText range)
|
(u/not-valid plugin-id :applyToText range)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape-id (obj/get range "$id")
|
(let [shape-id (obj/get range "$id")
|
||||||
@ -542,7 +542,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :typography-plugin-data-key key)
|
(u/not-valid plugin-id :typography-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typography (u/locate-library-typography file-id id)]
|
(let [typography (u/locate-library-typography file-id id)]
|
||||||
@ -552,16 +552,16 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -575,10 +575,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getSharedPluginData-key key)
|
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typography (u/locate-library-typography file-id id)]
|
(let [typography (u/locate-library-typography file-id id)]
|
||||||
@ -588,19 +588,19 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "shared" namespace) key value))))
|
||||||
@ -609,7 +609,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typography (u/locate-library-typography file-id id)]
|
(let [typography (u/locate-library-typography file-id id)]
|
||||||
@ -674,7 +674,7 @@
|
|||||||
:removeProperty
|
:removeProperty
|
||||||
(fn [pos]
|
(fn [pos]
|
||||||
(if (not (nat-int? pos))
|
(if (not (nat-int? pos))
|
||||||
(u/display-not-valid :pos pos)
|
(u/not-valid plugin-id :pos pos)
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(ev/event {::ev/name "remove-property" ::ev/origin "plugin:remove-property"})
|
(ev/event {::ev/name "remove-property" ::ev/origin "plugin:remove-property"})
|
||||||
(dwv/remove-property id pos))))
|
(dwv/remove-property id pos))))
|
||||||
@ -683,10 +683,10 @@
|
|||||||
(fn [pos name]
|
(fn [pos name]
|
||||||
(cond
|
(cond
|
||||||
(not (nat-int? pos))
|
(not (nat-int? pos))
|
||||||
(u/display-not-valid :pos pos)
|
(u/not-valid plugin-id :pos pos)
|
||||||
|
|
||||||
(not (string? name))
|
(not (string? name))
|
||||||
(u/display-not-valid :name name)
|
(u/not-valid plugin-id :name name)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit!
|
(st/emit!
|
||||||
@ -715,10 +715,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [component (u/proxy->library-component self)
|
(let [component (u/proxy->library-component self)
|
||||||
@ -732,10 +732,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :path value)
|
(u/not-valid plugin-id :path value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [component (u/proxy->library-component self)
|
(let [component (u/proxy->library-component self)
|
||||||
@ -746,7 +746,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwl/delete-component {:id id}))))
|
(st/emit! (dwl/delete-component {:id id}))))
|
||||||
@ -755,7 +755,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :instance "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :instance "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id-ref (atom nil)]
|
(let [id-ref (atom nil)]
|
||||||
@ -766,7 +766,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :component-plugin-data-key key)
|
(u/not-valid plugin-id :component-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [component (u/locate-library-component file-id id)]
|
(let [component (u/locate-library-component file-id id)]
|
||||||
@ -776,16 +776,16 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :component id (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :component id (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -799,10 +799,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :component-plugin-data-namespace namespace)
|
(u/not-valid plugin-id :component-plugin-data-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :component-plugin-data-key key)
|
(u/not-valid plugin-id :component-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [component (u/locate-library-component file-id id)]
|
(let [component (u/locate-library-component file-id id)]
|
||||||
@ -812,19 +812,19 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not= file-id (:current-file-id @st/state))
|
(not= file-id (:current-file-id @st/state))
|
||||||
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
|
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
|
||||||
|
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :component id (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :component id (keyword "shared" namespace) key value))))
|
||||||
@ -833,7 +833,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :component-plugin-data-namespace namespace)
|
(u/not-valid plugin-id :component-plugin-data-namespace namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [component (u/locate-library-component file-id id)]
|
(let [component (u/locate-library-component file-id id)]
|
||||||
@ -901,10 +901,10 @@
|
|||||||
(fn [pos value]
|
(fn [pos value]
|
||||||
(cond
|
(cond
|
||||||
(not (nat-int? pos))
|
(not (nat-int? pos))
|
||||||
(u/display-not-valid :pos (str pos))
|
(u/not-valid plugin-id :pos (str pos))
|
||||||
|
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit!
|
(st/emit!
|
||||||
@ -970,7 +970,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :createColor "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :createColor "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [color-id (uuid/next)]
|
(let [color-id (uuid/next)]
|
||||||
@ -981,7 +981,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :createTypography "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :createTypography "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typography-id (uuid/next)]
|
(let [typography-id (uuid/next)]
|
||||||
@ -992,7 +992,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :createComponent "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :createComponent "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id-ref (atom nil)
|
(let [id-ref (atom nil)
|
||||||
@ -1005,7 +1005,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :file-plugin-data-key key)
|
(u/not-valid plugin-id :file-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file file-id)]
|
(let [file (u/locate-file file-id)]
|
||||||
@ -1015,13 +1015,13 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :file (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :file (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -1035,10 +1035,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :file-plugin-data-namespace namespace)
|
(u/not-valid plugin-id :file-plugin-data-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :file-plugin-data-key key)
|
(u/not-valid plugin-id :file-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file file-id)]
|
(let [file (u/locate-file file-id)]
|
||||||
@ -1048,16 +1048,16 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :file (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :file (keyword "shared" namespace) key value))))
|
||||||
@ -1066,7 +1066,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :namespace namespace)
|
(u/not-valid plugin-id :namespace namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [file (u/locate-file file-id)]
|
(let [file (u/locate-file file-id)]
|
||||||
@ -1110,14 +1110,14 @@
|
|||||||
(fn [library-id]
|
(fn [library-id]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "library:write"))
|
(not (r/check-permission plugin-id "library:write"))
|
||||||
(u/display-not-valid :connectLibrary "Plugin doesn't have 'library:write' permission")
|
(u/not-valid plugin-id :connectLibrary "Plugin doesn't have 'library:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
(fn [resolve reject]
|
(fn [resolve reject]
|
||||||
(cond
|
(cond
|
||||||
(not (string? library-id))
|
(not (string? library-id))
|
||||||
(do (u/display-not-valid :connectLibrary library-id)
|
(do (u/not-valid plugin-id :connectLibrary library-id)
|
||||||
(reject nil))
|
(reject nil))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
|
|||||||
@ -30,10 +30,10 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "allow:localstorage"))
|
(not (r/check-permission plugin-id "allow:localstorage"))
|
||||||
(u/display-not-valid :getItem "Plugin doesn't have 'allow:localstorage' permission")
|
(u/not-valid plugin-id :getItem "Plugin doesn't have 'allow:localstorage' permission")
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getItem "The key must be a string")
|
(u/not-valid plugin-id :getItem "The key must be a string")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||||
@ -42,10 +42,10 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "allow:localstorage"))
|
(not (r/check-permission plugin-id "allow:localstorage"))
|
||||||
(u/display-not-valid :setItem "Plugin doesn't have 'allow:localstorage' permission")
|
(u/not-valid plugin-id :setItem "Plugin doesn't have 'allow:localstorage' permission")
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setItem "The key must be a string")
|
(u/not-valid plugin-id :setItem "The key must be a string")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(.setItem ^js local-storage (prefix-key plugin-id key) value)))
|
(.setItem ^js local-storage (prefix-key plugin-id key) value)))
|
||||||
@ -54,10 +54,10 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "allow:localstorage"))
|
(not (r/check-permission plugin-id "allow:localstorage"))
|
||||||
(u/display-not-valid :removeItem "Plugin doesn't have 'allow:localstorage' permission")
|
(u/not-valid plugin-id :removeItem "Plugin doesn't have 'allow:localstorage' permission")
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :removeItem "The key must be a string")
|
(u/not-valid plugin-id :removeItem "The key must be a string")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (empty? value))
|
(or (not (string? value)) (empty? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}
|
(st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}
|
||||||
@ -74,7 +74,7 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (shape/shape-proxy? value))
|
(not (shape/shape-proxy? value))
|
||||||
(u/display-not-valid :startingBoard value)
|
(u/not-valid plugin-id :startingBoard value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
|
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
|
||||||
@ -103,10 +103,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :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! (dw/rename-page id value))))}
|
(st/emit! (dw/rename-page id value))))}
|
||||||
@ -127,10 +127,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (cc/valid-hex-color? value)))
|
(or (not (string? value)) (not (cc/valid-hex-color? value)))
|
||||||
(u/display-not-valid :background value)
|
(u/not-valid plugin-id :background value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :background "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :background "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/change-canvas-color id {:color value}))))}
|
(st/emit! (dw/change-canvas-color id {:color value}))))}
|
||||||
@ -158,7 +158,7 @@
|
|||||||
(fn [shape-id]
|
(fn [shape-id]
|
||||||
(cond
|
(cond
|
||||||
(not (string? shape-id))
|
(not (string? shape-id))
|
||||||
(u/display-not-valid :getShapeById shape-id)
|
(u/not-valid plugin-id :getShapeById shape-id)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape-id (uuid/parse shape-id)
|
(let [shape-id (uuid/parse shape-id)
|
||||||
@ -195,7 +195,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :page-plugin-data-key key)
|
(u/not-valid plugin-id :page-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [page (u/locate-page file-id id)]
|
(let [page (u/locate-page file-id id)]
|
||||||
@ -205,13 +205,13 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -225,10 +225,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :page-plugin-data-namespace namespace)
|
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :page-plugin-data-key key)
|
(u/not-valid plugin-id :page-plugin-data-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [page (u/locate-page file-id id)]
|
(let [page (u/locate-page file-id id)]
|
||||||
@ -238,16 +238,16 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
|
||||||
@ -256,7 +256,7 @@
|
|||||||
(fn [self namespace]
|
(fn [self namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :page-plugin-data-namespace namespace)
|
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [page (u/proxy->page self)]
|
(let [page (u/proxy->page self)]
|
||||||
@ -266,7 +266,7 @@
|
|||||||
(fn [new-window]
|
(fn [new-window]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:read"))
|
(not (r/check-permission plugin-id "content:read"))
|
||||||
(u/display-not-valid :openPage "Plugin doesn't have 'content:read' permission")
|
(u/not-valid plugin-id :openPage "Plugin doesn't have 'content:read' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [new-window (if (boolean? new-window) new-window false)]
|
(let [new-window (if (boolean? new-window) new-window false)]
|
||||||
@ -276,10 +276,10 @@
|
|||||||
(fn [name frame]
|
(fn [name frame]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? name)) (empty? name))
|
(or (not (string? name)) (empty? name))
|
||||||
(u/display-not-valid :createFlow-name name)
|
(u/not-valid plugin-id :createFlow-name name)
|
||||||
|
|
||||||
(not (shape/shape-proxy? frame))
|
(not (shape/shape-proxy? frame))
|
||||||
(u/display-not-valid :createFlow-frame frame)
|
(u/not-valid plugin-id :createFlow-frame frame)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [flow-id (uuid/next)]
|
(let [flow-id (uuid/next)]
|
||||||
@ -290,7 +290,7 @@
|
|||||||
(fn [flow]
|
(fn [flow]
|
||||||
(cond
|
(cond
|
||||||
(not (flow-proxy? flow))
|
(not (flow-proxy? flow))
|
||||||
(u/display-not-valid :removeFlow-flow flow)
|
(u/not-valid plugin-id :removeFlow-flow flow)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/remove-flow id (obj/get flow "$id")))))
|
(st/emit! (dwi/remove-flow id (obj/get flow "$id")))))
|
||||||
@ -300,18 +300,18 @@
|
|||||||
(let [shape (u/proxy->shape board)]
|
(let [shape (u/proxy->shape board)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :addRulerGuide "Value not a safe number")
|
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
|
||||||
|
|
||||||
(not (contains? #{"vertical" "horizontal"} orientation))
|
(not (contains? #{"vertical" "horizontal"} orientation))
|
||||||
(u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
|
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
|
||||||
|
|
||||||
(and (some? shape)
|
(and (some? shape)
|
||||||
(or (not (shape/shape-proxy? board))
|
(or (not (shape/shape-proxy? board))
|
||||||
(not (cfh/frame-shape? shape))))
|
(not (cfh/frame-shape? shape))))
|
||||||
(u/display-not-valid :addRulerGuide "The shape is not a board")
|
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ruler-id (uuid/next)]
|
(let [ruler-id (uuid/next)]
|
||||||
@ -328,10 +328,10 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(cond
|
(cond
|
||||||
(not (rg/ruler-guide-proxy? value))
|
(not (rg/ruler-guide-proxy? value))
|
||||||
(u/display-not-valid :removeRulerGuide "Guide not provided")
|
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :removeRulerGuide "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [guide (u/proxy->ruler-guide value)]
|
(let [guide (u/proxy->ruler-guide value)]
|
||||||
@ -343,17 +343,17 @@
|
|||||||
position (parser/parse-point position)]
|
position (parser/parse-point position)]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? content)) (empty? content))
|
(or (not (string? content)) (empty? content))
|
||||||
(u/display-not-valid :addCommentThread "Content not valid")
|
(u/not-valid plugin-id :addCommentThread "Content not valid")
|
||||||
|
|
||||||
(or (not (sm/valid-safe-number? (:x position)))
|
(or (not (sm/valid-safe-number? (:x position)))
|
||||||
(not (sm/valid-safe-number? (:y position))))
|
(not (sm/valid-safe-number? (:y position))))
|
||||||
(u/display-not-valid :addCommentThread "Position not valid")
|
(u/not-valid plugin-id :addCommentThread "Position not valid")
|
||||||
|
|
||||||
(and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))
|
(and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))
|
||||||
(u/display-not-valid :addCommentThread "Board not valid")
|
(u/not-valid plugin-id :addCommentThread "Board not valid")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :addCommentThread "Plugin doesn't have 'comment:write' permission")
|
(u/not-valid plugin-id :addCommentThread "Plugin doesn't have 'comment:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [position
|
(let [position
|
||||||
@ -378,10 +378,10 @@
|
|||||||
(fn [thread]
|
(fn [thread]
|
||||||
(cond
|
(cond
|
||||||
(not (pc/comment-thread-proxy? thread))
|
(not (pc/comment-thread-proxy? thread))
|
||||||
(u/display-not-valid :removeCommentThread "Comment thread not valid")
|
(u/not-valid plugin-id :removeCommentThread "Comment thread not valid")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "comment:write"))
|
(not (r/check-permission plugin-id "comment:write"))
|
||||||
(u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :removeCommentThread "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
@ -400,7 +400,7 @@
|
|||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "comment:read"))
|
(not (r/check-permission plugin-id "comment:read"))
|
||||||
(do
|
(do
|
||||||
(u/display-not-valid :findCommentThreads "Plugin doesn't have 'comment:read' permission")
|
(u/not-valid plugin-id :findCommentThreads "Plugin doesn't have 'comment:read' permission")
|
||||||
(reject "Plugin doesn't have 'comment:read' permission"))
|
(reject "Plugin doesn't have 'comment:read' permission"))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
|
|||||||
@ -14,10 +14,10 @@
|
|||||||
[app.plugins.utils :as u]))
|
[app.plugins.utils :as u]))
|
||||||
|
|
||||||
(defn ^:export centerShapes
|
(defn ^:export centerShapes
|
||||||
[shapes]
|
[plugin-id shapes]
|
||||||
(cond
|
(cond
|
||||||
(not (every? shape/shape-proxy? shapes))
|
(not (every? shape/shape-proxy? shapes))
|
||||||
(u/display-not-valid :centerShapes shapes)
|
(u/not-valid plugin-id :centerShapes shapes)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shapes (->> shapes (map u/proxy->shape))]
|
(let [shapes (->> shapes (map u/proxy->shape))]
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[beicon.v2.core :as rx]))
|
[beicon.v2.core :as rx]))
|
||||||
|
|
||||||
|
;; Needs to be here because moving it to `app.main.data.workspace.mcp` will
|
||||||
|
;; cause a circular dependency
|
||||||
|
(def mcp-plugin-id "96dfa740-005d-8020-8007-55ede24a2bae")
|
||||||
|
|
||||||
;; Stores the installed plugins information
|
;; Stores the installed plugins information
|
||||||
(defonce ^:private registry (atom {}))
|
(defonce ^:private registry (atom {}))
|
||||||
|
|
||||||
@ -78,6 +82,7 @@
|
|||||||
(d/without-nils
|
(d/without-nils
|
||||||
{:plugin-id plugin-id
|
{:plugin-id plugin-id
|
||||||
:url (str plugin-url)
|
:url (str plugin-url)
|
||||||
|
:version vers
|
||||||
:name name
|
:name name
|
||||||
:description desc
|
:description desc
|
||||||
:host origin
|
:host origin
|
||||||
@ -127,5 +132,6 @@
|
|||||||
(defn check-permission
|
(defn check-permission
|
||||||
[plugin-id permission]
|
[plugin-id permission]
|
||||||
(or (= plugin-id "00000000-0000-0000-0000-000000000000")
|
(or (= plugin-id "00000000-0000-0000-0000-000000000000")
|
||||||
|
(= plugin-id mcp-plugin-id)
|
||||||
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
|
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
|
||||||
(contains? permissions permission))))
|
(contains? permissions permission))))
|
||||||
|
|||||||
@ -44,13 +44,13 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id (obj/get value "$id"))]
|
(let [shape (u/locate-shape file-id page-id (obj/get value "$id"))]
|
||||||
(cond
|
(cond
|
||||||
(not (shape-proxy? value))
|
(not (shape-proxy? value))
|
||||||
(u/display-not-valid :board "The board is not a shape proxy")
|
(u/not-valid plugin-id :board "The board is not a shape proxy")
|
||||||
|
|
||||||
(not (cfh/frame-shape? shape))
|
(not (cfh/frame-shape? shape))
|
||||||
(u/display-not-valid :board "The shape is not a board")
|
(u/not-valid plugin-id :board "The shape is not a board")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :board "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :board "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [board-id (when value (obj/get value "$id"))
|
(let [board-id (when value (obj/get value "$id"))
|
||||||
@ -78,10 +78,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :position "Not valid position")
|
(u/not-valid plugin-id :position "Not valid position")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :position "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [guide (u/proxy->ruler-guide self)
|
(let [guide (u/proxy->ruler-guide self)
|
||||||
|
|||||||
@ -31,8 +31,8 @@
|
|||||||
[app.common.types.shape.radius :as ctsr]
|
[app.common.types.shape.radius :as ctsr]
|
||||||
[app.common.types.shape.shadow :as ctss]
|
[app.common.types.shape.shadow :as ctss]
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.types.token :as cto]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
|
||||||
[app.main.data.plugins :as dp]
|
[app.main.data.plugins :as dp]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.groups :as dwg]
|
[app.main.data.workspace.groups :as dwg]
|
||||||
@ -47,7 +47,6 @@
|
|||||||
[app.main.data.workspace.variants :as dwv]
|
[app.main.data.workspace.variants :as dwv]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.flags :refer [natural-child-ordering?]]
|
|
||||||
[app.plugins.flex :as flex]
|
[app.plugins.flex :as flex]
|
||||||
[app.plugins.format :as format]
|
[app.plugins.format :as format]
|
||||||
[app.plugins.grid :as grid]
|
[app.plugins.grid :as grid]
|
||||||
@ -55,6 +54,7 @@
|
|||||||
[app.plugins.register :as r]
|
[app.plugins.register :as r]
|
||||||
[app.plugins.ruler-guides :as rg]
|
[app.plugins.ruler-guides :as rg]
|
||||||
[app.plugins.text :as text]
|
[app.plugins.text :as text]
|
||||||
|
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr?]]
|
||||||
[app.plugins.utils :as u]
|
[app.plugins.utils :as u]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
@ -91,7 +91,7 @@
|
|||||||
(let [value (parser/parse-keyword value)]
|
(let [value (parser/parse-keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? ctsi/event-types value))
|
(not (contains? ctsi/event-types value))
|
||||||
(u/display-not-valid :trigger value)
|
(u/not-valid plugin-id :trigger value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/update-interaction
|
(st/emit! (dwi/update-interaction
|
||||||
@ -107,7 +107,7 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(or (not (number? value)) (not (pos? value)))
|
(or (not (number? value)) (not (pos? value)))
|
||||||
(u/display-not-valid :delay value)
|
(u/not-valid plugin-id :delay value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/update-interaction
|
(st/emit! (dwi/update-interaction
|
||||||
@ -127,7 +127,7 @@
|
|||||||
(d/patch-object params))]
|
(d/patch-object params))]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate ctsi/schema:interaction interaction))
|
(not (sm/validate ctsi/schema:interaction interaction))
|
||||||
(u/display-not-valid :action interaction)
|
(u/not-valid plugin-id :action interaction)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/update-interaction
|
(st/emit! (dwi/update-interaction
|
||||||
@ -192,7 +192,8 @@
|
|||||||
(assert (uuid? id))
|
(assert (uuid? id))
|
||||||
|
|
||||||
(let [data (u/locate-shape file-id page-id id)]
|
(let [data (u/locate-shape file-id page-id id)]
|
||||||
(-> (obj/reify {:name "ShapeProxy"}
|
(-> (obj/reify {:name "ShapeProxy"
|
||||||
|
:on-error (u/handle-error plugin-id)}
|
||||||
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
||||||
:$id {:enumerable false :get (fn [] id)}
|
:$id {:enumerable false :get (fn [] id)}
|
||||||
:$file {:enumerable false :get (fn [] file-id)}
|
:$file {:enumerable false :get (fn [] file-id)}
|
||||||
@ -218,10 +219,10 @@
|
|||||||
(not (str/blank? value)))]
|
(not (str/blank? value)))]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :name "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(not valid?)
|
(not valid?)
|
||||||
(u/display-not-valid :name value)
|
(u/not-valid plugin-id :name value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))}
|
(st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))}
|
||||||
@ -233,10 +234,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :blocked value)
|
(u/not-valid plugin-id :blocked value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :blocked "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :blocked "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -249,10 +250,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :hidden value)
|
(u/not-valid plugin-id :hidden value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :hidden "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :hidden "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -265,10 +266,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :visible value)
|
(u/not-valid plugin-id :visible value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :visible "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :visible "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -281,10 +282,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :proportionLock value)
|
(u/not-valid plugin-id :proportionLock value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :proportionLock "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :proportionLock "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -299,10 +300,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? cts/horizontal-constraint-types value))
|
(not (contains? cts/horizontal-constraint-types value))
|
||||||
(u/display-not-valid :constraintsHorizontal value)
|
(u/not-valid plugin-id :constraintsHorizontal value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :constraintsHorizontal "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :constraintsHorizontal "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
|
||||||
@ -316,10 +317,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? cts/vertical-constraint-types value))
|
(not (contains? cts/vertical-constraint-types value))
|
||||||
(u/display-not-valid :constraintsVertical value)
|
(u/not-valid plugin-id :constraintsVertical value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :constraintsVertical "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :constraintsVertical "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
|
||||||
@ -332,10 +333,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (sm/valid-safe-int? value)) (< value 0))
|
(or (not (sm/valid-safe-int? value)) (< value 0))
|
||||||
(u/display-not-valid :borderRadius value)
|
(u/not-valid plugin-id :borderRadius value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :borderRadius "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :borderRadius "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))}
|
||||||
@ -348,10 +349,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :borderRadiusTopLeft value)
|
(u/not-valid plugin-id :borderRadiusTopLeft value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))}
|
||||||
@ -364,10 +365,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :borderRadiusTopRight value)
|
(u/not-valid plugin-id :borderRadiusTopRight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :borderRadiusTopRight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :borderRadiusTopRight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))}
|
||||||
@ -380,10 +381,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :borderRadiusBottomRight value)
|
(u/not-valid plugin-id :borderRadiusBottomRight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))}
|
||||||
@ -396,10 +397,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? value))
|
(not (sm/valid-safe-int? value))
|
||||||
(u/display-not-valid :borderRadiusBottomLeft value)
|
(u/not-valid plugin-id :borderRadiusBottomLeft value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))}
|
||||||
@ -412,10 +413,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (sm/valid-safe-number? value)) (< value 0) (> value 1))
|
(or (not (sm/valid-safe-number? value)) (< value 0) (> value 1))
|
||||||
(u/display-not-valid :opacity value)
|
(u/not-valid plugin-id :opacity value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :opacity "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :opacity "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
|
||||||
@ -429,10 +430,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? cts/blend-modes value))
|
(not (contains? cts/blend-modes value))
|
||||||
(u/display-not-valid :blendMode value)
|
(u/not-valid plugin-id :blendMode value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :blendMode "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :blendMode "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
|
||||||
@ -446,10 +447,10 @@
|
|||||||
value (mapv #(shadow-defaults (parser/parse-shadow %)) value)]
|
value (mapv #(shadow-defaults (parser/parse-shadow %)) value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector ctss/schema:shadow] value))
|
(not (sm/validate [:vector ctss/schema:shadow] value))
|
||||||
(u/display-not-valid :shadows value)
|
(u/not-valid plugin-id :shadows value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :shadows "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :shadows "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))}
|
||||||
@ -465,10 +466,10 @@
|
|||||||
value (blur-defaults (parser/parse-blur value))]
|
value (blur-defaults (parser/parse-blur value))]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate ctsb/schema:blur value))
|
(not (sm/validate ctsb/schema:blur value))
|
||||||
(u/display-not-valid :blur value)
|
(u/not-valid plugin-id :blur value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :blur "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :blur "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
||||||
@ -482,10 +483,10 @@
|
|||||||
value (parser/parse-exports value)]
|
value (parser/parse-exports value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector ctse/schema:export] value))
|
(not (sm/validate [:vector ctse/schema:export] value))
|
||||||
(u/display-not-valid :exports value)
|
(u/not-valid plugin-id :exports value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :exports "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :exports "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))}
|
||||||
@ -499,10 +500,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :x value)
|
(u/not-valid plugin-id :x value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :x "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :x "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/update-position id
|
(st/emit! (dw/update-position id
|
||||||
@ -517,10 +518,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :y value)
|
(u/not-valid plugin-id :y value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :y "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :y "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/update-position id
|
(st/emit! (dw/update-position id
|
||||||
@ -562,10 +563,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :parentX value)
|
(u/not-valid plugin-id :parentX value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :parentX "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :parentX "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
@ -589,10 +590,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :parentY value)
|
(u/not-valid plugin-id :parentY value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :parentY "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :parentY "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
@ -616,10 +617,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :frameX value)
|
(u/not-valid plugin-id :frameX value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :frameX "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :frameX "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
@ -643,10 +644,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :frameY value)
|
(u/not-valid plugin-id :frameY value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :frameY "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :frameY "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
@ -680,10 +681,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (number? value))
|
(not (number? value))
|
||||||
(u/display-not-valid :rotation value)
|
(u/not-valid plugin-id :rotation value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rotation "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rotation "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/proxy->shape self)]
|
(let [shape (u/proxy->shape self)]
|
||||||
@ -696,10 +697,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :flipX value)
|
(u/not-valid plugin-id :flipX value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :flipX "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :flipX "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -712,10 +713,10 @@
|
|||||||
(fn [self value]
|
(fn [self value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :flipY value)
|
(u/not-valid plugin-id :flipY value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :flipY "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :flipY "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
@ -734,13 +735,13 @@
|
|||||||
value (parser/parse-fills value)]
|
value (parser/parse-fills value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector types.fills/schema:fill] value))
|
(not (sm/validate [:vector types.fills/schema:fill] value))
|
||||||
(u/display-not-valid :fills value)
|
(u/not-valid plugin-id :fills value)
|
||||||
|
|
||||||
(cfh/text-shape? shape)
|
(cfh/text-shape? shape)
|
||||||
(st/emit! (dwt/update-attrs id {:fills value}))
|
(st/emit! (dwt/update-attrs id {:fills value}))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fills "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))}
|
||||||
@ -754,10 +755,10 @@
|
|||||||
value (parser/parse-strokes value)]
|
value (parser/parse-strokes value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector cts/schema:stroke] value))
|
(not (sm/validate [:vector cts/schema:stroke] value))
|
||||||
(u/display-not-valid :strokes value)
|
(u/not-valid plugin-id :strokes value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :strokes "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))}
|
||||||
@ -802,13 +803,13 @@
|
|||||||
(fn [width height]
|
(fn [width height]
|
||||||
(cond
|
(cond
|
||||||
(or (not (sm/valid-safe-number? width)) (<= width 0))
|
(or (not (sm/valid-safe-number? width)) (<= width 0))
|
||||||
(u/display-not-valid :resize width)
|
(u/not-valid plugin-id :resize width)
|
||||||
|
|
||||||
(or (not (sm/valid-safe-number? height)) (<= height 0))
|
(or (not (sm/valid-safe-number? height)) (<= height 0))
|
||||||
(u/display-not-valid :resize height)
|
(u/not-valid plugin-id :resize height)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/update-dimensions [id] :width width)
|
(st/emit! (dw/update-dimensions [id] :width width)
|
||||||
@ -819,13 +820,13 @@
|
|||||||
(let [center (when center {:x (obj/get center "x") :y (obj/get center "y")})]
|
(let [center (when center {:x (obj/get center "x") :y (obj/get center "y")})]
|
||||||
(cond
|
(cond
|
||||||
(not (number? angle))
|
(not (number? angle))
|
||||||
(u/display-not-valid :rotate-angle angle)
|
(u/not-valid plugin-id :rotate-angle angle)
|
||||||
|
|
||||||
(and (some? center) (or (not (number? (:x center))) (not (number? (:y center)))))
|
(and (some? center) (or (not (number? (:x center))) (not (number? (:y center)))))
|
||||||
(u/display-not-valid :rotate-center center)
|
(u/not-valid plugin-id :rotate-center center)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :rotate "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :rotate "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/increase-rotation [id] angle {:center center :delta? true})))))
|
(st/emit! (dw/increase-rotation [id] angle {:center center :delta? true})))))
|
||||||
@ -835,7 +836,7 @@
|
|||||||
(let [ret-v (atom nil)]
|
(let [ret-v (atom nil)]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :clone "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :clone "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v))
|
(do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v))
|
||||||
@ -845,7 +846,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :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! (dwsh/delete-shapes #{id}))))
|
(st/emit! (dwsh/delete-shapes #{id}))))
|
||||||
@ -855,7 +856,7 @@
|
|||||||
(fn [key]
|
(fn [key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getPluginData key)
|
(u/not-valid plugin-id :getPluginData key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
@ -865,13 +866,13 @@
|
|||||||
(fn [key value]
|
(fn [key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setPluginData-key key)
|
(u/not-valid plugin-id :setPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setPluginData-value value)
|
(u/not-valid plugin-id :setPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "plugin" (str plugin-id)) key value))))
|
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "plugin" (str plugin-id)) key value))))
|
||||||
@ -885,10 +886,10 @@
|
|||||||
(fn [namespace key]
|
(fn [namespace key]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :getSharedPluginData-key key)
|
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
@ -898,16 +899,16 @@
|
|||||||
(fn [namespace key value]
|
(fn [namespace key value]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||||
|
|
||||||
(not (string? key))
|
(not (string? key))
|
||||||
(u/display-not-valid :setSharedPluginData-key key)
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||||
|
|
||||||
(and (some? value) (not (string? value)))
|
(and (some? value) (not (string? value)))
|
||||||
(u/display-not-valid :setSharedPluginData-value value)
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value))))
|
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value))))
|
||||||
@ -916,7 +917,7 @@
|
|||||||
(fn [namespace]
|
(fn [namespace]
|
||||||
(cond
|
(cond
|
||||||
(not (string? namespace))
|
(not (string? namespace))
|
||||||
(u/display-not-valid :getSharedPluginDataKeys namespace)
|
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
@ -931,12 +932,12 @@
|
|||||||
(not (cfh/group-shape? shape))
|
(not (cfh/group-shape? shape))
|
||||||
(not (cfh/svg-raw-shape? shape))
|
(not (cfh/svg-raw-shape? shape))
|
||||||
(not (cfh/bool-shape? shape)))
|
(not (cfh/bool-shape? shape)))
|
||||||
(u/display-not-valid :getChildren (:type shape))
|
(u/not-valid plugin-id :getChildren (:type shape))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [is-reversed? (ctl/flex-layout? shape)
|
(let [is-reversed? (ctl/flex-layout? shape)
|
||||||
reverse-fn
|
reverse-fn
|
||||||
(if (and (natural-child-ordering? plugin-id) is-reversed?)
|
(if (and (u/natural-child-ordering? plugin-id) is-reversed?)
|
||||||
reverse identity)]
|
reverse identity)]
|
||||||
(->> (u/locate-shape file-id page-id id)
|
(->> (u/locate-shape file-id page-id id)
|
||||||
(:shapes)
|
(:shapes)
|
||||||
@ -951,19 +952,19 @@
|
|||||||
(not (cfh/group-shape? shape))
|
(not (cfh/group-shape? shape))
|
||||||
(not (cfh/svg-raw-shape? shape))
|
(not (cfh/svg-raw-shape? shape))
|
||||||
(not (cfh/bool-shape? shape)))
|
(not (cfh/bool-shape? shape)))
|
||||||
(u/display-not-valid :appendChild (:type shape))
|
(u/not-valid plugin-id :appendChild (:type shape))
|
||||||
|
|
||||||
(not (shape-proxy? child))
|
(not (shape-proxy? child))
|
||||||
(u/display-not-valid :appendChild-child child)
|
(u/not-valid plugin-id :appendChild-child child)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [child-id (obj/get child "$id")
|
(let [child-id (obj/get child "$id")
|
||||||
is-reversed? (ctl/flex-layout? shape)
|
is-reversed? (ctl/flex-layout? shape)
|
||||||
index
|
index
|
||||||
(if (or (not (natural-child-ordering? plugin-id)) is-reversed?)
|
(if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
|
||||||
0
|
0
|
||||||
(count (:shapes shape)))]
|
(count (:shapes shape)))]
|
||||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||||
@ -976,19 +977,19 @@
|
|||||||
(not (cfh/group-shape? shape))
|
(not (cfh/group-shape? shape))
|
||||||
(not (cfh/svg-raw-shape? shape))
|
(not (cfh/svg-raw-shape? shape))
|
||||||
(not (cfh/bool-shape? shape)))
|
(not (cfh/bool-shape? shape)))
|
||||||
(u/display-not-valid :insertChild (:type shape))
|
(u/not-valid plugin-id :insertChild (:type shape))
|
||||||
|
|
||||||
(not (shape-proxy? child))
|
(not (shape-proxy? child))
|
||||||
(u/display-not-valid :insertChild-child child)
|
(u/not-valid plugin-id :insertChild-child child)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :insertChild "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :insertChild "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [child-id (obj/get child "$id")
|
(let [child-id (obj/get child "$id")
|
||||||
is-reversed? (ctl/flex-layout? shape)
|
is-reversed? (ctl/flex-layout? shape)
|
||||||
index
|
index
|
||||||
(if (or (not (natural-child-ordering? plugin-id)) is-reversed?)
|
(if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
|
||||||
(- (count (:shapes shape)) index)
|
(- (count (:shapes shape)) index)
|
||||||
index)]
|
index)]
|
||||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||||
@ -999,10 +1000,10 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (cfh/frame-shape? shape))
|
(not (cfh/frame-shape? shape))
|
||||||
(u/display-not-valid :addFlexLayout (:type shape))
|
(u/not-valid plugin-id :addFlexLayout (:type shape))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addFlexLayout "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addFlexLayout "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (st/emit! (dwsl/create-layout-from-id id :flex :from-frame? true :calculate-params? false))
|
(do (st/emit! (dwsl/create-layout-from-id id :flex :from-frame? true :calculate-params? false))
|
||||||
@ -1013,10 +1014,10 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (cfh/frame-shape? shape))
|
(not (cfh/frame-shape? shape))
|
||||||
(u/display-not-valid :addGridLayout (:type shape))
|
(u/not-valid plugin-id :addGridLayout (:type shape))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addGridLayout "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addGridLayout "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do (st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false))
|
(do (st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false))
|
||||||
@ -1028,10 +1029,10 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (cfh/group-shape? shape))
|
(not (cfh/group-shape? shape))
|
||||||
(u/display-not-valid :makeMask (:type shape))
|
(u/not-valid plugin-id :makeMask (:type shape))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :makeMask "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :makeMask "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwg/mask-group #{id})))))
|
(st/emit! (dwg/mask-group #{id})))))
|
||||||
@ -1041,10 +1042,10 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (cfh/mask-shape? shape))
|
(not (cfh/mask-shape? shape))
|
||||||
(u/display-not-valid :removeMask (:type shape))
|
(u/not-valid plugin-id :removeMask (:type shape))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :removeMask "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :removeMask "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwg/unmask-group #{id})))))
|
(st/emit! (dwg/unmask-group #{id})))))
|
||||||
@ -1055,7 +1056,7 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(and (not (cfh/path-shape? shape)) (not (cfh/bool-shape? shape)))
|
(and (not (cfh/path-shape? shape)) (not (cfh/bool-shape? shape)))
|
||||||
(u/display-not-valid :toD (:type shape))
|
(u/not-valid plugin-id :toD (:type shape))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(.toString (:content shape)))))
|
(.toString (:content shape)))))
|
||||||
@ -1066,13 +1067,13 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (cfh/text-shape? shape))
|
(not (cfh/text-shape? shape))
|
||||||
(u/display-not-valid :getRange-shape "shape is not text")
|
(u/not-valid plugin-id :getRange-shape "shape is not text")
|
||||||
|
|
||||||
(or (not (sm/valid-safe-int? start)) (< start 0) (> start end))
|
(or (not (sm/valid-safe-int? start)) (< start 0) (> start end))
|
||||||
(u/display-not-valid :getRange-start start)
|
(u/not-valid plugin-id :getRange-start start)
|
||||||
|
|
||||||
(not (sm/valid-safe-int? end))
|
(not (sm/valid-safe-int? end))
|
||||||
(u/display-not-valid :getRange-end end)
|
(u/not-valid plugin-id :getRange-end end)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(text/text-range-proxy plugin-id file-id page-id id start end))))
|
(text/text-range-proxy plugin-id file-id page-id id start end))))
|
||||||
@ -1082,13 +1083,13 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (lib-typography-proxy? typography))
|
(not (lib-typography-proxy? typography))
|
||||||
(u/display-not-valid :applyTypography-typography typography)
|
(u/not-valid plugin-id :applyTypography-typography typography)
|
||||||
|
|
||||||
(not (cfh/text-shape? shape))
|
(not (cfh/text-shape? shape))
|
||||||
(u/display-not-valid :applyTypography-shape (:type shape))
|
(u/not-valid plugin-id :applyTypography-shape (:type shape))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :applyTypography "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :applyTypography "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [typography (u/proxy->library-typography typography)]
|
(let [typography (u/proxy->library-typography typography)]
|
||||||
@ -1099,10 +1100,10 @@
|
|||||||
(fn [index]
|
(fn [index]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-int? index))
|
(not (sm/valid-safe-int? index))
|
||||||
(u/display-not-valid :setParentIndex index)
|
(u/not-valid plugin-id :setParentIndex index)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :setParentIndex "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :setParentIndex "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/set-shape-index file-id page-id id index))))
|
(st/emit! (dw/set-shape-index file-id page-id id index))))
|
||||||
@ -1197,7 +1198,7 @@
|
|||||||
(let [value (parser/parse-export value)]
|
(let [value (parser/parse-export value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate ctse/schema:export value))
|
(not (sm/validate ctse/schema:export value))
|
||||||
(u/display-not-valid :export value)
|
(u/not-valid plugin-id :export value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)
|
(let [shape (u/locate-shape file-id page-id id)
|
||||||
@ -1233,7 +1234,7 @@
|
|||||||
(d/patch-object (parser/parse-interaction trigger action delay)))]
|
(d/patch-object (parser/parse-interaction trigger action delay)))]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate ctsi/schema:interaction interaction))
|
(not (sm/validate ctsi/schema:interaction interaction))
|
||||||
(u/display-not-valid :addInteraction interaction)
|
(u/not-valid plugin-id :addInteraction interaction)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)]
|
(let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)]
|
||||||
@ -1244,7 +1245,7 @@
|
|||||||
(fn [interaction]
|
(fn [interaction]
|
||||||
(cond
|
(cond
|
||||||
(not (interaction-proxy? interaction))
|
(not (interaction-proxy? interaction))
|
||||||
(u/display-not-valid :removeInteraction interaction)
|
(u/not-valid plugin-id :removeInteraction interaction)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwi/remove-interaction {:id id} (obj/get interaction "$index")))))
|
(st/emit! (dwi/remove-interaction {:id id} (obj/get interaction "$index")))))
|
||||||
@ -1255,16 +1256,16 @@
|
|||||||
(let [shape (u/locate-shape file-id page-id id)]
|
(let [shape (u/locate-shape file-id page-id id)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :addRulerGuide "Value not a safe number")
|
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
|
||||||
|
|
||||||
(not (contains? #{"vertical" "horizontal"} orientation))
|
(not (contains? #{"vertical" "horizontal"} orientation))
|
||||||
(u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
|
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
|
||||||
|
|
||||||
(not (cfh/frame-shape? shape))
|
(not (cfh/frame-shape? shape))
|
||||||
(u/display-not-valid :addRulerGuide "The shape is not a board")
|
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [id (uuid/next)
|
(let [id (uuid/next)
|
||||||
@ -1285,10 +1286,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (rg/ruler-guide-proxy? value))
|
(not (rg/ruler-guide-proxy? value))
|
||||||
(u/display-not-valid :removeRulerGuide "Guide not provided")
|
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :removeRulerGuide "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [guide (u/proxy->ruler-guide value)]
|
(let [guide (u/proxy->ruler-guide value)]
|
||||||
@ -1298,25 +1299,26 @@
|
|||||||
{:this true
|
{:this true
|
||||||
:get
|
:get
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(let [tokens
|
(let [applied-tokens
|
||||||
(-> (u/locate-shape file-id page-id id)
|
(-> (u/locate-shape file-id page-id id)
|
||||||
(get :applied-tokens))]
|
(get :applied-tokens)
|
||||||
|
(applied-tokens-plugin->applied-tokens))]
|
||||||
(reduce
|
(reduce
|
||||||
(fn [acc [prop name]]
|
(fn [acc [prop name]]
|
||||||
(obj/set! acc (json/write-camel-key prop) name))
|
(obj/set! acc (json/write-camel-key prop) name))
|
||||||
#js {}
|
#js {}
|
||||||
tokens)))}
|
applied-tokens)))}
|
||||||
|
|
||||||
:applyToken
|
:applyToken
|
||||||
{:enumerable false
|
{:enumerable false
|
||||||
:schema [:tuple
|
:schema [:tuple
|
||||||
[:fn token-proxy?]
|
[:fn token-proxy?]
|
||||||
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
|
[:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
|
||||||
:fn (fn [token attrs]
|
:fn (fn [token attrs]
|
||||||
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
|
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
|
||||||
kw-attrs (into #{} (map keyword attrs))]
|
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
|
||||||
(if (some #(not (cto/token-attr? %)) kw-attrs)
|
(if (some #(not (token-attr? %)) kw-attrs)
|
||||||
(u/display-not-valid :applyToken attrs)
|
(u/not-valid plugin-id :applyToken attrs)
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dwta/toggle-token {:token token
|
(dwta/toggle-token {:token token
|
||||||
:attrs kw-attrs
|
:attrs kw-attrs
|
||||||
@ -1338,10 +1340,10 @@
|
|||||||
(fn [pos value]
|
(fn [pos value]
|
||||||
(cond
|
(cond
|
||||||
(not (nat-int? pos))
|
(not (nat-int? pos))
|
||||||
(u/display-not-valid :pos pos)
|
(u/not-valid plugin-id :pos pos)
|
||||||
|
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :value value)
|
(u/not-valid plugin-id :value value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/locate-shape file-id page-id id)
|
(let [shape (u/locate-shape file-id page-id id)
|
||||||
@ -1351,16 +1353,30 @@
|
|||||||
|
|
||||||
:combineAsVariants
|
:combineAsVariants
|
||||||
(fn [ids]
|
(fn [ids]
|
||||||
(if (or (not (seq ids)) (not (every? uuid/parse* ids)))
|
(cond
|
||||||
(u/display-not-valid :ids ids)
|
(or (not (seq ids)) (not (every? uuid/parse* ids)))
|
||||||
(let [shape (u/locate-shape file-id page-id id)
|
(u/not-valid plugin-id :ids ids)
|
||||||
component (u/locate-library-component file-id (:component-id shape))
|
|
||||||
ids (->> ids
|
:else
|
||||||
|
(let [ids (->> ids
|
||||||
(map uuid/uuid)
|
(map uuid/uuid)
|
||||||
(into #{id}))]
|
(into #{id}))
|
||||||
(when (and component (not (ctk/is-variant? component)))
|
valid?
|
||||||
(st/emit!
|
(every?
|
||||||
(dwv/combine-as-variants ids {:trigger "plugin:combine-as-variants"})))))))
|
(fn [id]
|
||||||
|
(let [shape (u/locate-shape file-id page-id id)
|
||||||
|
component (u/locate-library-component file-id (:component-id shape))]
|
||||||
|
(not (ctk/is-variant? component))))
|
||||||
|
ids)]
|
||||||
|
|
||||||
|
(if valid?
|
||||||
|
(let [variant-id (uuid/next)]
|
||||||
|
(st/emit! (dwv/combine-as-variants
|
||||||
|
ids
|
||||||
|
{:trigger "plugin:combine-as-variants" :variant-id variant-id}))
|
||||||
|
(shape-proxy plugin-id variant-id))
|
||||||
|
|
||||||
|
(u/not-valid plugin-id :ids "One of the components is not on the same page or is already a variant"))))))
|
||||||
|
|
||||||
(cond-> (or (cfh/frame-shape? data) (cfh/group-shape? data) (cfh/svg-raw-shape? data) (cfh/bool-shape? data))
|
(cond-> (or (cfh/frame-shape? data) (cfh/group-shape? data) (cfh/svg-raw-shape? data) (cfh/bool-shape? data))
|
||||||
(crc/add-properties!
|
(crc/add-properties!
|
||||||
@ -1375,21 +1391,21 @@
|
|||||||
(fn [^js self children]
|
(fn [^js self children]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :children "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :children "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(not (every? shape-proxy? children))
|
(not (every? shape-proxy? children))
|
||||||
(u/display-not-valid :children "Every children needs to be shape proxies")
|
(u/not-valid plugin-id :children "Every children needs to be shape proxies")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [shape (u/proxy->shape self)
|
(let [shape (u/proxy->shape self)
|
||||||
file-id (obj/get self "$file")
|
file-id (obj/get self "$file")
|
||||||
page-id (obj/get self "$page")
|
page-id (obj/get self "$page")
|
||||||
reverse-fn (if (natural-child-ordering? plugin-id) reverse identity)
|
reverse-fn (if (u/natural-child-ordering? plugin-id) reverse identity)
|
||||||
ids (->> children reverse-fn (map #(obj/get % "$id")))]
|
ids (->> children reverse-fn (map #(obj/get % "$id")))]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(not= (set ids) (set (:shapes shape)))
|
(not= (set ids) (set (:shapes shape)))
|
||||||
(u/display-not-valid :children "Not all children are present in the input")
|
(u/not-valid plugin-id :children "Not all children are present in the input")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dw/reorder-children file-id page-id (:id shape) ids))))))}))
|
(st/emit! (dw/reorder-children file-id page-id (:id shape) ids))))))}))
|
||||||
@ -1405,10 +1421,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :clipContent value)
|
(u/not-valid plugin-id :clipContent value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :clipContent "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :clipContent "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
|
||||||
@ -1421,10 +1437,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(not (boolean? value))
|
(not (boolean? value))
|
||||||
(u/display-not-valid :showInViewMode value)
|
(u/not-valid plugin-id :showInViewMode value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :showInViewMode "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :showInViewMode "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
|
||||||
@ -1456,10 +1472,10 @@
|
|||||||
value (parser/parse-frame-guides value)]
|
value (parser/parse-frame-guides value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector ::ctg/grid] value))
|
(not (sm/validate [:vector ::ctg/grid] value))
|
||||||
(u/display-not-valid :guides value)
|
(u/not-valid plugin-id :guides value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :guides "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :guides "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))}
|
||||||
@ -1481,10 +1497,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? #{:fix :auto} value))
|
(not (contains? #{:fix :auto} value))
|
||||||
(u/display-not-valid :horizontalSizing value)
|
(u/not-valid plugin-id :horizontalSizing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
||||||
@ -1497,10 +1513,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? #{:fix :auto} value))
|
(not (contains? #{:fix :auto} value))
|
||||||
(u/display-not-valid :verticalSizing value)
|
(u/not-valid plugin-id :verticalSizing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}
|
||||||
@ -1524,10 +1540,10 @@
|
|||||||
(let [segments (parser/parse-commands value)]
|
(let [segments (parser/parse-commands value)]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(not (sm/validate path/schema:segments segments))
|
(not (sm/validate path/schema:segments segments))
|
||||||
(u/display-not-valid :content segments)
|
(u/not-valid plugin-id :content segments)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [selrect (path/calc-selrect segments)
|
(let [selrect (path/calc-selrect segments)
|
||||||
@ -1550,13 +1566,13 @@
|
|||||||
value)]
|
value)]
|
||||||
(cond
|
(cond
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(not (cfh/path-shape? data))
|
(not (cfh/path-shape? data))
|
||||||
(u/display-not-valid :content-type type)
|
(u/not-valid plugin-id :content-type type)
|
||||||
|
|
||||||
(not (sm/validate path/schema:segments segments))
|
(not (sm/validate path/schema:segments segments))
|
||||||
(u/display-not-valid :content segments)
|
(u/not-valid plugin-id :content segments)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [selrect (path/calc-selrect segments)
|
(let [selrect (path/calc-selrect segments)
|
||||||
|
|||||||
@ -8,9 +8,10 @@
|
|||||||
(: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.geom.shapes.text :as gst]
|
||||||
[app.common.record :as crc]
|
[app.common.record :as crc]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.fills :as types.fills]
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
@ -118,10 +119,10 @@
|
|||||||
variant (fonts/get-default-variant font)]
|
variant (fonts/get-default-variant font)]
|
||||||
(cond
|
(cond
|
||||||
(not font)
|
(not font)
|
||||||
(u/display-not-valid :fontId value)
|
(u/not-valid plugin-id :fontId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
||||||
@ -140,10 +141,10 @@
|
|||||||
variant (fonts/get-default-variant font)]
|
variant (fonts/get-default-variant font)]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontFamily value)
|
(u/not-valid plugin-id :fontFamily value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
||||||
@ -161,10 +162,10 @@
|
|||||||
variant (fonts/get-variant font value)]
|
variant (fonts/get-variant font value)]
|
||||||
(cond
|
(cond
|
||||||
(not (string? value))
|
(not (string? value))
|
||||||
(u/display-not-valid :fontVariantId value)
|
(u/not-valid plugin-id :fontVariantId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||||
@ -181,10 +182,10 @@
|
|||||||
(let [value (str/trim (dm/str value))]
|
(let [value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (empty? value) (not (re-matches font-size-re value)))
|
(or (empty? value) (not (re-matches font-size-re value)))
|
||||||
(u/display-not-valid :fontSize value)
|
(u/not-valid plugin-id :fontSize value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:font-size value})))))}
|
(st/emit! (dwt/update-text-range id start end {:font-size value})))))}
|
||||||
@ -208,10 +209,10 @@
|
|||||||
(fonts/find-variant font {:weight weight}))]
|
(fonts/find-variant font {:weight weight}))]
|
||||||
(cond
|
(cond
|
||||||
(nil? variant)
|
(nil? variant)
|
||||||
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
|
(u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||||
@ -234,10 +235,10 @@
|
|||||||
(fonts/find-variant font {:style style}))]
|
(fonts/find-variant font {:style style}))]
|
||||||
(cond
|
(cond
|
||||||
(nil? variant)
|
(nil? variant)
|
||||||
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
|
(u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||||
@ -254,10 +255,10 @@
|
|||||||
(let [value (str/trim (dm/str value))]
|
(let [value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (empty? value) (not (re-matches line-height-re value)))
|
(or (empty? value) (not (re-matches line-height-re value)))
|
||||||
(u/display-not-valid :lineHeight value)
|
(u/not-valid plugin-id :lineHeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:line-height value})))))}
|
(st/emit! (dwt/update-text-range id start end {:line-height value})))))}
|
||||||
@ -274,10 +275,10 @@
|
|||||||
(let [value (str/trim (dm/str value))]
|
(let [value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (empty? value) (re-matches letter-spacing-re value))
|
(or (empty? value) (re-matches letter-spacing-re value))
|
||||||
(u/display-not-valid :letterSpacing value)
|
(u/not-valid plugin-id :letterSpacing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:letter-spacing value})))))}
|
(st/emit! (dwt/update-text-range id start end {:letter-spacing value})))))}
|
||||||
@ -293,10 +294,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(and (string? value) (not (re-matches text-transform-re value)))
|
(and (string? value) (not (re-matches text-transform-re value)))
|
||||||
(u/display-not-valid :textTransform value)
|
(u/not-valid plugin-id :textTransform value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:text-transform value}))))}
|
(st/emit! (dwt/update-text-range id start end {:text-transform value}))))}
|
||||||
@ -312,10 +313,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(and (string? value) (re-matches text-decoration-re value))
|
(and (string? value) (re-matches text-decoration-re value))
|
||||||
(u/display-not-valid :textDecoration value)
|
(u/not-valid plugin-id :textDecoration value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:text-decoration value}))))}
|
(st/emit! (dwt/update-text-range id start end {:text-decoration value}))))}
|
||||||
@ -331,10 +332,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(and (string? value) (re-matches text-direction-re value))
|
(and (string? value) (re-matches text-direction-re value))
|
||||||
(u/display-not-valid :direction value)
|
(u/not-valid plugin-id :direction value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :direction "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :direction "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:direction value}))))}
|
(st/emit! (dwt/update-text-range id start end {:direction value}))))}
|
||||||
@ -350,10 +351,10 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(cond
|
(cond
|
||||||
(and (string? value) (re-matches text-align-re value))
|
(and (string? value) (re-matches text-align-re value))
|
||||||
(u/display-not-valid :align value)
|
(u/not-valid plugin-id :align value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :align "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:text-align value}))))}
|
(st/emit! (dwt/update-text-range id start end {:text-align value}))))}
|
||||||
@ -369,11 +370,11 @@
|
|||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(let [value (parser/parse-fills value)]
|
(let [value (parser/parse-fills value)]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/validate [:vector ::cts/fill] value))
|
(not (sm/validate [:vector types.fills/schema:fill] value))
|
||||||
(u/display-not-valid :fills value)
|
(u/not-valid plugin-id :fills value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fills "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-text-range id start end {:fills value})))))}
|
(st/emit! (dwt/update-text-range id start end {:fills value})))))}
|
||||||
@ -400,10 +401,10 @@
|
|||||||
;; editor as well
|
;; editor as well
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (empty? value))
|
(or (not (string? value)) (empty? value))
|
||||||
(u/display-not-valid :characters value)
|
(u/not-valid plugin-id :characters value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :characters "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :characters "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
(contains? (:workspace-editor-state @st/state) id)
|
(contains? (:workspace-editor-state @st/state) id)
|
||||||
(let [shape (u/proxy->shape self)
|
(let [shape (u/proxy->shape self)
|
||||||
@ -427,10 +428,10 @@
|
|||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(cond
|
(cond
|
||||||
(not (contains? #{:auto-width :auto-height :fixed} value))
|
(not (contains? #{:auto-width :auto-height :fixed} value))
|
||||||
(u/display-not-valid :growType value)
|
(u/not-valid plugin-id :growType value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :growType "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :growType "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))}
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))}
|
||||||
@ -444,10 +445,10 @@
|
|||||||
variant (fonts/get-default-variant font)]
|
variant (fonts/get-default-variant font)]
|
||||||
(cond
|
(cond
|
||||||
(not font)
|
(not font)
|
||||||
(u/display-not-valid :fontId value)
|
(u/not-valid plugin-id :fontId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
||||||
@ -461,10 +462,10 @@
|
|||||||
variant (fonts/get-default-variant font)]
|
variant (fonts/get-default-variant font)]
|
||||||
(cond
|
(cond
|
||||||
(not font)
|
(not font)
|
||||||
(u/display-not-valid :fontFamily value)
|
(u/not-valid plugin-id :fontFamily value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
||||||
@ -478,10 +479,10 @@
|
|||||||
variant (fonts/get-variant font value)]
|
variant (fonts/get-variant font value)]
|
||||||
(cond
|
(cond
|
||||||
(not variant)
|
(not variant)
|
||||||
(u/display-not-valid :fontVariantId value)
|
(u/not-valid plugin-id :fontVariantId value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||||
@ -494,10 +495,10 @@
|
|||||||
value (str/trim (dm/str value))]
|
value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (empty? value) (not (re-matches font-size-re value)))
|
(or (empty? value) (not (re-matches font-size-re value)))
|
||||||
(u/display-not-valid :fontSize value)
|
(u/not-valid plugin-id :fontSize value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:font-size value})))))}
|
(st/emit! (dwt/update-attrs id {:font-size value})))))}
|
||||||
@ -516,10 +517,10 @@
|
|||||||
(fonts/find-variant font {:weight weight}))]
|
(fonts/find-variant font {:weight weight}))]
|
||||||
(cond
|
(cond
|
||||||
(nil? variant)
|
(nil? variant)
|
||||||
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
|
(u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||||
@ -538,10 +539,10 @@
|
|||||||
(fonts/find-variant font {:style style}))]
|
(fonts/find-variant font {:style style}))]
|
||||||
(cond
|
(cond
|
||||||
(nil? variant)
|
(nil? variant)
|
||||||
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
|
(u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||||
@ -554,10 +555,10 @@
|
|||||||
value (str/trim (dm/str value))]
|
value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (empty? value) (not (re-matches line-height-re value)))
|
(or (empty? value) (not (re-matches line-height-re value)))
|
||||||
(u/display-not-valid :lineHeight value)
|
(u/not-valid plugin-id :lineHeight value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:line-height value})))))}
|
(st/emit! (dwt/update-attrs id {:line-height value})))))}
|
||||||
@ -570,10 +571,10 @@
|
|||||||
value (str/trim (dm/str value))]
|
value (str/trim (dm/str value))]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches letter-spacing-re value)))
|
(or (not (string? value)) (not (re-matches letter-spacing-re value)))
|
||||||
(u/display-not-valid :letterSpacing value)
|
(u/not-valid plugin-id :letterSpacing value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:letter-spacing value})))))}
|
(st/emit! (dwt/update-attrs id {:letter-spacing value})))))}
|
||||||
@ -585,10 +586,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches text-transform-re value)))
|
(or (not (string? value)) (not (re-matches text-transform-re value)))
|
||||||
(u/display-not-valid :textTransform value)
|
(u/not-valid plugin-id :textTransform value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:text-transform value})))))}
|
(st/emit! (dwt/update-attrs id {:text-transform value})))))}
|
||||||
@ -600,10 +601,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches text-decoration-re value)))
|
(or (not (string? value)) (not (re-matches text-decoration-re value)))
|
||||||
(u/display-not-valid :textDecoration value)
|
(u/not-valid plugin-id :textDecoration value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:text-decoration value})))))}
|
(st/emit! (dwt/update-attrs id {:text-decoration value})))))}
|
||||||
@ -615,10 +616,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches text-direction-re value)))
|
(or (not (string? value)) (not (re-matches text-direction-re value)))
|
||||||
(u/display-not-valid :textDirection value)
|
(u/not-valid plugin-id :textDirection value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :textDirection "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :textDirection "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:text-direction value})))))}
|
(st/emit! (dwt/update-attrs id {:text-direction value})))))}
|
||||||
@ -630,10 +631,10 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches text-align-re value)))
|
(or (not (string? value)) (not (re-matches text-align-re value)))
|
||||||
(u/display-not-valid :align value)
|
(u/not-valid plugin-id :align value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :align "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:text-align value})))))}
|
(st/emit! (dwt/update-attrs id {:text-align value})))))}
|
||||||
@ -645,10 +646,13 @@
|
|||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")]
|
||||||
(cond
|
(cond
|
||||||
(or (not (string? value)) (not (re-matches vertical-align-re value)))
|
(or (not (string? value)) (not (re-matches vertical-align-re value)))
|
||||||
(u/display-not-valid :verticalAlign value)
|
(u/not-valid plugin-id :verticalAlign value)
|
||||||
|
|
||||||
(not (r/check-permission plugin-id "content:write"))
|
(not (r/check-permission plugin-id "content:write"))
|
||||||
(u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission")
|
(u/not-valid plugin-id :verticalAlign "Plugin doesn't have 'content:write' permission")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (dwt/update-attrs id {:vertical-align value})))))}))
|
(st/emit! (dwt/update-attrs id {:vertical-align value})))))}
|
||||||
|
|
||||||
|
{:name "textBounds"
|
||||||
|
:get #(-> % u/proxy->shape gst/shape->bounds format/format-geom-rect)}))
|
||||||
|
|||||||
@ -19,18 +19,58 @@
|
|||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.utils :as u]
|
[app.plugins.utils :as u]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[clojure.datafy :refer [datafy]]))
|
[clojure.datafy :refer [datafy]]
|
||||||
|
[clojure.set :refer [map-invert]]))
|
||||||
|
|
||||||
;; === Token
|
;; === Token
|
||||||
|
|
||||||
|
;; Give more semantic names to the shape attributes that tokens can be applied to
|
||||||
|
(def ^:private map:token-attr->token-attr-plugin
|
||||||
|
{:r1 :border-radius-top-left
|
||||||
|
:r2 :border-radius-top-right
|
||||||
|
:r3 :border-radius-bottom-right
|
||||||
|
:r4 :border-radius-bottom-left
|
||||||
|
|
||||||
|
:p1 :padding-top-left
|
||||||
|
:p2 :padding-top-right
|
||||||
|
:p3 :padding-bottom-right
|
||||||
|
:p4 :padding-bottom-left
|
||||||
|
|
||||||
|
:m1 :margin-top-left
|
||||||
|
:m2 :margin-top-right
|
||||||
|
:m3 :margin-bottom-right
|
||||||
|
:m4 :margin-bottom-left})
|
||||||
|
|
||||||
|
(def ^:private map:token-attr-plugin->token-attr
|
||||||
|
(map-invert map:token-attr->token-attr-plugin))
|
||||||
|
|
||||||
|
(defn token-attr->token-attr-plugin
|
||||||
|
[k]
|
||||||
|
(get map:token-attr->token-attr-plugin k k))
|
||||||
|
|
||||||
|
(defn token-attr-plugin->token-attr
|
||||||
|
[k]
|
||||||
|
(get map:token-attr-plugin->token-attr k k))
|
||||||
|
|
||||||
|
(defn applied-tokens-plugin->applied-tokens
|
||||||
|
[value]
|
||||||
|
(into {}
|
||||||
|
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
|
||||||
|
value))
|
||||||
|
|
||||||
|
(defn token-attr?
|
||||||
|
[attr]
|
||||||
|
(cto/token-attr? (token-attr-plugin->token-attr attr)))
|
||||||
|
|
||||||
(defn- apply-token-to-shapes
|
(defn- apply-token-to-shapes
|
||||||
[file-id set-id id shape-ids attrs]
|
[plugin-id file-id set-id id shape-ids attrs]
|
||||||
|
|
||||||
(let [token (u/locate-token file-id set-id id)]
|
(let [token (u/locate-token file-id set-id id)]
|
||||||
(if (some #(not (cto/token-attr? %)) attrs)
|
(if (some #(not (token-attr? %)) attrs)
|
||||||
(u/display-not-valid :applyToSelected attrs)
|
(u/not-valid plugin-id :applyToSelected attrs)
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dwta/toggle-token {:token token
|
(dwta/toggle-token {:token token
|
||||||
:attrs attrs
|
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
|
||||||
:shape-ids shape-ids
|
:shape-ids shape-ids
|
||||||
:expand-with-children false})))))
|
:expand-with-children false})))))
|
||||||
|
|
||||||
@ -52,7 +92,7 @@
|
|||||||
(defn token-proxy
|
(defn token-proxy
|
||||||
[plugin-id file-id set-id id]
|
[plugin-id file-id set-id id]
|
||||||
(obj/reify {:name "TokenProxy"
|
(obj/reify {:name "TokenProxy"
|
||||||
:on-error u/handle-error}
|
:on-error (u/handle-error plugin-id)}
|
||||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||||
:$file-id {:enumerable false :get (constantly file-id)}
|
:$file-id {:enumerable false :get (constantly file-id)}
|
||||||
:$set-id {:enumerable false :get (constantly set-id)}
|
:$set-id {:enumerable false :get (constantly set-id)}
|
||||||
@ -146,16 +186,16 @@
|
|||||||
{:enumerable false
|
{:enumerable false
|
||||||
:schema [:tuple
|
:schema [:tuple
|
||||||
[:vector [:fn shape-proxy?]]
|
[:vector [:fn shape-proxy?]]
|
||||||
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
|
[:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
|
||||||
:fn (fn [shapes attrs]
|
:fn (fn [shapes attrs]
|
||||||
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
|
(apply-token-to-shapes plugin-id file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
|
||||||
|
|
||||||
:applyToSelected
|
:applyToSelected
|
||||||
{:enumerable false
|
{:enumerable false
|
||||||
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
|
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
|
||||||
:fn (fn [attrs]
|
:fn (fn [attrs]
|
||||||
(let [selected (get-in @st/state [:workspace-local :selected])]
|
(let [selected (get-in @st/state [:workspace-local :selected])]
|
||||||
(apply-token-to-shapes file-id set-id id selected attrs)))}))
|
(apply-token-to-shapes plugin-id file-id set-id id selected attrs)))}))
|
||||||
|
|
||||||
;; === Token Set
|
;; === Token Set
|
||||||
|
|
||||||
@ -165,7 +205,7 @@
|
|||||||
(defn token-set-proxy
|
(defn token-set-proxy
|
||||||
[plugin-id file-id id]
|
[plugin-id file-id id]
|
||||||
(obj/reify {:name "TokenSetProxy"
|
(obj/reify {:name "TokenSetProxy"
|
||||||
:on-error u/handle-error}
|
:on-error (u/handle-error plugin-id)}
|
||||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||||
:$file-id {:enumerable false :get (constantly file-id)}
|
:$file-id {:enumerable false :get (constantly file-id)}
|
||||||
:$id {:enumerable false :get (constantly id)}
|
:$id {:enumerable false :get (constantly id)}
|
||||||
@ -247,15 +287,19 @@
|
|||||||
:addToken
|
:addToken
|
||||||
{:enumerable false
|
{:enumerable false
|
||||||
:schema (fn [args]
|
:schema (fn [args]
|
||||||
[:tuple (-> (cfo/make-token-schema
|
(let [tokens-tree (-> (u/locate-tokens-lib file-id)
|
||||||
(-> (u/locate-tokens-lib file-id) (ctob/get-tokens id))
|
(ctob/get-tokens id)
|
||||||
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
|
;; Convert to the adecuate format for schema
|
||||||
;; Don't allow plugins to set the id
|
(ctob/tokens-tree))]
|
||||||
(sm/dissoc-key :id)
|
[:tuple (-> (cfo/make-token-schema
|
||||||
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
|
tokens-tree
|
||||||
;; and set a converter that changes DTCG types to internal types (:decode/json).
|
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
|
||||||
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
|
;; Don't allow plugins to set the id
|
||||||
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))])
|
(sm/dissoc-key :id)
|
||||||
|
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
|
||||||
|
;; and set a converter that changes DTCG types to internal types (:decode/json).
|
||||||
|
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
|
||||||
|
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
|
||||||
:decode/options {:key-fn identity}
|
:decode/options {:key-fn identity}
|
||||||
:fn (fn [attrs]
|
:fn (fn [attrs]
|
||||||
(let [tokens-lib (u/locate-tokens-lib file-id)
|
(let [tokens-lib (u/locate-tokens-lib file-id)
|
||||||
@ -270,7 +314,7 @@
|
|||||||
(if resolved-value
|
(if resolved-value
|
||||||
(do (st/emit! (dwtl/create-token id token))
|
(do (st/emit! (dwtl/create-token id token))
|
||||||
(token-proxy plugin-id file-id id (:id token)))
|
(token-proxy plugin-id file-id id (:id token)))
|
||||||
(do (u/display-not-valid :addToken (str errors))
|
(do (u/not-valid plugin-id :addToken (str errors))
|
||||||
nil))))}
|
nil))))}
|
||||||
|
|
||||||
:duplicate
|
:duplicate
|
||||||
@ -287,7 +331,7 @@
|
|||||||
(defn token-theme-proxy
|
(defn token-theme-proxy
|
||||||
[plugin-id file-id id]
|
[plugin-id file-id id]
|
||||||
(obj/reify {:name "TokenThemeProxy"
|
(obj/reify {:name "TokenThemeProxy"
|
||||||
:on-error u/handle-error}
|
:on-error (u/handle-error plugin-id)}
|
||||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||||
:$file-id {:enumerable false :get (constantly file-id)}
|
:$file-id {:enumerable false :get (constantly file-id)}
|
||||||
:$id {:enumerable false :get (constantly id)}
|
:$id {:enumerable false :get (constantly id)}
|
||||||
@ -394,7 +438,7 @@
|
|||||||
(defn tokens-catalog
|
(defn tokens-catalog
|
||||||
[plugin-id file-id]
|
[plugin-id file-id]
|
||||||
(obj/reify {:name "TokensCatalog"
|
(obj/reify {:name "TokensCatalog"
|
||||||
:on-error u/handle-error}
|
:on-error (u/handle-error plugin-id)}
|
||||||
:$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)}
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,17 @@
|
|||||||
(: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.json :as json]
|
[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.types.component :as ctk]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.file :as ctf]
|
[app.common.types.file :as ctf]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.util.object :as obj]))
|
[app.util.object :as obj]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defn locate-file
|
(defn locate-file
|
||||||
[id]
|
[id]
|
||||||
@ -221,6 +224,16 @@
|
|||||||
(resolve value)))))]
|
(resolve value)))))]
|
||||||
[ret-v ret-p]))
|
[ret-v ret-p]))
|
||||||
|
|
||||||
|
(defn natural-child-ordering?
|
||||||
|
[plugin-id]
|
||||||
|
(boolean
|
||||||
|
(dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering])))
|
||||||
|
|
||||||
|
(defn throw-validation-errors?
|
||||||
|
[plugin-id]
|
||||||
|
(boolean
|
||||||
|
(dm/get-in @st/state [:plugins :flags plugin-id :throw-validation-errors])))
|
||||||
|
|
||||||
(defn display-not-valid
|
(defn display-not-valid
|
||||||
[code value]
|
[code value]
|
||||||
(if (some? value)
|
(if (some? value)
|
||||||
@ -228,34 +241,54 @@
|
|||||||
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
|
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
(defn throw-not-valid
|
||||||
|
[code value]
|
||||||
|
(if (some? value)
|
||||||
|
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
|
||||||
|
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code))))
|
||||||
|
nil)
|
||||||
|
|
||||||
|
(defn not-valid
|
||||||
|
[plugin-id code value]
|
||||||
|
(if (throw-validation-errors? plugin-id)
|
||||||
|
(throw-not-valid code value)
|
||||||
|
(display-not-valid code value)))
|
||||||
|
|
||||||
(defn reject-not-valid
|
(defn reject-not-valid
|
||||||
[reject code value]
|
[reject code value]
|
||||||
(let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)]
|
(let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)]
|
||||||
(.error js/console msg)
|
(.error js/console msg)
|
||||||
(reject msg)))
|
(reject msg)))
|
||||||
|
|
||||||
(defn coerce
|
|
||||||
"Decodes a javascript object into clj and check against schema. If schema validation fails,
|
|
||||||
displays a not-valid message with the code and hint provided and returns nil."
|
|
||||||
[attrs schema code hint]
|
|
||||||
(let [decoder (sm/decoder schema sm/json-transformer)
|
|
||||||
explainer (sm/explainer schema)
|
|
||||||
attrs (-> attrs json/->clj decoder)]
|
|
||||||
(if-let [explain (explainer attrs)]
|
|
||||||
(display-not-valid code (str hint " " (sm/humanize-explain explain)))
|
|
||||||
attrs)))
|
|
||||||
|
|
||||||
(defn mixed-value
|
(defn mixed-value
|
||||||
[values]
|
[values]
|
||||||
(let [s (set values)]
|
(let [s (set values)]
|
||||||
(if (= (count s) 1) (first s) "mixed")))
|
(if (= (count s) 1) (first s) "mixed")))
|
||||||
|
|
||||||
|
(defn error-messages
|
||||||
|
[explain]
|
||||||
|
(->> (:errors explain)
|
||||||
|
(reduce csm/interpret-schema-problem {})
|
||||||
|
(mapcat (comp seq val))
|
||||||
|
(map (fn [[field {:keys [message]}]]
|
||||||
|
(tr "plugins.validation.message" (name field) message)))
|
||||||
|
(str/join ". ")))
|
||||||
|
|
||||||
(defn handle-error
|
(defn handle-error
|
||||||
"Function to be used in plugin proxies methods to handle errors and print a readable
|
"Function to be used in plugin proxies methods to handle errors and print a readable
|
||||||
message to the console."
|
message to the console."
|
||||||
[cause]
|
[plugin-id]
|
||||||
(display-not-valid (ex-message cause) nil)
|
(fn [cause]
|
||||||
(if-let [explain (-> cause ex-data ::sm/explain)]
|
(let [message
|
||||||
(println (sm/humanize-explain explain))
|
(if-let [explain (-> cause ex-data ::sm/explain)]
|
||||||
(js/console.log (ex-data cause)))
|
(do
|
||||||
(js/console.log (.-stack cause)))
|
(js/console.error (sm/humanize-explain explain))
|
||||||
|
(error-messages explain))
|
||||||
|
(ex-data cause))]
|
||||||
|
(js/console.log (.-stack cause))
|
||||||
|
(not-valid plugin-id :error message))))
|
||||||
|
|
||||||
|
(defn is-main-component-proxy?
|
||||||
|
[p]
|
||||||
|
(when-let [shape (proxy->shape p)]
|
||||||
|
(ctk/main-instance? shape)))
|
||||||
|
|||||||
@ -38,10 +38,10 @@
|
|||||||
new-y (obj/get value "y")]
|
new-y (obj/get value "y")]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? new-x))
|
(not (sm/valid-safe-number? new-x))
|
||||||
(u/display-not-valid :center-x new-x)
|
(u/not-valid plugin-id :center-x new-x)
|
||||||
|
|
||||||
(not (sm/valid-safe-number? new-y))
|
(not (sm/valid-safe-number? new-y))
|
||||||
(u/display-not-valid :center-y new-y)
|
(u/not-valid plugin-id :center-y new-y)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [vb (dm/get-in @st/state [:workspace-local :vbox])
|
(let [vb (dm/get-in @st/state [:workspace-local :vbox])
|
||||||
@ -63,7 +63,7 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(cond
|
(cond
|
||||||
(not (sm/valid-safe-number? value))
|
(not (sm/valid-safe-number? value))
|
||||||
(u/display-not-valid :zoom value)
|
(u/not-valid plugin-id :zoom value)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [z (dm/get-in @st/state [:workspace-local :zoom])]
|
(let [z (dm/get-in @st/state [:workspace-local :zoom])]
|
||||||
@ -87,7 +87,7 @@
|
|||||||
(fn [shapes]
|
(fn [shapes]
|
||||||
(cond
|
(cond
|
||||||
(not (every? ps/shape-proxy? shapes))
|
(not (every? ps/shape-proxy? shapes))
|
||||||
(u/display-not-valid :zoomIntoView "Argument should be valid shapes")
|
(u/not-valid plugin-id :zoomIntoView "Argument should be valid shapes")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [ids (->> shapes
|
(let [ids (->> shapes
|
||||||
|
|||||||
@ -10,84 +10,10 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.common.schema.messages :as csm]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.core :as m]
|
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; --- Handlers Helpers
|
|
||||||
|
|
||||||
(defn- translate-code
|
|
||||||
[code]
|
|
||||||
(if (vector? code)
|
|
||||||
(tr (nth code 0) (i18n/c (nth code 1)))
|
|
||||||
(tr code)))
|
|
||||||
|
|
||||||
(defn- handle-error-fn
|
|
||||||
[props problem]
|
|
||||||
(let [v-fn (:error/fn props)
|
|
||||||
result (v-fn problem)]
|
|
||||||
(if (string? result)
|
|
||||||
{:message result}
|
|
||||||
{:message (or (some-> (get result :code)
|
|
||||||
(translate-code))
|
|
||||||
(get result :message)
|
|
||||||
(tr "errors.invalid-data"))})))
|
|
||||||
|
|
||||||
(defn- handle-error-message
|
|
||||||
[props]
|
|
||||||
{:message (get props :error/message)})
|
|
||||||
|
|
||||||
(defn- handle-error-code
|
|
||||||
[props]
|
|
||||||
(let [code (get props :error/code)]
|
|
||||||
{:message (translate-code code)}))
|
|
||||||
|
|
||||||
(defn- interpret-schema-problem
|
|
||||||
[acc {:keys [schema in value type] :as problem}]
|
|
||||||
(let [props (m/properties schema)
|
|
||||||
tprops (m/type-properties schema)
|
|
||||||
field (or (:error/field props)
|
|
||||||
in)
|
|
||||||
field (if (vector? field)
|
|
||||||
field
|
|
||||||
[field])]
|
|
||||||
|
|
||||||
(if (and (= 1 (count field))
|
|
||||||
(contains? acc (first field)))
|
|
||||||
acc
|
|
||||||
(cond
|
|
||||||
(or (nil? field)
|
|
||||||
(empty? field))
|
|
||||||
acc
|
|
||||||
|
|
||||||
(or (= type :malli.core/missing-key)
|
|
||||||
(nil? value))
|
|
||||||
(assoc-in acc field {:message (tr "errors.field-missing")})
|
|
||||||
|
|
||||||
;; --- CHECK on schema props
|
|
||||||
(contains? props :error/fn)
|
|
||||||
(assoc-in acc field (handle-error-fn props problem))
|
|
||||||
|
|
||||||
(contains? props :error/message)
|
|
||||||
(assoc-in acc field (handle-error-message props))
|
|
||||||
|
|
||||||
(contains? props :error/code)
|
|
||||||
(assoc-in acc field (handle-error-code props))
|
|
||||||
|
|
||||||
;; --- CHECK on type props
|
|
||||||
(contains? tprops :error/fn)
|
|
||||||
(assoc-in acc field (handle-error-fn tprops problem))
|
|
||||||
|
|
||||||
(contains? tprops :error/message)
|
|
||||||
(assoc-in acc field (handle-error-message tprops))
|
|
||||||
|
|
||||||
(contains? tprops :error/code)
|
|
||||||
(assoc-in acc field (handle-error-code tprops))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(assoc-in acc field {:message (tr "errors.invalid-data")})))))
|
|
||||||
|
|
||||||
(defn- use-rerender-fn
|
(defn- use-rerender-fn
|
||||||
[]
|
[]
|
||||||
(let [state (mf/useState 0)
|
(let [state (mf/useState 0)
|
||||||
@ -97,24 +23,6 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(render-fn inc)))))
|
(render-fn inc)))))
|
||||||
|
|
||||||
(defn- apply-validators
|
|
||||||
[validators state errors]
|
|
||||||
(reduce (fn [errors validator-fn]
|
|
||||||
(merge errors (validator-fn errors (:data state))))
|
|
||||||
errors
|
|
||||||
validators))
|
|
||||||
|
|
||||||
(defn- collect-schema-errors
|
|
||||||
[schema validators state]
|
|
||||||
(let [explain (sm/explain schema (:data state))
|
|
||||||
errors (->> (reduce interpret-schema-problem {} (:errors explain))
|
|
||||||
(apply-validators validators state))]
|
|
||||||
|
|
||||||
(-> (:errors state)
|
|
||||||
(merge errors)
|
|
||||||
(d/without-nils)
|
|
||||||
(not-empty))))
|
|
||||||
|
|
||||||
(defn- wrap-update-schema-fn
|
(defn- wrap-update-schema-fn
|
||||||
[f {:keys [schema validators]}]
|
[f {:keys [schema validators]}]
|
||||||
(fn [& args]
|
(fn [& args]
|
||||||
@ -124,7 +32,7 @@
|
|||||||
|
|
||||||
errors
|
errors
|
||||||
(when-not valid?
|
(when-not valid?
|
||||||
(collect-schema-errors schema validators state))
|
(csm/collect-schema-errors schema validators state))
|
||||||
|
|
||||||
extra-errors
|
extra-errors
|
||||||
(not-empty (:extra-errors state))]
|
(not-empty (:extra-errors state))]
|
||||||
|
|||||||
@ -462,3 +462,9 @@
|
|||||||
(defn print-last-exception
|
(defn print-last-exception
|
||||||
[]
|
[]
|
||||||
(some-> errors/last-exception ex/print-throwable))
|
(some-> errors/last-exception ex/print-throwable))
|
||||||
|
|
||||||
|
|
||||||
|
(defn ^:export dbg
|
||||||
|
[o]
|
||||||
|
(app.common.pprint/pprint o {:level 100 :length 100}))
|
||||||
|
|
||||||
|
|||||||
@ -338,77 +338,6 @@ msgstr "You're going to restore %s."
|
|||||||
msgid "dashboard-restore-file-confirmation.title"
|
msgid "dashboard-restore-file-confirmation.title"
|
||||||
msgstr "Restore file"
|
msgstr "Restore file"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:103
|
|
||||||
msgid "dashboard.access-tokens.copied-success"
|
|
||||||
msgstr "Copied token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:189
|
|
||||||
msgid "dashboard.access-tokens.create"
|
|
||||||
msgstr "Generate new token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:64
|
|
||||||
msgid "dashboard.access-tokens.create.success"
|
|
||||||
msgstr "Access token created successfully."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:286
|
|
||||||
msgid "dashboard.access-tokens.empty.add-one"
|
|
||||||
msgstr "Press the button \"Generate new token\" to generate one."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:285
|
|
||||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
|
||||||
msgstr "You have no tokens so far."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:135
|
|
||||||
msgid "dashboard.access-tokens.expiration-180-days"
|
|
||||||
msgstr "180 days"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:132
|
|
||||||
msgid "dashboard.access-tokens.expiration-30-days"
|
|
||||||
msgstr "30 days"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:133
|
|
||||||
msgid "dashboard.access-tokens.expiration-60-days"
|
|
||||||
msgstr "60 days"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:134
|
|
||||||
msgid "dashboard.access-tokens.expiration-90-days"
|
|
||||||
msgstr "90 days"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:131
|
|
||||||
msgid "dashboard.access-tokens.expiration-never"
|
|
||||||
msgstr "Never"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:268
|
|
||||||
msgid "dashboard.access-tokens.expired-on"
|
|
||||||
msgstr "Expired on %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:269
|
|
||||||
msgid "dashboard.access-tokens.expires-on"
|
|
||||||
msgstr "Expires on %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:267
|
|
||||||
msgid "dashboard.access-tokens.no-expiration"
|
|
||||||
msgstr "No expiration date"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:184
|
|
||||||
msgid "dashboard.access-tokens.personal"
|
|
||||||
msgstr "Personal access tokens"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:185
|
|
||||||
msgid "dashboard.access-tokens.personal.description"
|
|
||||||
msgstr ""
|
|
||||||
"Personal access tokens function like an alternative to our login/password "
|
|
||||||
"authentication system and can be used to allow an application to access the "
|
|
||||||
"internal Penpot API"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:142
|
|
||||||
msgid "dashboard.access-tokens.token-will-expire"
|
|
||||||
msgstr "The token will expire on %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:143
|
|
||||||
msgid "dashboard.access-tokens.token-will-not-expire"
|
|
||||||
msgstr "The token has no expiration date"
|
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/placeholder.cljs:41
|
#: src/app/main/ui/dashboard/placeholder.cljs:41
|
||||||
msgid "dashboard.add-file"
|
msgid "dashboard.add-file"
|
||||||
msgstr "Add file"
|
msgstr "Add file"
|
||||||
@ -2138,6 +2067,237 @@ msgstr "Resolved value:"
|
|||||||
msgid "inspect.tabs.styles.variants-panel"
|
msgid "inspect.tabs.styles.variants-panel"
|
||||||
msgstr "Variant Properties"
|
msgstr "Variant Properties"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:189
|
||||||
|
msgid "integrations.access-tokens.create"
|
||||||
|
msgstr "Create new access token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:286
|
||||||
|
msgid "integrations.access-tokens.empty.add-one"
|
||||||
|
msgstr "Press the button \"Create new access token\" to generate one."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:285
|
||||||
|
msgid "integrations.access-tokens.empty.no-access-tokens"
|
||||||
|
msgstr "You have no tokens so far."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:184
|
||||||
|
msgid "integrations.access-tokens.personal"
|
||||||
|
msgstr "Personal access tokens"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:185
|
||||||
|
msgid "integrations.access-tokens.personal.description"
|
||||||
|
msgstr ""
|
||||||
|
"Personal access tokens function like an alternative to our login/password "
|
||||||
|
"authentication system and can be used to allow an application to access the "
|
||||||
|
"internal Penpot API"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158
|
||||||
|
msgid "integrations.copy-to-clipboard"
|
||||||
|
msgstr "Copy to clipboard"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:432
|
||||||
|
msgid "integrations.create-access-token.title"
|
||||||
|
msgstr "Create access token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:433
|
||||||
|
msgid "integrations.create-access-token.title.created"
|
||||||
|
msgstr "Access token created"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:257
|
||||||
|
msgid "integrations.delete-token.accept"
|
||||||
|
msgstr "Delete token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:256
|
||||||
|
msgid "integrations.delete-token.message"
|
||||||
|
msgstr "Are you sure you want to delete this token?"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:255
|
||||||
|
msgid "integrations.delete-token.title"
|
||||||
|
msgstr "Delete token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:135
|
||||||
|
msgid "integrations.expiration-180-days"
|
||||||
|
msgstr "180 days"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:132
|
||||||
|
msgid "integrations.expiration-30-days"
|
||||||
|
msgstr "30 days"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:133
|
||||||
|
msgid "integrations.expiration-60-days"
|
||||||
|
msgstr "60 days"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:134
|
||||||
|
msgid "integrations.expiration-90-days"
|
||||||
|
msgstr "90 days"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.expiration-never"
|
||||||
|
msgstr "Never"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:268
|
||||||
|
msgid "integrations.expired-on"
|
||||||
|
msgstr "Expired on %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:269
|
||||||
|
msgid "integrations.expires-on"
|
||||||
|
msgstr "Expires on %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:267
|
||||||
|
msgid "integrations.no-expiration"
|
||||||
|
msgstr "No expiration date"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:130
|
||||||
|
msgid "integrations.expiration-date.label"
|
||||||
|
msgstr "Expiration date"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:290
|
||||||
|
msgid "integrations.generate-mcp-key.title"
|
||||||
|
msgstr "Generate MCP key"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:291
|
||||||
|
msgid "integrations.generate-mcp-key.title.created"
|
||||||
|
msgstr "MCP key generated"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:113
|
||||||
|
msgid "integrations.info.mcp-client-config"
|
||||||
|
msgstr "Add this configuration to your MCP client (e.g. ~/.mcp.json)."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:183
|
||||||
|
msgid "integrations.info.mcp-server"
|
||||||
|
msgstr "The Penpot MCP Server enables MCP clients to interact directly with Penpot design files."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.token.info.non-recuperable"
|
||||||
|
msgstr "This unique token is non-recuperable. If you lose it, you will need to create a new one."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.mcp-key.info.non-recuperable"
|
||||||
|
msgstr "This unique MCP key is non-recoverable. If you lose it, you will need to create a new one."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:336
|
||||||
|
msgid "integrations.mcp-server.title"
|
||||||
|
msgstr "MCP Server"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:336
|
||||||
|
msgid "integrations.mcp-server.title.beta"
|
||||||
|
msgstr "Beta"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:347
|
||||||
|
msgid "integrations.mcp-server.description"
|
||||||
|
msgstr "The Penpot MCP Server enables MCP clients to interact directly with Penpot design files."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:353
|
||||||
|
msgid "integrations.mcp-server.status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:370
|
||||||
|
msgid "integrations.mcp-server.status.disabled"
|
||||||
|
msgstr "Disabled"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:370
|
||||||
|
msgid "integrations.mcp-server.status.enabled"
|
||||||
|
msgstr "Enabled"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:363
|
||||||
|
msgid "integrations.mcp-server.status.expired.0"
|
||||||
|
msgstr "The MCP key used to connect to the MCP server has expired. As a result, the connection cannot be established."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:368
|
||||||
|
msgid "integrations.mcp-server.status.expired.1"
|
||||||
|
msgstr "Please regenerate the MCP key and update your client configuration with the new key."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:415
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.copy"
|
||||||
|
msgstr "Copy link"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:422
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.help"
|
||||||
|
msgstr "How to configure MCP clients"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:405
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.info"
|
||||||
|
msgstr "This is the server url you'll need to configure your MCP client in order to connect it to the Penpot MCP server."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:387
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.regenerate"
|
||||||
|
msgstr "Regenerate MCP key"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:381
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.title"
|
||||||
|
msgstr "MCP key"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:388
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.tootip"
|
||||||
|
msgstr "The MCP key is needed for the MCP client set up"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:124
|
||||||
|
msgid "integrations.name.label"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:126
|
||||||
|
msgid "integrations.name.placeholder"
|
||||||
|
msgstr "The name can help to know what's the token for"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
msgid "integrations.notification.success.token-copied"
|
||||||
|
msgstr "Copied token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
msgid "integrations.notification.success.mcp-key-copied"
|
||||||
|
msgstr "MCP key copied"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:64
|
||||||
|
msgid "integrations.notification.success.created"
|
||||||
|
msgstr "Token created successfully"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:327
|
||||||
|
msgid "integrations.notification.success.copied-link"
|
||||||
|
msgstr "Link copied to clipboard"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:293
|
||||||
|
msgid "integrations.notification.success.mcp-server-disabled"
|
||||||
|
msgstr "MCP server disabled"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:299
|
||||||
|
msgid "integrations.notification.success.mcp-server-enabled"
|
||||||
|
msgstr "MCP server enabled"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:317
|
||||||
|
msgid "integrations.regenerate-mcp-key.info"
|
||||||
|
msgstr "Regenerating the MCP key will immediately revoke the current one. Any application using it will stop working."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:317
|
||||||
|
msgid "integrations.regenerate-mcp-key.title"
|
||||||
|
msgstr "Regenerate MCP key"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:318
|
||||||
|
msgid "integrations.regenerate-mcp-key.title.created"
|
||||||
|
msgstr "MCP key regenerated"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:480
|
||||||
|
msgid "integrations.title"
|
||||||
|
msgstr "Integrations"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
msgid "integrations.token.will-expire"
|
||||||
|
msgstr "The token will expire on %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
msgid "integrations.mcp-key.will-expire"
|
||||||
|
msgstr "The MCP key will expire on %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
msgid "integrations.token.will-not-expire"
|
||||||
|
msgstr "The token has no expiration date"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
msgid "integrations.mcp-key.will-not-expire"
|
||||||
|
msgstr "The MCP key has no expiration date"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/comments.cljs:96
|
#: src/app/main/ui/dashboard/comments.cljs:96
|
||||||
msgid "label.mark-all-as-read"
|
msgid "label.mark-all-as-read"
|
||||||
msgstr "Mark all as read"
|
msgstr "Mark all as read"
|
||||||
@ -2354,6 +2514,9 @@ msgstr "Director"
|
|||||||
msgid "labels.discard"
|
msgid "labels.discard"
|
||||||
msgstr "Discard"
|
msgstr "Discard"
|
||||||
|
|
||||||
|
msgid "labels.dismiss"
|
||||||
|
msgstr "Dismiss"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||||
msgid "labels.download"
|
msgid "labels.download"
|
||||||
msgstr "Download %s"
|
msgstr "Download %s"
|
||||||
@ -2489,6 +2652,10 @@ msgstr "Info"
|
|||||||
msgid "labels.installed-fonts"
|
msgid "labels.installed-fonts"
|
||||||
msgstr "Installed fonts"
|
msgstr "Installed fonts"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/sidebar.cljs:123
|
||||||
|
msgid "labels.integrations"
|
||||||
|
msgstr "Integrations"
|
||||||
|
|
||||||
#: src/app/main/ui/static.cljs:405
|
#: src/app/main/ui/static.cljs:405
|
||||||
msgid "labels.internal-error.desc-message-first"
|
msgid "labels.internal-error.desc-message-first"
|
||||||
msgstr "Something bad happened."
|
msgstr "Something bad happened."
|
||||||
@ -3148,30 +3315,6 @@ msgstr "Change email"
|
|||||||
msgid "modals.change-email.title"
|
msgid "modals.change-email.title"
|
||||||
msgstr "Change your email"
|
msgstr "Change your email"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158
|
|
||||||
msgid "modals.create-access-token.copy-token"
|
|
||||||
msgstr "Copy token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:130
|
|
||||||
msgid "modals.create-access-token.expiration-date.label"
|
|
||||||
msgstr "Expiration date"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:124
|
|
||||||
msgid "modals.create-access-token.name.label"
|
|
||||||
msgstr "Name"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:126
|
|
||||||
msgid "modals.create-access-token.name.placeholder"
|
|
||||||
msgstr "The name can help to know what's the token for"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:178
|
|
||||||
msgid "modals.create-access-token.submit-label"
|
|
||||||
msgstr "Create token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
|
||||||
msgid "modals.create-access-token.title"
|
|
||||||
msgstr "Generate access token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs:1127
|
#: src/app/main/ui/dashboard/team.cljs:1127
|
||||||
msgid "modals.create-webhook.submit-label"
|
msgid "modals.create-webhook.submit-label"
|
||||||
msgstr "Create webhook"
|
msgstr "Create webhook"
|
||||||
@ -3188,18 +3331,6 @@ msgstr "Payload URL"
|
|||||||
msgid "modals.create-webhook.url.placeholder"
|
msgid "modals.create-webhook.url.placeholder"
|
||||||
msgstr "https://example.com/postreceive"
|
msgstr "https://example.com/postreceive"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:257
|
|
||||||
msgid "modals.delete-acces-token.accept"
|
|
||||||
msgstr "Delete token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:256
|
|
||||||
msgid "modals.delete-acces-token.message"
|
|
||||||
msgstr "Are you sure you want to delete this token?"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:255
|
|
||||||
msgid "modals.delete-acces-token.title"
|
|
||||||
msgstr "Delete token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs:56
|
#: src/app/main/ui/settings/delete_account.cljs:56
|
||||||
msgid "modals.delete-account.cancel"
|
msgid "modals.delete-account.cancel"
|
||||||
msgstr "Cancel and keep my account"
|
msgstr "Cancel and keep my account"
|
||||||
@ -3699,6 +3830,12 @@ msgstr "Invitation sent successfully"
|
|||||||
msgid "notifications.invitation-link-copied"
|
msgid "notifications.invitation-link-copied"
|
||||||
msgstr "Invitation link copied"
|
msgstr "Invitation link copied"
|
||||||
|
|
||||||
|
msgid "notifications.mcp.active-in-another-tab"
|
||||||
|
msgstr "MCP is active in another tab. Switch here?"
|
||||||
|
|
||||||
|
msgid "notifications.mcp.active-in-this-tab"
|
||||||
|
msgstr "MCP is now active in this tab."
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs:24
|
#: src/app/main/ui/settings/delete_account.cljs:24
|
||||||
msgid "notifications.profile-deletion-not-allowed"
|
msgid "notifications.profile-deletion-not-allowed"
|
||||||
msgstr "You can't delete your profile. Reassign your teams before proceed."
|
msgstr "You can't delete your profile. Reassign your teams before proceed."
|
||||||
@ -5093,14 +5230,14 @@ msgstr "Shared Libraries - %s - Penpot"
|
|||||||
msgid "title.default"
|
msgid "title.default"
|
||||||
msgstr "Penpot - Design Freedom for Teams"
|
msgstr "Penpot - Design Freedom for Teams"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
|
||||||
msgid "title.settings.access-tokens"
|
|
||||||
msgstr "Profile - Access tokens"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/feedback.cljs:161
|
#: src/app/main/ui/settings/feedback.cljs:161
|
||||||
msgid "title.settings.feedback"
|
msgid "title.settings.feedback"
|
||||||
msgstr "Give feedback - Penpot"
|
msgstr "Give feedback - Penpot"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:278
|
||||||
|
msgid "title.settings.integrations"
|
||||||
|
msgstr "Integrations - Penpot"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/notifications.cljs:45
|
#: src/app/main/ui/settings/notifications.cljs:45
|
||||||
msgid "title.settings.notifications"
|
msgid "title.settings.notifications"
|
||||||
msgstr "Notifications - Penpot"
|
msgstr "Notifications - Penpot"
|
||||||
@ -5594,6 +5731,18 @@ msgstr "Hide rulers"
|
|||||||
msgid "workspace.header.menu.hide-textpalette"
|
msgid "workspace.header.menu.hide-textpalette"
|
||||||
msgstr "Hide fonts palette"
|
msgstr "Hide fonts palette"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.plugin.status.connect"
|
||||||
|
msgstr "Connect"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.plugin.status.disconnect"
|
||||||
|
msgstr "Disconnect"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.server.status.enabled"
|
||||||
|
msgstr "Manage (Status: enabled)"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.server.status.disabled"
|
||||||
|
msgstr "Manage (Status: disabled)"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/main_menu.cljs:884
|
#: src/app/main/ui/workspace/main_menu.cljs:884
|
||||||
msgid "workspace.header.menu.option.edit"
|
msgid "workspace.header.menu.option.edit"
|
||||||
msgstr "Edit"
|
msgstr "Edit"
|
||||||
@ -5606,6 +5755,9 @@ msgstr "File"
|
|||||||
msgid "workspace.header.menu.option.help-info"
|
msgid "workspace.header.menu.option.help-info"
|
||||||
msgstr "Help & info"
|
msgstr "Help & info"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.option.mcp"
|
||||||
|
msgstr "MCP server"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/main_menu.cljs:916
|
#: src/app/main/ui/workspace/main_menu.cljs:916
|
||||||
#, unused
|
#, unused
|
||||||
msgid "workspace.header.menu.option.power-up"
|
msgid "workspace.header.menu.option.power-up"
|
||||||
@ -7234,11 +7386,14 @@ msgid "workspace.plugins.empty-plugins"
|
|||||||
msgstr "No plugins installed yet"
|
msgstr "No plugins installed yet"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/plugins.cljs:193
|
#: src/app/main/ui/workspace/plugins.cljs:193
|
||||||
msgid "workspace.plugins.error.manifest"
|
msgid "workspace.plugins.error.manifest"
|
||||||
msgstr "The plugin manifest is incorrect."
|
msgstr "The plugin manifest is incorrect."
|
||||||
|
|
||||||
|
msgid "plugins.validation.message"
|
||||||
|
msgstr "Field %s is invalid: %s"
|
||||||
|
|
||||||
#: src/app/main/data/plugins.cljs:105, src/app/main/ui/workspace/main_menu.cljs:766, src/app/main/ui/workspace/plugins.cljs:84
|
#: src/app/main/data/plugins.cljs:105, src/app/main/ui/workspace/main_menu.cljs:766, src/app/main/ui/workspace/plugins.cljs:84
|
||||||
msgid "workspace.plugins.error.need-editor"
|
msgid "workspace.plugins.error.need-editor"
|
||||||
msgstr "You need to be an editor to use this plugin"
|
msgstr "You need to be an editor to use this plugin"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/plugins.cljs:189
|
#: src/app/main/ui/workspace/plugins.cljs:189
|
||||||
|
|||||||
@ -347,77 +347,6 @@ msgstr "Vas a restaurar %s."
|
|||||||
msgid "dashboard-restore-file-confirmation.title"
|
msgid "dashboard-restore-file-confirmation.title"
|
||||||
msgstr "Restaurar archivo"
|
msgstr "Restaurar archivo"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:103
|
|
||||||
msgid "dashboard.access-tokens.copied-success"
|
|
||||||
msgstr "Token copiado"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:189
|
|
||||||
msgid "dashboard.access-tokens.create"
|
|
||||||
msgstr "Generar nuevo token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:64
|
|
||||||
msgid "dashboard.access-tokens.create.success"
|
|
||||||
msgstr "Access token creado con éxito."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:286
|
|
||||||
msgid "dashboard.access-tokens.empty.add-one"
|
|
||||||
msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:285
|
|
||||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
|
||||||
msgstr "Todavía no tienes ningún token."
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:135
|
|
||||||
msgid "dashboard.access-tokens.expiration-180-days"
|
|
||||||
msgstr "180 días"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:132
|
|
||||||
msgid "dashboard.access-tokens.expiration-30-days"
|
|
||||||
msgstr "30 días"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:133
|
|
||||||
msgid "dashboard.access-tokens.expiration-60-days"
|
|
||||||
msgstr "60 días"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:134
|
|
||||||
msgid "dashboard.access-tokens.expiration-90-days"
|
|
||||||
msgstr "90 días"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:131
|
|
||||||
msgid "dashboard.access-tokens.expiration-never"
|
|
||||||
msgstr "Nunca"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:268
|
|
||||||
msgid "dashboard.access-tokens.expired-on"
|
|
||||||
msgstr "Expiró el %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:269
|
|
||||||
msgid "dashboard.access-tokens.expires-on"
|
|
||||||
msgstr "Expira el %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:267
|
|
||||||
msgid "dashboard.access-tokens.no-expiration"
|
|
||||||
msgstr "Sin fecha de expiración"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:184
|
|
||||||
msgid "dashboard.access-tokens.personal"
|
|
||||||
msgstr "Access tokens personales"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:185
|
|
||||||
msgid "dashboard.access-tokens.personal.description"
|
|
||||||
msgstr ""
|
|
||||||
"Los access tokens personales funcionan como una alternativa a nuestro "
|
|
||||||
"sistema de autenticación usuario/password y se pueden usar para permitir a "
|
|
||||||
"otras aplicaciones acceso a la API interna de Penpot"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:142
|
|
||||||
msgid "dashboard.access-tokens.token-will-expire"
|
|
||||||
msgstr "El token expirará el %s"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:143
|
|
||||||
msgid "dashboard.access-tokens.token-will-not-expire"
|
|
||||||
msgstr "El token no tiene fecha de expiración"
|
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/placeholder.cljs:41
|
#: src/app/main/ui/dashboard/placeholder.cljs:41
|
||||||
msgid "dashboard.add-file"
|
msgid "dashboard.add-file"
|
||||||
msgstr "Añadir archivo"
|
msgstr "Añadir archivo"
|
||||||
@ -2105,6 +2034,237 @@ msgstr "Valor resuelto:"
|
|||||||
msgid "inspect.tabs.styles.variants-panel"
|
msgid "inspect.tabs.styles.variants-panel"
|
||||||
msgstr "Propiedades de las variantes"
|
msgstr "Propiedades de las variantes"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:189
|
||||||
|
msgid "integrations.access-tokens.create"
|
||||||
|
msgstr "Crear nuevo token de acceso"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:286
|
||||||
|
msgid "integrations.access-tokens.empty.add-one"
|
||||||
|
msgstr "Pulsa el botón \"Crear nuevo token de accesso\" para generar uno."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:285
|
||||||
|
msgid "integrations.access-tokens.empty.no-access-tokens"
|
||||||
|
msgstr "Todavía no tienes ningún token."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:184
|
||||||
|
msgid "integrations.access-tokens.personal"
|
||||||
|
msgstr "Tokens de acceso personales"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:185
|
||||||
|
msgid "integrations.access-tokens.personal.description"
|
||||||
|
msgstr ""
|
||||||
|
"Los tokens de accesso personales funcionan como una alternativa a nuestro "
|
||||||
|
"sistema de autenticación usuario/password y se pueden usar para permitir a "
|
||||||
|
"otras aplicaciones acceso a la API interna de Penpot"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158
|
||||||
|
msgid "integrations.copy-to-clipboard"
|
||||||
|
msgstr "Copiar al portapapeles"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:432
|
||||||
|
msgid "integrations.create-access-token.title"
|
||||||
|
msgstr "Crear token de accesso"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:433
|
||||||
|
msgid "integrations.create-access-token.title.created"
|
||||||
|
msgstr "Token de acceso creado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:257
|
||||||
|
msgid "integrations.delete-token.accept"
|
||||||
|
msgstr "Borrar token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:256
|
||||||
|
msgid "integrations.delete-token.message"
|
||||||
|
msgstr "¿Seguro que deseas borrar este token?"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:255
|
||||||
|
msgid "integrations.delete-token.title"
|
||||||
|
msgstr "Borrar token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:135
|
||||||
|
msgid "integrations.expiration-180-days"
|
||||||
|
msgstr "180 días"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:132
|
||||||
|
msgid "integrations.expiration-30-days"
|
||||||
|
msgstr "30 días"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:133
|
||||||
|
msgid "integrations.expiration-60-days"
|
||||||
|
msgstr "60 días"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:134
|
||||||
|
msgid "integrations.expiration-90-days"
|
||||||
|
msgstr "90 días"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.expiration-never"
|
||||||
|
msgstr "Nunca"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:268
|
||||||
|
msgid "integrations.expired-on"
|
||||||
|
msgstr "Expiró el %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:269
|
||||||
|
msgid "integrations.expires-on"
|
||||||
|
msgstr "Expira el %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:267
|
||||||
|
msgid "integrations.no-expiration"
|
||||||
|
msgstr "Sin fecha de expiración"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:130
|
||||||
|
msgid "integrations.expiration-date.label"
|
||||||
|
msgstr "Fecha de expiración"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:290
|
||||||
|
msgid "integrations.generate-mcp-key.title"
|
||||||
|
msgstr "Generar clave MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:291
|
||||||
|
msgid "integrations.generate-mcp-key.title.created"
|
||||||
|
msgstr "Clave MCP generada"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:113
|
||||||
|
msgid "integrations.info.mcp-client-config"
|
||||||
|
msgstr "Agrega esta configuración a tu cliente MCP (por ejemplo, ~/.mcp.json)."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:183
|
||||||
|
msgid "integrations.info.mcp-server"
|
||||||
|
msgstr "El servidor MCP de Penpot permite a los clientes MCP interactuar directamente con los archivos de diseño de Penpot."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.token.info.non-recuperable"
|
||||||
|
msgstr "Esta clave única no es recuperable. Si la pierdes, tendrás que crear una nueva."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:131
|
||||||
|
msgid "integrations.mcp-key.info.non-recuperable"
|
||||||
|
msgstr "Esta clave MCP única no es recuperable. Si la pierdes, tendrás que crear una nueva."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:336
|
||||||
|
msgid "integrations.mcp-server.title"
|
||||||
|
msgstr "Servidor MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:336
|
||||||
|
msgid "integrations.mcp-server.title.beta"
|
||||||
|
msgstr "Beta"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:347
|
||||||
|
msgid "integrations.mcp-server.description"
|
||||||
|
msgstr "El servidor MCP de Penpot permite que los clientes MCP interactúen directamente con los archivos de diseño de Penpot."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:353
|
||||||
|
msgid "integrations.mcp-server.status"
|
||||||
|
msgstr "Estado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:370
|
||||||
|
msgid "integrations.mcp-server.status.enabled"
|
||||||
|
msgstr "Habilitado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:370
|
||||||
|
msgid "integrations.mcp-server.status.disabled"
|
||||||
|
msgstr "Deshabilitado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:363
|
||||||
|
msgid "integrations.mcp-server.status.expired.0"
|
||||||
|
msgstr "La clave MCP utilizada para conectarse al servidor MCP ha expirado. Como resultado, no se puede establecer la conexión."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:368
|
||||||
|
msgid "integrations.mcp-server.status.expired.1"
|
||||||
|
msgstr "Por favor, regenera la clave MCP y actualiza la configuración de tu cliente con la nueva clave."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:415
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.copy"
|
||||||
|
msgstr "Copiar enlace"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:422
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.help"
|
||||||
|
msgstr "Cómo configurar clientes MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:405
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.info"
|
||||||
|
msgstr "Esta es la URL del servidor que necesitarás configurar en tu cliente MCP para conectarlo al servidor MCP de Penpot."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:387
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.regenerate"
|
||||||
|
msgstr "Regenerar clave MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:381
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.title"
|
||||||
|
msgstr "Clave MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:388
|
||||||
|
msgid "integrations.mcp-server.mcp-keys.tootip"
|
||||||
|
msgstr "La clave MCP es necesaria para la configuración del cliente MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:124
|
||||||
|
msgid "integrations.name.label"
|
||||||
|
msgstr "Nombre"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:126
|
||||||
|
msgid "integrations.name.placeholder"
|
||||||
|
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
msgid "integrations.notification.success.token-copied"
|
||||||
|
msgstr "Token copiado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:103
|
||||||
|
msgid "integrations.notification.success.mcp-key-copied"
|
||||||
|
msgstr "Clave MCP copiada"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:64
|
||||||
|
msgid "integrations.notification.success.created"
|
||||||
|
msgstr "Token creado con éxito"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:327
|
||||||
|
msgid "integrations.notification.success.copied-link"
|
||||||
|
msgstr "Enlace copiado al portapapeles"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:293
|
||||||
|
msgid "integrations.notification.success.mcp-server-disabled"
|
||||||
|
msgstr "Servidor MCP deshabilitado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:299
|
||||||
|
msgid "integrations.notification.success.mcp-server-enabled"
|
||||||
|
msgstr "Servidor MCP habilitado"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:317
|
||||||
|
msgid "integrations.regenerate-mcp-key.info"
|
||||||
|
msgstr "Regenerar la clave MCP revocará inmediatamente la actual. Cualquier aplicación que la esté utilizando dejará de funcionar."
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:317
|
||||||
|
msgid "integrations.regenerate-mcp-key.title"
|
||||||
|
msgstr "Regenerar clave MCP"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:318
|
||||||
|
msgid "integrations.regenerate-mcp-key.title.created"
|
||||||
|
msgstr "Clave MCP regenerada"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:480
|
||||||
|
msgid "integrations.title"
|
||||||
|
msgstr "Integraciones"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
msgid "integrations.token.will-expire"
|
||||||
|
msgstr "El token expirará el %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:142
|
||||||
|
msgid "integrations.mcp-key.will-expire"
|
||||||
|
msgstr "La clave MCP expirará el %s"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
msgid "integrations.token.will-not-expire"
|
||||||
|
msgstr "El token no tiene fecha de expiración"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:143
|
||||||
|
msgid "integrations.mcp-key.will-not-expire"
|
||||||
|
msgstr "La clave MCP no tiene fecha de expiración"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/comments.cljs:96
|
#: src/app/main/ui/dashboard/comments.cljs:96
|
||||||
msgid "label.mark-all-as-read"
|
msgid "label.mark-all-as-read"
|
||||||
msgstr "Marcar todo como leído"
|
msgstr "Marcar todo como leído"
|
||||||
@ -2321,6 +2481,9 @@ msgstr "Director"
|
|||||||
msgid "labels.discard"
|
msgid "labels.discard"
|
||||||
msgstr "Descartar"
|
msgstr "Descartar"
|
||||||
|
|
||||||
|
msgid "labels.dismiss"
|
||||||
|
msgstr "Cancelar"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||||
msgid "labels.download"
|
msgid "labels.download"
|
||||||
msgstr "Descargar %s"
|
msgstr "Descargar %s"
|
||||||
@ -2456,6 +2619,10 @@ msgstr "Información"
|
|||||||
msgid "labels.installed-fonts"
|
msgid "labels.installed-fonts"
|
||||||
msgstr "Fuentes instaladas"
|
msgstr "Fuentes instaladas"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/sidebar.cljs:123
|
||||||
|
msgid "labels.integrations"
|
||||||
|
msgstr "Integraciones"
|
||||||
|
|
||||||
#: src/app/main/ui/static.cljs:405
|
#: src/app/main/ui/static.cljs:405
|
||||||
msgid "labels.internal-error.desc-message-first"
|
msgid "labels.internal-error.desc-message-first"
|
||||||
msgstr "Ha ocurrido algo extraño."
|
msgstr "Ha ocurrido algo extraño."
|
||||||
@ -3111,30 +3278,6 @@ msgstr "Cambiar correo"
|
|||||||
msgid "modals.change-email.title"
|
msgid "modals.change-email.title"
|
||||||
msgstr "Cambiar tu correo"
|
msgstr "Cambiar tu correo"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158
|
|
||||||
msgid "modals.create-access-token.copy-token"
|
|
||||||
msgstr "Copiar token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:130
|
|
||||||
msgid "modals.create-access-token.expiration-date.label"
|
|
||||||
msgstr "Fecha de expiración"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:124
|
|
||||||
msgid "modals.create-access-token.name.label"
|
|
||||||
msgstr "Nombre"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:126
|
|
||||||
msgid "modals.create-access-token.name.placeholder"
|
|
||||||
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:178
|
|
||||||
msgid "modals.create-access-token.submit-label"
|
|
||||||
msgstr "Crear token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
|
||||||
msgid "modals.create-access-token.title"
|
|
||||||
msgstr "Generar access token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs:1127
|
#: src/app/main/ui/dashboard/team.cljs:1127
|
||||||
msgid "modals.create-webhook.submit-label"
|
msgid "modals.create-webhook.submit-label"
|
||||||
msgstr "Crear webhook"
|
msgstr "Crear webhook"
|
||||||
@ -3151,18 +3294,6 @@ msgstr "Payload URL"
|
|||||||
msgid "modals.create-webhook.url.placeholder"
|
msgid "modals.create-webhook.url.placeholder"
|
||||||
msgstr "https://example.com/postreceive"
|
msgstr "https://example.com/postreceive"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:257
|
|
||||||
msgid "modals.delete-acces-token.accept"
|
|
||||||
msgstr "Borrar token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:256
|
|
||||||
msgid "modals.delete-acces-token.message"
|
|
||||||
msgstr "¿Seguro que deseas borrar este token?"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:255
|
|
||||||
msgid "modals.delete-acces-token.title"
|
|
||||||
msgstr "Borrar token"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs:56
|
#: src/app/main/ui/settings/delete_account.cljs:56
|
||||||
msgid "modals.delete-account.cancel"
|
msgid "modals.delete-account.cancel"
|
||||||
msgstr "Cancelar y mantener mi cuenta"
|
msgstr "Cancelar y mantener mi cuenta"
|
||||||
@ -3666,6 +3797,12 @@ msgstr "Invitación enviada con éxito"
|
|||||||
msgid "notifications.invitation-link-copied"
|
msgid "notifications.invitation-link-copied"
|
||||||
msgstr "Enlace de invitacion copiado"
|
msgstr "Enlace de invitacion copiado"
|
||||||
|
|
||||||
|
msgid "notifications.mcp.active-in-another-tab"
|
||||||
|
msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?"
|
||||||
|
|
||||||
|
msgid "notifications.mcp.active-in-this-tab"
|
||||||
|
msgstr "MCP está ahora activo en esta pestaña."
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs:24
|
#: src/app/main/ui/settings/delete_account.cljs:24
|
||||||
msgid "notifications.profile-deletion-not-allowed"
|
msgid "notifications.profile-deletion-not-allowed"
|
||||||
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
||||||
@ -5068,14 +5205,14 @@ msgstr "Bibliotecas Compartidas - %s - Penpot"
|
|||||||
msgid "title.default"
|
msgid "title.default"
|
||||||
msgstr "Penpot - Diseño Libre para Equipos"
|
msgstr "Penpot - Diseño Libre para Equipos"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
|
||||||
msgid "title.settings.access-tokens"
|
|
||||||
msgstr "Perfil - Access tokens"
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/feedback.cljs:161
|
#: src/app/main/ui/settings/feedback.cljs:161
|
||||||
msgid "title.settings.feedback"
|
msgid "title.settings.feedback"
|
||||||
msgstr "Danos tu opinión - Penpot"
|
msgstr "Danos tu opinión - Penpot"
|
||||||
|
|
||||||
|
#: src/app/main/ui/settings/integrations.cljs:278
|
||||||
|
msgid "title.settings.integrations"
|
||||||
|
msgstr "Integraciones - Penpot"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/notifications.cljs:45
|
#: src/app/main/ui/settings/notifications.cljs:45
|
||||||
msgid "title.settings.notifications"
|
msgid "title.settings.notifications"
|
||||||
msgstr "Notificaciones - Penpot"
|
msgstr "Notificaciones - Penpot"
|
||||||
@ -5571,6 +5708,18 @@ msgstr "Ocultar reglas"
|
|||||||
msgid "workspace.header.menu.hide-textpalette"
|
msgid "workspace.header.menu.hide-textpalette"
|
||||||
msgstr "Ocultar paleta de textos"
|
msgstr "Ocultar paleta de textos"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.plugin.status.connect"
|
||||||
|
msgstr "Conectar"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.plugin.status.disconnect"
|
||||||
|
msgstr "Desconectar"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.server.status.enabled"
|
||||||
|
msgstr "Gestionar (estado: habilitado)"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.mcp.server.status.disabled"
|
||||||
|
msgstr "Gestionar (estado: deshabilitado)"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/main_menu.cljs:884
|
#: src/app/main/ui/workspace/main_menu.cljs:884
|
||||||
msgid "workspace.header.menu.option.edit"
|
msgid "workspace.header.menu.option.edit"
|
||||||
msgstr "Editar"
|
msgstr "Editar"
|
||||||
@ -5583,6 +5732,9 @@ msgstr "Archivo"
|
|||||||
msgid "workspace.header.menu.option.help-info"
|
msgid "workspace.header.menu.option.help-info"
|
||||||
msgstr "Ayuda e información"
|
msgstr "Ayuda e información"
|
||||||
|
|
||||||
|
msgid "workspace.header.menu.option.mcp"
|
||||||
|
msgstr "Servidor MCP"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/main_menu.cljs:906
|
#: src/app/main/ui/workspace/main_menu.cljs:906
|
||||||
msgid "workspace.header.menu.option.preferences"
|
msgid "workspace.header.menu.option.preferences"
|
||||||
msgstr "Preferencias"
|
msgstr "Preferencias"
|
||||||
|
|||||||
2
mcp/.gitignore
vendored
2
mcp/.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
*.tgz
|
||||||
*.bak
|
*.bak
|
||||||
*.orig
|
*.orig
|
||||||
temp
|
temp
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
# Penpot MCP Project Overview - Updated
|
# Penpot MCP Project Overview - Updated
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
This project is a Model Context Protocol (MCP) server for Penpot integration. It provides a TypeScript-based server that can be used to extend Penpot's functionality through custom tools with bidirectional WebSocket communication.
|
This project is a Model Context Protocol (MCP) server for Penpot integration.
|
||||||
|
The MCP server communicates with a Penpot plugin via WebSockets, allowing
|
||||||
|
the MCP server to send tasks to the plugin and receive results,
|
||||||
|
enabling advanced AI-driven features in Penpot.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
@ -13,21 +16,22 @@ This project is a Model Context Protocol (MCP) server for Penpot integration. It
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
```
|
```
|
||||||
penpot-mcp/
|
/ (project root)
|
||||||
├── common/ # Shared type definitions
|
├── packages/common/ # Shared type definitions
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── index.ts # Exports for shared types
|
│ │ ├── index.ts # Exports for shared types
|
||||||
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
||||||
│ └── package.json # @penpot-mcp/common package
|
│ └── package.json # @penpot-mcp/common package
|
||||||
├── mcp-server/ # Main MCP server implementation
|
├── packages/server/ # Main MCP server implementation
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── index.ts # Main server entry point
|
│ │ ├── index.ts # Main server entry point
|
||||||
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
|
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
|
||||||
│ │ ├── PluginTask.ts # Now supports result promises
|
│ │ ├── PluginTask.ts # Now supports result promises
|
||||||
│ │ ├── tasks/ # PluginTask implementations
|
│ │ ├── tasks/ # PluginTask implementations
|
||||||
│ │ └── tools/ # Tool implementations
|
│ │ └── tools/ # Tool implementations
|
||||||
|
| ├── data/ # Contains resources, such as API info and prompts
|
||||||
│ └── package.json # Includes @penpot-mcp/common dependency
|
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||||
├── penpot-plugin/ # Penpot plugin with response capability
|
├── packages/plugin/ # Penpot plugin with response capability
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding
|
│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding
|
||||||
│ │ └── plugin.ts # Now sends task responses back to server
|
│ │ └── plugin.ts # Now sends task responses back to server
|
||||||
@ -37,55 +41,24 @@ penpot-mcp/
|
|||||||
|
|
||||||
## Key Tasks
|
## Key Tasks
|
||||||
|
|
||||||
|
### Adjusting the System Prompt
|
||||||
|
|
||||||
|
The system prompt file is located in `packages/server/data/initial_instructions.md`.
|
||||||
|
|
||||||
### Adding a new Tool
|
### Adding a new Tool
|
||||||
|
|
||||||
1. Implement the tool class in `mcp-server/src/tools/` following the `Tool` interface.
|
1. Implement the tool class in `packages/server/src/tools/` following the `Tool` interface.
|
||||||
IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally.
|
IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally.
|
||||||
2. Register the tool in `PenpotMcpServer`.
|
2. Register the tool in `PenpotMcpServer`.
|
||||||
|
|
||||||
Look at `PrintTextTool` as an example.
|
Tools can be associated with a `PluginTask` that is executed in the plugin.
|
||||||
|
Many tools build on `ExecuteCodePluginTask`, as many operations can be reduced to code execution.
|
||||||
Many tools are linked to tasks that are handled in the plugin, i.e. they have an associated `PluginTask` implementation in `mcp-server/src/tasks/`.
|
|
||||||
|
|
||||||
### Adding a new PluginTask
|
### Adding a new PluginTask
|
||||||
|
|
||||||
1. Implement the input data interface for the task in `common/src/types.ts`.
|
1. Implement the input data interface for the task in `packages/common/src/types.ts`.
|
||||||
2. Implement the `PluginTask` class in `mcp-server/src/tasks/`.
|
2. Implement the `PluginTask` class in `packages/server/src/tasks/`.
|
||||||
3. Implement the corresponding task handler class in the plugin (`penpot-plugin/src/task-handlers/`).
|
3. Implement the corresponding task handler class in the plugin (`packages/plugin/src/task-handlers/`).
|
||||||
* In the success case, call `task.sendSuccess`.
|
* In the success case, call `task.sendSuccess`.
|
||||||
* In the failure case, just throw an exception, which will be handled centrally!
|
* In the failure case, just throw an exception, which will be handled centrally!
|
||||||
* Look at `PrintTextTaskHandler` as an example.
|
4. Register the task handler in `packages/plugin/src/plugin.ts` in the `taskHandlers` list.
|
||||||
4. Register the task handler in `penpot-plugin/src/plugin.ts` in the `taskHandlers` list.
|
|
||||||
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
### Enhanced WebSocket Protocol
|
|
||||||
- **Request Format**: `{id: string, task: string, params: any}`
|
|
||||||
- **Response Format**: `{id: string, result: {success: boolean, error?: string, data?: any}}`
|
|
||||||
- **Request/Response Correlation**: Using unique UUIDs for task tracking
|
|
||||||
- **Timeout Handling**: 30-second timeout with automatic cleanup
|
|
||||||
- **Type Safety**: Shared definitions via @penpot-mcp/common package
|
|
||||||
|
|
||||||
### Core Classes
|
|
||||||
- **PenpotMcpServer**: Enhanced with pending task tracking and response handling
|
|
||||||
- **PluginTask**: Now creates result promises that resolve when plugin responds
|
|
||||||
- **Tool implementations**: Now properly await task completion and report results
|
|
||||||
- **Plugin handlers**: Send structured responses back to server
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
1. **Bidirectional Communication**: Plugin now responds with success/failure status
|
|
||||||
2. **Task Result Promises**: Every executePluginTask() sets and returns a promise
|
|
||||||
3. **Error Reporting**: Failed tasks properly report error messages to tools
|
|
||||||
4. **Shared Type Safety**: Common package ensures consistency across projects
|
|
||||||
5. **Timeout Protection**: Tasks don't hang indefinitely (30s limit)
|
|
||||||
6. **Request Correlation**: Unique IDs match requests to responses
|
|
||||||
|
|
||||||
## Task Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM Tool Call → MCP Server → WebSocket (Request) → Plugin → Penpot API
|
|
||||||
↑ ↓
|
|
||||||
Tool Response ← MCP Server ← WebSocket (Response) ← Plugin Result
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
# whether to use the project's gitignore file to ignore files
|
# whether to use the project's gitignore file to ignore files
|
||||||
# Added on 2025-04-07
|
# Added on 2025-04-07
|
||||||
ignore_all_files_in_gitignore: true
|
ignore_all_files_in_gitignore: true
|
||||||
@ -19,7 +17,7 @@ read_only: false
|
|||||||
|
|
||||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
# Below is the complete list of tools for convenience.
|
# Below is the complete list of tools for convenience.
|
||||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
# execute `uv run scripts/print_tool_overview.py`.
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
#
|
#
|
||||||
# * `activate_project`: Activates a project by name.
|
# * `activate_project`: Activates a project by name.
|
||||||
@ -62,15 +60,17 @@ excluded_tools: []
|
|||||||
# (contrary to the memories, which are loaded on demand).
|
# (contrary to the memories, which are loaded on demand).
|
||||||
initial_prompt: |
|
initial_prompt: |
|
||||||
IMPORTANT: You use an idiomatic, object-oriented style.
|
IMPORTANT: You use an idiomatic, object-oriented style.
|
||||||
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
|
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
|
||||||
rather than mere functions (i.e. use the strategy pattern, for example).
|
rather than mere functions (i.e. use the strategy pattern, for example).
|
||||||
|
|
||||||
Comments:
|
Always read the "project_overview" memory.
|
||||||
|
|
||||||
|
Comments:
|
||||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||||
|
|
||||||
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
|
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
|
||||||
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
|
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
|
||||||
required for sentences).
|
required for sentences).
|
||||||
# the name by which the project can be referenced within Serena
|
# the name by which the project can be referenced within Serena
|
||||||
project_name: "penpot-mcp"
|
project_name: "penpot-mcp"
|
||||||
@ -128,3 +128,39 @@ encoding: utf-8
|
|||||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
languages:
|
languages:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||||
|
# such as docstrings or parameter information.
|
||||||
|
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||||
|
# If null or missing, use the setting from the global configuration.
|
||||||
|
symbol_info_budget:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
|
# is activated post-init, an error will be returned.
|
||||||
|
language_backend:
|
||||||
|
|
||||||
|
# line ending convention to use when writing source files.
|
||||||
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||||
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||||
|
line_ending:
|
||||||
|
|
||||||
|
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
read_only_memory_patterns: []
|
||||||
|
|
||||||
|
# list of regex patterns for memories to completely ignore.
|
||||||
|
# Matching memories will not appear in list_memories or activate_project output
|
||||||
|
# and cannot be accessed via read_memory or write_memory.
|
||||||
|
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
# Example: ["_archive/.*", "_episodes/.*"]
|
||||||
|
ignored_memory_patterns: []
|
||||||
|
|
||||||
|
# advanced configuration option allowing to configure language server-specific options.
|
||||||
|
# Maps the language key to the options.
|
||||||
|
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||||
|
# No documentation on options means no options are available.
|
||||||
|
ls_specific_settings: {}
|
||||||
|
|||||||
129
mcp/README.md
129
mcp/README.md
@ -50,31 +50,65 @@ Follow the steps below to enable the integration.
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
The project requires [Node.js](https://nodejs.org/) (tested with v22.x
|
The project requires [Node.js](https://nodejs.org/) (tested with v22.x).
|
||||||
with corepack).
|
|
||||||
|
|
||||||
Following the installation of Node.js, the tools `pnpm` and `npx`
|
### 1. Starting the MCP Server and the Plugin Server
|
||||||
should be available in your terminal. For ensure corepack installed
|
|
||||||
and enabled correctly, just execute the `./scripts/setup`.
|
|
||||||
|
|
||||||
It is also required to have `caddy` executeable in the path, it is
|
#### Running a Released Version via npx
|
||||||
used for start a local server for generate types documentation from
|
|
||||||
the current branch. If you want to run it outside devenv where all
|
|
||||||
dependencies are already provided, please download caddy from
|
|
||||||
[here](https://caddyserver.com/download).
|
|
||||||
|
|
||||||
You should probably be using penpot devenv, where all this
|
The easiest way to launch the servers is to use `npx` to run the appropriate
|
||||||
dependencies are already present and correctly setup. But nothing
|
version that matches your Penpot version.
|
||||||
prevents you execute this outside of devenv if you satisfy the
|
|
||||||
specified dependencies.
|
|
||||||
|
|
||||||
|
* If you are using the latest Penpot release, e.g. as served on [design.penpot.app](https://design.penpot.app), run:
|
||||||
|
```shell
|
||||||
|
npx -y @penpot/mcp@latest
|
||||||
|
```
|
||||||
|
* If you are participating in the MCP beta-test, which uses [test-mcp.penpot.dev](https://test-mcp.penpot.dev), run:
|
||||||
|
```shell
|
||||||
|
npx -y @penpot/mcp@beta
|
||||||
|
```
|
||||||
|
|
||||||
### 1. Build & Launch the MCP Server and the Plugin Server
|
Once the servers are running, continue with step 2.
|
||||||
|
|
||||||
If it's your first execution, install the required dependencies:
|
#### Running the Source Version from the Repository
|
||||||
|
|
||||||
|
The tools `corepack` and `npx` should be available in your terminal.
|
||||||
|
|
||||||
|
On Windows, use the Git Bash terminal to ensure compatibility with the provided scripts.
|
||||||
|
|
||||||
|
##### Clone the Appropriate Branch of the Repository
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The branches are subject to change in the future.
|
||||||
|
> Be sure to check the instructions for the latest information on which branch to use.
|
||||||
|
|
||||||
|
Clone the Penpot repository, using the proper branch depending on the
|
||||||
|
version of Penpot you want to use the MCP server with.
|
||||||
|
|
||||||
|
* For the current Penpot release 2.14, use the `mcp-prod-2.14.1` branch:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/penpot/penpot.git --branch mcp-prod-2.14.1 --depth 1
|
||||||
|
```
|
||||||
|
|
||||||
|
* For the MCP beta-test, use the `staging` branch:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/penpot/penpot.git --branch staging --depth 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Then change into the `mcp` directory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd penpot/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Build & Launch the MCP Server and the Plugin Server
|
||||||
|
|
||||||
|
If it's your first execution, install the required dependencies.
|
||||||
|
(If you are using the Penpot devenv, this step is not necessary, as dependencies are already installed.)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd mcp/
|
|
||||||
./scripts/setup
|
./scripts/setup
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -86,9 +120,9 @@ pnpm run bootstrap
|
|||||||
|
|
||||||
This bootstrap command will:
|
This bootstrap command will:
|
||||||
|
|
||||||
* install dependencies for all components (`pnpm -r run install`)
|
* install dependencies for all components
|
||||||
* build all components (`pnpm -r run build`)
|
* build all components
|
||||||
* start all components (`pnpm -r --parallel run start`)
|
* start all components
|
||||||
|
|
||||||
### 2. Load the Plugin in Penpot and Establish the Connection
|
### 2. Load the Plugin in Penpot and Establish the Connection
|
||||||
|
|
||||||
@ -123,6 +157,19 @@ This bootstrap command will:
|
|||||||
|
|
||||||
### 3. Connect an MCP Client
|
### 3. Connect an MCP Client
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Use an appropriate model.**
|
||||||
|
>
|
||||||
|
> We recommend that you ...
|
||||||
|
> * use the most capable model at your disposal.
|
||||||
|
> You will achieve the best results with frontier models,
|
||||||
|
> especially when dealing with more complex tasks.
|
||||||
|
> Weaker models, including most locally hosted ones,
|
||||||
|
> are unlikely to produce usable results for anything beyond simple tasks.
|
||||||
|
> * use a vision language model (VLM), as many design tasks necessitate visual
|
||||||
|
> inspection.
|
||||||
|
> (If you are using a standard commercial model, it almost certainly supports vision already.)
|
||||||
|
|
||||||
By default, the server runs on port 4401 and provides:
|
By default, the server runs on port 4401 and provides:
|
||||||
|
|
||||||
- **Modern Streamable HTTP endpoint**: `http://localhost:4401/mcp`
|
- **Modern Streamable HTTP endpoint**: `http://localhost:4401/mcp`
|
||||||
@ -140,14 +187,9 @@ NOTE: only relevant if you are executing this outside of devenv
|
|||||||
|
|
||||||
The `mcp-remote` package can proxy stdio transport to HTTP/SSE,
|
The `mcp-remote` package can proxy stdio transport to HTTP/SSE,
|
||||||
allowing clients that support only stdio to connect to the MCP server indirectly.
|
allowing clients that support only stdio to connect to the MCP server indirectly.
|
||||||
|
Use it to provide the launch command for your MCP client as follows:
|
||||||
|
|
||||||
1. Install `mcp-remote` globally if you haven't already:
|
npx -y mcp-remote http://localhost:4401/mcp --allow-http
|
||||||
|
|
||||||
npm install -g mcp-remote
|
|
||||||
|
|
||||||
2. Use `mcp-remote` to provide the launch command for your MCP client:
|
|
||||||
|
|
||||||
npx -y mcp-remote http://localhost:4401/sse --allow-http
|
|
||||||
|
|
||||||
#### Example: Claude Desktop
|
#### Example: Claude Desktop
|
||||||
|
|
||||||
@ -170,7 +212,7 @@ Add a `penpot` entry under `mcpServers` with the following content:
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"penpot": {
|
"penpot": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-remote", "http://localhost:4401/sse", "--allow-http"]
|
"args": ["-y", "mcp-remote", "http://localhost:4401/mcp", "--allow-http"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,37 +237,36 @@ To add the Penpot MCP server to a Claude Code project, issue the command
|
|||||||
|
|
||||||
This repository is a monorepo containing four main components:
|
This repository is a monorepo containing four main components:
|
||||||
|
|
||||||
1. **Common Types** (`common/`):
|
1. **Common Types** (`packages/common/`):
|
||||||
- Shared TypeScript definitions for request/response protocol
|
- Shared TypeScript definitions for request/response protocol
|
||||||
- Ensures type safety across server and plugin components
|
- Ensures type safety across server and plugin components
|
||||||
|
|
||||||
2. **Penpot MCP Server** (`mcp-server/`):
|
2. **Penpot MCP Server** (`packages/server/`):
|
||||||
- Provides MCP tools to LLMs for Penpot interaction
|
- Provides MCP tools to LLMs for Penpot interaction
|
||||||
- Runs a WebSocket server accepting connections from the Penpot MCP plugin
|
- Runs a WebSocket server accepting connections from the Penpot MCP plugin
|
||||||
- Implements request/response correlation with unique task IDs
|
- Implements request/response correlation with unique task IDs
|
||||||
- Handles task timeouts and proper error reporting
|
- Handles task timeouts and proper error reporting
|
||||||
|
|
||||||
3. **Penpot MCP Plugin** (`penpot-plugin/`):
|
3. **Penpot MCP Plugin** (`packages/plugin/`):
|
||||||
- Connects to the MCP server via WebSocket
|
- Connects to the MCP server via WebSocket
|
||||||
- Executes tasks in Penpot using the Plugin API
|
- Executes tasks in Penpot using the Plugin API
|
||||||
- Sends structured responses back to the server#
|
- Sends structured responses back to the server#
|
||||||
|
|
||||||
4. **Helper Scripts** (`python-scripts/`):
|
4. **Types Generator** (`types-generator/`):
|
||||||
- Python scripts that prepare data for the MCP server (development use)
|
- Generates data on API types for the MCP server (development use)
|
||||||
|
|
||||||
The core components are written in TypeScript, rendering interactions with the
|
The core components are written in TypeScript, rendering interactions with the
|
||||||
Penpot Plugin API both natural and type-safe.
|
Penpot Plugin API both natural and type-safe.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The Penpot MCP server can be configured using environment variables. All configuration
|
The Penpot MCP server can be configured using environment variables.
|
||||||
options use the `PENPOT_MCP_` prefix for consistency.
|
|
||||||
|
|
||||||
### Server Configuration
|
### Server Configuration
|
||||||
|
|
||||||
| Environment Variable | Description | Default |
|
| Environment Variable | Description | Default |
|
||||||
|------------------------------------|----------------------------------------------------------------------------|--------------|
|
|------------------------------------|----------------------------------------------------------------------------|--------------|
|
||||||
| `PENPOT_MCP_SERVER_LISTEN_ADDRESS` | Address on which the MCP server listens (binds to) | `localhost` |
|
| `PENPOT_MCP_SERVER_HOST` | Address on which the MCP server listens (binds to) | `localhost` |
|
||||||
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
|
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
|
||||||
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
|
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
|
||||||
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
|
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
|
||||||
@ -243,7 +284,7 @@ options use the `PENPOT_MCP_` prefix for consistency.
|
|||||||
|
|
||||||
| Environment Variable | Description | Default |
|
| Environment Variable | Description | Default |
|
||||||
|-------------------------------------------|-----------------------------------------------------------------------------------------|--------------|
|
|-------------------------------------------|-----------------------------------------------------------------------------------------|--------------|
|
||||||
| `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS` | Address on which the plugin web server listens (single address or comma-separated list) | (local only) |
|
| `PENPOT_MCP_PLUGIN_SERVER_HOST` | Address on which the plugin web server listens (single address or comma-separated list) | (local only) |
|
||||||
|
|
||||||
## Beyond Local Execution
|
## Beyond Local Execution
|
||||||
|
|
||||||
@ -263,3 +304,17 @@ you may set the following environment variables to configure the two servers
|
|||||||
* `PENPOT_MCP_SERVER_ADDRESS=<your-address>`: This sets the hostname or IP address
|
* `PENPOT_MCP_SERVER_ADDRESS=<your-address>`: This sets the hostname or IP address
|
||||||
where the MCP server can be reached. The Penpot MCP Plugin uses this to construct
|
where the MCP server can be reached. The Penpot MCP Plugin uses this to construct
|
||||||
the WebSocket URL as `ws://<your-address>:<port>` (default port: `4402`).
|
the WebSocket URL as `ws://<your-address>:<port>` (default port: `4402`).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
* The [contribution guidelines for Penpot](../CONTRIBUTING.md) apply
|
||||||
|
* Auto-formatting: Use `pnpm run fmt`
|
||||||
|
* Generating API type data: See [types-generator/README.md](types-generator/README.md)
|
||||||
|
* Versioning: Use `bash scripts/set-version` to set the version for the MCP package (in `package.json`).
|
||||||
|
- Ensure that at least the major, minor and patch components of the version are always up-to-date.
|
||||||
|
- The MCP plugin assumes that a mismatch between the MCP version and the Penpot version (as returned by the API)
|
||||||
|
indicates incompatibility, resulting in the display of a warning message in the plugin UI.
|
||||||
|
* Packaging and publishing:
|
||||||
|
1. Ensure release version is set correctly in package.json (call `bash scripts/set-version` to update it automatically)
|
||||||
|
2. Create npm package: `bash scripts/pack` (creates `penpot-mcp-<version>.tgz` for publishing)
|
||||||
|
3. Publish to npm: `npm publish penpot-mcp-<version>.tgz --access public`
|
||||||
|
|||||||
31
mcp/bin/mcp-local.js
Normal file
31
mcp/bin/mcp-local.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
function run(command) {
|
||||||
|
execSync(command, { cwd: root, stdio: "inherit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml
|
||||||
|
// and restored here before bootstrap runs.
|
||||||
|
const distLock = path.join(root, "pnpm-lock.dist.yaml");
|
||||||
|
const lock = path.join(root, "pnpm-lock.yaml");
|
||||||
|
if (fs.existsSync(distLock)) {
|
||||||
|
fs.copyFileSync(distLock, lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
run("corepack pnpm run bootstrap");
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
console.error(
|
||||||
|
"corepack is required but was not found. It ships with Node.js >= 16."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.exit(error.status ?? 1);
|
||||||
|
}
|
||||||
@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-meta",
|
"name": "@penpot/mcp",
|
||||||
"version": "1.0.0",
|
"version": "2.15.0-rc.1.153",
|
||||||
"description": "",
|
"description": "MCP server for Penpot integration",
|
||||||
|
"bin": {
|
||||||
|
"penpot-mcp": "./bin/mcp-local.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm -r run build",
|
||||||
"build:multi-user": "pnpm -r run build:multi-user",
|
"build:multi-user": "pnpm -r run build:multi-user",
|
||||||
"build:types": "bash ./scripts/build-types",
|
"build:types": "bash ./scripts/build-types",
|
||||||
"start": "pnpm -r --parallel run start",
|
"start": "pnpm -r --parallel run start",
|
||||||
"start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user",
|
"start:multi-user": "pnpm -r --parallel run start:multi-user",
|
||||||
"bootstrap": "pnpm -r install && pnpm run build && pnpm run start",
|
"bootstrap": "pnpm -r install && pnpm run build && pnpm run start",
|
||||||
"bootstrap:multi-user": "pnpm -r install && pnpm run build:multi-user && pnpm run start:multi-user",
|
"bootstrap:multi-user": "pnpm -r install && pnpm run build && pnpm run start:multi-user",
|
||||||
"fmt": "prettier --write packages/",
|
"fmt": "prettier --write packages/",
|
||||||
"fmt:check": "prettier --check packages/"
|
"fmt:check": "prettier --check packages/"
|
||||||
},
|
},
|
||||||
@ -17,8 +20,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot.git"
|
"url": "https://github.com/penpot/penpot.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"prettier": "^3.0.0"
|
"prettier": "^3.0.0"
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"description": "Shared type definitions and interfaces for Penpot MCP",
|
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build --clean && tsc --build",
|
"build": "tsc --build --clean && tsc --build",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
|
|||||||
@ -3,12 +3,87 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Penpot plugin example</title>
|
<title>Penpot MCP Plugin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button type="button" data-appearance="secondary" data-handler="connect-mcp">Connect to MCP server</button>
|
<div class="plugin-container">
|
||||||
|
<div id="version-warning" class="version-warning" hidden>
|
||||||
|
<span id="version-warning-text" class="body-s"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="connection-status" style="margin-top: 10px; font-size: 12px; color: #666">Not connected</div>
|
<div id="connection-status" class="status-pill" data-status="idle">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span id="status-text" class="body-s">Not connected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="connect-btn" data-appearance="primary" data-handler="connect-mcp">
|
||||||
|
Connect MCP Server
|
||||||
|
</button>
|
||||||
|
<button type="button" id="disconnect-btn" data-appearance="secondary" data-handler="disconnect-mcp" hidden>
|
||||||
|
Disconnect MCP Server
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<details class="collapsible-section" id="execution-status">
|
||||||
|
<summary class="collapsible-header">
|
||||||
|
<svg
|
||||||
|
class="collapsible-arrow"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
|
||||||
|
</svg>
|
||||||
|
<span class="body-s">Execution status</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="collapsible-body">
|
||||||
|
<span class="body-s tool-label">Current task</span>
|
||||||
|
<div class="tool-display">
|
||||||
|
<svg
|
||||||
|
class="tool-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span id="current-task" class="body-s">---</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-section-header">
|
||||||
|
<span class="body-s tool-label">Executed code</span>
|
||||||
|
<button type="button" id="copy-code-btn" class="copy-btn" title="Copy code" disabled>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="executed-code"
|
||||||
|
class="code-textarea"
|
||||||
|
readonly
|
||||||
|
placeholder="No code executed yet..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -5,9 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite build --watch --config vite.config.ts",
|
"start": "vite build --watch --config vite.config.ts",
|
||||||
"start:multi-user": "cross-env MULTI_USER_MODE=true vite build --watch --config vite.config.ts",
|
"start:multi-user": "pnpm run start",
|
||||||
"build": "tsc && vite build --config vite.release.config.ts",
|
"build": "tsc && vite build --config vite.release.config.ts",
|
||||||
"build:multi-user": "tsc && cross-env MULTI_USER_MODE=true vite build --config vite.release.config.ts",
|
|
||||||
"types:check": "tsc --noEmit",
|
"types:check": "tsc --noEmit",
|
||||||
"clean": "rm -rf dist/"
|
"clean": "rm -rf dist/"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
mcp/packages/plugin/public/icon.jpg
Normal file
BIN
mcp/packages/plugin/public/icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Penpot MCP Plugin",
|
"name": "Penpot MCP Plugin",
|
||||||
"code": "plugin.js",
|
"code": "plugin.js",
|
||||||
|
"icon": "icon.jpg",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"description": "This plugin enables interaction with the Penpot MCP server",
|
"description": "This plugin enables interaction with the Penpot MCP server",
|
||||||
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
|
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Board, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape } from "@penpot/plugin-types";
|
import { Board, Bounds, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape, Text } from "@penpot/plugin-types";
|
||||||
|
|
||||||
export class PenpotUtils {
|
export class PenpotUtils {
|
||||||
/**
|
/**
|
||||||
@ -189,6 +189,24 @@ export class PenpotUtils {
|
|||||||
return penpot.generateStyle([shape], { type: "css", includeChildren: true });
|
return penpot.generateStyle([shape], { type: "css", includeChildren: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the actual rendering bounds of a shape. For most shapes, this is simply the `bounds` property.
|
||||||
|
* However, for Text shapes, the `bounds` may not reflect the true size of the rendered text content,
|
||||||
|
* so we use the `textBounds` property instead.
|
||||||
|
*
|
||||||
|
* @param shape - The shape to get the bounds for
|
||||||
|
*/
|
||||||
|
public static getBounds(shape: Shape): Bounds {
|
||||||
|
if (shape.type === "text") {
|
||||||
|
const text = shape as Text;
|
||||||
|
// TODO: Remove ts-ignore once type definitions are updated
|
||||||
|
// @ts-ignore
|
||||||
|
return text.textBounds;
|
||||||
|
} else {
|
||||||
|
return shape.bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a child shape is fully contained within its parent's bounds.
|
* Checks if a child shape is fully contained within its parent's bounds.
|
||||||
* Visual containment means all edges of the child are within the parent's bounding box.
|
* Visual containment means all edges of the child are within the parent's bounding box.
|
||||||
@ -198,11 +216,13 @@ export class PenpotUtils {
|
|||||||
* @returns true if child is fully contained within parent bounds, false otherwise
|
* @returns true if child is fully contained within parent bounds, false otherwise
|
||||||
*/
|
*/
|
||||||
public static isContainedIn(child: Shape, parent: Shape): boolean {
|
public static isContainedIn(child: Shape, parent: Shape): boolean {
|
||||||
|
const childBounds = this.getBounds(child);
|
||||||
|
const parentBounds = this.getBounds(parent);
|
||||||
return (
|
return (
|
||||||
child.x >= parent.x &&
|
childBounds.x >= parentBounds.x &&
|
||||||
child.y >= parent.y &&
|
childBounds.y >= parentBounds.y &&
|
||||||
child.x + child.width <= parent.x + parent.width &&
|
childBounds.x + childBounds.width <= parentBounds.x + parentBounds.width &&
|
||||||
child.y + child.height <= parent.y + parent.height
|
childBounds.y + childBounds.height <= parentBounds.y + parentBounds.height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,39 +318,16 @@ export class PenpotUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes a base64 string to a Uint8Array.
|
* Decodes a base64 string to a Uint8Array.
|
||||||
* This is required because the Penpot plugin environment does not provide the atob function.
|
|
||||||
*
|
*
|
||||||
* @param base64 - The base64-encoded string to decode
|
* @param base64 - The base64-encoded string to decode
|
||||||
* @returns The decoded data as a Uint8Array
|
* @returns The decoded data as a Uint8Array
|
||||||
*/
|
*/
|
||||||
public static atob(base64: string): Uint8Array {
|
public static base64ToByteArray(base64: string): Uint8Array {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
const binary = atob(base64);
|
||||||
const lookup = new Uint8Array(256);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (let i = 0; i < chars.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
lookup[chars.charCodeAt(i)] = i;
|
bytes[i] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bufferLength = base64.length * 0.75;
|
|
||||||
if (base64[base64.length - 1] === "=") {
|
|
||||||
bufferLength--;
|
|
||||||
if (base64[base64.length - 2] === "=") {
|
|
||||||
bufferLength--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bytes = new Uint8Array(bufferLength);
|
|
||||||
let p = 0;
|
|
||||||
for (let i = 0; i < base64.length; i += 4) {
|
|
||||||
const encoded1 = lookup[base64.charCodeAt(i)];
|
|
||||||
const encoded2 = lookup[base64.charCodeAt(i + 1)];
|
|
||||||
const encoded3 = lookup[base64.charCodeAt(i + 2)];
|
|
||||||
const encoded4 = lookup[base64.charCodeAt(i + 3)];
|
|
||||||
|
|
||||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
|
||||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
|
||||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +357,7 @@ export class PenpotUtils {
|
|||||||
height: number | undefined
|
height: number | undefined
|
||||||
): Promise<Rectangle> {
|
): Promise<Rectangle> {
|
||||||
// convert base64 to Uint8Array
|
// convert base64 to Uint8Array
|
||||||
const bytes = PenpotUtils.atob(base64);
|
const bytes = PenpotUtils.base64ToByteArray(base64);
|
||||||
|
|
||||||
// upload the image data to Penpot
|
// upload the image data to Penpot
|
||||||
const imageData = await penpot.uploadMediaData(name, bytes, mimeType);
|
const imageData = await penpot.uploadMediaData(name, bytes, mimeType);
|
||||||
@ -423,6 +420,11 @@ export class PenpotUtils {
|
|||||||
* - For mode="fill", it will be whatever format the fill image is stored in.
|
* - For mode="fill", it will be whatever format the fill image is stored in.
|
||||||
*/
|
*/
|
||||||
public static async exportImage(shape: Shape, mode: "shape" | "fill", asSVG: boolean): Promise<Uint8Array> {
|
public static async exportImage(shape: Shape, mode: "shape" | "fill", asSVG: boolean): Promise<Uint8Array> {
|
||||||
|
// Updates are asynchronous in Penpot, so wait a tick to ensure any pending updates are applied before export.
|
||||||
|
// The constant wait time is a temporary workardound until a better solution for penpot/penpot-mcp#27
|
||||||
|
// is implemented.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
// Perform export
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "shape":
|
case "shape":
|
||||||
return shape.export({ type: asSVG ? "svg" : "png" });
|
return shape.export({ type: asSVG ? "svg" : "png" });
|
||||||
|
|||||||
21
mcp/packages/plugin/src/index.d.ts
vendored
Normal file
21
mcp/packages/plugin/src/index.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import "@penpot/plugin-types";
|
||||||
|
|
||||||
|
declare module "@penpot/plugin-types" {
|
||||||
|
interface Penpot {
|
||||||
|
/** The Penpot application version string. */
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpOptions {
|
||||||
|
getToken(): string;
|
||||||
|
getServerUrl(): string;
|
||||||
|
setMcpStatus(status: string);
|
||||||
|
on(eventType: "disconnect" | "connect", cb: () => void);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const mcp: undefined | McpOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@ -1,29 +1,74 @@
|
|||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
// get the current theme from the URL
|
// get the current theme from the URL
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.hash.split("?")[1]);
|
||||||
document.body.dataset.theme = searchParams.get("theme") ?? "light";
|
document.body.dataset.theme = searchParams.get("theme") ?? "light";
|
||||||
|
|
||||||
// Determine whether multi-user mode is enabled based on URL parameters
|
|
||||||
const isMultiUserMode = searchParams.get("multiUser") === "true";
|
|
||||||
console.log("Penpot MCP multi-user mode:", isMultiUserMode);
|
|
||||||
|
|
||||||
// WebSocket connection management
|
// WebSocket connection management
|
||||||
let ws: WebSocket | null = null;
|
let ws: WebSocket | null = null;
|
||||||
const statusElement = document.getElementById("connection-status");
|
|
||||||
|
const statusPill = document.getElementById("connection-status") as HTMLElement;
|
||||||
|
const statusText = document.getElementById("status-text") as HTMLElement;
|
||||||
|
const currentTaskEl = document.getElementById("current-task") as HTMLElement;
|
||||||
|
const executedCodeEl = document.getElementById("executed-code") as HTMLTextAreaElement;
|
||||||
|
const copyCodeBtn = document.getElementById("copy-code-btn") as HTMLButtonElement;
|
||||||
|
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;
|
||||||
|
const disconnectBtn = document.getElementById("disconnect-btn") as HTMLButtonElement;
|
||||||
|
const versionWarningEl = document.getElementById("version-warning") as HTMLElement;
|
||||||
|
const versionWarningTextEl = document.getElementById("version-warning-text") as HTMLElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the connection status display element.
|
* Updates the status pill and button visibility based on connection state.
|
||||||
*
|
*
|
||||||
* @param status - the base status text to display
|
* @param code - the connection state code ("idle" | "connecting" | "connected" | "disconnected" | "error")
|
||||||
* @param isConnectedState - whether the connection is in a connected state (affects color)
|
* @param label - human-readable label to display inside the pill
|
||||||
* @param message - optional additional message to append to the status
|
|
||||||
*/
|
*/
|
||||||
function updateConnectionStatus(status: string, isConnectedState: boolean, message?: string): void {
|
function updateConnectionStatus(code: string, label: string): void {
|
||||||
if (statusElement) {
|
if (statusPill) {
|
||||||
const displayText = message ? `${status}: ${message}` : status;
|
statusPill.dataset.status = code;
|
||||||
statusElement.textContent = displayText;
|
}
|
||||||
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
|
if (statusText) {
|
||||||
|
statusText.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConnected = code === "connected";
|
||||||
|
if (connectBtn) connectBtn.hidden = isConnected;
|
||||||
|
if (disconnectBtn) disconnectBtn.hidden = !isConnected;
|
||||||
|
|
||||||
|
parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "update-connection-status",
|
||||||
|
status: code,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the "Current task" display with the currently executing task name.
|
||||||
|
*
|
||||||
|
* @param taskName - the task name to display, or null to reset to "---"
|
||||||
|
*/
|
||||||
|
function updateCurrentTask(taskName: string | null): void {
|
||||||
|
if (currentTaskEl) {
|
||||||
|
currentTaskEl.textContent = taskName ?? "---";
|
||||||
|
}
|
||||||
|
if (taskName === null) {
|
||||||
|
updateExecutedCode(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the executed code textarea with the last code run during task execution.
|
||||||
|
*
|
||||||
|
* @param code - the code string to display, or null to clear
|
||||||
|
*/
|
||||||
|
function updateExecutedCode(code: string | null): void {
|
||||||
|
if (executedCodeEl) {
|
||||||
|
executedCodeEl.value = code ?? "";
|
||||||
|
}
|
||||||
|
if (copyCodeBtn) {
|
||||||
|
copyCodeBtn.disabled = !code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,31 +89,41 @@ function sendTaskResponse(response: any): void {
|
|||||||
/**
|
/**
|
||||||
* Establishes a WebSocket connection to the MCP server.
|
* Establishes a WebSocket connection to the MCP server.
|
||||||
*/
|
*/
|
||||||
function connectToMcpServer(): void {
|
function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
updateConnectionStatus("Already connected", true);
|
updateConnectionStatus("connected", "Connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
|
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
|
||||||
if (isMultiUserMode) {
|
let wsError: unknown | undefined;
|
||||||
// TODO obtain proper userToken from penpot
|
|
||||||
const userToken = "dummyToken";
|
if (token) {
|
||||||
wsUrl += `?userToken=${encodeURIComponent(userToken)}`;
|
wsUrl += `?userToken=${encodeURIComponent(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
updateConnectionStatus("Connecting...", false);
|
updateConnectionStatus("connecting", "Connecting...");
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log("Connected to MCP server");
|
setTimeout(() => {
|
||||||
updateConnectionStatus("Connected to MCP server", true);
|
if (ws) {
|
||||||
|
console.log("Connected to MCP server");
|
||||||
|
updateConnectionStatus("connected", "Connected");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
console.log("Received from MCP server:", event.data);
|
|
||||||
try {
|
try {
|
||||||
|
console.log("Received from MCP server:", event.data);
|
||||||
const request = JSON.parse(event.data);
|
const request = JSON.parse(event.data);
|
||||||
|
// Track the current task received from the MCP server
|
||||||
|
if (request.task) {
|
||||||
|
updateCurrentTask(request.task);
|
||||||
|
updateExecutedCode(request.params?.code ?? null);
|
||||||
|
}
|
||||||
// Forward the task request to the plugin for execution
|
// Forward the task request to the plugin for execution
|
||||||
parent.postMessage(request, "*");
|
parent.postMessage(request, "*");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -77,34 +132,70 @@ function connectToMcpServer(): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (event: CloseEvent) => {
|
ws.onclose = (event: CloseEvent) => {
|
||||||
console.log("Disconnected from MCP server");
|
// If we've send the error update we don't send the disconnect as well
|
||||||
const message = event.reason || undefined;
|
if (!wsError) {
|
||||||
updateConnectionStatus("Disconnected", false, message);
|
console.log("Disconnected from MCP server");
|
||||||
|
const label = event.reason ? `Disconnected: ${event.reason}` : "Disconnected";
|
||||||
|
updateConnectionStatus("disconnected", label);
|
||||||
|
updateCurrentTask(null);
|
||||||
|
}
|
||||||
ws = null;
|
ws = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error("WebSocket error:", error);
|
console.error("WebSocket error:", error);
|
||||||
|
wsError = error;
|
||||||
// note: WebSocket error events typically don't contain detailed error messages
|
// note: WebSocket error events typically don't contain detailed error messages
|
||||||
updateConnectionStatus("Connection error", false);
|
updateConnectionStatus("error", "Connection error");
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to MCP server:", error);
|
console.error("Failed to connect to MCP server:", error);
|
||||||
const message = error instanceof Error ? error.message : undefined;
|
const reason = error instanceof Error ? error.message : undefined;
|
||||||
updateConnectionStatus("Connection failed", false, message);
|
const label = reason ? `Connection failed: ${reason}` : "Connection failed";
|
||||||
|
updateConnectionStatus("error", label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => {
|
copyCodeBtn?.addEventListener("click", () => {
|
||||||
|
const code = executedCodeEl?.value;
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
copyCodeBtn.classList.add("copied");
|
||||||
|
setTimeout(() => copyCodeBtn.classList.remove("copied"), 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connectBtn?.addEventListener("click", () => {
|
||||||
connectToMcpServer();
|
connectToMcpServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
disconnectBtn?.addEventListener("click", () => {
|
||||||
|
ws?.close();
|
||||||
|
});
|
||||||
|
|
||||||
// Listen plugin.ts messages
|
// Listen plugin.ts messages
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.data.source === "penpot") {
|
if (event.data.type === "start-server") {
|
||||||
|
connectToMcpServer(event.data.url, event.data.token);
|
||||||
|
}
|
||||||
|
if (event.data.type === "version-mismatch") {
|
||||||
|
if (versionWarningEl && versionWarningTextEl) {
|
||||||
|
versionWarningTextEl.innerHTML =
|
||||||
|
`<b>Version mismatch detected</b>: This version of the MCP server is intended for Penpot ` +
|
||||||
|
`${event.data.mcpVersion} while the current version is ${event.data.penpotVersion}. ` +
|
||||||
|
`Executions may not work or produce suboptimal results.`;
|
||||||
|
versionWarningEl.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.data.type === "stop-server") {
|
||||||
|
ws?.close();
|
||||||
|
} else if (event.data.source === "penpot") {
|
||||||
document.body.dataset.theme = event.data.theme;
|
document.body.dataset.theme = event.data.theme;
|
||||||
} else if (event.data.type === "task-response") {
|
} else if (event.data.type === "task-response") {
|
||||||
// Forward task response back to MCP server
|
// Forward task response back to MCP server
|
||||||
sendTaskResponse(event.data.response);
|
sendTaskResponse(event.data.response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
parent.postMessage({ type: "ui-initialized" }, "*");
|
||||||
|
|||||||
@ -1,22 +1,58 @@
|
|||||||
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
||||||
import { Task, TaskHandler } from "./TaskHandler";
|
import { Task, TaskHandler } from "./TaskHandler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the major.minor.patch prefix from a version string.
|
||||||
|
*
|
||||||
|
* @param version - a version string starting with major.minor.patch
|
||||||
|
* @returns the major.minor.patch prefix, or the original string if it does not match
|
||||||
|
*/
|
||||||
|
function extractVersionPrefix(version: string): string {
|
||||||
|
const match = version.match(/^(\d+\.\d+\.\d+)/);
|
||||||
|
return match ? match[1] : version;
|
||||||
|
}
|
||||||
|
|
||||||
|
mcp?.setMcpStatus("connecting");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of all available task handlers.
|
* Registry of all available task handlers.
|
||||||
*/
|
*/
|
||||||
const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()];
|
const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()];
|
||||||
|
|
||||||
// Determine whether multi-user mode is enabled based on build-time configuration
|
|
||||||
declare const IS_MULTI_USER_MODE: boolean;
|
|
||||||
const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false;
|
|
||||||
|
|
||||||
// Open the plugin UI (main.ts)
|
// Open the plugin UI (main.ts)
|
||||||
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { width: 158, height: 200 });
|
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, {
|
||||||
|
width: 236,
|
||||||
|
height: 210,
|
||||||
|
hidden: !!mcp,
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Handle messages
|
// Register message handlers
|
||||||
penpot.ui.onMessage<string | { id: string; task: string; params: any }>((message) => {
|
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
|
||||||
// Handle plugin task requests
|
if (typeof message === "object" && message.type === "ui-initialized") {
|
||||||
if (typeof message === "object" && message.task && message.id) {
|
// Check Penpot version compatibility
|
||||||
|
const penpotVersionPrefix = penpot.version ? extractVersionPrefix(penpot.version) : "<2.15"; // pre-2.15 versions don't have version info
|
||||||
|
const mcpVersionPrefix = extractVersionPrefix(PENPOT_MCP_VERSION);
|
||||||
|
console.log(`Penpot version: ${penpotVersionPrefix}, MCP version: ${mcpVersionPrefix}`);
|
||||||
|
const isLocalPenpotVersion = penpotVersionPrefix == "0.0.0";
|
||||||
|
if (penpotVersionPrefix !== mcpVersionPrefix && !isLocalPenpotVersion) {
|
||||||
|
penpot.ui.sendMessage({
|
||||||
|
type: "version-mismatch",
|
||||||
|
mcpVersion: mcpVersionPrefix,
|
||||||
|
penpotVersion: penpotVersionPrefix,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Initiate connection to remote MCP server (if enabled)
|
||||||
|
if (mcp) {
|
||||||
|
penpot.ui.sendMessage({
|
||||||
|
type: "start-server",
|
||||||
|
url: mcp?.getServerUrl(),
|
||||||
|
token: mcp?.getToken(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (typeof message === "object" && message.type === "update-connection-status") {
|
||||||
|
mcp?.setMcpStatus(message.status || "unknown");
|
||||||
|
} else if (typeof message === "object" && message.task && message.id) {
|
||||||
|
// Handle plugin tasks submitted by the MCP server
|
||||||
handlePluginTaskRequest(message).catch((error) => {
|
handlePluginTaskRequest(message).catch((error) => {
|
||||||
console.error("Error in handlePluginTaskRequest:", error);
|
console.error("Error in handlePluginTaskRequest:", error);
|
||||||
});
|
});
|
||||||
@ -59,6 +95,21 @@ async function handlePluginTaskRequest(request: { id: string; task: string; para
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mcp) {
|
||||||
|
mcp.on("disconnect", async () => {
|
||||||
|
penpot.ui.sendMessage({
|
||||||
|
type: "stop-server",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
mcp.on("connect", async () => {
|
||||||
|
penpot.ui.sendMessage({
|
||||||
|
type: "start-server",
|
||||||
|
url: mcp?.getServerUrl(),
|
||||||
|
token: mcp?.getToken(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle theme change in the iframe
|
// Handle theme change in the iframe
|
||||||
penpot.on("themechange", (theme) => {
|
penpot.on("themechange", (theme) => {
|
||||||
penpot.ui.sendMessage({
|
penpot.ui.sendMessage({
|
||||||
|
|||||||
@ -1,10 +1,190 @@
|
|||||||
@import "@penpot/plugin-styles/styles.css";
|
@import "@penpot/plugin-styles/styles.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
line-height: 1.5;
|
margin: 0;
|
||||||
padding: 10px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.plugin-container {
|
||||||
margin-block-end: 0.75rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-16) var(--spacing-8);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status pill ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-8) var(--spacing-16);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
border: 1px solid var(--background-quaternary);
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill[data-status="connected"] {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill[data-status="disconnected"],
|
||||||
|
.status-pill[data-status="error"] {
|
||||||
|
border-color: var(--error-500);
|
||||||
|
color: var(--error-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collapsible section ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.collapsible-section {
|
||||||
|
border: 1px solid var(--background-quaternary);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > .collapsible-header .collapsible-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
padding: var(--spacing-4) var(--spacing-12) var(--spacing-12);
|
||||||
|
border-top: 1px solid var(--background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tool section ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.tool-label {
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
min-height: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Code section ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.code-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
border: 1px solid var(--background-quaternary);
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--background-quaternary);
|
||||||
|
border-radius: var(--spacing-4);
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--foreground-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover:not(:disabled) {
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
color: var(--foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copied {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Version warning ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.version-warning {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
border-radius: var(--spacing-8);
|
||||||
|
border: 1px solid var(--warning-500, #f59e0b);
|
||||||
|
color: var(--warning-500, #f59e0b);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Action buttons ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#connect-btn,
|
||||||
|
#disconnect-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -195,10 +195,44 @@ export class ExecuteCodeTaskHandler extends TaskHandler<ExecuteCodeTaskParams> {
|
|||||||
const context = this.context;
|
const context = this.context;
|
||||||
const code = task.params.code;
|
const code = task.params.code;
|
||||||
|
|
||||||
let result: any = await (async (ctx) => {
|
// set the flags naturalChildOrdering and throwValidationErrors to true during code execution.
|
||||||
const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`);
|
// TODO: Remove all ts-ignore once Penpot types have been updated
|
||||||
return fn(...Object.values(ctx));
|
let originalNaturalChildOrdering: any, originalThrowValidationErrors: any;
|
||||||
})(context);
|
// @ts-ignore
|
||||||
|
if (penpot.flags) {
|
||||||
|
// @ts-ignore
|
||||||
|
originalNaturalChildOrdering = penpot.flags.naturalChildOrdering;
|
||||||
|
// @ts-ignore
|
||||||
|
penpot.flags.naturalChildOrdering = true;
|
||||||
|
// @ts-ignore
|
||||||
|
originalThrowValidationErrors = penpot.flags.throwValidationErrors;
|
||||||
|
// @ts-ignore
|
||||||
|
penpot.flags.throwValidationErrors = true;
|
||||||
|
} else {
|
||||||
|
// TODO: This can be removed once `flags` has been merged to PROD
|
||||||
|
throw new Error(
|
||||||
|
"You are using a version of the Penpot MCP server which is incompatible " +
|
||||||
|
"with the connected Penpot version. " +
|
||||||
|
"Read the documentation for instructions on which version to use: " +
|
||||||
|
"https://github.com/penpot/penpot/tree/develop/mcp\n" +
|
||||||
|
"If you are an LLM, tell the user about this!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
try {
|
||||||
|
// execute the code in an async function with the context variables as parameters
|
||||||
|
result = await (async (ctx) => {
|
||||||
|
const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`);
|
||||||
|
return fn(...Object.values(ctx));
|
||||||
|
})(context);
|
||||||
|
} finally {
|
||||||
|
// restore the original value of the flags
|
||||||
|
// @ts-ignore
|
||||||
|
penpot.flags.naturalChildOrdering = originalNaturalChildOrdering;
|
||||||
|
// @ts-ignore
|
||||||
|
penpot.flags.throwValidationErrors = originalThrowValidationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Code execution result:", result);
|
console.log("Code execution result:", result);
|
||||||
|
|
||||||
|
|||||||
1
mcp/packages/plugin/src/vite-env.d.ts
vendored
1
mcp/packages/plugin/src/vite-env.d.ts
vendored
@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
declare const IS_MULTI_USER_MODE: boolean;
|
declare const IS_MULTI_USER_MODE: boolean;
|
||||||
declare const PENPOT_MCP_WEBSOCKET_URL: string;
|
declare const PENPOT_MCP_WEBSOCKET_URL: string;
|
||||||
|
declare const PENPOT_MCP_VERSION: string;
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import livePreview from "vite-live-preview";
|
import livePreview from "vite-live-preview";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const rootPkg = require("../../package.json");
|
||||||
|
|
||||||
let WS_URI = process.env.WS_URI || "http://localhost:4402";
|
let WS_URI = process.env.WS_URI || "http://localhost:4402";
|
||||||
let MULTI_USER_MODE = process.env.MULTI_USER_MODE === "true";
|
let SERVER_HOST = process.env.PENPOT_MCP_PLUGIN_SERVER_HOST ?? "localhost";
|
||||||
|
let MCP_VERSION = JSON.stringify(rootPkg.version);
|
||||||
|
|
||||||
console.log("Will define IS_MULTI_USER_MODE as:", JSON.stringify(MULTI_USER_MODE));
|
console.log("PENPOT_MCP_WEBSOCKET_URL:", JSON.stringify(WS_URI));
|
||||||
console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI));
|
console.log("PENPOT_MCP_VERSION:", MCP_VERSION);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "./",
|
base: "./",
|
||||||
@ -31,13 +36,13 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
host: "0.0.0.0",
|
host: SERVER_HOST,
|
||||||
port: 4400,
|
port: 4400,
|
||||||
cors: true,
|
cors: true,
|
||||||
allowedHosts: [],
|
allowedHosts: [],
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"),
|
|
||||||
PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(WS_URI),
|
PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(WS_URI),
|
||||||
|
PENPOT_MCP_VERSION: MCP_VERSION,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import baseConfig from "./vite.config";
|
|||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
baseConfig,
|
baseConfig,
|
||||||
defineConfig({
|
defineConfig({
|
||||||
base: "./",
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
1
mcp/packages/server/.gitignore
vendored
1
mcp/packages/server/.gitignore
vendored
@ -0,0 +1 @@
|
|||||||
|
/pnpm-lock.yaml
|
||||||
File diff suppressed because it is too large
Load Diff
2
mcp/packages/server/data/base_instructions.md
Normal file
2
mcp/packages/server/data/base_instructions.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
You have access to Penpot tools in order to interact with Penpot designs.
|
||||||
|
Before working with these tools, be sure to read the 'Penpot High-Level Overview' via the `high_level_overview` tool.
|
||||||
@ -1,10 +1,6 @@
|
|||||||
You have access to Penpot tools in order to interact with a Penpot design project directly.
|
You have access to Penpot tools in order to interact with a Penpot design project directly.
|
||||||
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
|
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
|
||||||
|
|
||||||
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
|
||||||
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
|
||||||
non-creative defaults such as white/black if you are lacking information).
|
|
||||||
|
|
||||||
# Executing Code
|
# Executing Code
|
||||||
|
|
||||||
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
|
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
|
||||||
@ -43,18 +39,30 @@ Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`
|
|||||||
* `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board.
|
* `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board.
|
||||||
To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`.
|
To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`.
|
||||||
* `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions.
|
* `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions.
|
||||||
* `bounds` is a READ-ONLY property. Use `x`, `y` with `resize()` to modify shape bounds.
|
* `bounds` is READ-ONLY (members: x, y, width, height). To modify the bounding box, change `x`, `y` or apply `resize()`.
|
||||||
|
|
||||||
**Other Writable Properties**:
|
**Other Writable Properties**:
|
||||||
* `name` - Shape name
|
* `name` - Shape name
|
||||||
* `fills`, `strokes` - Styling properties
|
* `fills: Fill[]`, `strokes: Stroke[]`, `shadows: Shadow[]` - Styling properties
|
||||||
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
|
- Setting fills: `shape.fills = [{ fillColor: "#FF0000", fillOpacity: 1 }]`; no fill (transparent): `shape.fills = []`;
|
||||||
|
- Reusing objects in another shape: `targetShape.fills = sourceShape.fills` or more granular `targetShape.fills = [{ fillOpacity: 1, fillImage: sourceShape.fills[0].fillImage }]`
|
||||||
|
The objects are not shared references; you can modify properties of the fills in the target shape without affecting the source shape.
|
||||||
|
- Colors: Use hex strings with caps only (e.g. '#FF5533')
|
||||||
|
- IMPORTANT: The contents of the arrays are read-only. You cannot modify individual fills/strokes; you need to replace the entire array to change them!
|
||||||
|
* `borderRadius` - Uniform border radius for all corners
|
||||||
|
* `borderRadiusTopLeft`, `borderRadiusTopRight`, `borderRadiusBottomRight`, `borderRadiusBottomLeft` - Individual corner radii.
|
||||||
|
* `blur: Blur` - Blur properties
|
||||||
|
* `blendMode` - Blend mode (e.g. `"normal"`, `"multiply"`, `"overlay"`, etc.)
|
||||||
|
* `rotation` (deg), `opacity`, `blocked`, `hidden`, `visible`
|
||||||
|
* `proportionLock` - Whether width and height are locked to the same ratio
|
||||||
|
* `constraintsHorizontal` - Horizontal resize constraint (`"left"`, `"right"`, `"center"`, `"leftright"`, `"scale"`)
|
||||||
|
* `constraintsVertical` - Vertical resize constraint (`"top"`, `"bottom"`, `"center"`, `"topbottom"`, `"scale"`)
|
||||||
|
* `flipX`, `flipY` - Horizontal/vertical flip
|
||||||
|
|
||||||
**Z-Order**:
|
**Z-Order**:
|
||||||
* The z-order of shapes is determined by the order in the `children` array of the parent shape.
|
* The z-order of shapes is determined by the order in the `children` array of the parent shape.
|
||||||
Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order
|
Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order
|
||||||
(i.e. add background shapes first, then foreground shapes later).
|
(i.e. add background shapes first, then foreground shapes later).
|
||||||
CRITICAL: NEVER use the broken function `appendChild` to achieve this, ALWAYS use `parent.insertChild(parent.children.length, shape)`
|
|
||||||
* To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`,
|
* To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`,
|
||||||
and, for precise control, `setParentIndex(index)` (0-based).
|
and, for precise control, `setParentIndex(index)` (0-based).
|
||||||
|
|
||||||
@ -67,15 +75,15 @@ Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`
|
|||||||
**Hierarchical Structure**:
|
**Hierarchical Structure**:
|
||||||
* `parent` - The parent shape (null for root shapes)
|
* `parent` - The parent shape (null for root shapes)
|
||||||
Note: Hierarchical nesting does not necessarily imply visual containment
|
Note: Hierarchical nesting does not necessarily imply visual containment
|
||||||
* CRITICAL: To add children to a parent shape (e.g. a `Board`):
|
* To add children to a parent shape (e.g. a `Board`): `parent.appendChild(shape)` or `parent.insertChild(index, shape)`
|
||||||
- ALWAYS use `parent.insertChild(index, shape)` to add a child, e.g. `parent.insertChild(parent.children.length, shape)` to append
|
|
||||||
- NEVER use `parent.appendChild(shape)` as it is BROKEN and will not insert in a predictable place (except in flex layout boards)
|
|
||||||
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
|
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
|
||||||
- Automatically removes the shape from its old parent
|
- Automatically removes the shape from its old parent
|
||||||
- Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position)
|
- Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position)
|
||||||
|
|
||||||
Cloning: Use `shape.clone(): Shape` to create an exact duplicate (including all properties and children) of a shape; same position as original.
|
Cloning: Use `shape.clone(): Shape` to create an exact duplicate (including all properties and children) of a shape; same position as original.
|
||||||
|
|
||||||
|
Annotations: Don't add text elements to the design that just repeat a shape's name. In the Penpot UI, the name is displayed anyway.
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
|
|
||||||
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
|
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
|
||||||
@ -87,10 +95,10 @@ Use the `export_shape` and `import_image` tools to export and import images.
|
|||||||
Boards can have layout systems that automatically control the positioning and spacing of their children:
|
Boards can have layout systems that automatically control the positioning and spacing of their children:
|
||||||
|
|
||||||
* If a board has a layout system, then child positions are controlled by the layout system.
|
* If a board has a layout system, then child positions are controlled by the layout system.
|
||||||
For every child, key properties of the child within the layout are stored in `child.layoutChild: LayoutChildProperties`:
|
After adding a shape to the layout as a child, key properties of the child within the layout are controlled in `child.layoutChild: LayoutChildProperties`:
|
||||||
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
|
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
|
||||||
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
|
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
|
||||||
- sizing (`verticalSizing`, `horizontalSizing`: "fill" | "auto" | "fix")
|
- sizing (`verticalSizing`, `horizontalSizing`: "fix" | "auto" | "fill") - controls child resizing depending on the layout's sizing mode (see below)
|
||||||
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
|
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
|
||||||
- `zIndex: number` (higher numbers on top)
|
- `zIndex: number` (higher numbers on top)
|
||||||
|
|
||||||
@ -99,18 +107,11 @@ Boards can have layout systems that automatically control the positioning and sp
|
|||||||
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
|
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
|
||||||
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
||||||
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
||||||
Optionally, adjust indivudual child margins via `child.layoutChild`.
|
Optionally, adjust individual child margins via `child.layoutChild`.
|
||||||
- When a board has flex layout,
|
- When a board has flex layout, child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
|
||||||
- child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
|
appending or inserting children automatically positions them according to the layout rules.
|
||||||
appending or inserting children automatically positions them according to the layout rules.
|
- To append children to a flex layout board such that they appear visually at the end, use the Board's method `board.appendChild(shape)`, i.e. call it in the order of visual appearance.
|
||||||
- CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
|
To insert at a specific index, use `board.insertChild(index, shape)`.
|
||||||
Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa.
|
|
||||||
ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"!
|
|
||||||
- CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that
|
|
||||||
they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`; it will insert at the front
|
|
||||||
of the `children` array for dir="column" or dir="row", which is what you want. So call it in the order of visual appearance.
|
|
||||||
To insert at a specific index, use `board.insertChild(index, shape)`, bearing in mind the reversed order for dir="column"
|
|
||||||
or dir="row".
|
|
||||||
- Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessible via `board.flex`.
|
- Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessible via `board.flex`.
|
||||||
IMPORTANT: When adding a flex layout to a container that already has children,
|
IMPORTANT: When adding a flex layout to a container that already has children,
|
||||||
use `penpotUtils.addFlexLayout(container, dir)` instead! This preserves the existing visual order of children.
|
use `penpotUtils.addFlexLayout(container, dir)` instead! This preserves the existing visual order of children.
|
||||||
@ -122,9 +123,14 @@ Boards can have layout systems that automatically control the positioning and sp
|
|||||||
Check with: `if (board.grid) { ... }`
|
Check with: `if (board.grid) { ... }`
|
||||||
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
|
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
|
||||||
- Children are positioned via 1-based row/column indices
|
- Children are positioned via 1-based row/column indices
|
||||||
- Add to grid via `board.flex.appendChild(shape, row, column)`
|
- Add to grid via `board.grid.appendChild(shape, row, column)`
|
||||||
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
|
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
|
||||||
|
|
||||||
|
* Auto-sizing: both types of layouts have properties `verticalSizing`, `horizontalSizing`: "fix" | "auto" | "fill"
|
||||||
|
- `fix` (default): no resizing (size determined by shape's own width/height)
|
||||||
|
- `auto`: size determined by content (container will resize depending on children's dimensions); ALWAYS set this if you want the container size to adapt to contents/margins/spacings!
|
||||||
|
- `fill`: resize children to fill the container's size (child resizing is controlled by each child's `layoutChild` properties)
|
||||||
|
|
||||||
* When working with boards:
|
* When working with boards:
|
||||||
- ALWAYS check if the board has a layout system before attempting to reposition children
|
- ALWAYS check if the board has a layout system before attempting to reposition children
|
||||||
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
|
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
|
||||||
@ -132,13 +138,23 @@ Boards can have layout systems that automatically control the positioning and sp
|
|||||||
|
|
||||||
# Text Elements
|
# Text Elements
|
||||||
|
|
||||||
The rendered content of `Text` element is given by the `characters` property.
|
`Text` elements:
|
||||||
|
* The text to be rendered is given by the `characters` property.
|
||||||
To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size,
|
* To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size,
|
||||||
it only changes the formal bounding box; if the text does not fit it, it will overflow.
|
it only changes the formal bounding box; if the text does not fit it, it will overflow; use `textBounds` for the actual bounding box of the rendered text.
|
||||||
The bounding box is sized automatically as long as the `growType` property is set to "auto-width" or "auto-height".
|
* Property `bounds` is sized automatically (in one dimension) if the `growType` property is set to "auto-width" or "auto-height".
|
||||||
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing - otherwise the bounding box will be meaningless, with the text overflowing!
|
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-width" or "auto-height" if you want automatic sizing!
|
||||||
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
|
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
|
||||||
|
* Method `getRange(start, end): TextRange` to reference a range of characters as a `TextRange` object, which can be styled separately from the rest of the text; `start` index inclusive, `end` exclusive
|
||||||
|
* Other Writable font properties: `fontId`, `fontFamily`, `fontWeight`, `fontVariant`, `fontStyle`
|
||||||
|
- To discover valid values, check available fonts in `penpot.fonts: FontContext`
|
||||||
|
- `FontContext` provides `Font` instances; each font has property `variants: FontVariant[]`
|
||||||
|
- Example: Determine available weights for a font using `penpot.fonts.findByName("Laila").variants.map(v => v.fontWeight)`
|
||||||
|
- To apply a `Font` to a `Text` instance and set all font properties at once:
|
||||||
|
- `font.applyToText(text: Text, variant?: FontVariant)`
|
||||||
|
- `applyToRange(range: TextRange, variant?: FontVariant)`
|
||||||
|
* Further writable properties: `align`, `verticalAlign`, `lineHeight`, `letterSpacing`, `textTransform`, `textDecoration` (see API info)
|
||||||
|
* Method `applyTypography(typography: LibraryTypography)`
|
||||||
|
|
||||||
# The `penpot` and `penpotUtils` Objects, Exploring Designs
|
# The `penpot` and `penpotUtils` Objects, Exploring Designs
|
||||||
|
|
||||||
@ -210,19 +226,6 @@ Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
|
|||||||
});
|
});
|
||||||
Always validate against the root container that is supposed to contain the shapes.
|
Always validate against the root container that is supposed to contain the shapes.
|
||||||
|
|
||||||
# Visual Inspection of Designs
|
|
||||||
|
|
||||||
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
|
||||||
|
|
||||||
# Revising Designs
|
|
||||||
|
|
||||||
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
|
||||||
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
|
||||||
Container sizes are usually intentional, check content first.
|
|
||||||
* Check for reasonable font sizes and typefaces
|
|
||||||
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
|
||||||
Consider converting boards to flex layout when appropriate.
|
|
||||||
|
|
||||||
# Asset Libraries
|
# Asset Libraries
|
||||||
|
|
||||||
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
|
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
|
||||||
@ -242,31 +245,75 @@ Each `Library` object has:
|
|||||||
* `colors: LibraryColor[]` - Array of colors
|
* `colors: LibraryColor[]` - Array of colors
|
||||||
* `typographies: LibraryTypography[]` - Array of typographies
|
* `typographies: LibraryTypography[]` - Array of typographies
|
||||||
|
|
||||||
|
## Colors and Typographies
|
||||||
|
|
||||||
|
Adding a color:
|
||||||
|
```
|
||||||
|
const newColor: LibraryColor = penpot.library.local.createColor();
|
||||||
|
newColor.name = 'Brand Primary';
|
||||||
|
newColor.color = '#0066FF';
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a typography:
|
||||||
|
```
|
||||||
|
const newTypo: LibraryTypography = penpot.library.local.createTypography();
|
||||||
|
newTypo.name = 'Heading Large';
|
||||||
|
// Set typography properties...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
Using library components:
|
Using library components:
|
||||||
* find a component in the library by name:
|
* find a component in the library by name:
|
||||||
const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));
|
`const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));`
|
||||||
* create a new instance of the component on the current page:
|
* create a new instance of the component on the current page:
|
||||||
const instance: Shape = component.instance();
|
`const instance: Shape = component.instance();`
|
||||||
This returns a `Shape` (often a `Board` containing child elements).
|
This returns a `Shape` (often a `Board` containing child elements).
|
||||||
After instantiation, modify the instance's properties as desired.
|
After instantiation, modify the instance's properties as desired.
|
||||||
* get the reference to the main component shape:
|
* get the reference to the main component shape:
|
||||||
const mainShape: Shape = component.mainInstance();
|
`const mainShape: Shape = component.mainInstance();`
|
||||||
|
|
||||||
Adding assets to a library:
|
Adding a component to a library:
|
||||||
* const newColor: LibraryColor = penpot.library.local.createColor();
|
```
|
||||||
newColor.name = 'Brand Primary';
|
const shapes: Shape[] = [shape1, shape2]; // shapes to include
|
||||||
newColor.color = '#0066FF';
|
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
|
||||||
* const newTypo: LibraryTypography = penpot.library.local.createTypography();
|
newComponent.name = 'My Button';
|
||||||
newTypo.name = 'Heading Large';
|
```
|
||||||
// Set typography properties...
|
|
||||||
* const shapes: Shape[] = [shape1, shape2]; // shapes to include
|
|
||||||
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
|
|
||||||
newComponent.name = 'My Button';
|
|
||||||
|
|
||||||
Detaching:
|
Detaching:
|
||||||
* When creating new design elements based on a component instance/copy, use `shape.detach()` to break the link to the main component, allowing independent modification.
|
* When creating new design elements based on a component instance/copy, use `shape.detach()` to break the link to the main component, allowing independent modification.
|
||||||
* Without detaching, some manipulations will have no effect; e.g. child/descendant removal will not work.
|
* Without detaching, some manipulations will have no effect; e.g. child/descendant removal will not work.
|
||||||
|
|
||||||
|
### Variants
|
||||||
|
|
||||||
|
Variants are a system for grouping related component versions along named property axes (e.g. Type, Style), powering a structured swap UI for designers using component instances.
|
||||||
|
|
||||||
|
* `VariantContainer` (extends `Board`): The board that physically groups all variant components together.
|
||||||
|
- check with `isVariantContainer()`
|
||||||
|
- property `variants: Variants`.
|
||||||
|
* `Variants`: Defines the combinations of property values for which component variants can exist and manages the concrete component variants.
|
||||||
|
- `properties: string[]` (ordered list of property names); `addProperty(): void`, `renameProperty(pos, name)`, `currentValues(property)`
|
||||||
|
- `variantComponents(): LibraryVariantComponent[]`
|
||||||
|
* `LibraryVariantComponent` (extends `LibraryComponent`): full library component with metadata, for which `isVariant()` returns true.
|
||||||
|
- `variantProps: { [property: string]: string }` (this component's value for each property)
|
||||||
|
- `variantError` (non-null if e.g. two variants share the same combination of property values)
|
||||||
|
- `setVariantProperty(pos, value)`
|
||||||
|
|
||||||
|
Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`.
|
||||||
|
|
||||||
|
**Creating a variant group**:
|
||||||
|
- `penpot.createVariantFromComponents(mainInstances: Board[]): VariantContainer`: Combines several main component instances into a new variant group.
|
||||||
|
All components end up inside a single new container on the canvas.
|
||||||
|
The container's `Variants` instance is initialised with one property `Property 1`, with the property values set to the respective component's name.
|
||||||
|
- After creation, edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`.
|
||||||
|
|
||||||
|
**Adding a variant to an existing group**:
|
||||||
|
Use `variantContainer.appendChild(mainInstance)` to move a component's main instance into the container, then set its position manually and assign property values via `setVariantProperty`.
|
||||||
|
|
||||||
|
**Using Variants**:
|
||||||
|
- `compInstance.switchVariant(pos, value)`: On a component instance, switches to the nearest variant that has the given value at property position `pos`, keeping all other property values the same.
|
||||||
|
- To instantiate a specific variant, find the right `LibraryVariantComponent` by checking `variantProps`, then call `.instance()`.
|
||||||
|
|
||||||
# Design Tokens
|
# Design Tokens
|
||||||
|
|
||||||
Design tokens are reusable design values (colors, dimensions, typography, etc.) for consistent styling.
|
Design tokens are reusable design values (colors, dimensions, typography, etc.) for consistent styling.
|
||||||
@ -274,21 +321,22 @@ Design tokens are reusable design values (colors, dimensions, typography, etc.)
|
|||||||
The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
|
The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
|
||||||
* `sets: TokenSet[]` - Token collections (order matters for precedence)
|
* `sets: TokenSet[]` - Token collections (order matters for precedence)
|
||||||
* `themes: TokenTheme[]` - Presets that activate specific sets
|
* `themes: TokenTheme[]` - Presets that activate specific sets
|
||||||
* `addSet(name: string): TokenSet` - Create new set
|
* `addSet({name: string}): TokenSet` - Create new set
|
||||||
* `addTheme(group: string, name: string): TokenTheme` - Create new theme
|
* `addTheme(group: string, name: string): TokenTheme` - Create new theme
|
||||||
|
|
||||||
`TokenSet` contains tokens with unique names:
|
`TokenSet` contains tokens with unique names:
|
||||||
* `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();`
|
* `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();`
|
||||||
* `tokens: Token[]` - All tokens in set
|
* `tokens: Token[]` - All tokens in set
|
||||||
* `addToken(type: TokenType, name: string, value: TokenValueString): Token` - Creates a token, adding it to the set.
|
* `addToken({type: TokenType, name: string, value: TokenValueString}): Token` - Creates a token, adding it to the set.
|
||||||
- `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase"
|
- `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase"
|
||||||
|
- `value`: depends on the type of token (inspect `Token` and related types)
|
||||||
- Examples:
|
- Examples:
|
||||||
const token = set.addToken("color", "color.primary", "#0066FF"); // direct value
|
const token = set.addToken({type: "color", name: "color.primary", value: "#0066FF"}); // direct value
|
||||||
const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token
|
const token2 = set.addToken({type: "color", name: "color.accent", value: "{color.primary}"}); // reference to another token
|
||||||
|
|
||||||
`Token`:
|
`Token`: union type encompassing various token types, with common properties:
|
||||||
* `name: string` - Token name (may include group path like "color.base.white")
|
* `name: string` - Token name (typically structured, e.g. "color.base.white")
|
||||||
* `value: string | TokenValueString` - Raw value (may be direct value or reference to another token like "{color.primary}")
|
* `value` - Raw value (direct value or reference to another token like "{color.primary}")
|
||||||
* `resolvedValue` - Computed final value (follows references)
|
* `resolvedValue` - Computed final value (follows references)
|
||||||
* `type: TokenType`
|
* `type: TokenType`
|
||||||
|
|
||||||
@ -303,21 +351,21 @@ Applying tokens:
|
|||||||
(if properties is undefined, use a default property based on the token type - not usually recommended).
|
(if properties is undefined, use a default property based on the token type - not usually recommended).
|
||||||
`TokenProperty` is a union type; possible values are:
|
`TokenProperty` is a union type; possible values are:
|
||||||
- "all": applies the token to all properties it can control
|
- "all": applies the token to all properties it can control
|
||||||
- TokenBorderRadiusProps: "r1", "r2", "r3", "r4"
|
- TokenBorderRadiusProps: "borderRadiusTopLeft", "borderRadiusTopRight", "borderRadiusBottomRight", "borderRadiusBottomLeft"
|
||||||
- TokenShadowProps: "shadow"
|
- TokenShadowProps: "shadow"
|
||||||
- TokenColorProps: "fill", "stroke-color"
|
- TokenColorProps: "fill", "strokeColor"
|
||||||
- TokenDimensionProps: "x", "y", "stroke-width"
|
- TokenDimensionProps: "x", "y", "strokeWidth"
|
||||||
- TokenFontFamiliesProps: "font-families"
|
- TokenFontFamiliesProps: "fontFamilies"
|
||||||
- TokenFontSizesProps: "font-size"
|
- TokenFontSizesProps: "fontSize"
|
||||||
- TokenFontWeightProps: "font-weight"
|
- TokenFontWeightProps: "fontWeight"
|
||||||
- TokenLetterSpacingProps: "letter-spacing"
|
- TokenLetterSpacingProps: "letterSpacing"
|
||||||
- TokenNumberProps: "rotation", "line-height"
|
- TokenNumberProps: "rotation"
|
||||||
- TokenOpacityProps: "opacity"
|
- TokenOpacityProps: "opacity"
|
||||||
- TokenSizingProps: "width", "height", "layout-item-min-w", "layout-item-max-w", "layout-item-min-h", "layout-item-max-h"
|
- TokenSizingProps: "width", "height", "layoutItemMinW", "layoutItemMaxW", "layoutItemMinH", "layoutItemMaxH"
|
||||||
- TokenSpacingProps: "row-gap", "column-gap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
|
- TokenSpacingProps: "rowGap", "columnGap", "paddingLeft", "paddingTop", "paddingRight", "paddingBottom", "marginLeft", "marginTop", "marginRight", "marginBottom"
|
||||||
- TokenBorderWidthProps: "stroke-width"
|
- TokenBorderWidthProps: "strokeWidth"
|
||||||
- TokenTextCaseProps: "text-case"
|
- TokenTextCaseProps: "textCase"
|
||||||
- TokenTextDecorationProps: "text-decoration"
|
- TokenTextDecorationProps: "textDecoration"
|
||||||
- TokenTypographyProps: "typography"
|
- TokenTypographyProps: "typography"
|
||||||
* `token.applyToShapes(shapes, properties)` - Apply from token
|
* `token.applyToShapes(shapes, properties)` - Apply from token
|
||||||
* Application is **asynchronous** (wait for ~100ms to see the effects)
|
* Application is **asynchronous** (wait for ~100ms to see the effects)
|
||||||
@ -326,8 +374,27 @@ Applying tokens:
|
|||||||
- The actual shape properties that the tokens control will reflect the token's resolved value.
|
- The actual shape properties that the tokens control will reflect the token's resolved value.
|
||||||
|
|
||||||
Removing tokens:
|
Removing tokens:
|
||||||
Simply set the respective property directly - token binding is automatically removed, e.g.
|
Simply set the respective property directly - token binding is automatically removed, e.g.
|
||||||
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
|
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
|
||||||
|
|
||||||
|
# Visual Inspection of Designs
|
||||||
|
|
||||||
|
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
||||||
|
|
||||||
|
# Creating and Translating Designs
|
||||||
|
|
||||||
|
* When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
||||||
|
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
||||||
|
non-creative defaults such as white/black if you are lacking information).
|
||||||
|
|
||||||
|
# Revising Designs
|
||||||
|
|
||||||
|
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
||||||
|
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
||||||
|
Container sizes are usually intentional, check content first.
|
||||||
|
* Check for reasonable font sizes and typefaces
|
||||||
|
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
||||||
|
Consider converting boards to flex layout when appropriate.
|
||||||
|
|
||||||
--
|
--
|
||||||
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.
|
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.
|
||||||
|
|||||||
@ -6,8 +6,7 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:js-yaml --external:sharp",
|
"build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:js-yaml --external:sharp",
|
||||||
"build": "pnpm run build:server && cp -r src/static dist/static && cp -r data dist/data",
|
"build": "pnpm run build:server && node scripts/copy-resources.js",
|
||||||
"build:multi-user": "pnpm run build",
|
|
||||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"start:multi-user": "node dist/index.js --multi-user",
|
"start:multi-user": "node dist/index.js --multi-user",
|
||||||
@ -23,7 +22,7 @@
|
|||||||
],
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
@ -39,6 +38,7 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"@penpot/mcp-common": "workspace:../common",
|
"@penpot/mcp-common": "workspace:../common",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|||||||
5
mcp/packages/server/scripts/copy-resources.js
Normal file
5
mcp/packages/server/scripts/copy-resources.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { cpSync } from "fs";
|
||||||
|
|
||||||
|
// copy static assets and data to dist
|
||||||
|
cpSync("src/static", "dist/static", { recursive: true });
|
||||||
|
cpSync("data", "dist/data", { recursive: true });
|
||||||
@ -4,15 +4,12 @@ import { createLogger } from "./logger.js";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration loader for prompts and server settings.
|
* Configuration loader for prompts and server settings.
|
||||||
*
|
|
||||||
* Handles loading and parsing of YAML configuration files,
|
|
||||||
* providing type-safe access to configuration values with
|
|
||||||
* appropriate fallbacks for missing files or values.
|
|
||||||
*/
|
*/
|
||||||
export class ConfigurationLoader {
|
export class ConfigurationLoader {
|
||||||
private readonly logger = createLogger("ConfigurationLoader");
|
private readonly logger = createLogger("ConfigurationLoader");
|
||||||
private readonly baseDir: string;
|
private readonly baseDir: string;
|
||||||
private initialInstructions: string;
|
private readonly initialInstructions: string;
|
||||||
|
private readonly baseInstructions: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new configuration loader instance.
|
* Creates a new configuration loader instance.
|
||||||
@ -22,6 +19,7 @@ export class ConfigurationLoader {
|
|||||||
constructor(baseDir: string) {
|
constructor(baseDir: string) {
|
||||||
this.baseDir = baseDir;
|
this.baseDir = baseDir;
|
||||||
this.initialInstructions = this.loadFileContent(join(this.baseDir, "data", "initial_instructions.md"));
|
this.initialInstructions = this.loadFileContent(join(this.baseDir, "data", "initial_instructions.md"));
|
||||||
|
this.baseInstructions = this.loadFileContent(join(this.baseDir, "data", "base_instructions.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadFileContent(filePath: string): string {
|
private loadFileContent(filePath: string): string {
|
||||||
@ -32,11 +30,22 @@ export class ConfigurationLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the initial instructions for the MCP server.
|
* Gets the initial instructions for the MCP server corresponding to the
|
||||||
|
* 'Penpot High-Level Overview'
|
||||||
*
|
*
|
||||||
* @returns The initial instructions string, or undefined if not configured
|
* @returns The initial instructions string
|
||||||
*/
|
*/
|
||||||
public getInitialInstructions(): string {
|
public getInitialInstructions(): string {
|
||||||
return this.initialInstructions;
|
return this.initialInstructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base instructions which shall be provided to clients when connecting to
|
||||||
|
* the MCP server
|
||||||
|
*
|
||||||
|
* @returns The initial instructions string
|
||||||
|
*/
|
||||||
|
public getBaseInstructions(): string {
|
||||||
|
return this.baseInstructions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,34 +21,61 @@ export interface SessionContext {
|
|||||||
userToken?: string;
|
userToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an active Streamable HTTP session, grouping the transport, MCP server, and session metadata.
|
||||||
|
*/
|
||||||
|
class StreamableSession {
|
||||||
|
constructor(
|
||||||
|
public readonly transport: StreamableHTTPServerTransport,
|
||||||
|
public readonly userToken: string | undefined,
|
||||||
|
public lastActiveTime: number
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds information about a registered tool, including its instance, name, and configuration.
|
||||||
|
*/
|
||||||
|
class ToolInfo {
|
||||||
|
constructor(
|
||||||
|
public readonly instance: Tool<any>,
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly config: { description: string; inputSchema: any }
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
export class PenpotMcpServer {
|
export class PenpotMcpServer {
|
||||||
|
/**
|
||||||
|
* Timeout, in minutes, for idle Streamable HTTP sessions before they are automatically closed and removed.
|
||||||
|
*/
|
||||||
|
private static readonly SESSION_TIMEOUT_MINUTES = 60;
|
||||||
|
|
||||||
private readonly logger = createLogger("PenpotMcpServer");
|
private readonly logger = createLogger("PenpotMcpServer");
|
||||||
private readonly server: McpServer;
|
private readonly tools: ToolInfo[];
|
||||||
private readonly tools: Map<string, Tool<any>>;
|
|
||||||
public readonly configLoader: ConfigurationLoader;
|
public readonly configLoader: ConfigurationLoader;
|
||||||
private app: any;
|
private app: any;
|
||||||
public readonly pluginBridge: PluginBridge;
|
public readonly pluginBridge: PluginBridge;
|
||||||
private readonly replServer: ReplServer;
|
private readonly replServer: ReplServer;
|
||||||
private apiDocs: ApiDocs;
|
private apiDocs: ApiDocs;
|
||||||
|
private readonly penpotHighLevelOverview: string;
|
||||||
|
private readonly connectionInstructions: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages session-specific context, particularly user tokens for each request.
|
* Manages session-specific context, particularly user tokens for each request.
|
||||||
*/
|
*/
|
||||||
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
||||||
|
|
||||||
private readonly transports = {
|
private readonly streamableTransports: Record<string, StreamableSession> = {};
|
||||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
private readonly sseTransports: Record<string, { transport: SSEServerTransport; userToken?: string }> = {};
|
||||||
sse: {} as Record<string, { transport: SSEServerTransport; userToken?: string }>,
|
|
||||||
};
|
|
||||||
|
|
||||||
public readonly host: string;
|
public readonly host: string;
|
||||||
public readonly port: number;
|
public readonly port: number;
|
||||||
public readonly webSocketPort: number;
|
public readonly webSocketPort: number;
|
||||||
public readonly replPort: number;
|
public readonly replPort: number;
|
||||||
|
private sessionTimeoutInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
|
||||||
constructor(private isMultiUser: boolean = false) {
|
constructor(private isMultiUser: boolean = false) {
|
||||||
// read port configuration from environment variables
|
// read port configuration from environment variables
|
||||||
this.host = process.env.PENPOT_MCP_SERVER_HOST ?? "0.0.0.0";
|
this.host = process.env.PENPOT_MCP_SERVER_HOST ?? "localhost";
|
||||||
this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10);
|
this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10);
|
||||||
this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10);
|
this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10);
|
||||||
this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10);
|
this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10);
|
||||||
@ -56,21 +83,16 @@ export class PenpotMcpServer {
|
|||||||
this.configLoader = new ConfigurationLoader(process.cwd());
|
this.configLoader = new ConfigurationLoader(process.cwd());
|
||||||
this.apiDocs = new ApiDocs();
|
this.apiDocs = new ApiDocs();
|
||||||
|
|
||||||
this.server = new McpServer(
|
// prepare instructions
|
||||||
{
|
let instructions = this.configLoader.getInitialInstructions();
|
||||||
name: "penpot-mcp-server",
|
instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", "));
|
||||||
version: "1.0.0",
|
this.penpotHighLevelOverview = instructions;
|
||||||
},
|
this.connectionInstructions = this.configLoader.getBaseInstructions();
|
||||||
{
|
|
||||||
instructions: this.getInitialInstructions(),
|
this.tools = this.initTools();
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.tools = new Map<string, Tool<any>>();
|
|
||||||
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
|
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
|
||||||
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
|
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
|
||||||
|
|
||||||
this.registerTools();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,10 +126,11 @@ export class PenpotMcpServer {
|
|||||||
return !this.isRemoteMode();
|
return !this.isRemoteMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getInitialInstructions(): string {
|
/**
|
||||||
let instructions = this.configLoader.getInitialInstructions();
|
* Retrieves the high-level overview instructions explaining core Penpot usage.
|
||||||
instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", "));
|
*/
|
||||||
return instructions;
|
public getHighLevelOverviewInstructions(): string {
|
||||||
|
return this.penpotHighLevelOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,88 +142,134 @@ export class PenpotMcpServer {
|
|||||||
return this.sessionContext.getStore();
|
return this.sessionContext.getStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerTools(): void {
|
private initTools(): ToolInfo[] {
|
||||||
// Create relevant tool instances (depending on file system access)
|
|
||||||
const toolInstances: Tool<any>[] = [
|
const toolInstances: Tool<any>[] = [
|
||||||
new ExecuteCodeTool(this),
|
new ExecuteCodeTool(this),
|
||||||
new HighLevelOverviewTool(this),
|
new HighLevelOverviewTool(this),
|
||||||
new PenpotApiInfoTool(this, this.apiDocs),
|
new PenpotApiInfoTool(this, this.apiDocs),
|
||||||
new ExportShapeTool(this), // tool adapts to file system access internally
|
new ExportShapeTool(this),
|
||||||
];
|
];
|
||||||
if (this.isFileSystemAccessEnabled()) {
|
if (this.isFileSystemAccessEnabled()) {
|
||||||
toolInstances.push(new ImportImageTool(this));
|
toolInstances.push(new ImportImageTool(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tool of toolInstances) {
|
return toolInstances.map((instance) => {
|
||||||
const toolName = tool.getToolName();
|
this.logger.info(`Registering tool: ${instance.getToolName()}`);
|
||||||
this.tools.set(toolName, tool);
|
return new ToolInfo(instance, instance.getToolName(), {
|
||||||
|
description: instance.getToolDescription(),
|
||||||
|
inputSchema: instance.getInputSchema(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register each tool with McpServer
|
/**
|
||||||
this.logger.info(`Registering tool: ${toolName}`);
|
* Creates a fresh {@link McpServer} instance with all tools registered.
|
||||||
this.server.registerTool(
|
*/
|
||||||
toolName,
|
private createMcpServer(): McpServer {
|
||||||
{
|
const server = new McpServer(
|
||||||
description: tool.getToolDescription(),
|
{ name: "penpot", version: "1.0.0" },
|
||||||
inputSchema: tool.getInputSchema(),
|
{ instructions: this.connectionInstructions }
|
||||||
},
|
);
|
||||||
async (args) => {
|
|
||||||
return tool.execute(args);
|
for (const tool of this.tools) {
|
||||||
}
|
server.registerTool(tool.name, tool.config, async (args: any) => tool.instance.execute(args));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a periodic timer that closes and removes Streamable HTTP sessions that have been
|
||||||
|
* idle for longer than {@link SESSION_TIMEOUT_MINUTES}.
|
||||||
|
*/
|
||||||
|
private startSessionTimeoutChecker(): void {
|
||||||
|
const timeoutMs = PenpotMcpServer.SESSION_TIMEOUT_MINUTES * 60 * 1000;
|
||||||
|
const checkIntervalMs = timeoutMs / 2;
|
||||||
|
this.sessionTimeoutInterval = setInterval(() => {
|
||||||
|
this.logger.info("Checking for stale sessions...");
|
||||||
|
const now = Date.now();
|
||||||
|
let removed = 0;
|
||||||
|
for (const session of Object.values(this.streamableTransports)) {
|
||||||
|
if (now - session.lastActiveTime > timeoutMs) {
|
||||||
|
session.transport.close();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(
|
||||||
|
`Removed ${removed} stale session(s); total sessions remaining: ${Object.keys(this.streamableTransports).length}`
|
||||||
|
);
|
||||||
|
}, checkIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupHttpEndpoints(): void {
|
private setupHttpEndpoints(): void {
|
||||||
/**
|
/**
|
||||||
* Modern Streamable HTTP connection endpoint
|
* Modern Streamable HTTP connection endpoint.
|
||||||
|
*
|
||||||
|
* New sessions are created on initialize requests (no mcp-session-id header).
|
||||||
|
* Subsequent requests for an existing session are routed to the stored transport,
|
||||||
|
* with the session context populated from the stored userToken.
|
||||||
*/
|
*/
|
||||||
this.app.all("/mcp", async (req: any, res: any) => {
|
this.app.all("/mcp", async (req: any, res: any) => {
|
||||||
const userToken = req.query.userToken as string | undefined;
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||||
|
let userToken: string | undefined = undefined;
|
||||||
|
let transport: StreamableHTTPServerTransport;
|
||||||
|
|
||||||
await this.sessionContext.run({ userToken }, async () => {
|
// obtain transport and user token for the session, either from an existing session or by creating a new one
|
||||||
|
if (sessionId && this.streamableTransports[sessionId]) {
|
||||||
|
// existing session: reuse stored transport and token
|
||||||
|
const session = this.streamableTransports[sessionId];
|
||||||
|
transport = session.transport;
|
||||||
|
userToken = session.userToken;
|
||||||
|
session.lastActiveTime = Date.now();
|
||||||
|
this.logger.info(
|
||||||
|
`Received request for existing session with id=${sessionId}; userToken=${session.userToken}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// new session: create a fresh McpServer and transport
|
||||||
|
userToken = req.query.userToken as string | undefined;
|
||||||
|
this.logger.info(`Received new session request; userToken=${userToken}`);
|
||||||
const { randomUUID } = await import("node:crypto");
|
const { randomUUID } = await import("node:crypto");
|
||||||
|
const server = this.createMcpServer();
|
||||||
|
transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID(),
|
||||||
|
onsessioninitialized: (id) => {
|
||||||
|
this.streamableTransports[id] = new StreamableSession(transport, userToken, Date.now());
|
||||||
|
this.logger.info(
|
||||||
|
`Session initialized with id=${id} for userToken=${userToken}; total sessions: ${Object.keys(this.streamableTransports).length}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
transport.onclose = () => {
|
||||||
|
if (transport.sessionId) {
|
||||||
|
this.logger.info(`Closing session with id=${transport.sessionId} for userToken=${userToken}`);
|
||||||
|
delete this.streamableTransports[transport.sessionId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await server.connect(transport);
|
||||||
|
}
|
||||||
|
|
||||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
// handle the request
|
||||||
let transport: StreamableHTTPServerTransport;
|
await this.sessionContext.run({ userToken }, async () => {
|
||||||
|
|
||||||
if (sessionId && this.transports.streamable[sessionId]) {
|
|
||||||
transport = this.transports.streamable[sessionId];
|
|
||||||
} else {
|
|
||||||
transport = new StreamableHTTPServerTransport({
|
|
||||||
sessionIdGenerator: () => randomUUID(),
|
|
||||||
onsessioninitialized: (id: string) => {
|
|
||||||
this.transports.streamable[id] = transport;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
transport.onclose = () => {
|
|
||||||
if (transport.sessionId) {
|
|
||||||
delete this.transports.streamable[transport.sessionId];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.server.connect(transport);
|
|
||||||
}
|
|
||||||
|
|
||||||
await transport.handleRequest(req, res, req.body);
|
await transport.handleRequest(req, res, req.body);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy SSE connection endpoint
|
* Legacy SSE connection endpoint.
|
||||||
*/
|
*/
|
||||||
this.app.get("/sse", async (req: any, res: any) => {
|
this.app.get("/sse", async (req: any, res: any) => {
|
||||||
const userToken = req.query.userToken as string | undefined;
|
const userToken = req.query.userToken as string | undefined;
|
||||||
|
|
||||||
await this.sessionContext.run({ userToken }, async () => {
|
await this.sessionContext.run({ userToken }, async () => {
|
||||||
const transport = new SSEServerTransport("/messages", res);
|
const transport = new SSEServerTransport("/messages", res);
|
||||||
this.transports.sse[transport.sessionId] = { transport, userToken };
|
this.sseTransports[transport.sessionId] = { transport, userToken };
|
||||||
|
|
||||||
|
const server = this.createMcpServer();
|
||||||
|
await server.connect(transport);
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
delete this.transports.sse[transport.sessionId];
|
delete this.sseTransports[transport.sessionId];
|
||||||
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.server.connect(transport);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,7 +278,7 @@ export class PenpotMcpServer {
|
|||||||
*/
|
*/
|
||||||
this.app.post("/messages", async (req: any, res: any) => {
|
this.app.post("/messages", async (req: any, res: any) => {
|
||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const session = this.transports.sse[sessionId];
|
const session = this.sseTransports[sessionId];
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
await this.sessionContext.run({ userToken: session.userToken }, async () => {
|
await this.sessionContext.run({ userToken: session.userToken }, async () => {
|
||||||
@ -236,8 +305,9 @@ export class PenpotMcpServer {
|
|||||||
this.logger.info(`Legacy SSE endpoint: http://${this.host}:${this.port}/sse`);
|
this.logger.info(`Legacy SSE endpoint: http://${this.host}:${this.port}/sse`);
|
||||||
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
||||||
|
|
||||||
// start the REPL server
|
// start the REPL server and session timeout checker
|
||||||
await this.replServer.start();
|
await this.replServer.start();
|
||||||
|
this.startSessionTimeoutChecker();
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@ -251,6 +321,7 @@ export class PenpotMcpServer {
|
|||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this.logger.info("Stopping Penpot MCP Server...");
|
this.logger.info("Stopping Penpot MCP Server...");
|
||||||
|
clearInterval(this.sessionTimeoutInterval);
|
||||||
await this.replServer.stop();
|
await this.replServer.stop();
|
||||||
this.logger.info("Penpot MCP Server stopped");
|
this.logger.info("Penpot MCP Server stopped");
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user