Compare commits
391 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25455523ad | ||
|
|
8dfeb21978 | ||
|
|
ad2833bb7a | ||
|
|
21911e898f | ||
|
|
538073debf | ||
|
|
1ae7515189 | ||
|
|
8c401f5346 | ||
|
|
214b0efa02 | ||
|
|
661436ecae | ||
|
|
0d5fe6e527 | ||
|
|
e7230d9da4 | ||
|
|
5054f6bc38 | ||
|
|
b72de2dc8f | ||
|
|
38396ba299 | ||
|
|
b1997a83b3 | ||
|
|
68f5671eab | ||
|
|
92976143bb | ||
|
|
dd2d03e6a0 | ||
|
|
fd675e0194 | ||
|
|
51635770ce | ||
|
|
18a4e63da0 | ||
|
|
c98373658e | ||
|
|
01e42b0458 | ||
|
|
7529673812 | ||
|
|
d2dad35d7a | ||
|
|
d71f811dbe | ||
|
|
f7b5266304 | ||
|
|
09c23256b7 | ||
|
|
1ae1c0460e | ||
|
|
291c7349db | ||
|
|
1beb3b86aa | ||
|
|
b605a3b53d | ||
|
|
fe20bdd00e | ||
|
|
5420897b92 | ||
|
|
e430a4c9f3 | ||
|
|
ec6d72bd91 | ||
|
|
cc2dab2756 | ||
|
|
d0c0664338 | ||
|
|
2240f25143 | ||
|
|
93a5ec2f5d | ||
|
|
d6784771a8 | ||
|
|
930c814ded | ||
|
|
1a5a69bca2 | ||
|
|
9ad323a220 | ||
|
|
6ec451b46d | ||
|
|
5fa4368d70 | ||
|
|
b8efd2518d | ||
|
|
2b836f10cb | ||
|
|
7b2271ec38 | ||
|
|
2240d93069 | ||
|
|
3f4506284b | ||
|
|
af1dfd91aa | ||
|
|
24feebd73b | ||
|
|
33e5a9a538 | ||
|
|
9c69b07a62 | ||
|
|
56f5be4f37 | ||
|
|
8a70204d41 | ||
|
|
57a27f7e7f | ||
|
|
3b0b2a78d6 | ||
|
|
10bf4610df | ||
|
|
77e8414aea | ||
|
|
20ecf3b066 | ||
|
|
49b1032973 | ||
|
|
5ba7dd8c56 | ||
|
|
38b5125186 | ||
|
|
6677ae83d4 | ||
|
|
0737c055f0 | ||
|
|
4b88748fe3 | ||
|
|
92107e5b1e | ||
|
|
ebc0e3a23c | ||
|
|
ebe4f2da50 | ||
|
|
a07c1d6eaa | ||
|
|
613bfda955 | ||
|
|
f7ef6618e5 | ||
|
|
fe334d9cbe | ||
|
|
268b883c73 | ||
|
|
f6a4effa29 | ||
|
|
ced848077e | ||
|
|
7d9d318539 | ||
|
|
9781fceadb | ||
|
|
3178bd9a27 | ||
|
|
e5d677f449 | ||
|
|
6bf928893c | ||
|
|
1ae0f3fc87 | ||
|
|
53dd90aa24 | ||
|
|
e13c203b8d | ||
|
|
9fd0f6a8f3 | ||
|
|
638c3356d3 | ||
|
|
6879f54e5d | ||
|
|
a71baa5a78 | ||
|
|
8e4a89bd1c | ||
|
|
90efb665b5 | ||
|
|
47ee490158 | ||
|
|
f0f89599bc | ||
|
|
7aad9da285 | ||
|
|
ab57a4ae52 | ||
|
|
266ee29bb9 | ||
|
|
69ca86bb6c | ||
|
|
ee14a845fc | ||
|
|
73639f5d16 | ||
|
|
9bd106b2bc | ||
|
|
59c75afc7b | ||
|
|
bbc81586e3 | ||
|
|
c9c30eab75 | ||
|
|
86ba9280db | ||
|
|
5800cc4bb2 | ||
|
|
aa29a34c4c | ||
|
|
3276129cc7 | ||
|
|
67a96de475 | ||
|
|
48785b4846 | ||
|
|
3f0573f95d | ||
|
|
d94a2a8881 | ||
|
|
1c237a0968 | ||
|
|
b0dc7d6ffb | ||
|
|
b7eaeffa88 | ||
|
|
722fcc1f82 | ||
|
|
b7cd315872 | ||
|
|
2ad42cfd9b | ||
|
|
743d4e5c8d | ||
|
|
97e4f4c424 | ||
|
|
fb9560c315 | ||
|
|
795f65632a | ||
|
|
d53c090900 | ||
|
|
621e030095 | ||
|
|
157e4aa2d0 | ||
|
|
7cd2308f3b | ||
|
|
c315a15b48 | ||
|
|
8a3e6d026e | ||
|
|
0dd062d011 | ||
|
|
bfbb546699 | ||
|
|
083e77e9c5 | ||
|
|
7819e6c440 | ||
|
|
952f622ce9 | ||
|
|
a6c6f97f47 | ||
|
|
88424eb54a | ||
|
|
919f78daeb | ||
|
|
b5c30f8c41 | ||
|
|
60aa426753 | ||
|
|
86f7d6b26b | ||
|
|
36732a4bd3 | ||
|
|
eff572d3bb | ||
|
|
d470d96833 | ||
|
|
cab70773d2 | ||
|
|
de9a21121a | ||
|
|
32ca42a093 | ||
|
|
523a97a4ec | ||
|
|
260f6861a3 | ||
|
|
edd53b419a | ||
|
|
cea10308b7 | ||
|
|
078a3d5a5c | ||
|
|
c4e57427ac | ||
|
|
5223c9c881 | ||
|
|
be62fa10c4 | ||
|
|
7a6405481c | ||
|
|
218f34380a | ||
|
|
47aaa2b5fa | ||
|
|
6c6b3db87e | ||
|
|
6eb32cfb79 | ||
|
|
dbba3496af | ||
|
|
55752d361f | ||
|
|
fe94ee4526 | ||
|
|
e39f292499 | ||
|
|
52b8560b70 | ||
|
|
75860afe57 | ||
|
|
824ca1bbca | ||
|
|
5b6f9c1741 | ||
|
|
19853b832b | ||
|
|
d20c011db2 | ||
|
|
9431ae6858 | ||
|
|
96356c1b89 | ||
|
|
b7b68eeb47 | ||
|
|
9bbeb657f8 | ||
|
|
ec1af4ad96 | ||
|
|
23e7116b24 | ||
|
|
6b794c9d12 | ||
|
|
22a36d59d8 | ||
|
|
a948e49e51 | ||
|
|
d635f5a8dc | ||
|
|
ab3a3ef43b | ||
|
|
9c21fd3359 | ||
|
|
7b5817f407 | ||
|
|
e3405eacca | ||
|
|
44b70cf1d4 | ||
|
|
a8bd74b392 | ||
|
|
3d3e3582d6 | ||
|
|
e01654ba43 | ||
|
|
6ebd48b94c | ||
|
|
417cd80564 | ||
|
|
a57011ec7b | ||
|
|
69c880d00e | ||
|
|
9eebc467ef | ||
|
|
b77712ce73 | ||
|
|
3d3e81f314 | ||
|
|
fe6441bb24 | ||
|
|
e15f0baf30 | ||
|
|
c040cbb784 | ||
|
|
7f674b78a9 | ||
|
|
099b78affd | ||
|
|
78cc3f0aa4 | ||
|
|
76f5f12808 | ||
|
|
cb325282ec | ||
|
|
047483a70a | ||
|
|
8cb2f27de8 | ||
|
|
0433336fc9 | ||
|
|
ce234fbeda | ||
|
|
fc4d31eed7 | ||
|
|
c670aac339 | ||
|
|
1d3fb5434f | ||
|
|
f478399ae0 | ||
|
|
6a1854f180 | ||
|
|
0858e297e5 | ||
|
|
bd580ab159 | ||
|
|
5780a43fe0 | ||
|
|
737eceda3a | ||
|
|
923c3c2dbd | ||
|
|
a14b4561e7 | ||
|
|
105e1fe86c | ||
|
|
3e0a916883 | ||
|
|
4f80238bc2 | ||
|
|
5156cc5d9a | ||
|
|
42c46b6cfc | ||
|
|
8b3c40b35e | ||
|
|
d3996e5fb1 | ||
|
|
0c42bca866 | ||
|
|
e5685c1f1c | ||
|
|
2784209bde | ||
|
|
024f460e99 | ||
|
|
1d9b76b62a | ||
|
|
7e17a75b7d | ||
|
|
ca093d6fae | ||
|
|
0f0b7562b5 | ||
|
|
9cdc694697 | ||
|
|
b972a4033b | ||
|
|
cbe9f4da51 | ||
|
|
c583bde9e3 | ||
|
|
3911ebdc4e | ||
|
|
3e3b18667b | ||
|
|
ed81c9b8df | ||
|
|
fbdf98d29c | ||
|
|
e603825a55 | ||
|
|
1d724783e6 | ||
|
|
e0abe7dcb5 | ||
|
|
5c1bbf5be8 | ||
|
|
bbb0d58190 | ||
|
|
88dcf9d1fe | ||
|
|
20061067ad | ||
|
|
2acf15958b | ||
|
|
08267de242 | ||
|
|
35fb376a78 | ||
|
|
13fcf3a9bb | ||
|
|
dba6ae2820 | ||
|
|
ada101c236 | ||
|
|
ea48fb5825 | ||
|
|
8fde6b28ed | ||
|
|
63325ec796 | ||
|
|
84415476d0 | ||
|
|
33c786498d | ||
|
|
1f886b1f88 | ||
|
|
5a922c6bd6 | ||
|
|
1388865cfc | ||
|
|
1738847694 | ||
|
|
ca1c3c799d | ||
|
|
ce5006ae84 | ||
|
|
0a7a65af5d | ||
|
|
ea4d0e1238 | ||
|
|
b705cf953a | ||
|
|
90ce1f56e7 | ||
|
|
ab0438cc6f | ||
|
|
c6aa9cc4b7 | ||
|
|
5779adef33 | ||
|
|
ebf1758958 | ||
|
|
e94c56bfa7 | ||
|
|
89d9591011 | ||
|
|
3becfcd723 | ||
|
|
5501a2815f | ||
|
|
1066438b02 | ||
|
|
3b23a3ad19 | ||
|
|
7396f4bfb6 | ||
|
|
5cf51f3d26 | ||
|
|
25acad5154 | ||
|
|
0a212b6291 | ||
|
|
eb1eeb4750 | ||
|
|
a78477592b | ||
|
|
0956b66281 | ||
|
|
007b3f11f9 | ||
|
|
a661b2564f | ||
|
|
2c3732f3f4 | ||
|
|
e16645227b | ||
|
|
45665a3c21 | ||
|
|
179e6a195d | ||
|
|
b45bdd723f | ||
|
|
8696044620 | ||
|
|
8a8f360c7f | ||
|
|
e35fc85c3d | ||
|
|
81bc1bb0af | ||
|
|
1798461d21 | ||
|
|
dde0fddd6f | ||
|
|
7d36bc4025 | ||
|
|
b8feb6374d | ||
|
|
0889df8e08 | ||
|
|
4637aced8c | ||
|
|
9dfe5b0865 | ||
|
|
33bcc9544a | ||
|
|
babd481b7f | ||
|
|
a9733c792d | ||
|
|
7be8ac3fd7 | ||
|
|
9216d965ef | ||
|
|
d04fdb5fbd | ||
|
|
4f3ca6422c | ||
|
|
1c03457fda | ||
|
|
81e0e4f222 | ||
|
|
f13b3c8737 | ||
|
|
520e979363 | ||
|
|
a0f8559ffc | ||
|
|
a38f425dd3 | ||
|
|
74d4b9b045 | ||
|
|
416980f063 | ||
|
|
f76710296c | ||
|
|
d1379c55f6 | ||
|
|
b125c7b5a3 | ||
|
|
496d37795b | ||
|
|
9f6899007a | ||
|
|
641df77834 | ||
|
|
4e84deca44 | ||
|
|
0d21e52068 | ||
|
|
1b29e9a50f | ||
|
|
9f567c3bf4 | ||
|
|
1ba15e5d10 | ||
|
|
60df56caa3 | ||
|
|
53aad7bc15 | ||
|
|
37e45a8bbf | ||
|
|
3471d40f46 | ||
|
|
c6b64a8e39 | ||
|
|
511e80c948 | ||
|
|
f5a640d104 | ||
|
|
3ae7c514e4 | ||
|
|
57297741f5 | ||
|
|
d63d692d34 | ||
|
|
fad9ed1c48 | ||
|
|
0caaefefea | ||
|
|
b179aa79b1 | ||
|
|
fe72d0af82 | ||
|
|
405ddb60d8 | ||
|
|
ef68081d1d | ||
|
|
4ed49cdc5d | ||
|
|
95c0d42d5b | ||
|
|
721b337511 | ||
|
|
359379be09 | ||
|
|
876d5783cf | ||
|
|
786f73767b | ||
|
|
50f9eedcdf | ||
|
|
efe74e62e8 | ||
|
|
456afe46de | ||
|
|
4282cdcd2c | ||
|
|
964ef799c2 | ||
|
|
d34b6b88b6 | ||
|
|
9a58f0e954 | ||
|
|
adaf8be56d | ||
|
|
2f1b99fa53 | ||
|
|
5080fcc594 | ||
|
|
ea2d3758f0 | ||
|
|
e889413f26 | ||
|
|
115273b478 | ||
|
|
fdddd3284a | ||
|
|
51385a04a0 | ||
|
|
f96ed8ccd6 | ||
|
|
bda5de5c1b | ||
|
|
94c15916e2 | ||
|
|
ed0f3c3595 | ||
|
|
59f3b4db4c | ||
|
|
7ee03ad911 | ||
|
|
130b8c8214 | ||
|
|
0198d41757 | ||
|
|
567a955151 | ||
|
|
34da754357 | ||
|
|
c2014a37b4 | ||
|
|
6611fbd13b | ||
|
|
b5a6867058 | ||
|
|
0f88253dd5 | ||
|
|
8e3996fbb0 | ||
|
|
67762d9450 | ||
|
|
7f62652870 | ||
|
|
78d31ab11a | ||
|
|
0a80c47901 | ||
|
|
39eafae251 | ||
|
|
e1e09b7f96 | ||
|
|
3b39980f2f | ||
|
|
223b12d2c7 | ||
|
|
77f1046fc8 | ||
|
|
553b73a83c | ||
|
|
00a45cb274 |
@@ -1,305 +0,0 @@
|
|||||||
version: 2.1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "fmt check"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn install
|
|
||||||
yarn run fmt:clj:check
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint clj common"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn run lint:clj:common
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint clj frontend"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn run lint:clj:frontend
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint clj backend"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn run lint:clj:backend
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint clj exporter"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn run lint:clj:exporter
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint clj library"
|
|
||||||
working_directory: "."
|
|
||||||
command: |
|
|
||||||
yarn run lint:clj:library
|
|
||||||
|
|
||||||
test-common:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
# Download and cache dependencies
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "JVM tests"
|
|
||||||
working_directory: "./common"
|
|
||||||
command: |
|
|
||||||
clojure -M:dev:test
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "NODE tests"
|
|
||||||
working_directory: "./common"
|
|
||||||
command: |
|
|
||||||
yarn install
|
|
||||||
yarn run test
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2
|
|
||||||
- ~/.yarn
|
|
||||||
- ~/.gitlibs
|
|
||||||
- ~/.cache/ms-playwright
|
|
||||||
key: v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
|
|
||||||
|
|
||||||
test-frontend:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
# Download and cache dependencies
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "install dependencies"
|
|
||||||
working_directory: "./frontend"
|
|
||||||
# We install playwright here because the dependent tasks
|
|
||||||
# uses the same cache as this task so we prepopulate it
|
|
||||||
command: |
|
|
||||||
yarn install
|
|
||||||
yarn run playwright install chromium --with-deps
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint scss on frontend"
|
|
||||||
working_directory: "./frontend"
|
|
||||||
command: |
|
|
||||||
yarn run lint:scss
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "unit tests"
|
|
||||||
working_directory: "./frontend"
|
|
||||||
command: |
|
|
||||||
yarn run test
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2
|
|
||||||
- ~/.yarn
|
|
||||||
- ~/.gitlibs
|
|
||||||
- ~/.cache/ms-playwright
|
|
||||||
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
|
||||||
|
|
||||||
test-library:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JAVA_OPTS: -Xmx6g
|
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
# Download and cache dependencies
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Install dependencies and build
|
|
||||||
working_directory: "./library"
|
|
||||||
command: |
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Build and Test
|
|
||||||
working_directory: "./library"
|
|
||||||
command: |
|
|
||||||
./scripts/build
|
|
||||||
yarn run test
|
|
||||||
|
|
||||||
test-components:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JAVA_OPTS: -Xmx6g -Xms2g
|
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
# Download and cache dependencies
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Install dependencies
|
|
||||||
working_directory: "./frontend"
|
|
||||||
command: |
|
|
||||||
yarn install
|
|
||||||
yarn run playwright install chromium
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Build Storybook
|
|
||||||
working_directory: "./frontend"
|
|
||||||
command: yarn run build:storybook
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Serve Storybook and run tests
|
|
||||||
working_directory: "./frontend"
|
|
||||||
command: |
|
|
||||||
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
|
||||||
"npx http-server storybook-static --port 6006 --silent" \
|
|
||||||
"npx wait-on tcp:6006 && yarn test:storybook"
|
|
||||||
|
|
||||||
test-backend:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
- image: cimg/postgres:14.5
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: penpot_test
|
|
||||||
POSTGRES_PASSWORD: penpot_test
|
|
||||||
POSTGRES_DB: penpot_test
|
|
||||||
- image: cimg/redis:7.0.5
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "tests"
|
|
||||||
working_directory: "./backend"
|
|
||||||
command: |
|
|
||||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
|
||||||
|
|
||||||
environment:
|
|
||||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
|
||||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
|
||||||
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
|
|
||||||
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2
|
|
||||||
- ~/.gitlibs
|
|
||||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
|
|
||||||
|
|
||||||
test-render-wasm:
|
|
||||||
docker:
|
|
||||||
- image: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
resource_class: medium+
|
|
||||||
environment:
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "fmt check"
|
|
||||||
working_directory: "./render-wasm"
|
|
||||||
command: |
|
|
||||||
cargo fmt --check
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "lint"
|
|
||||||
working_directory: "./render-wasm"
|
|
||||||
command: |
|
|
||||||
./lint
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: "cargo tests"
|
|
||||||
working_directory: "./render-wasm"
|
|
||||||
command: |
|
|
||||||
./test
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
penpot:
|
|
||||||
jobs:
|
|
||||||
- test-frontend:
|
|
||||||
requires:
|
|
||||||
- lint: success
|
|
||||||
|
|
||||||
- test-library:
|
|
||||||
requires:
|
|
||||||
- lint: success
|
|
||||||
|
|
||||||
- test-components:
|
|
||||||
requires:
|
|
||||||
- lint: success
|
|
||||||
|
|
||||||
- test-backend:
|
|
||||||
requires:
|
|
||||||
- lint: success
|
|
||||||
|
|
||||||
- test-common:
|
|
||||||
requires:
|
|
||||||
- lint: success
|
|
||||||
|
|
||||||
- lint
|
|
||||||
- test-render-wasm
|
|
||||||
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: New Render Bug Report
|
||||||
|
about: Create a report about the bugs you have found in the new render
|
||||||
|
title: ''
|
||||||
|
labels: new render
|
||||||
|
assignees: claragvinola
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Steps to Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots or screen recordings**
|
||||||
|
If applicable, add screenshots or screen recording to help illustrate your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
2
.github/workflows/build-docker-devenv.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and push DevEnv Docker image
|
name: Build and push DevEnv Docker image
|
||||||
environment: release-admins
|
environment: release-admins
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
2
.github/workflows/build-docker.yml
vendored
@@ -19,7 +19,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and Push Penpot Docker Images
|
name: Build and Push Penpot Docker Images
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: penpot-runner-02
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: _NITRATE MODULE
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '36 5-20 * * 1-5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-bundle:
|
||||||
|
uses: ./.github/workflows/build-bundle.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "nitrate-module"
|
||||||
|
build_wasm: "yes"
|
||||||
|
build_storybook: "yes"
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
needs: build-bundle
|
||||||
|
uses: ./.github/workflows/build-docker.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "nitrate-module"
|
||||||
2
.github/workflows/build-tag.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
TEXT: |
|
TEXT: |
|
||||||
🐳 *[PENPOT] Docker image available.*
|
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}*
|
||||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
@infra
|
@infra
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Check Commit Type
|
- name: Check Commit Type
|
||||||
uses: gsactions/commit-message-checker@v2
|
uses: gsactions/commit-message-checker@v2
|
||||||
with:
|
with:
|
||||||
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
|
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$'
|
||||||
flags: 'gm'
|
flags: 'gm'
|
||||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||||
|
|||||||
82
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: "Linter"
|
name: "Linter"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
test-common:
|
test-common:
|
||||||
name: "Common Tests"
|
name: "Common Tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -51,9 +51,54 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./scripts/test
|
./scripts/test
|
||||||
|
|
||||||
|
test-plugins:
|
||||||
|
name: Plugins Runtime Linter & Tests
|
||||||
|
runs-on: penpot-runner-02
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
id: setup-node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
working-directory: ./plugins
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install;
|
||||||
|
|
||||||
|
- name: Run Lint
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Run Format Check
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run format:check
|
||||||
|
|
||||||
|
- name: Run Test
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
- name: Build runtime
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Build plugins
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run build:plugins
|
||||||
|
|
||||||
|
- name: Build styles
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm run build:styles-example
|
||||||
|
|
||||||
test-frontend:
|
test-frontend:
|
||||||
name: "Frontend Tests"
|
name: "Frontend Tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -67,12 +112,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Component Tests
|
- name: Component Tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
env:
|
||||||
|
VITEST_BROWSER_TIMEOUT: 120000
|
||||||
run: |
|
run: |
|
||||||
./scripts/test-components
|
./scripts/test-components
|
||||||
|
|
||||||
test-render-wasm:
|
test-render-wasm:
|
||||||
name: "Render WASM Tests"
|
name: "Render WASM Tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -96,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
name: "Backend Tests"
|
name: "Backend Tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -135,7 +182,7 @@ jobs:
|
|||||||
|
|
||||||
test-library:
|
test-library:
|
||||||
name: "Library Tests"
|
name: "Library Tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -149,7 +196,7 @@ jobs:
|
|||||||
|
|
||||||
build-integration:
|
build-integration:
|
||||||
name: "Build Integration Bundle"
|
name: "Build Integration Bundle"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -159,17 +206,7 @@ jobs:
|
|||||||
- name: Build Bundle
|
- name: Build Bundle
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
corepack enable;
|
./scripts/build 0.0.0
|
||||||
corepack install;
|
|
||||||
yarn install
|
|
||||||
yarn run build:app:assets
|
|
||||||
yarn run build:app
|
|
||||||
yarn run build:app:libs
|
|
||||||
|
|
||||||
- name: Build WASM
|
|
||||||
working-directory: "./render-wasm"
|
|
||||||
run: |
|
|
||||||
./build release
|
|
||||||
|
|
||||||
- name: Store Bundle Cache
|
- name: Store Bundle Cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -177,9 +214,10 @@ jobs:
|
|||||||
key: "integration-bundle-${{ github.sha }}"
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
|
|
||||||
|
|
||||||
test-integration-1:
|
test-integration-1:
|
||||||
name: "Integration Tests 1/4"
|
name: "Integration Tests 1/4"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -209,7 +247,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-2:
|
test-integration-2:
|
||||||
name: "Integration Tests 2/4"
|
name: "Integration Tests 2/4"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -239,7 +277,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-3:
|
test-integration-3:
|
||||||
name: "Integration Tests 3/4"
|
name: "Integration Tests 3/4"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -269,7 +307,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-4:
|
test-integration-4:
|
||||||
name: "Integration Tests 4/4"
|
name: "Integration Tests 4/4"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: penpot-runner-02
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
.pnpm-store
|
||||||
*-init.clj
|
*-init.clj
|
||||||
*.css.json
|
*.css.json
|
||||||
*.jar
|
*.jar
|
||||||
|
|||||||
57
CHANGES.md
@@ -1,10 +1,65 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 2.14.0 (Unreleased)
|
||||||
|
|
||||||
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
|
### :heart: Community contributions (Thank you!)
|
||||||
|
|
||||||
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
|
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
|
||||||
|
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
|
||||||
|
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
|
||||||
|
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
|
||||||
|
|
||||||
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
|
||||||
|
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
|
||||||
|
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.13.0 (Unreleased)
|
||||||
|
|
||||||
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
|
### :heart: Community contributions (Thank you!)
|
||||||
|
|
||||||
|
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
||||||
|
|
||||||
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
|
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
|
||||||
|
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
|
||||||
|
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
|
||||||
|
|
||||||
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
|
||||||
|
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
|
||||||
|
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
|
||||||
|
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
|
||||||
|
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
|
||||||
|
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
|
||||||
|
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
|
||||||
|
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
|
||||||
|
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
|
||||||
|
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
|
||||||
|
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
|
||||||
|
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||||
|
|
||||||
## 2.12.1
|
## 2.12.1
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
|
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
|
||||||
|
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
|
||||||
|
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
|
||||||
|
|
||||||
## 2.12.0
|
## 2.12.0
|
||||||
|
|
||||||
@@ -17,7 +72,6 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
|
|||||||
compatibility; however, if you are a user of this API, it is strongly
|
compatibility; however, if you are a user of this API, it is strongly
|
||||||
recommended that you adapt your code to use the new PATH.
|
recommended that you adapt your code to use the new PATH.
|
||||||
|
|
||||||
|
|
||||||
#### Updated SSO Callback URL
|
#### Updated SSO Callback URL
|
||||||
|
|
||||||
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
|
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
|
||||||
@@ -50,7 +104,6 @@ This update standardizes all authentication flows under the single URL
|
|||||||
and makis it more modular, enabling the ability to configure SSO auth
|
and makis it more modular, enabling the ability to configure SSO auth
|
||||||
provider dinamically.
|
provider dinamically.
|
||||||
|
|
||||||
|
|
||||||
#### Changes on default docker compose
|
#### Changes on default docker compose
|
||||||
|
|
||||||
We have updated the `docker/images/docker-compose.yaml` with a small
|
We have updated the `docker/images/docker-compose.yaml` with a small
|
||||||
|
|||||||
@@ -97,8 +97,8 @@
|
|||||||
|
|
||||||
:jmx-remote
|
:jmx-remote
|
||||||
{:jvm-opts ["-Dcom.sun.management.jmxremote"
|
{:jvm-opts ["-Dcom.sun.management.jmxremote"
|
||||||
"-Dcom.sun.management.jmxremote.port=9090"
|
"-Dcom.sun.management.jmxremote.port=9000"
|
||||||
"-Dcom.sun.management.jmxremote.rmi.port=9090"
|
"-Dcom.sun.management.jmxremote.rmi.port=9000"
|
||||||
"-Dcom.sun.management.jmxremote.local.only=false"
|
"-Dcom.sun.management.jmxremote.local.only=false"
|
||||||
"-Dcom.sun.management.jmxremote.authenticate=false"
|
"-Dcom.sun.management.jmxremote.authenticate=false"
|
||||||
"-Dcom.sun.management.jmxremote.ssl=false"
|
"-Dcom.sun.management.jmxremote.ssl=false"
|
||||||
|
|||||||
@@ -36,17 +36,6 @@
|
|||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[yetti.response :as-alias yres]))
|
[yetti.response :as-alias yres]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; HELPERS
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defn obfuscate-string
|
|
||||||
[s]
|
|
||||||
(if (< (count s) 10)
|
|
||||||
(apply str (take (count s) (repeat "*")))
|
|
||||||
(str (subs s 0 5)
|
|
||||||
(apply str (take (- (count s) 5) (repeat "*"))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; OIDC PROVIDER (GENERIC)
|
;; OIDC PROVIDER (GENERIC)
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -177,7 +166,7 @@
|
|||||||
(l/inf :hint "provider initialized"
|
(l/inf :hint "provider initialized"
|
||||||
:provider (:id provider)
|
:provider (:id provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider)))
|
:client-secret (d/obfuscate-string (:client-secret provider)))
|
||||||
provider)
|
provider)
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
@@ -222,7 +211,7 @@
|
|||||||
(l/inf :hint "provider initialized"
|
(l/inf :hint "provider initialized"
|
||||||
:provider (:id provider)
|
:provider (:id provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider)))
|
:client-secret (d/obfuscate-string (:client-secret provider)))
|
||||||
provider)
|
provider)
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
@@ -299,7 +288,7 @@
|
|||||||
(l/inf :hint "provider initialized"
|
(l/inf :hint "provider initialized"
|
||||||
:provider (:id provider)
|
:provider (:id provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider)))
|
:client-secret (d/obfuscate-string (:client-secret provider)))
|
||||||
provider)
|
provider)
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
@@ -341,7 +330,7 @@
|
|||||||
:provider "gitlab"
|
:provider "gitlab"
|
||||||
:base-uri (:base-uri provider)
|
:base-uri (:base-uri provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider)))
|
:client-secret (d/obfuscate-string (:client-secret provider)))
|
||||||
provider)
|
provider)
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(ex/raise :type ::internal
|
(ex/raise :type ::internal
|
||||||
@@ -361,7 +350,7 @@
|
|||||||
(l/inf :hint "provider initialized"
|
(l/inf :hint "provider initialized"
|
||||||
:provider (:id provider)
|
:provider (:id provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider)))
|
:client-secret (d/obfuscate-string (:client-secret provider)))
|
||||||
provider)
|
provider)
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
@@ -459,7 +448,7 @@
|
|||||||
(l/trc :hint "fetch access token"
|
(l/trc :hint "fetch access token"
|
||||||
:provider (:id provider)
|
:provider (:id provider)
|
||||||
:client-id (:client-id provider)
|
:client-id (:client-id provider)
|
||||||
:client-secret (obfuscate-string (:client-secret provider))
|
:client-secret (d/obfuscate-string (:client-secret provider))
|
||||||
:grant-type (:grant_type params)
|
:grant-type (:grant_type params)
|
||||||
:redirect-uri (:redirect_uri params))
|
:redirect-uri (:redirect_uri params))
|
||||||
|
|
||||||
@@ -512,7 +501,7 @@
|
|||||||
[cfg provider tdata]
|
[cfg provider tdata]
|
||||||
(l/trc :hint "fetch user info"
|
(l/trc :hint "fetch user info"
|
||||||
:uri (:user-uri provider)
|
:uri (:user-uri provider)
|
||||||
:token (obfuscate-string (:token/access tdata)))
|
:token (d/obfuscate-string (:token/access tdata)))
|
||||||
|
|
||||||
(let [params {:uri (:user-uri provider)
|
(let [params {:uri (:user-uri provider)
|
||||||
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
||||||
|
|||||||
@@ -331,6 +331,81 @@
|
|||||||
(set/difference cfeat/backend-only-features))
|
(set/difference cfeat/backend-only-features))
|
||||||
#{}))))
|
#{}))))
|
||||||
|
|
||||||
|
(defn check-file-exists
|
||||||
|
[cfg id & {:keys [include-deleted?]
|
||||||
|
:or {include-deleted? false}
|
||||||
|
:as options}]
|
||||||
|
(db/get-with-sql cfg [sql:get-minimal-file id]
|
||||||
|
{:db/remove-deleted (not include-deleted?)}))
|
||||||
|
|
||||||
|
(def ^:private sql:file-permissions
|
||||||
|
"select fpr.is_owner,
|
||||||
|
fpr.is_admin,
|
||||||
|
fpr.can_edit
|
||||||
|
from file_profile_rel as fpr
|
||||||
|
inner join file as f on (f.id = fpr.file_id)
|
||||||
|
where fpr.file_id = ?
|
||||||
|
and fpr.profile_id = ?
|
||||||
|
union all
|
||||||
|
select tpr.is_owner,
|
||||||
|
tpr.is_admin,
|
||||||
|
tpr.can_edit
|
||||||
|
from team_profile_rel as tpr
|
||||||
|
inner join project as p on (p.team_id = tpr.team_id)
|
||||||
|
inner join file as f on (p.id = f.project_id)
|
||||||
|
where f.id = ?
|
||||||
|
and tpr.profile_id = ?
|
||||||
|
union all
|
||||||
|
select ppr.is_owner,
|
||||||
|
ppr.is_admin,
|
||||||
|
ppr.can_edit
|
||||||
|
from project_profile_rel as ppr
|
||||||
|
inner join file as f on (f.project_id = ppr.project_id)
|
||||||
|
where f.id = ?
|
||||||
|
and ppr.profile_id = ?")
|
||||||
|
|
||||||
|
(defn- get-file-permissions*
|
||||||
|
[conn profile-id file-id]
|
||||||
|
(when (and profile-id file-id)
|
||||||
|
(db/exec! conn [sql:file-permissions
|
||||||
|
file-id profile-id
|
||||||
|
file-id profile-id
|
||||||
|
file-id profile-id])))
|
||||||
|
|
||||||
|
(defn get-file-permissions
|
||||||
|
([conn profile-id file-id]
|
||||||
|
(let [rows (get-file-permissions* conn profile-id file-id)
|
||||||
|
is-owner (boolean (some :is-owner rows))
|
||||||
|
is-admin (boolean (some :is-admin rows))
|
||||||
|
can-edit (boolean (some :can-edit rows))]
|
||||||
|
(when (seq rows)
|
||||||
|
{:type :membership
|
||||||
|
:is-owner is-owner
|
||||||
|
:is-admin (or is-owner is-admin)
|
||||||
|
:can-edit (or is-owner is-admin can-edit)
|
||||||
|
:can-read true
|
||||||
|
:is-logged (some? profile-id)})))
|
||||||
|
|
||||||
|
([conn profile-id file-id share-id]
|
||||||
|
(let [perms (get-file-permissions conn profile-id file-id)
|
||||||
|
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
|
||||||
|
(dissoc :flags)
|
||||||
|
(update :pages db/decode-pgarray #{}))]
|
||||||
|
|
||||||
|
;; NOTE: in a future when share-link becomes more powerful and
|
||||||
|
;; will allow us specify which parts of the app is available, we
|
||||||
|
;; will probably need to tweak this function in order to expose
|
||||||
|
;; this flags to the frontend.
|
||||||
|
(cond
|
||||||
|
(some? perms) perms
|
||||||
|
(some? ldata) {:type :share-link
|
||||||
|
:can-read true
|
||||||
|
:pages (:pages ldata)
|
||||||
|
:is-logged (some? profile-id)
|
||||||
|
:who-comment (:who-comment ldata)
|
||||||
|
:who-inspect (:who-inspect ldata)}))))
|
||||||
|
|
||||||
|
|
||||||
(defn get-project
|
(defn get-project
|
||||||
[cfg project-id]
|
[cfg project-id]
|
||||||
(db/get cfg :project {:id project-id}))
|
(db/get cfg :project {:id project-id}))
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
(defn- get-file-media-object
|
(defn- get-file-media-object
|
||||||
[pool id]
|
[pool id]
|
||||||
(db/get pool :file-media-object {:id id}))
|
(db/get pool :file-media-object {:id id} {::db/remove-deleted false}))
|
||||||
|
|
||||||
(defn- serve-object-from-s3
|
(defn- serve-object-from-s3
|
||||||
[{:keys [::sto/storage] :as cfg} obj]
|
[{:keys [::sto/storage] :as cfg} obj]
|
||||||
|
|||||||
@@ -309,7 +309,7 @@
|
|||||||
(fn [request]
|
(fn [request]
|
||||||
(let [key (yreq/get-header request "x-shared-key")]
|
(let [key (yreq/get-header request "x-shared-key")]
|
||||||
(if (= key shared-key)
|
(if (= key shared-key)
|
||||||
(handler request)
|
(handler (assoc request ::http/auth-with-shared-key true))
|
||||||
{::yres/status 403}))))
|
{::yres/status 403}))))
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{::yres/status 403})))
|
{::yres/status 403})))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
@@ -92,7 +93,11 @@
|
|||||||
(let [handler-name (:type path-params)
|
(let [handler-name (:type path-params)
|
||||||
etag (yreq/get-header request "if-none-match")
|
etag (yreq/get-header request "if-none-match")
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request))
|
(::actoken/profile-id request)
|
||||||
|
(if (::http/auth-with-shared-key request)
|
||||||
|
uuid/zero
|
||||||
|
nil))
|
||||||
|
|
||||||
ip-addr (inet/parse-request request)
|
ip-addr (inet/parse-request request)
|
||||||
|
|
||||||
data (-> params
|
data (-> params
|
||||||
|
|||||||
@@ -79,85 +79,14 @@
|
|||||||
|
|
||||||
;; --- FILE PERMISSIONS
|
;; --- FILE PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
(def ^:private sql:file-permissions
|
|
||||||
"select fpr.is_owner,
|
|
||||||
fpr.is_admin,
|
|
||||||
fpr.can_edit
|
|
||||||
from file_profile_rel as fpr
|
|
||||||
inner join file as f on (f.id = fpr.file_id)
|
|
||||||
where fpr.file_id = ?
|
|
||||||
and fpr.profile_id = ?
|
|
||||||
and f.deleted_at is null
|
|
||||||
union all
|
|
||||||
select tpr.is_owner,
|
|
||||||
tpr.is_admin,
|
|
||||||
tpr.can_edit
|
|
||||||
from team_profile_rel as tpr
|
|
||||||
inner join project as p on (p.team_id = tpr.team_id)
|
|
||||||
inner join file as f on (p.id = f.project_id)
|
|
||||||
where f.id = ?
|
|
||||||
and tpr.profile_id = ?
|
|
||||||
and f.deleted_at is null
|
|
||||||
union all
|
|
||||||
select ppr.is_owner,
|
|
||||||
ppr.is_admin,
|
|
||||||
ppr.can_edit
|
|
||||||
from project_profile_rel as ppr
|
|
||||||
inner join file as f on (f.project_id = ppr.project_id)
|
|
||||||
where f.id = ?
|
|
||||||
and ppr.profile_id = ?
|
|
||||||
and f.deleted_at is null")
|
|
||||||
|
|
||||||
(defn get-file-permissions
|
|
||||||
[conn profile-id file-id]
|
|
||||||
(when (and profile-id file-id)
|
|
||||||
(db/exec! conn [sql:file-permissions
|
|
||||||
file-id profile-id
|
|
||||||
file-id profile-id
|
|
||||||
file-id profile-id])))
|
|
||||||
|
|
||||||
(defn get-permissions
|
|
||||||
([conn profile-id file-id]
|
|
||||||
(let [rows (get-file-permissions conn profile-id file-id)
|
|
||||||
is-owner (boolean (some :is-owner rows))
|
|
||||||
is-admin (boolean (some :is-admin rows))
|
|
||||||
can-edit (boolean (some :can-edit rows))]
|
|
||||||
(when (seq rows)
|
|
||||||
{:type :membership
|
|
||||||
:is-owner is-owner
|
|
||||||
:is-admin (or is-owner is-admin)
|
|
||||||
:can-edit (or is-owner is-admin can-edit)
|
|
||||||
:can-read true
|
|
||||||
:is-logged (some? profile-id)})))
|
|
||||||
|
|
||||||
([conn profile-id file-id share-id]
|
|
||||||
(let [perms (get-permissions conn profile-id file-id)
|
|
||||||
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
|
|
||||||
(dissoc :flags)
|
|
||||||
(update :pages db/decode-pgarray #{}))]
|
|
||||||
|
|
||||||
;; NOTE: in a future when share-link becomes more powerful and
|
|
||||||
;; will allow us specify which parts of the app is available, we
|
|
||||||
;; will probably need to tweak this function in order to expose
|
|
||||||
;; this flags to the frontend.
|
|
||||||
(cond
|
|
||||||
(some? perms) perms
|
|
||||||
(some? ldata) {:type :share-link
|
|
||||||
:can-read true
|
|
||||||
:pages (:pages ldata)
|
|
||||||
:is-logged (some? profile-id)
|
|
||||||
:who-comment (:who-comment ldata)
|
|
||||||
:who-inspect (:who-inspect ldata)}))))
|
|
||||||
|
|
||||||
(def has-edit-permissions?
|
(def has-edit-permissions?
|
||||||
(perms/make-edition-predicate-fn get-permissions))
|
(perms/make-edition-predicate-fn bfc/get-file-permissions))
|
||||||
|
|
||||||
(def has-read-permissions?
|
(def has-read-permissions?
|
||||||
(perms/make-read-predicate-fn get-permissions))
|
(perms/make-read-predicate-fn bfc/get-file-permissions))
|
||||||
|
|
||||||
(def has-comment-permissions?
|
(def has-comment-permissions?
|
||||||
(perms/make-comment-predicate-fn get-permissions))
|
(perms/make-comment-predicate-fn bfc/get-file-permissions))
|
||||||
|
|
||||||
(def check-edition-permissions!
|
(def check-edition-permissions!
|
||||||
(perms/make-check-fn has-edit-permissions?))
|
(perms/make-check-fn has-edit-permissions?))
|
||||||
@@ -170,7 +99,7 @@
|
|||||||
|
|
||||||
(defn check-comment-permissions!
|
(defn check-comment-permissions!
|
||||||
[conn profile-id file-id share-id]
|
[conn profile-id file-id share-id]
|
||||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||||
can-read (has-read-permissions? perms)
|
can-read (has-read-permissions? perms)
|
||||||
can-comment (has-comment-permissions? perms)]
|
can-comment (has-comment-permissions? perms)]
|
||||||
(when-not (or can-read can-comment)
|
(when-not (or can-read can-comment)
|
||||||
@@ -222,7 +151,7 @@
|
|||||||
(defn- get-minimal-file-with-perms
|
(defn- get-minimal-file-with-perms
|
||||||
[cfg {:keys [:id ::rpc/profile-id]}]
|
[cfg {:keys [:id ::rpc/profile-id]}]
|
||||||
(let [mfile (get-minimal-file cfg id)
|
(let [mfile (get-minimal-file cfg id)
|
||||||
perms (get-permissions cfg profile-id id)]
|
perms (bfc/get-file-permissions cfg profile-id id)]
|
||||||
(assoc mfile :permissions perms)))
|
(assoc mfile :permissions perms)))
|
||||||
|
|
||||||
(defn get-file-etag
|
(defn get-file-etag
|
||||||
@@ -248,7 +177,7 @@
|
|||||||
;; will be already prefetched and we just reuse them instead
|
;; will be already prefetched and we just reuse them instead
|
||||||
;; of making an additional database queries.
|
;; of making an additional database queries.
|
||||||
(let [perms (or (:permissions (::cond/object params))
|
(let [perms (or (:permissions (::cond/object params))
|
||||||
(get-permissions conn profile-id id))]
|
(bfc/get-file-permissions conn profile-id id))]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
|
|
||||||
(let [team (teams/get-team conn
|
(let [team (teams/get-team conn
|
||||||
@@ -311,7 +240,7 @@
|
|||||||
::sm/result schema:file-fragment}
|
::sm/result schema:file-fragment}
|
||||||
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
|
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
|
||||||
(db/run! cfg (fn [cfg]
|
(db/run! cfg (fn [cfg]
|
||||||
(let [perms (get-permissions cfg profile-id file-id share-id)]
|
(let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
(-> (get-file-fragment cfg file-id fragment-id)
|
(-> (get-file-fragment cfg file-id fragment-id)
|
||||||
(rph/with-http-cache long-cache-duration))))))
|
(rph/with-http-cache long-cache-duration))))))
|
||||||
@@ -456,8 +385,7 @@
|
|||||||
:code :params-validation
|
:code :params-validation
|
||||||
:hint "page-id is required when object-id is provided"))
|
:hint "page-id is required when object-id is provided"))
|
||||||
|
|
||||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||||
|
|
||||||
file (bfc/get-file cfg file-id :read-only? true)
|
file (bfc/get-file cfg file-id :read-only? true)
|
||||||
|
|
||||||
proj (db/get conn :project {:id (:project-id file)})
|
proj (db/get conn :project {:id (:project-id file)})
|
||||||
@@ -688,11 +616,10 @@
|
|||||||
"Get libraries used by the specified file."
|
"Get libraries used by the specified file."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:get-file-libraries}
|
::sm/params schema:get-file-libraries}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
[cfg {:keys [::rpc/profile-id file-id]}]
|
||||||
(dm/with-open [conn (db/open pool)]
|
(bfc/check-file-exists cfg file-id)
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! cfg profile-id file-id)
|
||||||
(bfc/get-file-libraries conn file-id)))
|
(bfc/get-file-libraries cfg file-id))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: Files that use this File library
|
;; --- COMMAND QUERY: Files that use this File library
|
||||||
|
|
||||||
@@ -777,7 +704,6 @@
|
|||||||
f.created_at,
|
f.created_at,
|
||||||
f.modified_at,
|
f.modified_at,
|
||||||
f.name,
|
f.name,
|
||||||
f.is_shared,
|
|
||||||
f.deleted_at AS will_be_deleted_at,
|
f.deleted_at AS will_be_deleted_at,
|
||||||
ft.media_id AS thumbnail_id,
|
ft.media_id AS thumbnail_id,
|
||||||
row_number() OVER w AS row_num,
|
row_number() OVER w AS row_num,
|
||||||
@@ -785,8 +711,7 @@
|
|||||||
FROM file AS f
|
FROM file AS f
|
||||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||||
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
|
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
|
||||||
AND ft.revn = f.revn
|
AND ft.revn = f.revn)
|
||||||
AND ft.deleted_at is null)
|
|
||||||
WHERE p.team_id = ?
|
WHERE p.team_id = ?
|
||||||
AND (p.deleted_at > ?::timestamptz OR
|
AND (p.deleted_at > ?::timestamptz OR
|
||||||
f.deleted_at > ?::timestamptz)
|
f.deleted_at > ?::timestamptz)
|
||||||
@@ -888,7 +813,7 @@
|
|||||||
AND (f.deleted_at IS NULL OR f.deleted_at > now())
|
AND (f.deleted_at IS NULL OR f.deleted_at > now())
|
||||||
ORDER BY f.created_at ASC;")
|
ORDER BY f.created_at ASC;")
|
||||||
|
|
||||||
(defn- absorb-library-by-file!
|
(defn- absorb-library-by-file
|
||||||
[cfg ldata file-id]
|
[cfg ldata file-id]
|
||||||
|
|
||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection-map? cfg)
|
||||||
@@ -912,7 +837,7 @@
|
|||||||
:modified-at (ct/now)
|
:modified-at (ct/now)
|
||||||
:has-media-trimmed false}))))
|
:has-media-trimmed false}))))
|
||||||
|
|
||||||
(defn- absorb-library
|
(defn- absorb-library*
|
||||||
"Find all files using a shared library, and absorb all library assets
|
"Find all files using a shared library, and absorb all library assets
|
||||||
into the file local libraries"
|
into the file local libraries"
|
||||||
[cfg {:keys [id data] :as library}]
|
[cfg {:keys [id data] :as library}]
|
||||||
@@ -927,10 +852,10 @@
|
|||||||
:library-id (str id)
|
:library-id (str id)
|
||||||
:files (str/join "," (map str ids)))
|
:files (str/join "," (map str ids)))
|
||||||
|
|
||||||
(run! (partial absorb-library-by-file! cfg data) ids)
|
(run! (partial absorb-library-by-file cfg data) ids)
|
||||||
library))
|
library))
|
||||||
|
|
||||||
(defn absorb-library!
|
(defn absorb-library
|
||||||
[{:keys [::db/conn] :as cfg} id]
|
[{:keys [::db/conn] :as cfg} id]
|
||||||
(let [file (-> (bfc/get-file cfg id
|
(let [file (-> (bfc/get-file cfg id
|
||||||
:realize? true
|
:realize? true
|
||||||
@@ -947,7 +872,7 @@
|
|||||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||||
(cfeat/check-file-features! (:features file)))
|
(cfeat/check-file-features! (:features file)))
|
||||||
|
|
||||||
(absorb-library cfg file)))
|
(absorb-library* cfg file)))
|
||||||
|
|
||||||
(defn- set-file-shared
|
(defn- set-file-shared
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
@@ -960,14 +885,14 @@
|
|||||||
;; file, we need to perform more complex operation,
|
;; file, we need to perform more complex operation,
|
||||||
;; so in this case we retrieve the complete file and
|
;; so in this case we retrieve the complete file and
|
||||||
;; perform all required validations.
|
;; perform all required validations.
|
||||||
(let [file (-> (absorb-library! cfg id)
|
(let [file (-> (absorb-library cfg id)
|
||||||
(assoc :is-shared false))]
|
(assoc :is-shared false))]
|
||||||
(db/delete! conn :file-library-rel {:library-file-id id})
|
(db/delete! conn :file-library-rel {:library-file-id id})
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:is-shared false
|
{:is-shared false
|
||||||
:modified-at (ct/now)}
|
:modified-at (ct/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
(select-keys file [:id :name :is-shared]))
|
file)
|
||||||
|
|
||||||
(and (false? (:is-shared file))
|
(and (false? (:is-shared file))
|
||||||
(true? (:is-shared params)))
|
(true? (:is-shared params)))
|
||||||
@@ -1014,6 +939,11 @@
|
|||||||
{:id file-id}
|
{:id file-id}
|
||||||
{::db/return-keys [:id :name :is-shared :deleted-at
|
{::db/return-keys [:id :name :is-shared :deleted-at
|
||||||
:project-id :created-at :modified-at]})]
|
:project-id :created-at :modified-at]})]
|
||||||
|
|
||||||
|
;; Remove all possible relations for that file
|
||||||
|
(db/delete! conn :file-library-rel
|
||||||
|
{:library-file-id file-id})
|
||||||
|
|
||||||
(wrk/submit! {::db/conn conn
|
(wrk/submit! {::db/conn conn
|
||||||
::wrk/task :delete-object
|
::wrk/task :delete-object
|
||||||
::wrk/params {:object :file
|
::wrk/params {:object :file
|
||||||
@@ -1164,47 +1094,53 @@
|
|||||||
|
|
||||||
;; --- MUTATION COMMAND: delete-files-immediatelly
|
;; --- MUTATION COMMAND: delete-files-immediatelly
|
||||||
|
|
||||||
(def ^:private sql:delete-team-files
|
(def ^:private sql:get-delete-team-files-candidates
|
||||||
"UPDATE file AS uf SET deleted_at = ?::timestamptz
|
"SELECT f.id
|
||||||
FROM (
|
FROM file AS f
|
||||||
SELECT f.id
|
JOIN project AS p ON (p.id = f.project_id)
|
||||||
FROM file AS f
|
JOIN team AS t ON (t.id = p.team_id)
|
||||||
JOIN project AS p ON (p.id = f.project_id)
|
WHERE t.deleted_at IS NULL
|
||||||
JOIN team AS t ON (t.id = p.team_id)
|
AND t.id = ?
|
||||||
WHERE t.deleted_at IS NULL
|
AND f.id = ANY(?::uuid[])")
|
||||||
AND t.id = ?
|
|
||||||
AND f.id = ANY(?::uuid[])
|
|
||||||
) AS subquery
|
|
||||||
WHERE uf.id = subquery.id
|
|
||||||
RETURNING uf.id, uf.deleted_at;")
|
|
||||||
|
|
||||||
(def ^:private schema:permanently-delete-team-files
|
(def ^:private schema:permanently-delete-team-files
|
||||||
[:map {:title "permanently-delete-team-files"}
|
[:map {:title "permanently-delete-team-files"}
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
[:ids [::sm/set ::sm/uuid]]])
|
[:ids [::sm/set ::sm/uuid]]])
|
||||||
|
|
||||||
|
(defn- permanently-delete-team-files
|
||||||
|
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
|
||||||
|
(let [ids (into #{}
|
||||||
|
d/xf:map-id
|
||||||
|
(db/exec! conn [sql:get-delete-team-files-candidates team-id
|
||||||
|
(db/create-array conn "uuid" ids)]))]
|
||||||
|
|
||||||
|
(reduce (fn [acc id]
|
||||||
|
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
|
||||||
|
(db/update! conn :file
|
||||||
|
{:deleted-at request-at}
|
||||||
|
{:id id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
(wrk/submit! {::db/conn conn
|
||||||
|
::wrk/task :delete-object
|
||||||
|
::wrk/params {:object :file
|
||||||
|
:deleted-at request-at
|
||||||
|
:id id}})
|
||||||
|
(conj acc id))
|
||||||
|
#{}
|
||||||
|
ids)))
|
||||||
|
|
||||||
(sv/defmethod ::permanently-delete-team-files
|
(sv/defmethod ::permanently-delete-team-files
|
||||||
"Mark the specified files to be deleted immediatelly on the
|
"Mark the specified files to be deleted immediatelly on the
|
||||||
specified team. The team-id on params will be used to filter and
|
specified team. The team-id on params will be used to filter and
|
||||||
check writable permissons on team."
|
check writable permissons on team."
|
||||||
|
|
||||||
{::doc/added "2.12"
|
{::doc/added "2.13"
|
||||||
::sm/params schema:permanently-delete-team-files
|
::sm/params schema:permanently-delete-team-files}
|
||||||
::db/transaction true}
|
|
||||||
|
|
||||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
(teams/check-edition-permissions! conn profile-id team-id)
|
(teams/check-edition-permissions! pool profile-id team-id)
|
||||||
|
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
|
||||||
(reduce (fn [acc {:keys [id deleted-at]}]
|
|
||||||
(wrk/submit! {::db/conn conn
|
|
||||||
::wrk/task :delete-object
|
|
||||||
::wrk/params {:object :file
|
|
||||||
:deleted-at deleted-at
|
|
||||||
:id id}})
|
|
||||||
(conj acc id))
|
|
||||||
#{}
|
|
||||||
(db/plan conn [sql:delete-team-files request-at team-id
|
|
||||||
(db/create-array conn "uuid" ids)])))
|
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: restore-files-immediatelly
|
;; --- MUTATION COMMAND: restore-files-immediatelly
|
||||||
|
|
||||||
@@ -1268,7 +1204,7 @@
|
|||||||
{:keys [files projects]}
|
{:keys [files projects]}
|
||||||
(reduce (fn [result {:keys [id project-id]}]
|
(reduce (fn [result {:keys [id project-id]}]
|
||||||
(let [index (-> result :files count)]
|
(let [index (-> result :files count)]
|
||||||
(events/tap :progress {:file-id id :index index :total total-files})
|
(events/tap :progress {:file-id id :index (inc index) :total total-files})
|
||||||
(restore-file conn id)
|
(restore-file conn id)
|
||||||
|
|
||||||
(-> result
|
(-> result
|
||||||
@@ -1291,7 +1227,7 @@
|
|||||||
(sv/defmethod ::restore-deleted-team-files
|
(sv/defmethod ::restore-deleted-team-files
|
||||||
"Removes the deletion mark from the specified files (and respective
|
"Removes the deletion mark from the specified files (and respective
|
||||||
projects) on the specified team."
|
projects) on the specified team."
|
||||||
{::doc/added "2.12"
|
{::doc/added "2.13"
|
||||||
::sse/stream? true
|
::sse/stream? true
|
||||||
::sm/params schema:restore-deleted-team-files}
|
::sm/params schema:restore-deleted-team-files}
|
||||||
[cfg params]
|
[cfg params]
|
||||||
|
|||||||
@@ -199,15 +199,13 @@
|
|||||||
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
|
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
|
||||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
|
|
||||||
(let [team (teams/get-team conn
|
(let [team (teams/get-team conn
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:file-id file-id)
|
:file-id file-id)
|
||||||
|
|
||||||
file (bfc/get-file cfg file-id
|
file (bfc/get-file cfg file-id
|
||||||
|
:include-deleted? true
|
||||||
:realize? true
|
:realize? true
|
||||||
:read-only? true)
|
:read-only? true)
|
||||||
|
|
||||||
strip-frames-with-thumbnails
|
strip-frames-with-thumbnails
|
||||||
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
|
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
|
||||||
(true? strip-frames-with-thumbnails))]
|
(true? strip-frames-with-thumbnails))]
|
||||||
@@ -333,12 +331,16 @@
|
|||||||
|
|
||||||
;; --- MUTATION COMMAND: create-file-thumbnail
|
;; --- MUTATION COMMAND: create-file-thumbnail
|
||||||
|
|
||||||
(defn- create-file-thumbnail!
|
(defn- create-file-thumbnail
|
||||||
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
|
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id revn props media] :as params}]
|
||||||
(media/validate-media-type! media)
|
(media/validate-media-type! media)
|
||||||
(media/validate-media-size! media)
|
(media/validate-media-size! media)
|
||||||
|
|
||||||
(let [props (db/tjson (or props {}))
|
(let [file (bfc/get-file cfg file-id
|
||||||
|
:include-deleted? true
|
||||||
|
:load-data? false)
|
||||||
|
|
||||||
|
props (db/tjson (or props {}))
|
||||||
path (:path media)
|
path (:path media)
|
||||||
mtype (:mtype media)
|
mtype (:mtype media)
|
||||||
hash (sto/calculate-hash path)
|
hash (sto/calculate-hash path)
|
||||||
@@ -367,7 +369,7 @@
|
|||||||
|
|
||||||
(db/update! conn :file-thumbnail
|
(db/update! conn :file-thumbnail
|
||||||
{:media-id (:id media)
|
{:media-id (:id media)
|
||||||
:deleted-at nil
|
:deleted-at (:deleted-at file)
|
||||||
:updated-at tnow
|
:updated-at tnow
|
||||||
:props props}
|
:props props}
|
||||||
{:file-id file-id
|
{:file-id file-id
|
||||||
@@ -378,6 +380,7 @@
|
|||||||
:revn revn
|
:revn revn
|
||||||
:created-at tnow
|
:created-at tnow
|
||||||
:updated-at tnow
|
:updated-at tnow
|
||||||
|
:deleted-at (:deleted-at file)
|
||||||
:props props
|
:props props
|
||||||
:media-id (:id media)}))
|
:media-id (:id media)}))
|
||||||
|
|
||||||
@@ -402,6 +405,8 @@
|
|||||||
::rtry/when rtry/conflict-exception?
|
::rtry/when rtry/conflict-exception?
|
||||||
::sm/params schema:create-file-thumbnail}
|
::sm/params schema:create-file-thumbnail}
|
||||||
|
|
||||||
|
;; FIXME: do not run the thumbnail upload inside a transaction
|
||||||
|
|
||||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
;; TODO For now we check read permissions instead of write,
|
;; TODO For now we check read permissions instead of write,
|
||||||
@@ -409,6 +414,6 @@
|
|||||||
;; review this approach on the future.
|
;; review this approach on the future.
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
(when-not (db/read-only? conn)
|
(when-not (db/read-only? conn)
|
||||||
(let [media (create-file-thumbnail! cfg params)]
|
(let [media (create-file-thumbnail cfg params)]
|
||||||
{:uri (files/resolve-public-uri (:id media))
|
{:uri (files/resolve-public-uri (:id media))
|
||||||
:id (:id media)})))))
|
:id (:id media)})))))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.fonts
|
(ns app.rpc.commands.fonts
|
||||||
(:require
|
(:require
|
||||||
|
[app.binfile.common :as bfc]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
(uuid? file-id)
|
(uuid? file-id)
|
||||||
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
|
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
|
||||||
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
|
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
|
||||||
perms (files/get-permissions conn profile-id file-id share-id)]
|
perms (bfc/get-file-permissions conn profile-id file-id share-id)]
|
||||||
(files/check-read-permissions! perms)
|
(files/check-read-permissions! perms)
|
||||||
(db/query conn :team-font-variant
|
(db/query conn :team-font-variant
|
||||||
{:team-id (:team-id project)
|
{:team-id (:team-id project)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
|
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
|
||||||
where tpr.profile_id = ?
|
where tpr.profile_id = ?
|
||||||
and p.team_id = ?
|
and p.team_id = ?
|
||||||
and (p.deleted_at is null or p.deleted_at > now())
|
and (p.deleted_at is null)
|
||||||
and (tpr.is_admin = true or
|
and (tpr.is_admin = true or
|
||||||
tpr.is_owner = true or
|
tpr.is_owner = true or
|
||||||
tpr.can_edit = true)
|
tpr.can_edit = true)
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
inner join project_profile_rel as ppr on (ppr.project_id = p.id)
|
inner join project_profile_rel as ppr on (ppr.project_id = p.id)
|
||||||
where ppr.profile_id = ?
|
where ppr.profile_id = ?
|
||||||
and p.team_id = ?
|
and p.team_id = ?
|
||||||
and (p.deleted_at is null or p.deleted_at > now())
|
and (p.deleted_at is null)
|
||||||
and (ppr.is_admin = true or
|
and (ppr.is_admin = true or
|
||||||
ppr.is_owner = true or
|
ppr.is_owner = true or
|
||||||
ppr.can_edit = true)
|
ppr.can_edit = true)
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
|
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
|
||||||
inner join projects as pr on (f.project_id = pr.id)
|
inner join projects as pr on (f.project_id = pr.id)
|
||||||
where f.name ilike ('%' || ? || '%')
|
where f.name ilike ('%' || ? || '%')
|
||||||
and (f.deleted_at is null or f.deleted_at > now())
|
and (f.deleted_at is null)
|
||||||
order by f.created_at asc")
|
order by f.created_at asc")
|
||||||
|
|
||||||
(defn search-files
|
(defn search-files
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.cond :as-alias cond]
|
[app.rpc.cond :as-alias cond]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
[system {:keys [::rpc/profile-id file-id share-id] :as params}]
|
[system {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||||
(db/run! system
|
(db/run! system
|
||||||
(fn [{:keys [::db/conn] :as system}]
|
(fn [{:keys [::db/conn] :as system}]
|
||||||
(let [perms (files/get-permissions conn profile-id file-id share-id)
|
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||||
params (-> params
|
params (-> params
|
||||||
(assoc ::perms perms)
|
(assoc ::perms perms)
|
||||||
(assoc :profile-id profile-id))]
|
(assoc :profile-id profile-id))]
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
:deleted-at (ct/format-inst deleted-at))
|
:deleted-at (ct/format-inst deleted-at))
|
||||||
|
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:deleted-at deleted-at}
|
{:deleted-at deleted-at
|
||||||
|
:is-shared false}
|
||||||
{:id id}
|
{:id id}
|
||||||
{::db/return-keys false})
|
{::db/return-keys false})
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
(not *team-deletion*))
|
(not *team-deletion*))
|
||||||
;; NOTE: we don't prevent file deletion on absorb operation failure
|
;; NOTE: we don't prevent file deletion on absorb operation failure
|
||||||
(try
|
(try
|
||||||
(db/tx-run! cfg files/absorb-library! id)
|
(db/tx-run! cfg files/absorb-library id)
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "error on absorbing library"
|
(l/warn :hint "error on absorbing library"
|
||||||
:file-id id
|
:file-id id
|
||||||
|
|||||||
@@ -595,8 +595,8 @@
|
|||||||
(px/exec! :virtual #(rcp/write-body-to-stream body nil output))
|
(px/exec! :virtual #(rcp/write-body-to-stream body nil output))
|
||||||
(into []
|
(into []
|
||||||
(map (fn [{:keys [event data]}]
|
(map (fn [{:keys [event data]}]
|
||||||
[(keyword event)
|
(d/vec2 (keyword event)
|
||||||
(tr/decode-str data)]))
|
(tr/decode-str data))))
|
||||||
(parse-sse (slurp' input)))
|
(parse-sse (slurp' input)))
|
||||||
(finally
|
(finally
|
||||||
(.close input)))))
|
(.close input)))))
|
||||||
|
|||||||
@@ -1921,7 +1921,11 @@
|
|||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
(t/is (= (:ids data) result)))
|
(t/is (fn? result))
|
||||||
|
|
||||||
|
(let [[ev1 ev2 :as events] (th/consume-sse result)]
|
||||||
|
(t/is (= 2 (count events)))
|
||||||
|
(t/is (= (:ids data) (val ev2)))))
|
||||||
|
|
||||||
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
||||||
(t/is (= (:deleted-at row) now)))))))
|
(t/is (= (:deleted-at row) now)))))))
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||||
integrant/integrant {:mvn/version "1.0.0"}
|
integrant/integrant {:mvn/version "1.0.0"}
|
||||||
|
|
||||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
|
||||||
funcool/cuerdas {:mvn/version "2026.415"}
|
funcool/cuerdas {:mvn/version "2026.415"}
|
||||||
funcool/promesa
|
funcool/promesa
|
||||||
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
|
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
|
||||||
|
|||||||
@@ -1024,6 +1024,26 @@
|
|||||||
:clj
|
:clj
|
||||||
(sort comp-fn items))))
|
(sort comp-fn items))))
|
||||||
|
|
||||||
|
(defn obfuscate-string
|
||||||
|
"Obfuscates potentially sensitive values.
|
||||||
|
|
||||||
|
- One-arg arity:
|
||||||
|
* For strings shorter than 10 characters, all characters are replaced by `*`.
|
||||||
|
* For longer strings, the first 5 characters are preserved and the rest obfuscated.
|
||||||
|
- Two-arg arity accepts a boolean `full?` that, when true, replaces the whole value
|
||||||
|
by `*`, preserving only the length."
|
||||||
|
([v]
|
||||||
|
(obfuscate-string v false))
|
||||||
|
([v full?]
|
||||||
|
(let [s (str v)
|
||||||
|
n (count s)]
|
||||||
|
(cond
|
||||||
|
(zero? n) s
|
||||||
|
full? (apply str (repeat n "*"))
|
||||||
|
(< n 10) (apply str (repeat n "*"))
|
||||||
|
:else (str (subs s 0 5)
|
||||||
|
(apply str (repeat (- n 5) "*")))))))
|
||||||
|
|
||||||
(defn reorder
|
(defn reorder
|
||||||
"Reorder a vector by moving one of their items from some position to some space between positions.
|
"Reorder a vector by moving one of their items from some position to some space between positions.
|
||||||
It clamps the position numbers to a valid range."
|
It clamps the position numbers to a valid range."
|
||||||
|
|||||||
@@ -10,16 +10,20 @@
|
|||||||
(:refer-clojure :exclude [instance?])
|
(:refer-clojure :exclude [instance?])
|
||||||
(:require
|
(:require
|
||||||
#?(:clj [clojure.stacktrace :as strace])
|
#?(:clj [clojure.stacktrace :as strace])
|
||||||
|
[app.common.data :refer [obfuscate-string]]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str])
|
||||||
[expound.alpha :as expound])
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import
|
(:import
|
||||||
clojure.lang.IPersistentMap)))
|
clojure.lang.IPersistentMap)))
|
||||||
|
|
||||||
|
(def ^:private sensitive-fields
|
||||||
|
"Keys whose values must be obfuscated in validation explains."
|
||||||
|
#{:password :old-password :token :invitation-token})
|
||||||
|
|
||||||
#?(:clj (set! *warn-on-reflection* true))
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
(def ^:dynamic *data-length* 8)
|
(def ^:dynamic *data-length* 8)
|
||||||
@@ -110,15 +114,26 @@
|
|||||||
(contains? data :explain))
|
(contains? data :explain))
|
||||||
(explain (:explain data) opts)
|
(explain (:explain data) opts)
|
||||||
|
|
||||||
(and (contains? data ::s/problems)
|
|
||||||
(contains? data ::s/value)
|
|
||||||
(contains? data ::s/spec))
|
|
||||||
(binding [s/*explain-out* expound/printer]
|
|
||||||
(with-out-str
|
|
||||||
(s/explain-out (update data ::s/problems #(take (:length opts 10) %)))))
|
|
||||||
|
|
||||||
(contains? data ::sm/explain)
|
(contains? data ::sm/explain)
|
||||||
(sm/humanize-explain (::sm/explain data) opts)))
|
(let [exp (::sm/explain data)
|
||||||
|
sanitize-map (fn sanitize-map [m]
|
||||||
|
(reduce-kv
|
||||||
|
(fn [acc k v]
|
||||||
|
(let [k* (if (string? k) (keyword k) k)]
|
||||||
|
(cond
|
||||||
|
(contains? sensitive-fields k*)
|
||||||
|
(assoc acc k (if (map? v)
|
||||||
|
(sanitize-map v)
|
||||||
|
(obfuscate-string v true)))
|
||||||
|
|
||||||
|
(map? v) (assoc acc k (sanitize-map v))
|
||||||
|
:else (assoc acc k v))))
|
||||||
|
{}
|
||||||
|
m))
|
||||||
|
sanitize-explain (fn [exp]
|
||||||
|
(cond-> exp
|
||||||
|
(:value exp) (update :value sanitize-map)))]
|
||||||
|
(sm/humanize-explain (sanitize-explain exp) opts))))
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(defn format-throwable
|
(defn format-throwable
|
||||||
|
|||||||
@@ -526,20 +526,25 @@
|
|||||||
ids))
|
ids))
|
||||||
|
|
||||||
(defn clean-loops
|
(defn clean-loops
|
||||||
"Clean a list of ids from circular references."
|
"Clean a list of ids from circular references. Optimized fast-path for single selections."
|
||||||
[objects ids]
|
[objects ids]
|
||||||
(let [parent-selected?
|
(if (<= (count ids) 1)
|
||||||
(fn [id]
|
;; For single selection, there can't be circularity; return as ordered-set.
|
||||||
(let [parents (get-parent-ids objects id)]
|
(into (d/ordered-set) ids)
|
||||||
(some ids parents)))
|
(let [ids-set (if (set? ids) ids (set ids))
|
||||||
|
parent-selected?
|
||||||
|
(fn [id]
|
||||||
|
;; Stop early as soon as we find any selected parent
|
||||||
|
(let [parents (get-parent-ids objects id)]
|
||||||
|
(some #(contains? ids-set %) parents)))
|
||||||
|
|
||||||
add-element
|
add-element
|
||||||
(fn [result id]
|
(fn [result id]
|
||||||
(cond-> result
|
(cond-> result
|
||||||
(not (parent-selected? id))
|
(not (parent-selected? id))
|
||||||
(conj id)))]
|
(conj id)))]
|
||||||
|
|
||||||
(reduce add-element (d/ordered-set) ids)))
|
(reduce add-element (d/ordered-set) ids))))
|
||||||
|
|
||||||
(defn- indexed-shapes
|
(defn- indexed-shapes
|
||||||
"Retrieves a vector with the indexes for each element in the layer
|
"Retrieves a vector with the indexes for each element in the layer
|
||||||
|
|||||||
@@ -82,6 +82,113 @@
|
|||||||
(declare create-svg-children)
|
(declare create-svg-children)
|
||||||
(declare parse-svg-element)
|
(declare parse-svg-element)
|
||||||
|
|
||||||
|
(defn- process-gradient-stops
|
||||||
|
"Processes gradient stops to extract stop-color and stop-opacity from style attributes
|
||||||
|
and convert them to direct attributes. This ensures stops with style='stop-color:#...;stop-opacity:1'
|
||||||
|
are properly converted to stop-color and stop-opacity attributes."
|
||||||
|
[stops]
|
||||||
|
(mapv (fn [stop]
|
||||||
|
(let [stop-attrs (:attrs stop)
|
||||||
|
stop-style (get stop-attrs :style)
|
||||||
|
;; Parse style if it's a string using csvg/parse-style utility
|
||||||
|
parsed-style (when (and (string? stop-style) (seq stop-style))
|
||||||
|
(csvg/parse-style stop-style))
|
||||||
|
;; Extract stop-color and stop-opacity from style
|
||||||
|
style-stop-color (when parsed-style (:stop-color parsed-style))
|
||||||
|
style-stop-opacity (when parsed-style (:stop-opacity parsed-style))
|
||||||
|
;; Merge: use direct attributes first, then style values as fallback
|
||||||
|
final-attrs (cond-> stop-attrs
|
||||||
|
(and style-stop-color (not (contains? stop-attrs :stop-color)))
|
||||||
|
(assoc :stop-color style-stop-color)
|
||||||
|
|
||||||
|
(and style-stop-opacity (not (contains? stop-attrs :stop-opacity)))
|
||||||
|
(assoc :stop-opacity style-stop-opacity)
|
||||||
|
|
||||||
|
;; Remove style attribute if we've extracted its values
|
||||||
|
(or style-stop-color style-stop-opacity)
|
||||||
|
(dissoc :style))]
|
||||||
|
(assoc stop :attrs final-attrs)))
|
||||||
|
stops))
|
||||||
|
|
||||||
|
(defn- resolve-gradient-href
|
||||||
|
"Resolves xlink:href references in gradients by merging the referenced gradient's
|
||||||
|
stops and attributes with the referencing gradient. This ensures gradients that
|
||||||
|
reference other gradients (like linearGradient3550 referencing linearGradient3536)
|
||||||
|
inherit the stops from the base gradient.
|
||||||
|
|
||||||
|
According to SVG spec, when a gradient has xlink:href:
|
||||||
|
- It inherits all attributes from the referenced gradient
|
||||||
|
- It inherits all stops from the referenced gradient
|
||||||
|
- The referencing gradient's attributes override the base ones
|
||||||
|
- If the referencing gradient has stops, they replace the base stops
|
||||||
|
|
||||||
|
Returns the defs map with all gradient href references resolved."
|
||||||
|
[defs]
|
||||||
|
(letfn [(resolve-gradient [gradient-id gradient-node defs visited]
|
||||||
|
(if (contains? visited gradient-id)
|
||||||
|
(do
|
||||||
|
#?(:cljs (js/console.warn "[resolve-gradient] Circular reference detected for" gradient-id)
|
||||||
|
:clj nil)
|
||||||
|
gradient-node) ;; Avoid circular references
|
||||||
|
(let [attrs (:attrs gradient-node)
|
||||||
|
href-id (or (:href attrs) (:xlink:href attrs))
|
||||||
|
href-id (when (and (string? href-id) (pos? (count href-id)))
|
||||||
|
(subs href-id 1)) ;; Remove leading #
|
||||||
|
|
||||||
|
base-gradient (when (and href-id (contains? defs href-id))
|
||||||
|
(get defs href-id))
|
||||||
|
|
||||||
|
resolved-base (when base-gradient (resolve-gradient href-id base-gradient defs (conj visited gradient-id)))]
|
||||||
|
|
||||||
|
(if resolved-base
|
||||||
|
;; Merge: base gradient attributes + referencing gradient attributes
|
||||||
|
;; Use referencing gradient's stops if present, otherwise use base stops
|
||||||
|
(let [base-attrs (:attrs resolved-base)
|
||||||
|
ref-attrs (:attrs gradient-node)
|
||||||
|
|
||||||
|
;; Start with base attributes (without id), then merge with ref attributes
|
||||||
|
;; This ensures ref attributes override base ones
|
||||||
|
base-attrs-clean (dissoc base-attrs :id)
|
||||||
|
ref-attrs-clean (dissoc ref-attrs :href :xlink:href :id)
|
||||||
|
|
||||||
|
;; Special handling for gradientTransform: if both have it, combine them
|
||||||
|
base-transform (get base-attrs :gradientTransform)
|
||||||
|
ref-transform (get ref-attrs :gradientTransform)
|
||||||
|
combined-transform (cond
|
||||||
|
(and base-transform ref-transform)
|
||||||
|
(str base-transform " " ref-transform) ;; Apply base first, then ref
|
||||||
|
:else (or ref-transform base-transform))
|
||||||
|
|
||||||
|
;; Merge attributes: base first, then ref (ref overrides)
|
||||||
|
merged-attrs (-> (d/deep-merge base-attrs-clean ref-attrs-clean)
|
||||||
|
(cond-> combined-transform
|
||||||
|
(assoc :gradientTransform combined-transform)))
|
||||||
|
|
||||||
|
;; If referencing gradient has content (stops), use it; otherwise use base content
|
||||||
|
final-content (if (seq (:content gradient-node))
|
||||||
|
(:content gradient-node)
|
||||||
|
(:content resolved-base))
|
||||||
|
|
||||||
|
;; Process stops to extract stop-color and stop-opacity from style attributes
|
||||||
|
processed-content (process-gradient-stops final-content)
|
||||||
|
|
||||||
|
result {:tag (:tag gradient-node)
|
||||||
|
:attrs (assoc merged-attrs :id gradient-id)
|
||||||
|
:content processed-content}]
|
||||||
|
result)
|
||||||
|
;; Process stops even for gradients without references to extract style attributes
|
||||||
|
(let [processed-content (process-gradient-stops (:content gradient-node))]
|
||||||
|
(assoc gradient-node :content processed-content))))))]
|
||||||
|
(let [gradient-tags #{:linearGradient :radialGradient}
|
||||||
|
result (reduce-kv
|
||||||
|
(fn [acc id node]
|
||||||
|
(if (contains? gradient-tags (:tag node))
|
||||||
|
(assoc acc id (resolve-gradient id node defs #{}))
|
||||||
|
(assoc acc id node)))
|
||||||
|
{}
|
||||||
|
defs)]
|
||||||
|
result)))
|
||||||
|
|
||||||
(defn create-svg-shapes
|
(defn create-svg-shapes
|
||||||
([svg-data pos objects frame-id parent-id selected center?]
|
([svg-data pos objects frame-id parent-id selected center?]
|
||||||
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
|
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
|
||||||
@@ -112,6 +219,9 @@
|
|||||||
(csvg/fix-percents)
|
(csvg/fix-percents)
|
||||||
(csvg/extract-defs))
|
(csvg/extract-defs))
|
||||||
|
|
||||||
|
;; Resolve gradient href references in all defs before processing shapes
|
||||||
|
def-nodes (resolve-gradient-href def-nodes)
|
||||||
|
|
||||||
;; In penpot groups have the size of their children. To
|
;; In penpot groups have the size of their children. To
|
||||||
;; respect the imported svg size and empty space let's create
|
;; respect the imported svg size and empty space let's create
|
||||||
;; a transparent shape as background to respect the imported
|
;; a transparent shape as background to respect the imported
|
||||||
@@ -142,12 +252,23 @@
|
|||||||
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
|
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
|
||||||
[unames []]
|
[unames []]
|
||||||
(d/enumerate (->> (:content svg-data)
|
(d/enumerate (->> (:content svg-data)
|
||||||
(mapv #(csvg/inherit-attributes root-attrs %)))))]
|
(mapv #(csvg/inherit-attributes root-attrs %)))))
|
||||||
|
|
||||||
[root-shape children])))
|
;; Collect all defs from children and merge into root shape
|
||||||
|
all-defs-from-children (reduce (fn [acc child]
|
||||||
|
(if-let [child-defs (:svg-defs child)]
|
||||||
|
(merge acc child-defs)
|
||||||
|
acc))
|
||||||
|
{}
|
||||||
|
children)
|
||||||
|
|
||||||
|
;; Merge defs from svg-data and children into root shape
|
||||||
|
root-shape-with-defs (assoc root-shape :svg-defs (merge def-nodes all-defs-from-children))]
|
||||||
|
|
||||||
|
[root-shape-with-defs children])))
|
||||||
|
|
||||||
(defn create-raw-svg
|
(defn create-raw-svg
|
||||||
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
|
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs] :as data}]
|
||||||
(let [props (csvg/attrs->props attrs)
|
(let [props (csvg/attrs->props attrs)
|
||||||
vbox (grc/make-rect offset-x offset-y width height)]
|
vbox (grc/make-rect offset-x offset-y width height)]
|
||||||
(cts/setup-shape
|
(cts/setup-shape
|
||||||
@@ -160,10 +281,11 @@
|
|||||||
:y y
|
:y y
|
||||||
:content data
|
:content data
|
||||||
:svg-attrs props
|
:svg-attrs props
|
||||||
:svg-viewbox vbox})))
|
:svg-viewbox vbox
|
||||||
|
:svg-defs defs})))
|
||||||
|
|
||||||
(defn create-svg-root
|
(defn create-svg-root
|
||||||
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
|
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs defs] :as svg-data}]
|
||||||
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
|
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
|
||||||
(d/without-keys csvg/inheritable-props)
|
(d/without-keys csvg/inheritable-props)
|
||||||
(csvg/attrs->props))]
|
(csvg/attrs->props))]
|
||||||
@@ -177,7 +299,8 @@
|
|||||||
:height height
|
:height height
|
||||||
:x (+ x offset-x)
|
:x (+ x offset-x)
|
||||||
:y (+ y offset-y)
|
:y (+ y offset-y)
|
||||||
:svg-attrs props})))
|
:svg-attrs props
|
||||||
|
:svg-defs defs})))
|
||||||
|
|
||||||
(defn create-svg-children
|
(defn create-svg-children
|
||||||
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
|
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
|
||||||
@@ -198,7 +321,7 @@
|
|||||||
|
|
||||||
|
|
||||||
(defn create-group
|
(defn create-group
|
||||||
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
|
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs]}]
|
||||||
(let [transform (csvg/parse-transform (:transform attrs))
|
(let [transform (csvg/parse-transform (:transform attrs))
|
||||||
attrs (-> attrs
|
attrs (-> attrs
|
||||||
(d/without-keys csvg/inheritable-props)
|
(d/without-keys csvg/inheritable-props)
|
||||||
@@ -214,7 +337,8 @@
|
|||||||
:height height
|
:height height
|
||||||
:svg-transform transform
|
:svg-transform transform
|
||||||
:svg-attrs attrs
|
:svg-attrs attrs
|
||||||
:svg-viewbox vbox})))
|
:svg-viewbox vbox
|
||||||
|
:svg-defs defs})))
|
||||||
|
|
||||||
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||||
(when (and (contains? attrs :d) (seq (:d attrs)))
|
(when (and (contains? attrs :d) (seq (:d attrs)))
|
||||||
@@ -523,6 +647,21 @@
|
|||||||
:else (dm/str tag))]
|
:else (dm/str tag))]
|
||||||
(dm/str "svg-" suffix)))
|
(dm/str "svg-" suffix)))
|
||||||
|
|
||||||
|
(defn- filter-valid-def-references
|
||||||
|
"Filters out false positive references that are not valid def IDs.
|
||||||
|
Filters out:
|
||||||
|
- Colors in style attributes (hex colors like #f9dd67)
|
||||||
|
- Style fragments that contain CSS keywords (like stop-opacity)
|
||||||
|
- References that don't exist in defs"
|
||||||
|
[ref-ids defs]
|
||||||
|
(let [is-style-fragment? (fn [ref-id]
|
||||||
|
(or (clr/hex-color-string? (str "#" ref-id))
|
||||||
|
(str/includes? ref-id ";") ;; Contains CSS separator
|
||||||
|
(str/includes? ref-id "stop-opacity") ;; CSS keyword
|
||||||
|
(str/includes? ref-id "stop-color")))] ;; CSS keyword
|
||||||
|
(->> ref-ids
|
||||||
|
(remove is-style-fragment?) ;; Filter style fragments and hex colors
|
||||||
|
(filter #(contains? defs %))))) ;; Only existing defs
|
||||||
|
|
||||||
(defn parse-svg-element
|
(defn parse-svg-element
|
||||||
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
|
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
|
||||||
@@ -534,7 +673,11 @@
|
|||||||
(let [name (or (:id attrs) (tag->name tag))
|
(let [name (or (:id attrs) (tag->name tag))
|
||||||
att-refs (csvg/find-attr-references attrs)
|
att-refs (csvg/find-attr-references attrs)
|
||||||
defs (get svg-data :defs)
|
defs (get svg-data :defs)
|
||||||
references (csvg/find-def-references defs att-refs)
|
valid-refs (filter-valid-def-references att-refs defs)
|
||||||
|
all-refs (csvg/find-def-references defs valid-refs)
|
||||||
|
;; Filter the final result to ensure all references are valid defs
|
||||||
|
;; This prevents false positives from style attributes in gradient stops
|
||||||
|
references (filter-valid-def-references all-refs defs)
|
||||||
|
|
||||||
href-id (or (:href attrs) (:xlink:href attrs) " ")
|
href-id (or (:href attrs) (:xlink:href attrs) " ")
|
||||||
href-id (if (and (string? href-id)
|
href-id (if (and (string? href-id)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@
|
|||||||
:enable-component-thumbnails
|
:enable-component-thumbnails
|
||||||
:enable-render-wasm-dpr
|
:enable-render-wasm-dpr
|
||||||
:enable-token-color
|
:enable-token-color
|
||||||
|
:enable-token-shadow
|
||||||
:enable-inspect-styles
|
:enable-inspect-styles
|
||||||
:enable-feature-fdata-objects-map])
|
:enable-feature-fdata-objects-map])
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,6 @@
|
|||||||
"
|
"
|
||||||
#?(:cljs (:require-macros [app.common.logging :as l]))
|
#?(:cljs (:require-macros [app.common.logging :as l]))
|
||||||
(:require
|
(:require
|
||||||
#?(:clj [clojure.edn :as edn]
|
|
||||||
:cljs [cljs.reader :as edn])
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
[app.common.logic.shapes :as cls]
|
[app.common.logic.shapes :as cls]
|
||||||
[app.common.logic.variant-properties :as clvp]
|
[app.common.logic.variant-properties :as clvp]
|
||||||
[app.common.path-names :as cpn]
|
[app.common.path-names :as cpn]
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.components-list :as ctkl]
|
[app.common.types.components-list :as ctkl]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
@@ -39,8 +38,7 @@
|
|||||||
[app.common.types.typography :as cty]
|
[app.common.types.typography :as cty]
|
||||||
[app.common.types.variant :as ctv]
|
[app.common.types.variant :as ctv]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]))
|
||||||
[clojure.spec.alpha :as s]))
|
|
||||||
|
|
||||||
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
||||||
(log/set-level! :warn)
|
(log/set-level! :warn)
|
||||||
@@ -477,10 +475,10 @@
|
|||||||
If an asset id is given, only shapes linked to this particular asset will
|
If an asset id is given, only shapes linked to this particular asset will
|
||||||
be synchronized."
|
be synchronized."
|
||||||
[changes file-id asset-type asset-id library-id libraries current-file-id]
|
[changes file-id asset-type asset-id library-id libraries current-file-id]
|
||||||
(s/assert #{:colors :components :typographies} asset-type)
|
(assert (contains? #{:colors :components :typographies} asset-type))
|
||||||
(s/assert (s/nilable ::us/uuid) asset-id)
|
(assert (or (nil? asset-id) (uuid? asset-id)))
|
||||||
(s/assert ::us/uuid file-id)
|
(assert (uuid? file-id))
|
||||||
(s/assert ::us/uuid library-id)
|
(assert (uuid? library-id))
|
||||||
|
|
||||||
(container-log :info asset-id
|
(container-log :info asset-id
|
||||||
:msg "Sync file with library"
|
:msg "Sync file with library"
|
||||||
@@ -514,10 +512,10 @@
|
|||||||
If an asset id is given, only shapes linked to this particular asset will
|
If an asset id is given, only shapes linked to this particular asset will
|
||||||
be synchronized."
|
be synchronized."
|
||||||
[changes file-id asset-type asset-id library-id libraries current-file-id]
|
[changes file-id asset-type asset-id library-id libraries current-file-id]
|
||||||
(s/assert #{:colors :components :typographies} asset-type)
|
(assert (contains? #{:colors :components :typographies} asset-type))
|
||||||
(s/assert (s/nilable ::us/uuid) asset-id)
|
(assert (or (nil? asset-id) (uuid? asset-id)))
|
||||||
(s/assert ::us/uuid file-id)
|
(assert (uuid? file-id))
|
||||||
(s/assert ::us/uuid library-id)
|
(assert (uuid? library-id))
|
||||||
|
|
||||||
(container-log :info asset-id
|
(container-log :info asset-id
|
||||||
:msg "Sync local components with library"
|
:msg "Sync local components with library"
|
||||||
@@ -2493,11 +2491,13 @@
|
|||||||
(ctk/get-swap-slot))
|
(ctk/get-swap-slot))
|
||||||
(constantly false))
|
(constantly false))
|
||||||
|
|
||||||
|
;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the
|
||||||
|
;; mask property as part of the sanitization check on generate-delete-shapes, passing "ignore-mask" to prevent this
|
||||||
[all-parents changes]
|
[all-parents changes]
|
||||||
(-> changes
|
(-> changes
|
||||||
(cls/generate-delete-shapes
|
(cls/generate-delete-shapes
|
||||||
file page objects (d/ordered-set (:id shape))
|
file page objects (d/ordered-set (:id shape))
|
||||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
|
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true :ignore-flows-for #{(:id shape)}}))
|
||||||
[new-shape changes]
|
[new-shape changes]
|
||||||
(-> changes
|
(-> changes
|
||||||
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
|
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
|
||||||
@@ -2867,13 +2867,15 @@
|
|||||||
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
|
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
|
||||||
|
|
||||||
|
|
||||||
;; If there is an alt-duplication of a variant, change its parent to root
|
;; If there is an alt-duplication we change to root
|
||||||
;; so the copy is made as a child of root
|
;; For variants so the copy is made as a child of root
|
||||||
;; This is because inside a variant-container can't be a copy
|
;; This is because inside a variant-container can't be a copy
|
||||||
|
;; For other shape this way the layout won't be changed when duplicated
|
||||||
|
;; and if you move outside the layout will not change
|
||||||
shapes (map (fn [shape]
|
shapes (map (fn [shape]
|
||||||
(if (and alt-duplication? (ctk/is-variant? shape))
|
(cond-> shape
|
||||||
(assoc shape :parent-id uuid/zero :frame-id nil)
|
alt-duplication?
|
||||||
shape))
|
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
|
||||||
shapes)
|
shapes)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,8 +123,12 @@
|
|||||||
;; ignore-children-fn is used to ignore some descendants
|
;; ignore-children-fn is used to ignore some descendants
|
||||||
;; on the deletion process. It should receive a shape and
|
;; on the deletion process. It should receive a shape and
|
||||||
;; return a boolean
|
;; return a boolean
|
||||||
ignore-children-fn]
|
ignore-children-fn
|
||||||
:or {ignore-children-fn (constantly false)}}]
|
ignore-mask
|
||||||
|
ignore-flows-for]
|
||||||
|
:or {ignore-children-fn (constantly false)
|
||||||
|
ignore-mask false
|
||||||
|
ignore-flows-for #{}}}]
|
||||||
(let [objects (pcb/get-objects changes)
|
(let [objects (pcb/get-objects changes)
|
||||||
data (pcb/get-library-data changes)
|
data (pcb/get-library-data changes)
|
||||||
page-id (pcb/get-page-id changes)
|
page-id (pcb/get-page-id changes)
|
||||||
@@ -162,18 +166,20 @@
|
|||||||
lookup (d/getf objects)
|
lookup (d/getf objects)
|
||||||
|
|
||||||
groups-to-unmask
|
groups-to-unmask
|
||||||
(reduce (fn [group-ids id]
|
(when-not ignore-mask
|
||||||
;; When the shape to delete is the mask of a masked group,
|
(reduce (fn [group-ids id]
|
||||||
;; the mask condition must be removed, and it must be
|
;; When the shape to delete is the mask of a masked group,
|
||||||
;; converted to a normal group.
|
;; the mask condition must be removed, and it must be
|
||||||
(let [obj (lookup id)
|
;; converted to a normal group.
|
||||||
parent (lookup (:parent-id obj))]
|
(let [obj (lookup id)
|
||||||
(if (and (:masked-group parent)
|
parent (lookup (:parent-id obj))]
|
||||||
(= id (first (:shapes parent))))
|
(if (and (:masked-group parent)
|
||||||
(conj group-ids (:id parent))
|
(= id (first (:shapes parent))))
|
||||||
group-ids)))
|
(conj group-ids (:id parent))
|
||||||
#{}
|
group-ids)))
|
||||||
ids-to-delete)
|
#{}
|
||||||
|
ids-to-delete)
|
||||||
|
[])
|
||||||
|
|
||||||
interacting-shapes
|
interacting-shapes
|
||||||
(filter (fn [shape]
|
(filter (fn [shape]
|
||||||
@@ -190,7 +196,8 @@
|
|||||||
(->> (:flows page)
|
(->> (:flows page)
|
||||||
(reduce
|
(reduce
|
||||||
(fn [changes [id flow]]
|
(fn [changes [id flow]]
|
||||||
(if (id-to-delete? (:starting-frame flow))
|
(if (and (id-to-delete? (:starting-frame flow))
|
||||||
|
(not (contains? ignore-flows-for (:starting-frame flow))))
|
||||||
(-> changes
|
(-> changes
|
||||||
(pcb/with-page page)
|
(pcb/with-page page)
|
||||||
(pcb/set-flow id nil))
|
(pcb/set-flow id nil))
|
||||||
|
|||||||
@@ -132,3 +132,94 @@ Some naming conventions:
|
|||||||
(if-let [last-period (str/last-index-of s ".")]
|
(if-let [last-period (str/last-index-of s ".")]
|
||||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||||
[s ""]))
|
[s ""]))
|
||||||
|
|
||||||
|
;; Tree building functions --------------------------------------------------
|
||||||
|
|
||||||
|
"Build tree structure from flat list of paths"
|
||||||
|
|
||||||
|
"`build-tree-root` is the main function to build the tree."
|
||||||
|
|
||||||
|
"Receives a list of segments with 'name' properties representing paths,
|
||||||
|
and a separator string."
|
||||||
|
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||||
|
|
||||||
|
"Transforms into a tree structure like:
|
||||||
|
[{:name 'one'
|
||||||
|
:path 'one'
|
||||||
|
:depth 0
|
||||||
|
:leaf nil
|
||||||
|
:children-fn (fn [] [{:name 'two'
|
||||||
|
:path 'one.two'
|
||||||
|
:depth 1
|
||||||
|
:leaf nil
|
||||||
|
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||||
|
{:name 'five'
|
||||||
|
:path 'one.five'
|
||||||
|
:depth 1
|
||||||
|
:leaf {... :name 'five'}
|
||||||
|
...}])}]"
|
||||||
|
|
||||||
|
(defn- sort-by-children
|
||||||
|
"Sorts segments so that those with children come first."
|
||||||
|
[segments separator]
|
||||||
|
(sort-by (fn [segment]
|
||||||
|
(let [path (split-path (:name segment) :separator separator)
|
||||||
|
path-length (count path)]
|
||||||
|
(if (= path-length 1)
|
||||||
|
1
|
||||||
|
0)))
|
||||||
|
segments))
|
||||||
|
|
||||||
|
(defn- group-by-first-segment
|
||||||
|
"Groups segments by their first path segment and update segment name."
|
||||||
|
[segments separator]
|
||||||
|
(reduce (fn [acc segment]
|
||||||
|
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||||
|
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||||
|
(update acc first-segment (fnil conj [])
|
||||||
|
(if rest-path
|
||||||
|
(assoc segment :name rest-path)
|
||||||
|
segment))))
|
||||||
|
{}
|
||||||
|
segments))
|
||||||
|
|
||||||
|
(defn- sort-and-group-segments
|
||||||
|
"Sorts elements and groups them by their first path segment."
|
||||||
|
[segments separator]
|
||||||
|
(let [sorted (sort-by-children segments separator)
|
||||||
|
grouped (group-by-first-segment sorted separator)]
|
||||||
|
grouped))
|
||||||
|
|
||||||
|
(defn- build-tree-node
|
||||||
|
"Builds a single tree node with lazy children."
|
||||||
|
[segment-name remaining-segments separator parent-path depth]
|
||||||
|
(let [current-path (if parent-path
|
||||||
|
(str parent-path "." segment-name)
|
||||||
|
segment-name)
|
||||||
|
|
||||||
|
is-leaf? (and (seq remaining-segments)
|
||||||
|
(every? (fn [segment]
|
||||||
|
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||||
|
(= segment-name remaining-segment-name)))
|
||||||
|
remaining-segments))
|
||||||
|
|
||||||
|
leaf-segment (when is-leaf? (first remaining-segments))
|
||||||
|
node {:name segment-name
|
||||||
|
:path current-path
|
||||||
|
:depth depth
|
||||||
|
:leaf leaf-segment
|
||||||
|
:children-fn (when-not is-leaf?
|
||||||
|
(fn []
|
||||||
|
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||||
|
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||||
|
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||||
|
grouped-elements))))}]
|
||||||
|
node))
|
||||||
|
|
||||||
|
(defn build-tree-root
|
||||||
|
"Builds the root level of the tree."
|
||||||
|
[segments separator]
|
||||||
|
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||||
|
(mapv (fn [[segment-name remaining-segments]]
|
||||||
|
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||||
|
grouped-elements)))
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
|
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
|
||||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||||
(:require
|
(:require
|
||||||
|
#?(:clj [malli.dev.pretty :as mdp])
|
||||||
|
#?(:clj [malli.dev.virhe :as v])
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
@@ -19,8 +21,6 @@
|
|||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[malli.dev.pretty :as mdp]
|
|
||||||
[malli.dev.virhe :as v]
|
|
||||||
[malli.error :as me]
|
[malli.error :as me]
|
||||||
[malli.generator :as mg]
|
[malli.generator :as mg]
|
||||||
[malli.registry :as mr]
|
[malli.registry :as mr]
|
||||||
@@ -245,27 +245,30 @@
|
|||||||
:level (d/nilv level 8)
|
:level (d/nilv level 8)
|
||||||
:length (d/nilv length 12)})))))
|
:length (d/nilv length 12)})))))
|
||||||
|
|
||||||
(defmethod v/-format ::schemaless-explain
|
#?(:clj
|
||||||
[_ explanation printer]
|
(defmethod v/-format ::schemaless-explain
|
||||||
{:body [:group
|
[_ explanation printer]
|
||||||
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
{:body [:group
|
||||||
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]})
|
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
||||||
|
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]}))
|
||||||
|
|
||||||
(defmethod v/-format ::explain
|
#?(:clj
|
||||||
[_ {:keys [schema] :as explanation} printer]
|
(defmethod v/-format ::explain
|
||||||
{:body [:group
|
[_ {:keys [schema] :as explanation} printer]
|
||||||
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
{:body [:group
|
||||||
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
|
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
||||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
|
||||||
|
(v/-block "Schema" (v/-visit schema printer) printer)]}))
|
||||||
|
|
||||||
(defn pretty-explain
|
#?(:clj
|
||||||
"A helper that allows print a console-friendly output for the
|
(defn pretty-explain
|
||||||
explain; should not be used for other purposes"
|
"A helper that allows print a console-friendly output for the explain;
|
||||||
[explain & {:keys [variant message]
|
should not be used for other purposes"
|
||||||
:or {variant ::explain
|
[explain & {:keys [variant message]
|
||||||
message "Validation Error"}}]
|
:or {variant ::explain
|
||||||
(let [explain (fn [] (me/with-error-messages explain))]
|
message "Validation Error"}}]
|
||||||
((mdp/prettifier variant message explain default-options))))
|
(let [explain (fn [] (me/with-error-messages explain))]
|
||||||
|
((mdp/prettifier variant message explain default-options)))))
|
||||||
|
|
||||||
(defmacro ignoring
|
(defmacro ignoring
|
||||||
[expr]
|
[expr]
|
||||||
@@ -312,6 +315,13 @@
|
|||||||
::explain explain}))))
|
::explain explain}))))
|
||||||
value))))
|
value))))
|
||||||
|
|
||||||
|
(defn coercer
|
||||||
|
[schema & {:as opts}]
|
||||||
|
(let [decode-fn (lazy-decoder schema json-transformer)
|
||||||
|
check-fn (check-fn schema opts)]
|
||||||
|
(fn [data]
|
||||||
|
(-> data decode-fn check-fn))))
|
||||||
|
|
||||||
(defn check
|
(defn check
|
||||||
"A helper intended to be used on assertions for validate/check the
|
"A helper intended to be used on assertions for validate/check the
|
||||||
schema over provided data. Raises an assertion exception.
|
schema over provided data. Raises an assertion exception.
|
||||||
@@ -1006,6 +1016,9 @@
|
|||||||
(def valid-safe-number?
|
(def valid-safe-number?
|
||||||
(lazy-validator ::safe-number))
|
(lazy-validator ::safe-number))
|
||||||
|
|
||||||
|
(def valid-safe-int?
|
||||||
|
(lazy-validator ::safe-int))
|
||||||
|
|
||||||
(def valid-text?
|
(def valid-text?
|
||||||
(validator ::text))
|
(validator ::text))
|
||||||
|
|
||||||
|
|||||||
@@ -546,9 +546,19 @@
|
|||||||
filter-values)))
|
filter-values)))
|
||||||
|
|
||||||
(defn extract-ids [val]
|
(defn extract-ids [val]
|
||||||
(when (some? val)
|
;; Extract referenced ids from string values like "url(#myId)".
|
||||||
|
;; Non-string values (maps, numbers, nil, etc.) return an empty seq
|
||||||
|
;; to avoid re-seq type errors when attributes carry nested structures.
|
||||||
|
(cond
|
||||||
|
(string? val)
|
||||||
(->> (re-seq xml-id-regex val)
|
(->> (re-seq xml-id-regex val)
|
||||||
(mapv second))))
|
(mapv second))
|
||||||
|
|
||||||
|
(sequential? val)
|
||||||
|
(mapcat extract-ids val)
|
||||||
|
|
||||||
|
:else
|
||||||
|
[]))
|
||||||
|
|
||||||
(defn fix-dot-number
|
(defn fix-dot-number
|
||||||
"Fixes decimal numbers starting in dot but without leading 0"
|
"Fixes decimal numbers starting in dot but without leading 0"
|
||||||
|
|||||||
@@ -340,7 +340,7 @@
|
|||||||
(dfn-diff t2 t1)))
|
(dfn-diff t2 t1)))
|
||||||
|
|
||||||
#?(:cljs
|
#?(:cljs
|
||||||
(defn set-default-locale!
|
(defn set-default-locale
|
||||||
[locale]
|
[locale]
|
||||||
(when-let [locale (unchecked-get locales locale)]
|
(when-let [locale (unchecked-get locales locale)]
|
||||||
(dfn-set-default-options #js {:locale locale}))))
|
(dfn-set-default-options #js {:locale locale}))))
|
||||||
|
|||||||
@@ -140,7 +140,8 @@
|
|||||||
:layout-item-min-w
|
:layout-item-min-w
|
||||||
:layout-item-absolute
|
:layout-item-absolute
|
||||||
:layout-item-z-index
|
:layout-item-z-index
|
||||||
:layout-item-align-self})
|
:layout-item-align-self
|
||||||
|
:interactions})
|
||||||
|
|
||||||
(defn component-attr?
|
(defn component-attr?
|
||||||
"Check if some attribute is one that is involved in component syncrhonization.
|
"Check if some attribute is one that is involved in component syncrhonization.
|
||||||
|
|||||||
@@ -269,8 +269,8 @@
|
|||||||
"Remove flex children properties except the fit-content for flex layouts. These are properties
|
"Remove flex children properties except the fit-content for flex layouts. These are properties
|
||||||
that we don't have to propagate to copies but will be respected when swapping components"
|
that we don't have to propagate to copies but will be respected when swapping components"
|
||||||
[shape]
|
[shape]
|
||||||
(let [layout-item-h-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-width? shape)) :auto)
|
(let [layout-item-h-sizing (when (and (ctl/any-layout? shape) (ctl/auto-width? shape)) :auto)
|
||||||
layout-item-v-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-height? shape)) :auto)]
|
layout-item-v-sizing (when (and (ctl/any-layout? shape) (ctl/auto-height? shape)) :auto)]
|
||||||
(-> shape
|
(-> shape
|
||||||
(d/without-keys ctk/swap-keep-attrs)
|
(d/without-keys ctk/swap-keep-attrs)
|
||||||
(cond-> (some? layout-item-h-sizing)
|
(cond-> (some? layout-item-h-sizing)
|
||||||
|
|||||||
@@ -112,8 +112,10 @@
|
|||||||
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
||||||
content))]
|
content))]
|
||||||
|
|
||||||
(impl/path-data
|
(if (some? modifiers)
|
||||||
(reduce apply-to-index (vec content) modifiers))))
|
(impl/path-data
|
||||||
|
(reduce apply-to-index (vec content) modifiers))
|
||||||
|
content)))
|
||||||
|
|
||||||
(defn transform-content
|
(defn transform-content
|
||||||
"Applies a transformation matrix over content and returns a new
|
"Applies a transformation matrix over content and returns a new
|
||||||
@@ -234,16 +236,15 @@
|
|||||||
"Calculate the boolean content from shape and objects. Returns a
|
"Calculate the boolean content from shape and objects. Returns a
|
||||||
packed PathData instance"
|
packed PathData instance"
|
||||||
[shape objects]
|
[shape objects]
|
||||||
(let [content (if (fn? wasm:calc-bool-content)
|
(let [content (calc-bool-content* shape objects)]
|
||||||
(wasm:calc-bool-content (get shape :bool-type)
|
|
||||||
(get shape :shapes))
|
|
||||||
(calc-bool-content* shape objects))]
|
|
||||||
(impl/path-data content)))
|
(impl/path-data content)))
|
||||||
|
|
||||||
(defn update-bool-shape
|
(defn update-bool-shape
|
||||||
"Calculates the selrect+points for the boolean shape"
|
"Calculates the selrect+points for the boolean shape"
|
||||||
[shape objects]
|
[shape objects]
|
||||||
(let [content (calc-bool-content shape objects)
|
(let [content (if (fn? wasm:calc-bool-content)
|
||||||
|
(wasm:calc-bool-content shape objects)
|
||||||
|
(calc-bool-content shape objects))
|
||||||
shape (assoc shape :content content)]
|
shape (assoc shape :content content)]
|
||||||
(update-geometry shape)))
|
(update-geometry shape)))
|
||||||
|
|
||||||
|
|||||||
29
common/src/app/common/types/project.cljc
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.common.types.project
|
||||||
|
(:require
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.time :as cm]))
|
||||||
|
|
||||||
|
(def schema:project
|
||||||
|
[:map {:title "Profile"}
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:created-at {:optional true} ::cm/inst]
|
||||||
|
[:modified-at {:optional true} ::cm/inst]
|
||||||
|
[:name :string]
|
||||||
|
[:is-default {:optional true} ::sm/boolean]
|
||||||
|
[:is-pinned {:optional true} ::sm/boolean]
|
||||||
|
[:count {:optional true} ::sm/int]
|
||||||
|
[:total-count {:optional true} ::sm/int]
|
||||||
|
[:team-id ::sm/uuid]])
|
||||||
|
|
||||||
|
(def valid-project?
|
||||||
|
(sm/lazy-validator schema:project))
|
||||||
|
|
||||||
|
(def check-project
|
||||||
|
(sm/check-fn schema:project))
|
||||||
@@ -47,6 +47,18 @@
|
|||||||
self-reference? (get token-references token-name)]
|
self-reference? (get token-references token-name)]
|
||||||
self-reference?))
|
self-reference?))
|
||||||
|
|
||||||
|
(defn references-token?
|
||||||
|
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
|
||||||
|
[value token-name]
|
||||||
|
(cond
|
||||||
|
(string? value)
|
||||||
|
(boolean (some #(= % token-name) (find-token-value-references value)))
|
||||||
|
(map? value)
|
||||||
|
(some true? (map #(references-token? % token-name) (vals value)))
|
||||||
|
(sequential? value)
|
||||||
|
(some true? (map #(references-token? % token-name) value))
|
||||||
|
:else false))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; SCHEMA
|
;; SCHEMA
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -59,6 +71,7 @@
|
|||||||
:dimensions "dimension"
|
:dimensions "dimension"
|
||||||
:font-family "fontFamilies"
|
:font-family "fontFamilies"
|
||||||
:font-size "fontSizes"
|
:font-size "fontSizes"
|
||||||
|
:font-weight "fontWeights"
|
||||||
:letter-spacing "letterSpacing"
|
:letter-spacing "letterSpacing"
|
||||||
:number "number"
|
:number "number"
|
||||||
:opacity "opacity"
|
:opacity "opacity"
|
||||||
@@ -70,7 +83,6 @@
|
|||||||
:stroke-width "borderWidth"
|
:stroke-width "borderWidth"
|
||||||
:text-case "textCase"
|
:text-case "textCase"
|
||||||
:text-decoration "textDecoration"
|
:text-decoration "textDecoration"
|
||||||
:font-weight "fontWeights"
|
|
||||||
:typography "typography"})
|
:typography "typography"})
|
||||||
|
|
||||||
(def dtcg-token-type->token-type
|
(def dtcg-token-type->token-type
|
||||||
@@ -558,3 +570,18 @@
|
|||||||
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
||||||
[token-value]
|
[token-value]
|
||||||
(string? token-value))
|
(string? token-value))
|
||||||
|
|
||||||
|
(defn update-token-value-references
|
||||||
|
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
|
||||||
|
[value old-name new-name]
|
||||||
|
(cond
|
||||||
|
(string? value)
|
||||||
|
(str/replace value
|
||||||
|
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
|
||||||
|
(str "{" new-name "}"))
|
||||||
|
(map? value)
|
||||||
|
(d/update-vals value #(update-token-value-references % old-name new-name))
|
||||||
|
(sequential? value)
|
||||||
|
(mapv #(update-token-value-references % old-name new-name) value)
|
||||||
|
:else
|
||||||
|
value))
|
||||||
|
|||||||
@@ -909,7 +909,8 @@ Will return a value that matches this schema:
|
|||||||
`:all` All of the nested sets are active
|
`:all` All of the nested sets are active
|
||||||
`:partial` Mixed active state of nested sets")
|
`:partial` Mixed active state of nested sets")
|
||||||
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
|
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
|
||||||
(get-all-tokens [_] "all tokens in the lib")
|
(get-all-tokens [_] "all tokens in the lib, as a sequence")
|
||||||
|
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
|
||||||
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
|
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
|
||||||
|
|
||||||
(declare parse-multi-set-dtcg-json)
|
(declare parse-multi-set-dtcg-json)
|
||||||
@@ -1306,6 +1307,10 @@ Will return a value that matches this schema:
|
|||||||
tokens))
|
tokens))
|
||||||
|
|
||||||
(get-all-tokens [this]
|
(get-all-tokens [this]
|
||||||
|
(mapcat #(vals (get-tokens- %))
|
||||||
|
(get-sets this)))
|
||||||
|
|
||||||
|
(get-all-tokens-map [this]
|
||||||
(reduce
|
(reduce
|
||||||
(fn [tokens' set]
|
(fn [tokens' set]
|
||||||
(into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set)))))
|
(into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set)))))
|
||||||
@@ -1545,7 +1550,7 @@ Will return a value that matches this schema:
|
|||||||
(and (not (contains? decoded-json "$metadata"))
|
(and (not (contains? decoded-json "$metadata"))
|
||||||
(not (contains? decoded-json "$themes"))))
|
(not (contains? decoded-json "$themes"))))
|
||||||
|
|
||||||
(defn- convert-dtcg-font-family
|
(defn convert-dtcg-font-family
|
||||||
"Convert font-family token value from DTCG format to internal format.
|
"Convert font-family token value from DTCG format to internal format.
|
||||||
- If value is a string, split it into a collection of font families
|
- If value is a string, split it into a collection of font families
|
||||||
- If value is already an array, keep it as is
|
- If value is already an array, keep it as is
|
||||||
@@ -1556,7 +1561,7 @@ Will return a value that matches this schema:
|
|||||||
(sequential? value) value
|
(sequential? value) value
|
||||||
:else value))
|
:else value))
|
||||||
|
|
||||||
(defn- convert-dtcg-typography-composite
|
(defn convert-dtcg-typography-composite
|
||||||
"Convert typography token value keys from DTCG format to internal format."
|
"Convert typography token value keys from DTCG format to internal format."
|
||||||
[value]
|
[value]
|
||||||
(if (map? value)
|
(if (map? value)
|
||||||
@@ -1568,17 +1573,17 @@ Will return a value that matches this schema:
|
|||||||
;; Reference value
|
;; Reference value
|
||||||
value))
|
value))
|
||||||
|
|
||||||
(defn- convert-dtcg-shadow-composite
|
(defn convert-dtcg-shadow-composite
|
||||||
"Convert shadow token value from DTCG format to internal format."
|
"Convert shadow token value from DTCG format to internal format."
|
||||||
[value]
|
[value]
|
||||||
(let [process-shadow (fn [shadow]
|
(let [process-shadow (fn [shadow]
|
||||||
(if (map? shadow)
|
(if (map? shadow)
|
||||||
(let [legacy-shadow-type (get "type" shadow)]
|
(let [legacy-shadow-type (get "type" shadow)]
|
||||||
(-> shadow
|
(-> shadow
|
||||||
(set/rename-keys {"x" :offsetX
|
(set/rename-keys {"x" :offset-x
|
||||||
"offsetX" :offsetX
|
"offsetX" :offset-x
|
||||||
"y" :offsetY
|
"y" :offset-y
|
||||||
"offsetY" :offsetY
|
"offsetY" :offset-y
|
||||||
"blur" :blur
|
"blur" :blur
|
||||||
"spread" :spread
|
"spread" :spread
|
||||||
"color" :color
|
"color" :color
|
||||||
@@ -1589,7 +1594,7 @@ Will return a value that matches this schema:
|
|||||||
(= "false" %) false
|
(= "false" %) false
|
||||||
(= legacy-shadow-type "innerShadow") true
|
(= legacy-shadow-type "innerShadow") true
|
||||||
:else false))
|
:else false))
|
||||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||||
shadow))]
|
shadow))]
|
||||||
(cond
|
(cond
|
||||||
;; Reference value - keep as string
|
;; Reference value - keep as string
|
||||||
@@ -1860,8 +1865,8 @@ Will return a value that matches this schema:
|
|||||||
(mapv (fn [shadow]
|
(mapv (fn [shadow]
|
||||||
(if (map? shadow)
|
(if (map? shadow)
|
||||||
(-> shadow
|
(-> shadow
|
||||||
(set/rename-keys {:offsetX "offsetX"
|
(set/rename-keys {:offset-x "offsetX"
|
||||||
:offsetY "offsetY"
|
:offset-y "offsetY"
|
||||||
:blur "blur"
|
:blur "blur"
|
||||||
:spread "spread"
|
:spread "spread"
|
||||||
:color "color"
|
:color "color"
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
(defn parse
|
(defn parse
|
||||||
[data]
|
[data]
|
||||||
(cond
|
(cond
|
||||||
(str/starts-with? data "%")
|
(or (str/starts-with? data "%")
|
||||||
|
(= data "develop"))
|
||||||
{:full "develop"
|
{:full "develop"
|
||||||
:branch "develop"
|
:branch "develop"
|
||||||
:base "0.0.0"
|
:base "0.0.0"
|
||||||
|
|||||||
@@ -1897,15 +1897,15 @@
|
|||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type token)))
|
(t/is (= :shadow (:type token)))
|
||||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||||
(:value token)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "multiple shadow token"
|
(t/testing "multiple shadow token"
|
||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type token)))
|
(t/is (= :shadow (:type token)))
|
||||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||||
(:value token)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "shadow token with reference"
|
(t/testing "shadow token with reference"
|
||||||
@@ -1918,7 +1918,7 @@
|
|||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type token)))
|
(t/is (= :shadow (:type token)))
|
||||||
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||||
(:value token)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "shadow token with description"
|
(t/testing "shadow token with description"
|
||||||
@@ -1937,14 +1937,14 @@
|
|||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.single"
|
{:name "shadow.single"
|
||||||
:type :shadow
|
:type :shadow
|
||||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||||
:description "A single shadow"})
|
:description "A single shadow"})
|
||||||
"shadow.multiple"
|
"shadow.multiple"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.multiple"
|
{:name "shadow.multiple"
|
||||||
:type :shadow
|
:type :shadow
|
||||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||||
"shadow.ref"
|
"shadow.ref"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.ref"
|
{:name "shadow.ref"
|
||||||
@@ -1991,7 +1991,7 @@
|
|||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.test"
|
{:name "shadow.test"
|
||||||
:type :shadow
|
:type :shadow
|
||||||
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||||
:description "Round trip test"})
|
:description "Round trip test"})
|
||||||
"shadow.ref"
|
"shadow.ref"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
|
|||||||
@@ -25,48 +25,6 @@ RUN set -ex; \
|
|||||||
binutils \
|
binutils \
|
||||||
build-essential autoconf libtool pkg-config
|
build-essential autoconf libtool pkg-config
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
## IMAGE MAGICK
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM base AS build-imagemagick
|
|
||||||
|
|
||||||
ENV IMAGEMAGICK_VERSION=7.1.1-47 \
|
|
||||||
DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN set -ex; \
|
|
||||||
apt-get -qq update; \
|
|
||||||
apt-get -qq upgrade; \
|
|
||||||
apt-get -qqy --no-install-recommends install \
|
|
||||||
libltdl-dev \
|
|
||||||
libpng-dev \
|
|
||||||
libjpeg-dev \
|
|
||||||
libtiff-dev \
|
|
||||||
libwebp-dev \
|
|
||||||
libopenexr-dev \
|
|
||||||
libfftw3-dev \
|
|
||||||
libzip-dev \
|
|
||||||
liblcms2-dev \
|
|
||||||
liblzma-dev \
|
|
||||||
libzstd-dev \
|
|
||||||
libheif-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
|
|
||||||
mkdir -p /tmp/magick; \
|
|
||||||
cd /tmp/magick; \
|
|
||||||
tar -xf /tmp/magick.tar.gz --strip-components=1; \
|
|
||||||
./configure --prefix=/opt/imagick; \
|
|
||||||
make -j 2; \
|
|
||||||
make install; \
|
|
||||||
rm -rf /opt/imagick/lib/libMagick++*; \
|
|
||||||
rm -rf /opt/imagick/include; \
|
|
||||||
rm -rf /opt/imagick/share;
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
## NODE SETUP
|
## NODE SETUP
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -417,7 +375,7 @@ ENV LANG='C.UTF-8' \
|
|||||||
RUSTUP_HOME="/opt/rustup" \
|
RUSTUP_HOME="/opt/rustup" \
|
||||||
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
|
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
|
||||||
|
|
||||||
COPY --from=build-imagemagick /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/jdk /opt/jdk
|
||||||
COPY --from=setup-jvm /opt/clojure /opt/clojure
|
COPY --from=setup-jvm /opt/clojure /opt/clojure
|
||||||
COPY --from=setup-node /opt/node /opt/node
|
COPY --from=setup-node /opt/node /opt/node
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ services:
|
|||||||
- 6062:6062
|
- 6062:6062
|
||||||
- 6063:6063
|
- 6063:6063
|
||||||
- 6064:6064
|
- 6064:6064
|
||||||
|
- 9000:9000
|
||||||
|
- 9001:9001
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
|
- 9091:9091
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- EXTERNAL_UID=${CURRENT_USER_ID}
|
- EXTERNAL_UID=${CURRENT_USER_ID}
|
||||||
@@ -69,6 +72,11 @@ services:
|
|||||||
- PENPOT_LDAP_ATTRS_FULLNAME=cn
|
- PENPOT_LDAP_ATTRS_FULLNAME=cn
|
||||||
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
|
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- main
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
|
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
|
||||||
command: minio server /mnt/data --console-address ":9001"
|
command: minio server /mnt/data --console-address ":9001"
|
||||||
@@ -80,10 +88,6 @@ services:
|
|||||||
- MINIO_ROOT_USER=minioadmin
|
- MINIO_ROOT_USER=minioadmin
|
||||||
- MINIO_ROOT_PASSWORD=minioadmin
|
- MINIO_ROOT_PASSWORD=minioadmin
|
||||||
|
|
||||||
ports:
|
|
||||||
- 9000:9000
|
|
||||||
- 9001:9001
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
aliases:
|
aliases:
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ localhost:3449 {
|
|||||||
http://localhost:3450 {
|
http://localhost:3450 {
|
||||||
reverse_proxy localhost:4449
|
reverse_proxy localhost:4449
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http://penpot-devenv-main:3450 {
|
||||||
|
reverse_proxy localhost:4449
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ http {
|
|||||||
|
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_comp_level 3;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.1;
|
gzip_http_version 1.1;
|
||||||
|
|
||||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default upgrade;
|
default upgrade;
|
||||||
@@ -145,8 +145,8 @@ http {
|
|||||||
proxy_pass http://127.0.0.1:3000/;
|
proxy_pass http://127.0.0.1:3000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /playground {
|
location /wasm-playground {
|
||||||
alias /home/penpot/penpot/experiments/;
|
alias /home/penpot/penpot/frontend/resources/public/wasm-playground/;
|
||||||
add_header Cache-Control "no-cache, max-age=0";
|
add_header Cache-Control "no-cache, max-age=0";
|
||||||
autoindex on;
|
autoindex on;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,10 @@ source ~/.bashrc
|
|||||||
|
|
||||||
echo "[start-tmux.sh] Installing node dependencies"
|
echo "[start-tmux.sh] Installing node dependencies"
|
||||||
pushd ~/penpot/frontend/
|
pushd ~/penpot/frontend/
|
||||||
corepack install;
|
./scripts/setup;
|
||||||
yarn install;
|
|
||||||
yarn playwright install chromium
|
|
||||||
popd
|
popd
|
||||||
pushd ~/penpot/exporter/
|
pushd ~/penpot/exporter/
|
||||||
corepack install;
|
./scripts/setup;
|
||||||
yarn install
|
|
||||||
yarn playwright install chromium
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
tmux -2 new-session -d -s penpot
|
tmux -2 new-session -d -s penpot
|
||||||
@@ -23,30 +19,25 @@ tmux -2 new-session -d -s penpot
|
|||||||
tmux rename-window -t penpot:0 'frontend watch'
|
tmux rename-window -t penpot:0 'frontend watch'
|
||||||
tmux select-window -t penpot:0
|
tmux select-window -t penpot:0
|
||||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||||
tmux send-keys -t penpot 'yarn run watch' enter
|
tmux send-keys -t penpot './scripts/watch app' enter
|
||||||
|
|
||||||
tmux new-window -t penpot:1 -n 'frontend shadow'
|
tmux new-window -t penpot:1 -n 'frontend storybook'
|
||||||
tmux select-window -t penpot:1
|
tmux select-window -t penpot:1
|
||||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||||
tmux send-keys -t penpot 'yarn run watch:app' enter
|
tmux send-keys -t penpot './scripts/watch storybook' enter
|
||||||
|
|
||||||
tmux new-window -t penpot:2 -n 'frontend storybook'
|
tmux new-window -t penpot:2 -n 'exporter'
|
||||||
tmux select-window -t penpot:2
|
tmux select-window -t penpot:2
|
||||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
|
||||||
tmux send-keys -t penpot 'yarn run watch:storybook' enter
|
|
||||||
|
|
||||||
tmux new-window -t penpot:3 -n 'exporter'
|
|
||||||
tmux select-window -t penpot:3
|
|
||||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||||
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
|
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
|
||||||
tmux send-keys -t penpot 'yarn run watch' enter
|
tmux send-keys -t penpot './scripts/watch' enter
|
||||||
|
|
||||||
tmux split-window -v
|
tmux split-window -v
|
||||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||||
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
|
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
|
||||||
|
|
||||||
tmux new-window -t penpot:4 -n 'backend'
|
tmux new-window -t penpot:3 -n 'backend'
|
||||||
tmux select-window -t penpot:4
|
tmux select-window -t penpot:3
|
||||||
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
||||||
tmux send-keys -t penpot './scripts/start-dev' enter
|
tmux send-keys -t penpot './scripts/start-dev' enter
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ RUN set -ex; \
|
|||||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||||
mkdir -p /opt/data/assets; \
|
mkdir -p /opt/data/assets; \
|
||||||
chown -R penpot:penpot /opt/data; \
|
chown -R penpot:penpot /opt/data; \
|
||||||
|
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||||
|
mkdir -p /etc/nginx/overrides/assets.d/; \
|
||||||
mkdir -p /etc/nginx/overrides/location.d/;
|
mkdir -p /etc/nginx/overrides/location.d/;
|
||||||
|
|
||||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||||
|
|||||||
@@ -130,12 +130,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]
|
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]
|
||||||
|
|
||||||
## The PREPL host. Mainly used for external programatic access to penpot backend
|
|
||||||
## (example: admin). By default it will listen on `localhost` but if you are going to use
|
|
||||||
## the `admin`, you will need to uncomment this and set the host to `0.0.0.0`.
|
|
||||||
|
|
||||||
# PENPOT_PREPL_HOST: 0.0.0.0
|
|
||||||
|
|
||||||
## Database connection parameters. Don't touch them unless you are using custom
|
## Database connection parameters. Don't touch them unless you are using custom
|
||||||
## postgresql connection parameters.
|
## postgresql connection parameters.
|
||||||
|
|
||||||
@@ -151,8 +145,8 @@ services:
|
|||||||
## Default configuration for assets storage: using filesystem based with all files
|
## Default configuration for assets storage: using filesystem based with all files
|
||||||
## stored in a docker volume.
|
## stored in a docker volume.
|
||||||
|
|
||||||
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
|
PENPOT_OBJECTS_STORAGE_BACKEND: fs
|
||||||
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
|
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets
|
||||||
|
|
||||||
## Also can be configured to to use a S3 compatible storage.
|
## Also can be configured to to use a S3 compatible storage.
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ http {
|
|||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
gzip_comp_level 4;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.1;
|
gzip_http_version 1.1;
|
||||||
|
|
||||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||||
|
|
||||||
proxy_buffer_size 16k;
|
proxy_buffer_size 16k;
|
||||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||||
@@ -110,6 +110,8 @@ http {
|
|||||||
recursive_error_pages on;
|
recursive_error_pages on;
|
||||||
proxy_intercept_errors on;
|
proxy_intercept_errors on;
|
||||||
error_page 301 302 307 = @handle_redirect;
|
error_page 301 302 307 = @handle_redirect;
|
||||||
|
|
||||||
|
include /etc/nginx/overrides/assets.d/*.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /internal/assets {
|
location /internal/assets {
|
||||||
@@ -142,24 +144,15 @@ http {
|
|||||||
location / {
|
location / {
|
||||||
include /etc/nginx/overrides/location.d/*.conf;
|
include /etc/nginx/overrides/location.d/*.conf;
|
||||||
|
|
||||||
location ~ ^/js/config.js$ {
|
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
|
||||||
add_header Cache-Control "max-age=604800" always; # 7 days
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/[^/]+/(.*)$ {
|
location ~ ^/[^/]+/(.*)$ {
|
||||||
return 301 " /404";
|
return 301 " /404";
|
||||||
}
|
}
|
||||||
|
|
||||||
add_header Last-Modified $date_gmt;
|
|
||||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||||
if_modified_since off;
|
|
||||||
try_files $uri /index.html$is_args$args /index.html =404;
|
try_files $uri /index.html$is_args$args /index.html =404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
docs/img/design-tokens/37-tokens-shadow-individual.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/img/design-tokens/38-tokens-shadow-reference.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/img/files-projects/01-projects.webp
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
docs/img/files-projects/02-drafts.webp
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
docs/img/files-projects/03-trash.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/img/files-projects/04-pin-project.webp
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
docs/img/files-projects/05-create-file.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/img/files-projects/06-move-project.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
|
||||||
rm -rf ./_dist
|
rm -rf ./_dist
|
||||||
yarn
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
|||||||
<p>Create and manage your teams</p>
|
<p>Create and manage your teams</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/user-guide/account-teams/projects-files">
|
||||||
|
<h2>Projects and Files →</h2>
|
||||||
|
<p>Organize your work with projects and files</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/user-guide/account-teams/comments/">
|
<a href="/user-guide/account-teams/comments/">
|
||||||
<h2>Comments →</h2>
|
<h2>Comments →</h2>
|
||||||
|
|||||||
107
docs/user-guide/account-teams/projects-files.njk
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Projects and Files
|
||||||
|
order: 3
|
||||||
|
desc: Learn how to organize your work in Penpot. Create, manage and organize projects and files, work with drafts, and handle deleted items.
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 id="projects-files">Projects and Files</h1>
|
||||||
|
<p class="main-paragraph">Projects and files are the core organizational structure in Penpot. Projects work like folders that contain multiple design files, helping you organize your work efficiently. Files are your actual design documents where you create boards, pages, and all your design elements.</p>
|
||||||
|
<p class="main-paragraph">Understanding how to manage projects and files will help you keep your workspace organized and make it easier to collaborate with your team.</p>
|
||||||
|
|
||||||
|
<h2 id="projects-management">Projects</h2>
|
||||||
|
<p>Projects are containers that help you organize and group related design files together. Think of them as folders in a file system. You can create as many projects as you need to organize your work by client, product, feature, or any other structure that fits your workflow.</p>
|
||||||
|
<p>If you're working with others, projects should be created inside a team so that team members can collaborate on the files within them. Projects created in your personal space ("Your Penpot") remain private to you.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/01-projects.webp" alt="Projects view in dashboard" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h3 id="create-project">Create a project</h3>
|
||||||
|
<p>To create a new project, use the <strong>+ New project</strong> button in the dashboard. You can also use the keyboard shortcut <kbd>+</kbd> when you're on the dashboard. A dialog will appear where you can enter the project name. Once created, the project will appear in your projects list.</p>
|
||||||
|
<p>When you create a project, you can immediately start adding files to it, or create files first and move them into the project later.</p>
|
||||||
|
|
||||||
|
<h3 id="edit-project">Edit a project</h3>
|
||||||
|
<p>To edit a project's name, right-click on the project in the sidebar or click the three-dot menu next to the project name. Select <strong>Edit</strong> or <strong>Rename</strong> to change the project name. You can also update the project's profile picture from the same menu.</p>
|
||||||
|
|
||||||
|
<h3 id="pin-project">Pin a project</h3>
|
||||||
|
<p>Projects can be pinned to the sidebar for quick access. Right-click on a project and select <strong>Pin</strong> to keep it visible in the sidebar even when you have many projects. Pinned projects appear at the top of your projects list for easy access.</p>
|
||||||
|
<p>To unpin a project, right-click on it and select <strong>Unpin</strong>. The project will remain in your list but won't be pinned to the sidebar anymore.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/04-pin-project.webp" alt="Pin project option" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h3 id="move-project">Move a project</h3>
|
||||||
|
<p>Projects can be moved between teams. To move a project, right-click on it and select <strong>Move to</strong> from the context menu. A dialog will appear showing all available teams where you can move the project. Select the destination team and confirm the move.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/06-move-project.webp" alt="Move project to another team" />
|
||||||
|
</figure>
|
||||||
|
<p>When you move a project to another team, all files within the project are moved along with it. Team members of the destination team will gain access to the project and its files according to their permissions.</p>
|
||||||
|
<p class="advice">Moving a project to another team changes its ownership and access permissions. Make sure the destination team has the appropriate members and permissions for the work contained in the project.</p>
|
||||||
|
|
||||||
|
<h3 id="delete-project">Delete a project</h3>
|
||||||
|
<p>To delete a project, right-click on it and select <strong>Delete</strong> from the menu. You'll be asked to confirm the deletion. Keep in mind that deleting a project will also delete all files within it. Make sure you have backed up any important files before deleting a project.</p>
|
||||||
|
<p class="advice">Deleted projects and their files are moved to the trash area where they can be restored or permanently deleted.</p>
|
||||||
|
|
||||||
|
<h2 id="files-management">Files</h2>
|
||||||
|
<p>Files are your design documents in Penpot. Each file contains pages, boards, and all the design elements you create. Files can be created within a project or in the drafts section, and you can move them between projects as needed.</p>
|
||||||
|
|
||||||
|
<h3 id="create-file">Create a file</h3>
|
||||||
|
<p>To create a new file, you have several options:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Click the <strong>+</strong> button in a project to create a file inside that project</li>
|
||||||
|
<li>Use the keyboard shortcut <kbd>+</kbd> when you have a project selected</li>
|
||||||
|
<li>Create a file directly in the drafts section if you're not ready to organize it into a project yet</li>
|
||||||
|
</ul>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/05-create-file.webp" alt="Create a new file" />
|
||||||
|
</figure>
|
||||||
|
<p>When creating a file, you'll be asked to give it a name. The file will open in the workspace where you can start designing immediately.</p>
|
||||||
|
|
||||||
|
<h3 id="edit-file">Edit a file</h3>
|
||||||
|
<p>To rename a file, right-click on the file card in the dashboard and select <strong>Rename</strong>, or click on the three-dot menu on the file card. Enter the new name and confirm the change. You can also access file settings and other options from the file's context menu.</p>
|
||||||
|
|
||||||
|
<h3 id="move-file">Move a file</h3>
|
||||||
|
<p>Files can be moved between projects, from drafts to a project, or even to projects in other teams. To move a file, right-click on the file card and select <strong>Move to</strong> from the context menu. A dialog will appear showing all available projects across your teams where you can choose the destination. Select the project where you want to move the file and confirm.</p>
|
||||||
|
<p>You can also drag and drop files between projects in the dashboard for a quick way to reorganize your files within the same team.</p>
|
||||||
|
<p>When moving a file to a project in another team, the file becomes accessible to members of that team according to their permissions. Moving a file doesn't affect its content or any shared libraries it might be using. Only its location in your project structure changes.</p>
|
||||||
|
<p class="advice">When moving files between teams, be aware that this changes who has access to the file. Make sure the destination team has the appropriate members and permissions for the work contained in the file.</p>
|
||||||
|
|
||||||
|
<h3 id="duplicate-file">Duplicate a file</h3>
|
||||||
|
<p>To create a copy of an existing file, right-click on the file card and select <strong>Duplicate</strong>. The duplicated file will be created in the same location (project or drafts) with the same name plus "Copy" added to it. You can then rename or move it as needed.</p>
|
||||||
|
<p>Duplicating a file creates a complete copy including all pages, boards, and design elements. This is useful when you want to create variations of a design or use a file as a starting point for a new project.</p>
|
||||||
|
|
||||||
|
<h3 id="delete-file">Delete a file</h3>
|
||||||
|
<p>To delete a file, right-click on the file card and select <strong>Delete</strong>. You'll be asked to confirm the deletion. The file will be moved to the trash area where it can be restored or permanently deleted later.</p>
|
||||||
|
<p class="advice">Deleting a file doesn't immediately remove it permanently. You can recover deleted files from the trash area within a certain time period.</p>
|
||||||
|
|
||||||
|
<h2 id="drafts">Drafts</h2>
|
||||||
|
<p>The drafts section is a fixed, non-deletable space in your dashboard where you can create and store files that aren't part of any specific project yet. This is useful for quick sketches, experimental designs, or files you're not ready to organize into projects.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/02-drafts.webp" alt="Drafts section" />
|
||||||
|
</figure>
|
||||||
|
<p>Drafts appear in a dedicated section in the dashboard sidebar, separate from your projects. All team members can see and access files in the drafts section, depending on their permissions.</p>
|
||||||
|
<p>You can create files directly in drafts, or move existing files from projects into drafts if you want to temporarily remove them from a project's organization. Files in drafts work exactly like files in projects - they have the same functionality and features.</p>
|
||||||
|
<p>When you're ready to organize a file from drafts, you can move it into an appropriate project using the move option in the file's context menu.</p>
|
||||||
|
|
||||||
|
<h2 id="trash-area">Trash area</h2>
|
||||||
|
<p>When you delete projects or files, they are not removed permanently. Instead, they are moved to a trash area, a dedicated space for deleted content. This allows you to recover mistakenly deleted content or permanently remove items when you're sure you don't need them anymore.</p>
|
||||||
|
<p>The trash applies to both files and projects. Items in the trash remain there for a certain period depending on your Penpot subscription plan before being automatically deleted permanently.</p>
|
||||||
|
|
||||||
|
<h3 id="access-trash">Access the trash</h3>
|
||||||
|
<p>A <strong>Trash</strong> section is accessible from the dashboard navigation. When you access it, you'll see all your deleted files and projects, each clearly labeled so you can easily identify what you want to restore or permanently delete.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/files-projects/03-trash.webp" alt="Trash area" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h3 id="trash-permissions">Trash permissions</h3>
|
||||||
|
<p>Access to the trash and the actions you can perform depend on your role in the team:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Owner, Admin, and Editor:</strong> Can view the trash, restore deleted items, and permanently delete items from the trash.</li>
|
||||||
|
<li><strong>Viewer:</strong> Cannot access the trash or manage deleted content.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="restore-items">Restore items</h3>
|
||||||
|
<p>To restore a deleted file or project, access the trash area and find the item you want to recover. Select the item and choose <strong>Restore</strong>. The item will be restored to its original location (the project it belonged to, or the drafts section if it wasn't in a project).</p>
|
||||||
|
|
||||||
|
<h3 id="permanently-delete">Permanently delete items</h3>
|
||||||
|
<p>If you're sure you don't need an item anymore, you can permanently delete it from the trash. Select the item and choose <strong>Permanently delete</strong>. This action cannot be undone, so make sure you really want to remove the item permanently.</p>
|
||||||
|
<p class="advice">Items in the trash are automatically deleted after a certain period depending on your subscription plan. If you want to keep something, restore it before the auto-deletion period expires.</p>
|
||||||
@@ -455,6 +455,43 @@ ExtraBold Italic
|
|||||||
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
||||||
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
||||||
|
|
||||||
|
<h3 id="design-tokens-shadow">Shadow</h3>
|
||||||
|
<p>Shadow tokens are composite entities that encapsulate the properties of one or more shadows into a single token definition. This token can contain a single shadow or an array of multiple shadows that can be reordered.</p>
|
||||||
|
<p>Shadow tokens support both <strong>Drop Shadow</strong> and <strong>Inner Shadow</strong> types. When creating or editing a shadow token, you can select the type of shadow you want to use. The default selection is Drop Shadow.</p>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/design-tokens/37-tokens-shadow-individual.webp" alt="Shadow token creation with individual values" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h4 id="design-tokens-shadow-properties">Shadow properties</h4>
|
||||||
|
<p>Each shadow within a shadow token contains a set of properties that define how the shadow appears:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Color:</strong> The color of the shadow. Accepts the same values as <a href="#design-tokens-color">color tokens</a> (Hex, RGB, RGBA, ARGB, HSL, HSLA), and you can reference existing color tokens. The color picker is available when defining the value.</li>
|
||||||
|
<li><strong>X offset:</strong> The horizontal offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||||
|
<li><strong>Y offset:</strong> The vertical offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||||
|
<li><strong>Blur:</strong> The blur radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||||
|
<li><strong>Spread:</strong> The spread radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||||
|
<li><strong>Type:</strong> Whether the shadow is a drop shadow or an inner shadow. Selected via a dropdown menu, with Drop Shadow as the default.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Each property within a shadow token can reference existing tokens or be assigned hardcoded values. Shadows can also reference other shadow tokens (the type of shadow must match when using references).</p>
|
||||||
|
<p class="advice">Not all properties are mandatory to save a shadow token. Some can be empty (and will be computed as 0). Only the color property is mandatory. In an array of shadows, if any shadow does not have the color set, the form cannot be saved.</p>
|
||||||
|
|
||||||
|
<h4 id="design-tokens-shadow-create">Creating shadow tokens</h4>
|
||||||
|
<p>To create a shadow token, click on the <strong>+</strong> next to <strong>Shadow</strong> in the Tokens panel. Shadow tokens can be created in two ways:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Individual values:</strong> You can create one shadow or multiple shadows with individual property values. Click the <strong>+</strong> button to add more shadows to the array. New shadows are added at the top of the list.</li>
|
||||||
|
<li><strong>Single reference:</strong> You can reference another existing shadow token. When using a single reference, you cannot add more than one shadow. The resolved value will display the shadow or list of shadows that the referenced token contains.</li>
|
||||||
|
</ul>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/design-tokens/38-tokens-shadow-reference.webp" alt="Shadow token creation with reference" />
|
||||||
|
</figure>
|
||||||
|
<p>When creating a shadow with individual values, the color value starts empty, but the other inputs have default values (X: 4, Y: 4, Blur: 4, Spread: 0). You can reorder shadows by hovering over a shadow form and using the reorder button to drag it to a different position.</p>
|
||||||
|
<p>You can also reference another existing shadow token instead of defining each property manually. When doing so, Penpot resolves all shadow properties from the referenced token.</p>
|
||||||
|
|
||||||
|
<h4 id="design-tokens-shadow-apply">Applying shadow tokens</h4>
|
||||||
|
<p>Shadow tokens can be applied to any layer type. Clicking on a shadow token will apply it to the selected layer. Right-clicking on a shadow token shows the context menu with the <strong>Shadow</strong> option to apply it.</p>
|
||||||
|
<p class="advice">Text elements in CSS do not support inner shadows, but Penpot does, since it uses the filter property internally instead of the box-shadow property.</p>
|
||||||
|
<p>When applying a shadow token, any existing shadow on the layer will be overridden (whether it's a raw shadow or an applied token shadow). If the token contains an array of shadows, each shadow will be added in the same order as in the creation form.</p>
|
||||||
|
<p class="advice">In Penpot, an element can have multiple shadows, but only one token of the same type can be applied. This means that applying a second shadow token would override the first one, regardless of how many shadows the shape currently has.</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
|
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"generic-pool": "^3.9.0",
|
"generic-pool": "^3.9.0",
|
||||||
"inflation": "^2.1.0",
|
"inflation": "^2.1.0",
|
||||||
"ioredis": "^5.8.1",
|
"ioredis": "^5.8.2",
|
||||||
"playwright": "^1.55.1",
|
"playwright": "^1.57.0",
|
||||||
"raw-body": "^3.0.1",
|
"raw-body": "^3.0.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"svgo": "penpot/svgo#v3.1",
|
"svgo": "penpot/svgo#v3.1",
|
||||||
"undici": "^7.16.0",
|
"undici": "^7.16.0",
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
|
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
|
||||||
"watch:app": "clojure -M:dev:shadow-cljs watch main",
|
"watch:app": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main",
|
||||||
"watch": "yarn run clear:shadow-cache && yarn run watch:app",
|
"watch": "yarn run watch:app",
|
||||||
"build:app": "clojure -M:dev:shadow-cljs release main",
|
"build:app": "clojure -M:dev:shadow-cljs release main",
|
||||||
"build": "yarn run clear:shadow-cache && yarn run build:app",
|
"build": "yarn run clear:shadow-cache && yarn run build:app",
|
||||||
"fmt:clj:check": "cljfmt check --parallel=false src/",
|
"fmt:clj:check": "cljfmt check --parallel=false src/",
|
||||||
|
|||||||
8
exporter/scripts/setup
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e;
|
||||||
|
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
yarn install;
|
||||||
|
yarn playwright install chromium
|
||||||
7
exporter/scripts/watch
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TARGET=${1:-app};
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
exec yarn run watch:$TARGET
|
||||||
@@ -243,7 +243,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"bytes@npm:3.1.2":
|
"bytes@npm:~3.1.2":
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
resolution: "bytes@npm:3.1.2"
|
resolution: "bytes@npm:3.1.2"
|
||||||
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
|
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
|
||||||
@@ -442,7 +442,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"depd@npm:2.0.0, depd@npm:~2.0.0":
|
"depd@npm:~2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "depd@npm:2.0.0"
|
resolution: "depd@npm:2.0.0"
|
||||||
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
|
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
|
||||||
@@ -577,9 +577,9 @@ __metadata:
|
|||||||
date-fns: "npm:^4.1.0"
|
date-fns: "npm:^4.1.0"
|
||||||
generic-pool: "npm:^3.9.0"
|
generic-pool: "npm:^3.9.0"
|
||||||
inflation: "npm:^2.1.0"
|
inflation: "npm:^2.1.0"
|
||||||
ioredis: "npm:^5.8.1"
|
ioredis: "npm:^5.8.2"
|
||||||
playwright: "npm:^1.55.1"
|
playwright: "npm:^1.57.0"
|
||||||
raw-body: "npm:^3.0.1"
|
raw-body: "npm:^3.0.2"
|
||||||
source-map-support: "npm:^0.5.21"
|
source-map-support: "npm:^0.5.21"
|
||||||
svgo: "penpot/svgo#v3.1"
|
svgo: "penpot/svgo#v3.1"
|
||||||
undici: "npm:^7.16.0"
|
undici: "npm:^7.16.0"
|
||||||
@@ -683,16 +683,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"http-errors@npm:2.0.0":
|
"http-errors@npm:~2.0.1":
|
||||||
version: 2.0.0
|
version: 2.0.1
|
||||||
resolution: "http-errors@npm:2.0.0"
|
resolution: "http-errors@npm:2.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
depd: "npm:2.0.0"
|
depd: "npm:~2.0.0"
|
||||||
inherits: "npm:2.0.4"
|
inherits: "npm:~2.0.4"
|
||||||
setprototypeof: "npm:1.2.0"
|
setprototypeof: "npm:~1.2.0"
|
||||||
statuses: "npm:2.0.1"
|
statuses: "npm:~2.0.2"
|
||||||
toidentifier: "npm:1.0.1"
|
toidentifier: "npm:~1.0.1"
|
||||||
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19
|
checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -716,15 +716,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"iconv-lite@npm:0.7.0":
|
|
||||||
version: 0.7.0
|
|
||||||
resolution: "iconv-lite@npm:0.7.0"
|
|
||||||
dependencies:
|
|
||||||
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
|
|
||||||
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"iconv-lite@npm:^0.6.2":
|
"iconv-lite@npm:^0.6.2":
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
resolution: "iconv-lite@npm:0.6.3"
|
resolution: "iconv-lite@npm:0.6.3"
|
||||||
@@ -734,6 +725,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"iconv-lite@npm:~0.7.0":
|
||||||
|
version: 0.7.0
|
||||||
|
resolution: "iconv-lite@npm:0.7.0"
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
|
||||||
|
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ieee754@npm:^1.2.1":
|
"ieee754@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "ieee754@npm:1.2.1"
|
resolution: "ieee754@npm:1.2.1"
|
||||||
@@ -755,16 +755,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"inherits@npm:2.0.4, inherits@npm:~2.0.3":
|
"inherits@npm:~2.0.3, inherits@npm:~2.0.4":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "inherits@npm:2.0.4"
|
resolution: "inherits@npm:2.0.4"
|
||||||
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
|
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"ioredis@npm:^5.8.1":
|
"ioredis@npm:^5.8.2":
|
||||||
version: 5.8.1
|
version: 5.8.2
|
||||||
resolution: "ioredis@npm:5.8.1"
|
resolution: "ioredis@npm:5.8.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ioredis/commands": "npm:1.4.0"
|
"@ioredis/commands": "npm:1.4.0"
|
||||||
cluster-key-slot: "npm:^1.1.0"
|
cluster-key-slot: "npm:^1.1.0"
|
||||||
@@ -775,7 +775,7 @@ __metadata:
|
|||||||
redis-errors: "npm:^1.2.0"
|
redis-errors: "npm:^1.2.0"
|
||||||
redis-parser: "npm:^3.0.0"
|
redis-parser: "npm:^3.0.0"
|
||||||
standard-as-callback: "npm:^2.1.0"
|
standard-as-callback: "npm:^2.1.0"
|
||||||
checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465
|
checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -1106,27 +1106,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"playwright-core@npm:1.55.1":
|
"playwright-core@npm:1.57.0":
|
||||||
version: 1.55.1
|
version: 1.57.0
|
||||||
resolution: "playwright-core@npm:1.55.1"
|
resolution: "playwright-core@npm:1.57.0"
|
||||||
bin:
|
bin:
|
||||||
playwright-core: cli.js
|
playwright-core: cli.js
|
||||||
checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f
|
checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"playwright@npm:^1.55.1":
|
"playwright@npm:^1.57.0":
|
||||||
version: 1.55.1
|
version: 1.57.0
|
||||||
resolution: "playwright@npm:1.55.1"
|
resolution: "playwright@npm:1.57.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
fsevents: "npm:2.3.2"
|
fsevents: "npm:2.3.2"
|
||||||
playwright-core: "npm:1.55.1"
|
playwright-core: "npm:1.57.0"
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
fsevents:
|
fsevents:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
playwright: cli.js
|
playwright: cli.js
|
||||||
checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771
|
checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -1161,15 +1161,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"raw-body@npm:^3.0.1":
|
"raw-body@npm:^3.0.2":
|
||||||
version: 3.0.1
|
version: 3.0.2
|
||||||
resolution: "raw-body@npm:3.0.1"
|
resolution: "raw-body@npm:3.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: "npm:3.1.2"
|
bytes: "npm:~3.1.2"
|
||||||
http-errors: "npm:2.0.0"
|
http-errors: "npm:~2.0.1"
|
||||||
iconv-lite: "npm:0.7.0"
|
iconv-lite: "npm:~0.7.0"
|
||||||
unpipe: "npm:1.0.0"
|
unpipe: "npm:~1.0.0"
|
||||||
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a
|
checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -1270,7 +1270,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"setprototypeof@npm:1.2.0":
|
"setprototypeof@npm:~1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "setprototypeof@npm:1.2.0"
|
resolution: "setprototypeof@npm:1.2.0"
|
||||||
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
|
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
|
||||||
@@ -1368,10 +1368,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"statuses@npm:2.0.1":
|
"statuses@npm:~2.0.2":
|
||||||
version: 2.0.1
|
version: 2.0.2
|
||||||
resolution: "statuses@npm:2.0.1"
|
resolution: "statuses@npm:2.0.2"
|
||||||
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
|
checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -1500,7 +1500,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"toidentifier@npm:1.0.1":
|
"toidentifier@npm:~1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "toidentifier@npm:1.0.1"
|
resolution: "toidentifier@npm:1.0.1"
|
||||||
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
|
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
|
||||||
@@ -1539,7 +1539,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"unpipe@npm:1.0.0":
|
"unpipe@npm:~1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "unpipe@npm:1.0.0"
|
resolution: "unpipe@npm:1.0.0"
|
||||||
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
|
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||||
const config = {
|
const config = {
|
||||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||||
@@ -5,18 +7,38 @@ const config = {
|
|||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-themes",
|
"@storybook/addon-themes",
|
||||||
"@storybook/addon-docs",
|
"@storybook/addon-docs",
|
||||||
"@storybook/addon-vitest"
|
"@storybook/addon-vitest",
|
||||||
],
|
],
|
||||||
core: {
|
|
||||||
builder: "@storybook/builder-vite",
|
|
||||||
options: {
|
|
||||||
viteConfigPath: "../vite.config.js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
framework: {
|
framework: {
|
||||||
name: "@storybook/react-vite",
|
name: "@storybook/react-vite",
|
||||||
options: {},
|
options: {
|
||||||
|
// fastRefresh: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
docs: {},
|
docs: {},
|
||||||
|
|
||||||
|
async viteFinal(config) {
|
||||||
|
return defineConfig({
|
||||||
|
...config,
|
||||||
|
plugins: [
|
||||||
|
...(config.plugins ?? []),
|
||||||
|
{
|
||||||
|
name: 'force-full-reload-always',
|
||||||
|
apply: 'serve',
|
||||||
|
enforce: 'post',
|
||||||
|
|
||||||
|
handleHotUpdate(ctx) {
|
||||||
|
ctx.server.ws.send({
|
||||||
|
type: 'full-reload',
|
||||||
|
path: '*',
|
||||||
|
});
|
||||||
|
|
||||||
|
// returning [] tells Vite: “no modules handled”
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||||
|
|
||||||
|
import Components from "@target/components";
|
||||||
|
import translations from "@public/translation.en.js";
|
||||||
|
Components.setDefaultTranslations(translations);
|
||||||
|
|
||||||
import '../resources/public/css/ds.css';
|
import '../resources/public/css/ds.css';
|
||||||
|
|
||||||
export const decorators = [
|
export const decorators = [
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
metosin/reitit-core {:mvn/version "0.9.1"}
|
metosin/reitit-core {:mvn/version "0.9.1"}
|
||||||
funcool/okulary {:mvn/version "2022.04.11-16"}
|
funcool/okulary {:mvn/version "2022.04.11-16"}
|
||||||
|
|
||||||
|
funcool/tubax
|
||||||
|
{:git/tag "v2025.11.28"
|
||||||
|
:git/sha "2d9a986"
|
||||||
|
:git/url "https://github.com/funcool/tubax.git"}
|
||||||
|
|
||||||
funcool/potok2
|
funcool/potok2
|
||||||
{:git/tag "v2.2"
|
{:git/tag "v2.2"
|
||||||
:git/sha "0f7e15a"
|
:git/sha "0f7e15a"
|
||||||
@@ -20,8 +25,8 @@
|
|||||||
:git/url "https://github.com/funcool/beicon.git"}
|
:git/url "https://github.com/funcool/beicon.git"}
|
||||||
|
|
||||||
funcool/rumext
|
funcool/rumext
|
||||||
{:git/tag "v2.24"
|
{:git/tag "v2.25"
|
||||||
:git/sha "17a0c94"
|
:git/sha "27e5a1a"
|
||||||
:git/url "https://github.com/funcool/rumext.git"}
|
:git/url "https://github.com/funcool/rumext.git"}
|
||||||
|
|
||||||
instaparse/instaparse {:mvn/version "1.5.0"}
|
instaparse/instaparse {:mvn/version "1.5.0"}
|
||||||
@@ -42,10 +47,10 @@
|
|||||||
:dev
|
:dev
|
||||||
{:extra-paths ["dev"]
|
{:extra-paths ["dev"]
|
||||||
:extra-deps
|
:extra-deps
|
||||||
{thheller/shadow-cljs {:mvn/version "3.2.0"}
|
{thheller/shadow-cljs {:mvn/version "3.2.2"}
|
||||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||||
criterium/criterium {:mvn/version "RELEASE"}
|
criterium/criterium {:mvn/version "0.4.6"}
|
||||||
cider/cider-nrepl {:mvn/version "0.57.0"}}}
|
cider/cider-nrepl {:mvn/version "0.57.0"}}}
|
||||||
|
|
||||||
:shadow-cljs
|
:shadow-cljs
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"e2e:server": "node ./scripts/e2e-server.js",
|
"e2e:server": "node ./scripts/e2e-server.js",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
|
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
|
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
||||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||||
@@ -47,89 +47,81 @@
|
|||||||
"watch:app:libs": "node ./scripts/build-libs.js --watch",
|
"watch:app:libs": "node ./scripts/build-libs.js --watch",
|
||||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
||||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||||
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
"watch": "exit 0",
|
||||||
"watch": "yarn run watch:app:assets",
|
"watch:app": "yarn run clear:shadow-cache && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||||
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
|
"watch:storybook": "yarn run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
|
||||||
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.52.0",
|
"@penpot/draft-js": "portal:./packages/draft-js",
|
||||||
"@storybook/addon-docs": "10.0.4",
|
"@penpot/mousetrap": "portal:./packages/mousetrap",
|
||||||
"@storybook/addon-themes": "10.0.4",
|
"@penpot/plugins-runtime": "1.3.2",
|
||||||
"@storybook/addon-vitest": "10.0.4",
|
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||||
"@storybook/react-vite": "10.0.4",
|
"@penpot/text-editor": "portal:./text-editor",
|
||||||
"@types/node": "^22.15.21",
|
"@playwright/test": "1.57.0",
|
||||||
"@vitest/browser": "3.2.4",
|
"@storybook/addon-docs": "10.1.11",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@storybook/addon-themes": "10.1.11",
|
||||||
|
"@storybook/addon-vitest": "10.1.11",
|
||||||
|
"@storybook/react-vite": "10.1.11",
|
||||||
|
"@tokens-studio/sd-transforms": "1.2.11",
|
||||||
|
"@types/node": "^22.19.3",
|
||||||
|
"@vitest/browser": "4.0.16",
|
||||||
|
"@vitest/browser-playwright": "^4.0.16",
|
||||||
|
"@vitest/coverage-v8": "4.0.16",
|
||||||
|
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.9",
|
||||||
|
"eventsource-parser": "^3.0.6",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"getopts": "^2.3.0",
|
"getopts": "^2.3.0",
|
||||||
"gettext-parser": "^8.0.0",
|
"gettext-parser": "^8.0.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"highlight.js": "^11.10.0",
|
||||||
"gulp-gzip": "^1.4.2",
|
"js-beautify": "^1.15.4",
|
||||||
"gulp-mustache": "^5.0.0",
|
"jsdom": "^27.4.0",
|
||||||
"gulp-postcss": "^10.0.0",
|
"lodash": "^4.17.21",
|
||||||
"gulp-rename": "^2.0.0",
|
"lodash.debounce": "^4.0.8",
|
||||||
"gulp-sourcemaps": "^3.0.0",
|
|
||||||
"gulp-svg-sprite": "^2.0.3",
|
|
||||||
"jsdom": "^27.0.0",
|
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
"mkdirp": "^3.0.1",
|
"mkdirp": "^3.0.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"opentype.js": "^1.3.4",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"playwright": "1.56.1",
|
"playwright": "1.56.1",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.4",
|
||||||
"postcss-clean": "^1.2.2",
|
"postcss-clean": "^1.2.2",
|
||||||
|
"postcss-modules": "^6.0.1",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"rimraf": "^6.0.1",
|
|
||||||
"sass": "^1.89.0",
|
|
||||||
"sass-embedded": "^1.89.0",
|
|
||||||
"storybook": "10.0.4",
|
|
||||||
"svg-sprite": "^2.0.4",
|
|
||||||
"typescript": "^5.9.2",
|
|
||||||
"vite": "^6.3.5",
|
|
||||||
"vitest": "^3.2.0",
|
|
||||||
"wasm-pack": "^0.13.1",
|
|
||||||
"watcher": "^2.3.1",
|
|
||||||
"workerpool": "^9.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@penpot/draft-js": "portal:./vendor/draft-js",
|
|
||||||
"@penpot/hljs": "portal:./vendor/hljs",
|
|
||||||
"@penpot/mousetrap": "portal:./vendor/mousetrap",
|
|
||||||
"@penpot/plugins-runtime": "1.3.2",
|
|
||||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
|
||||||
"@penpot/text-editor": "portal:./text-editor",
|
|
||||||
"@tokens-studio/sd-transforms": "1.2.11",
|
|
||||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
|
||||||
"compression": "^1.8.1",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"eventsource-parser": "^3.0.6",
|
|
||||||
"js-beautify": "^1.15.4",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lodash.debounce": "^4.0.8",
|
|
||||||
"opentype.js": "^1.3.4",
|
|
||||||
"postcss-modules": "^6.0.1",
|
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-virtualized": "^9.22.6",
|
"react-virtualized": "^9.22.6",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
"rxjs": "8.0.0-alpha.14",
|
"rxjs": "8.0.0-alpha.14",
|
||||||
|
"sass": "^1.89.0",
|
||||||
|
"sass-embedded": "^1.89.0",
|
||||||
"sax": "^1.4.1",
|
"sax": "^1.4.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
|
"storybook": "10.1.11",
|
||||||
"style-dictionary": "5.0.0-rc.1",
|
"style-dictionary": "5.0.0-rc.1",
|
||||||
|
"svg-sprite": "^2.0.4",
|
||||||
"tdigest": "^0.1.2",
|
"tdigest": "^0.1.2",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
"ua-parser-js": "2.0.5",
|
"ua-parser-js": "2.0.5",
|
||||||
|
"vite": "^7.3.0",
|
||||||
|
"vitest": "^4.0.16",
|
||||||
|
"wait-on": "^9.0.3",
|
||||||
|
"wasm-pack": "^0.13.1",
|
||||||
|
"watcher": "^2.3.1",
|
||||||
|
"workerpool": "^9.3.2",
|
||||||
"xregexp": "^5.1.2"
|
"xregexp": "^5.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export const {
|
|||||||
RichTextEditorUtil,
|
RichTextEditorUtil,
|
||||||
SelectionState,
|
SelectionState,
|
||||||
convertFromRaw,
|
convertFromRaw,
|
||||||
convertToRaw
|
convertToRaw,
|
||||||
|
EditorBlock,
|
||||||
|
Editor
|
||||||
} = pkg;
|
} = pkg;
|
||||||
|
|
||||||
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';
|
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"author": "Andrey Antukh",
|
"author": "Andrey Antukh",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
|
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
|
||||||
|
"immutable": "^5.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=0.17.0",
|
"react": ">=0.17.0",
|
||||||
@@ -173,12 +173,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@penpot/draft-js-wrapper@workspace:.":
|
"@penpot/draft-js@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@penpot/draft-js-wrapper@workspace:."
|
resolution: "@penpot/draft-js@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
|
draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
|
||||||
esbuild: "npm:^0.24.0"
|
esbuild: "npm:^0.24.0"
|
||||||
|
immutable: "npm:^5.1.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=0.17.0"
|
react: ">=0.17.0"
|
||||||
react-dom: ">=0.17.0"
|
react-dom: ">=0.17.0"
|
||||||
@@ -320,6 +321,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"immutable@npm:^5.1.4":
|
||||||
|
version: 5.1.4
|
||||||
|
resolution: "immutable@npm:5.1.4"
|
||||||
|
checksum: 10c0/f1c98382e4cde14a0b218be3b9b2f8441888da8df3b8c064aa756071da55fbed6ad696e5959982508456332419be9fdeaf29b2e58d0eadc45483cc16963c0446
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"immutable@npm:~3.7.4":
|
"immutable@npm:~3.7.4":
|
||||||
version: 3.7.6
|
version: 3.7.6
|
||||||
resolution: "immutable@npm:3.7.6"
|
resolution: "immutable@npm:3.7.6"
|
||||||
@@ -22,9 +22,9 @@ export default defineConfig({
|
|||||||
workers: 1,
|
workers: 1,
|
||||||
/* Timeout for expects (longer in CI) */
|
/* Timeout for expects (longer in CI) */
|
||||||
|
|
||||||
timeout: 60000,
|
timeout: 80000,
|
||||||
expect: {
|
expect: {
|
||||||
timeout: process.env.CI ? 30000 : 5000,
|
timeout: process.env.CI ? 40000 : 5000,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41234",
|
||||||
|
"~:revn": 1,
|
||||||
|
"~:vern": 1,
|
||||||
|
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||||
|
"~:created-at": "~m1705307400000",
|
||||||
|
"~:modified-at": "~m1732111500000",
|
||||||
|
"~:deleted-at": "~m1732111500000",
|
||||||
|
"~:name": "Deleted Design File 1",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:will-be-deleted-at": "~m1732716300000",
|
||||||
|
"~:thumbnail-id": null,
|
||||||
|
"~:row-num": 1,
|
||||||
|
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41235",
|
||||||
|
"~:revn": 2,
|
||||||
|
"~:vern": 2,
|
||||||
|
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||||
|
"~:created-at": "~m1704875700000",
|
||||||
|
"~:modified-at": "~m1732025400000",
|
||||||
|
"~:deleted-at": "~m1732025400000",
|
||||||
|
"~:name": "Deleted Design File 2",
|
||||||
|
"~:is-shared": true,
|
||||||
|
"~:will-be-deleted-at": "~m1732630200000",
|
||||||
|
"~:thumbnail-id": null,
|
||||||
|
"~:row-num": 2,
|
||||||
|
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41236",
|
||||||
|
"~:revn": 3,
|
||||||
|
"~:vern": 3,
|
||||||
|
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
|
||||||
|
"~:created-at": "~m1706792400000",
|
||||||
|
"~:modified-at": "~m1731939600000",
|
||||||
|
"~:deleted-at": "~m1731939600000",
|
||||||
|
"~:name": "Old Project Design",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:will-be-deleted-at": "~m1732544400000",
|
||||||
|
"~:thumbnail-id": null,
|
||||||
|
"~:row-num": 3,
|
||||||
|
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
|
||||||
|
}
|
||||||
|
]
|
||||||
58
frontend/playwright/data/text-editor/get-file-blank.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:permissions": {
|
||||||
|
"~:type": "~:membership",
|
||||||
|
"~:is-owner": true,
|
||||||
|
"~:is-admin": true,
|
||||||
|
"~:can-edit": true,
|
||||||
|
"~:can-read": true,
|
||||||
|
"~:is-logged": true
|
||||||
|
},
|
||||||
|
"~:has-media-trimmed": false,
|
||||||
|
"~:comment-thread-seqn": 0,
|
||||||
|
"~:name": "New File 1",
|
||||||
|
"~:revn": 11,
|
||||||
|
"~:modified-at": "~m1713873823633",
|
||||||
|
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:version": 46,
|
||||||
|
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||||
|
"~:created-at": "~m1713536343369",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~u66697432-c33d-8055-8006-2c62cc084cad"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u66697432-c33d-8055-8006-2c62cc084cad": {
|
||||||
|
"~#penpot/pointer": [
|
||||||
|
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
|
||||||
|
{
|
||||||
|
"~:created-at": "~m1713873823636"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true
|
||||||
|
},
|
||||||
|
"~:recent-colors": [
|
||||||
|
{
|
||||||
|
"~:color": "#0000ff",
|
||||||
|
"~:opacity": 1,
|
||||||
|
"~:id": null,
|
||||||
|
"~:file-id": null,
|
||||||
|
"~:image": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
345
frontend/playwright/data/text-editor/get-file-lorem-ipsum.json
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"plugins/runtime",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type",
|
||||||
|
"text-editor/v2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
|
||||||
|
"~:permissions": {
|
||||||
|
"~:type": "~:membership",
|
||||||
|
"~:is-owner": true,
|
||||||
|
"~:is-admin": true,
|
||||||
|
"~:can-edit": true,
|
||||||
|
"~:can-read": true,
|
||||||
|
"~:is-logged": true
|
||||||
|
},
|
||||||
|
"~:has-media-trimmed": false,
|
||||||
|
"~:comment-thread-seqn": 0,
|
||||||
|
"~:name": "Bug 11552",
|
||||||
|
"~:revn": 3,
|
||||||
|
"~:modified-at": "~m1753957736516",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:migrations": {
|
||||||
|
"~#ordered-set": [
|
||||||
|
"legacy-2",
|
||||||
|
"legacy-3",
|
||||||
|
"legacy-5",
|
||||||
|
"legacy-6",
|
||||||
|
"legacy-7",
|
||||||
|
"legacy-8",
|
||||||
|
"legacy-9",
|
||||||
|
"legacy-10",
|
||||||
|
"legacy-11",
|
||||||
|
"legacy-12",
|
||||||
|
"legacy-13",
|
||||||
|
"legacy-14",
|
||||||
|
"legacy-16",
|
||||||
|
"legacy-17",
|
||||||
|
"legacy-18",
|
||||||
|
"legacy-19",
|
||||||
|
"legacy-25",
|
||||||
|
"legacy-26",
|
||||||
|
"legacy-27",
|
||||||
|
"legacy-28",
|
||||||
|
"legacy-29",
|
||||||
|
"legacy-31",
|
||||||
|
"legacy-32",
|
||||||
|
"legacy-33",
|
||||||
|
"legacy-34",
|
||||||
|
"legacy-36",
|
||||||
|
"legacy-37",
|
||||||
|
"legacy-38",
|
||||||
|
"legacy-39",
|
||||||
|
"legacy-40",
|
||||||
|
"legacy-41",
|
||||||
|
"legacy-42",
|
||||||
|
"legacy-43",
|
||||||
|
"legacy-44",
|
||||||
|
"legacy-45",
|
||||||
|
"legacy-46",
|
||||||
|
"legacy-47",
|
||||||
|
"legacy-48",
|
||||||
|
"legacy-49",
|
||||||
|
"legacy-50",
|
||||||
|
"legacy-51",
|
||||||
|
"legacy-52",
|
||||||
|
"legacy-53",
|
||||||
|
"legacy-54",
|
||||||
|
"legacy-55",
|
||||||
|
"legacy-56",
|
||||||
|
"legacy-57",
|
||||||
|
"legacy-59",
|
||||||
|
"legacy-62",
|
||||||
|
"legacy-65",
|
||||||
|
"legacy-66",
|
||||||
|
"legacy-67",
|
||||||
|
"0001-remove-tokens-from-groups",
|
||||||
|
"0002-normalize-bool-content-v2",
|
||||||
|
"0002-clean-shape-interactions",
|
||||||
|
"0003-fix-root-shape",
|
||||||
|
"0003-convert-path-content-v2",
|
||||||
|
"0004-clean-shadow-color",
|
||||||
|
"0005-deprecate-image-type",
|
||||||
|
"0006-fix-old-texts-fills",
|
||||||
|
"0007-clear-invalid-strokes-and-fills-v2",
|
||||||
|
"0008-fix-library-colors-v4",
|
||||||
|
"0009-clean-library-colors",
|
||||||
|
"0009-add-partial-text-touched-flags"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
|
||||||
|
"~:created-at": "~m1753957644225",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u238a17e0-75ff-8075-8006-934586ea2231": {
|
||||||
|
"~:objects": {
|
||||||
|
"~u00000000-0000-0000-0000-000000000000": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 0,
|
||||||
|
"~:hide-fill-on-export": false,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "Root Frame",
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:type": "~:frame",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.0,
|
||||||
|
"~:y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.0,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:r3": 0,
|
||||||
|
"~:r1": 0,
|
||||||
|
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [],
|
||||||
|
"~:x": 0,
|
||||||
|
"~:proportion": 1.0,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0,
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:x1": 0,
|
||||||
|
"~:y1": 0,
|
||||||
|
"~:x2": 0.01,
|
||||||
|
"~:y2": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#FFFFFF",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:flip-y": null,
|
||||||
|
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 438,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:auto-width",
|
||||||
|
"~:content": {
|
||||||
|
"~:type": "root",
|
||||||
|
"~:key": "1s4am1jl24s",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:type": "paragraph-set",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:typography-ref-id": null,
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "13p0zwl2yhc",
|
||||||
|
"~:font-size": "14",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:typography-ref-file": null,
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:text": "Lorem ipsum"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:typography-ref-id": null,
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "20hf3kmyoub",
|
||||||
|
"~:font-size": "14",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:typography-ref-file": null,
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:type": "paragraph",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:vertical-align": "top"
|
||||||
|
},
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "Lorem ipsum",
|
||||||
|
"~:width": 77,
|
||||||
|
"~:type": "~:text",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 404,
|
||||||
|
"~:y": 438
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 481,
|
||||||
|
"~:y": 438
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 481,
|
||||||
|
"~:y": 455
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 404,
|
||||||
|
"~:y": 455
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:x": 404,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 404,
|
||||||
|
"~:y": 438,
|
||||||
|
"~:width": 77,
|
||||||
|
"~:height": 17,
|
||||||
|
"~:x1": 404,
|
||||||
|
"~:y1": 438,
|
||||||
|
"~:x2": 481,
|
||||||
|
"~:y2": 455
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 17,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
|
||||||
|
"~:name": "Page 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1 @@
|
|||||||
{
|
w
|
||||||
"~:revn": 2,
|
|
||||||
"~:lagged": []
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
4
frontend/playwright/data/text-editor/update-file.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"~:revn": 2,
|
||||||
|
"~:lagged": []
|
||||||
|
}
|
||||||
@@ -5947,8 +5947,8 @@
|
|||||||
"~:spread": "10",
|
"~:spread": "10",
|
||||||
"~:color": "rgb(160, 73, 73)",
|
"~:color": "rgb(160, 73, 73)",
|
||||||
"~:inset": true,
|
"~:inset": true,
|
||||||
"~:offsetX": "10",
|
"~:offset-x": "10",
|
||||||
"~:offsetY": "10"
|
"~:offset-y": "10"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"~:description": "",
|
"~:description": "",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
|
||||||
|
"~:revn": 21,
|
||||||
|
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||||
|
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
|
||||||
|
"~:changes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
36
frontend/playwright/helpers/Clipboard.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export class Clipboard {
|
||||||
|
static Permission = {
|
||||||
|
ONLY_READ: ["clipboard-read"],
|
||||||
|
ONLY_WRITE: ["clipboard-write"],
|
||||||
|
ALL: ["clipboard-read", "clipboard-write"],
|
||||||
|
};
|
||||||
|
|
||||||
|
static enable(context, permissions) {
|
||||||
|
return context.grantPermissions(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static writeText(page, text) {
|
||||||
|
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static readText(page) {
|
||||||
|
return page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(page, context) {
|
||||||
|
this.page = page;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(permissions) {
|
||||||
|
return Clipboard.enable(this.context, permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeText(text) {
|
||||||
|
return Clipboard.writeText(this.page, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
readText() {
|
||||||
|
return Clipboard.readText(this.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
frontend/playwright/helpers/Transit.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export class Transit {
|
||||||
|
static parse(value) {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
|
||||||
|
if (value.startsWith("~")) return value.slice(2);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(object, ...path) {
|
||||||
|
let aux = object;
|
||||||
|
for (const name of path) {
|
||||||
|
if (typeof name !== "string") {
|
||||||
|
if (!(name in aux)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
aux = aux[name];
|
||||||
|
} else {
|
||||||
|
const transitName = `~:${name}`;
|
||||||
|
if (!(transitName in aux)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
aux = aux[transitName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.parse(aux);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,27 @@
|
|||||||
export class BasePage {
|
export class BasePage {
|
||||||
|
/**
|
||||||
|
* Mocks multiple RPC calls in a single call.
|
||||||
|
*
|
||||||
|
* @param {Page} page
|
||||||
|
* @param {object<string, string>} paths
|
||||||
|
* @param {*} options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async mockRPCs(page, paths, options) {
|
||||||
|
for (const [path, jsonFilename] of Object.entries(paths)) {
|
||||||
|
await this.mockRPC(page, path, jsonFilename, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks an RPC call using a file.
|
||||||
|
*
|
||||||
|
* @param {Page} page
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} jsonFilename
|
||||||
|
* @param {*} options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
static async mockRPC(page, path, jsonFilename, options) {
|
static async mockRPC(page, path, jsonFilename, options) {
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new TypeError("Invalid page argument. Must be a Playwright page.");
|
throw new TypeError("Invalid page argument. Must be a Playwright page.");
|
||||||
@@ -73,7 +96,7 @@ export class BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async mockConfigFlags(page, flags) {
|
static async mockConfigFlags(page, flags) {
|
||||||
const url = "**/js/config.js?ts=*";
|
const url = "**/js/config.js*";
|
||||||
return await page.route(url, (route) =>
|
return await page.route(url, (route) =>
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -93,6 +116,10 @@ export class BasePage {
|
|||||||
return this.#page;
|
return this.#page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async mockRPCs(paths, options) {
|
||||||
|
return BasePage.mockRPCs(this.page, paths, options);
|
||||||
|
}
|
||||||
|
|
||||||
async mockRPC(path, jsonFilename, options) {
|
async mockRPC(path, jsonFilename, options) {
|
||||||
return BasePage.mockRPC(this.page, path, jsonFilename, options);
|
return BasePage.mockRPC(this.page, path, jsonFilename, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupDeletedFiles() {
|
||||||
|
await this.mockRPC(
|
||||||
|
"get-team-deleted-files?team-id=*",
|
||||||
|
"dashboard/get-team-deleted-files.json",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async setupDrafts() {
|
async setupDrafts() {
|
||||||
await this.mockRPC(
|
await this.mockRPC(
|
||||||
"get-project-files?project-id=*",
|
"get-project-files?project-id=*",
|
||||||
@@ -160,6 +167,10 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||||||
});
|
});
|
||||||
await this.mockRPC("search-files", "dashboard/search-files.json");
|
await this.mockRPC("search-files", "dashboard/search-files.json");
|
||||||
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
|
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
|
||||||
|
await this.mockRPC(
|
||||||
|
"get-team-deleted-files?team-id=*",
|
||||||
|
"dashboard/get-team-deleted-files.json",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupAccessTokensEmpty() {
|
async setupAccessTokensEmpty() {
|
||||||
@@ -289,6 +300,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||||||
await expect(this.mainHeading).toHaveText("Libraries");
|
await expect(this.mainHeading).toHaveText("Libraries");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async goToDeleted() {
|
||||||
|
await this.page.goto(
|
||||||
|
`#/dashboard/deleted?team-id=${DashboardPage.anyTeamId}`,
|
||||||
|
);
|
||||||
|
await expect(this.mainHeading).toHaveText("Projects");
|
||||||
|
}
|
||||||
|
|
||||||
async openProfileMenu() {
|
async openProfileMenu() {
|
||||||
await this.userAccount.click();
|
await this.userAccount.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,146 @@
|
|||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||||
|
import { Transit } from "../../helpers/Transit";
|
||||||
|
|
||||||
export class WorkspacePage extends BaseWebSocketPage {
|
export class WorkspacePage extends BaseWebSocketPage {
|
||||||
|
static TextEditor = class TextEditor {
|
||||||
|
constructor(workspacePage) {
|
||||||
|
this.workspacePage = workspacePage;
|
||||||
|
|
||||||
|
// locators.
|
||||||
|
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||||
|
name: "Font Size",
|
||||||
|
});
|
||||||
|
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||||
|
name: "Line Height",
|
||||||
|
});
|
||||||
|
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
|
||||||
|
"textbox",
|
||||||
|
{
|
||||||
|
name: "Letter Spacing",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return this.workspacePage.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForStyle(locator, styleName) {
|
||||||
|
return locator.evaluate(
|
||||||
|
(element, styleName) => element.style.getPropertyValue(styleName),
|
||||||
|
styleName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForEditor() {
|
||||||
|
return this.page.waitForSelector('[data-itype="editor"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForRoot() {
|
||||||
|
return this.page.waitForSelector('[data-itype="root"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForParagraph(nth) {
|
||||||
|
if (!nth) {
|
||||||
|
return this.page.waitForSelector('[data-itype="paragraph"]');
|
||||||
|
}
|
||||||
|
return this.page.waitForSelector(
|
||||||
|
`[data-itype="paragraph"]:nth-child(${nth})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForParagraphStyle(nth, styleName) {
|
||||||
|
const paragraph = await this.waitForParagraph(nth);
|
||||||
|
return this.waitForStyle(paragraph, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForTextSpan(nth = 0) {
|
||||||
|
if (!nth) {
|
||||||
|
return this.page.waitForSelector('[data-itype="inline"]');
|
||||||
|
}
|
||||||
|
return this.page.waitForSelector(
|
||||||
|
`[data-itype="inline"]:nth-child(${nth})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForTextSpanContent(nth = 0) {
|
||||||
|
const textSpan = await this.waitForTextSpan(nth);
|
||||||
|
const textContent = await textSpan.textContent();
|
||||||
|
return textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForTextSpanStyle(nth, styleName) {
|
||||||
|
const textSpan = await this.waitForTextSpan(nth);
|
||||||
|
return this.waitForStyle(textSpan, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startEditing() {
|
||||||
|
await this.page.keyboard.press("Enter");
|
||||||
|
return this.waitForEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEditing() {
|
||||||
|
return this.page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToLeft(amount = 0) {
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
await this.page.keyboard.press("ArrowLeft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToRight(amount = 0) {
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
await this.page.keyboard.press("ArrowRight");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveFromStart(offset = 0) {
|
||||||
|
await this.page.keyboard.press("ArrowLeft");
|
||||||
|
await this.moveToRight(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveFromEnd(offset = 0) {
|
||||||
|
await this.page.keyboard.press("ArrowRight");
|
||||||
|
await this.moveToLeft(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFromStart(length, offset = 0) {
|
||||||
|
await this.moveFromStart(offset);
|
||||||
|
await this.page.keyboard.down("Shift");
|
||||||
|
await this.moveToRight(length);
|
||||||
|
await this.page.keyboard.up("Shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFromEnd(length, offset = 0) {
|
||||||
|
await this.moveFromEnd(offset);
|
||||||
|
await this.page.keyboard.down("Shift");
|
||||||
|
await this.moveToLeft(length);
|
||||||
|
await this.page.keyboard.up("Shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeNumericInput(locator, newValue) {
|
||||||
|
await expect(locator).toBeVisible();
|
||||||
|
await locator.focus();
|
||||||
|
await locator.fill(`${newValue}`);
|
||||||
|
await locator.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFontSize(newValue) {
|
||||||
|
return this.changeNumericInput(this.fontSize, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLineHeight(newValue) {
|
||||||
|
return this.changeNumericInput(this.lineHeight, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLetterSpacing(newValue) {
|
||||||
|
return this.changeNumericInput(this.letterSpacing, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should be called on `test.beforeEach`.
|
* This should be called on `test.beforeEach`.
|
||||||
*
|
*
|
||||||
@@ -11,50 +150,21 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
static async init(page) {
|
static async init(page) {
|
||||||
await BaseWebSocketPage.initWebSockets(page);
|
await BaseWebSocketPage.initWebSockets(page);
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
await BaseWebSocketPage.mockRPCs(page, {
|
||||||
page,
|
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||||
"get-profile",
|
"get-team-users?file-id=*":
|
||||||
"logged-in-user/get-profile-logged-in.json",
|
"logged-in-user/get-team-users-single-user.json",
|
||||||
);
|
"get-comment-threads?file-id=*":
|
||||||
await BaseWebSocketPage.mockRPC(
|
"workspace/get-comment-threads-empty.json",
|
||||||
page,
|
"get-project?id=*": "workspace/get-project-default.json",
|
||||||
"get-team-users?file-id=*",
|
"get-team?id=*": "workspace/get-team-default.json",
|
||||||
"logged-in-user/get-team-users-single-user.json",
|
"get-teams": "get-teams.json",
|
||||||
);
|
"get-team-members?team-id=*":
|
||||||
await BaseWebSocketPage.mockRPC(
|
"logged-in-user/get-team-members-your-penpot.json",
|
||||||
page,
|
"get-profiles-for-file-comments?file-id=*":
|
||||||
"get-comment-threads?file-id=*",
|
"workspace/get-profile-for-file-comments.json",
|
||||||
"workspace/get-comment-threads-empty.json",
|
"update-profile-props": "workspace/update-profile-empty.json",
|
||||||
);
|
});
|
||||||
await BaseWebSocketPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"get-project?id=*",
|
|
||||||
"workspace/get-project-default.json",
|
|
||||||
);
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"get-team?id=*",
|
|
||||||
"workspace/get-team-default.json",
|
|
||||||
);
|
|
||||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
|
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"get-team-members?team-id=*",
|
|
||||||
"logged-in-user/get-team-members-your-penpot.json",
|
|
||||||
);
|
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"get-profiles-for-file-comments?file-id=*",
|
|
||||||
"workspace/get-profile-for-file-comments.json",
|
|
||||||
);
|
|
||||||
|
|
||||||
await BaseWebSocketPage.mockRPC(
|
|
||||||
page,
|
|
||||||
"update-profile-props",
|
|
||||||
"workspace/update-profile-empty.json",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||||
@@ -62,9 +172,20 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
||||||
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket mock
|
||||||
|
*
|
||||||
|
* @type {MockWebSocketHelper}
|
||||||
|
*/
|
||||||
#ws = null;
|
#ws = null;
|
||||||
|
|
||||||
constructor(page) {
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param {Page} page
|
||||||
|
* @param {} [options]
|
||||||
|
*/
|
||||||
|
constructor(page, options) {
|
||||||
super(page);
|
super(page);
|
||||||
this.pageName = page.getByTestId("page-name");
|
this.pageName = page.getByTestId("page-name");
|
||||||
|
|
||||||
@@ -77,10 +198,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
`[id="shape-00000000-0000-0000-0000-000000000000"]`,
|
`[id="shape-00000000-0000-0000-0000-000000000000"]`,
|
||||||
);
|
);
|
||||||
this.toolbarOptions = page.getByTestId("toolbar-options");
|
this.toolbarOptions = page.getByTestId("toolbar-options");
|
||||||
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
this.rectShapeButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Rectangle" });
|
||||||
this.ellipseShapeButton = page.getByRole("button", { name: "Ellipse (E)" });
|
this.ellipseShapeButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Ellipse" });
|
||||||
this.moveButton = page.getByRole("button", { name: "Move (V)" });
|
this.moveButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Move" });
|
||||||
this.boardButton = page.getByRole("button", { name: "Board (B)" });
|
this.boardButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Board" });
|
||||||
this.toggleToolbarButton = page.getByRole("button", {
|
this.toggleToolbarButton = page.getByRole("button", {
|
||||||
name: "Toggle toolbar",
|
name: "Toggle toolbar",
|
||||||
});
|
});
|
||||||
@@ -112,11 +233,14 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
"tokens-context-menu-for-set",
|
"tokens-context-menu-for-set",
|
||||||
);
|
);
|
||||||
this.contextMenuForShape = page.getByTestId("context-menu");
|
this.contextMenuForShape = page.getByTestId("context-menu");
|
||||||
|
if (options?.textEditor) {
|
||||||
|
this.textEditor = new WorkspacePage.TextEditor(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async goToWorkspace({
|
async goToWorkspace({
|
||||||
fileId = WorkspacePage.anyFileId,
|
fileId = this.fileId ?? WorkspacePage.anyFileId,
|
||||||
pageId = WorkspacePage.anyPageId,
|
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
await this.page.goto(
|
await this.page.goto(
|
||||||
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
||||||
@@ -141,48 +265,59 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setupEmptyFile() {
|
async setupEmptyFile() {
|
||||||
await this.mockRPC(
|
await this.mockRPCs({
|
||||||
"get-profile",
|
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||||
"logged-in-user/get-profile-logged-in.json",
|
"get-team-users?file-id=*":
|
||||||
);
|
"logged-in-user/get-team-users-single-user.json ",
|
||||||
await this.mockRPC(
|
"get-comment-threads?file-id=*":
|
||||||
"get-team-users?file-id=*",
|
"workspace/get-comment-threads-empty.json",
|
||||||
"logged-in-user/get-team-users-single-user.json",
|
"get-project?id=*": "workspace/get-project-default.json",
|
||||||
);
|
"get-team?id=*": "workspace/get-team-default.json",
|
||||||
await this.mockRPC(
|
"get-profiles-for-file-comments?file-id=*":
|
||||||
"get-comment-threads?file-id=*",
|
"workspace/get-profile-for-file-comments.json",
|
||||||
"workspace/get-comment-threads-empty.json",
|
"get-file-object-thumbnails?file-id=*":
|
||||||
);
|
"workspace/get-file-object-thumbnails-blank.json",
|
||||||
await this.mockRPC(
|
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
|
||||||
"get-project?id=*",
|
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
|
||||||
"workspace/get-project-default.json",
|
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
|
||||||
);
|
});
|
||||||
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
|
|
||||||
await this.mockRPC(
|
if (this.textEditor) {
|
||||||
"get-profiles-for-file-comments?file-id=*",
|
await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
"workspace/get-profile-for-file-comments.json",
|
}
|
||||||
);
|
|
||||||
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json");
|
// by default we mock the blank file.
|
||||||
await this.mockRPC(
|
await this.mockGetFile("workspace/get-file-blank.json");
|
||||||
"get-file-object-thumbnails?file-id=*",
|
|
||||||
"workspace/get-file-object-thumbnails-blank.json",
|
|
||||||
);
|
|
||||||
await this.mockRPC(
|
|
||||||
"get-font-variants?team-id=*",
|
|
||||||
"workspace/get-font-variants-empty.json",
|
|
||||||
);
|
|
||||||
await this.mockRPC(
|
|
||||||
"get-file-fragment?file-id=*",
|
|
||||||
"workspace/get-file-fragment-blank.json",
|
|
||||||
);
|
|
||||||
await this.mockRPC(
|
|
||||||
"get-file-libraries?file-id=*",
|
|
||||||
"workspace/get-file-libraries-empty.json",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async mockGetFile(jsonFile) {
|
async mockGetFile(jsonFilename, options) {
|
||||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
const page = this.page;
|
||||||
|
const jsonPath = `playwright/data/${jsonFilename}`;
|
||||||
|
const body = await readFile(jsonPath, "utf-8");
|
||||||
|
const payload = JSON.parse(body);
|
||||||
|
|
||||||
|
const fileId = Transit.get(payload, "id");
|
||||||
|
const pageId = Transit.get(payload, "data", "pages", 0);
|
||||||
|
const teamId = Transit.get(payload, "team-id");
|
||||||
|
|
||||||
|
this.fileId = fileId ?? this.anyFileId;
|
||||||
|
this.pageId = pageId ?? this.anyPageId;
|
||||||
|
this.teamId = teamId ?? this.anyTeamId;
|
||||||
|
|
||||||
|
const path = /get\-file\?/;
|
||||||
|
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
||||||
|
const interceptConfig = {
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/transit+json",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
return page.route(url, (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
...interceptConfig,
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// await this.mockRPC(/get\-file\?/, jsonFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async mockGetAsset(regex, asset) {
|
async mockGetAsset(regex, asset) {
|
||||||
@@ -190,22 +325,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setupFileWithComments() {
|
async setupFileWithComments() {
|
||||||
await this.mockRPC(
|
await this.mockRPCs({
|
||||||
"get-comment-threads?file-id=*",
|
"get-comment-threads?file-id=*":
|
||||||
"workspace/get-comment-threads-unread.json",
|
"workspace/get-comment-threads-unread.json",
|
||||||
);
|
"get-file-fragment?file-id=*&fragment-id=*":
|
||||||
await this.mockRPC(
|
"viewer/get-file-fragment-single-board.json",
|
||||||
"get-file-fragment?file-id=*&fragment-id=*",
|
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
|
||||||
"viewer/get-file-fragment-single-board.json",
|
"update-comment-thread-status":
|
||||||
);
|
"workspace/update-comment-thread-status.json",
|
||||||
await this.mockRPC(
|
});
|
||||||
"get-comments?thread-id=*",
|
|
||||||
"workspace/get-thread-comments.json",
|
|
||||||
);
|
|
||||||
await this.mockRPC(
|
|
||||||
"update-comment-thread-status",
|
|
||||||
"workspace/update-comment-thread-status.json",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickWithDragViewportAt(x, y, width, height) {
|
async clickWithDragViewportAt(x, y, width, height) {
|
||||||
@@ -223,6 +351,67 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
await this.page.mouse.up();
|
await this.page.mouse.up();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks and moves from the coordinates x1,y1 to x2,y2
|
||||||
|
*
|
||||||
|
* @param {number} x1
|
||||||
|
* @param {number} y1
|
||||||
|
* @param {number} x2
|
||||||
|
* @param {number} y2
|
||||||
|
*/
|
||||||
|
async clickAndMove(x1, y1, x2, y2) {
|
||||||
|
await this.page.waitForTimeout(100);
|
||||||
|
await this.viewport.hover({ position: { x: x1, y: y1 } });
|
||||||
|
await this.page.mouse.down();
|
||||||
|
await this.viewport.hover({ position: { x: x2, y: y2 } });
|
||||||
|
await this.page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Text Shape in the specified coordinates
|
||||||
|
* with an initial text.
|
||||||
|
*
|
||||||
|
* @param {number} x1
|
||||||
|
* @param {number} y1
|
||||||
|
* @param {number} x2
|
||||||
|
* @param {number} y2
|
||||||
|
* @param {string} initialText
|
||||||
|
* @param {*} [options]
|
||||||
|
*/
|
||||||
|
async createTextShape(x1, y1, x2, y2, initialText, options) {
|
||||||
|
const timeToWait = options?.timeToWait ?? 100;
|
||||||
|
await this.page.keyboard.press("T");
|
||||||
|
await this.page.waitForTimeout(timeToWait);
|
||||||
|
await this.clickAndMove(x1, y1, x2, y2);
|
||||||
|
await this.page.waitForTimeout(timeToWait);
|
||||||
|
if (initialText) {
|
||||||
|
await this.page.keyboard.type(initialText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the selected element into the clipboard.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async copy() {
|
||||||
|
return this.page.keyboard.press("Control+C");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pastes something from the clipboard.
|
||||||
|
*
|
||||||
|
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async paste(kind = "keyboard") {
|
||||||
|
if (kind === "context-menu") {
|
||||||
|
await this.viewport.click({ button: "right" });
|
||||||
|
return this.page.getByText("PasteCtrlV").click();
|
||||||
|
}
|
||||||
|
return this.page.keyboard.press("Control+V");
|
||||||
|
}
|
||||||
|
|
||||||
async panOnViewportAt(x, y, width, height) {
|
async panOnViewportAt(x, y, width, height) {
|
||||||
await this.page.waitForTimeout(100);
|
await this.page.waitForTimeout(100);
|
||||||
await this.viewport.hover({ position: { x, y } });
|
await this.viewport.hover({ position: { x, y } });
|
||||||
@@ -250,10 +439,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
await this.page.waitForTimeout(500);
|
await this.page.waitForTimeout(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doubleClickLeafLayer(name, clickOptions = {}) {
|
||||||
|
await this.clickLeafLayer(name, clickOptions);
|
||||||
|
await this.clickLeafLayer(name, clickOptions);
|
||||||
|
}
|
||||||
|
|
||||||
async clickToggableLayer(name, clickOptions = {}) {
|
async clickToggableLayer(name, clickOptions = {}) {
|
||||||
const layer = this.layers
|
const layer = this.layers
|
||||||
.getByTestId("layer-row")
|
.getByTestId("layer-row")
|
||||||
.filter({ hasText: name });
|
.filter({ hasText: name });
|
||||||
const button = layer.getByRole("button");
|
const button = layer.getByRole("button");
|
||||||
|
|
||||||
await button.waitFor();
|
await button.waitFor();
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
|
|||||||
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
||||||
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
||||||
});
|
});
|
||||||
await workspace.waitForFirstRender();
|
await workspace.waitForFirstRenderWithoutUI();
|
||||||
await expect(workspace.canvas).toHaveScreenshot();
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
26
frontend/playwright/ui/specs/dashboard-deleted.spec.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import DashboardPage from "../pages/DashboardPage";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await DashboardPage.init(page);
|
||||||
|
await DashboardPage.mockRPC(
|
||||||
|
page,
|
||||||
|
"get-profile",
|
||||||
|
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Dashboard Deleted Page", () => {
|
||||||
|
test("User can navigate to deleted page", async ({ page }) => {
|
||||||
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
|
// Setup mock for deleted files API
|
||||||
|
await dashboardPage.setupDeletedFiles();
|
||||||
|
|
||||||
|
// Navigate directly to deleted page
|
||||||
|
await dashboardPage.goToDeleted();
|
||||||
|
|
||||||
|
// Check for the delete-page-section element
|
||||||
|
await expect(page.getByTestId("deleted-page-section")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -189,8 +189,8 @@ test("BUG 7760 - Layout losing properties when changing parents", async ({
|
|||||||
await workspacePage.clickLeafLayer("Flex Board");
|
await workspacePage.clickLeafLayer("Flex Board");
|
||||||
|
|
||||||
// Move the first board into the second
|
// Move the first board into the second
|
||||||
const hAuto = await workspacePage.page.getByTitle("Fit content (Horizontal)");
|
const hAuto = await workspacePage.page.getByTestId("behaviour-h-auto");
|
||||||
const vAuto = await workspacePage.page.getByTitle("Fit content (Vertical)");
|
const vAuto = await workspacePage.page.getByTestId("behaviour-v-auto");
|
||||||
|
|
||||||
await expect(vAuto.locator("input")).toBeChecked();
|
await expect(vAuto.locator("input")).toBeChecked();
|
||||||
await expect(hAuto.locator("input")).toBeChecked();
|
await expect(hAuto.locator("input")).toBeChecked();
|
||||||
|
|||||||