mirror of
https://github.com/penpot/penpot.git
synced 2026-05-19 14:14:05 -04:00
⏪ Backport MCP from staging (part 1)
This commit is contained in:
@@ -90,6 +90,8 @@
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
|
||||
@@ -463,8 +463,10 @@
|
||||
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
|
||||
|
||||
{: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!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE access_token
|
||||
ADD COLUMN type text NULL;
|
||||
@@ -23,7 +23,7 @@
|
||||
(dissoc row :perms))
|
||||
|
||||
(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)
|
||||
expires-at (some-> expiration (ct/in-future))
|
||||
created-at (ct/now)
|
||||
@@ -36,6 +36,7 @@
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:type type
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
@@ -50,17 +51,18 @@
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
[: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
|
||||
{::doc/added "1.18"
|
||||
::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/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
|
||||
[:map {:title "delete-access-token"}
|
||||
@@ -83,5 +85,22 @@
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id}
|
||||
{: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)))
|
||||
|
||||
(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
|
||||
[:map {:title "ProfileProps"}
|
||||
[:plugins {:optional true} schema:plugin-registry]
|
||||
[:mcp-enabled {:optional true} ::sm/boolean]
|
||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||
[:newsletter-news {:optional true} ::sm/boolean]
|
||||
[:onboarding-team-id {:optional true} ::sm/uuid]
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
(t/deftest access-token-authz
|
||||
(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*)]
|
||||
|
||||
(let [response (handler nil)]
|
||||
|
||||
@@ -107,4 +107,18 @@
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error 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
|
||||
|
||||
;; Activates the nitrate module
|
||||
:nitrate})
|
||||
:nitrate
|
||||
|
||||
:mcp})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -13,3 +13,10 @@
|
||||
unit tests or backend code for logs or error messages."
|
||||
[key & _args]
|
||||
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]
|
||||
(assert (uuid? token-id) "expected uuid for `token-id`")
|
||||
(if-let [token (get-token- this token-id)]
|
||||
(let [token' (-> (make-token (f token))
|
||||
(assoc :modified-at (ct/now)))]
|
||||
(TokenSet. id
|
||||
name
|
||||
description
|
||||
(ct/now)
|
||||
(if (= (:name token) (:name token'))
|
||||
(assoc tokens (:name token') token')
|
||||
(-> tokens
|
||||
(d/oassoc-before (:name token) (:name token') token')
|
||||
(dissoc (:name token))))))
|
||||
(let [token' (f token)]
|
||||
(if (not= token token')
|
||||
(let [token' (assoc token' :modified-at (ct/now))]
|
||||
(TokenSet. id
|
||||
name
|
||||
description
|
||||
(ct/now)
|
||||
(if (= (:name token) (:name token'))
|
||||
(assoc tokens (:name token') token')
|
||||
(-> tokens
|
||||
(d/oassoc-before (:name token) (:name token') token')
|
||||
(dissoc (:name token))))))
|
||||
this))
|
||||
this))
|
||||
|
||||
(delete-token- [this token-id]
|
||||
@@ -303,6 +305,35 @@
|
||||
(-clj->js [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?
|
||||
[o]
|
||||
(instance? TokenSet o))
|
||||
@@ -357,6 +388,7 @@
|
||||
(def check-token-set
|
||||
(sm/check-fn schema:token-set :hint "expected valid token set"))
|
||||
|
||||
|
||||
(defn map->token-set
|
||||
[& {:as attrs}]
|
||||
(TokenSet. (:id attrs)
|
||||
@@ -372,38 +404,10 @@
|
||||
(update :modified-at #(or % (ct/now)))
|
||||
(update :tokens #(into (d/ordered-map) %))
|
||||
(update :description d/nilv "")
|
||||
(update :name normalize-set-name)
|
||||
(check-token-set-attrs)
|
||||
(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?
|
||||
"Check if a set name is normalized (no extra spaces)."
|
||||
[name]
|
||||
@@ -609,7 +613,7 @@
|
||||
is-source
|
||||
external-id
|
||||
(ct/now)
|
||||
set-names))
|
||||
(into #{} (filter some?) set-names)))
|
||||
|
||||
(enable-set [this set-name]
|
||||
(set-sets this (conj sets set-name)))
|
||||
@@ -630,14 +634,9 @@
|
||||
|
||||
(update-set-name [this prev-set-name set-name]
|
||||
(if (get sets prev-set-name)
|
||||
(TokenTheme. id
|
||||
name
|
||||
group
|
||||
description
|
||||
is-source
|
||||
external-id
|
||||
(ct/now)
|
||||
(conj (disj sets prev-set-name) set-name))
|
||||
(let [sets (-> (disj sets prev-set-name)
|
||||
(conj set-name))]
|
||||
(set-sets this sets))
|
||||
this))
|
||||
|
||||
(theme-matches-group-name [this group name]
|
||||
@@ -722,7 +721,8 @@
|
||||
(update :is-source d/nilv false)
|
||||
(update :external-id #(or % (str new-id)))
|
||||
(update :modified-at #(or % (ct/now)))
|
||||
(update :sets set)
|
||||
(update :sets #(into #{} (comp (filter some?)
|
||||
(map normalize-set-name)) %))
|
||||
(check-token-theme-attrs)
|
||||
(map->TokenTheme))))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ localhost:3449 {
|
||||
header -Strict-Transport-Security
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
:3450 {
|
||||
# For subpath test
|
||||
# handle_path /penpot/* {
|
||||
# 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 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 ll='ls --color -GFlh'
|
||||
alias rm='rm -rf'
|
||||
|
||||
@@ -126,6 +126,12 @@ http {
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
location /plugins {
|
||||
autoindex on;
|
||||
alias /home/penpot/penpot/plugins/dist/apps;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
location /mcp/ws {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
@@ -5,7 +5,8 @@ ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
PATH=/opt/node/bin:$PATH \
|
||||
PENPOT_MCP_SERVER_HOST=0.0.0.0
|
||||
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"devDependencies": {
|
||||
"@penpot/draft-js": "workspace:./packages/draft-js",
|
||||
"@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/text-editor": "workspace:./text-editor",
|
||||
"@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
|
||||
version: link:packages/mousetrap
|
||||
'@penpot/plugins-runtime':
|
||||
specifier: link:../plugins/dist/plugins-runtime
|
||||
version: link:../plugins/dist/plugins-runtime
|
||||
specifier: link:../plugins/libs/plugins-runtime
|
||||
version: link:../plugins/libs/plugins-runtime
|
||||
'@penpot/svgo':
|
||||
specifier: penpot/svgo#v3.2
|
||||
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
||||
|
||||
@@ -36,7 +36,7 @@ popd
|
||||
pushd ../mcp;
|
||||
rm -rf node_modules;
|
||||
./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;
|
||||
|
||||
pnpm run build:app:main $EXTRA_PARAMS;
|
||||
|
||||
@@ -172,6 +172,10 @@
|
||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||
(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
|
||||
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
||||
public-uri))
|
||||
@@ -205,6 +209,9 @@
|
||||
(let [f (obj/get global "initializeExternalConfigInfo")]
|
||||
(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
|
||||
|
||||
(defn ^boolean check-browser? [candidate]
|
||||
|
||||
@@ -57,5 +57,6 @@
|
||||
[type data]
|
||||
(ptk/reify ::event
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(emit! type data))))
|
||||
(effect [_ state _]
|
||||
(let [session-id (get state :session-id)]
|
||||
(emit! session-id type data)))))
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
(ns app.main.data.plugins
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.flags :as pflag]
|
||||
[app.plugins.register :as preg]
|
||||
@@ -20,7 +22,8 @@
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn save-plugin-permissions-peek
|
||||
[id permissions]
|
||||
@@ -52,27 +55,47 @@
|
||||
(update [_ state]
|
||||
(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!
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (pflag/clear plugin-id)
|
||||
(save-current-plugin plugin-id))
|
||||
[{:keys [plugin-id name version description host code icon permissions]}]
|
||||
(st/emit! (pflag/clear plugin-id)
|
||||
(save-current-plugin plugin-id))
|
||||
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:description description
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
(-> (.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:description description
|
||||
:version version
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
|
||||
(catch :default e
|
||||
(st/emit! (remove-current-plugin plugin-id))
|
||||
(.error js/console "Error" e))))
|
||||
(p/catch (fn [cause]
|
||||
(st/emit! (remove-current-plugin plugin-id))
|
||||
(ex/print-throwable cause :prefix "Plugin Error")
|
||||
(errors/flash :cause cause :type :handled)))))
|
||||
|
||||
(defn open-plugin!
|
||||
[{:keys [url] :as manifest} user-can-edit?]
|
||||
|
||||
@@ -498,4 +498,3 @@
|
||||
(->> (rp/cmd! :delete-access-token params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
[app.main.data.workspace.layers :as dwly]
|
||||
[app.main.data.workspace.layout :as layout]
|
||||
[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.pages :as dwpg]
|
||||
[app.main.data.workspace.path :as dwdp]
|
||||
@@ -211,8 +212,11 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dp/check-open-plugin)
|
||||
(fdf/fix-deleted-fonts-for-local-library file-id)))))
|
||||
(rx/merge
|
||||
(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
|
||||
[{:keys [file file-id thumbnails] :as bundle}]
|
||||
@@ -304,163 +308,169 @@
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
|
||||
(->> (rx/merge
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/filter true?)
|
||||
(rx/tap (fn [_]
|
||||
(let [event (ug/event "penpot:wasm:loaded")]
|
||||
(ug/dispatch! event))))
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
(rx/concat
|
||||
(->> (rx/merge
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/filter true?)
|
||||
(rx/tap (fn [_]
|
||||
(let [event (ug/event "penpot:wasm:loaded")]
|
||||
(ug/dispatch! event))))
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(df/fetch-fonts team-id)))
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(df/fetch-fonts team-id))
|
||||
|
||||
;; Once the essential data is fetched, lets proceed to
|
||||
;; fetch teh file bunldle
|
||||
(rx/of (fetch-bundle file-id features)))
|
||||
(when (contains? cf/flags :mcp)
|
||||
(rx/of (du/fetch-access-tokens))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::bundle-fetched))
|
||||
(rx/take 1)
|
||||
(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))
|
||||
;; Once the essential data is fetched, lets proceed to
|
||||
;; fetch teh file bunldle
|
||||
(rx/of (fetch-bundle file-id features)))
|
||||
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id features)
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::bundle-fetched))
|
||||
(rx/take 1)
|
||||
(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))
|
||||
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/tap (fn [_] (perf/setup)))))
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id features)
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
(rx/map dwc/set-workspace-visited))
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(->> stream
|
||||
(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
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
(rx/map dwc/set-workspace-visited))
|
||||
|
||||
(when (:board-id rparams)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(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)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||
(when (:board-id rparams)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
|
||||
(when render-wasm?
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [redo-changes]}]
|
||||
(let [added (->> redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(map :id))]
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
(when render-wasm?
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [redo-changes]}]
|
||||
(let [added (->> redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(map :id))]
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(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 %))))))))
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
|
||||
(rx/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
: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))))
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(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 %))))))))
|
||||
|
||||
(->> 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/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
: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
|
||||
(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 update-presence))))))
|
||||
|
||||
|
||||
(defn handle-pointer-update
|
||||
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]
|
||||
(ptk/reify ::handle-pointer-update
|
||||
|
||||
@@ -49,14 +49,14 @@
|
||||
|
||||
;; (note that dwsh/update-shapes function returns an event)
|
||||
|
||||
(defn update-shape-radius-all
|
||||
([value shape-ids attributes] (update-shape-radius-all 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
|
||||
(defn update-shape-radius
|
||||
([value shape-ids attributes] (update-shape-radius value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(when (number? value)
|
||||
(let [value (max 0 value)]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(ctsr/set-radius-to-all-corners shape value))
|
||||
(ctsr/set-radius-for-corners shape attributes value))
|
||||
{:reg-objects? true
|
||||
:ignore-touched true
|
||||
:page-id page-id
|
||||
@@ -531,7 +531,7 @@
|
||||
|
||||
(some 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
|
||||
value shape-ids
|
||||
(set (filter attributes #{:r1 :r2 :r3 :r4}))
|
||||
@@ -607,6 +607,46 @@
|
||||
:state state})]
|
||||
(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 ------------------------------------------------------------
|
||||
|
||||
(defn apply-token
|
||||
@@ -620,65 +660,73 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; We do not allow to apply tokens while text editor is open.
|
||||
(if (empty? (get state :workspace-editor-state))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (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)
|
||||
;; The classic text editor sets :workspace-editor-state; the WASM text editor
|
||||
;; does not, so we also check :workspace-local :edition for text shapes.
|
||||
(let [edition (get-in state [:workspace-local :edition])
|
||||
objects (dsh/lookup-page-objects state)
|
||||
text-editing? (and (some? edition)
|
||||
(= :text (:type (get objects edition))))]
|
||||
(if (and (empty? (get state :workspace-editor-state))
|
||||
(not text-editing?))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (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
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
shapes (->> selected-shapes
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))
|
||||
(rx/of (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 3000}))))))
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))
|
||||
|
||||
(rx/of (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 3000})))))))
|
||||
|
||||
(defn apply-spacing-token-separated
|
||||
"Handles edge-case for spacing token when applying token via toggle button.
|
||||
@@ -744,10 +792,16 @@
|
||||
{:keys [attributes all-attributes on-update-shape]}
|
||||
(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?
|
||||
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
|
||||
shape-ids (map :id shapes)]
|
||||
shape-ids
|
||||
(map :id shapes)]
|
||||
|
||||
(if unapply-tokens?
|
||||
(rx/of
|
||||
@@ -808,7 +862,7 @@
|
||||
:border-radius
|
||||
{:title "Border Radius"
|
||||
:attributes ctt/border-radius-keys
|
||||
:on-update-shape update-shape-radius-all
|
||||
:on-update-shape update-shape-radius
|
||||
:modal {:key :tokens/border-radius
|
||||
:fields [{:label "Border Radius"
|
||||
:key :border-radius}]}}
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
vec))
|
||||
|
||||
(defn combine-as-variants
|
||||
[ids {:keys [page-id trigger]}]
|
||||
[ids {:keys [page-id trigger variant-id]}]
|
||||
(ptk/reify ::combine-as-variants
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
@@ -647,7 +647,7 @@
|
||||
:shapes
|
||||
count
|
||||
inc)
|
||||
variant-id (uuid/next)
|
||||
variant-id (or variant-id (uuid/next))
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
(rx/concat
|
||||
|
||||
@@ -33,6 +33,16 @@
|
||||
;; Will contain last uncaught exception
|
||||
(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.
|
||||
;; 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
|
||||
@@ -206,6 +216,16 @@
|
||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||
(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
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
;; for show the error page. Otherwise this explicitly clears all
|
||||
@@ -420,6 +440,15 @@
|
||||
(and (string? stack)
|
||||
(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]
|
||||
(let [message (ex-message cause)]
|
||||
(or (from-extension? cause)
|
||||
@@ -447,32 +476,55 @@
|
||||
(on-unhandled-error [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(if (stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
(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)))))))))
|
||||
(cond
|
||||
(stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
|
||||
;; Plugin errors: log to console and ignore
|
||||
(from-plugin? cause)
|
||||
(ex/print-throwable cause :prefix "Plugin Error")
|
||||
|
||||
;; Other ignorable exceptions: ignore silently
|
||||
(is-ignorable-exception? cause)
|
||||
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]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(if (stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
(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)))))))))]
|
||||
(cond
|
||||
(stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
|
||||
;; Plugin errors: log to console and ignore
|
||||
(from-plugin? cause)
|
||||
(ex/print-throwable cause :prefix "Plugin Error")
|
||||
|
||||
;; Other ignorable exceptions: ignore silently
|
||||
(is-ignorable-exception? cause)
|
||||
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 "unhandledrejection" on-unhandled-rejection)
|
||||
|
||||
@@ -150,6 +150,9 @@
|
||||
(def workspace-global
|
||||
(l/derived :workspace-global st/state))
|
||||
|
||||
(def mcp
|
||||
(l/derived :mcp st/state))
|
||||
|
||||
(def workspace-drawing
|
||||
(l/derived :workspace-drawing st/state))
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
:settings-options
|
||||
:settings-feedback
|
||||
:settings-subscription
|
||||
:settings-access-tokens
|
||||
:settings-integrations
|
||||
:settings-notifications)
|
||||
(let [params (get params :query)
|
||||
error-report-id (some-> params :error-report-id uuid/parse*)]
|
||||
|
||||
@@ -9,14 +9,17 @@
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[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.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
(:import
|
||||
goog.events.EventType))
|
||||
|
||||
(mf/defc confirm-dialog
|
||||
{::mf/register modal/components
|
||||
@@ -68,8 +71,11 @@
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} title]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click cancel-fn} deprecated-icon/close]]
|
||||
[:div {:class (stl/css :modal-close-btn)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click cancel-fn
|
||||
:icon i/close}]]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(when (and (string? message) (not= message ""))
|
||||
@@ -87,24 +93,19 @@
|
||||
[:ul {:class (stl/css :component-list)}
|
||||
(for [item items]
|
||||
[:li {:class (stl/css :modal-item-element)}
|
||||
[:span {:class (stl/css :modal-component-icon)}
|
||||
deprecated-icon/component]
|
||||
[:> icon* {:icon-id i/component
|
||||
:class (stl/css :modal-component-icon)
|
||||
:size "s"}]
|
||||
[:span {:class (stl/css :modal-component-name)}
|
||||
(:name item)]])]])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
(when-not (= cancel-label :omit)
|
||||
[:input
|
||||
{:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value cancel-label
|
||||
:on-click cancel-fn}])
|
||||
|
||||
[:input
|
||||
{:class (stl/css-case :accept-btn true
|
||||
:danger (= accept-style :danger)
|
||||
:primary (= accept-style :primary))
|
||||
:type "button"
|
||||
:value accept-label
|
||||
:on-click accept-fn}]]]]]))
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click cancel-fn}
|
||||
cancel-label])
|
||||
[:> button* {:variant (cond (= accept-style :danger) "destructive"
|
||||
(= accept-style :primary) "primary")
|
||||
:on-click accept-fn}
|
||||
accept-label]]]]]))
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -27,12 +26,13 @@
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend .modal-close-btn-base;
|
||||
position: absolute;
|
||||
top: var(--sp-m);
|
||||
right: var(--sp-m);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-item-element {
|
||||
@@ -41,32 +41,18 @@
|
||||
|
||||
.modal-component-icon {
|
||||
@include deprecated.flexCenter;
|
||||
height: deprecated.$s-16;
|
||||
width: deprecated.$s-16;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--color);
|
||||
}
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.modal-component-name {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@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-subtitle,
|
||||
.modal-msg {
|
||||
|
||||
@@ -18,6 +18,7 @@ $sz-32: px2rem(32);
|
||||
$sz-36: px2rem(36);
|
||||
$sz-40: px2rem(40);
|
||||
$sz-48: px2rem(48);
|
||||
$sz-64: px2rem(64);
|
||||
$sz-88: px2rem(88);
|
||||
$sz-96: px2rem(96);
|
||||
$sz-120: px2rem(120);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
@@ -52,10 +51,11 @@
|
||||
:has-hint has-hint
|
||||
:hint-type hint-type
|
||||
:variant variant})]
|
||||
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
|
||||
:variant-dense (= variant "dense")
|
||||
:variant-comfortable (= variant "comfortable")
|
||||
:has-hint has-hint))}
|
||||
|
||||
[:div {:class [class (stl/css-case :input-wrapper true
|
||||
:variant-dense (= variant "dense")
|
||||
:variant-comfortable (= variant "comfortable")
|
||||
:has-hint has-hint)]}
|
||||
(when has-label
|
||||
[:> label* {:for id :is-optional is-optional} label])
|
||||
[:> input-field* props]
|
||||
@@ -64,4 +64,3 @@
|
||||
:class hint-class
|
||||
:message hint-message
|
||||
:type hint-type}])]))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.keyboard :as k]
|
||||
@@ -47,6 +48,23 @@
|
||||
|
||||
[:> 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*
|
||||
[{:keys [disabled on-submit] :rest props}]
|
||||
(let [form (mf/use-ctx context)
|
||||
@@ -79,4 +97,4 @@
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event))))]
|
||||
[:> (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]
|
||||
["/options" :settings-options]
|
||||
["/subscriptions" :settings-subscription]
|
||||
["/access-tokens" :settings-access-tokens]
|
||||
["/integrations" :settings-integrations]
|
||||
["/notifications" :settings-notifications]]
|
||||
|
||||
["/frame-preview" :frame-preview]
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[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.delete-account]
|
||||
[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.options :refer [options-page]]
|
||||
[app.main.ui.settings.password :refer [password-page]]
|
||||
@@ -73,8 +73,8 @@
|
||||
:settings-subscription
|
||||
[:> subscription-page* {:profile profile}]
|
||||
|
||||
:settings-access-tokens
|
||||
[:& access-tokens-page]
|
||||
:settings-integrations
|
||||
[:> integrations-page*]
|
||||
|
||||
:settings-notifications
|
||||
[:& 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
|
||||
#(st/emit! (rt/nav :settings-subscription)))
|
||||
|
||||
(def ^:private go-settings-access-tokens
|
||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
||||
(def ^:private go-settings-integrations
|
||||
#(st/emit! (rt/nav :settings-integrations)))
|
||||
|
||||
(def ^:private go-settings-notifications
|
||||
#(st/emit! (rt/nav :settings-notifications)))
|
||||
@@ -66,7 +66,7 @@
|
||||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
subscription? (= section :settings-subscription)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
integrations? (= section :settings-integrations)
|
||||
notifications? (= section :settings-notifications)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
@@ -115,12 +115,13 @@
|
||||
:data-testid "settings-subscription"}
|
||||
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
|
||||
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
[:li {:class (stl/css-case :current access-tokens?
|
||||
(when (or (contains? cf/flags :access-tokens)
|
||||
(contains? cf/flags :mcp))
|
||||
[:li {:class (stl/css-case :current integrations?
|
||||
:settings-item true)
|
||||
:on-click go-settings-access-tokens
|
||||
:data-testid "settings-access-tokens"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
|
||||
:on-click go-settings-integrations
|
||||
:data-testid "settings-integrations"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]])
|
||||
|
||||
[:hr {:class (stl/css :sidebar-separator)}]
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.main.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.main-menu :as main-menu]
|
||||
[app.util.dom :as dom]
|
||||
@@ -27,12 +26,10 @@
|
||||
;; --- Header Component
|
||||
|
||||
(mf/defc left-header*
|
||||
[{:keys [file layout project page-id class]}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
file-id (:id file)
|
||||
[{:keys [file layout project class]}]
|
||||
(let [file-id (:id file)
|
||||
file-name (:name file)
|
||||
project-id (:id project)
|
||||
team-id (:team-id project)
|
||||
shared? (:is-shared file)
|
||||
persistence
|
||||
(mf/deref refs/persistence)
|
||||
@@ -40,8 +37,6 @@
|
||||
persistence-status
|
||||
(get persistence :status)
|
||||
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
editing* (mf/use-state false)
|
||||
editing? (deref editing*)
|
||||
input-ref (mf/use-ref nil)
|
||||
@@ -137,10 +132,5 @@
|
||||
(when ^boolean shared?
|
||||
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
|
||||
[:div {:class (stl/css :menu-section)}
|
||||
[:& main-menu/menu
|
||||
{:layout layout
|
||||
:file file
|
||||
:profile profile
|
||||
:read-only? read-only?
|
||||
:team-id team-id
|
||||
:page-id page-id}]]]))
|
||||
[:> main-menu/menu* {:layout layout
|
||||
:file file}]]]))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,125 +4,178 @@
|
||||
//
|
||||
// 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 {
|
||||
@extend .menu-dropdown;
|
||||
top: deprecated.$s-48;
|
||||
left: calc(var(--right-sidebar-width, deprecated.$s-256) - deprecated.$s-16);
|
||||
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;
|
||||
top: $sz-48;
|
||||
left: calc(var(--right-sidebar-width) - $sz-40);
|
||||
inline-size: $sz-192;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
@extend .menu-dropdown;
|
||||
left: calc(var(--right-sidebar-width, deprecated.$s-256) + deprecated.$s-180);
|
||||
width: deprecated.$s-192;
|
||||
min-width: calc(deprecated.$s-272 - deprecated.$s-2);
|
||||
width: 110%;
|
||||
left: calc(var(--right-sidebar-width) + $sz-154);
|
||||
min-width: $sz-284;
|
||||
width: 115%;
|
||||
|
||||
.submenu-item {
|
||||
@extend .menu-item-base;
|
||||
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
&.pos-1 {
|
||||
top: calc($sz-16 + $sz-32);
|
||||
}
|
||||
|
||||
.menu-disabled {
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
color: var(--color-foreground-secondary);
|
||||
background-color: var(--menu-background-color);
|
||||
}
|
||||
&.pos-2 {
|
||||
top: calc($sz-16 + (2 * $sz-32));
|
||||
}
|
||||
|
||||
&.file {
|
||||
top: deprecated.$s-48;
|
||||
&.pos-3 {
|
||||
top: calc($sz-16 + (3 * $sz-32));
|
||||
}
|
||||
|
||||
&.edit {
|
||||
top: deprecated.$s-76;
|
||||
&.pos-4 {
|
||||
top: calc($sz-16 + (4 * $sz-32));
|
||||
}
|
||||
|
||||
&.view {
|
||||
top: deprecated.$s-116;
|
||||
&.pos-5 {
|
||||
top: calc($sz-16 + (5 * $sz-32));
|
||||
}
|
||||
|
||||
&.preferences {
|
||||
top: deprecated.$s-148;
|
||||
&.pos-6 {
|
||||
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 {
|
||||
top: deprecated.$s-180;
|
||||
max-height: calc(100vh - deprecated.$s-180);
|
||||
max-height: calc(100vh - $sz-200);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.help-info {
|
||||
top: deprecated.$s-232;
|
||||
.base-menu-item {
|
||||
@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 {
|
||||
top: deprecated.$s-192;
|
||||
&.disabled {
|
||||
--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 {
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
color: var(--menu-icon-foreground-color);
|
||||
display: flex;
|
||||
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/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)
|
||||
project (mf/deref refs/project)
|
||||
file-id (get file :id)
|
||||
@@ -185,12 +185,10 @@
|
||||
:class aside-class
|
||||
:style {:--left-sidebar-width (dm/str width "px")}}
|
||||
|
||||
[:> left-header*
|
||||
{:file file
|
||||
:layout layout
|
||||
:project project
|
||||
:page-id page-id
|
||||
:class (stl/css :left-header)}]
|
||||
[:> left-header* {:file file
|
||||
:layout layout
|
||||
:project project
|
||||
:class (stl/css :left-header)}]
|
||||
|
||||
[:div {:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
:r4 "Bottom Left"
|
||||
:r3 "Bottom Right"}
|
||||
: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})
|
||||
shadow (partial generic-attribute-actions #{:shadow} "Shadow")]
|
||||
{:border-radius border-radius
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"RPC for plugins runtime."
|
||||
(:require
|
||||
["@penpot/plugins-runtime" :as runtime]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
@@ -30,6 +31,8 @@
|
||||
(ptk/reify ::initialize
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(set! errors/is-plugin-error? runtime/isPluginError)
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::features/initialize))
|
||||
(rx/observe-on :async)
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.changes :as ch]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.helpers :as dsh]
|
||||
@@ -26,6 +28,7 @@
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :refer [fetch-font-css]]
|
||||
@@ -82,6 +85,10 @@
|
||||
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
||||
|
||||
;; Public properties
|
||||
:version
|
||||
{:this true
|
||||
:get (constantly (:base cf/version))}
|
||||
|
||||
:root
|
||||
{:this true
|
||||
:get #(.getRoot ^js %)}
|
||||
@@ -110,7 +117,7 @@
|
||||
(fn [_ shapes]
|
||||
(cond
|
||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||
(u/display-not-valid :selection shapes)
|
||||
(u/not-valid plugin-id :selection shapes)
|
||||
|
||||
:else
|
||||
(let [ids (into (d/ordered-set) (map #(obj/get % "$id")) shapes)]
|
||||
@@ -175,7 +182,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [objects (u/locate-objects)
|
||||
@@ -195,13 +202,13 @@
|
||||
new-color (parser/parse-color-data new-color)]
|
||||
(cond
|
||||
(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))
|
||||
(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))
|
||||
(u/display-not-valid :replaceColor-newColor new-color)
|
||||
(u/not-valid plugin-id :replaceColor-newColor new-color)
|
||||
|
||||
:else
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
@@ -254,10 +261,10 @@
|
||||
(fn [name url]
|
||||
(cond
|
||||
(not (string? name))
|
||||
(u/display-not-valid :uploadMedia-name name)
|
||||
(u/not-valid plugin-id :uploadMedia-name name)
|
||||
|
||||
(not (string? url))
|
||||
(u/display-not-valid :uploadMedia-url url)
|
||||
(u/not-valid plugin-id :uploadMedia-url url)
|
||||
|
||||
:else
|
||||
(let [file-id (:current-file-id @st/state)]
|
||||
@@ -288,7 +295,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
@@ -303,10 +310,10 @@
|
||||
(fn [group & rest]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :ungroup rest)
|
||||
(u/not-valid plugin-id :ungroup rest)
|
||||
|
||||
:else
|
||||
(let [shapes (concat [group] rest)
|
||||
@@ -346,7 +353,7 @@
|
||||
(fn [text]
|
||||
(cond
|
||||
(or (not (string? text)) (empty? text))
|
||||
(u/display-not-valid :createText text)
|
||||
(u/not-valid plugin-id :createText text)
|
||||
|
||||
:else
|
||||
(let [page (dsh/lookup-page @st/state)
|
||||
@@ -377,7 +384,7 @@
|
||||
(fn [svg-string]
|
||||
(cond
|
||||
(or (not (string? svg-string)) (empty? svg-string))
|
||||
(u/display-not-valid :createShapeFromSvg svg-string)
|
||||
(u/not-valid plugin-id :createShapeFromSvg svg-string)
|
||||
|
||||
:else
|
||||
(let [id (uuid/next)
|
||||
@@ -394,7 +401,7 @@
|
||||
(cond
|
||||
(or (not (string? svg-string)) (empty? svg-string))
|
||||
(do
|
||||
(u/display-not-valid :createShapeFromSvg "Svg not valid")
|
||||
(u/not-valid plugin-id :createShapeFromSvg "Svg not valid")
|
||||
(reject "Svg not valid"))
|
||||
|
||||
:else
|
||||
@@ -412,10 +419,10 @@
|
||||
(let [bool-type (keyword bool-type)]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :createBoolean-shapes shapes)
|
||||
(u/not-valid plugin-id :createBoolean-shapes shapes)
|
||||
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
|
||||
@@ -429,10 +436,10 @@
|
||||
(let [type (d/nilv (obj/get options "type") "html")]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :generateMarkup-type type)
|
||||
(u/not-valid plugin-id :generateMarkup-type type)
|
||||
|
||||
:else
|
||||
(let [resolved-code
|
||||
@@ -464,16 +471,16 @@
|
||||
children? (d/nilv (obj/get options "includeChildren") true)]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :generateStyle-type type)
|
||||
(u/not-valid plugin-id :generateStyle-type type)
|
||||
|
||||
(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?)))
|
||||
(u/display-not-valid :generateStyle-includeChildren children?)
|
||||
(u/not-valid plugin-id :generateStyle-includeChildren children?)
|
||||
|
||||
:else
|
||||
(let [resolved-styles
|
||||
@@ -546,7 +553,7 @@
|
||||
:else nil)
|
||||
new-window (if (boolean? new-window) new-window false)]
|
||||
(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)))))
|
||||
|
||||
:alignHorizontal
|
||||
@@ -558,10 +565,10 @@
|
||||
nil)]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :alignHorizontal-shapes "Not valid shapes")
|
||||
(u/not-valid plugin-id :alignHorizontal-shapes "Not valid shapes")
|
||||
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
@@ -576,10 +583,10 @@
|
||||
nil)]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :alignVertical-shapes "Not valid shapes")
|
||||
(u/not-valid plugin-id :alignVertical-shapes "Not valid shapes")
|
||||
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
@@ -589,7 +596,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
@@ -599,7 +606,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
@@ -609,8 +616,41 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(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)]
|
||||
(cond
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(->> (rp/cmd! :update-comment {:id (:id data) :content content})
|
||||
@@ -81,7 +81,7 @@
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "comment:write"))
|
||||
(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"))
|
||||
|
||||
:else
|
||||
@@ -120,10 +120,10 @@
|
||||
(cond
|
||||
(or (not (sm/valid-safe-number? (:x 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"))
|
||||
(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
|
||||
(do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)]))
|
||||
@@ -137,10 +137,10 @@
|
||||
(fn [is-resolved]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved)))
|
||||
@@ -153,7 +153,7 @@
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "comment:read"))
|
||||
(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"))
|
||||
|
||||
:else
|
||||
@@ -169,10 +169,10 @@
|
||||
(fn [content]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :reply "Not valid")
|
||||
(u/not-valid plugin-id :reply "Not valid")
|
||||
|
||||
:else
|
||||
(js/Promise.
|
||||
@@ -186,10 +186,10 @@
|
||||
owner (dsh/lookup-profile @st/state (:owner-id data))]
|
||||
(cond
|
||||
(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)
|
||||
(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
|
||||
(js/Promise.
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
(fn [value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :label value)
|
||||
(u/not-valid plugin-id :label value)
|
||||
|
||||
:else
|
||||
(do (swap! data assoc :label value :created-by "user")
|
||||
@@ -145,7 +145,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getPluginData-key key)
|
||||
(u/not-valid plugin-id :getPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file id)]
|
||||
@@ -155,13 +155,13 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(or (not (string? key)) (empty? key))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data id :file (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -175,10 +175,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file id)]
|
||||
@@ -188,16 +188,16 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data id :file (keyword "shared" namespace) key value))))
|
||||
@@ -206,7 +206,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginDataKeys namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file id)]
|
||||
@@ -216,7 +216,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [page-id (uuid/next)]
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
|
||||
(ns app.plugins.flags
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
[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
|
||||
[id]
|
||||
(ptk/reify ::reset
|
||||
@@ -37,13 +31,27 @@
|
||||
:naturalChildOrdering
|
||||
{:this false
|
||||
:get
|
||||
(fn [] (natural-child-ordering? plugin-id))
|
||||
(fn [] (u/natural-child-ordering? plugin-id))
|
||||
|
||||
:set
|
||||
(fn [value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :naturalChildOrdering value)
|
||||
(u/not-valid plugin-id :naturalChildOrdering value)
|
||||
|
||||
: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.shapes :as dwsh]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.flags :refer [natural-child-ordering?]]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]))
|
||||
@@ -39,10 +38,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-flex-dir value})))))}
|
||||
@@ -55,10 +54,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-wrap-type value})))))}
|
||||
@@ -71,10 +70,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
||||
@@ -87,10 +86,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
||||
@@ -103,10 +102,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
||||
@@ -119,10 +118,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
||||
@@ -134,10 +133,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
||||
@@ -149,10 +148,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
||||
@@ -164,10 +163,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
||||
@@ -179,10 +178,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
||||
@@ -195,10 +194,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
||||
@@ -210,10 +209,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
||||
@@ -225,10 +224,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
||||
@@ -240,10 +239,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
||||
@@ -256,13 +255,13 @@
|
||||
(fn [child]
|
||||
(cond
|
||||
(not (shape-proxy? child))
|
||||
(u/display-not-valid :appendChild child)
|
||||
(u/not-valid plugin-id :appendChild child)
|
||||
|
||||
:else
|
||||
(let [child-id (obj/get child "$id")
|
||||
shape (u/locate-shape file-id page-id id)
|
||||
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
|
||||
(count (:shapes shape)))]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index)))))
|
||||
@@ -275,10 +274,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
||||
@@ -291,10 +290,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}))
|
||||
@@ -317,10 +316,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :absolute value)
|
||||
(u/not-valid plugin-id :absolute value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-absolute value}))))}
|
||||
@@ -332,10 +331,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-z-index value}))))}
|
||||
@@ -348,10 +347,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
|
||||
@@ -364,10 +363,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}
|
||||
@@ -380,10 +379,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-align-self value})))))}
|
||||
@@ -395,10 +394,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))}
|
||||
@@ -410,10 +409,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))}
|
||||
@@ -425,10 +424,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))}
|
||||
@@ -440,10 +439,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))}
|
||||
@@ -455,10 +454,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))}
|
||||
@@ -470,10 +469,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))}
|
||||
@@ -485,10 +484,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-w value}))))}
|
||||
@@ -500,10 +499,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-w value}))))}
|
||||
@@ -515,10 +514,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-h value}))))}
|
||||
@@ -530,10 +529,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-h value}))))}))
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
(obj/type-of? p "FontProxy"))
|
||||
|
||||
(defn font-proxy
|
||||
[{:keys [id family name variants] :as font}]
|
||||
[plugin-id {:keys [id family name variants] :as font}]
|
||||
(when (some? font)
|
||||
(let [default-variant (fonts/get-default-variant font)]
|
||||
(obj/reify {:name "FontProxy"}
|
||||
@@ -55,10 +55,10 @@
|
||||
(fn [text variant]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get text "$id")
|
||||
@@ -73,10 +73,10 @@
|
||||
(fn [range variant]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get range "$id")
|
||||
@@ -98,53 +98,53 @@
|
||||
{:get
|
||||
(fn []
|
||||
(format/format-array
|
||||
font-proxy
|
||||
(partial font-proxy plugin-id)
|
||||
(vals @fonts/fontsdb)))}
|
||||
|
||||
:findById
|
||||
(fn [id]
|
||||
(cond
|
||||
(not (string? id))
|
||||
(u/display-not-valid :findbyId id)
|
||||
(u/not-valid plugin-id :findbyId id)
|
||||
|
||||
:else
|
||||
(->> (vals @fonts/fontsdb)
|
||||
(d/seek #(str/includes? (str/lower (:id %)) (str/lower id)))
|
||||
(font-proxy))))
|
||||
(font-proxy plugin-id))))
|
||||
|
||||
:findByName
|
||||
(fn [name]
|
||||
(cond
|
||||
(not (string? name))
|
||||
(u/display-not-valid :findByName name)
|
||||
(u/not-valid plugin-id :findByName name)
|
||||
|
||||
:else
|
||||
(->> (vals @fonts/fontsdb)
|
||||
(d/seek #(str/includes? (str/lower (:name %)) (str/lower name)))
|
||||
(font-proxy))))
|
||||
(font-proxy plugin-id))))
|
||||
|
||||
:findAllById
|
||||
(fn [id]
|
||||
(cond
|
||||
(not (string? id))
|
||||
(u/display-not-valid :findAllById name)
|
||||
(u/not-valid plugin-id :findAllById name)
|
||||
|
||||
:else
|
||||
(format/format-array
|
||||
(fn [font]
|
||||
(when (str/includes? (str/lower (:id font)) (str/lower id))
|
||||
(font-proxy font)))
|
||||
(font-proxy plugin-id font)))
|
||||
(vals @fonts/fontsdb))))
|
||||
|
||||
:findAllByName
|
||||
(fn [name]
|
||||
(cond
|
||||
(not (string? name))
|
||||
(u/display-not-valid :findAllByName name)
|
||||
(u/not-valid plugin-id :findAllByName name)
|
||||
|
||||
:else
|
||||
(format/format-array
|
||||
(fn [font]
|
||||
(when (str/includes? (str/lower (:name font)) (str/lower name))
|
||||
(font-proxy font)))
|
||||
(font-proxy plugin-id font)))
|
||||
(vals @fonts/fontsdb))))))
|
||||
|
||||
@@ -598,3 +598,10 @@
|
||||
(case axis
|
||||
:y "horizontal"
|
||||
: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)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value})))))}
|
||||
@@ -64,10 +64,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
|
||||
@@ -80,10 +80,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
|
||||
@@ -96,10 +96,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
|
||||
@@ -112,10 +112,10 @@
|
||||
(let [value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
|
||||
@@ -127,10 +127,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
|
||||
@@ -142,10 +142,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
|
||||
@@ -157,10 +157,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
|
||||
@@ -172,10 +172,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
|
||||
@@ -187,10 +187,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
|
||||
@@ -202,10 +202,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
|
||||
@@ -217,10 +217,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
|
||||
@@ -232,10 +232,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
|
||||
@@ -245,14 +245,14 @@
|
||||
(let [type (keyword type)]
|
||||
(cond
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value})))))
|
||||
@@ -262,17 +262,17 @@
|
||||
(let [type (keyword type)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :addRowAtIndex-type type)
|
||||
(u/not-valid plugin-id :addRowAtIndex-type type)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index)))))
|
||||
@@ -282,14 +282,14 @@
|
||||
(let [type (keyword type)]
|
||||
(cond
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value})))))
|
||||
@@ -298,17 +298,17 @@
|
||||
(fn [index type value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :addColumnAtIndex-type type)
|
||||
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(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"))
|
||||
(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
|
||||
(let [type (keyword type)]
|
||||
@@ -318,10 +318,10 @@
|
||||
(fn [index]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
|
||||
@@ -330,10 +330,10 @@
|
||||
(fn [index]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
|
||||
@@ -343,17 +343,17 @@
|
||||
(let [type (keyword type)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setColumn-type type)
|
||||
(u/not-valid plugin-id :setColumn-type type)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/change-layout-track #{id} :column index (d/without-nils {:type type :value value}))))))
|
||||
@@ -363,17 +363,17 @@
|
||||
(let [type (keyword type)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setRow-type type)
|
||||
(u/not-valid plugin-id :setRow-type type)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/change-layout-track #{id} :row index (d/without-nils {:type type :value value}))))))
|
||||
@@ -382,7 +382,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dwsl/remove-layout #{id}))))
|
||||
@@ -391,16 +391,16 @@
|
||||
(fn [child row column]
|
||||
(cond
|
||||
(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)))
|
||||
(u/display-not-valid :appendChild-row row)
|
||||
(u/not-valid plugin-id :appendChild-row row)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(let [child-id (obj/get child "$id")]
|
||||
@@ -432,13 +432,13 @@
|
||||
shape (u/proxy->shape self)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? value))
|
||||
(u/display-not-valid :row-value value)
|
||||
(u/not-valid plugin-id :row-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row value})))))}
|
||||
@@ -452,13 +452,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? value))
|
||||
(u/display-not-valid :rowSpan-value value)
|
||||
(u/not-valid plugin-id :rowSpan-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row-span value})))))}
|
||||
@@ -472,13 +472,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? value))
|
||||
(u/display-not-valid :column-value value)
|
||||
(u/not-valid plugin-id :column-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column value})))))}
|
||||
@@ -492,13 +492,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? value))
|
||||
(u/display-not-valid :columnSpan-value value)
|
||||
(u/not-valid plugin-id :columnSpan-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column-span value})))))}
|
||||
@@ -512,13 +512,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :areaName-value value)
|
||||
(u/not-valid plugin-id :areaName-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:area-name value})))))}
|
||||
@@ -533,13 +533,13 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(not (contains? ctl/grid-position-types value))
|
||||
(u/display-not-valid :position-value value)
|
||||
(u/not-valid plugin-id :position-value value)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/change-cells-mode (:parent-id shape) #{(:id cell)} value)))))}
|
||||
@@ -554,13 +554,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(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)
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:align-self value})))))}
|
||||
@@ -575,13 +575,13 @@
|
||||
cell (locate-cell self)]
|
||||
(cond
|
||||
(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)
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:justify-self value})))))})))
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [id (js/Symbol)]
|
||||
@@ -35,10 +35,10 @@
|
||||
(fn [block-id]
|
||||
(cond
|
||||
(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)
|
||||
(u/display-not-valid :undoBlockFinish block-id)
|
||||
(u/not-valid plugin-id :undoBlockFinish block-id)
|
||||
|
||||
:else
|
||||
(st/emit! (dwu/commit-undo-transaction block-id))))))
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
(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
|
||||
(let [color (u/proxy->library-color self)
|
||||
@@ -77,10 +77,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :path value)
|
||||
(u/not-valid plugin-id :path value)
|
||||
|
||||
(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
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
@@ -94,10 +94,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
@@ -111,10 +111,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
@@ -129,10 +129,10 @@
|
||||
(let [value (parser/parse-gradient value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
@@ -147,10 +147,10 @@
|
||||
(let [value (parser/parse-image-data value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
@@ -161,7 +161,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dwl/delete-color {:id id}))))
|
||||
@@ -170,7 +170,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [color-id (uuid/next)
|
||||
@@ -207,7 +207,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getPluginData-key key)
|
||||
(u/not-valid plugin-id :getPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [color (u/locate-library-color file-id id)]
|
||||
@@ -217,16 +217,16 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :color id (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -240,10 +240,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [color (u/locate-library-color file-id id)]
|
||||
@@ -253,19 +253,19 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :color id (keyword "shared" namespace) key value))))
|
||||
@@ -274,7 +274,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [color (u/locate-library-color file-id id)]
|
||||
@@ -301,10 +301,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
(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
|
||||
(let [typo (u/proxy->library-typography self)
|
||||
@@ -318,10 +318,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :path value)
|
||||
(u/not-valid plugin-id :path value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -335,10 +335,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontId value)
|
||||
(u/not-valid plugin-id :fontId value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -352,10 +352,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontFamily value)
|
||||
(u/not-valid plugin-id :fontFamily value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -369,10 +369,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontVariantId value)
|
||||
(u/not-valid plugin-id :fontVariantId value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -386,10 +386,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontSize value)
|
||||
(u/not-valid plugin-id :fontSize value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -403,10 +403,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontWeight value)
|
||||
(u/not-valid plugin-id :fontWeight value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -420,10 +420,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontStyle value)
|
||||
(u/not-valid plugin-id :fontStyle value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -437,10 +437,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :lineHeight value)
|
||||
(u/not-valid plugin-id :lineHeight value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -454,10 +454,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :letterSpacing value)
|
||||
(u/not-valid plugin-id :letterSpacing value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -471,10 +471,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :textTransform value)
|
||||
(u/not-valid plugin-id :textTransform value)
|
||||
|
||||
(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
|
||||
(let [typo (-> (u/proxy->library-typography self)
|
||||
@@ -485,7 +485,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dwl/delete-typography {:id id}))))
|
||||
@@ -494,7 +494,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [typo-id (uuid/next)
|
||||
@@ -507,10 +507,10 @@
|
||||
(fn [shape]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [shape-id (obj/get shape "$id")
|
||||
@@ -521,10 +521,10 @@
|
||||
(fn [range]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [shape-id (obj/get range "$id")
|
||||
@@ -542,7 +542,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :typography-plugin-data-key key)
|
||||
(u/not-valid plugin-id :typography-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [typography (u/locate-library-typography file-id id)]
|
||||
@@ -552,16 +552,16 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -575,10 +575,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [typography (u/locate-library-typography file-id id)]
|
||||
@@ -588,19 +588,19 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "shared" namespace) key value))))
|
||||
@@ -609,7 +609,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [typography (u/locate-library-typography file-id id)]
|
||||
@@ -674,7 +674,7 @@
|
||||
:removeProperty
|
||||
(fn [pos]
|
||||
(if (not (nat-int? pos))
|
||||
(u/display-not-valid :pos pos)
|
||||
(u/not-valid plugin-id :pos pos)
|
||||
(st/emit!
|
||||
(ev/event {::ev/name "remove-property" ::ev/origin "plugin:remove-property"})
|
||||
(dwv/remove-property id pos))))
|
||||
@@ -683,10 +683,10 @@
|
||||
(fn [pos name]
|
||||
(cond
|
||||
(not (nat-int? pos))
|
||||
(u/display-not-valid :pos pos)
|
||||
(u/not-valid plugin-id :pos pos)
|
||||
|
||||
(not (string? name))
|
||||
(u/display-not-valid :name name)
|
||||
(u/not-valid plugin-id :name name)
|
||||
|
||||
:else
|
||||
(st/emit!
|
||||
@@ -715,10 +715,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
(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
|
||||
(let [component (u/proxy->library-component self)
|
||||
@@ -732,10 +732,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :path value)
|
||||
(u/not-valid plugin-id :path value)
|
||||
|
||||
(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
|
||||
(let [component (u/proxy->library-component self)
|
||||
@@ -746,7 +746,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dwl/delete-component {:id id}))))
|
||||
@@ -755,7 +755,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [id-ref (atom nil)]
|
||||
@@ -766,7 +766,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :component-plugin-data-key key)
|
||||
(u/not-valid plugin-id :component-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [component (u/locate-library-component file-id id)]
|
||||
@@ -776,16 +776,16 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :component id (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -799,10 +799,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :component-plugin-data-key key)
|
||||
(u/not-valid plugin-id :component-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [component (u/locate-library-component file-id id)]
|
||||
@@ -812,19 +812,19 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :component id (keyword "shared" namespace) key value))))
|
||||
@@ -833,7 +833,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :component-plugin-data-namespace namespace)
|
||||
(u/not-valid plugin-id :component-plugin-data-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [component (u/locate-library-component file-id id)]
|
||||
@@ -901,10 +901,10 @@
|
||||
(fn [pos value]
|
||||
(cond
|
||||
(not (nat-int? pos))
|
||||
(u/display-not-valid :pos (str pos))
|
||||
(u/not-valid plugin-id :pos (str pos))
|
||||
|
||||
(not (string? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
:else
|
||||
(st/emit!
|
||||
@@ -970,7 +970,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [color-id (uuid/next)]
|
||||
@@ -981,7 +981,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(let [typography-id (uuid/next)]
|
||||
@@ -992,7 +992,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [id-ref (atom nil)
|
||||
@@ -1005,7 +1005,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :file-plugin-data-key key)
|
||||
(u/not-valid plugin-id :file-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file file-id)]
|
||||
@@ -1015,13 +1015,13 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :file (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -1035,10 +1035,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :file-plugin-data-key key)
|
||||
(u/not-valid plugin-id :file-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file file-id)]
|
||||
@@ -1048,16 +1048,16 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :file (keyword "shared" namespace) key value))))
|
||||
@@ -1066,7 +1066,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :namespace namespace)
|
||||
(u/not-valid plugin-id :namespace namespace)
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file file-id)]
|
||||
@@ -1110,14 +1110,14 @@
|
||||
(fn [library-id]
|
||||
(cond
|
||||
(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
|
||||
(js/Promise.
|
||||
(fn [resolve reject]
|
||||
(cond
|
||||
(not (string? library-id))
|
||||
(do (u/display-not-valid :connectLibrary library-id)
|
||||
(do (u/not-valid plugin-id :connectLibrary library-id)
|
||||
(reject nil))
|
||||
|
||||
:else
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :getItem "The key must be a string")
|
||||
(u/not-valid plugin-id :getItem "The key must be a string")
|
||||
|
||||
:else
|
||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||
@@ -42,10 +42,10 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :setItem "The key must be a string")
|
||||
(u/not-valid plugin-id :setItem "The key must be a string")
|
||||
|
||||
:else
|
||||
(.setItem ^js local-storage (prefix-key plugin-id key) value)))
|
||||
@@ -54,10 +54,10 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :removeItem "The key must be a string")
|
||||
(u/not-valid plugin-id :removeItem "The key must be a string")
|
||||
|
||||
:else
|
||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(or (not (string? value)) (empty? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}
|
||||
@@ -74,7 +74,7 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(not (shape/shape-proxy? value))
|
||||
(u/display-not-valid :startingBoard value)
|
||||
(u/not-valid plugin-id :startingBoard value)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
|
||||
@@ -103,10 +103,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
(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
|
||||
(st/emit! (dw/rename-page id value))))}
|
||||
@@ -127,10 +127,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dw/change-canvas-color id {:color value}))))}
|
||||
@@ -158,7 +158,7 @@
|
||||
(fn [shape-id]
|
||||
(cond
|
||||
(not (string? shape-id))
|
||||
(u/display-not-valid :getShapeById shape-id)
|
||||
(u/not-valid plugin-id :getShapeById shape-id)
|
||||
|
||||
:else
|
||||
(let [shape-id (uuid/parse shape-id)
|
||||
@@ -195,7 +195,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :page-plugin-data-key key)
|
||||
(u/not-valid plugin-id :page-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [page (u/locate-page file-id id)]
|
||||
@@ -205,13 +205,13 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value))))
|
||||
@@ -225,10 +225,10 @@
|
||||
(fn [namespace key]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :page-plugin-data-key key)
|
||||
(u/not-valid plugin-id :page-plugin-data-key key)
|
||||
|
||||
:else
|
||||
(let [page (u/locate-page file-id id)]
|
||||
@@ -238,16 +238,16 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
|
||||
@@ -256,7 +256,7 @@
|
||||
(fn [self namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :page-plugin-data-namespace namespace)
|
||||
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [page (u/proxy->page self)]
|
||||
@@ -266,7 +266,7 @@
|
||||
(fn [new-window]
|
||||
(cond
|
||||
(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
|
||||
(let [new-window (if (boolean? new-window) new-window false)]
|
||||
@@ -276,10 +276,10 @@
|
||||
(fn [name frame]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :createFlow-frame frame)
|
||||
(u/not-valid plugin-id :createFlow-frame frame)
|
||||
|
||||
:else
|
||||
(let [flow-id (uuid/next)]
|
||||
@@ -290,7 +290,7 @@
|
||||
(fn [flow]
|
||||
(cond
|
||||
(not (flow-proxy? flow))
|
||||
(u/display-not-valid :removeFlow-flow flow)
|
||||
(u/not-valid plugin-id :removeFlow-flow flow)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/remove-flow id (obj/get flow "$id")))))
|
||||
@@ -300,18 +300,18 @@
|
||||
(let [shape (u/proxy->shape board)]
|
||||
(cond
|
||||
(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))
|
||||
(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)
|
||||
(or (not (shape/shape-proxy? board))
|
||||
(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"))
|
||||
(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
|
||||
(let [ruler-id (uuid/next)]
|
||||
@@ -328,10 +328,10 @@
|
||||
(fn [value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [guide (u/proxy->ruler-guide value)]
|
||||
@@ -343,17 +343,17 @@
|
||||
position (parser/parse-point position)]
|
||||
(cond
|
||||
(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)))
|
||||
(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))))
|
||||
(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"))
|
||||
(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
|
||||
(let [position
|
||||
@@ -378,10 +378,10 @@
|
||||
(fn [thread]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(js/Promise.
|
||||
@@ -400,7 +400,7 @@
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "comment:read"))
|
||||
(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"))
|
||||
|
||||
:else
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
[app.plugins.utils :as u]))
|
||||
|
||||
(defn ^:export centerShapes
|
||||
[shapes]
|
||||
[plugin-id shapes]
|
||||
(cond
|
||||
(not (every? shape/shape-proxy? shapes))
|
||||
(u/display-not-valid :centerShapes shapes)
|
||||
(u/not-valid plugin-id :centerShapes shapes)
|
||||
|
||||
:else
|
||||
(let [shapes (->> shapes (map u/proxy->shape))]
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
[app.util.object :as obj]
|
||||
[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
|
||||
(defonce ^:private registry (atom {}))
|
||||
|
||||
@@ -78,6 +82,7 @@
|
||||
(d/without-nils
|
||||
{:plugin-id plugin-id
|
||||
:url (str plugin-url)
|
||||
:version vers
|
||||
:name name
|
||||
:description desc
|
||||
:host origin
|
||||
@@ -127,5 +132,6 @@
|
||||
(defn check-permission
|
||||
[plugin-id permission]
|
||||
(or (= plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
(= plugin-id mcp-plugin-id)
|
||||
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
|
||||
(contains? permissions permission))))
|
||||
|
||||
@@ -44,13 +44,13 @@
|
||||
(let [shape (u/locate-shape file-id page-id (obj/get value "$id"))]
|
||||
(cond
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(let [board-id (when value (obj/get value "$id"))
|
||||
@@ -78,10 +78,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [guide (u/proxy->ruler-guide self)
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
@@ -47,7 +47,6 @@
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.flags :refer [natural-child-ordering?]]
|
||||
[app.plugins.flex :as flex]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.grid :as grid]
|
||||
@@ -55,6 +54,7 @@
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.ruler-guides :as rg]
|
||||
[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.util.http :as http]
|
||||
[app.util.object :as obj]
|
||||
@@ -91,7 +91,7 @@
|
||||
(let [value (parser/parse-keyword value)]
|
||||
(cond
|
||||
(not (contains? ctsi/event-types value))
|
||||
(u/display-not-valid :trigger value)
|
||||
(u/not-valid plugin-id :trigger value)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
@@ -107,7 +107,7 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(or (not (number? value)) (not (pos? value)))
|
||||
(u/display-not-valid :delay value)
|
||||
(u/not-valid plugin-id :delay value)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
@@ -127,7 +127,7 @@
|
||||
(d/patch-object params))]
|
||||
(cond
|
||||
(not (sm/validate ctsi/schema:interaction interaction))
|
||||
(u/display-not-valid :action interaction)
|
||||
(u/not-valid plugin-id :action interaction)
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
@@ -192,7 +192,8 @@
|
||||
(assert (uuid? 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)}
|
||||
:$id {:enumerable false :get (fn [] id)}
|
||||
:$file {:enumerable false :get (fn [] file-id)}
|
||||
@@ -218,10 +219,10 @@
|
||||
(not (str/blank? value)))]
|
||||
(cond
|
||||
(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?)
|
||||
(u/display-not-valid :name value)
|
||||
(u/not-valid plugin-id :name value)
|
||||
|
||||
:else
|
||||
(st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))}
|
||||
@@ -233,10 +234,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :blocked value)
|
||||
(u/not-valid plugin-id :blocked value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -249,10 +250,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :hidden value)
|
||||
(u/not-valid plugin-id :hidden value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -265,10 +266,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :visible value)
|
||||
(u/not-valid plugin-id :visible value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -281,10 +282,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :proportionLock value)
|
||||
(u/not-valid plugin-id :proportionLock value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -299,10 +300,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
|
||||
@@ -316,10 +317,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
|
||||
@@ -332,10 +333,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))}
|
||||
@@ -348,10 +349,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))}
|
||||
@@ -364,10 +365,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))}
|
||||
@@ -380,10 +381,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))}
|
||||
@@ -396,10 +397,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))}
|
||||
@@ -412,10 +413,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
|
||||
@@ -429,10 +430,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
|
||||
@@ -446,10 +447,10 @@
|
||||
value (mapv #(shadow-defaults (parser/parse-shadow %)) value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))}
|
||||
@@ -465,10 +466,10 @@
|
||||
value (blur-defaults (parser/parse-blur value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
||||
@@ -482,10 +483,10 @@
|
||||
value (parser/parse-exports value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))}
|
||||
@@ -499,10 +500,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dw/update-position id
|
||||
@@ -517,10 +518,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dw/update-position id
|
||||
@@ -562,10 +563,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get self "$id")
|
||||
@@ -589,10 +590,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get self "$id")
|
||||
@@ -616,10 +617,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get self "$id")
|
||||
@@ -643,10 +644,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [id (obj/get self "$id")
|
||||
@@ -680,10 +681,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (number? value))
|
||||
(u/display-not-valid :rotation value)
|
||||
(u/not-valid plugin-id :rotation value)
|
||||
|
||||
(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
|
||||
(let [shape (u/proxy->shape self)]
|
||||
@@ -696,10 +697,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :flipX value)
|
||||
(u/not-valid plugin-id :flipX value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -712,10 +713,10 @@
|
||||
(fn [self value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :flipY value)
|
||||
(u/not-valid plugin-id :flipY value)
|
||||
|
||||
(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
|
||||
(let [id (obj/get self "$id")]
|
||||
@@ -734,13 +735,13 @@
|
||||
value (parser/parse-fills value)]
|
||||
(cond
|
||||
(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)
|
||||
(st/emit! (dwt/update-attrs id {:fills value}))
|
||||
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))}
|
||||
@@ -754,10 +755,10 @@
|
||||
value (parser/parse-strokes value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))}
|
||||
@@ -802,13 +803,13 @@
|
||||
(fn [width height]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :resize height)
|
||||
(u/not-valid plugin-id :resize height)
|
||||
|
||||
(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
|
||||
(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")})]
|
||||
(cond
|
||||
(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)))))
|
||||
(u/display-not-valid :rotate-center center)
|
||||
(u/not-valid plugin-id :rotate-center center)
|
||||
|
||||
(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
|
||||
(st/emit! (dw/increase-rotation [id] angle {:center center :delta? true})))))
|
||||
@@ -835,7 +836,7 @@
|
||||
(let [ret-v (atom nil)]
|
||||
(cond
|
||||
(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
|
||||
(do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v))
|
||||
@@ -845,7 +846,7 @@
|
||||
(fn []
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dwsh/delete-shapes #{id}))))
|
||||
@@ -855,7 +856,7 @@
|
||||
(fn [key]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getPluginData key)
|
||||
(u/not-valid plugin-id :getPluginData key)
|
||||
|
||||
:else
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
@@ -865,13 +866,13 @@
|
||||
(fn [key value]
|
||||
(cond
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setPluginData-key key)
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(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]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :getSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :getSharedPluginData-key key)
|
||||
|
||||
:else
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
@@ -898,16 +899,16 @@
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :setSharedPluginData-namespace namespace)
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
(not (string? key))
|
||||
(u/display-not-valid :setSharedPluginData-key key)
|
||||
(u/not-valid plugin-id :setSharedPluginData-key key)
|
||||
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value))))
|
||||
@@ -916,7 +917,7 @@
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/display-not-valid :getSharedPluginDataKeys namespace)
|
||||
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
|
||||
|
||||
:else
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
@@ -931,12 +932,12 @@
|
||||
(not (cfh/group-shape? shape))
|
||||
(not (cfh/svg-raw-shape? shape))
|
||||
(not (cfh/bool-shape? shape)))
|
||||
(u/display-not-valid :getChildren (:type shape))
|
||||
(u/not-valid plugin-id :getChildren (:type shape))
|
||||
|
||||
:else
|
||||
(let [is-reversed? (ctl/flex-layout? shape)
|
||||
reverse-fn
|
||||
(if (and (natural-child-ordering? plugin-id) is-reversed?)
|
||||
(if (and (u/natural-child-ordering? plugin-id) is-reversed?)
|
||||
reverse identity)]
|
||||
(->> (u/locate-shape file-id page-id id)
|
||||
(:shapes)
|
||||
@@ -951,19 +952,19 @@
|
||||
(not (cfh/group-shape? shape))
|
||||
(not (cfh/svg-raw-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))
|
||||
(u/display-not-valid :appendChild-child child)
|
||||
(u/not-valid plugin-id :appendChild-child child)
|
||||
|
||||
(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
|
||||
(let [child-id (obj/get child "$id")
|
||||
is-reversed? (ctl/flex-layout? shape)
|
||||
index
|
||||
(if (or (not (natural-child-ordering? plugin-id)) is-reversed?)
|
||||
(if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
|
||||
0
|
||||
(count (:shapes shape)))]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||
@@ -976,19 +977,19 @@
|
||||
(not (cfh/group-shape? shape))
|
||||
(not (cfh/svg-raw-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))
|
||||
(u/display-not-valid :insertChild-child child)
|
||||
(u/not-valid plugin-id :insertChild-child child)
|
||||
|
||||
(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
|
||||
(let [child-id (obj/get child "$id")
|
||||
is-reversed? (ctl/flex-layout? shape)
|
||||
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)
|
||||
index)]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||
@@ -999,10 +1000,10 @@
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(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)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(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)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwg/mask-group #{id})))))
|
||||
@@ -1041,10 +1042,10 @@
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwg/unmask-group #{id})))))
|
||||
@@ -1055,7 +1056,7 @@
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(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
|
||||
(.toString (:content shape)))))
|
||||
@@ -1066,13 +1067,13 @@
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :getRange-start start)
|
||||
(u/not-valid plugin-id :getRange-start start)
|
||||
|
||||
(not (sm/valid-safe-int? end))
|
||||
(u/display-not-valid :getRange-end end)
|
||||
(u/not-valid plugin-id :getRange-end end)
|
||||
|
||||
:else
|
||||
(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)]
|
||||
(cond
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(let [typography (u/proxy->library-typography typography)]
|
||||
@@ -1099,10 +1100,10 @@
|
||||
(fn [index]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dw/set-shape-index file-id page-id id index))))
|
||||
@@ -1197,7 +1198,7 @@
|
||||
(let [value (parser/parse-export value)]
|
||||
(cond
|
||||
(not (sm/validate ctse/schema:export value))
|
||||
(u/display-not-valid :export value)
|
||||
(u/not-valid plugin-id :export value)
|
||||
|
||||
:else
|
||||
(let [shape (u/locate-shape file-id page-id id)
|
||||
@@ -1233,7 +1234,7 @@
|
||||
(d/patch-object (parser/parse-interaction trigger action delay)))]
|
||||
(cond
|
||||
(not (sm/validate ctsi/schema:interaction interaction))
|
||||
(u/display-not-valid :addInteraction interaction)
|
||||
(u/not-valid plugin-id :addInteraction interaction)
|
||||
|
||||
:else
|
||||
(let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)]
|
||||
@@ -1244,7 +1245,7 @@
|
||||
(fn [interaction]
|
||||
(cond
|
||||
(not (interaction-proxy? interaction))
|
||||
(u/display-not-valid :removeInteraction interaction)
|
||||
(u/not-valid plugin-id :removeInteraction interaction)
|
||||
|
||||
:else
|
||||
(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)]
|
||||
(cond
|
||||
(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))
|
||||
(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))
|
||||
(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"))
|
||||
(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
|
||||
(let [id (uuid/next)
|
||||
@@ -1285,10 +1286,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(let [guide (u/proxy->ruler-guide value)]
|
||||
@@ -1298,25 +1299,26 @@
|
||||
{:this true
|
||||
:get
|
||||
(fn [_]
|
||||
(let [tokens
|
||||
(let [applied-tokens
|
||||
(-> (u/locate-shape file-id page-id id)
|
||||
(get :applied-tokens))]
|
||||
(get :applied-tokens)
|
||||
(applied-tokens-plugin->applied-tokens))]
|
||||
(reduce
|
||||
(fn [acc [prop name]]
|
||||
(obj/set! acc (json/write-camel-key prop) name))
|
||||
#js {}
|
||||
tokens)))}
|
||||
applied-tokens)))}
|
||||
|
||||
:applyToken
|
||||
{:enumerable false
|
||||
:schema [:tuple
|
||||
[:fn token-proxy?]
|
||||
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
|
||||
[:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
|
||||
:fn (fn [token attrs]
|
||||
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
|
||||
kw-attrs (into #{} (map keyword attrs))]
|
||||
(if (some #(not (cto/token-attr? %)) kw-attrs)
|
||||
(u/display-not-valid :applyToken attrs)
|
||||
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
|
||||
(if (some #(not (token-attr? %)) kw-attrs)
|
||||
(u/not-valid plugin-id :applyToken attrs)
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token token
|
||||
:attrs kw-attrs
|
||||
@@ -1338,10 +1340,10 @@
|
||||
(fn [pos value]
|
||||
(cond
|
||||
(not (nat-int? pos))
|
||||
(u/display-not-valid :pos pos)
|
||||
(u/not-valid plugin-id :pos pos)
|
||||
|
||||
(not (string? value))
|
||||
(u/display-not-valid :value value)
|
||||
(u/not-valid plugin-id :value value)
|
||||
|
||||
:else
|
||||
(let [shape (u/locate-shape file-id page-id id)
|
||||
@@ -1351,16 +1353,30 @@
|
||||
|
||||
:combineAsVariants
|
||||
(fn [ids]
|
||||
(if (or (not (seq ids)) (not (every? uuid/parse* ids)))
|
||||
(u/display-not-valid :ids ids)
|
||||
(let [shape (u/locate-shape file-id page-id id)
|
||||
component (u/locate-library-component file-id (:component-id shape))
|
||||
ids (->> ids
|
||||
(cond
|
||||
(or (not (seq ids)) (not (every? uuid/parse* ids)))
|
||||
(u/not-valid plugin-id :ids ids)
|
||||
|
||||
:else
|
||||
(let [ids (->> ids
|
||||
(map uuid/uuid)
|
||||
(into #{id}))]
|
||||
(when (and component (not (ctk/is-variant? component)))
|
||||
(st/emit!
|
||||
(dwv/combine-as-variants ids {:trigger "plugin:combine-as-variants"})))))))
|
||||
(into #{id}))
|
||||
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-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))
|
||||
(crc/add-properties!
|
||||
@@ -1375,21 +1391,21 @@
|
||||
(fn [^js self children]
|
||||
(cond
|
||||
(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))
|
||||
(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
|
||||
(let [shape (u/proxy->shape self)
|
||||
file-id (obj/get self "$file")
|
||||
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")))]
|
||||
|
||||
(cond
|
||||
(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
|
||||
(st/emit! (dw/reorder-children file-id page-id (:id shape) ids))))))}))
|
||||
@@ -1405,10 +1421,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :clipContent value)
|
||||
(u/not-valid plugin-id :clipContent value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
|
||||
@@ -1421,10 +1437,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :showInViewMode value)
|
||||
(u/not-valid plugin-id :showInViewMode value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
|
||||
@@ -1456,10 +1472,10 @@
|
||||
value (parser/parse-frame-guides value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))}
|
||||
@@ -1481,10 +1497,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
||||
@@ -1497,10 +1513,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}
|
||||
@@ -1524,10 +1540,10 @@
|
||||
(let [segments (parser/parse-commands value)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :content segments)
|
||||
(u/not-valid plugin-id :content segments)
|
||||
|
||||
:else
|
||||
(let [selrect (path/calc-selrect segments)
|
||||
@@ -1550,13 +1566,13 @@
|
||||
value)]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :content-type type)
|
||||
(u/not-valid plugin-id :content-type type)
|
||||
|
||||
(not (sm/validate path/schema:segments segments))
|
||||
(u/display-not-valid :content segments)
|
||||
(u/not-valid plugin-id :content segments)
|
||||
|
||||
:else
|
||||
(let [selrect (path/calc-selrect segments)
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes.text :as gst]
|
||||
[app.common.record :as crc]
|
||||
[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.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
@@ -118,10 +119,10 @@
|
||||
variant (fonts/get-default-variant font)]
|
||||
(cond
|
||||
(not font)
|
||||
(u/display-not-valid :fontId value)
|
||||
(u/not-valid plugin-id :fontId value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
||||
@@ -140,10 +141,10 @@
|
||||
variant (fonts/get-default-variant font)]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontFamily value)
|
||||
(u/not-valid plugin-id :fontFamily value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
|
||||
@@ -161,10 +162,10 @@
|
||||
variant (fonts/get-variant font value)]
|
||||
(cond
|
||||
(not (string? value))
|
||||
(u/display-not-valid :fontVariantId value)
|
||||
(u/not-valid plugin-id :fontVariantId value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||
@@ -181,10 +182,10 @@
|
||||
(let [value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:font-size value})))))}
|
||||
@@ -208,10 +209,10 @@
|
||||
(fonts/find-variant font {:weight weight}))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||
@@ -234,10 +235,10 @@
|
||||
(fonts/find-variant font {:style style}))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
|
||||
@@ -254,10 +255,10 @@
|
||||
(let [value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:line-height value})))))}
|
||||
@@ -274,10 +275,10 @@
|
||||
(let [value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:letter-spacing value})))))}
|
||||
@@ -293,10 +294,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:text-transform value}))))}
|
||||
@@ -312,10 +313,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:text-decoration value}))))}
|
||||
@@ -331,10 +332,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:direction value}))))}
|
||||
@@ -350,10 +351,10 @@
|
||||
(fn [_ value]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:text-align value}))))}
|
||||
@@ -369,11 +370,11 @@
|
||||
(fn [_ value]
|
||||
(let [value (parser/parse-fills value)]
|
||||
(cond
|
||||
(not (sm/validate [:vector ::cts/fill] value))
|
||||
(u/display-not-valid :fills value)
|
||||
(not (sm/validate [:vector types.fills/schema:fill] value))
|
||||
(u/not-valid plugin-id :fills value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-text-range id start end {:fills value})))))}
|
||||
@@ -400,10 +401,10 @@
|
||||
;; editor as well
|
||||
(cond
|
||||
(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"))
|
||||
(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)
|
||||
(let [shape (u/proxy->shape self)
|
||||
@@ -427,10 +428,10 @@
|
||||
value (keyword value)]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))}
|
||||
@@ -444,10 +445,10 @@
|
||||
variant (fonts/get-default-variant font)]
|
||||
(cond
|
||||
(not font)
|
||||
(u/display-not-valid :fontId value)
|
||||
(u/not-valid plugin-id :fontId value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
||||
@@ -461,10 +462,10 @@
|
||||
variant (fonts/get-default-variant font)]
|
||||
(cond
|
||||
(not font)
|
||||
(u/display-not-valid :fontFamily value)
|
||||
(u/not-valid plugin-id :fontFamily value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
|
||||
@@ -478,10 +479,10 @@
|
||||
variant (fonts/get-variant font value)]
|
||||
(cond
|
||||
(not variant)
|
||||
(u/display-not-valid :fontVariantId value)
|
||||
(u/not-valid plugin-id :fontVariantId value)
|
||||
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||
@@ -494,10 +495,10 @@
|
||||
value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:font-size value})))))}
|
||||
@@ -516,10 +517,10 @@
|
||||
(fonts/find-variant font {:weight weight}))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||
@@ -538,10 +539,10 @@
|
||||
(fonts/find-variant font {:style style}))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
|
||||
@@ -554,10 +555,10 @@
|
||||
value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:line-height value})))))}
|
||||
@@ -570,10 +571,10 @@
|
||||
value (str/trim (dm/str value))]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:letter-spacing value})))))}
|
||||
@@ -585,10 +586,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:text-transform value})))))}
|
||||
@@ -600,10 +601,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:text-decoration value})))))}
|
||||
@@ -615,10 +616,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:text-direction value})))))}
|
||||
@@ -630,10 +631,10 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(st/emit! (dwt/update-attrs id {:text-align value})))))}
|
||||
@@ -645,10 +646,13 @@
|
||||
(let [id (obj/get self "$id")]
|
||||
(cond
|
||||
(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"))
|
||||
(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
|
||||
(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.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
[clojure.datafy :refer [datafy]]))
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[clojure.set :refer [map-invert]]))
|
||||
|
||||
;; === Token
|
||||
|
||||
;; Give more semantic names to the shape attributes that tokens can be applied to
|
||||
(def ^:private map:token-attr->token-attr-plugin
|
||||
{:r1 :border-radius-top-left
|
||||
:r2 :border-radius-top-right
|
||||
:r3 :border-radius-bottom-right
|
||||
:r4 :border-radius-bottom-left
|
||||
|
||||
:p1 :padding-top-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
|
||||
[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)]
|
||||
(if (some #(not (cto/token-attr? %)) attrs)
|
||||
(u/display-not-valid :applyToSelected attrs)
|
||||
(if (some #(not (token-attr? %)) attrs)
|
||||
(u/not-valid plugin-id :applyToSelected attrs)
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token token
|
||||
:attrs attrs
|
||||
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
|
||||
:shape-ids shape-ids
|
||||
:expand-with-children false})))))
|
||||
|
||||
@@ -52,7 +92,7 @@
|
||||
(defn token-proxy
|
||||
[plugin-id file-id set-id id]
|
||||
(obj/reify {:name "TokenProxy"
|
||||
:on-error u/handle-error}
|
||||
:on-error (u/handle-error plugin-id)}
|
||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||
:$file-id {:enumerable false :get (constantly file-id)}
|
||||
:$set-id {:enumerable false :get (constantly set-id)}
|
||||
@@ -146,16 +186,16 @@
|
||||
{:enumerable false
|
||||
:schema [:tuple
|
||||
[: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]
|
||||
(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
|
||||
{: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]
|
||||
(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
|
||||
|
||||
@@ -165,7 +205,7 @@
|
||||
(defn token-set-proxy
|
||||
[plugin-id file-id id]
|
||||
(obj/reify {:name "TokenSetProxy"
|
||||
:on-error u/handle-error}
|
||||
:on-error (u/handle-error plugin-id)}
|
||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||
:$file-id {:enumerable false :get (constantly file-id)}
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
@@ -247,15 +287,19 @@
|
||||
:addToken
|
||||
{:enumerable false
|
||||
:schema (fn [args]
|
||||
[:tuple (-> (cfo/make-token-schema
|
||||
(-> (u/locate-tokens-lib file-id) (ctob/get-tokens id))
|
||||
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
|
||||
;; Don't allow plugins to set the id
|
||||
(sm/dissoc-key :id)
|
||||
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
|
||||
;; and set a converter that changes DTCG types to internal types (:decode/json).
|
||||
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
|
||||
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))])
|
||||
(let [tokens-tree (-> (u/locate-tokens-lib file-id)
|
||||
(ctob/get-tokens id)
|
||||
;; Convert to the adecuate format for schema
|
||||
(ctob/tokens-tree))]
|
||||
[:tuple (-> (cfo/make-token-schema
|
||||
tokens-tree
|
||||
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
|
||||
;; Don't allow plugins to set the id
|
||||
(sm/dissoc-key :id)
|
||||
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
|
||||
;; and set a converter that changes DTCG types to internal types (:decode/json).
|
||||
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
|
||||
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
|
||||
:decode/options {:key-fn identity}
|
||||
:fn (fn [attrs]
|
||||
(let [tokens-lib (u/locate-tokens-lib file-id)
|
||||
@@ -270,7 +314,7 @@
|
||||
(if resolved-value
|
||||
(do (st/emit! (dwtl/create-token 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))))}
|
||||
|
||||
:duplicate
|
||||
@@ -287,7 +331,7 @@
|
||||
(defn token-theme-proxy
|
||||
[plugin-id file-id id]
|
||||
(obj/reify {:name "TokenThemeProxy"
|
||||
:on-error u/handle-error}
|
||||
:on-error (u/handle-error plugin-id)}
|
||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||
:$file-id {:enumerable false :get (constantly file-id)}
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
@@ -394,7 +438,7 @@
|
||||
(defn tokens-catalog
|
||||
[plugin-id file-id]
|
||||
(obj/reify {:name "TokensCatalog"
|
||||
:on-error u/handle-error}
|
||||
:on-error (u/handle-error plugin-id)}
|
||||
:$plugin {:enumerable false :get (constantly plugin-id)}
|
||||
:$id {:enumerable false :get (constantly file-id)}
|
||||
|
||||
|
||||
@@ -9,14 +9,17 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[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.messages :as csm]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.store :as st]
|
||||
[app.util.object :as obj]))
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn locate-file
|
||||
[id]
|
||||
@@ -221,6 +224,16 @@
|
||||
(resolve value)))))]
|
||||
[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
|
||||
[code value]
|
||||
(if (some? value)
|
||||
@@ -228,34 +241,54 @@
|
||||
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
|
||||
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
|
||||
[reject code value]
|
||||
(let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)]
|
||||
(.error js/console 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
|
||||
[values]
|
||||
(let [s (set values)]
|
||||
(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
|
||||
"Function to be used in plugin proxies methods to handle errors and print a readable
|
||||
message to the console."
|
||||
[cause]
|
||||
(display-not-valid (ex-message cause) nil)
|
||||
(if-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(println (sm/humanize-explain explain))
|
||||
(js/console.log (ex-data cause)))
|
||||
(js/console.log (.-stack cause)))
|
||||
[plugin-id]
|
||||
(fn [cause]
|
||||
(let [message
|
||||
(if-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(do
|
||||
(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")]
|
||||
(cond
|
||||
(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))
|
||||
(u/display-not-valid :center-y new-y)
|
||||
(u/not-valid plugin-id :center-y new-y)
|
||||
|
||||
:else
|
||||
(let [vb (dm/get-in @st/state [:workspace-local :vbox])
|
||||
@@ -63,7 +63,7 @@
|
||||
(fn [value]
|
||||
(cond
|
||||
(not (sm/valid-safe-number? value))
|
||||
(u/display-not-valid :zoom value)
|
||||
(u/not-valid plugin-id :zoom value)
|
||||
|
||||
:else
|
||||
(let [z (dm/get-in @st/state [:workspace-local :zoom])]
|
||||
@@ -87,7 +87,7 @@
|
||||
(fn [shapes]
|
||||
(cond
|
||||
(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
|
||||
(let [ids (->> shapes
|
||||
|
||||
@@ -10,84 +10,10 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.common.schema.messages :as csm]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[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
|
||||
[]
|
||||
(let [state (mf/useState 0)
|
||||
@@ -97,24 +23,6 @@
|
||||
(fn []
|
||||
(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
|
||||
[f {:keys [schema validators]}]
|
||||
(fn [& args]
|
||||
@@ -124,7 +32,7 @@
|
||||
|
||||
errors
|
||||
(when-not valid?
|
||||
(collect-schema-errors schema validators state))
|
||||
(csm/collect-schema-errors schema validators state))
|
||||
|
||||
extra-errors
|
||||
(not-empty (:extra-errors state))]
|
||||
|
||||
@@ -462,3 +462,9 @@
|
||||
(defn print-last-exception
|
||||
[]
|
||||
(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"
|
||||
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
|
||||
msgid "dashboard.add-file"
|
||||
msgstr "Add file"
|
||||
@@ -2138,6 +2067,237 @@ msgstr "Resolved value:"
|
||||
msgid "inspect.tabs.styles.variants-panel"
|
||||
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
|
||||
msgid "label.mark-all-as-read"
|
||||
msgstr "Mark all as read"
|
||||
@@ -2354,6 +2514,9 @@ msgstr "Director"
|
||||
msgid "labels.discard"
|
||||
msgstr "Discard"
|
||||
|
||||
msgid "labels.dismiss"
|
||||
msgstr "Dismiss"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||
msgid "labels.download"
|
||||
msgstr "Download %s"
|
||||
@@ -2489,6 +2652,10 @@ msgstr "Info"
|
||||
msgid "labels.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
|
||||
msgid "labels.internal-error.desc-message-first"
|
||||
msgstr "Something bad happened."
|
||||
@@ -3148,30 +3315,6 @@ msgstr "Change email"
|
||||
msgid "modals.change-email.title"
|
||||
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
|
||||
msgid "modals.create-webhook.submit-label"
|
||||
msgstr "Create webhook"
|
||||
@@ -3188,18 +3331,6 @@ msgstr "Payload URL"
|
||||
msgid "modals.create-webhook.url.placeholder"
|
||||
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
|
||||
msgid "modals.delete-account.cancel"
|
||||
msgstr "Cancel and keep my account"
|
||||
@@ -3699,6 +3830,12 @@ msgstr "Invitation sent successfully"
|
||||
msgid "notifications.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
|
||||
msgid "notifications.profile-deletion-not-allowed"
|
||||
msgstr "You can't delete your profile. Reassign your teams before proceed."
|
||||
@@ -5093,14 +5230,14 @@ msgstr "Shared Libraries - %s - Penpot"
|
||||
msgid "title.default"
|
||||
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
|
||||
msgid "title.settings.feedback"
|
||||
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
|
||||
msgid "title.settings.notifications"
|
||||
msgstr "Notifications - Penpot"
|
||||
@@ -5594,6 +5731,18 @@ msgstr "Hide rulers"
|
||||
msgid "workspace.header.menu.hide-textpalette"
|
||||
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
|
||||
msgid "workspace.header.menu.option.edit"
|
||||
msgstr "Edit"
|
||||
@@ -5606,6 +5755,9 @@ msgstr "File"
|
||||
msgid "workspace.header.menu.option.help-info"
|
||||
msgstr "Help & info"
|
||||
|
||||
msgid "workspace.header.menu.option.mcp"
|
||||
msgstr "MCP server"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:916
|
||||
#, unused
|
||||
msgid "workspace.header.menu.option.power-up"
|
||||
@@ -7234,11 +7386,14 @@ msgid "workspace.plugins.empty-plugins"
|
||||
msgstr "No plugins installed yet"
|
||||
|
||||
#: src/app/main/ui/workspace/plugins.cljs:193
|
||||
msgid "workspace.plugins.error.manifest"
|
||||
msgstr "The plugin manifest is incorrect."
|
||||
msgid "workspace.plugins.error.manifest"
|
||||
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
|
||||
msgid "workspace.plugins.error.need-editor"
|
||||
msgid "workspace.plugins.error.need-editor"
|
||||
msgstr "You need to be an editor to use this plugin"
|
||||
|
||||
#: src/app/main/ui/workspace/plugins.cljs:189
|
||||
|
||||
@@ -347,77 +347,6 @@ msgstr "Vas a restaurar %s."
|
||||
msgid "dashboard-restore-file-confirmation.title"
|
||||
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
|
||||
msgid "dashboard.add-file"
|
||||
msgstr "Añadir archivo"
|
||||
@@ -2105,6 +2034,237 @@ msgstr "Valor resuelto:"
|
||||
msgid "inspect.tabs.styles.variants-panel"
|
||||
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
|
||||
msgid "label.mark-all-as-read"
|
||||
msgstr "Marcar todo como leído"
|
||||
@@ -2321,6 +2481,9 @@ msgstr "Director"
|
||||
msgid "labels.discard"
|
||||
msgstr "Descartar"
|
||||
|
||||
msgid "labels.dismiss"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||
msgid "labels.download"
|
||||
msgstr "Descargar %s"
|
||||
@@ -2456,6 +2619,10 @@ msgstr "Información"
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "Fuentes instaladas"
|
||||
|
||||
#: src/app/main/ui/settings/sidebar.cljs:123
|
||||
msgid "labels.integrations"
|
||||
msgstr "Integraciones"
|
||||
|
||||
#: src/app/main/ui/static.cljs:405
|
||||
msgid "labels.internal-error.desc-message-first"
|
||||
msgstr "Ha ocurrido algo extraño."
|
||||
@@ -3111,30 +3278,6 @@ msgstr "Cambiar correo"
|
||||
msgid "modals.change-email.title"
|
||||
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
|
||||
msgid "modals.create-webhook.submit-label"
|
||||
msgstr "Crear webhook"
|
||||
@@ -3151,18 +3294,6 @@ msgstr "Payload URL"
|
||||
msgid "modals.create-webhook.url.placeholder"
|
||||
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
|
||||
msgid "modals.delete-account.cancel"
|
||||
msgstr "Cancelar y mantener mi cuenta"
|
||||
@@ -3666,6 +3797,12 @@ msgstr "Invitación enviada con éxito"
|
||||
msgid "notifications.invitation-link-copied"
|
||||
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
|
||||
msgid "notifications.profile-deletion-not-allowed"
|
||||
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
||||
@@ -5068,14 +5205,14 @@ msgstr "Bibliotecas Compartidas - %s - Penpot"
|
||||
msgid "title.default"
|
||||
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
|
||||
msgid "title.settings.feedback"
|
||||
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
|
||||
msgid "title.settings.notifications"
|
||||
msgstr "Notificaciones - Penpot"
|
||||
@@ -5571,6 +5708,18 @@ msgstr "Ocultar reglas"
|
||||
msgid "workspace.header.menu.hide-textpalette"
|
||||
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
|
||||
msgid "workspace.header.menu.option.edit"
|
||||
msgstr "Editar"
|
||||
@@ -5583,6 +5732,9 @@ msgstr "Archivo"
|
||||
msgid "workspace.header.menu.option.help-info"
|
||||
msgstr "Ayuda e información"
|
||||
|
||||
msgid "workspace.header.menu.option.mcp"
|
||||
msgstr "Servidor MCP"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:906
|
||||
msgid "workspace.header.menu.option.preferences"
|
||||
msgstr "Preferencias"
|
||||
|
||||
2
mcp/.gitignore
vendored
2
mcp/.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.idea
|
||||
.claude
|
||||
node_modules
|
||||
dist
|
||||
*.tgz
|
||||
*.bak
|
||||
*.orig
|
||||
temp
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Penpot MCP Project Overview - Updated
|
||||
|
||||
## 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
|
||||
- **Language**: TypeScript
|
||||
@@ -13,21 +16,22 @@ This project is a Model Context Protocol (MCP) server for Penpot integration. It
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
penpot-mcp/
|
||||
├── common/ # Shared type definitions
|
||||
/ (project root)
|
||||
├── packages/common/ # Shared type definitions
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # Exports for shared types
|
||||
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
||||
│ └── package.json # @penpot-mcp/common package
|
||||
├── mcp-server/ # Main MCP server implementation
|
||||
├── packages/server/ # Main MCP server implementation
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # Main server entry point
|
||||
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
|
||||
│ │ ├── PluginTask.ts # Now supports result promises
|
||||
│ │ ├── tasks/ # PluginTask implementations
|
||||
│ │ └── tools/ # Tool implementations
|
||||
| ├── data/ # Contains resources, such as API info and prompts
|
||||
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||
├── penpot-plugin/ # Penpot plugin with response capability
|
||||
├── packages/plugin/ # Penpot plugin with response capability
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding
|
||||
│ │ └── plugin.ts # Now sends task responses back to server
|
||||
@@ -37,55 +41,24 @@ penpot-mcp/
|
||||
|
||||
## Key Tasks
|
||||
|
||||
### Adjusting the System Prompt
|
||||
|
||||
The system prompt file is located in `packages/server/data/initial_instructions.md`.
|
||||
|
||||
### 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.
|
||||
2. Register the tool in `PenpotMcpServer`.
|
||||
|
||||
Look at `PrintTextTool` as an example.
|
||||
|
||||
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/`.
|
||||
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.
|
||||
|
||||
### Adding a new PluginTask
|
||||
|
||||
1. Implement the input data interface for the task in `common/src/types.ts`.
|
||||
2. Implement the `PluginTask` class in `mcp-server/src/tasks/`.
|
||||
3. Implement the corresponding task handler class in the plugin (`penpot-plugin/src/task-handlers/`).
|
||||
1. Implement the input data interface for the task in `packages/common/src/types.ts`.
|
||||
2. Implement the `PluginTask` class in `packages/server/src/tasks/`.
|
||||
3. Implement the corresponding task handler class in the plugin (`packages/plugin/src/task-handlers/`).
|
||||
* In the success case, call `task.sendSuccess`.
|
||||
* 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 `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
|
||||
```
|
||||
|
||||
4. Register the task handler in `packages/plugin/src/plugin.ts` in the `taskHandlers` list.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
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.
|
||||
# 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`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
@@ -62,15 +60,17 @@ excluded_tools: []
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: |
|
||||
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).
|
||||
|
||||
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
|
||||
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
|
||||
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).
|
||||
# the name by which the project can be referenced within Serena
|
||||
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.
|
||||
languages:
|
||||
- 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
|
||||
|
||||
The project requires [Node.js](https://nodejs.org/) (tested with v22.x
|
||||
with corepack).
|
||||
The project requires [Node.js](https://nodejs.org/) (tested with v22.x).
|
||||
|
||||
Following the installation of Node.js, the tools `pnpm` and `npx`
|
||||
should be available in your terminal. For ensure corepack installed
|
||||
and enabled correctly, just execute the `./scripts/setup`.
|
||||
### 1. Starting the MCP Server and the Plugin Server
|
||||
|
||||
It is also required to have `caddy` executeable in the path, it is
|
||||
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).
|
||||
#### Running a Released Version via npx
|
||||
|
||||
You should probably be using penpot devenv, where all this
|
||||
dependencies are already present and correctly setup. But nothing
|
||||
prevents you execute this outside of devenv if you satisfy the
|
||||
specified dependencies.
|
||||
The easiest way to launch the servers is to use `npx` to run the appropriate
|
||||
version that matches your Penpot version.
|
||||
|
||||
* 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
|
||||
cd mcp/
|
||||
./scripts/setup
|
||||
```
|
||||
|
||||
@@ -86,9 +120,9 @@ pnpm run bootstrap
|
||||
|
||||
This bootstrap command will:
|
||||
|
||||
* install dependencies for all components (`pnpm -r run install`)
|
||||
* build all components (`pnpm -r run build`)
|
||||
* start all components (`pnpm -r --parallel run start`)
|
||||
* install dependencies for all components
|
||||
* build all components
|
||||
* start all components
|
||||
|
||||
### 2. Load the Plugin in Penpot and Establish the Connection
|
||||
|
||||
@@ -123,6 +157,19 @@ This bootstrap command will:
|
||||
|
||||
### 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:
|
||||
|
||||
- **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,
|
||||
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:
|
||||
|
||||
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
|
||||
npx -y mcp-remote http://localhost:4401/mcp --allow-http
|
||||
|
||||
#### Example: Claude Desktop
|
||||
|
||||
@@ -170,7 +212,7 @@ Add a `penpot` entry under `mcpServers` with the following content:
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"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:
|
||||
|
||||
1. **Common Types** (`common/`):
|
||||
1. **Common Types** (`packages/common/`):
|
||||
- Shared TypeScript definitions for request/response protocol
|
||||
- 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
|
||||
- Runs a WebSocket server accepting connections from the Penpot MCP plugin
|
||||
- Implements request/response correlation with unique task IDs
|
||||
- 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
|
||||
- Executes tasks in Penpot using the Plugin API
|
||||
- Sends structured responses back to the server#
|
||||
|
||||
4. **Helper Scripts** (`python-scripts/`):
|
||||
- Python scripts that prepare data for the MCP server (development use)
|
||||
4. **Types Generator** (`types-generator/`):
|
||||
- Generates data on API types for the MCP server (development use)
|
||||
|
||||
The core components are written in TypeScript, rendering interactions with the
|
||||
Penpot Plugin API both natural and type-safe.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Penpot MCP server can be configured using environment variables. All configuration
|
||||
options use the `PENPOT_MCP_` prefix for consistency.
|
||||
The Penpot MCP server can be configured using environment variables.
|
||||
|
||||
### Server Configuration
|
||||
|
||||
| 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_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
|
||||
| `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 |
|
||||
|-------------------------------------------|-----------------------------------------------------------------------------------------|--------------|
|
||||
| `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
|
||||
|
||||
@@ -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
|
||||
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`).
|
||||
|
||||
## 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",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"name": "@penpot/mcp",
|
||||
"version": "2.15.0-rc.1.153",
|
||||
"description": "MCP server for Penpot integration",
|
||||
"bin": {
|
||||
"penpot-mcp": "./bin/mcp-local.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm -r run build",
|
||||
"build:multi-user": "pnpm -r run build:multi-user",
|
||||
"build:types": "bash ./scripts/build-types",
|
||||
"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: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:check": "prettier --check packages/"
|
||||
},
|
||||
@@ -17,8 +20,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
"prettier": "^3.0.0"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"scripts": {
|
||||
"build": "tsc --build --clean && tsc --build",
|
||||
"watch": "tsc --watch",
|
||||
|
||||
@@ -3,12 +3,87 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Penpot plugin example</title>
|
||||
<title>Penpot MCP Plugin</title>
|
||||
</head>
|
||||
<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>
|
||||
</body>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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:multi-user": "tsc && cross-env MULTI_USER_MODE=true vite build --config vite.release.config.ts",
|
||||
"types:check": "tsc --noEmit",
|
||||
"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",
|
||||
"code": "plugin.js",
|
||||
"icon": "icon.jpg",
|
||||
"version": 2,
|
||||
"description": "This plugin enables interaction with the Penpot MCP server",
|
||||
"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 {
|
||||
/**
|
||||
@@ -189,6 +189,24 @@ export class PenpotUtils {
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
public static isContainedIn(child: Shape, parent: Shape): boolean {
|
||||
const childBounds = this.getBounds(child);
|
||||
const parentBounds = this.getBounds(parent);
|
||||
return (
|
||||
child.x >= parent.x &&
|
||||
child.y >= parent.y &&
|
||||
child.x + child.width <= parent.x + parent.width &&
|
||||
child.y + child.height <= parent.y + parent.height
|
||||
childBounds.x >= parentBounds.x &&
|
||||
childBounds.y >= parentBounds.y &&
|
||||
childBounds.x + childBounds.width <= parentBounds.x + parentBounds.width &&
|
||||
childBounds.y + childBounds.height <= parentBounds.y + parentBounds.height
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,39 +318,16 @@ export class PenpotUtils {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns The decoded data as a Uint8Array
|
||||
*/
|
||||
public static atob(base64: string): Uint8Array {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const lookup = new Uint8Array(256);
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
lookup[chars.charCodeAt(i)] = i;
|
||||
public static base64ToByteArray(base64: string): Uint8Array {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; 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;
|
||||
}
|
||||
|
||||
@@ -360,7 +357,7 @@ export class PenpotUtils {
|
||||
height: number | undefined
|
||||
): Promise<Rectangle> {
|
||||
// convert base64 to Uint8Array
|
||||
const bytes = PenpotUtils.atob(base64);
|
||||
const bytes = PenpotUtils.base64ToByteArray(base64);
|
||||
|
||||
// upload the image data to Penpot
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
case "shape":
|
||||
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";
|
||||
|
||||
// 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";
|
||||
|
||||
// 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
|
||||
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 isConnectedState - whether the connection is in a connected state (affects color)
|
||||
* @param message - optional additional message to append to the status
|
||||
* @param code - the connection state code ("idle" | "connecting" | "connected" | "disconnected" | "error")
|
||||
* @param label - human-readable label to display inside the pill
|
||||
*/
|
||||
function updateConnectionStatus(status: string, isConnectedState: boolean, message?: string): void {
|
||||
if (statusElement) {
|
||||
const displayText = message ? `${status}: ${message}` : status;
|
||||
statusElement.textContent = displayText;
|
||||
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
|
||||
function updateConnectionStatus(code: string, label: string): void {
|
||||
if (statusPill) {
|
||||
statusPill.dataset.status = code;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
function connectToMcpServer(): void {
|
||||
function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
updateConnectionStatus("Already connected", true);
|
||||
updateConnectionStatus("connected", "Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
|
||||
if (isMultiUserMode) {
|
||||
// TODO obtain proper userToken from penpot
|
||||
const userToken = "dummyToken";
|
||||
wsUrl += `?userToken=${encodeURIComponent(userToken)}`;
|
||||
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
|
||||
let wsError: unknown | undefined;
|
||||
|
||||
if (token) {
|
||||
wsUrl += `?userToken=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
updateConnectionStatus("Connecting...", false);
|
||||
updateConnectionStatus("connecting", "Connecting...");
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("Connected to MCP server");
|
||||
updateConnectionStatus("Connected to MCP server", true);
|
||||
setTimeout(() => {
|
||||
if (ws) {
|
||||
console.log("Connected to MCP server");
|
||||
updateConnectionStatus("connected", "Connected");
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log("Received from MCP server:", event.data);
|
||||
try {
|
||||
console.log("Received from MCP server:", 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
|
||||
parent.postMessage(request, "*");
|
||||
} catch (error) {
|
||||
@@ -77,34 +132,70 @@ function connectToMcpServer(): void {
|
||||
};
|
||||
|
||||
ws.onclose = (event: CloseEvent) => {
|
||||
console.log("Disconnected from MCP server");
|
||||
const message = event.reason || undefined;
|
||||
updateConnectionStatus("Disconnected", false, message);
|
||||
// If we've send the error update we don't send the disconnect as well
|
||||
if (!wsError) {
|
||||
console.log("Disconnected from MCP server");
|
||||
const label = event.reason ? `Disconnected: ${event.reason}` : "Disconnected";
|
||||
updateConnectionStatus("disconnected", label);
|
||||
updateCurrentTask(null);
|
||||
}
|
||||
ws = null;
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
wsError = error;
|
||||
// note: WebSocket error events typically don't contain detailed error messages
|
||||
updateConnectionStatus("Connection error", false);
|
||||
updateConnectionStatus("error", "Connection error");
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MCP server:", error);
|
||||
const message = error instanceof Error ? error.message : undefined;
|
||||
updateConnectionStatus("Connection failed", false, message);
|
||||
const reason = error instanceof Error ? error.message : undefined;
|
||||
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();
|
||||
});
|
||||
|
||||
disconnectBtn?.addEventListener("click", () => {
|
||||
ws?.close();
|
||||
});
|
||||
|
||||
// Listen plugin.ts messages
|
||||
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;
|
||||
} else if (event.data.type === "task-response") {
|
||||
// Forward task response back to MCP server
|
||||
sendTaskResponse(event.data.response);
|
||||
}
|
||||
});
|
||||
|
||||
parent.postMessage({ type: "ui-initialized" }, "*");
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
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
|
||||
penpot.ui.onMessage<string | { id: string; task: string; params: any }>((message) => {
|
||||
// Handle plugin task requests
|
||||
if (typeof message === "object" && message.task && message.id) {
|
||||
// Register message handlers
|
||||
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
|
||||
if (typeof message === "object" && message.type === "ui-initialized") {
|
||||
// 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) => {
|
||||
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
|
||||
penpot.on("themechange", (theme) => {
|
||||
penpot.ui.sendMessage({
|
||||
|
||||
@@ -1,10 +1,190 @@
|
||||
@import "@penpot/plugin-styles/styles.css";
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-end: 0.75rem;
|
||||
.plugin-container {
|
||||
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 code = task.params.code;
|
||||
|
||||
let result: any = await (async (ctx) => {
|
||||
const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`);
|
||||
return fn(...Object.values(ctx));
|
||||
})(context);
|
||||
// set the flags naturalChildOrdering and throwValidationErrors to true during code execution.
|
||||
// TODO: Remove all ts-ignore once Penpot types have been updated
|
||||
let originalNaturalChildOrdering: any, originalThrowValidationErrors: any;
|
||||
// @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);
|
||||
|
||||
|
||||
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 PENPOT_MCP_WEBSOCKET_URL: string;
|
||||
declare const PENPOT_MCP_VERSION: string;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { defineConfig } from "vite";
|
||||
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 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("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI));
|
||||
console.log("PENPOT_MCP_WEBSOCKET_URL:", JSON.stringify(WS_URI));
|
||||
console.log("PENPOT_MCP_VERSION:", MCP_VERSION);
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
@@ -31,13 +36,13 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
host: "0.0.0.0",
|
||||
host: SERVER_HOST,
|
||||
port: 4400,
|
||||
cors: true,
|
||||
allowedHosts: [],
|
||||
},
|
||||
define: {
|
||||
IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"),
|
||||
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(
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
base: "./",
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
* `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**:
|
||||
* `name` - Shape name
|
||||
* `fills`, `strokes` - Styling properties
|
||||
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
|
||||
* `fills: Fill[]`, `strokes: Stroke[]`, `shadows: Shadow[]` - Styling properties
|
||||
- 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**:
|
||||
* 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
|
||||
(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()`,
|
||||
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**:
|
||||
* `parent` - The parent shape (null for root shapes)
|
||||
Note: Hierarchical nesting does not necessarily imply visual containment
|
||||
* CRITICAL: To add children to a parent shape (e.g. a `Board`):
|
||||
- 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)
|
||||
* To add children to a parent shape (e.g. a `Board`): `parent.appendChild(shape)` or `parent.insertChild(index, shape)`
|
||||
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
|
||||
- Automatically removes the shape from its old parent
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
* 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!
|
||||
- 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`)
|
||||
- `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"
|
||||
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
||||
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
||||
Optionally, adjust indivudual child margins via `child.layoutChild`.
|
||||
- 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);
|
||||
appending or inserting children automatically positions them according to the layout rules.
|
||||
- CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
|
||||
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".
|
||||
Optionally, adjust individual child margins via `child.layoutChild`.
|
||||
- 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);
|
||||
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.
|
||||
To insert at a specific index, use `board.insertChild(index, shape)`.
|
||||
- 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,
|
||||
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) { ... }`
|
||||
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
|
||||
- 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`
|
||||
|
||||
* 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:
|
||||
- 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
|
||||
@@ -132,13 +138,23 @@ Boards can have layout systems that automatically control the positioning and sp
|
||||
|
||||
# Text Elements
|
||||
|
||||
The rendered content of `Text` element 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,
|
||||
it only changes the formal bounding box; if the text does not fit it, it will overflow.
|
||||
The bounding box is sized automatically as long as 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!
|
||||
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
|
||||
`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,
|
||||
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.
|
||||
* 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-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.
|
||||
* 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
* `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:
|
||||
* 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:
|
||||
const instance: Shape = component.instance();
|
||||
`const instance: Shape = component.instance();`
|
||||
This returns a `Shape` (often a `Board` containing child elements).
|
||||
After instantiation, modify the instance's properties as desired.
|
||||
* get the reference to the main component shape:
|
||||
const mainShape: Shape = component.mainInstance();
|
||||
`const mainShape: Shape = component.mainInstance();`
|
||||
|
||||
Adding assets to a library:
|
||||
* const newColor: LibraryColor = penpot.library.local.createColor();
|
||||
newColor.name = 'Brand Primary';
|
||||
newColor.color = '#0066FF';
|
||||
* const newTypo: LibraryTypography = penpot.library.local.createTypography();
|
||||
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';
|
||||
Adding a component to a library:
|
||||
```
|
||||
const shapes: Shape[] = [shape1, shape2]; // shapes to include
|
||||
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
|
||||
newComponent.name = 'My Button';
|
||||
```
|
||||
|
||||
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.
|
||||
* 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 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`)
|
||||
* `sets: TokenSet[]` - Token collections (order matters for precedence)
|
||||
* `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
|
||||
|
||||
`TokenSet` contains tokens with unique names:
|
||||
* `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();`
|
||||
* `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"
|
||||
- `value`: depends on the type of token (inspect `Token` and related types)
|
||||
- Examples:
|
||||
const token = set.addToken("color", "color.primary", "#0066FF"); // direct value
|
||||
const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token
|
||||
const token = set.addToken({type: "color", name: "color.primary", value: "#0066FF"}); // direct value
|
||||
const token2 = set.addToken({type: "color", name: "color.accent", value: "{color.primary}"}); // reference to another token
|
||||
|
||||
`Token`:
|
||||
* `name: string` - Token name (may include group path like "color.base.white")
|
||||
* `value: string | TokenValueString` - Raw value (may be direct value or reference to another token like "{color.primary}")
|
||||
`Token`: union type encompassing various token types, with common properties:
|
||||
* `name: string` - Token name (typically structured, e.g. "color.base.white")
|
||||
* `value` - Raw value (direct value or reference to another token like "{color.primary}")
|
||||
* `resolvedValue` - Computed final value (follows references)
|
||||
* `type: TokenType`
|
||||
|
||||
@@ -303,21 +351,21 @@ Applying tokens:
|
||||
(if properties is undefined, use a default property based on the token type - not usually recommended).
|
||||
`TokenProperty` is a union type; possible values are:
|
||||
- "all": applies the token to all properties it can control
|
||||
- TokenBorderRadiusProps: "r1", "r2", "r3", "r4"
|
||||
- TokenBorderRadiusProps: "borderRadiusTopLeft", "borderRadiusTopRight", "borderRadiusBottomRight", "borderRadiusBottomLeft"
|
||||
- TokenShadowProps: "shadow"
|
||||
- TokenColorProps: "fill", "stroke-color"
|
||||
- TokenDimensionProps: "x", "y", "stroke-width"
|
||||
- TokenFontFamiliesProps: "font-families"
|
||||
- TokenFontSizesProps: "font-size"
|
||||
- TokenFontWeightProps: "font-weight"
|
||||
- TokenLetterSpacingProps: "letter-spacing"
|
||||
- TokenNumberProps: "rotation", "line-height"
|
||||
- TokenColorProps: "fill", "strokeColor"
|
||||
- TokenDimensionProps: "x", "y", "strokeWidth"
|
||||
- TokenFontFamiliesProps: "fontFamilies"
|
||||
- TokenFontSizesProps: "fontSize"
|
||||
- TokenFontWeightProps: "fontWeight"
|
||||
- TokenLetterSpacingProps: "letterSpacing"
|
||||
- TokenNumberProps: "rotation"
|
||||
- TokenOpacityProps: "opacity"
|
||||
- TokenSizingProps: "width", "height", "layout-item-min-w", "layout-item-max-w", "layout-item-min-h", "layout-item-max-h"
|
||||
- TokenSpacingProps: "row-gap", "column-gap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
|
||||
- TokenBorderWidthProps: "stroke-width"
|
||||
- TokenTextCaseProps: "text-case"
|
||||
- TokenTextDecorationProps: "text-decoration"
|
||||
- TokenSizingProps: "width", "height", "layoutItemMinW", "layoutItemMaxW", "layoutItemMinH", "layoutItemMaxH"
|
||||
- TokenSpacingProps: "rowGap", "columnGap", "paddingLeft", "paddingTop", "paddingRight", "paddingBottom", "marginLeft", "marginTop", "marginRight", "marginBottom"
|
||||
- TokenBorderWidthProps: "strokeWidth"
|
||||
- TokenTextCaseProps: "textCase"
|
||||
- TokenTextDecorationProps: "textDecoration"
|
||||
- TokenTypographyProps: "typography"
|
||||
* `token.applyToShapes(shapes, properties)` - Apply from token
|
||||
* 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.
|
||||
|
||||
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
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"main": "dist/index.js",
|
||||
"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": "pnpm run build:server && cp -r src/static dist/static && cp -r data dist/data",
|
||||
"build:multi-user": "pnpm run build",
|
||||
"build": "pnpm run build:server && node scripts/copy-resources.js",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"start": "node dist/index.js",
|
||||
"start:multi-user": "node dist/index.js --multi-user",
|
||||
@@ -23,7 +22,7 @@
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -39,6 +38,7 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"@penpot/mcp-common": "workspace:../common",
|
||||
"@types/express": "^4.17.0",
|
||||
"@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.
|
||||
*
|
||||
* 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 {
|
||||
private readonly logger = createLogger("ConfigurationLoader");
|
||||
private readonly baseDir: string;
|
||||
private initialInstructions: string;
|
||||
private readonly initialInstructions: string;
|
||||
private readonly baseInstructions: string;
|
||||
|
||||
/**
|
||||
* Creates a new configuration loader instance.
|
||||
@@ -22,6 +19,7 @@ export class ConfigurationLoader {
|
||||
constructor(baseDir: string) {
|
||||
this.baseDir = baseDir;
|
||||
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 {
|
||||
@@ -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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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 server: McpServer;
|
||||
private readonly tools: Map<string, Tool<any>>;
|
||||
private readonly tools: ToolInfo[];
|
||||
public readonly configLoader: ConfigurationLoader;
|
||||
private app: any;
|
||||
public readonly pluginBridge: PluginBridge;
|
||||
private readonly replServer: ReplServer;
|
||||
private apiDocs: ApiDocs;
|
||||
private readonly penpotHighLevelOverview: string;
|
||||
private readonly connectionInstructions: string;
|
||||
|
||||
/**
|
||||
* Manages session-specific context, particularly user tokens for each request.
|
||||
*/
|
||||
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
||||
|
||||
private readonly transports = {
|
||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
||||
sse: {} as Record<string, { transport: SSEServerTransport; userToken?: string }>,
|
||||
};
|
||||
private readonly streamableTransports: Record<string, StreamableSession> = {};
|
||||
private readonly sseTransports: Record<string, { transport: SSEServerTransport; userToken?: string }> = {};
|
||||
|
||||
public readonly host: string;
|
||||
public readonly port: number;
|
||||
public readonly webSocketPort: number;
|
||||
public readonly replPort: number;
|
||||
private sessionTimeoutInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
constructor(private isMultiUser: boolean = false) {
|
||||
// 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.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 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.apiDocs = new ApiDocs();
|
||||
|
||||
this.server = new McpServer(
|
||||
{
|
||||
name: "penpot-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
instructions: this.getInitialInstructions(),
|
||||
}
|
||||
);
|
||||
// prepare instructions
|
||||
let instructions = this.configLoader.getInitialInstructions();
|
||||
instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", "));
|
||||
this.penpotHighLevelOverview = instructions;
|
||||
this.connectionInstructions = this.configLoader.getBaseInstructions();
|
||||
|
||||
this.tools = this.initTools();
|
||||
|
||||
this.tools = new Map<string, Tool<any>>();
|
||||
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
|
||||
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
|
||||
|
||||
this.registerTools();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,10 +126,11 @@ export class PenpotMcpServer {
|
||||
return !this.isRemoteMode();
|
||||
}
|
||||
|
||||
public getInitialInstructions(): string {
|
||||
let instructions = this.configLoader.getInitialInstructions();
|
||||
instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", "));
|
||||
return instructions;
|
||||
/**
|
||||
* Retrieves the high-level overview instructions explaining core Penpot usage.
|
||||
*/
|
||||
public getHighLevelOverviewInstructions(): string {
|
||||
return this.penpotHighLevelOverview;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,88 +142,134 @@ export class PenpotMcpServer {
|
||||
return this.sessionContext.getStore();
|
||||
}
|
||||
|
||||
private registerTools(): void {
|
||||
// Create relevant tool instances (depending on file system access)
|
||||
private initTools(): ToolInfo[] {
|
||||
const toolInstances: Tool<any>[] = [
|
||||
new ExecuteCodeTool(this),
|
||||
new HighLevelOverviewTool(this),
|
||||
new PenpotApiInfoTool(this, this.apiDocs),
|
||||
new ExportShapeTool(this), // tool adapts to file system access internally
|
||||
new ExportShapeTool(this),
|
||||
];
|
||||
if (this.isFileSystemAccessEnabled()) {
|
||||
toolInstances.push(new ImportImageTool(this));
|
||||
}
|
||||
|
||||
for (const tool of toolInstances) {
|
||||
const toolName = tool.getToolName();
|
||||
this.tools.set(toolName, tool);
|
||||
return toolInstances.map((instance) => {
|
||||
this.logger.info(`Registering tool: ${instance.getToolName()}`);
|
||||
return new ToolInfo(instance, instance.getToolName(), {
|
||||
description: instance.getToolDescription(),
|
||||
inputSchema: instance.getInputSchema(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Register each tool with McpServer
|
||||
this.logger.info(`Registering tool: ${toolName}`);
|
||||
this.server.registerTool(
|
||||
toolName,
|
||||
{
|
||||
description: tool.getToolDescription(),
|
||||
inputSchema: tool.getInputSchema(),
|
||||
},
|
||||
async (args) => {
|
||||
return tool.execute(args);
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Creates a fresh {@link McpServer} instance with all tools registered.
|
||||
*/
|
||||
private createMcpServer(): McpServer {
|
||||
const server = new McpServer(
|
||||
{ name: "penpot", version: "1.0.0" },
|
||||
{ instructions: this.connectionInstructions }
|
||||
);
|
||||
|
||||
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 {
|
||||
/**
|
||||
* 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) => {
|
||||
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 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;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// handle the request
|
||||
await this.sessionContext.run({ userToken }, async () => {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Legacy SSE connection endpoint
|
||||
* Legacy SSE connection endpoint.
|
||||
*/
|
||||
this.app.get("/sse", async (req: any, res: any) => {
|
||||
const userToken = req.query.userToken as string | undefined;
|
||||
|
||||
await this.sessionContext.run({ userToken }, async () => {
|
||||
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", () => {
|
||||
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) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const session = this.transports.sse[sessionId];
|
||||
const session = this.sseTransports[sessionId];
|
||||
|
||||
if (session) {
|
||||
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(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
||||
|
||||
// start the REPL server
|
||||
// start the REPL server and session timeout checker
|
||||
await this.replServer.start();
|
||||
this.startSessionTimeoutChecker();
|
||||
|
||||
resolve();
|
||||
});
|
||||
@@ -251,6 +321,7 @@ export class PenpotMcpServer {
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.logger.info("Stopping Penpot MCP Server...");
|
||||
clearInterval(this.sessionTimeoutInterval);
|
||||
await this.replServer.stop();
|
||||
this.logger.info("Penpot MCP Server stopped");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user