Compare commits

..

1 Commits

Author SHA1 Message Date
Elena Torro
6ede83d6af 🐛 Fix lazy load intersection on dragging at the beginning 2026-02-03 09:23:55 +01:00
189 changed files with 1072 additions and 34162 deletions

View File

@@ -1,45 +0,0 @@
name: "MCP CI"
on:
pull_request:
branches:
- develop
- staging
- main
types:
- opened
- synchronize
paths:
- 'mcp/**'
push:
branches:
- develop
- staging
- main
paths:
- 'mcp/**'
jobs:
test:
name: "Test"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup
working-directory: ./mcp
run: ./scripts/setup
- name: Check
working-directory: ./mcp
run: |
pnpm run fmt:check;
pnpm -r run build;
pnpm -r run types:check;

2
.nvmrc
View File

@@ -1 +1 @@
v22.22.0
v22.21.1

View File

@@ -32,9 +32,12 @@
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
## 2.13.0
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
@@ -70,9 +73,6 @@
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
## 2.12.1

View File

@@ -3,7 +3,7 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.4"}
org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.9"
:git/sha "5fad7a9"
{:git/tag "v11.8"
:git/sha "1d1b33f"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
@@ -39,7 +39,7 @@
metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.4.0"}
org.postgresql/postgresql {:mvn/version "42.7.9"}
org.postgresql/postgresql {:mvn/version "42.7.7"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
@@ -49,7 +49,7 @@
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.3"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.im4java/im4java
@@ -66,7 +66,7 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.41.21"}}
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
:paths ["src" "resources" "target/classes"]
:aliases

View File

@@ -5,6 +5,7 @@
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "app/templates/styles.css" %}
</style>

View File

@@ -5,25 +5,23 @@ penpot - error list
{% endblock %}
{% block content %}
<nav>
<div class="title">
<a href="/dbg"> [BACK]</a>
<h1>Error reports (last 300)</h1>
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a>
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
</div>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<a class="hint" href="/dbg/error/{{item.id}}">
<span class="title">{{item.hint|abbreviate:150}}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
<nav>
<div class="title">
<h1>Error reports (last 200)
<a href="/dbg">[GO BACK]</a>
</h1>
</div>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<a class="hint" href="/dbg/error/{{item.id}}">
<span class="title">{{item.hint|abbreviate:150}}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
{% endblock %}

View File

@@ -6,7 +6,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div>

View File

@@ -1,46 +0,0 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<!-- <div>[<a href="#props">props</a>]</div> -->
<div>[<a href="#context">context</a>]</div>
{% if report %}
<div>[<a href="#report">report</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if report %}
<div class="table-row multiline">
<div id="report" class="table-key">REPORT:</div>
<div class="table-val">
<pre>{{report}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

@@ -1,5 +1,5 @@
* {
font-family: monospace;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}
@@ -36,10 +36,6 @@ small {
color: #888;
}
.strong {
font-weight: 900;
}
.not-important {
color: #888;
font-weight: 200;
@@ -61,26 +57,14 @@ nav {
nav > .title {
display: flex;
justify-content: center;
width: 100%;
}
nav > .title > a {
color: black;
text-decoration: none;
}
nav > .title > a.strong {
text-decoration: underline;
}
nav > .title > h1 {
padding: 0px;
margin: 0px;
font-size: 11px;
display: block;
}
nav > .title > * {
padding: 0px 6px;
}
nav > div {

View File

@@ -232,22 +232,13 @@
(-> (io/resource "app/templates/error-report.v3.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 3)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v4 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v4.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 4)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))]
(if-let [report (get-report request)]
(let [result (case (:version report)
1 (render-template-v1 report)
2 (render-template-v2 report)
3 (render-template-v3 report)
4 (render-template-v4 report))]
3 (render-template-v3 report))]
{::yres/status 200
::yres/body result
::yres/headers {"content-type" "text/html; charset=utf-8"
@@ -255,22 +246,20 @@
{::yres/status 404
::yres/body "not found"})))
(def ^:private sql:error-reports
(def sql:error-reports
"SELECT id, created_at,
content->>'~:hint' AS hint
FROM server_error_report
WHERE version = ?
ORDER BY created_at DESC
LIMIT 300")
LIMIT 200")
(defn- error-list-handler
[{:keys [::db/pool]} {:keys [params]}]
(let [version (or (some-> (get params :version) parse-long) 3)
items (->> (db/exec! pool [sql:error-reports version])
(map #(update % :created-at ct/format-inst :rfc1123)))]
(defn error-list-handler
[{:keys [::db/pool]} _request]
(let [items (->> (db/exec! pool [sql:error-reports])
(map #(update % :created-at ct/format-inst :rfc1123)))]
{::yres/status 200
::yres/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items :version version}))
(tmpl/render {:items items}))
::yres/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}}))

View File

@@ -32,7 +32,7 @@
(assoc :request/ip-addr (inet/parse-request request))
(assoc :request/profile-id (get claims :uid))
(assoc :request/auth-data auth)
(assoc :frontend/version (or (yreq/get-header request "x-frontend-version") "unknown")))))
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
(defmulti handle-error
(fn [cause _ _]

View File

@@ -113,8 +113,6 @@
;; COLLECTOR API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private prepare-context-from-request)
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
@@ -128,8 +126,6 @@
[::props {:optional true} [:map-of :keyword :any]]
[::context {:optional true} [:map-of :keyword :any]]
[::tracked-at {:optional true} ::ct/inst]
[::created-at {:optional true} ::ct/inst]
[::source {:optional true} ::sm/text]
[::webhooks/event? {:optional true} ::sm/boolean]
[::webhooks/batch-timeout {:optional true} ::ct/duration]
[::webhooks/batch-key {:optional true}
@@ -138,8 +134,36 @@
(def ^:private check-event
(sm/check-fn schema:event))
(def valid-event?
(sm/validator schema:event))
(defn- prepare-context-from-request
[request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
key-id (::http/auth-key-id request)
token-id (::actoken/id request)
token-type (::actoken/type request)]
(d/without-nils
{:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)})))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
event {::type "action"
::profile-id (or (::rpc/profile-id params) uuid/zero)
::ip-addr (::rpc/ip-addr params)}]
(cond-> event
(some? context)
(assoc ::context context))))
(defn prepare-event
[cfg mdata params result]
@@ -188,38 +212,6 @@
(::webhooks/event? resultm)
false)}))
(defn- prepare-context-from-request
"Prepare backend event context from request"
[request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
key-id (::http/auth-key-id request)
token-id (::actoken/id request)
token-type (::actoken/type request)]
(d/without-nils
{:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)})))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
event {::type "action"
::profile-id (or (::rpc/profile-id params) uuid/zero)
::ip-addr (::rpc/ip-addr params)}]
(cond-> event
(some? context)
(assoc ::context context))))
(defn- event->params
[event]
(let [params {:id (uuid/next)
@@ -246,10 +238,8 @@
(defn- handle-event!
[cfg event]
(let [tnow (ct/now)
params (-> (event->params event)
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(let [params (event->params event)
tnow (ct/now)]
(when (contains? cf/flags :audit-log-logger)
(l/log! ::l/logger "app.audit"
@@ -265,7 +255,10 @@
;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation.
(append-audit-entry cfg params))
(let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry cfg params)))
(when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
@@ -276,6 +269,8 @@
;;
;; NOTE: this is only executed when general audit log is disabled
(let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow))
(assoc :props {})
(assoc :context {}))]
(append-audit-entry cfg params)))

View File

@@ -14,7 +14,6 @@
[app.common.schema :as sm]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -29,108 +28,69 @@
(defonce enabled (atom true))
(defn- persist-on-database!
[pool id version report]
[pool id report]
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report
{:id id
:version version
:version 3
:content (db/tjson report)})))
(defn- concurrent-exception?
[cause]
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause)))
(defn record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(let [data (if (concurrent-exception? cause)
(ex-data (ex-cause cause))
(ex-data cause))
(if (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(-> record
(assoc ::trace (ex/format-throwable cause :data? true :explain? false :header? false :summary? false))
(assoc ::l/cause (ex-cause cause))
(record->report))
ctx (-> context
(assoc :service/tenant (cf/get :tenant))
(assoc :service/host (cf/get :host))
(assoc :service/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version))
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
(let [data (ex-data cause)
ctx (-> context
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (str (cf/get :public-uri)))
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)]
(str props-hint ": " message)
message))
@message)
:trace (or (::trace record)
(some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))}
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)]
(str props-hint ": " message)
message))
@message)
:trace (or (::trace record)
(some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))}
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 20 :level 20)})
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 20 :level 20)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 13)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 13)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :length 30 :level 13)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :length 30 :level 13)})
(when-let [explain (ex/explain data :length 30 :level 13)]
{:explain explain}))))
(when-let [explain (ex/explain data :length 30 :level 13)]
{:explain explain})))))
(defn- handle-log-record
"Convert the log record into a report object and persist it on the database"
(defn error-record?
[{:keys [::l/level]}]
(= :error level))
(defn- handle-event
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try
(let [uri (cf/get :public-uri)
report (-> record record->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id id
:src "logging"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 3 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(l/debug :hint "registering error on database" :id id
:uri (str uri "/dbg/error/" id))
(defn- event->report
[{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}]
(let [context
(reduce-kv (fn [context k v]
(let [k' (keyword "frontend" (name k))]
(-> context
(dissoc k)
(assoc k' v))))
context
context)
context
(-> context
(assoc :backend/tenant (cf/get :tenant))
(assoc :backend/host (cf/get :host))
(assoc :backend/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version))
(assoc :frontend/ip-addr ip-addr))]
{:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (get props :hint)
:report (get props :report)}))
(defn- handle-audit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::audit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event event->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id id
:src "audit-log"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 4 report))
(persist-on-database! pool id report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
@@ -140,49 +100,26 @@
(defmethod ig/init-key ::reporter
[_ cfg]
(let [input (sp/chan :buf (sp/sliding-buffer 256))
thread (px/thread
{:name "penpot/reporter/database"}
(l/info :hint "initializing database error persistence")
(try
(loop []
(when-let [item (sp/take! input)]
(cond
(::l/id item)
(handle-log-record cfg item)
(let [input (sp/chan :buf (sp/sliding-buffer 64)
:xf (filter error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(::audit/id item)
(handle-audit-event cfg item)
:else
(l/warn :hint "received unexpected item" :item item))
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(l/info :hint "reporter terminated"))))]
(add-watch l/log-record ::reporter
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input
::thread thread}))
(px/thread {:name "penpot/database-reporter"}
(l/info :hint "initializing database error persistence")
(try
(loop []
(when-let [record (sp/take! input)]
(handle-event cfg record)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated"))))))
(defmethod ig/halt-key! ::reporter
[_ {:keys [::input ::thread]}]
(remove-watch l/log-record ::reporter)
(sp/close! input)
(px/interrupt! thread))
(defn emit
"Emit an event/report into the database reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))
[_ thread]
(some-> thread px/interrupt!))

View File

@@ -9,10 +9,9 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.uri :as u]
[app.config :as cf]
[app.http.client :as http]
[app.loggers.audit :as audit]
[app.loggers.database :as ldb]
[app.util.json :as json]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -21,27 +20,24 @@
(defonce enabled (atom true))
(defn- send-mattermost-notification!
[cfg {:keys [id] :as report}]
[cfg {:keys [id public-uri] :as report}]
(let [url (u/join (cf/get :public-uri) "/dbg/error/" id)
text (str "Exception: " url " "
(let [text (str "Exception: " public-uri "/dbg/error/" id " "
(when-let [pid (:profile-id report)]
(str "(pid: #uuid-" pid ")"))
"\n"
"- host: #" (:host report) "\n"
"- tenant: #" (:tenant report) "\n"
"- origin: #" (:origin report) "\n"
"- href: `" (:href report) "`\n"
"- logger: #" (:logger report) "\n"
"- request-path: `" (:request-path report) "`\n"
"- frontend-version: `" (:frontend-version report) "`\n"
"- backend-version: `" (:backend-version report) "`\n"
"\n"
(when-let [trace (:trace report)]
(str "```\n"
"Trace:\n"
trace
"```")))
"```\n"
"Trace:\n"
(:trace report)
"```")
resp (http/req! cfg
{:uri (cf/get :error-report-webhook)
@@ -54,49 +50,28 @@
(l/warn :hint "error on sending data"
:response (pr-str resp)))))
(defn- record->report
(defn record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(let [public-uri (cf/get :public-uri)]
{:id id
:origin "logging"
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:frontend-version (:frontend/version context)
:profile-id (:request/profile-id context)
:href (-> public-uri
(assoc :path (:request/path context))
(str))
:trace (ex/format-throwable cause :detail? false :header? false)}))
(defn- audit-event->report
[{:keys [::audit/context ::audit/props ::audit/id] :as event}]
{:id id
:origin "audit-log"
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:frontend-version (:version context)
:profile-id (:audit/profile-id event)
:href (get props :href)})
:public-uri (cf/get :public-uri)
:backend-version (or (:version/backend context) (:full cf/version))
:frontend-version (:version/frontend context)
:profile-id (:request/profile-id context)
:request-path (:request/path context)
:logger (::l/logger record)
:trace (ex/format-throwable cause :detail? false :header? false)})
(defn- handle-log-record
(defn handle-event
[cfg record]
(try
(let [report (record->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
(defn- handle-audit-event
[cfg record]
(try
(let [report (audit-event->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
(when @enabled
(try
(let [report (record->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause)))))
(defmethod ig/assert-key ::reporter
[_ params]
@@ -105,49 +80,27 @@
(defmethod ig/init-key ::reporter
[_ cfg]
(when-let [uri (cf/get :error-report-webhook)]
(let [input (sp/chan :buf (sp/sliding-buffer 256))
thread (px/thread
{:name "penpot/reporter/mattermost"}
(l/info :hint "initializing error reporter" :uri uri)
(try
(loop []
(when-let [item (sp/take! input)]
(when @enabled
(cond
(::l/id item)
(handle-log-record cfg item)
(::audit/id item)
(handle-audit-event cfg item)
:else
(l/warn :hint "received unexpected item" :item item)))
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(l/info :hint "reporter terminated"))))]
(add-watch l/log-record ::reporter
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input
::thread thread})))
(px/thread
{:name "penpot/mattermost-reporter"
:virtual true}
(l/info :hint "initializing error reporter" :uri uri)
(let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (filter ldb/error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(try
(loop []
(when-let [msg (sp/take! input)]
(handle-event cfg msg)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated")))))))
(defmethod ig/halt-key! ::reporter
[_ {:keys [::input ::thread]}]
(remove-watch l/log-record ::reporter)
(some-> input sp/close!)
[_ thread]
(some-> thread px/interrupt!))
(defn emit
"Emit an event/report into the mattermost reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))

View File

@@ -337,13 +337,7 @@
::setup/props (ig/ref ::setup/props)
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)}
::email/whitelist (ig/ref ::email/whitelist)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)
@@ -369,7 +363,6 @@
;; FIXME: revisit if db/pool is necessary here
::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager)
::setup/props (ig/ref ::setup/props)
::setup/shared-keys (ig/ref ::setup/shared-keys)}
::wrk/registry

View File

@@ -456,10 +456,7 @@
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
{:name "0143-http-session-v2-table"
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
{:name "0144-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}])
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -1,11 +0,0 @@
ALTER TABLE server_error_report DROP CONSTRAINT server_error_report_pkey;
DELETE FROM server_error_report a
USING server_error_report b
WHERE a.id = b.id
AND a.ctid < b.ctid;
ALTER TABLE server_error_report ADD PRIMARY KEY (id);
CREATE INDEX server_error_report__version__idx
ON server_error_report ( version );

View File

@@ -16,8 +16,6 @@
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as-alias audit]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
@@ -38,79 +36,52 @@
:context])
(defn- event->row [event]
[(::audit/id event)
(::audit/name event)
(::audit/source event)
(::audit/type event)
(::audit/tracked-at event)
(::audit/created-at event)
(::audit/profile-id event)
(db/inet (::audit/ip-addr event))
(db/tjson (::audit/props event))
(db/tjson (d/without-nils (::audit/context event)))])
[(uuid/next)
(:name event)
(:source event)
(:type event)
(:timestamp event)
(:created-at event)
(:profile-id event)
(db/inet (:ip-addr event))
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))])
(defn- adjust-timestamp
[{:keys [::audit/tracked-at ::audit/created-at] :as event}]
(let [margin (inst-ms (ct/diff tracked-at created-at))]
[{:keys [timestamp created-at] :as event}]
(let [margin (inst-ms (ct/diff timestamp created-at))]
(if (or (neg? margin)
(> margin 3600000))
;; If event is in future or lags more than 1 hour, we reasign
;; tracked-at to the server creation date
;; timestamp to the server creation date
(-> event
(assoc ::audit/tracked-at created-at)
(update ::audit/context assoc :original-tracked-at tracked-at))
(assoc :timestamp created-at)
(update :context assoc :original-timestamp timestamp))
event)))
(defn- exception-event?
[{:keys [::audit/type ::audit/name] :as ev}]
(and (= "action" type)
(or (= "unhandled-exception" name)
(= "exception-page" name))))
(def ^:private xf:map-event-row
(comp
(map adjust-timestamp)
(map event->row)))
(defn- get-events
[{:keys [::rpc/request-at ::rpc/profile-id events] :as params}]
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (inet/parse-request request)
xform (map (fn [event]
{::audit/id (uuid/next)
::audit/type (:type event)
::audit/name (:name event)
::audit/props (:props event)
::audit/context (:context event)
::audit/profile-id profile-id
::audit/ip-addr ip-addr
::audit/source "frontend"
::audit/tracked-at (:timestamp event)
::audit/created-at request-at}))]
(sequence xform events)))
(defn- handle-events
[{:keys [::db/pool] :as cfg} params]
(let [events (get-events params)]
;; Look for error reports and save them on internal reports table
(when-let [events (->> events
(sequence (filter exception-event?))
(not-empty))]
(run! (partial loggers.db/emit cfg) events)
(run! (partial loggers.mm/emit cfg) events))
;; Process and save events
tnow (ct/now)
xform (comp
(map (fn [event]
(-> event
(assoc :created-at tnow)
(assoc :profile-id profile-id)
(assoc :ip-addr ip-addr)
(assoc :source "frontend"))))
(filter :profile-id)
(map adjust-timestamp)
(map event->row))
events (sequence xform events)]
(when (seq events)
(let [rows (sequence xf:map-event-row events)]
(db/insert-many! pool :audit-log event-columns rows)))))
(db/insert-many! pool :audit-log event-columns events))))
(def ^:private valid-event-types
(def valid-event-types
#{"action" "identify" "trigger"})
(def ^:private schema:frontend-event
(def schema:event
[:map {:title "Event"}
[:name
[:and {:gen/elements ["update-file", "get-profile"]}
@@ -122,13 +93,12 @@
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword ::sm/any]]
[:timestamp ::ct/inst]
[:context {:optional true}
[:map-of :keyword ::sm/any]]])
(def ^:private schema:push-audit-events
(def schema:push-audit-events
[:map {:title "push-audit-events"}
[:events [:vector schema:frontend-event]]])
[:events [:vector schema:event]]])
(sv/defmethod ::push-audit-events
{::climit/id :submit-audit-events/by-profile

View File

@@ -103,14 +103,6 @@
(assoc-in [::db/pool ::db/username] (:database-username config))
(assoc-in [::db/pool ::db/password] (:database-password config))
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(update :app.rpc/methods
(fn [state]
(-> state
(assoc :app.setup/templates templates)
(assoc :app.loggers.mattermost/reporter nil)
(assoc :app.loggers.database/reporter nil))))
(dissoc :app.srepl/server
:app.http/server
:app.http/route

View File

@@ -1,5 +1,5 @@
{:deps
{org.clojure/clojure {:mvn/version "1.12.4"}
{org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/data.json {:mvn/version "2.5.1"}
org.clojure/tools.cli {:mvn/version "1.1.230"}
org.clojure/test.check {:mvn/version "1.1.1"}
@@ -9,15 +9,15 @@
org.apache.commons/commons-pool2 {:mvn/version "2.12.1"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.25.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.3"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.41"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
selmer/selmer {:mvn/version "1.12.70"}
selmer/selmer {:mvn/version "1.12.69"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
@@ -27,7 +27,7 @@
com.cognitect/transit-clj {:mvn/version "1.0.333"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.1"}
integrant/integrant {:mvn/version "1.0.0"}
funcool/cuerdas {:mvn/version "2026.415"}
funcool/promesa

View File

@@ -239,63 +239,22 @@
(recur cause))))))]
(with-out-str
(print-all cause))))
:cljs
(defn format-throwable
[cause & {:as opts}]
(with-out-str
(when-let [exdata (ex-data cause)]
(when-let [hint (get exdata :hint)]
(when (str/index-of hint "\n")
(println "Hint:")
(println "--------------------")
(println hint)
(println)))
(when-let [explain (get exdata ::sm/explain)]
(println "Explain:")
(println "--------------------")
(println (sm/humanize-explain explain))
(println))
(when-let [explain (get exdata :explain)]
(println "Server Explain:")
(println "--------------------")
(println explain))
(println "Data:")
(println "--------------------")
(pp/pprint (dissoc exdata ::sm/explain :explain))
(println))
(when-let [trace (.-stack cause)]
(println "Trace:")
(println "--------------------")
(println (.-stack cause))))))
(defn first-line
[s]
(let [break-index (str/index-of s "\n")]
(if (pos? break-index)
(subs s 0 break-index)
s)))
(print-all cause)))))
(defn print-throwable
[cause & {:as opts}]
#?(:clj
(println (format-throwable cause opts))
:cljs
(let [prefix (get opts :prefix)
data (ex-data cause)
title (cond->> (or (some-> (:hint data) first-line)
(ex-message cause))
(string? prefix)
(str prefix ": "))]
(let [prefix (get opts :prefix "exception")
title (str prefix ": " (ex-message cause))
exdata (ex-data cause)]
(js/console.group title)
(try
(js/console.log (format-throwable cause))
(finally
(js/console.groupEnd))))))
(when-let [explain (get exdata ::sm/explain)]
(println (sm/humanize-explain explain)))
(js/console.log "\nData:")
(pp/pprint (dissoc exdata ::sm/explain))
(js/console.log "\nTrace:")
(js/console.error (.-stack cause)))))

View File

@@ -124,7 +124,6 @@
:token-base-font-size
:token-color
:token-shadow
:token-tokenscript
:transit-readable-response
:user-feedback
;; TODO: remove this flag.

View File

@@ -2016,9 +2016,7 @@
(let [;; We need to sync only the position relative to the origin of the component.
;; (see update-attrs for a full explanation)
previous-shape (reposition-shape previous-shape prev-root current-root)
touched (get previous-shape :touched #{})
text-auto? (and (cfh/text-shape? current-shape)
(contains? #{:auto-height :auto-width} (:grow-type current-shape)))]
touched (get previous-shape :touched #{})]
(loop [attrs updatable-attrs
roperations [{:type :set-touched :touched (:touched previous-shape)}]
@@ -2027,10 +2025,6 @@
(let [attr-group (get ctk/sync-attrs attr)
skip-operations?
(or
;; For auto text, avoid copying geometry-driven attrs on switch.
(and text-auto?
(contains? #{:points :selrect :width :height :position-data} attr))
;; If the attribute is not valid for the destiny, don't copy it
(not (cts/is-allowed-switch-keep-attr? attr (:type current-shape)))

View File

@@ -109,12 +109,9 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
(def token-name-validation-regex
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
token-name-validation-regex])
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$"])
(def ^:private schema:color
[:map

View File

@@ -1467,12 +1467,11 @@ Will return a value that matches this schema:
(def ^:private schema:dtcg-node
[:schema {:registry
{::simple-value
[:or :string :int :double ::sm/boolean]
[:or :string :int :double]
::value
[:or
[:ref ::simple-value]
[:vector ::simple-value]
[:vector [:map-of :string ::simple-value]]
[:map-of :string [:or
[:ref ::simple-value]
[:vector ::simple-value]]]]}}

View File

@@ -31,7 +31,7 @@ RUN set -ex; \
FROM base AS setup-node
ENV NODE_VERSION=v22.22.0 \
ENV NODE_VERSION=v22.21.1 \
PATH=/opt/node/bin:$PATH
RUN set -eux; \
@@ -97,19 +97,18 @@ RUN set -eux; \
FROM base AS setup-jvm
# https://clojure.org/releases/tools
ENV CLOJURE_VERSION=1.12.4.1602
ENV CLOJURE_VERSION=1.12.3.1577
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='9903c6b19183a33725ca1dfdae5b72400c9d00995c76fafc4a0d31c5152f33f7'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_aarch64.tar.gz'; \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='946ad9766d98fc6ab495a1a120072197db54997f6925fb96680f1ecd5591db4e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_x64.tar.gz'; \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@@ -182,8 +181,7 @@ FROM base AS setup-utils
ENV CLJKONDO_VERSION=2026.01.19 \
BABASHKA_VERSION=1.12.208 \
CLJFMT_VERSION=0.15.6 \
PIXI_VERSION=0.63.2
CLJFMT_VERSION=0.15.6
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
@@ -226,26 +224,6 @@ RUN set -ex; \
tar -xf /tmp/babashka.tar.gz; \
rm -rf /tmp/babashka.tar.gz;
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://github.com/prefix-dev/pixi/releases/download/v$PIXI_VERSION/pixi-aarch64-unknown-linux-musl.tar.gz"; \
;; \
amd64|x86_64) \
BINARY_URL="https://github.com/prefix-dev/pixi/releases/download/v$PIXI_VERSION/pixi-x86_64-unknown-linux-musl.tar.gz"; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
cd /tmp; \
curl -LfsSo /tmp/pixi.tar.gz ${BINARY_URL}; \
cd /opt/utils/bin; \
tar -xf /tmp/pixi.tar.gz; \
rm -rf /tmp/pixi.tar.gz;
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
@@ -397,7 +375,7 @@ ENV LANG='C.UTF-8' \
RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=penpotapp/imagemagick:7.1.2-13 /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --from=setup-jvm /opt/jdk /opt/jdk
COPY --from=setup-jvm /opt/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node

View File

@@ -46,11 +46,6 @@ services:
- 9090:9090
- 9091:9091
# MCP
- 4400:4400
- 4401:4401
- 4402:4402
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}
# SMTP setup

View File

@@ -6,8 +6,7 @@ export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bi
export CARGO_HOME="/home/penpot/.cargo"
alias l='ls --color -GFlh'
alias ll='ls --color -GFlh'
alias rm='rm -rf'
alias rm='rm -r'
alias ls='ls --color -F'
alias lsd='ls -d *(/)'
alias lsf='ls -h *(.)'

View File

@@ -121,28 +121,6 @@ http {
proxy_http_version 1.1;
}
location /mcp {
alias /home/penpot/penpot/mcp/packages/plugin/dist;
proxy_http_version 1.1;
}
location /mcp/ws {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_pass http://127.0.0.1:4402;
proxy_http_version 1.1;
}
location /mcp/stream {
proxy_pass http://127.0.0.1:4401/mcp;
proxy_http_version 1.1;
}
location /mcp/sse {
proxy_pass http://127.0.0.1:4401/sse;
proxy_http_version 1.1;
}
location /admin {
proxy_pass http://127.0.0.1:6063/admin;
}

View File

@@ -1,4 +1,3 @@
set -g default-command "${SHELL}"
set -g mouse off
set -g history-limit 50000
setw -g mode-keys emacs

View File

@@ -6,7 +6,7 @@ ENV LANG='C.UTF-8' \
DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC
ARG IMAGEMAGICK_VERSION=7.1.2-13
ARG IMAGEMAGICK_VERSION=7.1.1-47
RUN set -e; \
apt-get -qq update; \

View File

@@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=v22.22.0 \
NODE_VERSION=v22.21.1 \
TZ=Etc/UTC
RUN set -ex; \
@@ -46,12 +46,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='9903c6b19183a33725ca1dfdae5b72400c9d00995c76fafc4a0d31c5152f33f7'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_aarch64.tar.gz'; \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='946ad9766d98fc6ab495a1a120072197db54997f6925fb96680f1ecd5591db4e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_x64.tar.gz'; \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \

View File

@@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v22.22.0 \
NODE_VERSION=v22.21.1 \
DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:/opt/imagick/bin:$PATH

View File

@@ -1,58 +0,0 @@
FROM ubuntu:24.04
LABEL maintainer="Penpot <docker@penpot.app>"
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
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install \
curl \
tzdata \
locales \
ca-certificates \
; \
rm -rf /var/lib/apt/lists/*; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
find /usr/share/i18n/locales/ -type f ! -name "en_US" ! -name "POSIX" ! -name "C" -delete;
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \
;; \
amd64|x86_64) \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \
mkdir -p /opt/node; \
cd /opt/node; \
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
chown -R root /opt/node; \
rm -rf /tmp/nodejs.tar.gz; \
corepack enable; \
mkdir -p /opt/penpot; \
chown -R penpot:penpot /opt/penpot;
ARG BUNDLE_PATH="./bundle-mcp/"
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/mcp/
WORKDIR /opt/penpot/mcp
USER penpot:penpot
RUN ./setup
CMD ["node", "index.js", "--multi-user"]

View File

@@ -198,13 +198,6 @@ services:
## Valkey (or previously Redis) is used for the websockets notifications.
PENPOT_REDIS_URI: redis://penpot-valkey/0
penpot-mcp:
image: penpotapp/mcp:${PENPOT_VERSION:-latest}
restart: always
networks:
- penpot
penpot-postgres:
image: "postgres:15"
restart: always

View File

@@ -130,7 +130,6 @@ http {
}
location /readyz {
access_log off;
proxy_pass $PENPOT_BACKEND_URI$request_uri;
}

View File

@@ -47,7 +47,6 @@
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/tokenscript": "workspace:./packages/tokenscript",
"@penpot/plugins-runtime": "1.4.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",

View File

@@ -1,3 +0,0 @@
node_modules
*.sublime-workspace
/.yarn

View File

@@ -1,2 +0,0 @@
export * from "./schemas.js";
export * from "@tokens-studio/tokenscript-interpreter";

View File

@@ -1,13 +0,0 @@
{
"name": "@penpot/tokenscript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
"author": "Andrey Antukh",
"license": "MPL-2.0",
"dependencies": {
"@tokens-studio/tokenscript-interpreter": "^0.23.1"
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,4 @@
import { defineConfig, devices } from "@playwright/test";
import { platform } from "os";
/**
* Read environment variables from file.
@@ -7,10 +6,6 @@ import { platform } from "os";
*/
// require('dotenv').config();
const userAgent = platform === 'darwin' ?
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" :
undefined;
/**
* @see https://playwright.dev/docs/test-configuration
*/
@@ -48,20 +43,12 @@ export default defineConfig({
projects: [
{
name: "default",
use: { ...devices["Desktop Chrome"] },
testDir: "./playwright/ui/specs",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
video: 'retain-on-failure',
trace: 'retain-on-failure',
userAgent,
},
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.001,
},
},
}
},
{
name: "ds",

View File

@@ -1,7 +0,0 @@
{
"~:file-id": "~u8d38942d-b01f-800e-8007-79ee6a9bac45",
"~:tag": "component",
"~:object-id": "8d38942d-b01f-800e-8007-79ee6a9bac45/8d38942d-b01f-800e-8007-79ee6a9bac46/6b68aedd-4c5b-80b9-8007-7b38c1d34ce4/component",
"~:media-id": "~ube2dc82e-615b-486b-a193-8768bdb51d7a",
"~:created-at": "~m1769523563389"
}

View File

@@ -253,7 +253,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async #waitForWebSocketReadiness() {
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 })
await expect(this.pageName).toHaveText("Page 1");
}
async sendPresenceMessage(fixture) {
@@ -383,46 +383,19 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait);
await this.clickAndMove(x1, y1, x2, y2);
await expect(this.page.getByTestId("text-editor")).toBeVisible();
await this.page.waitForTimeout(timeToWait);
if (initialText) {
await this.page.keyboard.type(initialText);
}
}
/**
* Copies the selected element into the clipboard, or copy the
* content of the locator into the clipboard.
* Copies the selected element into the clipboard.
*
* @returns {Promise<void>}
*/
async copy(kind = "keyboard", locator = undefined) {
if (kind === "context-menu" && locator) {
await locator.click({ button: "right" });
await this.page.getByText("Copy", { exact: true }).click();
} else {
await this.page.keyboard.press("ControlOrMeta+C");
}
// wait for the clipboard to be updated
await this.page.waitForFunction(async () => {
const content = await navigator.clipboard.readText()
return content !== "";
}, { timeout: 1000 });
}
async cut(kind = "keyboard", locator = undefined) {
if (kind === "context-menu" && locator) {
await locator.click({ button: "right" });
await this.page.getByText("Cut", { exact: true }).click();
} else {
await this.page.keyboard.press("ControlOrMeta+X");
}
// wait for the clipboard to be updated
await this.page.waitForFunction(async () => {
const content = await navigator.clipboard.readText()
return content !== "";
}, { timeout: 1000 });
async copy() {
return this.page.keyboard.press("Control+C");
}
/**
@@ -434,9 +407,9 @@ export class WorkspacePage extends BaseWebSocketPage {
async paste(kind = "keyboard") {
if (kind === "context-menu") {
await this.viewport.click({ button: "right" });
return this.page.getByText("Paste", { exact: true }).click();
return this.page.getByText("PasteCtrlV").click();
}
return this.page.keyboard.press("ControlOrMeta+V");
return this.page.keyboard.press("Control+V");
}
async panOnViewportAt(x, y, width, height) {
@@ -475,11 +448,11 @@ export class WorkspacePage extends BaseWebSocketPage {
const layer = this.layers
.getByTestId("layer-row")
.filter({ hasText: name });
const button = layer.getByTestId("toggle-content");
const button = layer.getByRole("button");
await expect(button).toBeVisible();
await button.waitFor();
await button.click(clickOptions);
await button.waitFor({ ariaExpanded: true });
await this.page.waitForTimeout(500);
}
async expectSelectedLayer(name) {
@@ -522,7 +495,13 @@ export class WorkspacePage extends BaseWebSocketPage {
async clickColorPalette(clickOptions = {}) {
await this.palette
.getByRole("button", { name: /Color Palette/ })
.getByRole("button", { name: "Color Palette (Alt+P)" })
.click(clickOptions);
}
async clickColorPalette(clickOptions = {}) {
await this.palette
.getByRole("button", { name: "Color Palette (Alt+P)" })
.click(clickOptions);
}

View File

@@ -15,8 +15,6 @@ test("User can complete the onboarding", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
const onboardingPage = new OnboardingPage(page);
await dashboardPage.mockConfigFlags(["enable-onboarding"]);
await dashboardPage.goToDashboard();
await expect(
page.getByRole("heading", { name: "Help us get to know you" }),

View File

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { WasmWorkspacePage, WASM_FLAGS } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);

View File

@@ -5,7 +5,7 @@ import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100;
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL);
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
@@ -110,7 +110,7 @@ test("Update an already created text shape by prepending text", async ({
await workspace.textEditor.stopEditing();
});
test.skip("Update an already created text shape by inserting text in between", async ({
test("Update an already created text shape by inserting text in between", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
@@ -151,7 +151,7 @@ test("Update a new text shape appending text by pasting text", async ({
await workspace.textEditor.stopEditing();
});
test.skip("Update a new text shape prepending text by pasting text", async ({
test("Update a new text shape prepending text by pasting text", async ({
page,
context,
}) => {

View File

@@ -774,7 +774,7 @@ test.describe("Tokens: Apply token", () => {
await workspace.layers
.getByTestId("layer-row")
.nth(1)
.getByTestId("toggle-content")
.getByRole("button", { name: "Toggle layer" })
.click();
await workspace.layers.getByTestId("layer-row").nth(2).click();

View File

@@ -158,7 +158,7 @@ test.describe("Tokens - creation", () => {
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
@@ -189,7 +189,7 @@ test.describe("Tokens - creation", () => {
// 2. Invalid value → disabled + error message
await valueField.fill("1");
const invalidValueErrorNode =
tokensUpdateCreateModal.getByText(invalidValueError);
tokensUpdateCreateModal.getByText(invalidValueError);
await expect(invalidValueErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -197,7 +197,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -207,7 +207,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("{color.primary}");
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -320,7 +320,7 @@ test.describe("Tokens - creation", () => {
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -356,7 +356,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -366,7 +366,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("{my-token}");
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -459,13 +459,13 @@ test.describe("Tokens - creation", () => {
test("User creates font weight token", async ({ page }) => {
const invalidValueError =
"Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'";
"Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'";
const emptyNameError = "Name should be at least 1 character";
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -501,7 +501,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("red");
const invalidValueErrorNode =
tokensUpdateCreateModal.getByText(invalidValueError);
tokensUpdateCreateModal.getByText(invalidValueError);
await expect(invalidValueErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -510,7 +510,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -520,7 +520,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("{my-token}");
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -595,13 +595,13 @@ test.describe("Tokens - creation", () => {
test("User creates text case token", async ({ page }) => {
const invalidValueError =
"Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted";
"Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted";
const emptyNameError = "Name should be at least 1 character";
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -637,7 +637,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("red");
const invalidValueErrorNode =
tokensUpdateCreateModal.getByText(invalidValueError);
tokensUpdateCreateModal.getByText(invalidValueError);
await expect(invalidValueErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -646,7 +646,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -656,7 +656,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("{my-token}");
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -711,13 +711,13 @@ test.describe("Tokens - creation", () => {
test("User creates text decoration token", async ({ page }) => {
const invalidValueError =
"Invalid token value: only none, underline and strike-through are accepted";
"Invalid token value: only none, underline and strike-through are accepted";
const emptyNameError = "Name should be at least 1 character";
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -755,7 +755,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("red");
const invalidValueErrorNode =
tokensUpdateCreateModal.getByText(invalidValueError);
tokensUpdateCreateModal.getByText(invalidValueError);
await expect(invalidValueErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -764,7 +764,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -774,7 +774,7 @@ test.describe("Tokens - creation", () => {
await valueField.fill("{my-token}");
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -831,7 +831,7 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -900,7 +900,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -977,9 +977,9 @@ test.describe("Tokens - creation", () => {
await nameField.fill("my-token-2");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
tokensUpdateCreateModal.getByTestId("reference-opt");
const compositeToggle =
tokensUpdateCreateModal.getByTestId("composite-opt");
tokensUpdateCreateModal.getByTestId("composite-opt");
await referenceToggle.click();
const referenceInput = tokensUpdateCreateModal.getByPlaceholder(
@@ -1012,7 +1012,7 @@ test.describe("Tokens - creation", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1038,7 +1038,7 @@ test.describe("Tokens - creation", () => {
// Switch to reference tab, should not be submittable either
const referenceTabButton =
tokensUpdateCreateModal.getByTestId("reference-opt");
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
await expect(submitButton).toBeDisabled();
});
@@ -1047,7 +1047,7 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]});
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1116,7 +1116,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -1198,9 +1198,9 @@ test.describe("Tokens - creation", () => {
await nameField.fill("my-token-2");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
tokensUpdateCreateModal.getByTestId("reference-opt");
const compositeToggle =
tokensUpdateCreateModal.getByTestId("composite-opt");
tokensUpdateCreateModal.getByTestId("composite-opt");
await referenceToggle.click();
const referenceInput = tokensUpdateCreateModal.getByPlaceholder(
@@ -1232,7 +1232,7 @@ test.describe("Tokens - creation", () => {
test("User creates typography token", async ({ page }) => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1287,7 +1287,7 @@ test.describe("Tokens - creation", () => {
await nameField.fill("");
const emptyNameErrorNode =
tokensUpdateCreateModal.getByText(emptyNameError);
tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
@@ -1443,9 +1443,9 @@ test.describe("Tokens - creation", () => {
await nameField.fill("my-token-2");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
tokensUpdateCreateModal.getByTestId("reference-opt");
const compositeToggle =
tokensUpdateCreateModal.getByTestId("composite-opt");
tokensUpdateCreateModal.getByTestId("composite-opt");
await referenceToggle.click();
@@ -1479,7 +1479,7 @@ test.describe("Tokens - creation", () => {
test("User adds typography token with reference", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
await setupTypographyTokensFile(page);
const newTokenTitle = "NewReference";
@@ -1508,7 +1508,7 @@ test.describe("Tokens - creation", () => {
});
const resolvedValue =
await tokensUpdateCreateModal.getByText("Resolved value:");
await tokensUpdateCreateModal.getByText("Resolved value:");
await expect(resolvedValue).toBeVisible();
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
await expect(resolvedValue).toContainText("Font Size: 100");
@@ -1531,7 +1531,7 @@ test.describe("Tokens - creation", () => {
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
@@ -1591,7 +1591,7 @@ test.describe("Tokens - creation", () => {
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1613,11 +1613,15 @@ test.describe("Tokens - creation", () => {
});
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click();
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
@@ -1643,7 +1647,9 @@ test("User creates grouped color token", async ({ page }) => {
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({ page }) => {
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1671,7 +1677,7 @@ test("User cant create regular token with value missing", async ({ page }) => {
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1697,7 +1703,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
await setupTypographyTokensFile(page);
await tokensSidebar
.getByRole("button")
@@ -1718,8 +1724,8 @@ test.describe("Tokens tab - edition", () => {
// Fill font-family to verify to verify that input value doesn't get split into list of characters
const fontFamilyField = tokensUpdateCreateModal
.getByLabel("Font family")
.first();
.getByLabel("Font family")
.first();
await fontFamilyField.fill("OneWord");
// Invalidate incorrect values for font size
@@ -1740,11 +1746,11 @@ test.describe("Tokens tab - edition", () => {
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i);
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
const textDecorationField =
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
// Capture all values before switching tabs
const originalValues = {
@@ -1759,14 +1765,14 @@ test.describe("Tokens tab - edition", () => {
// Switch to reference tab and back to composite tab
const referenceTabButton =
tokensUpdateCreateModal.getByTestId("reference-opt");
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
// Empty reference tab should be disabled
await expect(saveButton).toBeDisabled();
const compositeTabButton =
tokensUpdateCreateModal.getByTestId("composite-opt");
tokensUpdateCreateModal.getByTestId("composite-opt");
await compositeTabButton.click();
// Filled composite tab should be enabled
@@ -1793,7 +1799,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1829,7 +1835,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFile(page);
await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1884,7 +1890,7 @@ test.describe("Tokens tab - edition", () => {
test.describe("Tokens tab - delete", () => {
test("User delete color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1904,7 +1910,7 @@ test.describe("Tokens tab - delete", () => {
});
test("User removes node and all child tokens", async ({ page }) => {
const { tokensSidebar } = await setupTokensFile(page);
const { tokensSidebar, workspacePage } = await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1913,7 +1919,7 @@ test.describe("Tokens tab - delete", () => {
// Verify that the node and child token are visible before deletion
const colorNode = tokensSidebar.getByRole("button", {
name: "colors",
name: "blue",
exact: true,
});
const colorNodeToken = tokensSidebar.getByRole("button", {
@@ -1937,13 +1943,5 @@ test.describe("Tokens tab - delete", () => {
await expect(colorNode).not.toBeVisible();
// Verify that child token is also removed
await expect(colorNodeToken).not.toBeVisible();
// Save the type button to verify that expands/folds
const tokenTypeButton = await tokensSidebar.getByRole("button", {
name: "Color",
exact: true,
});
await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
});
});

View File

@@ -1,19 +1,12 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
import { WorkspacePage } from "../pages/WorkspacePage";
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
import { Clipboard } from "../../helpers/Clipboard";
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL);
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
});
test.afterEach(async ({ context }) => {
context.clearPermissions();
});
const setupVariantsFile = async (workspacePage) => {
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
@@ -41,9 +34,9 @@ const setupVariantsFileWithVariant = async (workspacePage) => {
await setupVariantsFile(workspacePage);
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
await workspacePage.page.waitForTimeout(500);
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
await workspacePage.page.waitForTimeout(500);
// We wait until layer-row starts looking like it an component
@@ -163,7 +156,7 @@ test("User duplicates a variant container", async ({ page }) => {
await variant.container.click();
//Duplicate the variant container
await workspacePage.page.keyboard.press("ControlOrMeta+d");
await workspacePage.page.keyboard.press("Control+d");
const variant_original = await findVariant(workspacePage, 1); // On duplicate, the new item is the first
const variant_duplicate = await findVariant(workspacePage, 0);
@@ -176,27 +169,25 @@ test("User duplicates a variant container", async ({ page }) => {
await validateVariant(variant_duplicate);
});
test("User copy paste a variant container", async ({ page, context }) => {
test("User copy paste a variant container", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
// Access to the read/write clipboard necesary for this functionality
await setupVariantsFileWithVariant(workspacePage);
await workspacePage.mockRPC(
/create-file-object-thumbnail.*/,
"workspace/create-file-object-thumbnail.json",
);
const variant = findVariantNoWait(workspacePage, 0);
// await variant.container.waitFor();
// Select the variant container
await variant.container.click();
await workspacePage.page.waitForTimeout(1000);
// Copy the variant container
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.copy("keyboard");
await workspacePage.page.keyboard.press("Control+c");
// Paste the variant container
await workspacePage.clickAt(400, 400);
await workspacePage.paste("keyboard");
const variants = workspacePage.layers.getByText("Rectangle");
await expect(variants).toHaveCount(2);
await workspacePage.page.keyboard.press("Control+v");
const variantDuplicate = findVariantNoWait(workspacePage, 0);
const variantOriginal = findVariantNoWait(workspacePage, 1);
@@ -221,17 +212,18 @@ test("User cut paste a variant container", async ({ page }) => {
await variant.container.click();
//Cut the variant container
await workspacePage.cut("keyboard");
await workspacePage.page.keyboard.press("Control+x");
await workspacePage.page.waitForTimeout(500);
//Paste the variant container
await workspacePage.clickAt(500, 500);
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
await workspacePage.page.waitForTimeout(500);
const variantPasted = await findVariant(workspacePage, 0);
// Expand the layers
await workspacePage.clickToggableLayer("Rectangle");
await variantPasted.container.locator("button").first().click();
// The variants are valid
await validateVariant(variantPasted);
@@ -247,34 +239,27 @@ test("User cut paste a variant container into a board, and undo twice", async ({
//Create a board
await workspacePage.boardButton.click();
// NOTE: this board should not intersect the existing variants, otherwise
// this test is flaky
await workspacePage.clickWithDragViewportAt(200, 200, 100, 100);
await workspacePage.clickWithDragViewportAt(500, 500, 100, 100);
await workspacePage.clickAt(495, 495);
const board = await workspacePage.rootShape.locator("Board");
// Select the variant container
// await variant.container.click();
await workspacePage.clickLeafLayer("Rectangle");
await variant.container.click();
//Cut the variant container
await workspacePage.cut("keyboard");
await expect(variant.container).not.toBeVisible();
await workspacePage.page.keyboard.press("Control+x");
await workspacePage.page.waitForTimeout(500);
//Select the board
await workspacePage.clickLeafLayer("Board");
//Paste the variant container inside the board
await workspacePage.paste("keyboard");
await expect(variant.container).toBeVisible();
await workspacePage.page.keyboard.press("Control+v");
//Undo twice
await workspacePage.page.keyboard.press("ControlOrMeta+z");
await expect(variant.container).not.toBeVisible();
await workspacePage.page.keyboard.press("ControlOrMeta+z");
await expect(variant.container).toBeVisible();
await workspacePage.page.keyboard.press("Control+z");
await workspacePage.page.keyboard.press("Control+z");
await workspacePage.page.waitForTimeout(500);
const variantAfterUndo = await findVariant(workspacePage, 0);
@@ -291,12 +276,12 @@ test("User copy paste a variant", async ({ page }) => {
// Select the variant1
await variant.variant1.click();
// Copy the variant
await workspacePage.copy("keyboard");
//Cut the variant
await workspacePage.page.keyboard.press("Control+c");
// Paste the variant
//Paste the variant
await workspacePage.clickAt(500, 500);
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
const copy = await workspacePage.layers
.getByTestId("layer-row")
@@ -317,11 +302,11 @@ test("User cut paste a variant outside the container", async ({ page }) => {
await variant.variant1.click();
//Cut the variant
await workspacePage.cut("keyboard");
await workspacePage.page.keyboard.press("Control+x");
//Paste the variant
await workspacePage.clickAt(500, 500);
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
const component = await workspacePage.layers
.getByTestId("layer-row")
@@ -339,11 +324,15 @@ test("User drag and drop a variant outside the container", async ({ page }) => {
const variant = await findVariant(workspacePage, 0);
// Drag and drop the variant
// FIXME: to make this test more resilient, we should get the bounding box of the Value 1 variant
// and use it to calculate the target position
await workspacePage.clickWithDragViewportAt(600, 500, 0, 300);
await workspacePage.clickWithDragViewportAt(350, 400, 0, 200);
await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible();
const component = await workspacePage.layers
.getByTestId("layer-row")
.filter({ has: workspacePage.page.getByText("Rectangle / Value 1") })
.filter({ has: workspacePage.page.getByTestId("icon-component") });
//The component exists and is visible
await expect(component).toBeVisible();
});
test("User cut paste a component inside a variant", async ({ page }) => {
@@ -356,14 +345,14 @@ test("User cut paste a component inside a variant", async ({ page }) => {
await workspacePage.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
//Cut the component
await workspacePage.cut("keyboard");
await workspacePage.page.keyboard.press("Control+x");
//Paste the component inside the variant
await variant.container.click();
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
const variant3 = await workspacePage.layers
.getByTestId("layer-row")
@@ -387,7 +376,7 @@ test("User cut paste a component with path inside a variant", async ({
await workspacePage.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
//Rename the component
await workspacePage.layers.getByText("Ellipse").dblclick();
@@ -398,11 +387,11 @@ test("User cut paste a component with path inside a variant", async ({
await workspacePage.page.keyboard.press("Enter");
//Cut the component
await workspacePage.cut("keyboard");
await workspacePage.page.keyboard.press("Control+x");
//Paste the component inside the variant
await variant.container.click();
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
const variant3 = await workspacePage.layers
.getByTestId("layer-row")
@@ -426,7 +415,7 @@ test("User drag and drop a component with path inside a variant", async ({
await workspacePage.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
//Rename the component
await workspacePage.layers.getByText("Ellipse").dblclick();
@@ -437,7 +426,7 @@ test("User drag and drop a component with path inside a variant", async ({
await workspacePage.page.keyboard.press("Enter");
//Drag and drop the component the component
await workspacePage.clickWithDragViewportAt(510, 510, 200, 0);
await workspacePage.clickWithDragViewportAt(510, 510, 0, -200);
const variant3 = await workspacePage.layers
.getByTestId("layer-row")
@@ -457,8 +446,8 @@ test("User cut paste a variant into another container", async ({ page }) => {
await workspacePage.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k");
await workspacePage.page.keyboard.press("Control+k");
const variantOrigin = await findVariantNoWait(workspacePage, 1);
@@ -468,11 +457,11 @@ test("User cut paste a variant into another container", async ({ page }) => {
await variantOrigin.variant1.click();
//Cut the variant
await workspacePage.cut("keyboard");
await workspacePage.page.keyboard.press("Control+x");
//Paste the variant
await workspacePage.layers.getByText("Ellipse").first().click();
await workspacePage.paste("keyboard");
await workspacePage.page.keyboard.press("Control+v");
const variant3 = workspacePage.layers
.getByTestId("layer-row")

View File

@@ -1,13 +1,13 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { WorkspacePage } from "../pages/WorkspacePage";
import { presenceFixture, joinFixture2, joinFixture3 } from "../../data/workspace/ws-notifications";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
});
test("User loads worskpace with empty file", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
@@ -16,7 +16,7 @@ test("User loads worskpace with empty file", async ({ page }) => {
});
test("User opens a file with a bad page id", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace({
@@ -29,7 +29,7 @@ test("User opens a file with a bad page id", async ({ page }) => {
test("User receives presence notifications updates in the workspace", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
@@ -43,7 +43,7 @@ test("User receives presence notifications updates in the workspace", async ({
test("BUG 13058 - Presence list shows up to 3 user avatars", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
@@ -63,7 +63,7 @@ test("BUG 13058 - Presence list shows up to 3 user avatars", async ({
});
test("User draws a rect", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"update-file?id=*",
@@ -74,12 +74,13 @@ test("User draws a rect", async ({ page }) => {
await workspacePage.rectShapeButton.click();
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
await workspacePage.hideUI();
await expect(workspacePage.canvas).toHaveScreenshot();
const shape = await workspacePage.rootShape.locator("rect");
await expect(shape).toHaveAttribute("width", "200");
await expect(shape).toHaveAttribute("height", "100");
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
@@ -95,14 +96,14 @@ test("User makes a group", async ({ page }) => {
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.page.keyboard.press("ControlOrMeta+g");
await workspacePage.page.keyboard.press("Control+g");
await workspacePage.expectSelectedLayer("Group");
});
test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
@@ -112,10 +113,10 @@ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
await workspacePage.expectHiddenToolbarOptions();
});
test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", async ({
test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
@@ -131,8 +132,8 @@ test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", asy
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
// Move created rect to a corner, in order to get scrollbars
await workspacePage.panOnViewportAt(128, 128, 600, 600);
// Move created rect to a corner, in orther to get scrollbars
await workspacePage.panOnViewportAt(128, 128, 300, 300);
// Check scrollbars appear
const horizontalScrollbar = workspacePage.horizontalScrollbar;
@@ -151,7 +152,7 @@ test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", asy
test("User adds a library and its automatically selected in the color palette", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"link-file-to-library",
@@ -196,7 +197,7 @@ test("User adds a library and its automatically selected in the color palette",
test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.moveButton.click();
@@ -239,7 +240,7 @@ test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette",
test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
@@ -256,7 +257,7 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.pageName.click();
@@ -266,7 +267,7 @@ test("Bug 8784 - Use keyboard arrow to move inside a text input does not change
});
test("Bug 9066 - Problem with grid layout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
@@ -294,7 +295,7 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => {
});
test("User have toolbar", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
@@ -303,7 +304,7 @@ test("User have toolbar", async ({ page }) => {
});
test("User have edition menu entries", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
@@ -319,7 +320,7 @@ test("User have edition menu entries", async ({ page }) => {
});
test("Copy/paste properties", async ({ page, context }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(
/get\-file\?/,
@@ -385,23 +386,23 @@ test("Copy/paste properties", async ({ page, context }) => {
});
test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor"));
await workspacePage.viewport.click({ button: "right" });
await page.getByText(/^Paste/i).click();
await page.getByText("PasteCtrlV").click();
await workspacePage.viewport
.getByRole("textbox")
.getByText("Lorem ipsum dolor");
});
test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
page,
context,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json");
await workspacePage.goToWorkspace({
@@ -409,18 +410,16 @@ test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c",
});
const zoom = page.getByTitle("Zoom");
const zoom = await page.getByTitle("Zoom");
await zoom.click();
const zoomIn = page.getByRole("button", { name: "Zoom in" });
const zoomIn = await page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
await zoomIn.click();
await zoomIn.click();
// Zoom fit all
await page.keyboard.press("Shift+1");
// Select all shapes to display selrect
await workspacePage.page.keyboard.press("ControlOrMeta+a");
const ids = [
"shape-165d1e5a-5873-8010-8005-9ffdbeaeec59",
@@ -442,7 +441,7 @@ test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
const viewportBoundingBox = await workspacePage.viewport.boundingBox();
for (const id of ids) {
const shape = page.locator(`.viewport-selrect`);
const shape = await page.locator(`.ws-shape-wrapper > g#${id}`);
const shapeBoundingBox = await shape.boundingBox();
expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy();
}
@@ -451,7 +450,7 @@ test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
test("Bug 9877, user navigation to dashboard from header goes to blank page", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
@@ -468,7 +467,7 @@ test("Bug 9877, user navigation to dashboard from header goes to blank page", as
test("Bug 8371 - Flatten option is not visible in context menu", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-8371.json");
await workspacePage.goToWorkspace({

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

123
frontend/pnpm-lock.yaml generated
View File

@@ -28,9 +28,6 @@ importers:
'@penpot/text-editor':
specifier: workspace:./text-editor
version: link:text-editor
'@penpot/tokenscript':
specifier: workspace:./packages/tokenscript
version: link:packages/tokenscript
'@playwright/test':
specifier: 1.58.0
version: 1.58.0
@@ -251,12 +248,6 @@ importers:
packages/mousetrap: {}
packages/tokenscript:
dependencies:
'@tokens-studio/tokenscript-interpreter':
specifier: ^0.23.1
version: 0.23.1
text-editor:
devDependencies:
'@playwright/test':
@@ -308,12 +299,6 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@ark/schema@0.56.0':
resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==}
'@ark/util@0.56.0':
resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==}
'@asamuzakjp/css-color@4.1.1':
resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}
@@ -939,42 +924,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -1056,28 +1035,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
@@ -1174,145 +1149,121 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-gnueabihf@4.56.0':
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-gnu@4.56.0':
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-musl@4.56.0':
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-gnu@4.56.0':
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.56.0':
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.56.0':
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-musl@4.56.0':
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.56.0':
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.56.0':
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-x64-musl@4.56.0':
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.56.0':
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
@@ -1485,11 +1436,6 @@ packages:
peerDependencies:
style-dictionary: '>=4.3.0 < 6'
'@tokens-studio/tokenscript-interpreter@0.23.1':
resolution: {integrity: sha512-aIcJprCkHIyckl0Knn78Sn7ef3U3IXLjNv9MOePdNR0Mz3Z4PleerldtfLmr1DdXUXiroVSyJROyJrO3TfB2Gg==}
engines: {node: '>=16.0.0'}
hasBin: true
'@tokens-studio/types@0.5.2':
resolution: {integrity: sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==}
@@ -1728,12 +1674,6 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
arkregex@0.0.5:
resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==}
arktype@2.1.29:
resolution: {integrity: sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==}
array-buffer-byte-length@1.0.2:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'}
@@ -1842,9 +1782,6 @@ packages:
buffer-builder@0.2.0:
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -2015,10 +1952,6 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
@@ -3427,9 +3360,6 @@ packages:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3707,10 +3637,6 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
readline-sync@1.4.10:
resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==}
engines: {node: '>= 0.8.0'}
recast@0.23.11:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
@@ -3858,56 +3784,48 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: glibc
sass-embedded-linux-arm@1.97.1:
resolution: {integrity: sha512-48VxaTUApLyx1NXFdZhKqI/7FYLmz8Ju3Ki2V/p+mhn5raHgAiYeFgn8O1WGxTOh+hBb9y3FdSR5a8MNTbmKMQ==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: glibc
sass-embedded-linux-musl-arm64@1.97.1:
resolution: {integrity: sha512-kD35WSD9o0279Ptwid3Jnbovo1FYnuG2mayYk9z4ZI4mweXEK6vTu+tlvCE/MdF/zFKSj11qaxaH+uzXe2cO5A==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: musl
sass-embedded-linux-musl-arm@1.97.1:
resolution: {integrity: sha512-FUFs466t3PVViVOKY/60JgLLtl61Pf7OW+g5BeEfuqVcSvYUECVHeiYHtX1fT78PEVa0h9tHpM6XpWti+7WYFA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: musl
sass-embedded-linux-musl-riscv64@1.97.1:
resolution: {integrity: sha512-ZgpYps5YHuhA2+KiLkPukRbS5298QObgUhPll/gm5i0LOZleKCwrFELpVPcbhsSBuxqji2uaag5OL+n3JRBVVg==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: musl
sass-embedded-linux-musl-x64@1.97.1:
resolution: {integrity: sha512-wcAigOyyvZ6o1zVypWV7QLZqpOEVnlBqJr9MbpnRIm74qFTSbAEmShoh8yMXBymzuVSmEbThxAwW01/TLf62tA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: musl
sass-embedded-linux-riscv64@1.97.1:
resolution: {integrity: sha512-9j1qE1ZrLMuGb+LUmBzw93Z4TNfqlRkkxjPVZy6u5vIggeSfvGbte7eRoYBNWX6SFew/yBCL90KXIirWFSGrlQ==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: glibc
sass-embedded-linux-x64@1.97.1:
resolution: {integrity: sha512-7nrLFYMH/UgvEgXR5JxQJ6y9N4IJmnFnYoDxN0nw0jUp+CQWQL4EJ4RqAKTGelneueRbccvt2sEyPK+X0KJ9Jg==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: glibc
sass-embedded-unknown-all@1.97.1:
resolution: {integrity: sha512-oPSeKc7vS2dx3ZJHiUhHKcyqNq0GWzAiR8zMVpPd/kVMl5ZfVyw+5HTCxxWDBGkX02lNpou27JkeBPCaneYGAQ==}
@@ -4238,7 +4156,6 @@ packages:
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
@@ -4760,10 +4677,6 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yauzl@3.2.0:
resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==}
engines: {node: '>=12'}
yocto-queue@1.2.2:
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
engines: {node: '>=12.20'}
@@ -4782,12 +4695,6 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@ark/schema@0.56.0':
dependencies:
'@ark/util': 0.56.0
'@ark/util@0.56.0': {}
'@asamuzakjp/css-color@4.1.1':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -5701,13 +5608,6 @@ snapshots:
is-mergeable-object: 1.1.1
style-dictionary: 5.0.0-rc.1
'@tokens-studio/tokenscript-interpreter@0.23.1':
dependencies:
arktype: 2.1.29
commander: 14.0.2
readline-sync: 1.4.10
yauzl: 3.2.0
'@tokens-studio/types@0.5.2': {}
'@trysound/sax@0.2.0': {}
@@ -6001,16 +5901,6 @@ snapshots:
aria-query@5.3.2: {}
arkregex@0.0.5:
dependencies:
'@ark/util': 0.56.0
arktype@2.1.29:
dependencies:
'@ark/schema': 0.56.0
'@ark/util': 0.56.0
arkregex: 0.0.5
array-buffer-byte-length@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -6150,8 +6040,6 @@ snapshots:
buffer-builder@0.2.0: {}
buffer-crc32@0.2.13: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -6324,8 +6212,6 @@ snapshots:
commander@12.1.0: {}
commander@14.0.2: {}
commander@7.2.0: {}
component-emitter@2.0.0: {}
@@ -7849,8 +7735,6 @@ snapshots:
pathval@2.0.1: {}
pend@1.2.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -8145,8 +8029,6 @@ snapshots:
readdirp@4.1.2: {}
readline-sync@1.4.10: {}
recast@0.23.11:
dependencies:
ast-types: 0.16.1
@@ -9313,11 +9195,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yauzl@3.2.0:
dependencies:
buffer-crc32: 0.2.13
pend: 1.2.0
yocto-queue@1.2.2: {}
zod@3.25.76: {}

View File

@@ -6,5 +6,4 @@ shamefullyHoist: true
packages:
- "packages/draft-js"
- "packages/mousetrap"
- "packages/tokenscript"
- "text-editor"

View File

@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>WASM + WebGL2 Canvas</title>
<style>
body {
margin: 0;
background: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
} from './js/lib.js';
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const params = new URLSearchParams(document.location.search);
const shapes = params.get("shapes") || 1000;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._init_shapes_pool(shapes + 1);
setupInteraction(canvas);
const children = [];
for (let i = 0; i < shapes; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
Module._add_shape_center_stroke(10, 0, 0, 0);
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidStrokeFill(argb2);
// Add shadows
// Shadow 1: drop-shadow, #dedede opacity 0.33, blur 4, spread -2, offsetX 0, offsetY 2
Module._add_shape_shadow(hexToU32ARGB("#dedede", 0.33), 4, -2, 0, 2, 0, false);
// Shadow 2: drop-shadow, #dedede opacity 1, blur 12, spread -8, offsetX 0, offsetY 12
Module._add_shape_shadow(hexToU32ARGB("#dedede", 1), 12, -8, 0, 12, 0, false);
// Shadow 3: inner-shadow, #002046 opacity 0.12, blur 12, spread -8, offsetX 0, offsetY -4
Module._add_shape_shadow(hexToU32ARGB("#002046", 0.12), 12, -8, 0, -4, 1, false);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
performance.mark('render:begin');
Module._set_view(1, 0, 0);
Module._render(Date.now());
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
// alert(`render time: ${duration.toFixed(2)}ms`);
});
</script>
</body>
</html>

View File

@@ -44,20 +44,6 @@
(update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
(defn reset-plugin-flags
[id]
(ptk/reify ::reset-plugin-flags
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :plugin-flags] assoc id {}))))
(defn set-plugin-flag
[id key value]
(ptk/reify ::set-plugin-flag
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :plugin-flags id] assoc key value))))
(defn remove-current-plugin
[id]
(ptk/reify ::remove-current-plugin
@@ -68,9 +54,7 @@
(defn- load-plugin!
[{:keys [plugin-id name description host code icon permissions]}]
(try
(st/emit! (save-current-plugin plugin-id)
(reset-plugin-flags plugin-id))
(st/emit! (save-current-plugin plugin-id))
(.ɵloadPlugin
^js ug/global
#js {:pluginId plugin-id

View File

@@ -14,9 +14,7 @@
[app.common.time :as ct]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.warnings :as wtw]
[beicon.v2.core :as rx]
@@ -588,25 +586,22 @@
;; FIXME: this with effect with trigger all the time because
;; `config` will be always a different instance
(mf/with-effect [tokens config]
(when-not (contains? cf/flags :tokenscript)
(let [cached (get @cache-atom tokens)]
(cond
(nil? tokens) nil
;; The tokens are already processing somewhere
(rx/observable? cached) (rx/sub! cached #(reset! tokens-state %))
;; Get the cached entry
(some? cached) (reset! tokens-state cached)
;; No cached entry, start processing
:else (let [resolved-tokens-s (if interactive?
(resolve-tokens-interactive tokens)
(resolve-tokens tokens))]
(swap! cache-atom assoc tokens resolved-tokens-s)
(rx/sub! resolved-tokens-s (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens)
(reset! tokens-state resolved-tokens))))))))
(if (contains? cf/flags :tokenscript)
(ts/resolve-tokens tokens)
@tokens-state)))
(let [cached (get @cache-atom tokens)]
(cond
(nil? tokens) nil
;; The tokens are already processing somewhere
(rx/observable? cached) (rx/sub! cached #(reset! tokens-state %))
;; Get the cached entry
(some? cached) (reset! tokens-state cached)
;; No cached entry, start processing
:else (let [resolved-tokens-s (if interactive?
(resolve-tokens-interactive tokens)
(resolve-tokens tokens))]
(swap! cache-atom assoc tokens resolved-tokens-s)
(rx/sub! resolved-tokens-s (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens)
(reset! tokens-state resolved-tokens)))))))
@tokens-state))
(defn use-resolved-tokens*
"This hook will return the unresolved tokens as state until they are
@@ -617,19 +612,16 @@
[tokens & {:keys [interactive?]}]
(let [state* (mf/use-state tokens)]
(mf/with-effect [tokens interactive?]
(when-not (contains? cf/flags :tokenscript)
(if (seq tokens)
(let [tpoint (ct/tpoint-ms)
tokens-s (if interactive?
(resolve-tokens-interactive tokens)
(resolve-tokens tokens))]
(if (seq tokens)
(let [tpoint (ct/tpoint-ms)
tokens-s (if interactive?
(resolve-tokens-interactive tokens)
(resolve-tokens tokens))]
(-> tokens-s
(rx/sub! (fn [resolved-tokens]
(let [elapsed (tpoint)]
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
(reset! state* resolved-tokens))))))
(reset! state* tokens))))
(if (contains? cf/flags :tokenscript)
(ts/resolve-tokens tokens)
@state*)))
(-> tokens-s
(rx/sub! (fn [resolved-tokens]
(let [elapsed (tpoint)]
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
(reset! state* resolved-tokens))))))
(reset! state* tokens)))
@state*))

View File

@@ -1,164 +0,0 @@
(ns app.main.data.tokenscript
(:require
["@penpot/tokenscript" :refer [BaseSymbolType
ColorSymbol
ListSymbol NumberSymbol
NumberWithUnitSymbol
ProcessorError
processTokens
TokenSymbol
makeConfig]]
[app.common.logging :as l]
[app.common.time :as ct]
[app.main.data.workspace.tokens.errors :as wte]))
(l/set-level! :debug)
;; Config ----------------------------------------------------------------------
(def config (makeConfig))
;; Class predicates ------------------------------------------------------------
;; Predicates to get information about the tokenscript interpreter symbol type
;; Or to determine the error
(defn tokenscript-symbol? [v]
(instance? BaseSymbolType v))
(defn structured-token? [v]
(instance? TokenSymbol v))
(defn structured-record-token? [^js v]
(and (structured-token? v) (instance? js/Map (.-value v))))
(defn structured-array-token? [^js v]
(and (structured-token? v) (instance? js/Array (.-value v))))
(defn number-with-unit-symbol? [v]
(instance? NumberWithUnitSymbol v))
(defn number-symbol? [v]
(instance? NumberSymbol v))
(defn list-symbol? [v]
(instance? ListSymbol v))
(defn color-symbol? [v]
(instance? ColorSymbol v))
(defn processor-error? [err]
(instance? ProcessorError err))
;; Conversion Tools ------------------------------------------------------------
;; Helpers to convert tokenscript symbols to penpot accepted formats
(defn color-symbol->hex-string [^js v]
(when (color-symbol? v)
(.toString (.to v "hex"))))
(defn color-alpha [^js v]
(if (.isHex v)
1
(or (.getAttribute v "alpha") 1)))
(defn color-symbol->penpot-color [^js v]
{:color (color-symbol->hex-string v)
:opacity (color-alpha v)})
(defn rem-number-with-unit? [v]
(and (number-with-unit-symbol? v)
(= (.-unit v) "rem")))
(defn rem->px [^js v]
(* (.-value v) 16))
(declare tokenscript-symbols->penpot-unit)
(defn structured-token->penpot-map
"Converts structured token (record or array) to penpot map format.
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
[^js token-symbol]
(if (instance? js/Array (.-value token-symbol))
(mapv structured-token->penpot-map (.-value token-symbol))
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
(into {} (map (fn [[k v :as V]]
[(keyword k) (tokenscript-symbols->penpot-unit v)])
entries)))))
(defn tokenscript-symbols->penpot-unit [^js v]
(cond
(structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v))
(color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v)
:else (.-value v)))
;; Processors ------------------------------------------------------------------
;; The processor resolves tokens
;; resolved/error tokens get put back into a clojure structure directly during build time
;; For updating tokens we use the `TokenResolver` crud methods from the processing result
;; The `TokenResolver` has detailed information for each tokens dependency graph
(defn create-token-builder
"Collects resolved tokens during build time into a clojure structure.
Returns Tokenscript Symbols in `:resolved-value` key."
[tokens]
(let [output (volatile! tokens)
;; When a token is resolved (No parsing / reference errors) we assing `:resolved-value` for the original token
on-resolve
(fn [^js/String token-name ^js/Symbol resolved-symbol]
(vswap! output assoc-in [token-name :resolved-value] resolved-symbol))
;; When a token contains any errors we assing `:errors` for the original token
on-error
(fn [^js/String token-name ^js/Error _error ^js/String _original-value]
(let [value (get tokens token-name)
default-error [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]]
(vswap! output assoc-in [token-name :errors] default-error)))
;; Extract the atom value
get-result
(fn [] @output)]
#js {:onResolve on-resolve
:onError on-error
:getResult get-result}))
(defn clj->token->tokenscript-token
"Convert penpot token into a format that tokenscript can handle."
[{:keys [type value]}]
#js {"$type" (name type)
"$value" (clj->js value)})
(defn clj-tokens->tokenscript-tokens
"Convert penpot map of tokens into tokenscript map structure.
tokenscript accepts a map of [token-name {\"$type\": string, \"$value\": any}]"
[tokens]
(let [token-map (js/Map.)]
(doseq [[k token] tokens]
(.set token-map k (clj->token->tokenscript-token token)))
token-map))
(defn process-tokens
"Builds tokens using `tokenscript`."
[tokens]
(let [input (clj-tokens->tokenscript-tokens tokens)
result (processTokens input #js {:config config
:builder (create-token-builder tokens)})]
result))
(defn update-token
[tokens token]
(let [result (process-tokens tokens)
resolver (.-resolver result)]
(.updateToken resolver #js {:tokenPath (:name token)
:tokenData (clj->token->tokenscript-token token)})))
;; Main ------------------------------------------------------------------------
(defn resolve-tokens [tokens]
(let [tpoint (ct/tpoint-ms)
result (process-tokens tokens)
elapsed (tpoint)]
(l/dbg :hint "tokenscript/resolve-tokens" :elapsed elapsed)
(.-output result)))

View File

@@ -1410,7 +1410,6 @@
(dm/export dwt/start-move-selected)
(dm/export dwt/move-selected)
(dm/export dwt/update-position)
(dm/export dwt/update-positions)
(dm/export dwt/flip-horizontal-selected)
(dm/export dwt/flip-vertical-selected)
(dm/export dwly/set-opacity)

View File

@@ -46,9 +46,7 @@
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.data.workspace.zoom :as dwz]
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp]
@@ -1014,13 +1012,6 @@
updated-objects (pcb/get-objects changes)
new-children-ids (cfh/get-children-ids-with-self updated-objects (:id new-shape))
new-text-ids (->> new-children-ids
(keep (fn [id]
(when-let [child (get updated-objects id)]
(when (and (cfh/text-shape? child)
(not= :fixed (:grow-type child)))
id))))
(vec))
[changes parents-of-swapped]
(if keep-touched?
@@ -1030,9 +1021,6 @@
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(when (and (features/active-feature? state "render-wasm/v1")
(seq new-text-ids))
(dwwt/resize-wasm-text-all new-text-ids))
(ptk/data-event :layout/update {:ids update-layout-ids :undo-group undo-group})
(dwu/commit-undo-transaction undo-id)
(dws/select-shape (:id new-shape) false))))))

View File

@@ -712,7 +712,8 @@
(ctm/rotation-modifiers shape center angle))
modif-tree
(build-modif-tree ids objects get-modifier)
(-> (build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))
modifiers
(mapv (fn [[id {:keys [modifiers]}]]

View File

@@ -11,6 +11,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
@@ -28,10 +29,10 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.router :as rt]
[app.render-wasm.api :as wasm.api]
[app.util.text-editor :as ted]
[app.util.text.content.styles :as styles]
[app.util.timers :as ts]
@@ -51,6 +52,50 @@
(declare v2-update-text-shape-content)
(declare v2-update-text-editor-styles)
(defn resize-wasm-text-modifiers
([shape]
(resize-wasm-text-modifiers shape (:content shape)))
([{:keys [id points selrect grow-type] :as shape} content]
(wasm.api/use-shape id)
(wasm.api/set-shape-text-content id content)
(wasm.api/set-shape-text-images id content)
(let [dimension (wasm.api/get-text-dimensions)
width-scale (if (#{:fixed :auto-height} grow-type)
1.0
(/ (:width dimension) (:width selrect)))
height-scale (if (= :fixed grow-type)
1.0
(/ (:height dimension) (:height selrect)))
resize-v (gpt/point width-scale height-scale)
origin (first points)]
{id
{:modifiers
(ctm/resize-modifiers
resize-v
origin
(:transform shape (gmt/matrix))
(:transform-inverse shape (gmt/matrix)))}})))
(defn resize-wasm-text
[id]
(ptk/reify ::resize-wasm-text
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)]
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))))))
(defn resize-wasm-text-all
[ids]
(ptk/reify ::resize-wasm-text-all
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/from ids)
(rx/map resize-wasm-text)))))
;; -- Content helpers
(defn- v2-content-has-text?
@@ -133,7 +178,7 @@
{:undo-group (when new-shape? id)})
(dwm/apply-wasm-modifiers
(dwwt/resize-wasm-text-modifiers shape content)
(resize-wasm-text-modifiers shape content)
{:undo-group (when new-shape? id)})))))
(let [content (d/merge (ted/export-content content)
@@ -778,7 +823,7 @@
(when (features/active-feature? state "render-wasm/v1")
;; This delay is to give time for the font to be correctly rendered
;; in wasm.
(cond->> (rx/of (dwwt/resize-wasm-text id))
(cond->> (rx/of (resize-wasm-text id))
(contains? attrs :font-id)
(rx/delay 200)))))))
@@ -928,11 +973,11 @@
(if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers
(dwwt/resize-wasm-text-modifiers shape content)
(resize-wasm-text-modifiers shape content)
{:undo-group (when new-shape? id)})
(dwm/set-wasm-modifiers
(dwwt/resize-wasm-text-modifiers shape content)
(resize-wasm-text-modifiers shape content)
{:undo-group (when new-shape? id)})))
(when finalize?

View File

@@ -18,20 +18,18 @@
[app.common.types.tokens-lib :as ctob]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.notifications :as ntf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as wdc]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.store :as st]
@@ -306,7 +304,7 @@
(and affects-layout?
(features/active-feature? state "render-wasm/v1"))
(rx/merge
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
@@ -365,7 +363,7 @@
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn- create-font-family-text-attrs
[value]
@@ -442,7 +440,7 @@
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-font-weight
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
@@ -629,9 +627,7 @@
(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))
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
@@ -649,9 +645,6 @@
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/concat

View File

@@ -7,9 +7,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cft]
[app.config :as cf]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]))
[app.main.data.tinycolor :as tinycolor]))
(defn color-bullet-color [token-color-value]
(when-let [tc (tinycolor/valid-color token-color-value)]
@@ -19,8 +17,5 @@
(tinycolor/->hex-string tc))))
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
(if (contains? cf/flags :tokenscript)
(when (and resolved-value (ts/color-symbol? resolved-value))
(ts/color-symbol->penpot-color resolved-value))
(when (and resolved-value (cft/color-token? token))
(color-bullet-color resolved-value))))
(when (and resolved-value (cft/color-token? token))
(color-bullet-color resolved-value)))

View File

@@ -1,6 +1,5 @@
(ns app.main.data.workspace.tokens.format
(:require
[app.main.data.tokenscript :as ts]
[cuerdas.core :as str]))
(def category-dictionary
@@ -24,52 +23,14 @@
:color "Color"
:inset "Inner Shadow"})
(declare format-token-value)
(defn- format-map-entries
"Formats a sequence of [k v] entries into a formatted string."
[entries]
(->> entries
(map (fn [[k v]]
(str "- " (category-dictionary (keyword k)) ": " (format-token-value v))))
(str/join "\n")
(str "\n")))
(defn- format-structured-token
"Formats tokenscript Token"
[token-symbol]
(->> (.-value token-symbol)
(.entries)
(es6-iterator-seq)
(format-map-entries)))
(defn format-tokenscript-symbol
[^js tokenscript-symbol]
(cond
(ts/rem-number-with-unit? tokenscript-symbol)
(str (ts/rem->px tokenscript-symbol) "px")
(ts/color-symbol? tokenscript-symbol)
(ts/color-symbol->hex-string tokenscript-symbol)
(ts/structured-record-token? tokenscript-symbol)
(format-structured-token tokenscript-symbol)
(ts/structured-array-token? tokenscript-symbol)
(str/join "\n" (map format-tokenscript-symbol (.-value tokenscript-symbol)))
:else
(.toString tokenscript-symbol)))
(defn format-token-value
"Converts token value of any shape to a string."
[token-value]
(cond
(ts/tokenscript-symbol? token-value)
(format-tokenscript-symbol token-value)
(map? token-value)
(format-map-entries token-value)
(->> (map (fn [[k v]] (str "- " (category-dictionary k) ": " (format-token-value v))) token-value)
(str/join "\n")
(str "\n"))
(and (sequential? token-value) (every? map? token-value))
(str/join "\n" (map format-token-value token-value))

View File

@@ -62,52 +62,6 @@
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Toggle tree nodes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- remove-paths-recursively
[path paths]
(->> paths
(remove #(str/starts-with? % (str path)))
vec))
(defn add-path
[path paths]
(let [split-path (cpn/split-path path :separator ".")
partial-paths (->> split-path
(reduce
(fn [acc segment]
(let [new-acc (if (empty? acc)
segment
(str (last acc) "." segment))]
(conj acc new-acc)))
[]))]
(->> paths
(into partial-paths)
distinct
vec)))
(defn clear-tokens-paths
[]
(ptk/reify ::clear-tokens-paths
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-tokens :unfolded-token-paths] []))))
(defn toggle-token-path
[path]
(ptk/reify ::toggle-token-path
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-tokens :unfolded-token-paths]
(fn [paths]
(let [paths (or paths [])]
(if (some #(= % path) paths)
(remove-paths-recursively path paths)
(add-path path paths))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -291,9 +245,8 @@
(pcb/with-library-data data)
(clt/generate-toggle-token-set tlib name))]
(rx/of
(dch/commit-changes changes)
(dwtp/propagate-workspace-tokens))))))
(rx/of (dch/commit-changes changes)
(dwtp/propagate-workspace-tokens))))))
(defn toggle-token-set-group
[group-path]
@@ -304,7 +257,6 @@
changes (-> (pcb/empty-changes)
(pcb/with-library-data data)
(clt/generate-toggle-token-set-group (get-tokens-lib state) group-path))]
(rx/of
(dch/commit-changes changes)
(dwtp/propagate-workspace-tokens))))))
@@ -534,7 +486,35 @@
;; TOKEN UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn clean-tokens-paths
[]
(ptk/reify ::clean-tokens-paths
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-tokens :unfolded-token-paths] []))))
(defn toggle-token-path
[path]
(ptk/reify ::toggle-token-path
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-tokens :unfolded-token-paths]
(fn [paths]
(let [paths (or paths [])]
(if (some #(= % path) paths)
(vec (remove #(or (= % path)
(str/starts-with? % (str path ".")))
paths))
(let [split-path (cpn/split-path path :separator ".")
partial-paths (reduce
(fn [acc segment]
(let [new-acc (if (empty? acc)
segment
(str (last acc) "." segment))]
(conj acc new-acc)))
[]
split-path)]
(into paths partial-paths)))))))))
(defn assign-token-context-menu
[{:keys [position] :as params}]

View File

@@ -406,13 +406,13 @@
(ctm/change-property :grow-type new-grow-type)))
modifiers)))
modif-tree (dwm/build-modif-tree ids objects get-modifier)]
modif-tree
(-> (dwm/build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
(if (features/active-feature? state "render-wasm/v1")
(rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true}))
(let [modif-tree (gm/set-objects-modifiers modif-tree objects)]
(rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))))
(rx/of (dwm/apply-modifiers* objects modif-tree nil options))))))))
(defn change-orientation
"Change orientation of shapes, from the sidebar options form.
@@ -1050,20 +1050,6 @@
:ignore-touched (:ignore-touched options)
:ignore-snap-pixel true}))))))))
(defn update-positions
"Move multiple shapes to a new position."
([ids position] (update-positions ids position nil))
([ids position options]
(assert (every? uuid? ids)
"expected valid coll of uuids")
(assert (map? position) "expected a valid map for `position`")
(ptk/reify ::update-positions
ptk/WatchEvent
(watch [_ _ _]
(->> ids
(map (fn [id] (update-position id position options)))
(rx/from))))))
(defn position-shapes
[shapes]
(ptk/reify ::position-shapes

View File

@@ -1,72 +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.data.workspace.wasm-text
"Helpers/events to resize wasm text shapes without depending on workspace.texts.
This exists to avoid circular deps:
workspace.texts -> workspace.libraries -> workspace.texts"
(:require
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.types.modifiers :as ctm]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.modifiers :as dwm]
[app.render-wasm.api :as wasm.api]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn resize-wasm-text-modifiers
([shape]
(resize-wasm-text-modifiers shape (:content shape)))
([{:keys [id points selrect grow-type] :as shape} content]
(wasm.api/use-shape id)
(wasm.api/set-shape-text-content id content)
(wasm.api/set-shape-text-images id content)
(let [dimension (wasm.api/get-text-dimensions)
width-scale (if (#{:fixed :auto-height} grow-type)
1.0
(/ (:width dimension) (:width selrect)))
height-scale (if (= :fixed grow-type)
1.0
(/ (:height dimension) (:height selrect)))
resize-v (gpt/point width-scale height-scale)
origin (first points)]
{id
{:modifiers
(ctm/resize-modifiers
resize-v
origin
(:transform shape (gmt/matrix))
(:transform-inverse shape (gmt/matrix)))}})))
(defn resize-wasm-text
"Resize a single text shape (auto-width/auto-height) by id.
No-op if the id is not a text shape or is :fixed."
[id]
(ptk/reify ::resize-wasm-text
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)]
(if (and (some? shape)
(cfh/text-shape? shape)
(not= :fixed (:grow-type shape)))
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
(rx/empty))))))
(defn resize-wasm-text-all
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
[ids]
(ptk/reify ::resize-wasm-text-all
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/from ids)
(rx/map resize-wasm-text)))))

View File

@@ -9,16 +9,15 @@
(:require
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.config :as cf]
[app.common.schema :as sm]
[app.main.data.auth :as da]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.workspace :as-alias dw]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.worker]
[app.util.globals :as g]
[app.util.globals :as glob]
[app.util.i18n :refer [tr]]
[app.util.timers :as ts]
[cuerdas.core :as str]
@@ -33,6 +32,44 @@
;; Will contain last uncaught exception
(def last-exception nil)
(defn- print-data!
[data]
(-> data
(dissoc ::sm/explain)
(dissoc :explain)
(dissoc ::trace)
(dissoc ::instance)
(pp/pprint {:width 70})))
(defn- print-explain!
[data]
(when-let [{:keys [errors] :as explain} (::sm/explain data)]
(let [errors (mapv #(update % :schema sm/form) errors)]
(pp/pprint errors {:width 100 :level 15 :length 20})))
(when-let [explain (:explain data)]
(js/console.log explain)))
(defn- print-trace!
[data]
(some-> data ::trace js/console.log))
(defn- print-group!
[message f]
(try
(js/console.group message)
(f)
(catch :default _ nil)
(finally
(js/console.groupEnd message))))
(defn print-cause!
[message cause]
(print-group! message (fn []
(print-data! cause)
(print-explain! cause)
(print-trace! cause))))
(defn exception->error-data
[cause]
(let [data (ex-data cause)]
@@ -41,6 +78,18 @@
(assoc ::instance cause)
(assoc ::trace (.-stack cause)))))
(defn print-error!
[cause]
(cond
(map? cause)
(print-cause! (:hint cause "Unexpected Error") cause)
(ex/error? cause)
(print-cause! (ex-message cause) (ex-data cause))
:else
(print-cause! (ex-message cause) (exception->error-data cause))))
(defn on-error
"A general purpose error handler."
[error]
@@ -55,66 +104,13 @@
;; Set the main potok error handler
(reset! st/on-error on-error)
(defn generate-report
[cause]
(try
(let [team-id (:current-team-id @st/state)
file-id (:current-file-id @st/state)
profile-id (:profile-id @st/state)
data (ex-data cause)]
(with-out-str
(println "Context:")
(println "--------------------")
(println "Hint: " (or (:hint data) (ex-message cause) "--"))
(println "Prof ID: " (str (or profile-id "--")))
(println "Team ID: " (str (or team-id "--")))
(when-let [file-id (or (:file-id data) file-id)]
(println "File ID: " (str file-id)))
(println "Version: " (:full cf/version))
(println "URI: " (str cf/public-uri))
(println "HREF: " (rt/get-current-href))
(println)
(println
(ex/format-throwable cause))
(println)
(println "Last events:")
(println "--------------------")
(pp/pprint @st/last-events {:length 200})
(println)))
(catch :default cause
(.error js/console "error on generating report" cause)
nil)))
(defn- show-not-blocking-error
"Show a non user blocking error notification"
[cause]
(let [data (ex-data cause)
hint (or (some-> (:hint data) ex/first-line)
(ex-message cause))]
(st/emit!
(ev/event {::ev/name "unhandled-exception"
:hint hint
:href (rt/get-current-href)
:type (get data :type :unknown)
:report (generate-report cause)})
(ntf/show {:content (tr "errors.unexpected-exception" hint)
:type :toast
:level :error
:timeout 3000}))))
(defmethod ptk/handle-error :default
[error]
(if (and (string? (:hint error))
(str/starts-with? (:hint error) "Assert failed:"))
(ptk/handle-error (assoc error :type :assertion))
(when-let [cause (::instance error)]
(ex/print-throwable cause :prefix "Unexpected Error")
(show-not-blocking-error cause))))
(st/async-emit! (rt/assign-exception error))
(print-group! "Unhandled Error"
(fn []
(print-trace! error)
(print-data! error))))
;; We receive a explicit authentication error; If the uri is for
;; workspace, dashboard, viewer or settings, then assign the exception
@@ -146,10 +142,10 @@
(defmethod ptk/handle-error :validation
[{:keys [code] :as error}]
(when-let [instance (get error ::instance)]
(ex/print-throwable instance :prefix "Validation Error"))
(print-group! "Validation Error"
(fn []
(print-data! error)
(print-explain! error)))
(cond
(= code :invalid-paste-data)
(let [message (tr "errors.paste-data-validation")]
@@ -197,14 +193,23 @@
:else
(st/async-emit! (rt/assign-exception error))))
;; This is a pure frontend error that can be caused by an active
;; assertion (assertion that is preserved on production builds). From
;; the user perspective this should be treated as internal error.
(defmethod ptk/handle-error :assertion
[error]
(when-let [cause (::instance error)]
(show-not-blocking-error cause)
(ex/print-throwable cause :prefix "Assertion Error")))
(ts/schedule
#(st/emit! (ntf/show {:content (tr "errors.internal-assertion-error")
:type :toast
:level :error
:timeout 3000})))
(print-group! "Internal Assertion Error"
(fn []
(print-trace! error)
(print-data! error)
(print-explain! error))))
;; ;; All the errors that happens on worker are handled here.
(defmethod ptk/handle-error :worker-error
@@ -216,8 +221,9 @@
:level :error
:timeout 3000})))
(some-> (::instance error)
(ex/print-throwable :prefix "Web Worker Error")))
(print-group! "Internal Worker Error"
(fn []
(print-data! error))))
;; Error on parsing an SVG
(defmethod ptk/handle-error :svg-parser
@@ -246,9 +252,11 @@
(defmethod ptk/handle-error ::exceptional-state
[error]
(when-let [instance (get error ::instance)]
(ex/print-throwable instance :prefix "Exceptional State"))
(ts/schedule #(st/emit! (rt/assign-exception error))))
(when-let [cause (::instance error)]
(js/console.log (.-stack cause)))
(ts/schedule
#(st/emit! (rt/assign-exception error))))
(defn- redirect-to-dashboard
[]
@@ -256,7 +264,7 @@
project-id (:current-project-id @st/state)]
(if (and project-id team-id)
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
(set! (.-href g/location) ""))))
(set! (.-href glob/location) ""))))
(defmethod ptk/handle-error :restriction
[{:keys [code] :as error}]
@@ -304,10 +312,9 @@
:text (tr "errors.deprecated.contact.text")
:after (tr "errors.deprecated.contact.after")
:on-click #(st/emit! (rt/nav :settings-feedback))}}))
:else
(when-let [cause (::instance error)]
(ex/print-throwable cause :prefix "Restriction Error")
(show-not-blocking-error cause))))
(print-cause! "Restriction Error" error)))
;; This happens when the backed server fails to process the
;; request. This can be caused by an internal assertion or any other
@@ -315,9 +322,24 @@
(defmethod ptk/handle-error :server-error
[error]
(when-let [instance (get error ::instance)]
(ex/print-throwable instance :prefix "Server Error"))
(st/async-emit! (rt/assign-exception error)))
(st/async-emit! (rt/assign-exception error))
(print-group! "Server Error"
(fn []
(print-data! (dissoc error :data))
(when-let [werror (:data error)]
(cond
(= :assertion (:type werror))
(print-group! "Assertion Error"
(fn []
(print-data! werror)
(print-explain! werror)))
:else
(print-group! "Unexpected"
(fn []
(print-data! werror)
(print-explain! werror))))))))
(defonce uncaught-error-handler
(letfn [(is-ignorable-exception? [cause]
@@ -332,19 +354,28 @@
(when-let [cause (unchecked-get event "error")]
(set! last-exception cause)
(when-not (is-ignorable-exception? cause)
(ex/print-throwable cause :prefix "Uncaught Exception")
(ts/schedule #(show-not-blocking-error cause)))))
(ex/print-throwable cause :prefix "uncaught exception")
(st/async-emit!
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
:type :toast
:level :error
:timeout 3000})))))
(on-unhandled-rejection [event]
(.preventDefault ^js event)
(when-let [cause (unchecked-get event "reason")]
(set! last-exception cause)
(ex/print-throwable cause :prefix "Uncaught Rejection")
(ts/schedule #(show-not-blocking-error cause))))]
(ex/print-throwable cause :prefix "uncaught rejection")
(st/async-emit!
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
:type :toast
:level :error
:timeout 3000}))))]
(.addEventListener g/window "error" on-unhandled-error)
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
(.addEventListener glob/window "error" on-unhandled-error)
(.addEventListener glob/window "unhandledrejection" on-unhandled-rejection)
(fn []
(.removeEventListener g/window "error" on-unhandled-error)
(.removeEventListener g/window "unhandledrejection" on-unhandled-rejection))))
(.removeEventListener glob/window "error" on-unhandled-error)
(.removeEventListener glob/window "unhandledrejection" on-unhandled-rejection))))

View File

@@ -9,12 +9,12 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
[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.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.ds.product.loader :refer [loader*]]
@@ -360,7 +360,7 @@
on-error
(fn [cause]
(reset! status* :error)
(ex/print-throwable cause)
(errors/print-error! cause)
(rx/of (modal/hide)
(ntf/error (tr "dashboard.libraries-and-templates.import-error"))))

View File

@@ -36,12 +36,10 @@
(defn- hide-popover
[node]
(when (and (some? node)
(fn? (.-hidePopover node)))
(dom/unset-css-property! node "block-size")
(dom/unset-css-property! node "inset-block-start")
(dom/unset-css-property! node "inset-inline-start")
(.hidePopover ^js node)))
(dom/unset-css-property! node "block-size")
(dom/unset-css-property! node "inset-block-start")
(dom/unset-css-property! node "inset-inline-start")
(.hidePopover ^js node))
(defn- calculate-placement-bounding-rect
"Given a placement, calcultates the bounding rect for it taking in

View File

@@ -16,7 +16,7 @@
(def context (mf/create-context nil))
(mf/defc form-input*
[{:keys [name trim] :rest props}]
[{:keys [name] :rest props}]
(let [form (mf/use-ctx context)
input-name name
@@ -33,7 +33,7 @@
(mf/deps input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value trim))))
(fm/on-input-change form input-name value true))))
props
(mf/spread-props props {:on-change on-change

View File

@@ -21,7 +21,7 @@
(def ^:private schema:properties-row
[:map
[:term :string]
[:detail {:optional true} [:maybe :string]]
[:detail :string]
[:property {:optional true} :string] ;; CSS valid property
[:token {:optional true} :any] ;; resolved token object
[:copiable {:optional true} :boolean]])

View File

@@ -36,6 +36,7 @@
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
;; FIXME: this is a workaround until we export this class on beicon library
@@ -446,24 +447,25 @@
(rx/of default)
(rx/throw cause)))))))
(mf/defc exception-section*
{::mf/private true}
[{:keys [data route] :as props}]
(let [type (get data :type)
report (mf/with-memo [data]
(some-> data ::errors/instance errors/generate-report))
(generate-report data))
props (mf/spread-props props {:report report})]
(mf/with-effect [data route report]
(let [params (:query-params route)
params (u/map->query-string params)]
(st/emit! (ev/event {::ev/name "exception-page"
:type (get data :type :unknown)
:href (rt/get-current-href)
:hint (get data :hint)
:path (get route :path)
:report report
:params params}))))
(st/emit! (ptk/data-event ::ev/event
{::ev/name "exception-page"
:type (get data :type :unknown)
:hint (get data :hint)
:path (get route :path)
:report report
:params params}))))
(case type
:not-found

View File

@@ -78,15 +78,13 @@
(fn []
(close-modals)
;; FIXME: move set-mode to uri?
(st/emit! :interrupt
(dw/set-options-mode :design)
(st/emit! (dw/set-options-mode :design)
(dcm/go-to-dashboard-recent))))
nav-to-project
(mf/use-fn
(mf/deps project-id)
#(st/emit! :interrupt
(dcm/go-to-dashboard-files ::rt/new-window true :project-id project-id)))]
#(st/emit! (dcm/go-to-dashboard-files ::rt/new-window true :project-id project-id)))]
(mf/with-effect [editing?]
(when ^boolean editing?

View File

@@ -401,8 +401,7 @@
(dm/fmt "scale(%)" maybe-zoom))}))]
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
:transform (dm/str transform)
:data-testid "text-editor"}
:transform (dm/str transform)}
[:defs
[:clipPath {:id clip-id}
[:rect {:x x :y y :width width :height height}]]]

View File

@@ -9,15 +9,10 @@
(:require
[app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.constants :refer [left-sidebar-default-max-width
left-sidebar-default-width
right-sidebar-default-max-width
right-sidebar-default-width]]
[app.main.constants :refer [right-sidebar-default-width right-sidebar-default-max-width left-sidebar-default-max-width left-sidebar-default-width]]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace :as dw]
[app.main.features :as features]
[app.main.refs :as refs]
@@ -374,12 +369,6 @@
(ctob/get-tokens-in-active-sets tokens-lib)
{}))
tokenscript? (contains? cf/flags :tokenscript)
tokenscript-resolved-active-tokens
(mf/with-memo [tokens-lib tokenscript?]
(when tokenscript? (ts/resolve-tokens active-tokens)))
resolved-active-tokens
(sd/use-resolved-tokens* active-tokens)]
@@ -391,9 +380,7 @@
:page-id page-id
:tokens-lib tokens-lib
:active-tokens active-tokens
:resolved-active-tokens (if (contains? cf/flags :tokenscript)
tokenscript-resolved-active-tokens
resolved-active-tokens)}])
:resolved-active-tokens resolved-active-tokens}])
[:> right-sidebar* {:section section
:selected selected
:drawing-tool drawing-tool

View File

@@ -119,8 +119,7 @@
[:button {:class (stl/css-case
:toggle-content true
:inverse expanded?)
:data-testid "toggle-content"
:aria-expanded expanded?
:aria-label "Toggle layer"
:on-click on-toggle-collapse}
deprecated-icon/arrow])
@@ -300,7 +299,7 @@
on-drop
(mf/use-fn
(mf/deps id index objects expanded? selected)
(mf/deps id objects expanded? selected)
(fn [side _data]
(let [single? (= (count selected) 1)
same? (and single? (= (first selected) id))]
@@ -321,14 +320,18 @@
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
parent (get objects parent-id)
parent (get objects parent-id)
current-index (d/index-of (:shapes parent) id)
to-index (cond
(= side :center) 0
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(= side :top) (inc index)
:else index)]
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))))
;; target not found in parent (while lazy loading)
(neg? current-index) nil
(= side :top) (inc current-index)
:else current-index)]
(when (some? to-index)
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
on-hold
(mf/use-fn
@@ -417,11 +420,7 @@
current @children-count*
new-count (min total (max current chunk-size min-count))]
(reset! children-count* new-count))
(reset! children-count* 0)))
(fn []
(when-let [obs ^js @observer-var]
(.disconnect obs)
(reset! observer-var nil))))
(reset! children-count* 0))))
;; Re-observe sentinel whenever children-count changes (sentinel moves)
;; and (shapes item) to reconnect observer after shape changes
@@ -502,4 +501,4 @@
:component-child? component-tree?}])))
(when (< children-count (count (:shapes item)))
[:div {:ref lazy-ref
:style {:min-height 1}}])])]))
:class (stl/css :lazy-load-sentinel)}])])]))

View File

@@ -298,3 +298,7 @@
.filtered {
min-inline-size: $sz-12;
}
.lazy-load-sentinel {
min-height: 1px;
pointer-events: none;
}

View File

@@ -108,7 +108,6 @@
:on-blur accept-edit
:on-key-down on-key-down
:auto-focus true
:id (dm/str "layer-name-" shape-id)
:default-value (d/nilv default-value "")}]
[:*
[:span
@@ -119,7 +118,6 @@
:hidden is-hidden
:type-comp type-comp
:type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size}
:ref ref
:on-double-click start-edit}

View File

@@ -521,8 +521,7 @@
[:& filters-tree {:objects filtered-objects
:key (dm/str (:id page))
:parent-size size-parent}]
[:div {:ref lazy-load-ref
:style {:min-height 16}}]]
[:div {:ref lazy-load-ref}]]
[:div {:on-scroll on-scroll
:class (stl/css :tool-window-content)
:data-scroll-container true

View File

@@ -283,7 +283,7 @@
(if (or (string? value) (number? value))
(do
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/update-positions ids {attr value})))
(st/emit! (udw/update-position ids {attr value})))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwta/toggle-token {:token (first value)
:attrs #{attr}

View File

@@ -15,7 +15,6 @@
[app.main.data.workspace.shortcuts :as sc]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -142,7 +141,7 @@
(dwsh/update-shapes ids #(assoc % :grow-type grow-type)))
(when (features/active-feature? @st/state "render-wasm/v1")
(st/emit! (dwwt/resize-wasm-text-all ids)))
(st/emit! (dwt/resize-wasm-text-all ids)))
;; We asynchronously commit so every sychronous event is resolved first and inside the transaction
(ts/schedule #(st/emit! (dwu/commit-undo-transaction uid))))
(when (some? on-blur)

View File

@@ -309,7 +309,7 @@
on-remove'
(mf/use-fn
(mf/deps index on-remove)
(mf/deps index)
(fn [_]
(when on-remove
(on-remove index))))

View File

@@ -94,7 +94,10 @@
;; This only checks for the currently explicitly selected set
;; id, it is ephimeral and can be nil
;; FIXME: this is a repeated deref for the same `:workspace-tokens` state
selected-token-set-id (mf/deref refs/selected-token-set-id)
selected-token-set-id
(mf/deref refs/selected-token-set-id)
;; If we have not selected any set explicitly we just
;; select the first one from the list of sets
@@ -109,7 +112,6 @@
tokens
(sd/use-resolved-tokens* tokens)
;; Group tokens by their type
tokens-by-type
(mf/with-memo [tokens selected-token-set-tokens]
(let [tokens (reduce-kv (fn [tokens k _]
@@ -127,9 +129,9 @@
;; Filter tokens by their path and return their ids
filter-tokens-by-path-ids
(mf/use-fn
(mf/deps selected-token-set-tokens)
(mf/deps tokens)
(fn [type path]
(->> selected-token-set-tokens
(->> tokens
(filter (fn [token]
(let [[_ token-value] token]
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
@@ -137,47 +139,12 @@
(let [[_ token-value] token]
(:id token-value)))))))
remaining-tokens-of-type-in-set?
(mf/use-fn
(fn [selected-token-set-tokens tokens-in-path-ids]
(let [token-ids (set tokens-in-path-ids)
remaining-tokens (filter (fn [token]
(not (contains? token-ids (:id token))))
selected-token-set-tokens)
_ (prn "Remaining tokens:" remaining-tokens)]
(seq remaining-tokens))))
delete-token
(mf/with-memo [selected-token-set-tokens selected-token-set-id]
(fn [token]
(let [id (:id token)
type (:type token)
path (:name token)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
tokens-in-path-ids (filter-tokens-by-path-ids type path)
remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)]
;; Delete the token
(st/emit! (dwtl/delete-token selected-token-set-id id))
;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
(st/emit! (dwtl/toggle-token-path (name type)))))))
delete-node
(mf/with-memo [selected-token-set-tokens selected-token-set-id]
(mf/with-memo [tokens selected-token-set-id]
(fn [node type]
(let [path (:path node)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
tokens-in-path-ids (filter-tokens-by-path-ids type path)
remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)]
;; Delete tokens in path
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids))
;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
(st/emit! (dwtl/toggle-token-path (name type)))))))]
tokens-in-path-ids (filter-tokens-by-path-ids type path)]
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)))))]
(mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib
@@ -190,7 +157,7 @@
(st/emit! (dwtl/set-selected-token-set-id (ctob/get-id match)))))))
[:*
[:& token-context-menu {:on-delete-token delete-token}]
[:& token-context-menu]
[:> token-node-context-menu* {:on-delete-node delete-node}]
[:> selected-set-info* {:tokens-lib tokens-lib

View File

@@ -316,9 +316,8 @@
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape dwta/update-shape-position)))
(clean-separators)))}))
(defn default-actions [{:keys [token selected-token-set-id on-delete-token]}]
(let [{:keys [modal]} (dwta/get-token-properties token)
on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))]
(defn default-actions [{:keys [token selected-token-set-id]}]
(let [{:keys [modal]} (dwta/get-token-properties token)]
[{:title (tr "workspace.tokens.edit")
:no-selectable true
:action (fn [event]
@@ -334,10 +333,12 @@
:token token}))))}
{:title (tr "workspace.tokens.duplicate")
:no-selectable true
:action on-duplicate-token}
:action #(st/emit! (dwtl/duplicate-token (:id token)))}
{:title (tr "workspace.tokens.delete")
:no-selectable true
:action #(on-delete-token token)}]))
:action #(st/emit! (dwtl/delete-token
selected-token-set-id
(:id token)))}]))
(defn- allowed-shape-attributes [shapes]
(reduce into #{} (map #(ctt/shape-type->attributes (:type %) (:layout %)) shapes)))
@@ -463,7 +464,7 @@
:selected? selected?}])])))
(mf/defc token-context-menu-tree
[{:keys [width errors on-delete-token] :as mdata}]
[{:keys [width errors] :as mdata}]
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
@@ -487,11 +488,10 @@
:errors errors
:selected-token-set-id selected-token-set-id
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:on-delete-token on-delete-token}]]))
:is-selected-inside-layout is-selected-inside-layout}]]))
(mf/defc token-context-menu
[{:keys [on-delete-token]}]
[]
(let [mdata (mf/deref tokens-menu-ref)
is-open? (boolean mdata)
width (mf/use-state 0)
@@ -538,5 +538,5 @@
:left (dm/str left "px")}
:on-context-menu prevent-default}
(when mdata
[:& token-context-menu-tree (assoc mdata :width @width :on-delete-token on-delete-token)])]])
[:& token-context-menu-tree (assoc mdata :width @width)])]])
(dom/get-body)))))

View File

@@ -11,7 +11,6 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.color :as cl]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
@@ -52,15 +51,12 @@
;; Both variants provide identical color-picker and text-input behavior, but
;; differ in how they persist the value within the forms nested structure.
(defn- resolve-value
[tokens prev-token token-name value]
(let [valid-token-name?
(and (string? token-name)
(re-matches cto/token-name-validation-regex token-name))
token
(let [token
{:value value
:name (if (or (not valid-token-name?) (str/blank? token-name))
:name (if (str/blank? token-name)
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
token-name)}

View File

@@ -50,13 +50,9 @@
(defn- resolve-value
[tokens prev-token token-name value]
(let [valid-token-name?
(and (string? token-name)
(re-matches cto/token-name-validation-regex token-name))
token
(let [token
{:value (cto/split-font-family value)
:name (if (or (not valid-token-name?) (str/blank? token-name))
:name (if (str/blank? token-name)
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
token-name)}

View File

@@ -8,12 +8,8 @@
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.ui.ds.controls.input :as ds]
[app.main.ui.forms :as fc]
@@ -142,27 +138,11 @@
;; -----------------------------------------------------------------------------
(defn- resolve-value-tokenscript
[tokens prev-token value]
(let [result (ts/update-token tokens (assoc prev-token :value value))
token-result (.-resolved result)]
(rx/of
(cond
(ts/processor-error? token-result) {:error (wte/error-with-value :error.style-dictionary/missing-reference (some->> (.-dependencyChain token-result)
(seq)
(rest)))}
(instance? js/Error token-result) {:error (wte/error-with-value :error.style-dictionary/invalid-token-value value)}
:else {:value token-result}))))
(defn- resolve-value
[tokens prev-token token-name value]
(let [valid-token-name?
(and (string? token-name)
(re-matches cto/token-name-validation-regex token-name))
token
(let [token
{:value value
:name (if (or (not valid-token-name?) (str/blank? token-name))
:name (if (str/blank? token-name)
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
token-name)}
tokens
@@ -231,10 +211,7 @@
(mf/with-effect [resolve-stream tokens token input-name token-name]
(let [resolve-value (if (contains? cf/flags :tokenscript)
resolve-value-tokenscript
resolve-value)
subs (->> resolve-stream
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token token-name))
(rx/map (fn [result]

View File

@@ -25,7 +25,6 @@
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
@@ -98,12 +97,12 @@
value-subfield
input-value-placeholder] :as props}]
(let [make-schema (or make-schema default-make-schema)
(let [make-schema (or make-schema default-make-schema)
input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token)
validate-token (or validator default-validate-token)
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
token
(mf/with-memo [token]
@@ -143,10 +142,6 @@
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-toggle-tab
(mf/use-fn
(mf/deps form)
@@ -275,13 +270,7 @@
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:trim true
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
:auto-focus true}]]
[:div {:class (stl/css :input-row)}
(case type

View File

@@ -13,7 +13,6 @@
[app.common.files.tokens :as cft]
[app.common.path-names :as cpn]
[app.common.types.token :as ctt]
[app.config :as cf]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.color :as dwtc]
[app.main.data.workspace.tokens.format :as dwtf]
@@ -178,8 +177,6 @@
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
resolved-token (get active-theme-tokens (:name token))
has-selected? (pos? (count selected-shapes))
is-reference? (cft/is-reference? token)
contains-path? (str/includes? name ".")
@@ -212,10 +209,8 @@
is-viewer? (not can-edit?)
ref-not-in-active-set
(if (contains? cf/flags :tokenscript)
(seq (:errors resolved-token))
(and is-reference?
(not (contains-reference-value? value active-theme-tokens))))
(and is-reference?
(not (contains-reference-value? value active-theme-tokens)))
no-valid-value (seq errors)
@@ -225,8 +220,9 @@
color
(when (cft/color-token? token)
(or (dwtc/resolved-token-bullet-color resolved-token)
(dwtc/resolved-token-bullet-color token)))
(let [theme-token (get active-theme-tokens name)]
(or (dwtc/resolved-token-bullet-color theme-token)
(dwtc/resolved-token-bullet-color token))))
status-icon? (contains? token-types-with-status-icon type)

View File

@@ -16,7 +16,6 @@
[rumext.v2 :as mf]))
(defn- on-select-token-set-click [id]
(st/emit! (dwtl/clear-tokens-paths))
(st/emit! (dwtl/set-selected-token-set-id id)))
(defn- on-toggle-token-set-click [name]

View File

@@ -32,7 +32,6 @@
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
[app.plugins.events :as events]
[app.plugins.file :as file]
[app.plugins.flags :as flags]
[app.plugins.fonts :as fonts]
[app.plugins.format :as format]
[app.plugins.history :as history]
@@ -41,7 +40,6 @@
[app.plugins.page :as page]
[app.plugins.parser :as parser]
[app.plugins.shape :as shape]
[app.plugins.system-events :as se]
[app.plugins.user :as user]
[app.plugins.utils :as u]
[app.plugins.viewport :as viewport]
@@ -67,10 +65,7 @@
(cb/with-objects (:objects page))
(cb/add-object shape))]
(st/emit!
(ch/commit-changes changes)
(se/event plugin-id "create-shape" :type type))
(st/emit! (ch/commit-changes changes))
(shape/shape-proxy plugin-id (:id shape))))
(defn create-context
@@ -129,9 +124,6 @@
:fonts
{:get (fn [] (fonts/fonts-subcontext plugin-id))}
:flags
{:get (fn [] (flags/flags-proxy plugin-id))}
:library
{:get (fn [] (library/library-subcontext plugin-id))}
@@ -293,8 +285,7 @@
page-id (:current-page-id @st/state)
id (uuid/next)
ids (into #{} (map #(obj/get % "$id")) shapes)]
(st/emit! (dwg/group-shapes id ids)
(se/event plugin-id "create-shape" :type type))
(st/emit! (dwg/group-shapes id ids))
(shape/shape-proxy plugin-id file-id page-id id))))
:ungroup
@@ -336,8 +327,7 @@
(cb/with-objects (:objects page))
(cb/add-object shape))]
(st/emit! (ch/commit-changes changes)
(se/event plugin-id "create-shape" :type :path))
(st/emit! (ch/commit-changes changes))
(shape/shape-proxy plugin-id (:id shape))))
:createText
@@ -358,9 +348,7 @@
(cb/with-objects (:objects page))
(cb/add-object shape))]
(st/emit!
(ch/commit-changes changes)
(se/event plugin-id "create-shape" :type :text))
(st/emit! (ch/commit-changes changes))
(shape/shape-proxy plugin-id (:id shape)))))
:createShapeFromSvg
@@ -373,8 +361,7 @@
(let [id (uuid/next)
file-id (:current-file-id @st/state)
page-id (:current-page-id @st/state)]
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0))
(se/event plugin-id "create-shape" :type :svg))
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0)))
(shape/shape-proxy plugin-id file-id page-id id))))
:createShapeFromSvgWithImages
@@ -394,8 +381,7 @@
(st/emit! (dwm/create-svg-shape-with-images
file-id id "svg" svg-string (gpt/point 0 0)
#(resolve (shape/shape-proxy plugin-id file-id page-id id))
reject)
(se/event plugin-id "create-shape" :type :text)))))))
reject)))))))
:createBoolean
(fn [bool-type shapes]
@@ -410,8 +396,7 @@
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
shape-id (uuid/next)]
(st/emit! (dwb/create-bool bool-type :ids ids :force-shape-id shape-id)
(se/event plugin-id "create-shape" :type :boolean))
(st/emit! (dwb/create-bool bool-type :ids ids :force-shape-id shape-id))
(shape/shape-proxy plugin-id shape-id)))))
:generateMarkup

View File

@@ -1,33 +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.plugins.flags
(:require
[app.main.data.plugins :as dp]
[app.main.store :as st]
[app.plugins.utils :as u]
[app.util.object :as obj]))
(defn flags-proxy
[plugin-id]
(obj/reify {:name "FlagProxy"}
:naturalChildOrdering
{:this false
:get
(fn []
(boolean
(get-in
@st/state
[:workspace-local :plugin-flags plugin-id :natural-child-ordering])))
:set
(fn [value]
(cond
(not (boolean? value))
(u/display-not-valid :naturalChildOrdering value)
:else
(st/emit! (dp/set-plugin-flag plugin-id :natural-child-ordering value))))}))

View File

@@ -10,7 +10,6 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.types.plugins :as ctp]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.main.store :as st]
@@ -51,13 +50,7 @@
(contains? permissions "comment:write")
(conj "comment:read"))
plugin-url
(u/uri plugin-url)
origin
(-> plugin-url
(u/join ".")
(str))
origin (obj/get (js/URL. plugin-url) "origin")
prev-plugin
(->> (:data @registry)
@@ -66,13 +59,12 @@
(and (= name (:name plugin))
(= origin (:host plugin))))))
plugin-id
(d/nilv (:plugin-id prev-plugin) (str (uuid/next)))
plugin-id (d/nilv (:plugin-id prev-plugin) (str (uuid/next)))
manifest
(d/without-nils
{:plugin-id plugin-id
:url (str plugin-url)
:url plugin-url
:name name
:description desc
:host origin

View File

@@ -52,7 +52,6 @@
[app.plugins.parser :as parser]
[app.plugins.register :as r]
[app.plugins.ruler-guides :as rg]
[app.plugins.state :refer [natural-child-ordering?]]
[app.plugins.text :as text]
[app.plugins.utils :as u]
[app.util.http :as http]
@@ -922,6 +921,7 @@
(fn []
(let [shape (u/locate-shape file-id page-id id)]
(cond
(and (not (cfh/frame-shape? shape))
(not (cfh/group-shape? shape))
(not (cfh/svg-raw-shape? shape))
@@ -929,14 +929,9 @@
(u/display-not-valid :getChildren (:type shape))
:else
(let [is-reversed? (ctl/flex-layout? shape)
reverse-fn
(if (and (natural-child-ordering? plugin-id) is-reversed?)
reverse identity)]
(->> (u/locate-shape file-id page-id id)
(:shapes)
(reverse-fn)
(format/format-array #(shape-proxy plugin-id file-id page-id %)))))))
(->> (u/locate-shape file-id page-id id)
(:shapes)
(format/format-array #(shape-proxy plugin-id file-id page-id %))))))
:appendChild
(fn [child]
@@ -955,10 +950,8 @@
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
:else
(let [child-id (obj/get child "$id")
is-reversed? (ctl/flex-layout? shape)
index (if (and (natural-child-ordering? plugin-id) is-reversed?) 0 (count (:shapes shape)))]
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
(let [child-id (obj/get child "$id")]
(st/emit! (dwsh/relocate-shapes #{child-id} id 0))))))
:insertChild
(fn [index child]
@@ -977,12 +970,7 @@
(u/display-not-valid :insertChild "Plugin doesn't have 'content:write' permission")
:else
(let [child-id (obj/get child "$id")
is-reversed? (ctl/flex-layout? shape)
index
(if (and (natural-child-ordering? plugin-id) is-reversed?)
(- (count (:shapes shape)) index)
index)]
(let [child-id (obj/get child "$id")]
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
;; Only for frames
@@ -1372,8 +1360,7 @@
(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)
ids (->> children reverse-fn (map #(obj/get % "$id")))]
ids (->> children (map #(obj/get % "$id")))]
(cond
(not= (set ids) (set (:shapes shape)))

View File

@@ -1,16 +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.plugins.state
(:require
[app.main.store :as st]))
(defn natural-child-ordering?
[plugin-id]
(boolean
(get-in
@st/state
[:workspace-local :plugin-flags plugin-id :natural-child-ordering])))

View File

@@ -1,23 +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.plugins.system-events
(:require
[app.main.data.event :as ev]
[app.main.store :as st]))
;; Formats an event from the plugin system
(defn event
[plugin-id name & {:as props}]
(let [plugin-data (get-in @st/state [:profile :props :plugins :data plugin-id])]
(-> props
(assoc ::ev/name name)
(assoc ::ev/origin "plugin")
(assoc ::ev/context
{:plugin-name (:name plugin-data)
:plugin-url (:url plugin-data)})
(ev/event))))

View File

@@ -106,20 +106,17 @@
(defn stop-propagation
[^js event]
(when (and (some? event)
(fn? (.-stopPropagation event)))
(when event
(.stopPropagation event)))
(defn stop-immediate-propagation
[^js event]
(when (and (some? event)
(fn? (.-stopImmediatePropagation event)))
(when event
(.stopImmediatePropagation event)))
(defn prevent-default
[^js event]
(when (and (some? event)
(fn? (.-preventDefault event)))
(when event
(.preventDefault event)))
(defn get-target

View File

@@ -7,16 +7,15 @@
(ns app.util.keyboard
(:require
[app.config :as cfg]
[app.util.dom :as dom]
[cuerdas.core :as str]))
(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
Object
(preventDefault [_]
(dom/prevent-default native-event))
(.preventDefault native-event))
(stopPropagation [_]
(dom/stop-propagation native-event)))
(.stopPropagation native-event)))
(defn keyboard-event?
[o]

Some files were not shown because too many files have changed in this diff Show More