mirror of
https://github.com/penpot/penpot.git
synced 2025-12-23 22:48:40 -05:00
Compare commits
200 Commits
2.12.0-RC4
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69c880d00e | ||
|
|
9eebc467ef | ||
|
|
b77712ce73 | ||
|
|
3d3e81f314 | ||
|
|
fe6441bb24 | ||
|
|
e15f0baf30 | ||
|
|
c040cbb784 | ||
|
|
7f674b78a9 | ||
|
|
099b78affd | ||
|
|
78cc3f0aa4 | ||
|
|
76f5f12808 | ||
|
|
047483a70a | ||
|
|
8cb2f27de8 | ||
|
|
0433336fc9 | ||
|
|
ce234fbeda | ||
|
|
fc4d31eed7 | ||
|
|
c670aac339 | ||
|
|
1d3fb5434f | ||
|
|
f478399ae0 | ||
|
|
6a1854f180 | ||
|
|
0858e297e5 | ||
|
|
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
|
|
||||||
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
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"
|
||||||
13
.github/workflows/tests.yml
vendored
13
.github/workflows/tests.yml
vendored
@@ -159,17 +159,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,6 +167,7 @@ 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: ubuntu-24.04
|
||||||
|
|||||||
23
CHANGES.md
23
CHANGES.md
@@ -1,5 +1,28 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
|
||||||
## 2.12.0 (Unreleased)
|
## 2.12.0 (Unreleased)
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -785,8 +712,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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
[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)))
|
||||||
@@ -110,13 +109,6 @@
|
|||||||
(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)))
|
(sm/humanize-explain (::sm/explain data) opts)))
|
||||||
|
|
||||||
|
|||||||
@@ -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}))
|
||||||
[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,10 @@
|
|||||||
;; 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]
|
||||||
|
:or {ignore-children-fn (constantly false)
|
||||||
|
ignore-mask false}}]
|
||||||
(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 +164,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]
|
||||||
|
|||||||
@@ -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}))))
|
||||||
|
|||||||
@@ -234,16 +234,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)))
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,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 +71,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
|
||||||
|
|||||||
@@ -1545,7 +1545,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 +1556,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 +1568,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 +1589,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 +1860,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
|
||||||
|
|||||||
@@ -69,6 +69,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 +85,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;
|
||||||
|
|||||||
@@ -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/"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,5 +1,10 @@
|
|||||||
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",
|
||||||
@@ -53,83 +53,74 @@
|
|||||||
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@penpot/draft-js": "portal:./packages/draft-js",
|
||||||
|
"@penpot/mousetrap": "portal:./packages/mousetrap",
|
||||||
|
"@penpot/plugins-runtime": "1.3.2",
|
||||||
|
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||||
|
"@penpot/text-editor": "portal:./text-editor",
|
||||||
"@playwright/test": "1.52.0",
|
"@playwright/test": "1.52.0",
|
||||||
"@storybook/addon-docs": "10.0.4",
|
"@storybook/addon-docs": "10.0.4",
|
||||||
"@storybook/addon-themes": "10.0.4",
|
"@storybook/addon-themes": "10.0.4",
|
||||||
"@storybook/addon-vitest": "10.0.4",
|
"@storybook/addon-vitest": "10.0.4",
|
||||||
"@storybook/react-vite": "10.0.4",
|
"@storybook/react-vite": "10.0.4",
|
||||||
|
"@tokens-studio/sd-transforms": "1.2.11",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@vitest/browser": "3.2.4",
|
"@vitest/browser": "3.2.4",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
|
"@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",
|
|
||||||
"gulp-postcss": "^10.0.0",
|
|
||||||
"gulp-rename": "^2.0.0",
|
|
||||||
"gulp-sourcemaps": "^3.0.0",
|
|
||||||
"gulp-svg-sprite": "^2.0.3",
|
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"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.0.4",
|
||||||
"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": "^6.3.5",
|
||||||
|
"vitest": "^3.2.0",
|
||||||
|
"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
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
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
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
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
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");
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
|||||||
Binary file not shown.
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
31
frontend/playwright/ui/specs/dashboard-deleted.spec.js
Normal file
31
frontend/playwright/ui/specs/dashboard-deleted.spec.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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 restore all and clear trash buttons
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "Restore All" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "Clear trash" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,10 @@ const setupFile = async (workspacePage) => {
|
|||||||
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
||||||
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
||||||
});
|
});
|
||||||
|
await workspacePage.mockRPC(
|
||||||
|
"update-file?id=*",
|
||||||
|
"workspace/update-file-empty.json",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const shapeToLayerName = {
|
const shapeToLayerName = {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
|||||||
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
||||||
});
|
});
|
||||||
|
|
||||||
await workspacePage.page.waitForTimeout(1000)
|
await workspacePage.page.waitForTimeout(1000);
|
||||||
await workspacePage.waitForFirstRender();
|
await workspacePage.waitForFirstRender();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -1,12 +1,323 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { Clipboard } from "../../helpers/Clipboard";
|
||||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
const timeToWait = 100;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, context }) => {
|
||||||
|
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||||
|
|
||||||
await WorkspacePage.init(page);
|
await WorkspacePage.init(page);
|
||||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
test.afterEach(async ({ context }) => {
|
||||||
|
context.clearPermissions();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Create a new text shape", async ({ page }) => {
|
||||||
|
const initialText = "Lorem ipsum";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.createTextShape(190, 150, 300, 200, initialText);
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe(initialText);
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||||
|
const textToPaste = "Lorem ipsum";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.clickAt(190, 150);
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
|
||||||
|
await page.waitForTimeout(timeToWait);
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe(textToPaste);
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Create a new text shape from pasting text using context menu", async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = "Lorem ipsum";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.clickAt(190, 150);
|
||||||
|
await workspace.paste("context-menu");
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe(textToPaste);
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update an already created text shape by appending text", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.moveFromEnd(0);
|
||||||
|
await page.keyboard.type(" dolor sit amet");
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update an already created text shape by prepending text", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.moveFromStart(0);
|
||||||
|
await page.keyboard.type("Dolor sit amet ");
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update an already created text shape by inserting text in between", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.moveFromStart(5);
|
||||||
|
await page.keyboard.type(" dolor sit amet");
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lorem dolor sit amet ipsum");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update a new text shape appending text by pasting text", async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = " dolor sit amet";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.moveFromEnd();
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update a new text shape prepending text by pasting text", async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = "Dolor sit amet ";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.moveFromStart();
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update a new text shape replacing (starting) text with pasted text", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = "Dolor sit amet";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromStart(5);
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Dolor sit amet ipsum");
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update a new text shape replacing (ending) text with pasted text", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = "dolor sit amet";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromEnd(5);
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lorem dolor sit amet");
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update a new text shape replacing (in between) text with pasted text", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const textToPaste = "dolor sit amet";
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromStart(5, 3);
|
||||||
|
|
||||||
|
await Clipboard.writeText(page, textToPaste);
|
||||||
|
|
||||||
|
await workspace.paste("keyboard");
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lordolor sit ametsum");
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update text font size selecting a part of it (starting)", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromStart(5);
|
||||||
|
await workspace.textEditor.changeFontSize(36);
|
||||||
|
|
||||||
|
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||||
|
expect(textContent1).toBe("Lorem");
|
||||||
|
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||||
|
expect(textContent2).toBe(" ipsum");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Update text line height selecting a part of it (starting)", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromStart(5);
|
||||||
|
await workspace.textEditor.changeLineHeight(1.4);
|
||||||
|
|
||||||
|
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
|
||||||
|
1,
|
||||||
|
"line-height",
|
||||||
|
);
|
||||||
|
expect(lineHeight).toBe("1.4");
|
||||||
|
|
||||||
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
expect(textContent).toBe("Lorem ipsum");
|
||||||
|
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page, {
|
||||||
|
textEditor: true,
|
||||||
|
});
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
|
await workspace.goToWorkspace();
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
await workspace.textEditor.startEditing();
|
||||||
|
await workspace.textEditor.selectFromStart(5);
|
||||||
|
await workspace.textEditor.changeLetterSpacing(10);
|
||||||
|
|
||||||
|
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||||
|
expect(textContent1).toBe("Lorem");
|
||||||
|
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||||
|
expect(textContent2).toBe(" ipsum");
|
||||||
|
await workspace.textEditor.stopEditing();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||||
const workspace = new WorkspacePage(page);
|
const workspace = new WorkspacePage(page);
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-11552.json");
|
await workspace.mockGetFile("text-editor/get-file-11552.json");
|
||||||
@@ -14,21 +325,16 @@ test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
|||||||
"update-file?id=*",
|
"update-file?id=*",
|
||||||
"text-editor/update-file-11552.json",
|
"text-editor/update-file-11552.json",
|
||||||
);
|
);
|
||||||
|
await workspace.goToWorkspace();
|
||||||
await workspace.goToWorkspace({
|
await workspace.doubleClickLeafLayer("Lorem ipsum");
|
||||||
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
|
|
||||||
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
|
|
||||||
});
|
|
||||||
await workspace.clickLeafLayer("Lorem ipsum");
|
|
||||||
await workspace.clickLeafLayer("Lorem ipsum");
|
|
||||||
|
|
||||||
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
||||||
name: "Font Size",
|
name: "Font Size",
|
||||||
});
|
});
|
||||||
await expect(fontSizeInput).toBeVisible();
|
await expect(fontSizeInput).toBeVisible();
|
||||||
|
|
||||||
await workspace.page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
await workspace.page.keyboard.press("ArrowRight");
|
await page.keyboard.press("ArrowRight");
|
||||||
|
|
||||||
await fontSizeInput.fill("36");
|
await fontSizeInput.fill("36");
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -746,20 +746,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
@include flexCenter;
|
|
||||||
height: $s-48;
|
|
||||||
width: $s-48;
|
|
||||||
border-radius: $br-circle;
|
|
||||||
background-color: var(--empty-message-background-color);
|
|
||||||
svg {
|
|
||||||
@extend .button-icon;
|
|
||||||
height: $s-28;
|
|
||||||
width: $s-28;
|
|
||||||
stroke: var(--empty-message-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.attr-title {
|
.attr-title {
|
||||||
div {
|
div {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|||||||
@@ -17,23 +17,26 @@
|
|||||||
<meta name="twitter:site" content="@penpotapp">
|
<meta name="twitter:site" content="@penpotapp">
|
||||||
<meta name="twitter:creator" content="@penpotapp">
|
<meta name="twitter:creator" content="@penpotapp">
|
||||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||||
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||||
{{#isDebug}}
|
{{#isDebug}}
|
||||||
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||||
{{/isDebug}}
|
{{/isDebug}}
|
||||||
|
|
||||||
<link rel="icon" href="images/favicon.png" />
|
<link rel="icon" href="images/favicon.png" />
|
||||||
|
|
||||||
|
<script type="importmap">{{& manifest.importmap }}</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
globalThis.penpotVersion = "{{& version}}";
|
||||||
|
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||||
|
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||||
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
<script src="{{& config}}"></script>
|
||||||
<script defer src="{{& config}}"></script>
|
<script src="{{& polyfills}}"></script>
|
||||||
<script defer src="{{& polyfills}}"></script>
|
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
|
|
||||||
<script>
|
|
||||||
window.penpotTranslations = JSON.parse({{& translations}});
|
|
||||||
window.penpotVersion = "%version%";
|
|
||||||
window.penpotBuildDate = "%buildDate%";
|
|
||||||
</script>
|
|
||||||
<!--cookie-consent-->
|
<!--cookie-consent-->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -44,9 +47,13 @@
|
|||||||
<section id="modal"></section>
|
<section id="modal"></section>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script defer src="js/libs.js?ts={{& ts}}"></script>
|
<script type="module" src="{{& libs}}"></script>
|
||||||
<script defer src="{{& shared}}"></script>
|
<script type="module">
|
||||||
<script defer src="{{& main}}"></script>
|
import { init } from "{{& app_main}}";
|
||||||
|
import defaultTranslations from "{{& default_translations}}";
|
||||||
|
|
||||||
|
init({defaultTranslations});
|
||||||
|
</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
@@ -9,7 +9,3 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
window.penpotTranslations = JSON.parse({{& translations}});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -6,22 +6,24 @@
|
|||||||
<link rel="icon" href="images/favicon.png" />
|
<link rel="icon" href="images/favicon.png" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.penpotVersion = "%version%";
|
globalThis.penpotVersion = "{{& version}}";
|
||||||
window.penpotBuildDate = "%buildDate%";
|
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||||
|
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
|
||||||
<script src="{{& config}}"></script>
|
<script src="{{& config}}"></script>
|
||||||
<script src="{{& polyfills}}"></script>
|
<script src="{{& polyfills}}"></script>
|
||||||
|
<script type="importmap">{{& importmap }}</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script src="js/libs.js?ts={{& ts}}"></script>
|
<script type="module" src="{{& libs}}"></script>
|
||||||
<script src="{{& shared}}"></script>
|
<script type="module">
|
||||||
<script src="{{& rasterizer}}"></script>
|
import { init } from "{{& rasterizer_main}}";
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,20 +7,24 @@
|
|||||||
<link rel="icon" href="images/favicon.png" />
|
<link rel="icon" href="images/favicon.png" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.penpotVersion = "%version%";
|
globalThis.penpotVersion = "{{& version}}";
|
||||||
|
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script src="{{& config}}"></script>
|
<script src="{{& config}}"></script>
|
||||||
<script src="{{& polyfills}}"></script>
|
<script src="{{& polyfills}}"></script>
|
||||||
|
<script type="importmap">{{& importmap }}</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script src="js/libs.js?ts={{& ts}}"></script>
|
<script type="module" src="{{& libs}}"></script>
|
||||||
<script src="{{& shared}}"></script>
|
<script type="module">
|
||||||
<script src="{{& render}}"></script>
|
import { init } from "{{& render_main}}";
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function startWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isDebug = process.env.NODE_ENV !== "production";
|
export const isDebug = process.env.NODE_ENV !== "production";
|
||||||
|
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||||
|
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||||
|
|
||||||
async function findFiles(basePath, predicate, options = {}) {
|
async function findFiles(basePath, predicate, options = {}) {
|
||||||
predicate =
|
predicate =
|
||||||
@@ -47,8 +49,7 @@ async function findFiles(basePath, predicate, options = {}) {
|
|||||||
function syncDirs(originPath, destPath) {
|
function syncDirs(originPath, destPath) {
|
||||||
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
|
||||||
proc.exec(command, (cause, stdout) => {
|
|
||||||
if (cause) {
|
if (cause) {
|
||||||
reject(cause);
|
reject(cause);
|
||||||
} else {
|
} else {
|
||||||
@@ -186,38 +187,36 @@ async function readManifestFile(resource) {
|
|||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readShadowManifest() {
|
async function generateManifest() {
|
||||||
const ts = Date.now();
|
const index = {
|
||||||
try {
|
app_main: "./js/main.js",
|
||||||
const content = await readManifestFile("js/manifest.json");
|
render_main: "./js/render.js",
|
||||||
|
rasterizer_main: "./js/rasterizer.js",
|
||||||
|
|
||||||
const index = {
|
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||||
ts: ts,
|
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||||
config: "js/config.js?ts=" + ts,
|
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||||
polyfills: "js/polyfills.js?ts=" + ts,
|
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||||
};
|
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
|
||||||
|
|
||||||
for (let item of content) {
|
importmap: JSON.stringify({
|
||||||
index[item.name] = "js/" + item["output-name"];
|
"imports": {
|
||||||
}
|
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||||
|
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
const content2 = await readManifestFile("js/worker/manifest.json");
|
return index;
|
||||||
for (let item of content2) {
|
|
||||||
index["worker_" + item.name] = "js/worker/" + item["output-name"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
} catch (cause) {
|
|
||||||
return {
|
|
||||||
ts: ts,
|
|
||||||
config: "js/config.js?ts=" + ts,
|
|
||||||
polyfills: "js/polyfills.js?ts=" + ts,
|
|
||||||
main: "js/main.js?ts=" + ts,
|
|
||||||
shared: "js/shared.js?ts=" + ts,
|
|
||||||
worker_main: "js/worker/main.js?ts=" + ts,
|
|
||||||
rasterizer: "js/rasterizer.js?ts=" + ts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderTemplate(path, context = {}, partials = {}) {
|
async function renderTemplate(path, context = {}, partials = {}) {
|
||||||
@@ -257,7 +256,7 @@ const markedOptions = {
|
|||||||
|
|
||||||
marked.use(markedOptions);
|
marked.use(markedOptions);
|
||||||
|
|
||||||
async function readTranslations() {
|
export async function compileTranslations() {
|
||||||
const langs = [
|
const langs = [
|
||||||
"ar",
|
"ar",
|
||||||
"ca",
|
"ca",
|
||||||
@@ -295,9 +294,10 @@ async function readTranslations() {
|
|||||||
["uk", "ukr_UA"],
|
["uk", "ukr_UA"],
|
||||||
"ha",
|
"ha",
|
||||||
];
|
];
|
||||||
const result = {};
|
|
||||||
|
|
||||||
for (let lang of langs) {
|
for (let lang of langs) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
let filename = `${lang}.po`;
|
let filename = `${lang}.po`;
|
||||||
if (l.isArray(lang)) {
|
if (l.isArray(lang)) {
|
||||||
filename = `${lang[1]}.po`;
|
filename = `${lang[1]}.po`;
|
||||||
@@ -316,11 +316,6 @@ async function readTranslations() {
|
|||||||
for (let key of Object.keys(trdata)) {
|
for (let key of Object.keys(trdata)) {
|
||||||
if (key === "") continue;
|
if (key === "") continue;
|
||||||
const comments = trdata[key].comments || {};
|
const comments = trdata[key].comments || {};
|
||||||
|
|
||||||
if (l.isNil(result[key])) {
|
|
||||||
result[key] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||||
|
|
||||||
const msgs = trdata[key].msgstr;
|
const msgs = trdata[key].msgstr;
|
||||||
@@ -330,9 +325,9 @@ async function readTranslations() {
|
|||||||
message = marked.parseInline(message);
|
message = marked.parseInline(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
result[key][lang] = message;
|
result[key] = message;
|
||||||
} else {
|
} else {
|
||||||
result[key][lang] = msgs.map((item) => {
|
result[key] = msgs.map((item) => {
|
||||||
if (isMarkdown) {
|
if (isMarkdown) {
|
||||||
return marked.parseInline(item);
|
return marked.parseInline(item);
|
||||||
} else {
|
} else {
|
||||||
@@ -341,22 +336,12 @@ async function readTranslations() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||||
|
const outputDir = "resources/public/js/";
|
||||||
|
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||||
|
await fs.writeFile(outputFile, esm);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterTranslations(translations, langs = [], keyFilter) {
|
|
||||||
const filteredEntries = Object.entries(translations)
|
|
||||||
.filter(([translationKey, _]) => keyFilter(translationKey))
|
|
||||||
.map(([translationKey, value]) => {
|
|
||||||
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
|
||||||
langs.includes(lang),
|
|
||||||
);
|
|
||||||
return [translationKey, Object.fromEntries(langEntries)];
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.fromEntries(filteredEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSvgSprite(files, prefix) {
|
async function generateSvgSprite(files, prefix) {
|
||||||
@@ -408,15 +393,7 @@ async function generateTemplates() {
|
|||||||
const isDebug = process.env.NODE_ENV !== "production";
|
const isDebug = process.env.NODE_ENV !== "production";
|
||||||
await fs.mkdir("./resources/public/", { recursive: true });
|
await fs.mkdir("./resources/public/", { recursive: true });
|
||||||
|
|
||||||
let translations = await readTranslations();
|
const manifest = await generateManifest();
|
||||||
const storybookTranslations = JSON.stringify(
|
|
||||||
filterTranslations(translations, ["en"], (key) =>
|
|
||||||
key.startsWith("labels."),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
translations = JSON.stringify(translations);
|
|
||||||
|
|
||||||
const manifest = await readShadowManifest();
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
const iconsSprite = await fs.readFile(
|
const iconsSprite = await fs.readFile(
|
||||||
@@ -437,13 +414,16 @@ async function generateTemplates() {
|
|||||||
"../public/images/sprites/assets.svg": assetsSprite,
|
"../public/images/sprites/assets.svg": assetsSprite,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
manifest: manifest,
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
build_date: BUILD_DATE,
|
||||||
|
isDebug,
|
||||||
|
};
|
||||||
|
|
||||||
content = await renderTemplate(
|
content = await renderTemplate(
|
||||||
"resources/templates/index.mustache",
|
"resources/templates/index.mustache",
|
||||||
{
|
context,
|
||||||
manifest: manifest,
|
|
||||||
translations: JSON.stringify(translations),
|
|
||||||
isDebug,
|
|
||||||
},
|
|
||||||
partials,
|
partials,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -451,41 +431,30 @@ async function generateTemplates() {
|
|||||||
|
|
||||||
content = await renderTemplate(
|
content = await renderTemplate(
|
||||||
"resources/templates/challenge.mustache",
|
"resources/templates/challenge.mustache",
|
||||||
{},
|
context,
|
||||||
partials,
|
partials,
|
||||||
);
|
);
|
||||||
await fs.writeFile("./resources/public/challenge.html", content);
|
await fs.writeFile("./resources/public/challenge.html", content);
|
||||||
|
|
||||||
content = await renderTemplate(
|
content = await renderTemplate(
|
||||||
"resources/templates/preview-body.mustache",
|
"resources/templates/preview-body.mustache",
|
||||||
{
|
context,
|
||||||
manifest: manifest,
|
|
||||||
},
|
|
||||||
partials,
|
partials,
|
||||||
);
|
);
|
||||||
await fs.writeFile("./.storybook/preview-body.html", content);
|
await fs.writeFile("./.storybook/preview-body.html", content);
|
||||||
|
|
||||||
content = await renderTemplate(
|
content = await renderTemplate(
|
||||||
"resources/templates/preview-head.mustache",
|
"resources/templates/preview-head.mustache",
|
||||||
{
|
context,
|
||||||
manifest: manifest,
|
|
||||||
translations: JSON.stringify(storybookTranslations),
|
|
||||||
},
|
|
||||||
partials,
|
partials,
|
||||||
);
|
);
|
||||||
await fs.writeFile("./.storybook/preview-head.html", content);
|
await fs.writeFile("./.storybook/preview-head.html", content);
|
||||||
|
|
||||||
content = await renderTemplate("resources/templates/render.mustache", {
|
content = await renderTemplate("resources/templates/render.mustache", context);
|
||||||
manifest: manifest,
|
|
||||||
translations: JSON.stringify(translations),
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.writeFile("./resources/public/render.html", content);
|
await fs.writeFile("./resources/public/render.html", content);
|
||||||
|
|
||||||
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
|
||||||
manifest: manifest,
|
|
||||||
translations: JSON.stringify(translations),
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.writeFile("./resources/public/rasterizer.html", content);
|
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,26 +27,20 @@ rm -rf target/dist;
|
|||||||
rm -rf resources/public;
|
rm -rf resources/public;
|
||||||
|
|
||||||
mkdir -p resources/public;
|
mkdir -p resources/public;
|
||||||
|
mkdir -p target/dist;
|
||||||
|
|
||||||
pushd ../render-wasm;
|
pushd ../render-wasm;
|
||||||
./build
|
./build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS;
|
yarn run build:app:main $EXTRA_PARAMS;
|
||||||
yarn run build:app:libs || exit 1;
|
yarn run build:app:libs;
|
||||||
yarn run build:app:assets || exit 1;
|
yarn run build:app:assets;
|
||||||
|
|
||||||
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
||||||
|
|
||||||
mkdir -p target/dist;
|
|
||||||
rsync -avr resources/public/ target/dist/
|
rsync -avr resources/public/ target/dist/
|
||||||
|
|
||||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
|
|
||||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/render.html;
|
|
||||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/rasterizer.html;
|
|
||||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
|
|
||||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/rasterizer.html;
|
|
||||||
|
|
||||||
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
|
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
|
||||||
# build storybook
|
# build storybook
|
||||||
yarn run build:storybook || exit 1;
|
yarn run build:storybook || exit 1;
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ await h.compileStyles();
|
|||||||
await h.copyAssets();
|
await h.copyAssets();
|
||||||
await h.copyWasmPlayground();
|
await h.copyWasmPlayground();
|
||||||
await h.compileSvgSprites();
|
await h.compileSvgSprites();
|
||||||
|
await h.compileTranslations();
|
||||||
await h.compileTemplates();
|
await h.compileTemplates();
|
||||||
await h.compilePolyfills();
|
await h.compilePolyfills();
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ const rebuildNotify = {
|
|||||||
const config = {
|
const config = {
|
||||||
entryPoints: ["target/index.js"],
|
entryPoints: ["target/index.js"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "iife",
|
format: "esm",
|
||||||
banner: {
|
banner: {
|
||||||
js: '"use strict"; var global = globalThis;',
|
js: '"use strict";\nvar global = globalThis;',
|
||||||
},
|
},
|
||||||
outfile: "resources/public/js/libs.js",
|
outfile: "resources/public/js/libs.js",
|
||||||
plugins: [fixReactVirtualized, rebuildNotify],
|
plugins: [fixReactVirtualized, rebuildNotify],
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
import * as h from "./_helpers.js";
|
import * as h from "./_helpers.js";
|
||||||
|
|
||||||
|
await fs.mkdir("resources/public/js", {recursive: true});
|
||||||
|
|
||||||
await h.compileStorybookStyles();
|
await h.compileStorybookStyles();
|
||||||
await h.copyAssets();
|
await h.copyAssets();
|
||||||
await h.compileSvgSprites();
|
await h.compileSvgSprites();
|
||||||
|
await h.compileTranslations();
|
||||||
await h.compileTemplates();
|
await h.compileTemplates();
|
||||||
await h.compilePolyfills();
|
await h.compilePolyfills();
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
|
export JAVA_OPTS="\
|
||||||
|
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||||
|
-Djdk.attach.allowAttachSelf \
|
||||||
|
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||||
|
-Djdk.tracePinnedThreads=full \
|
||||||
|
-XX:+EnableDynamicAgentLoading \
|
||||||
|
-XX:-OmitStackTraceInFastThrow \
|
||||||
|
-XX:+UnlockDiagnosticVMOptions \
|
||||||
|
-XX:+DebugNonSafepoints \
|
||||||
|
--sun-misc-unsafe-memory-access=allow \
|
||||||
|
--enable-preview \
|
||||||
|
--enable-native-access=ALL-UNNAMED";
|
||||||
|
|
||||||
|
export OPTIONS="-A:dev"
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
exec clojure $OPTIONS -M -m rebel-readline.main
|
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
|
|||||||
await compileSassAll();
|
await compileSassAll();
|
||||||
await h.copyAssets();
|
await h.copyAssets();
|
||||||
await h.copyWasmPlayground();
|
await h.copyWasmPlayground();
|
||||||
|
await h.compileTranslations();
|
||||||
await h.compileSvgSprites();
|
await h.compileSvgSprites();
|
||||||
await h.compileTemplates();
|
await h.compileTemplates();
|
||||||
await h.compilePolyfills();
|
await h.compilePolyfills();
|
||||||
@@ -81,7 +82,7 @@ h.watch("resources/templates", null, async function (path) {
|
|||||||
log.info("watch: translations (~)");
|
log.info("watch: translations (~)");
|
||||||
h.watch("translations", null, async function (path) {
|
h.watch("translations", null, async function (path) {
|
||||||
log.info("changed:", path);
|
log.info("changed:", path);
|
||||||
await h.compileTemplates();
|
await h.compileTranslations();
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("watch: assets (~)");
|
log.info("watch: assets (~)");
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
|
|
||||||
:builds
|
:builds
|
||||||
{:main
|
{:main
|
||||||
{:target :browser
|
{:target :esm
|
||||||
:output-dir "resources/public/js/"
|
:output-dir "resources/public/js/"
|
||||||
:asset-path "/js"
|
:asset-path "/js"
|
||||||
:devtools {:watch-dir "resources/public"
|
:devtools {:watch-dir "resources/public"
|
||||||
:reload-strategy :full}
|
:reload-strategy :full}
|
||||||
:build-options {:manifest-name "manifest.json"}
|
:build-options {:manifest-name "manifest.json"}
|
||||||
:module-loader true
|
|
||||||
:modules
|
:modules
|
||||||
{:shared
|
{:shared
|
||||||
{:entries []}
|
{:entries []}
|
||||||
@@ -20,42 +19,42 @@
|
|||||||
:main
|
:main
|
||||||
{:entries [app.main app.plugins.api]
|
{:entries [app.main app.plugins.api]
|
||||||
:depends-on #{:shared}
|
:depends-on #{:shared}
|
||||||
:init-fn app.main/init}
|
:exports {init app.main/init}}
|
||||||
|
|
||||||
:util-highlight
|
:util-highlight
|
||||||
{:entries [app.util.code-highlight]
|
{:entries [app.util.code-highlight]
|
||||||
:depends-on #{:main}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
:main-auth
|
:main-auth
|
||||||
{:entries [app.main.ui.auth
|
{:entries [app.main.ui.auth
|
||||||
app.main.ui.auth.verify-token]
|
app.main.ui.auth.verify-token]
|
||||||
:depends-on #{:main}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
:main-viewer
|
:main-viewer
|
||||||
{:entries [app.main.ui.viewer]
|
{:entries [app.main.ui.viewer]
|
||||||
:depends-on #{:main :main-auth}}
|
:depends-on #{:shared :main-auth}}
|
||||||
|
|
||||||
:main-workspace
|
:main-workspace
|
||||||
{:entries [app.main.ui.workspace]
|
{:entries [app.main.ui.workspace]
|
||||||
:depends-on #{:main}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
:main-dashboard
|
:main-dashboard
|
||||||
{:entries [app.main.ui.dashboard]
|
{:entries [app.main.ui.dashboard]
|
||||||
:depends-on #{:main}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
:main-settings
|
:main-settings
|
||||||
{:entries [app.main.ui.settings]
|
{:entries [app.main.ui.settings]
|
||||||
:depends-on #{:main}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
:render
|
:render
|
||||||
{:entries [app.render]
|
{:entries [app.render]
|
||||||
:depends-on #{:shared}
|
:depends-on #{:shared}
|
||||||
:init-fn app.render/init}
|
:exports {init app.render/init}}
|
||||||
|
|
||||||
:rasterizer
|
:rasterizer
|
||||||
{:entries [app.rasterizer]
|
{:entries [app.rasterizer]
|
||||||
:depends-on #{:shared}
|
:depends-on #{:shared}
|
||||||
:init-fn app.rasterizer/init}}
|
:exports {init app.rasterizer/init}}}
|
||||||
|
|
||||||
:js-options
|
:js-options
|
||||||
{:entry-keys ["module" "browser" "main"]
|
{:entry-keys ["module" "browser" "main"]
|
||||||
@@ -75,11 +74,10 @@
|
|||||||
:compiler-options
|
:compiler-options
|
||||||
{:fn-invoke-direct true
|
{:fn-invoke-direct true
|
||||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
||||||
:output-wrapper true
|
|
||||||
:rename-prefix-namespace "PENPOT"
|
|
||||||
:source-map true
|
:source-map true
|
||||||
:elide-asserts true
|
:elide-asserts true
|
||||||
:anon-fn-naming-policy :off
|
:anon-fn-naming-policy :off
|
||||||
|
:cross-chunk-method-motion false
|
||||||
:source-map-detail-level :all}}}
|
:source-map-detail-level :all}}}
|
||||||
|
|
||||||
:worker
|
:worker
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
(def default-theme "default")
|
(def default-theme "default")
|
||||||
(def default-language "en")
|
(def default-language "en")
|
||||||
|
|
||||||
(def translations (obj/get global "penpotTranslations"))
|
|
||||||
(def themes (obj/get global "penpotThemes"))
|
(def themes (obj/get global "penpotThemes"))
|
||||||
|
|
||||||
(def build-date (parse-build-date global))
|
(def build-date (parse-build-date global))
|
||||||
|
|||||||
@@ -90,9 +90,12 @@
|
|||||||
(rx/map #(ws/initialize)))))))
|
(rx/map #(ws/initialize)))))))
|
||||||
|
|
||||||
(defn ^:export init
|
(defn ^:export init
|
||||||
[]
|
[options]
|
||||||
|
(some-> (unchecked-get options "defaultTranslations")
|
||||||
|
(i18n/set-default-translations))
|
||||||
|
|
||||||
(mw/init!)
|
(mw/init!)
|
||||||
(i18n/init! cf/translations)
|
(i18n/init)
|
||||||
(cur/init-styles)
|
(cur/init-styles)
|
||||||
(thr/init!)
|
(thr/init!)
|
||||||
(init-ui)
|
(init-ui)
|
||||||
@@ -114,11 +117,4 @@
|
|||||||
[]
|
[]
|
||||||
(reinit))
|
(reinit))
|
||||||
|
|
||||||
;; Reload the UI when the language changes
|
|
||||||
(add-watch
|
|
||||||
i18n/locale "locale"
|
|
||||||
(fn [_ _ old-value current-value]
|
|
||||||
(when (not= old-value current-value)
|
|
||||||
(reinit))))
|
|
||||||
|
|
||||||
(set! (.-stackTraceLimit js/Error) 50)
|
(set! (.-stackTraceLimit js/Error) 50)
|
||||||
|
|||||||
@@ -148,17 +148,17 @@
|
|||||||
:width 768
|
:width 768
|
||||||
:height 1024}
|
:height 1024}
|
||||||
{:name "Google Pixel 7 Pro"
|
{:name "Google Pixel 7 Pro"
|
||||||
:width 1440
|
:width 412
|
||||||
:height 3120}
|
:height 892}
|
||||||
{:name "Google Pixel 6a/6"
|
{:name "Google Pixel 6a/6"
|
||||||
:width 1080
|
:width 412
|
||||||
:height 2400}
|
:height 915}
|
||||||
{:name "Google Pixel 4a/5"
|
{:name "Google Pixel 4a/5"
|
||||||
:width 393
|
:width 393
|
||||||
:height 851}
|
:height 851}
|
||||||
{:name "Samsung Galaxy S22"
|
{:name "Samsung Galaxy S22"
|
||||||
:width 1080
|
:width 360
|
||||||
:height 2340}
|
:height 780}
|
||||||
{:name "Samsung Galaxy S20+"
|
{:name "Samsung Galaxy S20+"
|
||||||
:width 384
|
:width 384
|
||||||
:height 854}
|
:height 854}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
(map :page-id))
|
(map :page-id))
|
||||||
|
|
||||||
(defn- apply-changes-localy
|
(defn- apply-changes-localy
|
||||||
[{:keys [file-id redo-changes] :as commit} pending]
|
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||||
(ptk/reify ::apply-changes-localy
|
(ptk/reify ::apply-changes-localy
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
pids (into #{} xf:map-page-id redo-changes)]
|
pids (into #{} xf:map-page-id redo-changes)]
|
||||||
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
||||||
|
|
||||||
(if (features/active-feature? state "render-wasm/v1")
|
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
||||||
;; Update the wasm model
|
;; Update the wasm model
|
||||||
(let [shape-changes (volatile! {})
|
(let [shape-changes (volatile! {})
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
(defn commit
|
(defn commit
|
||||||
"Create a commit event instance"
|
"Create a commit event instance"
|
||||||
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
||||||
file-id file-revn file-vern undo-group tags stack-undo? source]}]
|
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
|
||||||
|
|
||||||
(assert (cpc/check-changes redo-changes)
|
(assert (cpc/check-changes redo-changes)
|
||||||
"expect valid vector of changes for redo-changes")
|
"expect valid vector of changes for redo-changes")
|
||||||
@@ -147,7 +147,8 @@
|
|||||||
:save-undo? save-undo?
|
:save-undo? save-undo?
|
||||||
:undo-group undo-group
|
:undo-group undo-group
|
||||||
:tags tags
|
:tags tags
|
||||||
:stack-undo? stack-undo?}]
|
:stack-undo? stack-undo?
|
||||||
|
:ignore-wasm? ignore-wasm?}]
|
||||||
|
|
||||||
(ptk/reify ::commit
|
(ptk/reify ::commit
|
||||||
cljs.core/IDeref
|
cljs.core/IDeref
|
||||||
|
|||||||
@@ -386,3 +386,21 @@
|
|||||||
(rx/of ::dps/force-persist
|
(rx/of ::dps/force-persist
|
||||||
(rt/nav :viewer params options))))))
|
(rt/nav :viewer params options))))))
|
||||||
|
|
||||||
|
(defn go-to-dashboard-deleted
|
||||||
|
[& {:keys [team-id] :as options}]
|
||||||
|
(ptk/reify ::go-to-dashboard-deleted
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [profile (get state :profile)
|
||||||
|
team-id (cond
|
||||||
|
(= :default team-id)
|
||||||
|
(:default-team-id profile)
|
||||||
|
|
||||||
|
(uuid? team-id)
|
||||||
|
team-id
|
||||||
|
|
||||||
|
:else
|
||||||
|
(:current-team-id state))
|
||||||
|
params {:team-id team-id}]
|
||||||
|
(rx/of (modal/hide)
|
||||||
|
(rt/nav :dashboard-deleted params options))))))
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.websocket :as dws]
|
[app.main.data.websocket :as dws]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
|
[app.main.store :as st]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.sse :as sse]
|
[app.util.sse :as sse]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
@@ -76,7 +77,8 @@
|
|||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(reduce (fn [state {:keys [id] :as project}]
|
(reduce (fn [state {:keys [id] :as project}]
|
||||||
(update-in state [:projects id] merge project))
|
;; Replace completely instead of merge to ensure deleted-at is removed
|
||||||
|
(assoc-in state [:projects id] project))
|
||||||
state
|
state
|
||||||
projects))))
|
projects))))
|
||||||
|
|
||||||
@@ -152,6 +154,34 @@
|
|||||||
(->> (rp/cmd! :get-builtin-templates)
|
(->> (rp/cmd! :get-builtin-templates)
|
||||||
(rx/map builtin-templates-fetched)))))
|
(rx/map builtin-templates-fetched)))))
|
||||||
|
|
||||||
|
;; --- EVENT: deleted-files
|
||||||
|
|
||||||
|
(defn- deleted-files-fetched
|
||||||
|
[files]
|
||||||
|
(ptk/reify ::deleted-files-fetched
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [now (ct/now)
|
||||||
|
filtered-files (filterv (fn [file]
|
||||||
|
(let [will-be-deleted-at (:will-be-deleted-at file)]
|
||||||
|
(or (nil? will-be-deleted-at)
|
||||||
|
(ct/is-after? will-be-deleted-at now))))
|
||||||
|
files)
|
||||||
|
files (d/index-by :id filtered-files)]
|
||||||
|
(-> state
|
||||||
|
(assoc :deleted-files files)
|
||||||
|
(update :files d/merge files))))))
|
||||||
|
|
||||||
|
(defn fetch-deleted-files
|
||||||
|
([] (fetch-deleted-files nil))
|
||||||
|
([team-id]
|
||||||
|
(ptk/reify ::fetch-deleted-files
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(when-let [team-id (or team-id (:current-team-id state))]
|
||||||
|
(->> (rp/cmd! :get-team-deleted-files {:team-id team-id})
|
||||||
|
(rx/map deleted-files-fetched)))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Data Selection
|
;; Data Selection
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -460,6 +490,7 @@
|
|||||||
(-> state
|
(-> state
|
||||||
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
|
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
|
||||||
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
|
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
|
||||||
|
(d/update-in-when [:deleted-files file-id] assoc :thumbnail-id thumbnail-id)
|
||||||
(d/update-when :dashboard-search-result update-search-files))))))
|
(d/update-when :dashboard-search-result update-search-files))))))
|
||||||
|
|
||||||
;; --- EVENT: create-file
|
;; --- EVENT: create-file
|
||||||
@@ -656,3 +687,156 @@
|
|||||||
:team-role-change (handle-change-team-role msg)
|
:team-role-change (handle-change-team-role msg)
|
||||||
:team-membership-change (dcm/team-membership-change msg)
|
:team-membership-change (dcm/team-membership-change msg)
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Delete files immediately
|
||||||
|
|
||||||
|
(defn delete-files-immediately
|
||||||
|
[{:keys [team-id ids] :as params}]
|
||||||
|
(assert (uuid? team-id))
|
||||||
|
(assert (set? ids))
|
||||||
|
(assert (every? uuid? ids))
|
||||||
|
|
||||||
|
(ptk/reify ::delete-files-immediately
|
||||||
|
ev/Event
|
||||||
|
(-data [_]
|
||||||
|
{:team-id team-id
|
||||||
|
:num-files (count ids)})
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(let [{:keys [on-success on-error]
|
||||||
|
:or {on-success identity
|
||||||
|
on-error rx/throw}} (meta params)]
|
||||||
|
(->> (rp/cmd! :permanently-delete-team-files {:team-id team-id :ids ids})
|
||||||
|
(rx/tap on-success)
|
||||||
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
;; --- Restore deleted files immediately
|
||||||
|
|
||||||
|
(defn- initialize-restore-status
|
||||||
|
[files]
|
||||||
|
(ptk/reify ::init-restore-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [restore-state {:in-progress true
|
||||||
|
:healthy? true
|
||||||
|
:error false
|
||||||
|
:progress 0
|
||||||
|
:widget-visible true
|
||||||
|
:detail-visible true
|
||||||
|
:files files
|
||||||
|
:last-update (ct/now)
|
||||||
|
:cmd :restore-files}]
|
||||||
|
(assoc state :restore restore-state)))))
|
||||||
|
|
||||||
|
(defn- update-restore-status
|
||||||
|
[{:keys [index total] :as data}]
|
||||||
|
(ptk/reify ::upd-restore-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [time-diff (ct/diff-ms (get-in state [:restore :last-update]) (ct/now))
|
||||||
|
healthy? (< time-diff 6000)]
|
||||||
|
(update state :restore assoc
|
||||||
|
:progress index
|
||||||
|
:total total
|
||||||
|
:last-update (ct/now)
|
||||||
|
:healthy? healthy?)))))
|
||||||
|
|
||||||
|
(defn- complete-restore-status
|
||||||
|
[]
|
||||||
|
(ptk/reify ::comp-restore-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [total (get-in state [:restore :total])]
|
||||||
|
(update state :restore assoc
|
||||||
|
:in-progress false
|
||||||
|
:progress total ; Ensure progress equals total on completion
|
||||||
|
:last-update (ct/now))))))
|
||||||
|
|
||||||
|
(defn- error-restore-status
|
||||||
|
[error]
|
||||||
|
(ptk/reify ::err-restore-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :restore assoc
|
||||||
|
:in-progress false
|
||||||
|
:error error
|
||||||
|
:last-update (ct/now)
|
||||||
|
:healthy? false))))
|
||||||
|
|
||||||
|
(defn toggle-restore-detail-visibility
|
||||||
|
[]
|
||||||
|
(ptk/reify ::toggle-restore-detail
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update-in state [:restore :detail-visible] not))))
|
||||||
|
|
||||||
|
(defn retry-last-restore
|
||||||
|
[]
|
||||||
|
(ptk/reify ::retry-restore
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
;; Reset restore state for retry - actual retry will be handled by UI
|
||||||
|
(if (get state :restore)
|
||||||
|
(update state :restore assoc :error false :in-progress false)
|
||||||
|
state))))
|
||||||
|
|
||||||
|
(defn clear-restore-state
|
||||||
|
[]
|
||||||
|
(ptk/reify ::clear-restore
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(dissoc state :restore))))
|
||||||
|
|
||||||
|
(defn- projects-restored
|
||||||
|
[team-id]
|
||||||
|
(ptk/reify ::projects-restored
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
;; Refetch projects to get the updated state without deleted-at
|
||||||
|
(rx/of (fetch-projects team-id)))))
|
||||||
|
|
||||||
|
(defn restore-files-immediately
|
||||||
|
[{:keys [team-id ids] :as params}]
|
||||||
|
(dm/assert! (uuid? team-id))
|
||||||
|
(dm/assert! (set? ids))
|
||||||
|
(dm/assert! (every? uuid? ids))
|
||||||
|
|
||||||
|
(ptk/reify ::restore-files-immediately
|
||||||
|
ev/Event
|
||||||
|
(-data [_]
|
||||||
|
{:team-id team-id
|
||||||
|
:num-files (count ids)})
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(let [{:keys [on-success on-error]
|
||||||
|
:or {on-success identity
|
||||||
|
on-error rx/throw}} (meta params)
|
||||||
|
files (mapv #(hash-map :id %) ids)]
|
||||||
|
|
||||||
|
(rx/merge
|
||||||
|
(rx/of (initialize-restore-status files))
|
||||||
|
|
||||||
|
(->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
|
||||||
|
(rx/tap (fn [event]
|
||||||
|
(let [payload (sse/get-payload event)
|
||||||
|
type (sse/get-type event)]
|
||||||
|
(when (and payload (= type "progress"))
|
||||||
|
(let [{:keys [index total]} payload]
|
||||||
|
(when (and index total)
|
||||||
|
;; Dispatch progress update
|
||||||
|
(st/emit! (update-restore-status {:index index :total total}))))))))
|
||||||
|
(rx/filter sse/end-of-stream?)
|
||||||
|
(rx/map sse/get-payload)
|
||||||
|
(rx/tap on-success)
|
||||||
|
(rx/mapcat (fn [_]
|
||||||
|
(rx/of (complete-restore-status)
|
||||||
|
(projects-restored team-id))))
|
||||||
|
(rx/catch (fn [error]
|
||||||
|
(rx/concat
|
||||||
|
(rx/of (error-restore-status (ex-message error)))
|
||||||
|
(on-error error)))))
|
||||||
|
|
||||||
|
(rx/of (ptk/data-event ::restore-start {:total (count ids)})))))))
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
(let [uagent (new ua/UAParser)]
|
(let [uagent (new ua/UAParser)]
|
||||||
(merge
|
(merge
|
||||||
{:version (:full cf/version)
|
{:version (:full cf/version)
|
||||||
:locale @i18n/locale}
|
:locale i18n/*current-locale*}
|
||||||
(let [browser (.getBrowser uagent)]
|
(let [browser (.getBrowser uagent)]
|
||||||
{:browser (obj/get browser "name")
|
{:browser (obj/get browser "name")
|
||||||
:browser-version (obj/get browser "version")})
|
:browser-version (obj/get browser "version")})
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
(def context
|
(def context
|
||||||
(atom (d/without-nils (collect-context))))
|
(atom (d/without-nils (collect-context))))
|
||||||
|
|
||||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
(add-watch i18n/locale "events" #(swap! context assoc :locale %4))
|
||||||
|
|
||||||
;; --- EVENT TRANSLATION
|
;; --- EVENT TRANSLATION
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
(def revn-data (atom {}))
|
(def revn-data (atom {}))
|
||||||
(def queue-conj (fnil conj #queue []))
|
(def queue-conj (fnil conj #queue []))
|
||||||
|
|
||||||
|
(def force-persist? #(= % ::force-persist))
|
||||||
|
|
||||||
(defn- update-status
|
(defn- update-status
|
||||||
[status]
|
[status]
|
||||||
(ptk/reify ::update-status
|
(ptk/reify ::update-status
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.types.profile :refer [schema:profile]]
|
[app.common.types.profile :refer [schema:profile]]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
@@ -54,11 +53,16 @@
|
|||||||
(assoc :profile-id id)
|
(assoc :profile-id id)
|
||||||
(assoc :profile profile)))
|
(assoc :profile profile)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [profile (:profile state)]
|
||||||
|
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||||
|
(rx/ignore))))
|
||||||
|
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ state _]
|
(effect [_ state _]
|
||||||
(let [profile (:profile state)]
|
(let [profile (:profile state)]
|
||||||
(swap! storage/user assoc :profile profile)
|
(swap! storage/user assoc :profile profile)
|
||||||
(i18n/set-locale! (:lang profile))
|
|
||||||
(plugins.register/init)))))
|
(plugins.register/init)))))
|
||||||
|
|
||||||
(def profile-fetched?
|
(def profile-fetched?
|
||||||
@@ -484,7 +488,7 @@
|
|||||||
|
|
||||||
(defn delete-access-token
|
(defn delete-access-token
|
||||||
[{:keys [id] :as params}]
|
[{:keys [id] :as params}]
|
||||||
(us/assert! ::us/uuid id)
|
(assert (uuid? id))
|
||||||
(ptk/reify ::delete-access-token
|
(ptk/reify ::delete-access-token
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
|
|||||||
@@ -59,9 +59,15 @@
|
|||||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||||
[value]
|
[value]
|
||||||
(if-let [tc (tinycolor/valid-color value)]
|
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||||
{:value value :unit (tinycolor/color-format tc)}
|
(if-let [tc (tinycolor/valid-color value)]
|
||||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
{:value value :unit (tinycolor/color-format tc)}
|
||||||
|
(cond
|
||||||
|
missing-references
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||||
|
:references missing-references}
|
||||||
|
:else
|
||||||
|
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||||
|
|
||||||
(defn- numeric-string? [s]
|
(defn- numeric-string? [s]
|
||||||
(and (string? s)
|
(and (string? s)
|
||||||
@@ -120,7 +126,7 @@
|
|||||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||||
[value]
|
[value]
|
||||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||||
parsed-value (cft/parse-token-value value)
|
parsed-value (cft/parse-token-value value)
|
||||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||||
references (seq (cto/find-token-value-references value))]
|
references (seq (cto/find-token-value-references value))]
|
||||||
@@ -373,8 +379,8 @@
|
|||||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||||
(update shadow-result :errors concat
|
(update shadow-result :errors concat
|
||||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||||
parsers {:offsetX parse-sd-token-general-value
|
parsers {:offset-x parse-sd-token-general-value
|
||||||
:offsetY parse-sd-token-general-value
|
:offset-y parse-sd-token-general-value
|
||||||
:blur parse-sd-token-shadow-blur
|
:blur parse-sd-token-shadow-blur
|
||||||
:spread parse-sd-token-shadow-spread
|
:spread parse-sd-token-shadow-spread
|
||||||
:color parse-sd-token-color-value
|
:color parse-sd-token-color-value
|
||||||
@@ -394,35 +400,42 @@
|
|||||||
(defn- parse-sd-token-shadow-value
|
(defn- parse-sd-token-shadow-value
|
||||||
"Parses shadow value and validates it."
|
"Parses shadow value and validates it."
|
||||||
[value]
|
[value]
|
||||||
(cond
|
(let [missing-references
|
||||||
;; Reference value (string)
|
(when (string? value)
|
||||||
(string? value) {:value value}
|
(seq (cto/find-token-value-references value)))]
|
||||||
|
(cond
|
||||||
|
missing-references
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||||
|
:references missing-references}
|
||||||
|
|
||||||
|
(string? value)
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
|
||||||
|
|
||||||
;; Empty value
|
;; Empty value
|
||||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||||
|
|
||||||
;; Invalid value
|
;; Invalid value
|
||||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||||
|
|
||||||
;; Array of shadows
|
;; Array of shadows
|
||||||
:else
|
:else
|
||||||
(let [converted (js->clj value :keywordize-keys true)
|
(let [converted (js->clj value :keywordize-keys true)
|
||||||
;; Parse each shadow with its index
|
;; Parse each shadow with its index
|
||||||
parsed-shadows (map-indexed
|
parsed-shadows (map-indexed
|
||||||
(fn [idx shadow-map]
|
(fn [idx shadow-map]
|
||||||
(parse-single-shadow shadow-map idx))
|
(parse-single-shadow shadow-map idx))
|
||||||
converted)
|
converted)
|
||||||
|
|
||||||
;; Collect all errors from all shadows
|
;; Collect all errors from all shadows
|
||||||
all-errors (mapcat :errors parsed-shadows)
|
all-errors (mapcat :errors parsed-shadows)
|
||||||
|
|
||||||
;; Collect all values from shadows that have values
|
;; Collect all values from shadows that have values
|
||||||
all-values (into [] (keep :value parsed-shadows))]
|
all-values (into [] (keep :value parsed-shadows))]
|
||||||
|
|
||||||
(if (seq all-errors)
|
(if (seq all-errors)
|
||||||
{:errors all-errors
|
{:errors all-errors
|
||||||
:value all-values}
|
:value all-values}
|
||||||
{:value all-values}))))
|
{:value all-values})))))
|
||||||
|
|
||||||
(defn collect-shadow-errors [token shadow-index]
|
(defn collect-shadow-errors [token shadow-index]
|
||||||
(group-by :shadow-key
|
(group-by :shadow-key
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
[app.main.data.persistence :as-alias dps]
|
[app.main.data.persistence :as dps]
|
||||||
[app.main.data.plugins :as dp]
|
[app.main.data.plugins :as dp]
|
||||||
[app.main.data.profile :as du]
|
[app.main.data.profile :as du]
|
||||||
[app.main.data.project :as dpj]
|
[app.main.data.project :as dpj]
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
[app.main.errors]
|
[app.main.errors]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.features.pointer-map :as fpmap]
|
[app.main.features.pointer-map :as fpmap]
|
||||||
|
[app.main.refs :as refs]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.render-wasm :as wasm]
|
[app.render-wasm :as wasm]
|
||||||
@@ -269,8 +270,12 @@
|
|||||||
(ptk/reify ::process-wasm-object
|
(ptk/reify ::process-wasm-object
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ state _]
|
(effect [_ state _]
|
||||||
(let [objects (dsh/lookup-page-objects state)]
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
(wasm.api/process-object (get objects id))))))
|
shape (get objects id)]
|
||||||
|
;; Only process objects that exist in the current page
|
||||||
|
;; This prevents errors when processing changes from other pages
|
||||||
|
(when shape
|
||||||
|
(wasm.api/process-object shape))))))
|
||||||
|
|
||||||
(defn initialize-workspace
|
(defn initialize-workspace
|
||||||
[team-id file-id]
|
[team-id file-id]
|
||||||
@@ -379,6 +384,59 @@
|
|||||||
(->> (rx/from added)
|
(->> (rx/from added)
|
||||||
(rx/map process-wasm-object)))))))
|
(rx/map process-wasm-object)))))))
|
||||||
|
|
||||||
|
(when render-wasm?
|
||||||
|
(let [local-commits-s
|
||||||
|
(->> stream
|
||||||
|
(rx/filter dch/commit?)
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/filter #(and (= :local (:source %))
|
||||||
|
(not (contains? (:tags %) :position-data))))
|
||||||
|
(rx/filter (complement empty?)))
|
||||||
|
|
||||||
|
notifier-s
|
||||||
|
(rx/merge
|
||||||
|
(->> local-commits-s (rx/debounce 1000))
|
||||||
|
(->> stream (rx/filter dps/force-persist?)))
|
||||||
|
|
||||||
|
objects-s
|
||||||
|
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||||
|
|
||||||
|
current-page-id-s
|
||||||
|
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||||
|
|
||||||
|
(->> local-commits-s
|
||||||
|
(rx/buffer-until notifier-s)
|
||||||
|
(rx/with-latest-from objects-s)
|
||||||
|
(rx/map
|
||||||
|
(fn [[commits objects]]
|
||||||
|
(->> commits
|
||||||
|
(mapcat :redo-changes)
|
||||||
|
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||||
|
(filter #(cfh/text-shape? objects (:id %)))
|
||||||
|
(map #(vector
|
||||||
|
(:id %)
|
||||||
|
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||||
|
|
||||||
|
(rx/with-latest-from current-page-id-s)
|
||||||
|
(rx/map
|
||||||
|
(fn [[text-position-data page-id]]
|
||||||
|
(let [changes
|
||||||
|
(->> text-position-data
|
||||||
|
(mapv (fn [[id position-data]]
|
||||||
|
{:type :mod-obj
|
||||||
|
:id id
|
||||||
|
:page-id page-id
|
||||||
|
:operations
|
||||||
|
[{:type :set
|
||||||
|
:attr :position-data
|
||||||
|
:val position-data
|
||||||
|
:ignore-touched true
|
||||||
|
:ignore-geometry true}]})))]
|
||||||
|
(dch/commit-changes
|
||||||
|
{:redo-changes changes :undo-changes []
|
||||||
|
:save-undo? false
|
||||||
|
:tags #{:position-data}})))))))
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter dch/commit?)
|
(rx/filter dch/commit?)
|
||||||
(rx/map deref)
|
(rx/map deref)
|
||||||
|
|||||||
@@ -706,53 +706,58 @@
|
|||||||
(= 1 (count tree-root)))]
|
(= 1 (count tree-root)))]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
|
;; Paste next to selected frame, if selected is itself or of the same size as the copied
|
||||||
|
(and (selected-frame? state)
|
||||||
|
(or (any-same-frame-from-selected? state (keys pobjects))
|
||||||
|
(and only-one-root-shape?
|
||||||
|
(frame-same-size? pobjects (first tree-root)))))
|
||||||
|
(let [selected-frame-obj (get page-objects (first page-selected))
|
||||||
|
parent-id (:parent-id base)
|
||||||
|
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
|
||||||
|
paste-y (:y selected-frame-obj)
|
||||||
|
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
|
||||||
|
|
||||||
|
[parent-id delta index])
|
||||||
|
|
||||||
|
;; Paste inside selected frame otherwise
|
||||||
(selected-frame? state)
|
(selected-frame? state)
|
||||||
|
(let [selected-frame-obj (get page-objects (first page-selected))
|
||||||
|
origin-frame-id (:frame-id first-selected-obj)
|
||||||
|
origin-frame-object (get page-objects origin-frame-id)
|
||||||
|
|
||||||
(if (or (any-same-frame-from-selected? state (keys pobjects))
|
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
|
||||||
(and only-one-root-shape?
|
(min (- (:width frame-object) (:width wrapper))))
|
||||||
(frame-same-size? pobjects (first tree-root))))
|
|
||||||
;; Paste next to selected frame, if selected is itself or of the same size as the copied
|
|
||||||
(let [selected-frame-obj (get page-objects (first page-selected))
|
|
||||||
parent-id (:parent-id base)
|
|
||||||
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
|
|
||||||
paste-y (:y selected-frame-obj)
|
|
||||||
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
|
|
||||||
|
|
||||||
[parent-id delta index])
|
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
|
||||||
|
(min (- (:height frame-object) (:height wrapper))))
|
||||||
|
|
||||||
;; Paste inside selected frame otherwise
|
;; Pasted objects mustn't exceed the selected frame x limit
|
||||||
(let [selected-frame-obj (get page-objects (first page-selected))
|
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
|
||||||
origin-frame-id (:frame-id first-selected-obj)
|
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
|
||||||
origin-frame-object (get page-objects origin-frame-id)
|
(:x frame-object))
|
||||||
|
|
||||||
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
|
;; Pasted objects mustn't exceed the selected frame y limit
|
||||||
(min (- (:width frame-object) (:width wrapper))))
|
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
|
||||||
|
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
|
||||||
|
(:y frame-object))
|
||||||
|
|
||||||
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
|
delta (if (= origin-frame-id uuid/zero)
|
||||||
(min (- (:height frame-object) (:height wrapper))))
|
;; When the origin isn't in a frame the result is pasted in the center.
|
||||||
|
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
|
||||||
|
;; When pasting from one frame to another frame the object
|
||||||
|
;; position must be limited to container boundaries. If
|
||||||
|
;; the pasted object doesn't fit we try to:
|
||||||
|
;;
|
||||||
|
;; - Align it to the limits on the x and y axis
|
||||||
|
;; - Respect the distance of the object to the right
|
||||||
|
;; and bottom in the original frame
|
||||||
|
(gpt/point paste-x paste-y))
|
||||||
|
|
||||||
;; Pasted objects mustn't exceed the selected frame x limit
|
target-index
|
||||||
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
|
(if (and (ctl/flex-layout? selected-frame-obj) (ctl/reverse? selected-frame-obj))
|
||||||
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
|
(dec 0) ;; Before the first index 0
|
||||||
(:x frame-object))
|
(count (:shapes selected-frame-obj)))]
|
||||||
|
[frame-id delta target-index])
|
||||||
;; Pasted objects mustn't exceed the selected frame y limit
|
|
||||||
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
|
|
||||||
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
|
|
||||||
(:y frame-object))
|
|
||||||
|
|
||||||
delta (if (= origin-frame-id uuid/zero)
|
|
||||||
;; When the origin isn't in a frame the result is pasted in the center.
|
|
||||||
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
|
|
||||||
;; When pasting from one frame to another frame the object
|
|
||||||
;; position must be limited to container boundaries. If
|
|
||||||
;; the pasted object doesn't fit we try to:
|
|
||||||
;;
|
|
||||||
;; - Align it to the limits on the x and y axis
|
|
||||||
;; - Respect the distance of the object to the right
|
|
||||||
;; and bottom in the original frame
|
|
||||||
(gpt/point paste-x paste-y))]
|
|
||||||
[frame-id delta (dec (count (:shapes selected-frame-obj)))]))
|
|
||||||
|
|
||||||
(empty? page-selected)
|
(empty? page-selected)
|
||||||
(let [frame-id (ctst/top-nested-frame page-objects position)
|
(let [frame-id (ctst/top-nested-frame page-objects position)
|
||||||
|
|||||||
@@ -102,7 +102,8 @@
|
|||||||
{:origin it
|
{:origin it
|
||||||
:redo-changes changes
|
:redo-changes changes
|
||||||
:undo-changes []
|
:undo-changes []
|
||||||
:save-undo? false})))))))
|
:save-undo? false
|
||||||
|
:ignore-wasm? true})))))))
|
||||||
|
|
||||||
;; FIXME: would be nice to not execute this code twice per page in the
|
;; FIXME: would be nice to not execute this code twice per page in the
|
||||||
;; same working session, maybe some local memoization can improve that
|
;; same working session, maybe some local memoization can improve that
|
||||||
@@ -119,4 +120,5 @@
|
|||||||
{:origin it
|
{:origin it
|
||||||
:redo-changes changes
|
:redo-changes changes
|
||||||
:undo-changes []
|
:undo-changes []
|
||||||
:save-undo? false})))))))
|
:save-undo? false
|
||||||
|
:ignore-wasm? true})))))))
|
||||||
|
|||||||
@@ -119,21 +119,6 @@
|
|||||||
(let [page (dsh/lookup-page state)]
|
(let [page (dsh/lookup-page state)]
|
||||||
(rx/of (update-flow (:id page) flow-id #(assoc % :name name)))))))
|
(rx/of (update-flow (:id page) flow-id #(assoc % :name name)))))))
|
||||||
|
|
||||||
(defn start-rename-flow
|
|
||||||
[id]
|
|
||||||
(dm/assert! (uuid? id))
|
|
||||||
(ptk/reify ::start-rename-flow
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(assoc-in state [:workspace-local :flow-for-rename] id))))
|
|
||||||
|
|
||||||
(defn end-rename-flow
|
|
||||||
[]
|
|
||||||
(ptk/reify ::end-rename-flow
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(update state :workspace-local dissoc :flow-for-rename))))
|
|
||||||
|
|
||||||
;; --- Interactions
|
;; --- Interactions
|
||||||
|
|
||||||
(defn- connected-frame?
|
(defn- connected-frame?
|
||||||
|
|||||||
@@ -649,7 +649,7 @@
|
|||||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||||
|
|
||||||
ids
|
ids
|
||||||
(into [] xf:without-uuid-zero (keys transforms))
|
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||||
|
|
||||||
update-shape
|
update-shape
|
||||||
(fn [shape]
|
(fn [shape]
|
||||||
|
|||||||
@@ -831,7 +831,8 @@
|
|||||||
(effect [_ state _]
|
(effect [_ state _]
|
||||||
(when (features/active-feature? state "text-editor/v2")
|
(when (features/active-feature? state "text-editor/v2")
|
||||||
(let [instance (:workspace-editor state)
|
(let [instance (:workspace-editor state)
|
||||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration))
|
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
|
||||||
|
(styles/get-styles-from-style-declaration))
|
||||||
overriden-attrs (merge attrs-to-override attrs)
|
overriden-attrs (merge attrs-to-override attrs)
|
||||||
styles (styles/attrs->styles overriden-attrs)]
|
styles (styles/attrs->styles overriden-attrs)]
|
||||||
(editor.v2/applyStylesToSelection instance styles))))))
|
(editor.v2/applyStylesToSelection instance styles))))))
|
||||||
|
|||||||
@@ -153,11 +153,11 @@
|
|||||||
(defn value->shadow
|
(defn value->shadow
|
||||||
"Transform a token shadow value into penpot shadow data structure"
|
"Transform a token shadow value into penpot shadow data structure"
|
||||||
[value]
|
[value]
|
||||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||||
{:id (random-uuid)
|
{:id (random-uuid)
|
||||||
:hidden false
|
:hidden false
|
||||||
:offset-x offsetX
|
:offset-x offset-x
|
||||||
:offset-y offsetY
|
:offset-y offset-y
|
||||||
:blur blur
|
:blur blur
|
||||||
:color (value->color color)
|
:color (value->color color)
|
||||||
:spread spread
|
:spread spread
|
||||||
|
|||||||
@@ -112,6 +112,10 @@
|
|||||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||||
|
|
||||||
|
:error.style-dictionary/invalid-token-value-shadow
|
||||||
|
{:error/code :error.style-dictionary/invalid-token-value-shadow
|
||||||
|
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
|
||||||
|
|
||||||
:error/unknown
|
:error/unknown
|
||||||
{:error/code :error/unknown
|
{:error/code :error/unknown
|
||||||
:error/fn #(tr "labels.unknown-error")}})
|
:error/fn #(tr "labels.unknown-error")}})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user