Compare commits

...

37 Commits

Author SHA1 Message Date
Andrey Antukh
a72c07b657 Merge pull request #6309 from penpot/niwinz-staging-bugfixes-2
🐛 Several bugfixes
2025-04-22 09:15:02 +02:00
Andrey Antukh
708492afeb 💄 Add mainly cosmetic changes to dashboard placeholder components 2025-04-17 09:20:35 +02:00
Andrey Antukh
1305ab3cc6 🐛 Fix issue with empty placeholder on team change 2025-04-17 09:20:34 +02:00
Andrey Antukh
29cc6b4f9c Print the current seed on test.check fail 2025-04-17 09:20:34 +02:00
Andrey Antukh
cc7f0b145c 🐛 Make shape interaction properly decode on binfile import 2025-04-17 09:20:34 +02:00
Andrey Antukh
e69c0c3e27 Make schema uuid parsing fns private 2025-04-17 09:20:34 +02:00
Andrey Antukh
a209966427 🐛 Don't use schema uuid parsing function on websocket ns 2025-04-17 09:20:34 +02:00
Andrey Antukh
d5abbd4220 📎 Add missing entries on the changelog 2025-04-17 09:20:32 +02:00
Pablo Alba
70a23a14c4 🐛 Fix allow moving a main component into another 2025-04-16 22:54:30 +02:00
Marina López
93c81ea49c 🐛 Fix pricing CTA to be under a config flag (#6304) 2025-04-16 17:17:47 +02:00
Alejandro Alonso
ddc41027ab Merge pull request #6316 from penpot/palba-fix-instanciate-component
🐛 Fix error while drag an drop a component to the canvas
2025-04-16 13:15:07 +02:00
Pablo Alba
4f931fbe6a 🐛 Fix error while drag an drop a component to the canvas 2025-04-16 13:05:56 +02:00
Andrey Antukh
b49a4734ff 🐛 Fix srepl helper for restore file snapshots 2025-04-15 11:03:50 +02:00
Alejandro Alonso
2aaa2f3033 🐛 Fix template import (#6299) 2025-04-15 10:39:22 +02:00
Alejandro Alonso
202b9f3075 Merge pull request #6284 from penpot/niwinz-staging-several-bugfixes
🐛 Several bugfixes and enhacements
2025-04-15 10:33:59 +02:00
Andrey Antukh
be0814cdac Improve internal error reporting 2025-04-14 13:26:12 +02:00
Andrey Antukh
80d719353c Make auth data available before request parsing
For properly report profile-id
2025-04-14 09:23:41 +02:00
Andrey Antukh
fa3fc12594 Sanitize uuid on the rest of code 2025-04-14 09:23:29 +02:00
Andrey Antukh
422a9db07b Sanitize uuid parsing on legacy zip import code 2025-04-14 09:13:35 +02:00
Andrey Antukh
a4145a30f5 🐛 Fix uuid encode/decode on schema 2025-04-14 09:13:34 +02:00
Andrey Antukh
38e5c161e7 Sanitize plugins uuid parsing 2025-04-11 13:21:26 +02:00
Andrey Antukh
a7c1f7ba69 🐛 Fix incorrect undo handling on path edition 2025-04-11 08:54:02 +02:00
Florian Schroedl
e9755d437e 🐛 Fix sets and set groups with same name cannot be renamed 2025-04-10 13:27:49 +02:00
Eva Marco
e5db66351e 🐛 Fix scroll on token themes modal (#6251)
* 🐛 Fix scroll on token themes modal

* 🐛 Fix collapse set group error
2025-04-10 10:25:08 +02:00
ºelhombretecla
89153eef23 🎉 Increase height presets dropdown (#6185)
* 🎉 Add new measures dropdown height

* 🎉 Add enhancement to CHANGES.md
2025-04-10 10:01:52 +02:00
Alejandro
b7a8677036 Merge pull request #6262 from penpot/niwinz-staging-clean-data
🐛 Clean workspace state on exit or url change
2025-04-10 08:38:55 +02:00
Andrey Antukh
9ff2160c77 🐛 Clean workspace state on exit or url change 2025-04-09 16:31:49 +02:00
Alejandro
4c77b32171 Merge pull request #6256 from penpot/niwinz-staging-fix-backend-tests
📎 Fix backend tests
2025-04-09 13:52:30 +02:00
Andrey Antukh
34141ce9af 📎 Fix backend tests
Caused by update of image procesing libraries on the devenv docker
image update from debian to ubuntu
2025-04-09 13:37:52 +02:00
Alejandro
58c867885c Merge pull request #6250 from penpot/alotor-bug-colorpicker
🐛 Fix colorpicker scroll when dropdown displayed
2025-04-09 12:51:17 +02:00
alonso.torres
ccb6e25914 🐛 Fix colorpicker scroll when dropdown displayed 2025-04-09 12:50:55 +02:00
Alejandro
965d2d4036 🐛 Fix webhooks not shown in list (#6254) 2025-04-09 12:46:00 +02:00
Yamila Moreno
9f8d7c9e41 🐳 Improve https documentation 2025-04-09 12:24:43 +02:00
Andrey Antukh
8d352c1f82 Merge branch 'main' into staging 2025-04-09 10:59:37 +02:00
Andrey Antukh
faead09174 Merge tag '2.6.0' 2025-04-09 10:58:39 +02:00
Yamila Moreno
ae3ce1220b 🐳 Improve https documentation 2025-04-09 10:05:18 +02:00
Yamila Moreno
28caa1d47d 🐛 Fix docker-compose.yaml (#6236) 2025-04-07 16:29:47 +02:00
55 changed files with 516 additions and 330 deletions

View File

@@ -1,5 +1,26 @@
# CHANGELOG
## 2.6.2 (Unreleased)
### :bug: Bugs fixed
- Increase the height of the right sidebar dropdowns [Taiga #10615](https://tree.taiga.io/project/penpot/issue/10615)
- Fix scroll on token themes modal [Taiga #10745](https://tree.taiga.io/project/penpot/issue/10745)
- Fix unexpected exception on path editor on merge segments when undo stack is empty
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
- Fix several issues with internal srepl helpers
- Fix unexpected exception on template import from libraries
- Fix incorrect uuid parsing from different parts of code
## 2.6.1
### :bug: Bugs fixed
- Fix webhooks not shown in list [Taiga #10763](https://tree.taiga.io/project/penpot/issue/10763)
- Fix colorpicker scroll when dropdown displayed [Taiga #10696](https://tree.taiga.io/project/penpot/issue/10696)
- Clean internal workspace state on exit or url changed [Taiga #10619](https://tree.taiga.io/project/penpot/issue/10619)
## 2.6.0
### :rocket: Epics and highlights
@@ -50,6 +71,7 @@
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
## 2.5.4
### :heart: Community contributions (Thank you!)

View File

@@ -30,7 +30,8 @@ export PENPOT_FLAGS="\
enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation";
enable-file-schema-validation \
enable-subscriptions-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"

View File

@@ -23,7 +23,8 @@ export PENPOT_FLAGS="\
enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation";
enable-file-schema-validation \
enable-subscriptions-old";
export OPTIONS="
-A:jmx-remote -A:dev \

View File

@@ -155,9 +155,9 @@
[["" {:middleware [[mw/server-timing]
[mw/params]
[mw/format-response]
[mw/parse-request]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/parse-request]
[mw/errors errors/handle]
[mw/restrict-methods]]}

View File

@@ -273,7 +273,7 @@
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}]
(let [session-id (some-> params :session-id sm/parse-uuid)]
(let [session-id (some-> params :session-id uuid/parse*)]
(when-not (uuid? session-id)
(ex/raise :type :validation
:code :missing-session-id

View File

@@ -337,14 +337,23 @@
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
(defn restore-file-snapshot!
[file-id label]
(let [file-id (h/parse-uuid file-id)]
[file-id & {:keys [label id]}]
(let [file-id (h/parse-uuid file-id)
snapshot-id (some-> id h/parse-uuid)]
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as system}]
(when-let [snapshot (->> (h/search-file-snapshots conn #{file-id} label)
(map :id)
(first))]
(fsnap/restore-file-snapshot! system file-id (:id snapshot)))))))
(cond
(uuid? snapshot-id)
(fsnap/restore-file-snapshot! system file-id snapshot-id)
(string? label)
(->> (h/search-file-snapshots conn #{file-id} label)
(map :id)
(first)
(fsnap/restore-file-snapshot! system file-id))
:else
(throw (ex-info "snapshot id or label should be provided" {})))))))
(defn list-file-snapshots!
[file-id & {:as _}]

View File

@@ -48,7 +48,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1)))
(t/is (= 3302 (:size mobj2)))))))
(t/is (= 3299 (:size mobj2)))))))
(t/deftest media-object-upload
(let [prof (th/create-profile* 1)
@@ -85,7 +85,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1)))
(t/is (= 3887 (:size mobj2)))))))
(t/is (= 3901 (:size mobj2)))))))
(t/deftest media-object-upload-idempotency
@@ -163,7 +163,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1)))
(t/is (= 3302 (:size mobj2)))))))
(t/is (= 3299 (:size mobj2)))))))
(t/deftest media-object-upload-command
(let [prof (th/create-profile* 1)
@@ -200,7 +200,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1)))
(t/is (= 3887 (:size mobj2)))))))
(t/is (= 3901 (:size mobj2)))))))
(t/deftest media-object-upload-idempotency-command

View File

@@ -47,7 +47,7 @@
(defn undo
[stack]
(update stack :index dec))
(update stack :index #(max 0 (dec %))))
(defn redo
[{index :index items :items :as stack}]

View File

@@ -124,7 +124,8 @@
;; TODO: deprecate this flag and consolidate the code
:export-file-v3
:render-wasm-dpr
:hide-release-modal})
:hide-release-modal
:subscriptions-old})
(def all-flags
(set/union email login varia))

View File

@@ -390,14 +390,22 @@
(register! :merge (mu/-merge))
(register! :union (mu/-union))
(def uuid-rx
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
(defn parse-uuid
(defn- parse-uuid
[s]
(if (string? s)
(some->> (re-matches uuid-rx s) uuid/uuid)
s))
(if (uuid? s)
s
(if (str/empty? s)
nil
(try
(uuid/parse s)
(catch #?(:clj Exception :cljs :default) _cause
s)))))
(defn- encode-uuid
[v]
(if (uuid? v)
(str v)
v))
(register!
{:type ::uuid
@@ -409,8 +417,8 @@
:gen/gen (sg/uuid)
:decode/string parse-uuid
:decode/json parse-uuid
:encode/string str
:encode/json str
:encode/string encode-uuid
:encode/json encode-uuid
::oapi/type "string"
::oapi/format "uuid"}})

View File

@@ -60,6 +60,7 @@
(let [smallest (-> params :shrunk :smallest vec)]
(println)
(println "Condition failed with the following params:")
(println "Seed:" (:seed params))
(println)
(pp/pprint smallest)))

View File

@@ -543,14 +543,23 @@
;; We can always move the children to the parent they already have.
;; But if we are pasting, those are new items, so it is considered a change
no-changes?
(and (->> children (every? #(= parent-id (:parent-id %))))
(and (every? #(= parent-id (:parent-id %)) children)
(not pasting?))
all-main?
(->> children (every? #(ctk/main-instance? %)))]
(every? ctk/main-instance? children)
any-main-descendant
(some
(fn [shape]
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
children)]
(if (or no-changes?
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
;; If we are moving into a variant-container, all the items should be main
(or all-main? (not (ctk/is-variant-container? parent)))))
(or all-main? (not (ctk/is-variant-container? parent)))
;; If we are moving into a main component, no descendant can be main
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))))
[parent-id (get-frame parent-id)]
(recur (:parent-id parent) objects children pasting? libraries))))))

View File

@@ -109,13 +109,27 @@
(def check-animation!
(sm/check-fn schema:animation))
(def schema:interaction-attrs
[:map {:title "InteractionAttrs"}
[:action-type {:optional true} [::sm/one-of action-types]]
[:event-type {:optional true} [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:preserve-scroll {:optional true} :boolean]
[:animation {:optional true} schema:animation]
[:overlay-position {:optional true} ::gpt/point]
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]
[:url {:optional true} :string]])
(def schema:navigate-interaction
[:map
[:action-type [:= :navigate]]
[:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:preserve-scroll {:optional true} :boolean]
[:animation {:optional true} ::animation]])
[:animation {:optional true} schema:animation]])
(def schema:open-overlay-interaction
[:map
@@ -126,7 +140,7 @@
[:destination {:optional true} [:maybe ::sm/uuid]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:toggle-overlay-interaction
@@ -138,7 +152,7 @@
[:destination {:optional true} [:maybe ::sm/uuid]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:close-overlay-interaction
@@ -146,7 +160,7 @@
[:action-type [:= :close-overlay]]
[:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:prev-scren-interaction
@@ -161,21 +175,21 @@
[:url :string]])
(def schema:interaction
[:multi {:dispatch :action-type
:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))
:decode/json #(update % :action-type keyword)}
[:navigate schema:navigate-interaction]
[:open-overlay schema:open-overlay-interaction]
[:toggle-overlay schema:toggle-overlay-interaction]
[:close-overlay schema:close-overlay-interaction]
[:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]])
[:and {:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))}
schema:interaction-attrs
[:multi {:dispatch :action-type}
[:navigate schema:navigate-interaction]
[:open-overlay schema:open-overlay-interaction]
[:toggle-overlay schema:toggle-overlay-interaction]
[:close-overlay schema:close-overlay-interaction]
[:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]]])
(sm/register! ::interaction schema:interaction)

View File

@@ -17,9 +17,14 @@
java.util.UUID
java.nio.ByteBuffer)))
(def regex
#"^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$")
(defn uuid
"Creates an UUID instance from string, expectes valid uuid strings,
the existense of validation is implementation detail"
the existense of validation is implementation detail.
UNSAFE: this can accept invalid uuids or incomplete uuids"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/uuid s)))
@@ -27,8 +32,21 @@
(defn parse
"Parse string uuid representation into proper UUID instance, validates input"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/parse-uuid s)))
(if (and (string? s) ^boolean (re-matches regex s))
#?(:clj (UUID/fromString s)
:cljs (uuid s))
(let [message (str "invalid string '" s "' for uuid")]
(throw #?(:clj (IllegalArgumentException. message)
:cljs (js/Error. message))))))
(defn parse*
"Exception safe version of `parse`."
[s]
(try
(parse s)
(catch #?(:clj Exception :cljs :default) _cause
nil)))
(defn next
[]

View File

@@ -156,10 +156,13 @@ http {
}
location / {
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
location ~ ^/github/penpot-files/(.+)$ {
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
proxy_pass https://raw.githubusercontent.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_hide_header Cookies;
proxy_set_header User-Agent "curl/8.5.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;

View File

@@ -87,7 +87,7 @@ services:
networks:
- penpot
labels:
# labels:
# - "traefik.enable=true"
# ## HTTPS: example of labels for the case where penpot will be exposed to the

View File

@@ -135,10 +135,13 @@ http {
}
location / {
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
location ~ ^/github/penpot-files/(.+)$ {
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
proxy_pass https://raw.githubusercontent.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_hide_header Cookies;
proxy_set_header User-Agent "curl/8.5.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;

View File

@@ -151,9 +151,20 @@ Postgres database and another one for the assets uploaded by your users (images
clips). There may be more volumes if you enable other features, as explained in the file
itself.
## Configure the proxy
## Configure the proxy and HTTPS
Your host configuration needs to make a proxy to http://localhost:9001.
We strongly recommend to use Penpot under HTTPS/SSL, which will require specific server configurations for DNS and SSL certificates.
Besides, your host configuration needs to make a proxy to http://localhost:9001.
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments; as some browser APIs do
not work properly under non-https environments, this unsecure configuration
may limit the usage of Penpot; as an example, the clipboard does not work with HTTP.
</p>
Below, you can see three examples with three different proxys:
### Example with NGINX

View File

@@ -511,9 +511,7 @@ test.describe("Tokens: Sets Tab", () => {
// Creates nesting by renaming set with double click
await tokenThemesSetsSidebar
.getByRole("button", { name: "light-renamed" })
.click({ button: "right" });
await expect(tokenContextMenuForSet).toBeVisible();
await tokenContextMenuForSet.getByText("Rename").click();
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "nested/light");
await assertSetsList(tokenThemesSetsSidebar, [
@@ -558,6 +556,45 @@ test.describe("Tokens: Sets Tab", () => {
]);
});
test("User can create & edit sets and set groups with an identical name", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
await setupEmptyTokensFile(page);
const tokensTabButton = tokenThemesSetsSidebar
.getByRole("button", { name: "Add set" })
.click();
await createSet(tokenThemesSetsSidebar, "core/colors");
await createSet(tokenThemesSetsSidebar, "core");
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
await tokenThemesSetsSidebar
.getByRole("button", { name: "core" })
.nth(0)
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "core-group-renamed");
await assertSetsList(tokenThemesSetsSidebar, [
"core-group-renamed",
"colors",
"core",
]);
await page.keyboard.press(`ControlOrMeta+z`);
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
await tokenThemesSetsSidebar
.getByRole("button", { name: "core" })
.nth(1)
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "core-set-renamed");
await assertSetsList(tokenThemesSetsSidebar, [
"core",
"colors",
"core-set-renamed",
]);
});
test("Fold/Unfold set", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
await setupTokensFile(page);

View File

@@ -239,15 +239,15 @@
(str (:last-id file)))
(lookupShape [_ shape-id]
(clj->js (fb/lookup-shape file (uuid/uuid shape-id))))
(clj->js (fb/lookup-shape file (uuid/parse shape-id))))
(updateObject [_ id new-obj]
(let [old-obj (fb/lookup-shape file (uuid/uuid id))
(let [old-obj (fb/lookup-shape file (uuid/parse id))
new-obj (d/deep-merge old-obj (parse-data new-obj))]
(set! file (fb/update-object file old-obj new-obj))))
(deleteObject [_ id]
(set! file (fb/delete-object file (uuid/uuid id))))
(set! file (fb/delete-object file (uuid/parse id))))
(getId [_]
(:id file))

View File

@@ -15,7 +15,7 @@
[file ^string page-id]
;; Better to expose the api as a promise to be consumed from JS
(let [page-id (uuid/uuid page-id)
(let [page-id (uuid/parse page-id)
file-data (.-file file)
data (get-in file-data [:data :pages-index page-id])]
(p/create

View File

@@ -69,7 +69,7 @@
"Retrieves the mentions in the content as an array of uuids"
[content]
(->> (re-seq r-mentions content)
(mapv (fn [[_ _ id]] (uuid/uuid id)))))
(mapv (fn [[_ _ id]] (uuid/parse id)))))
(defn update-mentions
"Updates the params object with the mentiosn"

View File

@@ -199,7 +199,7 @@
(ptk/reify ::webhooks-fetched
ptk/UpdateEvent
(update [_ state]
(update-in state [:team-id team-id] assoc :webhooks webhooks))))
(update-in state [:teams team-id] assoc :webhooks webhooks))))
(defn fetch-webhooks
[]

View File

@@ -248,7 +248,7 @@
(defn fetch-comments
[{:keys [thread-id]}]
(dm/assert! (uuid thread-id))
(assert (uuid? thread-id))
(letfn [(fetched [comments state]
(update state :comments assoc thread-id (d/index-by :id comments)))]
(ptk/reify ::retrieve-comments
@@ -413,7 +413,7 @@
(watch [_ state _]
(let [params (rt/get-params state)
index (some-> params :index parse-long)
page-id (some-> params :page-id parse-uuid)
page-id (some-> params :page-id uuid/parse)
total (count (get-in state [:viewer :pages page-id :frames]))]

View File

@@ -369,7 +369,7 @@
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id parse-uuid)]
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
@@ -382,7 +382,7 @@
(rx/take 1)
(rx/map zoom-to-frame)))
(when-let [comment-id (some-> rparams :comment-id parse-uuid)]
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
@@ -423,7 +423,8 @@
:workspace-tokens
:workspace-undo)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)))
(assoc-in [:workspace-global :options-mode] :design)
(update :files d/update-vals #(dissoc % :data))))
ptk/WatchEvent
(watch [_ state _]
@@ -2444,13 +2445,6 @@
(js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref))
(js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref))))))
(defn set-shape-ref
[id shape-ref]
(ptk/reify ::set-shape-ref
ptk/WatchEvent
(watch [_ _ _]
(rx/of (update-shape (uuid/uuid id) {:shape-ref (uuid/uuid shape-ref)})))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -7,6 +7,7 @@
(ns app.main.ui
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.team :as dtm]
@@ -69,7 +70,7 @@
(mf/defc dashboard-legacy-redirect*
{::mf/props :obj
::mf/private true}
[{:keys [section team-id project-id search-term plugin-url template-url]}]
[{:keys [section team-id project-id search-term plugin-url template]}]
(let [section (case section
:dashboard-legacy-search
:dashboard-search
@@ -97,7 +98,7 @@
:project-id project-id
:search-term search-term
:plugin plugin-url
:template-url template-url}]
:template template}]
(st/emit! (rt/nav section (d/without-nils params)))))
[:> loader*
@@ -212,11 +213,11 @@
:dashboard-webhooks
:dashboard-settings)
(let [params (get params :query)
team-id (some-> params :team-id uuid)
project-id (some-> params :project-id uuid)
team-id (some-> params :team-id uuid/parse*)
project-id (some-> params :project-id uuid/parse*)
search-term (some-> params :search-term)
plugin-url (some-> params :plugin)
template-url (some-> params :template)]
template (some-> params :template)]
[:?
#_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
@@ -243,13 +244,13 @@
:search-term search-term
:plugin-url plugin-url
:project-id project-id
:template-url template-url}]]])
:template template}]]])
:workspace
(let [params (get params :query)
team-id (some-> params :team-id uuid)
file-id (some-> params :file-id uuid)
page-id (some-> params :page-id uuid)
team-id (some-> params :team-id uuid/parse*)
file-id (some-> params :file-id uuid/parse*)
page-id (some-> params :page-id uuid/parse*)
layout (some-> params :layout keyword)]
[:? {}
(when (cf/external-feature-flag "onboarding-03" "test")
@@ -276,15 +277,15 @@
:viewer
(let [params (get params :query)
index (some-> (:index params) parse-long)
share-id (some-> (:share-id params) parse-uuid)
share-id (some-> (:share-id params) uuid/parse*)
section (or (some-> (:section params) keyword)
:interactions)
file-id (some-> (:file-id params) parse-uuid)
page-id (some-> (:page-id params) parse-uuid)
file-id (some-> (:file-id params) uuid/parse*)
page-id (some-> (:page-id params) uuid/parse*)
imode (or (some-> (:interactions-mode params) keyword)
:show-on-click)
frame-id (some-> (:frame-id params) parse-uuid)
frame-id (some-> (:frame-id params) uuid/parse*)
share (:share params)]
[:? {}
@@ -300,9 +301,9 @@
:workspace-legacy
(let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid)
page-id (some-> params :query :page-id uuid)
(let [project-id (some-> params :path :project-id uuid/parse*)
file-id (some-> params :path :file-id uuid/parse*)
page-id (some-> params :query :page-id uuid/parse*)
layout (some-> params :query :layout keyword)]
[:> workspace-legacy-redirect*
@@ -321,18 +322,18 @@
:dashboard-legacy-team-invitations
:dashboard-legacy-team-webhooks
:dashboard-legacy-team-settings)
(let [team-id (some-> params :path :team-id uuid)
project-id (some-> params :path :project-id uuid)
(let [team-id (some-> params :path :team-id uuid/parse*)
project-id (some-> params :path :project-id uuid/parse*)
search-term (some-> params :query :search-term)
plugin-url (some-> params :query :plugin)
template-url (some-> params :template)]
template (some-> params :template)]
[:> dashboard-legacy-redirect*
{:team-id team-id
:section section
:project-id project-id
:search-term search-term
:plugin-url plugin-url
:template-url template-url}])
:template template}])
:viewer-legacy
(let [{:keys [query-params path-params]} route

View File

@@ -487,7 +487,7 @@
(dom/stop-propagation event)
(let [id (-> (dom/get-current-target event)
(dom/get-data "user-id")
(uuid/uuid))
(uuid/parse))
user (d/seek #(= (:id %) id) members)]

View File

@@ -23,7 +23,7 @@
[item item item]))
(mf/defc select
[{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled]}]
[{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled data-direction]}]
(let [label-index (mf/with-memo [options]
(into {} (map as-key-value) options))
@@ -112,7 +112,7 @@
[:span {:class (stl/css :current-label)} current-label]
[:span {:class (stl/css :dropdown-button)} i/arrow]
[:& dropdown {:show is-open? :on-close close-dropdown}
[:ul {:ref dropdown-element* :data-direction @dropdown-direction*
[:ul {:ref dropdown-element* :data-direction (or data-direction @dropdown-direction*)
:class (dm/str dropdown-class " " (stl/css :custom-select-dropdown))}
(for [[index item] (d/enumerate options)]
(if (= :separator item)

View File

@@ -34,12 +34,10 @@
[app.main.ui.workspace.plugins]
[app.plugins.register :as preg]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.storage :as storage]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.events :as events]
@@ -211,17 +209,22 @@
(swap! storage/session dissoc :plugin-url))))))
(defn use-templates-import
[can-edit? template-url project]
[can-edit? template project]
(let [project-id (get project :id)
team-id (get project :team-id)]
(mf/with-layout-effect [can-edit? template-url project-id team-id]
(when (and (some? template-url)
(mf/with-layout-effect [can-edit? template project-id team-id]
(when (and (some? template)
(some? project-id)
(some? team-id))
(if can-edit?
(let [valid-url? (and (str/ends-with? template-url ".penpot")
(str/starts-with? template-url cf/templates-uri))
template-name (when valid-url? (subs template-url (count cf/templates-uri)))
(let [valid-url? (str/ends-with? template ".penpot")
;; Backwards compatibility, ideally the template should be only the .penpot file name, not the full url
template-name (if (str/starts-with? template "http")
(subs template (count cf/templates-uri))
template)
template-url (str "/github/penpot-files/" template-name)
on-import #(st/emit! (dpj/fetch-files project-id)
(dd/fetch-recent-files team-id)
(dd/fetch-projects team-id)
@@ -230,30 +233,22 @@
:name template-name
:url template-url}))]
(if valid-url?
(do
(st/emit! (ptk/event ::ev/event {::ev/name "install-template-from-link" :name template-name :url template-url}))
(->> (http/send! {:method :get
:uri template-url
:response-type :blob
:omit-default-headers true})
(rx/subs!
(fn [result]
(if (or (< (:status result) 200) (>= (:status result) 300))
(st/emit! (notif/error (tr "dashboard.import.error")))
(st/emit! (modal/show
{:type :import
:project-id project-id
:entries [{:name template-name :uri (wapi/create-uri (:body result))}]
:on-finish-import on-import})))))))
(st/emit!
(ptk/event ::ev/event {::ev/name "install-template-from-link" :name template-name :url template-url})
(modal/show
{:type :import
:project-id project-id
:entries [{:name template-name :uri template-url}]
:on-finish-import on-import}))
(st/emit! (notif/error (tr "dashboard.import.bad-url")))))
(st/emit! (notif/error (tr "dashboard.import.no-perms"))))
(binding [storage/*sync* true]
(swap! storage/session dissoc :template-url))))))
(swap! storage/session dissoc :template))))))
(mf/defc dashboard*
{::mf/props :obj}
[{:keys [profile project-id team-id search-term plugin-url template-url section]}]
[{:keys [profile project-id team-id search-term plugin-url template section]}]
(let [team (mf/deref refs/team)
projects (mf/deref refs/projects)
@@ -263,7 +258,7 @@
(filterv #(= team-id (:team-id %)))))
can-edit? (dm/get-in team [:permissions :can-edit])
template-url (or template-url (:template-url storage/session))
template (or template (:template storage/session))
plugin-url (or plugin-url (:plugin-url storage/session))
default-project
@@ -289,7 +284,7 @@
(events/unlistenByKey key))))
(use-plugin-register plugin-url team-id (:id default-project))
(use-templates-import can-edit? template-url default-project)
(use-templates-import can-edit? template default-project)
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:> modal-container*]

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.media :as cm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.fonts :as df]
[app.main.data.modal :as modal]
@@ -121,7 +122,7 @@
(fn [event]
(let [id (-> (dom/get-current-target event)
(dom/get-data "id")
(parse-uuid))
(uuid/parse))
item (get fonts id)]
(on-upload* item))))
@@ -132,7 +133,7 @@
(let [target (dom/get-current-target event)
id (-> target
(dom/get-data "id")
(parse-uuid))
(uuid/parse))
name (dom/get-value target)]
(when-not (str/blank? name)
(swap! fonts* df/rename-and-regroup id name installed-fonts)))))
@@ -143,7 +144,7 @@
(let [target (dom/get-current-target event)
id (-> target
(dom/get-data "id")
(parse-uuid))
(uuid/parse))
name (dom/get-value target)]
(swap! fonts* update id assoc :font-family-tmp name))))
@@ -153,7 +154,7 @@
(fn [event]
(let [id (-> (dom/get-current-target event)
(dom/get-data "id")
(parse-uuid))]
(uuid/parse))]
(swap! fonts* dissoc id))))
on-upload-all
@@ -344,7 +345,7 @@
(fn [event]
(let [id (-> (dom/get-current-target event)
(dom/get-data "id")
(parse-uuid))
(uuid/parse))
options {:type :confirm
:title (tr "modals.delete-font-variant.title")
:message (tr "modals.delete-font-variant.message")

View File

@@ -28,7 +28,7 @@
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
[app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
[app.main.ui.dashboard.placeholder :refer [empty-grid-placeholder* loading-placeholder*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i]
@@ -511,7 +511,7 @@
:ref node-ref}
(cond
(nil? files)
[:& loading-placeholder]
[:> loading-placeholder*]
(seq files)
(for [[index slice] (d/enumerate (partition-all limit files))]
@@ -528,12 +528,13 @@
:can-edit can-edit}])])
:else
[:& empty-placeholder
[:> empty-grid-placeholder*
{:limit limit
:can-edit can-edit
:create-fn create-fn
:origin origin
:project-id project-id
:team-id team-id
:on-finish-import on-finish-import}])]))
(mf/defc line-grid-row
@@ -645,7 +646,7 @@
:on-drop on-drop}
(cond
(nil? files)
[:& loading-placeholder]
[:> loading-placeholder*]
(seq files)
[:& line-grid-row {:files files
@@ -656,10 +657,11 @@
:limit limit}]
:else
[:& empty-placeholder
{:dragging? @dragging?
[:> empty-grid-placeholder*
{:is-dragging @dragging?
:limit limit
:can-edit can-edit
:create-fn create-fn
:project-id project-id
:team-id team-id
:on-finish-import on-finish-import}])]))

View File

@@ -8,7 +8,6 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.import :as udi]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
@@ -16,51 +15,92 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(mf/defc empty-placeholder-projects*
{::mf/wrap-props false}
[{:keys [on-create on-finish-import project-id] :as props}]
(mf/defc empty-project-placeholder*
{::mf/private true}
[{:keys [on-create on-finish-import project-id]}]
(let [file-input (mf/use-ref nil)
on-add-library (mf/use-fn
(fn [_]
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
::ev/origin "dashboard"
:section "empty-placeholder-projects"}))
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
on-import-files (mf/use-fn #(dom/click (mf/ref-val file-input)))]
on-add-library
(mf/use-fn
(fn [_]
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
::ev/origin "dashboard"
:section "empty-placeholder-projects"}))
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
on-import
(mf/use-fn #(dom/click (mf/ref-val file-input)))]
[:div {:class (stl/css :empty-project-container)}
[:div {:class (stl/css :empty-project-card) :on-click on-create :title (tr "dashboard.add-file")}
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.create")]
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.start")]]
[:div {:class (stl/css :empty-project-card)
:on-click on-create
:title (tr "dashboard.add-file")}
[:div {:class (stl/css :empty-project-card-title)}
(tr "dashboard.empty-project.create")]
[:div {:class (stl/css :empty-project-card-subtitle)}
(tr "dashboard.empty-project.start")]]
[:div {:class (stl/css :empty-project-card) :on-click on-import-files :title (tr "dashboard.empty-project.import")}
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.import")]
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.import-penpot")]]
[:div {:class (stl/css :empty-project-card)
:on-click on-import
:title (tr "dashboard.empty-project.import")}
[:div {:class (stl/css :empty-project-card-title)}
(tr "dashboard.empty-project.import")]
[:div {:class (stl/css :empty-project-card-subtitle)}
(tr "dashboard.empty-project.import-penpot")]]
[:div {:class (stl/css :empty-project-card) :on-click on-add-library :title (tr "dashboard.empty-project.go-to-libraries")}
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.add-library")]
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.explore")]]
[:div {:class (stl/css :empty-project-card)
:on-click on-add-library
:title (tr "dashboard.empty-project.go-to-libraries")}
[:div {:class (stl/css :empty-project-card-title)}
(tr "dashboard.empty-project.add-library")]
[:div {:class (stl/css :empty-project-card-subtitle)}
(tr "dashboard.empty-project.explore")]]
[:& udi/import-form {:ref file-input
:project-id project-id
:on-finish-import on-finish-import}]]))
(mf/defc empty-placeholder
[{:keys [dragging? limit origin create-fn can-edit project-id on-finish-import]}]
(defn- make-has-other-files-or-projects-ref
"Return a ref that resolves to true or false if there are at least some
file or some project (a part of the default) exists; this determines
if we need to show a complete placeholder or the small one."
[team-id]
(l/derived (fn [state]
(or (let [projects (get state :projects)]
(some (fn [[_ project]]
(and (= (:team-id project) team-id)
(not (:is-default project))))
projects))
(let [files (get state :files)]
(some (fn [[_ file]]
(= (:team-id file) team-id))
files))))
st/state))
(mf/defc empty-grid-placeholder*
[{:keys [is-dragging limit origin create-fn can-edit team-id project-id on-finish-import]}]
(let [on-click
(mf/use-fn
(mf/deps create-fn)
(fn [_]
(create-fn "dashboard:empty-folder-placeholder")))
show-text (mf/use-state nil)
on-mouse-enter (mf/use-fn #(reset! show-text true))
on-mouse-leave (mf/use-fn #(reset! show-text nil))
files (mf/deref refs/files)]
show-text* (mf/use-state nil)
show-text? (deref show-text*)
on-mouse-enter (mf/use-fn #(reset! show-text* true))
on-mouse-leave (mf/use-fn #(reset! show-text* nil))
has-other* (mf/with-memo [team-id]
(make-has-other-files-or-projects-ref team-id))
has-other? (mf/deref has-other*)]
(cond
(true? dragging?)
(true? is-dragging)
[:ul
{:class (stl/css :grid-row :no-wrap)
:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
@@ -80,18 +120,24 @@
:tag-name "span"}])]
:else
(if (= (count files) 0)
[:> empty-placeholder-projects* {:on-create on-click :on-finish-import on-finish-import :project-id project-id}]
(if-not has-other?
[:> empty-project-placeholder*
{:on-create on-click
:on-finish-import on-finish-import
:project-id project-id}]
[:div {:class (stl/css :grid-empty-placeholder)}
[:button {:class (stl/css :create-new)
:on-click on-click
:on-mouse-enter on-mouse-enter
:on-mouse-leave on-mouse-leave}
(if @show-text (tr "dashboard.empty-project.create") i/add)]]))))
(if show-text?
(tr "dashboard.empty-project.create")
i/add)]]))))
(mf/defc loading-placeholder
(mf/defc loading-placeholder*
[]
[:> loader* {:width 32
:title (tr "labels.loading")
:class (stl/css :placeholder-loader)}
[:span {:class (stl/css :placeholder-text)} (tr "dashboard.loading-files")]])
[:span {:class (stl/css :placeholder-text)}
(tr "dashboard.loading-files")]])

View File

@@ -371,6 +371,7 @@
show-team-hero?
can-invite))}
(for [{:keys [id] :as project} projects]
;; FIXME: refactor this, looks inneficient
(let [files (when recent-map
(->> (vals recent-map)
(filterv #(= id (:project-id %)))

View File

@@ -284,7 +284,6 @@
(let [team-id (-> (dom/get-current-target event)
(dom/get-data "value")
(uuid/parse))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
handle-select-default
@@ -963,13 +962,14 @@
(dom/open-new-window "https://penpot.app/pricing")))]
[:*
[:button {:class (stl/css :upgrade-plan-section)
:on-click on-power-up-click}
[:div {:class (stl/css :penpot-free)}
[:span (tr "dashboard.upgrade-plan.penpot-free")]
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]]
[:div {:class (stl/css :power-up)}
(tr "dashboard.upgrade-plan.power-up")]]
(when (contains? cf/flags :subscriptions-old)
[:button {:class (stl/css :upgrade-plan-section)
:on-click on-power-up-click}
[:div {:class (stl/css :penpot-free)}
[:span (tr "dashboard.upgrade-plan.penpot-free")]
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]]
[:div {:class (stl/css :power-up)}
(tr "dashboard.upgrade-plan.power-up")]])
(when (and team profile)
[:& comments-section
{:profile profile

View File

@@ -1045,7 +1045,7 @@
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect [team]
(mf/with-effect []
(st/emit! (dtm/fetch-webhooks)))
[:*

View File

@@ -82,7 +82,7 @@
(binding [storage/*sync* true]
(when (some? template)
(swap! storage/session assoc
:template-url template))
:template template))
(when (some? plugin)
(swap! storage/session assoc
:plugin-url plugin))))

View File

@@ -202,9 +202,8 @@
cancel-text])
[:button {:on-click on-click} button-text]]]]))
(mf/defc request-access
{::mf/props :obj}
[{:keys [file-id team-id is-default workspace?]}]
(mf/defc request-access*
[{:keys [file-id team-id is-default is-workspace]}]
(let [profile (mf/deref refs/profile)
requested* (mf/use-state {:sent false :already-requested false})
requested (deref requested*)
@@ -227,11 +226,11 @@
on-request-access
(mf/use-fn
(mf/deps file-id team-id workspace?)
(mf/deps file-id team-id is-workspace)
(fn []
(let [params (if (some? file-id)
{:file-id file-id
:is-viewer (not workspace?)}
:is-viewer (not is-workspace)}
{:team-id team-id})
mdata {:on-success on-success
:on-error on-error}]
@@ -240,7 +239,7 @@
[:*
(if (some? file-id)
(if workspace?
(if is-workspace
[:div {:class (stl/css :workspace)}
[:div {:class (stl/css :workspace-left)}
i/logo-icon
@@ -341,7 +340,7 @@
[:div {:class (stl/css :sign-info)}
[:button {:on-click handle-retry} (tr "labels.retry")]]]))
(mf/defc service-unavailable
(mf/defc service-unavailable*
[]
(let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))]
[:> error-container* {}
@@ -350,58 +349,55 @@
[:div {:class (stl/css :sign-info)}
[:button {:on-click on-click} (tr "labels.retry")]]]))
(defn generate-report
(defn- generate-report
[data]
(try
(let [team-id (:current-team-id @st/state)
profile-id (:profile-id @st/state)
trace (:app.main.errors/trace data)
instance (:app.main.errors/instance data)
content (with-out-str
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
(println "Prof ID:" (str (or profile-id "--")))
(println "Team ID:" (str (or team-id "--")))
instance (:app.main.errors/instance data)]
(with-out-str
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
(println "Prof ID:" (str (or profile-id "--")))
(println "Team ID:" (str (or team-id "--")))
(when-let [file-id (:file-id data)]
(println "File ID:" (str file-id)))
(when-let [file-id (:file-id data)]
(println "File ID:" (str file-id)))
(println)
(println)
(println "Data:")
(loop [data data]
(-> (d/without-qualified data)
(dissoc :explain)
(d/update-when :data (constantly "(...)"))
(pp/pprint {:level 8 :length 10}))
(println "Data:")
(loop [data data]
(-> (d/without-qualified data)
(dissoc :explain)
(d/update-when :data (constantly "(...)"))
(pp/pprint {:level 8 :length 10}))
(println)
(println)
(when-let [explain (:explain data)]
(print explain))
(when-let [explain (:explain data)]
(print explain))
(when (and (= :server-error (:type data))
(contains? data :data))
(recur (:data data))))
(when (and (= :server-error (:type data))
(contains? data :data))
(recur (:data data))))
(println "Trace:")
(println trace)
(println)
(println "Trace:")
(println trace)
(println)
(println "Last events:")
(pp/pprint @st/last-events {:length 200})
(println "Last events:")
(pp/pprint @st/last-events {:length 200})
(println))]
(wapi/create-blob content "text/plain"))
(println)))
(catch :default cause
(.error js/console "error on generating report.txt" cause)
nil)))
(mf/defc internal-error*
{::mf/props :obj}
[{:keys [data on-reset] :as props}]
[{:keys [on-reset report] :as props}]
(let [report-uri (mf/use-ref nil)
report (mf/use-memo (mf/deps data) #(generate-report data))
on-reset (or on-reset #(st/emit! (rt/assign-exception nil)))
on-download
@@ -413,8 +409,8 @@
(mf/with-effect [report]
(when (some? report)
(let [uri (wapi/create-uri report)]
(let [report (wapi/create-blob report "text/plain")
uri (wapi/create-uri report)]
(mf/set-ref-val! report-uri uri)
(fn []
(wapi/revoke-uri uri)))))
@@ -455,6 +451,38 @@
(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]
(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! (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
[:> not-found* {}]
:authentication
[:> not-found* {}]
:bad-gateway
[:> bad-gateway* props]
:service-unavailable
[:> service-unavailable*]
[:> internal-error* props])))
(mf/defc exception-page*
{::mf/props :obj}
@@ -477,42 +505,23 @@
request-access?
(and
(or (= (:type data) :not-found)
(= (:type data) :authentication))
(or (= type :not-found)
(= type :authentication))
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
(mf/with-effect [type path params]
(st/emit! (ptk/data-event ::ev/event
{::ev/name "exception-page"
:type type
:path path
:params (u/map->query-string params)})))
(mf/with-effect [params info]
(when-not (:loaded info)
(->> (load-info params)
(rx/subs! (partial reset! info*)))))
(rx/subs! (partial reset! info*)
(partial reset! info* {:loaded true})))))
(when loaded?
(if request-access?
[:& request-access {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:workspace? workspace?}]
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:is-workspace workspace?}]
[:> exception-section* props]))))
(case (:type data)
:not-found
[:> not-found* {}]
:authentication
[:> not-found* {}]
:bad-gateway
[:> bad-gateway* props]
:service-unavailable
[:& service-unavailable]
[:> internal-error* props])))))

View File

@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.common :as dc]
[app.main.data.event :as ev]
@@ -104,7 +105,7 @@
(fn [event]
(let [target (dom/get-target event)
checked? (dom/checked? target)
page-id (parse-uuid (dom/get-data target "page-id"))
page-id (uuid/parse (dom/get-data target "page-id"))
dif-pages? (not= page-id (first (:pages options)))
no-one-page (< 1 (count (:pages options)))
should-change? (or ^boolean no-one-page

View File

@@ -20,7 +20,7 @@
.colorpicker {
border-radius: $br-8;
overflow: auto;
overflow: hidden;
}
.colorpicker-tabs {

View File

@@ -11,6 +11,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.color :as ctc]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
@@ -62,7 +63,7 @@
(if (or (= event "recent")
(= event "file"))
(keyword event)
(parse-uuid event)))))
(uuid/parse event)))))
valid-color?
(mf/use-fn
@@ -124,6 +125,7 @@
[:div {:class (stl/css :select-wrapper)}
[:& select
{:class (stl/css :shadow-type-select)
:data-direction "up"
:default-value (or (d/name selected) "recent")
:options options
:on-change on-library-change}]]

View File

@@ -228,7 +228,7 @@
(fn [event]
(let [library-id (some-> (dom/get-current-target event)
(dom/get-data "library-id")
(parse-uuid))]
(uuid/parse))]
(reset! selected library-id)
(st/emit! (dwl/link-file-to-library file-id library-id)))))
@@ -238,7 +238,7 @@
(fn [event]
(let [library-id (some-> (dom/get-current-target event)
(dom/get-data "library-id")
(parse-uuid))]
(uuid/parse))]
(when (= library-id @selected)
(reset! selected :file))
(st/emit! (dwl/unlink-file-from-library file-id library-id)
@@ -451,7 +451,7 @@
(when-not updating?
(let [library-id (some-> (dom/get-target event)
(dom/get-data "library-id")
(parse-uuid))]
(uuid/parse))]
(st/emit!
(dwl/set-updating-library true)
(dwl/sync-file file-id library-id))))))]

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
@@ -87,7 +88,7 @@
value (dom/get-attribute node "data-palette")]
(on-select (if (or (= "file" value) (= "recent" value))
(keyword value)
(parse-uuid value))))))
(uuid/parse value))))))
on-select-text-palette-menu
(mf/use-fn

View File

@@ -251,14 +251,14 @@
(mf/deps index update-interaction)
(fn [event]
(let [value event
value (when (not= value "") (uuid/uuid value))]
value (when (not= value "") (uuid/parse value))]
(update-interaction index #(ctsi/set-destination % value)))))
change-position-relative-to
(mf/use-fn
(mf/deps index update-interaction)
(fn [event]
(let [value (uuid/uuid event)]
(let [value (uuid/parse event)]
(update-interaction index #(ctsi/set-position-relative-to % value)))))
change-preserve-scroll

View File

@@ -55,6 +55,7 @@
.custom-select-dropdown {
@extend .dropdown-wrapper;
margin-top: $s-2;
max-height: 70vh;
width: $s-252;
.dropdown-element {
@extend .dropdown-element-base;

View File

@@ -153,7 +153,7 @@
(mf/deps on-pin-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> (dom/get-data node "id") uuid/uuid)]
id (-> (dom/get-data node "id") uuid/parse)]
(when on-pin-snapshot (on-pin-snapshot id)))))
handle-restore-snapshot
@@ -161,7 +161,7 @@
(mf/deps on-restore-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> (dom/get-data node "id") uuid/uuid)]
id (-> (dom/get-data node "id") uuid/parse)]
(when on-restore-snapshot (on-restore-snapshot id)))))

View File

@@ -190,6 +190,7 @@
border: $s-1 solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
border-radius: $s-8;
overflow-y: auto;
max-height: $s-452;
}
.sets-count-empty-button {

View File

@@ -65,6 +65,11 @@
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(dt/create-token-set name))))
(defn group-edition-id
"Prefix editing groups `edition-id` so it can be differentiated from sets with the same id."
[edition-id]
(str "group-" edition-id))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -166,16 +171,18 @@
{:position (dom/get-client-position event)
:is-group true
:id id
:edition-id (group-edition-id id)
:path tree-path})))))
on-collapse-click
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(on-toggle-collapse tree-path)))
on-double-click
(mf/use-fn (mf/deps id) #(on-start-edition id))
(mf/use-fn (mf/deps id) #(on-start-edition (group-edition-id id)))
on-checkbox-click
(mf/use-fn
@@ -267,6 +274,7 @@
{:position (dom/get-client-position event)
:is-group false
:id id
:edition-id id
:path tree-path})))))
on-double-click
@@ -398,7 +406,7 @@
:is-active (is-token-set-group-active path)
:is-selected false
:is-draggable is-draggable
:is-editing (= edition-id id)
:is-editing (= edition-id (group-edition-id id))
:is-collapsed (collapsed? path)
:on-select on-select

View File

@@ -34,7 +34,7 @@
(mf/defc menu*
{::mf/private true}
[{:keys [is-group id path]}]
[{:keys [is-group id edition-id path]}]
(let [create-set-at-path
(mf/use-fn (mf/deps path) #(st/emit! (dt/start-token-set-creation path)))
@@ -42,7 +42,7 @@
(mf/use-fn
(mf/deps id)
(fn []
(st/emit! (dt/start-token-set-edition id))))
(st/emit! (dt/start-token-set-edition edition-id))))
on-delete
(mf/use-fn
@@ -57,7 +57,7 @@
(mf/defc token-set-context-menu*
[]
(let [{:keys [position is-group id path]}
(let [{:keys [position is-group id edition-id path]}
(mf/deref ref:token-sets-context-menu)
position-top
@@ -78,4 +78,5 @@
:on-context-menu prevent-default}
[:> menu* {:is-group is-group
:id id
:edition-id edition-id
:path path}]]]))

View File

@@ -468,10 +468,7 @@
(fn [event]
(dom/prevent-default event)
(let [point (gpt/point (.-clientX event) (.-clientY event))
viewport-coord (uwvv/point->viewport point)
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
asset-name (dnd/get-data event "text/asset-name")
asset-type (dnd/get-data event "text/asset-type")]
viewport-coord (uwvv/point->viewport point)]
(cond
(dnd/has-type? event "penpot/shape")
(let [shape (dnd/get-data event "penpot/shape")
@@ -516,25 +513,6 @@
(assoc params :blobs (map wapi/data-uri->blob data)))]
(st/emit! (dwm/upload-media-workspace params)))
;; Will trigger when the user drags an SVG asset from the assets panel
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
(let [path (cfg/resolve-file-media {:id asset-id})
params {:file-id (:id file)
:position viewport-coord
:uris [path]
:name asset-name
:mtype asset-type}]
(st/emit! (dwm/upload-media-workspace params)))
;; Will trigger when the user drags an image from the assets SVG
(dnd/has-type? event "text/asset-id")
(let [params {:file-id (:id file)
:object-id asset-id
:name asset-name}]
(st/emit! (dwm/clone-media-object
(with-meta params
{:on-success #(st/emit! (dwm/image-uploaded % viewport-coord))}))))
;; Will trigger when the user drags a file from their file explorer into the viewport
;; Or the user pastes an image
;; Or the user uploads an image using the image tool

View File

@@ -969,7 +969,7 @@
:else
(let [file-id (:current-file-id @st/state)
library-id (uuid/uuid library-id)]
library-id (uuid/parse library-id)]
(->> st/stream
(rx/filter (ptk/type? ::dwl/attach-library-finished))
(rx/take 1)

View File

@@ -160,7 +160,7 @@
(u/display-not-valid :getShapeById shape-id)
:else
(let [shape-id (uuid/uuid shape-id)
(let [shape-id (uuid/parse shape-id)
shape (u/locate-shape file-id id shape-id)]
(when (some? shape)
(shape/shape-proxy plugin-id file-id id shape-id)))))

View File

@@ -13,7 +13,7 @@
(defn parse-id
[id]
(when id (uuid/uuid id)))
(when id (uuid/parse id)))
(defn parse-keyword
[kw]

View File

@@ -43,14 +43,14 @@
(defn read-json-key
[m]
(or (sm/parse-uuid m)
(or (uuid/parse m)
(json/read-kebab-key m)))
(defn read-json-val
[m]
(cond
(and (string? m)
(re-matches sm/uuid-rx m))
(re-matches uuid/regex m))
(uuid/uuid m)
(and (string? m)
@@ -521,8 +521,8 @@
id (resolve old-id)
path (get-in node [:attrs :penpot:path] "")
type (parser/get-type content)
main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
data (-> (parser/parse-data type content)
(assoc :path path)
(assoc :id id)
@@ -547,12 +547,12 @@
old-id (parser/get-id node)
id (resolve old-id)
path (get-in node [:attrs :penpot:path] "")
main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
main-instance-x (-> (get-in node [:attrs :penpot:main-instance-x] "") (d/parse-double))
main-instance-y (-> (get-in node [:attrs :penpot:main-instance-y] "") (d/parse-double))
main-instance-parent (resolve (uuid (get-in node [:attrs :penpot:main-instance-parent] "")))
main-instance-frame (resolve (uuid (get-in node [:attrs :penpot:main-instance-frame] "")))
main-instance-parent (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-parent] "")))
main-instance-frame (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-frame] "")))
type (parser/get-type content)
data (-> (parser/parse-data type content)

View File

@@ -20,9 +20,6 @@
(def url-regex
#"url\(#([^\)]*)\)")
(def uuid-regex
#"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}")
(def uuid-regex-prefix
#"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}-")
@@ -84,7 +81,7 @@
(defn get-id
[node]
(let [attr-id (get-in node [:attrs :id])
id (when (string? attr-id) (re-find uuid-regex attr-id))]
id (when (string? attr-id) (re-find uuid/regex attr-id))]
(when (some? id)
(uuid/uuid id))))
@@ -189,10 +186,10 @@
[m]
(letfn [(convert [value]
(cond
(and (string? value) (re-matches uuid-regex value))
(and (string? value) (re-matches uuid/regex value))
(uuid/uuid value)
(and (keyword? value) (re-matches uuid-regex (d/name value)))
(and (keyword? value) (re-matches uuid/regex (d/name value)))
(uuid/uuid (d/name value))
(vector? value)
@@ -429,11 +426,11 @@
(defn add-library-refs
[props node]
(let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid)
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid)
component-id (get-meta node :component-id uuid/uuid)
component-file (get-meta node :component-file uuid/uuid)
shape-ref (get-meta node :shape-ref uuid/uuid)
(let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/parse)
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/parse)
component-id (get-meta node :component-id uuid/parse)
component-file (get-meta node :component-file uuid/parse)
shape-ref (get-meta node :shape-ref uuid/parse)
component-root? (get-meta node :component-root str->bool)
main-instance? (get-meta node :main-instance str->bool)
touched (get-meta node :touched parse-touched)]
@@ -463,8 +460,8 @@
[props node svg-data]
(let [fill (:fill svg-data)
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/parse)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/parse)
meta-fill-color (get-meta node :fill-color)
meta-fill-opacity (get-meta node :fill-opacity)
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient")
@@ -627,7 +624,7 @@
(let [attrs (-> node :attrs remove-penpot-prefix)]
{:id (uuid/next)
:name (-> attrs :name)
:starting-frame (-> attrs :starting-frame uuid)}))
:starting-frame (-> attrs :starting-frame uuid/parse)}))
(defn parse-flows [node]
(let [flows-node (get-data node :penpot:flows)]
@@ -638,7 +635,7 @@
id (uuid/next)]
[id
{:id id
:frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid))
:frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid/parse))
:axis (-> attrs :axis keyword)
:position (-> attrs :position d/parse-double)}]))
@@ -775,8 +772,8 @@
(parse-gradient node (get-meta fill-node :fill-color)))
:fill-image (when fill-image-id
(get images fill-image-id))
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/parse)
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/parse)
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})))
(mapv d/without-nils)
(filterv #(not= (:fill-color %) "none")))]
@@ -800,8 +797,8 @@
(parse-gradient node (get-meta stroke-node :stroke-color)))
:stroke-image (when stroke-image-id
(get images stroke-image-id))
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/parse)
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/parse)
:stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
:stroke-style (get-meta stroke-node :stroke-style keyword)
:stroke-width (get-meta stroke-node :stroke-width d/parse-double)
@@ -993,7 +990,7 @@
align-self
justify-self
shapes]} (-> cell-node :attrs remove-penpot-prefix)
id (uuid/uuid id)]
id (uuid/parse id)]
[id (d/without-nils
{:id id
:area-name area-name
@@ -1006,7 +1003,7 @@
:justify-self (keyword justify-self)
:shapes (if (and (some? shapes) (d/not-empty? shapes))
(->> (str/split shapes " ")
(mapv uuid/uuid))
(mapv uuid/parse))
[])})])))
(into {}))))
@@ -1154,7 +1151,7 @@
(assoc :delay (get-meta node :delay d/parse-double))
(ctsi/has-destination interaction)
(assoc :destination (get-meta node :destination uuid/uuid)
(assoc :destination (get-meta node :destination uuid/parse)
:preserve-scroll (get-meta node :preserve-scroll str->bool))
(ctsi/has-url interaction)

View File

@@ -179,7 +179,7 @@
[state name]
(let [objects (dsh/lookup-page-objects state)
result (or (d/seek (fn [shape] (= name (:name shape))) (vals objects))
(get objects (uuid/uuid name)))]
(get objects (uuid/parse name)))]
result))
(defn ^:export dump-object
@@ -222,12 +222,12 @@
(defn ^:export select-by-object-id
[object-id]
(let [[_ page-id shape-id _] (str/split object-id #"/")]
(st/emit! (dcm/go-to-workspace :page-id (uuid/uuid page-id)))
(st/emit! (dws/select-shape (uuid/uuid shape-id)))))
(st/emit! (dcm/go-to-workspace :page-id (uuid/parse page-id)))
(st/emit! (dws/select-shape (uuid/parse shape-id)))))
(defn ^:export select-by-id
[shape-id]
(st/emit! (dws/select-shape (uuid/uuid shape-id))))
(st/emit! (dws/select-shape (uuid/parse shape-id))))
(defn dump-tree'
([state] (dump-tree' state false false false))
@@ -255,7 +255,7 @@
file (dsh/lookup-file state)
libraries (get state :files)
shape-id (if (some? shape-id)
(uuid/uuid shape-id)
(uuid/parse shape-id)
(first (dsh/lookup-selected state)))]
(if (some? shape-id)
(ctf/dump-subtree file page-id shape-id libraries {:show-ids show-ids
@@ -369,7 +369,7 @@
(let [file (dsh/lookup-file @st/state)
libraries (get @st/state :files)]
(try
(->> (if-let [shape-id (some-> shape-id parse-uuid)]
(->> (if-let [shape-id (some-> shape-id uuid/parse)]
(let [page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)])]
(cfv/validate-shape shape-id file page libraries))
(cfv/validate-file file libraries))
@@ -426,6 +426,15 @@
[]
(st/emit! (dw/find-components-norefs)))
(defn- set-shape-ref*
[id shape-ref]
(ptk/reify ::set-shape-ref
ptk/WatchEvent
(watch [_ _ _]
(let [shape-id (uuid/parse id)
shape-ref (uuid/parse shape-ref)]
(rx/of (dw/update-shape shape-id {:shape-ref shape-ref}))))))
(defn ^:export set-shape-ref
[id shape-ref]
(st/emit! (dw/set-shape-ref id shape-ref)))
(st/emit! (set-shape-ref* id shape-ref)))