Compare commits

...

61 Commits

Author SHA1 Message Date
Andrey Antukh
a7f65259b1 Make automatic workflows not dependent on yarn 2025-11-28 12:19:37 +01:00
Andrey Antukh
9280d3937c Move frontend/vendor to frontend/packages 2025-11-28 12:19:37 +01:00
Andrey Antukh
8b12e5d996 Replace tubax with more modern tooling 2025-11-28 12:19:37 +01:00
Andrey Antukh
63763aace7 Replace hightlight.js internal bundle with direct npm use 2025-11-28 12:19:37 +01:00
Andrey Antukh
45f98b716e Replace direct draft-js usage with internal module 2025-11-28 12:19:37 +01:00
Andrey Antukh
fd2e5e31e2 🚧 Migrate frontend to pnpm 2025-11-28 12:19:37 +01:00
Andrey Antukh
d1f660c70c 🔥 Remove old gulp related dependencies 2025-11-28 12:19:37 +01:00
Andrey Antukh
119d901ce3 📎 Move all deps to dev-dependencies on frontend package.json
All they only needed for build process.
2025-11-28 12:19:37 +01:00
Andrey Antukh
856dd2238d 📎 Update frontend yarn.lock file 2025-11-28 12:19:37 +01:00
Andrey Antukh
31017654bb 📎 Add immutable dependency to vendor/draft-js 2025-11-28 12:19:37 +01:00
Andrey Antukh
91337c074a 📎 Add .pnpm-store to gitignore 2025-11-28 12:19:37 +01:00
Andrey Antukh
0200ba15f2 ⬆️ Update dependencies on exporter 2025-11-28 12:19:37 +01:00
Andrey Antukh
957d51c68f ⬆️ Update frontend module depenedencies 2025-11-28 12:19:37 +01:00
Andrey Antukh
a0fd066af1 🔥 Remove core.spec usage on common and frontend 2025-11-27 18:01:25 +01:00
Andrey Antukh
4ed307be7a Remove malli dev stuff from cljs build
It only used on backend.
2025-11-27 18:01:25 +01:00
Andrey Antukh
c4260c17b2 🔧 Disable code motion on shadow config 2025-11-27 18:01:24 +01:00
Andrey Antukh
692471c927 Use sm/coercer on app.render entry point 2025-11-27 18:01:24 +01:00
Andrey Antukh
5b62936ab4 ♻️ Use ESM target for build frontend 2025-11-27 18:01:24 +01:00
Andrey Antukh
ada0d8b293 🔥 Remove unused require of edn reader on loggin ns 2025-11-27 18:01:24 +01:00
Andrey Antukh
b5a6867058 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 18:01:08 +01:00
Andrey Antukh
7fe20b65dc 🔧 Add more cache efficient configuration for devenv nginx 2025-11-27 17:59:12 +01:00
Andrey Antukh
e5638cd769 ⬆️ Update clojure tools version on devenv 2025-11-27 17:58:56 +01:00
Eva Marco
8e79dfcb82 🐛 Fix input variant 2025-11-27 17:54:11 +01:00
Eva Marco
508db99a57 🐛 Restore empty field error on dimension, text-case and color forms 2025-11-27 17:54:11 +01:00
Andrey Antukh
3c6c9894da 🐛 Restore empty value error on border radius token form 2025-11-27 17:54:11 +01:00
Andrey Antukh
972b23e6c0 🐛 Fix incorect pred build on ::sm/text schema 2025-11-27 17:54:11 +01:00
Andrey Antukh
28f550d533 🔥 Remove commented code 2025-11-27 17:54:11 +01:00
Elena Torró
2b20f75fd4 Merge pull request #7837 from penpot/ladybenko-12719-fix-editor-unicode-fonts
🐛 Fix editor not using fallback fonts for selected text
2025-11-27 17:37:00 +01:00
Belén Albeza
4d6d7a6a3d 🐛 Fix emoji font not being used as fallback in text editor dom 2025-11-27 17:23:20 +01:00
Andrey Antukh
0f88253dd5 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 16:11:36 +01:00
Andrey Antukh
db1ab7be69 📎 Run worker bundling serially on devenv 2025-11-27 16:09:15 +01:00
Andrey Antukh
fcbe9d92dc 🐛 Fix unexpected exception on rendering feedback email
Looks like a bug on selmer library
2025-11-27 16:09:15 +01:00
Andrey Antukh
9998ce0bb4 🔥 Remove fipps direct dependency 2025-11-27 16:09:15 +01:00
Andrey Antukh
6061391c89 Don't require cljs.analyzer api under cljs on data.macros
Reduces the final production bundle size
2025-11-27 16:09:15 +01:00
Andrey Antukh
eabf6e36ed Remove a level of indentation on subscriptions-dashboard tests 2025-11-27 16:09:15 +01:00
Andrey Antukh
04274e53fa 📎 Fix advanced compilation warnings related to jsdoc 2025-11-27 16:09:15 +01:00
Andrey Antukh
52dd9271a9 🐛 Encode header values as strings on audit archive task 2025-11-27 16:09:15 +01:00
andrés gonzález
8f5a81e179 📚 Add info about boolean variants (#7828) 2025-11-27 16:03:11 +01:00
Alonso Torres
a940c08da9 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 14:13:48 +01:00
Alejandro Alonso
3de4473251 Merge pull request #7845 from penpot/elenatorro-fix-case
🐛 Fix editor vertical align default case
2025-11-27 14:00:12 +01:00
Andrey Antukh
0735140f07 🔧 Change concurrency rules on tests github workflow 2025-11-27 13:46:48 +01:00
Elena Torro
dc8a07099d 🐛 Fix vertical align default case 2025-11-27 13:38:51 +01:00
Andrey Antukh
8e3996fbb0 🔧 Change concirrency rules on tests github workflow 2025-11-27 13:16:08 +01:00
Alonso Torres
67762d9450 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 13:02:47 +01:00
Elena Torró
90dcf04fb0 Merge pull request #7841 from penpot/superalex-fix-boolean-operators-no-selection
🐛 Fix boolean operators no selection
2025-11-27 12:50:16 +01:00
Belén Albeza
f84c236e02 🐛 Fix text editor v2 not using fallback fonts for selected text 2025-11-27 12:26:39 +01:00
Alejandro Alonso
63959a22cc 🐛 Fix svg attrs 2025-11-27 12:23:46 +01:00
Alejandro Alonso
8840246425 🐛 Fix bleeding masks 2025-11-27 12:23:46 +01:00
Alejandro Alonso
62ec66cd15 🔧 Adding more e2e tests for nested frames with clipping 2025-11-27 12:23:46 +01:00
Alejandro Alonso
e3b87390f6 🐛 Fix nested shadows clipping 2025-11-27 12:23:46 +01:00
Alejandro Alonso
d9ab28e6ed 🐛 Fix nested clipping 2025-11-27 12:23:46 +01:00
Belén Albeza
9183dbbc43 🔧 Fix lint error (rust) 2025-11-27 11:51:05 +01:00
Andrey Antukh
74d00473e9 Add missing render-wasm to the ci workflow 2025-11-27 11:51:05 +01:00
Alejandro Alonso
1c70f5a36b 🐛 Fix boolean operatos shown when there is no selection 2025-11-27 11:22:15 +01:00
Andrey Antukh
b23e0c0642 Add tempfile storage bucket handler test case (#7839) 2025-11-27 10:27:57 +01:00
Andrey Antukh
7f62652870 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 09:24:40 +01:00
Andrey Antukh
78d31ab11a 🐳 Update devenv docker and compose files
Reuse the already builded imagemagick instead of building
it again on the devenv.
2025-11-26 07:44:56 +01:00
Andrey Antukh
0a80c47901 Merge remote-tracking branch 'origin/staging' into develop 2025-11-26 07:30:42 +01:00
Yamila Moreno
77f1046fc8 🔧 Add MT notification when a docker image with final tag is built (#7824) 2025-11-25 16:39:42 +01:00
Andrey Antukh
553b73a83c ♻️ Replace CircleCI with Github Actions (#7789)
* ♻️ Replace circleci with github actions

* 📎 Add integration test sharding

* 📎 Reuse single build for integration tests shards
2025-11-24 10:44:04 +01:00
Andrey Antukh
00a45cb274 📎 Bump new version on changelog 2025-11-24 09:47:00 +01:00
107 changed files with 3352 additions and 2524 deletions

View File

@@ -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

View File

@@ -21,6 +21,21 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs: build-docker

View File

@@ -8,8 +8,6 @@ on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
push:
branches:
@@ -17,12 +15,12 @@ on:
- staging
concurrency:
group: ${{ github.ref }}
group: ${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: "Code Linter"
name: "Linter"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
@@ -32,10 +30,7 @@ jobs:
- name: Check clojure code format
run: |
corepack enable;
corepack install;
yarn install
yarn run fmt:clj:check
./scripts/lint
test-common:
name: "Common Tests"
@@ -54,10 +49,7 @@ jobs:
- name: Run tests on NODE
working-directory: ./common
run: |
corepack enable;
corepack install;
yarn install;
yarn run test;
./scripts/test
test-frontend:
name: "Frontend Tests"
@@ -71,25 +63,36 @@ jobs:
- name: Unit Tests
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install;
yarn run test;
./scripts/test
- name: Component Tests
working-directory: ./frontend
run: |
yarn run playwright install chromium --with-deps;
yarn run build:storybook
./scripts/test-components
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-render-wasm:
name: "Render WASM Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
- name: Check SCSS Format
working-directory: ./frontend
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Format
working-directory: ./render-wasm
run: |
yarn run lint:scss;
cargo fmt --check
- name: Lint
working-directory: ./render-wasm
run: |
./lint
- name: Test
working-directory: ./render-wasm
run: |
./test
test-backend:
name: "Backend Tests"
@@ -142,11 +145,7 @@ jobs:
- name: Run tests
working-directory: ./library
run: |
corepack enable;
corepack install;
yarn install;
yarn run build:bundle;
yarn run test;
./scripts/test
build-integration:
name: "Build Integration Bundle"
@@ -160,17 +159,7 @@ jobs:
- name: Build Bundle
working-directory: ./frontend
run: |
corepack enable;
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
./scripts/build 0.0.0
- name: Store Bundle Cache
uses: actions/cache@v4
@@ -178,6 +167,7 @@ jobs:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04
@@ -197,11 +187,7 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list --shard="1/4";
./scripts/test-e2e --shard="1/4";
- name: Upload test result
uses: actions/upload-artifact@v4
@@ -231,11 +217,7 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list --shard "2/4";
./scripts/test-e2e --shard="2/4";
- name: Upload test result
uses: actions/upload-artifact@v4
@@ -265,11 +247,7 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list --shard "3/4";
./scripts/test-e2e --shard="3/4";
- name: Upload test result
uses: actions/upload-artifact@v4
@@ -281,7 +259,7 @@ jobs:
retention-days: 3
test-integration-4:
name: "Integration Tests 3/4"
name: "Integration Tests 4/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
@@ -299,11 +277,7 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list --shard "4/4";
./scripts/test-e2e --shard="4/4";
- name: Upload test result
uses: actions/upload-artifact@v4

1
.gitignore vendored
View File

@@ -80,3 +80,4 @@ node_modules
/playwright/.cache/
/render-wasm/target/
/**/.yarn/*
/.pnpm-store

View File

@@ -1,5 +1,18 @@
# CHANGELOG
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations

View File

@@ -1,7 +1,8 @@
From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
Subject: {{feedback-subject}}
Type: {{feedback-type}}
{%- if feedback-error-href %}
{% if feedback-error-href %}
HREF: {{feedback-error-href}}
{% endif -%}

View File

@@ -57,7 +57,7 @@
:uid uuid/zero})
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (cf/get :public-uri)
"origin" (str (cf/get :public-uri))
"cookie" (u/map->query-string {:auth-token token})}
params {:uri uri
:timeout 12000

View File

@@ -9,7 +9,7 @@
[app.common.exceptions :as ex]
[selmer.parser :as sp]))
(sp/cache-off!)
;; (sp/cache-off!)
(defn render
[path context]

View File

@@ -318,3 +318,35 @@
;; check that we have all no objects
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(t/is (= 0 (count rows))))))
(t/deftest tempfile-bucket-test
(let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend))
content1 (sto/content "content1")
now (ct/now)
object1 (sto/put-object! storage {::sto/content content1
::sto/touched-at (ct/plus now {:minutes 1})
:bucket "tempfile"
:content-type "text/plain"})]
(binding [ct/*clock* (clock/fixed now)]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 1 (:delete res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res)))))))

View File

@@ -17,7 +17,7 @@
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
selmer/selmer {:mvn/version "1.12.62"}
selmer/selmer {:mvn/version "1.12.69"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
@@ -29,7 +29,6 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
@@ -48,12 +47,8 @@
com.sun.mail/jakarta.mail {:mvn/version "2.0.2"}
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.29"}
me.flowthing/pp {:mvn/version "2024-11-13.77"}
io.aviso/pretty {:mvn/version "1.4.4"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src" "vendor" "target/classes"]

7
common/scripts/test Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
corepack enable;
corepack install;
yarn install;
yarn run test;

View File

@@ -9,10 +9,10 @@
(:refer-clojure :exclude [get-in select-keys str with-open max])
#?(:cljs (:require-macros [app.common.data.macros]))
(:require
#?(:clj [cljs.analyzer.api :as aapi])
#?(:clj [clojure.core :as c]
:cljs [cljs.core :as c])
[app.common.data :as d]
[cljs.analyzer.api :as aapi]
[cuerdas.core :as str]))
(defmacro select-keys
@@ -44,42 +44,43 @@
[& params]
`(str/concat ~@params))
(defmacro export
"A helper macro that allows reexport a var in a current namespace."
[v]
(if (boolean (:ns &env))
#?(:clj
(defmacro export
"A helper macro that allows reexport a var in a current namespace."
[v]
(if (boolean (:ns &env))
;; Code for ClojureScript
(let [mdata (aapi/resolve &env v)
arglists (second (get-in mdata [:meta :arglists]))
sym (symbol (c/name v))
andsym (symbol "&")
procarg #(if (= % andsym) % (gensym "param"))]
(if (pos? (count arglists))
`(def
~(with-meta sym (:meta mdata))
(fn ~@(for [args arglists]
(let [args (map procarg args)]
(if (some #(= andsym %) args)
(let [[sargs dargs] (split-with #(not= andsym %) args)]
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
`([~@args] (~v ~@args)))))))
`(def ~(with-meta sym (:meta mdata)) ~v)))
;; Code for ClojureScript
(let [mdata (aapi/resolve &env v)
arglists (second (get-in mdata [:meta :arglists]))
sym (symbol (c/name v))
andsym (symbol "&")
procarg #(if (= % andsym) % (gensym "param"))]
(if (pos? (count arglists))
`(def
~(with-meta sym (:meta mdata))
(fn ~@(for [args arglists]
(let [args (map procarg args)]
(if (some #(= andsym %) args)
(let [[sargs dargs] (split-with #(not= andsym %) args)]
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
`([~@args] (~v ~@args)))))))
`(def ~(with-meta sym (:meta mdata)) ~v)))
;; Code for Clojure
(let [vr (resolve v)
m (meta vr)
n (:name m)
n (with-meta n
(cond-> {}
(:dynamic m) (assoc :dynamic true)
(:protocol m) (assoc :protocol (:protocol m))))]
`(let [m# (meta ~vr)]
(def ~n (deref ~vr))
(alter-meta! (var ~n) merge (dissoc m# :name))
;; (when (:macro m#)
;; (.setMacro (var ~n)))
~vr))))
;; Code for Clojure
(let [vr (resolve v)
m (meta vr)
n (:name m)
n (with-meta n
(cond-> {}
(:dynamic m) (assoc :dynamic true)
(:protocol m) (assoc :protocol (:protocol m))))]
`(let [m# (meta ~vr)]
(def ~n (deref ~vr))
(alter-meta! (var ~n) merge (dissoc m# :name))
;; (when (:macro m#)
;; (.setMacro (var ~n)))
~vr)))))
(defmacro fmt
"String interpolation helper. Can only be used with strings known at

View File

@@ -14,8 +14,7 @@
[app.common.schema :as sm]
[clojure.core :as c]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[expound.alpha :as expound])
[cuerdas.core :as str])
#?(:clj
(:import
clojure.lang.IPersistentMap)))
@@ -110,13 +109,6 @@
(contains? data :explain))
(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)
(sm/humanize-explain (::sm/explain data) opts)))

View File

@@ -43,8 +43,6 @@
"
#?(:cljs (:require-macros [app.common.logging :as l]))
(:require
#?(:clj [clojure.edn :as edn]
:cljs [cljs.reader :as edn])
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pprint :as pp]

View File

@@ -18,7 +18,6 @@
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
@@ -35,8 +34,7 @@
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
[clojure.set :as set]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
@@ -473,10 +471,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync file with library"
@@ -510,10 +508,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync local components with library"

View File

@@ -8,6 +8,8 @@
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
#?(:clj [malli.dev.pretty :as mdp])
#?(:clj [malli.dev.virhe :as v])
[app.common.data :as d]
[app.common.math :as mth]
[app.common.pprint :as pp]
@@ -19,8 +21,6 @@
[clojure.core :as c]
[cuerdas.core :as str]
[malli.core :as m]
[malli.dev.pretty :as mdp]
[malli.dev.virhe :as v]
[malli.error :as me]
[malli.generator :as mg]
[malli.registry :as mr]
@@ -245,27 +245,30 @@
:level (d/nilv level 8)
:length (d/nilv length 12)})))))
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(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)]})
#?(:clj
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(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
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(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) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]})
#?(:clj
(defmethod v/-format ::explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(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) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]}))
(defn pretty-explain
"A helper that allows print a console-friendly output for the
explain; should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options))))
#?(:clj
(defn pretty-explain
"A helper that allows print a console-friendly output for the explain;
should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options)))))
(defmacro ignoring
[expr]
@@ -299,6 +302,13 @@
::explain explain}))))
value))))
(defn coercer
[schema & {:as opts}]
(let [decode-fn (decoder schema json-transformer)
check-fn (check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))
(defn check
"A helper intended to be used on assertions for validate/check the
schema over provided data. Raises an assertion exception.
@@ -842,38 +852,6 @@
choices))]
{:pred pred}))})
;; (register!
;; {:type ::inst
;; :pred tm/instant?
;; :type-properties
;; {:title "inst"
;; :description "Satisfies Inst protocol"
;; :error/message "should be an instant"
;; :gen/gen (->> (sg/small-int :min 0 :max 100000)
;; (sg/fmap (fn [v] (tm/parse-inst v))))
;; :decode/string tm/parse-inst
;; :encode/string tm/format-inst
;; :decode/json tm/parse-inst
;; :encode/json tm/format-inst
;; ::oapi/type "string"
;; ::oapi/format "iso"}})
;; (register!
;; {:type ::timestamp
;; :pred tm/instant?
;; :type-properties
;; {:title "inst"
;; :description "Satisfies Inst protocol, the same as ::inst but encodes to epoch"
;; :error/message "should be an instant"
;; :gen/gen (->> (sg/small-int)
;; (sg/fmap (fn [v] (tm/parse-inst v))))
;; :decode/string tm/parse-inst
;; :encode/string inst-ms
;; :decode/json tm/parse-inst
;; :encode/json inst-ms
;; ::oapi/type "string"
;; ::oapi/format "number"}})
#?(:clj
(register!
@@ -951,7 +929,7 @@
:pred #(and (string? %) (not (str/blank? %)))
:property-pred
(fn [{:keys [min max] :as props}]
(if (seq props)
(if (or min max)
(fn [value]
(let [size (count value)]
(cond
@@ -1025,6 +1003,9 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-safe-int?
(lazy-validator ::safe-int))
(def valid-text?
(validator ::text))

View File

@@ -25,48 +25,6 @@ RUN set -ex; \
binutils \
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
################################################################################
@@ -107,7 +65,7 @@ RUN set -eux; \
FROM base AS setup-jvm
ENV CLOJURE_VERSION=1.12.2.1565
ENV CLOJURE_VERSION=1.12.3.1577
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
@@ -385,7 +343,7 @@ ENV LANG='C.UTF-8' \
RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=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/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node

View File

@@ -67,6 +67,11 @@ services:
- PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
networks:
default:
aliases:
- main
minio:
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
command: minio server /mnt/data --console-address ":9001"
@@ -78,10 +83,6 @@ services:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
ports:
- 9000:9000
- 9001:9001
networks:
default:
aliases:

View File

@@ -12,7 +12,7 @@ http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_timeout 0;
types_hash_max_size 2048;
server_tokens off;
@@ -223,16 +223,6 @@ http {
add_header X-Cache-Status $upstream_cache_status;
}
location ~ ^/js/config.js$ {
add_header Cache-Control "no-store, no-cache, max-age=0" always;
}
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
# We set no cache only on devenv
add_header Cache-Control "no-store, no-cache, max-age=0" always;
# add_header Cache-Control "max-age=604800" always; # 7 days
}
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
}
@@ -240,9 +230,10 @@ http {
return 301 " /404";
}
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
if_modified_since off;
add_header Cache-Control "no-store";
add_header Connection close always;
# This header is what we need to use on prod
# add_header Cache-Control "public, must-revalidate, max-age=0";
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -107,6 +107,25 @@ desc: Streamline your design workflow with Penpot's Components guide! Learn to c
<li>Select the variant copy, press right-click, and select the menu option <strong>Restore variant</strong> (will show if the main component still exists).</li>
</ul>
<h3 id="component-variants-toggle">Toggle for boolean variants</h3>
<p>When a variant property represents a boolean state, Penpot can display it as a toggle instead of a dropdown. This offers a quicker and more visual way to switch between two opposite values when working with copies.</p>
<p>The toggle appears in place of the property values dropdown, <strong>only when a copy is selected</strong>.</p>
<figure>
<img src="/img/variants/07-variants-boolean.webp" alt="Boolean variant option" />
</figure>
<h4>Accepted value pairs</h4>
<p>For Penpot to recognize the property as a boolean and display the toggle, the property must be defined with exactly two opposing values. These can be any of the following pairs:</p>
<ul>
<li><code>true</code> / <code>false</code></li>
<li><code>on</code> / <code>off</code></li>
<li><code>yes</code> / <code>no</code></li>
</ul>
<p>The order of the values does not matter. Penpot automatically maps them to ON and OFF states:</p>
<ul>
<li><strong>ON state:</strong> <code>true</code>, <code>yes</code>, <code>on</code></li>
<li><strong>OFF state:</strong> <code>false</code>, <code>no</code>, <code>off</code></li>
</ul>
<h3 id="component-use-variants">Use variants</h3>
<p>Once you have created your variants, you can place a copy of a component with variants into your design and then switch between its different versions.</p>

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
@@ -16,9 +16,9 @@
"date-fns": "^4.1.0",
"generic-pool": "^3.9.0",
"inflation": "^2.1.0",
"ioredis": "^5.8.1",
"playwright": "^1.55.1",
"raw-body": "^3.0.1",
"ioredis": "^5.8.2",
"playwright": "^1.57.0",
"raw-body": "^3.0.2",
"source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1",
"xml-js": "^1.6.11",

View File

@@ -243,7 +243,7 @@ __metadata:
languageName: node
linkType: hard
"bytes@npm:3.1.2":
"bytes@npm:~3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -442,7 +442,7 @@ __metadata:
languageName: node
linkType: hard
"depd@npm:2.0.0, depd@npm:~2.0.0":
"depd@npm:~2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -577,9 +577,9 @@ __metadata:
date-fns: "npm:^4.1.0"
generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0"
ioredis: "npm:^5.8.1"
playwright: "npm:^1.55.1"
raw-body: "npm:^3.0.1"
ioredis: "npm:^5.8.2"
playwright: "npm:^1.57.0"
raw-body: "npm:^3.0.2"
source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1"
ws: "npm:^8.18.3"
@@ -682,16 +682,16 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
"http-errors@npm:~2.0.1":
version: 2.0.1
resolution: "http-errors@npm:2.0.1"
dependencies:
depd: "npm:2.0.0"
inherits: "npm:2.0.4"
setprototypeof: "npm:1.2.0"
statuses: "npm:2.0.1"
toidentifier: "npm:1.0.1"
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19
depd: "npm:~2.0.0"
inherits: "npm:~2.0.4"
setprototypeof: "npm:~1.2.0"
statuses: "npm:~2.0.2"
toidentifier: "npm:~1.0.1"
checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
languageName: node
linkType: hard
@@ -715,15 +715,6 @@ __metadata:
languageName: node
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":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
@@ -733,6 +724,15 @@ __metadata:
languageName: node
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":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
@@ -754,16 +754,16 @@ __metadata:
languageName: node
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
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node
linkType: hard
"ioredis@npm:^5.8.1":
version: 5.8.1
resolution: "ioredis@npm:5.8.1"
"ioredis@npm:^5.8.2":
version: 5.8.2
resolution: "ioredis@npm:5.8.2"
dependencies:
"@ioredis/commands": "npm:1.4.0"
cluster-key-slot: "npm:^1.1.0"
@@ -774,7 +774,7 @@ __metadata:
redis-errors: "npm:^1.2.0"
redis-parser: "npm:^3.0.0"
standard-as-callback: "npm:^2.1.0"
checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465
checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88
languageName: node
linkType: hard
@@ -1105,27 +1105,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.55.1":
version: 1.55.1
resolution: "playwright-core@npm:1.55.1"
"playwright-core@npm:1.57.0":
version: 1.57.0
resolution: "playwright-core@npm:1.57.0"
bin:
playwright-core: cli.js
checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f
checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
languageName: node
linkType: hard
"playwright@npm:^1.55.1":
version: 1.55.1
resolution: "playwright@npm:1.55.1"
"playwright@npm:^1.57.0":
version: 1.57.0
resolution: "playwright@npm:1.57.0"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.55.1"
playwright-core: "npm:1.57.0"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771
checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
languageName: node
linkType: hard
@@ -1160,15 +1160,15 @@ __metadata:
languageName: node
linkType: hard
"raw-body@npm:^3.0.1":
version: 3.0.1
resolution: "raw-body@npm:3.0.1"
"raw-body@npm:^3.0.2":
version: 3.0.2
resolution: "raw-body@npm:3.0.2"
dependencies:
bytes: "npm:3.1.2"
http-errors: "npm:2.0.0"
iconv-lite: "npm:0.7.0"
unpipe: "npm:1.0.0"
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a
bytes: "npm:~3.1.2"
http-errors: "npm:~2.0.1"
iconv-lite: "npm:~0.7.0"
unpipe: "npm:~1.0.0"
checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
languageName: node
linkType: hard
@@ -1269,7 +1269,7 @@ __metadata:
languageName: node
linkType: hard
"setprototypeof@npm:1.2.0":
"setprototypeof@npm:~1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -1367,10 +1367,10 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
"statuses@npm:~2.0.2":
version: 2.0.2
resolution: "statuses@npm:2.0.2"
checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
languageName: node
linkType: hard
@@ -1499,7 +1499,7 @@ __metadata:
languageName: node
linkType: hard
"toidentifier@npm:1.0.1":
"toidentifier@npm:~1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -1531,7 +1531,7 @@ __metadata:
languageName: node
linkType: hard
"unpipe@npm:1.0.0":
"unpipe@npm:~1.0.0":
version: 1.0.0
resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c

View File

@@ -1,18 +0,0 @@
diff --git a/lib/zip-fs.js b/lib/zip-fs.js
index 1444c0f00e5f1ad6c13521f90a7f3c6659d81116..90e38baef5365c2abbcb9337f7ab37f800e883a4 100644
--- a/lib/zip-fs.js
+++ b/lib/zip-fs.js
@@ -33,12 +33,7 @@ import { initShimAsyncCodec } from "./core/util/stream-codec-shim.js";
import { terminateWorkers } from "./core/codec-pool.js";
let baseURL;
-try {
- baseURL = import.meta.url;
- // eslint-disable-next-line no-unused-vars
-} catch (_) {
- // ignored
-}
+
configure({ baseURL });
configureWebWorker(configure);

View File

@@ -0,0 +1,17 @@
diff --git a/lib/zip-core-base.js b/lib/zip-core-base.js
index 142155c8c3ab1a6caa7c370e8a2931a954556ca9..61b0fe6efb91312f437750e0002218f218afad8b 100644
--- a/lib/zip-core-base.js
+++ b/lib/zip-core-base.js
@@ -28,12 +28,6 @@
import { configure } from "./core/configuration.js";
-try {
- configure({ baseURI: import.meta.url });
-} catch {
- // ignored
-}
-
export * from "./zip-core-reader.js";
export * from "./zip-core-writer.js";
export {

View File

@@ -8,6 +8,11 @@
metosin/reitit-core {:mvn/version "0.9.1"}
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
{:git/tag "v2.2"
:git/sha "0f7e15a"
@@ -20,8 +25,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.24"
:git/sha "17a0c94"
{:git/tag "v2.25"
:git/sha "27e5a1a"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}
@@ -42,7 +47,7 @@
:dev
{:extra-paths ["dev"]
: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"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"browserslist": [
"defaults"
],
@@ -14,10 +14,10 @@
"url": "https://github.com/penpot/penpot"
},
"resolutions": {
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"@vitejs/plugin-react": "^4.2.0",
"playwright": "1.52.0",
"playwright-core": "1.52.0"
"playwright-core": "1.52.0",
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.8.11#~/.yarn/patches/@zip.js-zip.js-npm-2.8.11-b131c96df8.patch"
},
"scripts": {
"build:app:assets": "node ./scripts/build-app-assets.js",
@@ -27,6 +27,7 @@
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
"build:app:libs": "node ./scripts/build-libs.js",
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
"build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs",
"e2e:server": "node ./scripts/e2e-server.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
@@ -52,83 +53,74 @@
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
},
"devDependencies": {
"@playwright/test": "1.52.0",
"@storybook/addon-docs": "10.0.4",
"@storybook/addon-themes": "10.0.4",
"@storybook/addon-vitest": "10.0.4",
"@storybook/react-vite": "10.0.4",
"@types/node": "^22.15.21",
"@penpot/draft-js": "portal:./packages/draft-js",
"@penpot/mousetrap": "portal:./packages/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.1",
"@penpot/text-editor": "portal:./text-editor",
"@playwright/test": "1.57.0",
"@storybook/addon-docs": "10.1.0",
"@storybook/addon-themes": "10.1.0",
"@storybook/addon-vitest": "10.1.0",
"@storybook/react-vite": "10.1.0",
"@tokens-studio/sd-transforms": "1.2.11",
"@types/node": "^22.19.1",
"@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "3.2.4",
"autoprefixer": "^10.4.21",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.8.11#~/.yarn/patches/@zip.js-zip.js-npm-2.8.11-b131c96df8.patch",
"autoprefixer": "^10.4.22",
"compression": "^1.8.1",
"concurrently": "^9.2.1",
"esbuild": "^0.25.9",
"date-fns": "^4.1.0",
"esbuild": "^0.25.12",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0",
"fancy-log": "^2.0.0",
"getopts": "^2.3.0",
"gettext-parser": "^8.0.0",
"gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.2",
"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",
"map-stream": "0.0.7",
"marked": "^15.0.12",
"mkdirp": "^3.0.1",
"mustache": "^4.2.0",
"nodemon": "^3.1.10",
"npm-run-all": "^4.1.5",
"p-limit": "^6.2.0",
"playwright": "1.56.1",
"postcss": "^8.5.4",
"postcss-clean": "^1.2.2",
"prettier": "3.5.3",
"pretty-time": "^1.1.0",
"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.1",
"@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",
"highlight.js": "^11.10.0",
"js-beautify": "^1.15.4",
"jsdom": "^27.2.0",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"map-stream": "0.0.7",
"marked": "^17.0.1",
"mkdirp": "^3.0.1",
"mustache": "^4.2.0",
"nodemon": "^3.1.11",
"npm-run-all": "^4.1.5",
"opentype.js": "^1.3.4",
"p-limit": "^7.2.0",
"playwright": "1.57.0",
"postcss": "^8.5.6",
"postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
"prettier": "3.7.1",
"pretty-time": "^1.1.0",
"prop-types": "^15.8.1",
"randomcolor": "^0.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "^6.0.0",
"react-virtualized": "^9.22.6",
"rimraf": "^6.1.2",
"rxjs": "8.0.0-alpha.14",
"sax": "^1.4.1",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.3",
"source-map-support": "^0.5.21",
"storybook": "10.1.0",
"style-dictionary": "5.0.0-rc.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2",
"tinycolor2": "^1.6.0",
"ua-parser-js": "2.0.5",
"typescript": "^5.9.3",
"ua-parser-js": "2.0.6",
"vite": "^6.4.1",
"vitest": "^3.2.4",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^10.0.1",
"xregexp": "^5.1.2"
}
}

View File

@@ -16,7 +16,9 @@ export const {
RichTextEditorUtil,
SelectionState,
convertFromRaw,
convertToRaw
convertToRaw,
EditorBlock,
Editor
} = pkg;
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';

View File

@@ -8,7 +8,8 @@
"author": "Andrey Antukh",
"license": "MPL-2.0",
"dependencies": {
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
"immutable": "^5.1.4"
},
"peerDependencies": {
"react": ">=0.17.0",

View File

@@ -173,12 +173,13 @@ __metadata:
languageName: node
linkType: hard
"@penpot/draft-js-wrapper@workspace:.":
"@penpot/draft-js@workspace:.":
version: 0.0.0-use.local
resolution: "@penpot/draft-js-wrapper@workspace:."
resolution: "@penpot/draft-js@workspace:."
dependencies:
draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
esbuild: "npm:^0.24.0"
immutable: "npm:^5.1.4"
peerDependencies:
react: ">=0.17.0"
react-dom: ">=0.17.0"
@@ -320,6 +321,13 @@ __metadata:
languageName: node
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":
version: 3.7.6
resolution: "immutable@npm:3.7.6"

View File

@@ -0,0 +1,17 @@
diff --git a/lib/zip-core-base.js b/lib/zip-core-base.js
index 142155c8c3ab1a6caa7c370e8a2931a954556ca9..61b0fe6efb91312f437750e0002218f218afad8b 100644
--- a/lib/zip-core-base.js
+++ b/lib/zip-core-base.js
@@ -28,12 +28,6 @@
import { configure } from "./core/configuration.js";
-try {
- configure({ baseURI: import.meta.url });
-} catch {
- // ignored
-}
-
export * from "./zip-core-reader.js";
export * from "./zip-core-writer.js";
export {

View File

@@ -22,9 +22,9 @@ export default defineConfig({
workers: 1,
/* Timeout for expects (longer in CI) */
timeout: 60000,
timeout: 80000,
expect: {
timeout: process.env.CI ? 30000 : 5000,
timeout: process.env.CI ? 40000 : 5000,
},
/* Reporter to use. See https://playwright.dev/docs/test-reporters */

View File

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,7 @@ export class BasePage {
}
static async mockConfigFlags(page, flags) {
const url = "**/js/config.js?ts=*";
const url = "**/js/config.js";
return await page.route(url, (route) =>
route.fulfill({
status: 200,

View File

@@ -42,7 +42,7 @@ export class WasmWorkspacePage extends WorkspacePage {
}
async waitForFirstRenderWithoutUI() {
await waitForFirstRender();
await this.waitForFirstRender();
await this.hideUI();
}

View File

@@ -258,6 +258,22 @@ test("Renders a file with nested frames with inherited blur", async ({
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with nested clipping frames", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-frame-nested-clipping.json",
);
await workspace.goToWorkspace({
id: "44471494-966a-8178-8006-c5bd93f0fe72",
pageId: "44471494-966a-8178-8006-c5bd93f0fe73",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a clipped frame with a large blur drop shadow", async ({
page,
}) => {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -9,403 +9,399 @@ test.beforeEach(async ({ page }) => {
]);
});
test.describe("Subscriptions: dashboard", () => {
test("Team with unlimited subscription has specific icon in menu", async ({
test("Team with unlimited subscription has specific icon in menu", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamDashboard();
await expect(page.getByTestId("subscription-icon")).toBeVisible();
});
test("The Unlimited subscription has its name in the sidebar dropdown", async ({
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage-one-editor.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await expect(page.getByTestId("subscription-name")).toHaveText(
"Unlimited plan (trial)",
);
});
test("When the subscription status is unpaid, the sidebar dropdown displays the name Professional for the Unlimited subscription", async ({
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-unpaid-subscription.json",
);
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await expect(page.getByTestId("subscription-name")).toHaveText(
"Professional plan",
);
});
test("When the subscription status is canceled, the sidebar dropdown displays the name Professional for the Enterprise subscription", async ({
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-enterprise-canceled-subscription.json",
);
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await expect(page.getByTestId("subscription-name")).toHaveText(
"Professional plan",
);
});
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamDashboard();
await expect(page.getByTestId("subscription-icon")).toBeVisible();
});
test.describe("Subscriptions: team members and invitations", () => {
test("Team settings has susbscription name and no manage subscription link when is member", async ({
test("The Unlimited subscription has its name in the sidebar dropdown", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in.json",
);
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-member.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-member.json",
);
await DashboardPage.mockRPC(
page,
"get-team-stats?team-id=*",
"dashboard/get-team-stats.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamSettingsSection();
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
await expect(
page.getByRole("button", { name: "Manage your subscription" }),
).not.toBeVisible();
});
test("Team settings has susbscription name and manage subscription link when is owner", async ({
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
"get-subscription-usage",
"subscription/get-subscription-usage-one-editor.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-team-stats?team-id=*",
"dashboard/get-team-stats.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamSettingsSection();
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
await expect(
page.getByRole("button", { name: "Manage your subscription" }),
).toBeVisible();
});
test("Members tab has warning message when user has more seats than editors.", async ({
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-eight-member.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamMembersSection();
const ctas = page.getByTestId("cta");
await expect(ctas).toHaveCount(2);
await expect(
page.getByText("Inviting people while on the unlimited plan"),
).toBeVisible();
});
test("Invitations tab has warning message when user has more seats than editors.", async ({
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-eight-member.json",
);
await DashboardPage.mockRPC(
page,
"get-team-invitations?team-id=*",
"subscription/get-team-invitations.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamInvitationsSection();
const ctas = page.getByTestId("cta");
await expect(ctas).toHaveCount(2);
await expect(
page.getByText("Inviting people while on the unlimited plan"),
).toBeVisible();
});
await expect(page.getByTestId("subscription-name")).toHaveText(
"Unlimited plan (trial)",
);
});
test("The sidebar dropdown displays the correct subscription name when status is Unpaid", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-unpaid-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await expect(page.getByTestId("subscription-name")).toHaveText(
"Professional plan",
);
});
test("The sidebar dropdown displays the correct subscription name when status is cancelled", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-enterprise-canceled-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToDashboard();
await expect(page.getByTestId("subscription-name")).toHaveText(
"Professional plan",
);
});
test("Team settings has susbscription name and no manage subscription link when is member", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-member.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-member.json",
);
await DashboardPage.mockRPC(
page,
"get-team-stats?team-id=*",
"dashboard/get-team-stats.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamSettingsSection();
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
await expect(
page.getByRole("button", { name: "Manage your subscription" }),
).not.toBeVisible();
});
test("Team settings has susbscription name and manage subscription link when is owner", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-team-stats?team-id=*",
"dashboard/get-team-stats.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamSettingsSection();
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
await expect(
page.getByRole("button", { name: "Manage your subscription" }),
).toBeVisible();
});
test("Members tab has warning message when user has more seats than editors", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-eight-member.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamMembersSection();
const ctas = page.getByTestId("cta");
await expect(ctas).toHaveCount(2);
await expect(
page.getByText("Inviting people while on the unlimited plan"),
).toBeVisible();
});
test("Invitations tab has warning message when user has more seats than editors", async ({
page,
}) => {
await DashboardPage.mockRPC(
page,
"get-profile",
"subscription/get-profile-unlimited-subscription.json",
);
await DashboardPage.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",
);
await DashboardPage.mockRPC(
page,
"get-team-info",
"subscription/get-team-info-subscriptions.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"subscription/get-teams-unlimited-subscription-owner.json",
);
await DashboardPage.mockRPC(
page,
"get-projects?team-id=*",
"dashboard/get-projects-second-team.json",
);
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"subscription/get-team-members-subscription-eight-member.json",
);
await DashboardPage.mockRPC(
page,
"get-team-invitations?team-id=*",
"subscription/get-team-invitations.json",
);
await dashboardPage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await dashboardPage.goToSecondTeamInvitationsSection();
const ctas = page.getByTestId("cta");
await expect(ctas).toHaveCount(2);
await expect(
page.getByText("Inviting people while on the unlimited plan"),
).toBeVisible();
});

View File

@@ -499,7 +499,7 @@ test.describe("Tokens: Tokens Tab", () => {
await valueField.fill("");
// TODO: We need to fix this translation
await expect(
tokensUpdateCreateModal.getByText("Empty field"),
tokensUpdateCreateModal.getByText("Token value cannot be empty"),
).toBeVisible();
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/);

View File

@@ -0,0 +1,6 @@
patchedDependencies:
'@zip.js/zip.js': patches/@zip.js__zip.js.patch
shamefullyHoist: true
recursiveInstall: true

View File

@@ -25,14 +25,14 @@
<link rel="icon" href="images/favicon.png" />
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
<script defer src="{{& config}}"></script>
<script defer src="{{& polyfills}}"></script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
{{/manifest}}
<script>
window.penpotTranslations = JSON.parse({{& translations}});
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
<script type="module">
globalThis.penpotTranslations = JSON.parse({{& translations}});
globalThis.penpotVersion = "%version%";
globalThis.penpotBuildDate = "%buildDate%";
</script>
<!--cookie-consent-->
</head>
@@ -44,9 +44,11 @@
<section id="modal"></section>
{{# manifest}}
<script defer src="js/libs.js?ts={{& ts}}"></script>
<script defer src="{{& shared}}"></script>
<script defer src="{{& main}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& main}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -19,9 +19,11 @@
</head>
<body>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& rasterizer}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& rasterizer}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -18,9 +18,11 @@
<body>
<div id="app"></div>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& render}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& render}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -193,26 +193,30 @@ async function readShadowManifest() {
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
config: "./js/config.js",
polyfills: "./js/polyfills.js",
worker_main: "./js/worker/main.js",
libs: "./js/libs.js",
};
for (let item of content) {
index[item.name] = "js/" + item["output-name"];
index[item.name] = "./js/" + item["output-name"] + "";
}
return index;
} catch (cause) {
return {
const index = {
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,
config: "./js/config.js",
polyfills: "./js/polyfills.js",
main: "./js/main.js",
shared: "./js/shared.js",
worker_main: "./js/worker/main.js",
rasterizer: "./js/rasterizer.js",
libs: "./js/libs.js",
};
return index;
}
}

View File

@@ -26,7 +26,10 @@ yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
mkdir -p resources/public;
mkdir -p target/dist;
yarn run build:app:main $EXTRA_PARAMS || exit 1
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
@@ -35,19 +38,18 @@ fi
yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
mkdir -p 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;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/index.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/render.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/rasterizer.html;
if [ "$INCLUDE_WASM" = "yes" ]; then
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./resources/public/js/render_wasm.js;
fi
rsync -avr resources/public/ target/dist/;
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook
yarn run build:storybook || exit 1;

View File

@@ -31,9 +31,9 @@ const rebuildNotify = {
const config = {
entryPoints: ["target/index.js"],
bundle: true,
format: "iife",
format: "esm",
banner: {
js: '"use strict"; var global = globalThis;',
js: '"use strict";\nvar global = globalThis;',
},
outfile: "resources/public/js/libs.js",
plugins: [fixReactVirtualized, rebuildNotify],

9
frontend/scripts/test Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -ex
corepack enable;
corepack install;
yarn install;
yarn run lint:scss;
yarn run test;

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -ex
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run build:storybook
exec 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"

8
frontend/scripts/test-e2e Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -ex
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list "$@";

View File

@@ -6,13 +6,12 @@
:builds
{:main
{:target :browser
{:target :esm
:output-dir "resources/public/js/"
:asset-path "/js"
:devtools {:watch-dir "resources/public"
:reload-strategy :full}
:build-options {:manifest-name "manifest.json"}
:module-loader true
:modules
{:shared
{:entries []}
@@ -20,7 +19,7 @@
:main
{:entries [app.main app.plugins.api]
:depends-on #{:shared}
:init-fn app.main/init}
:exports {init app.main/init}}
:util-highlight
{:entries [app.util.code-highlight]
@@ -50,12 +49,12 @@
:render
{:entries [app.render]
:depends-on #{:shared}
:init-fn app.render/init}
:exports {init app.render/init}}
:rasterizer
{:entries [app.rasterizer]
:depends-on #{:shared}
:init-fn app.rasterizer/init}}
:exports {init app.rasterizer/init}}}
:js-options
{:entry-keys ["module" "browser" "main"]
@@ -75,11 +74,10 @@
:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:output-wrapper true
:rename-prefix-namespace "PENPOT"
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
:cross-chunk-method-motion false
:source-map-detail-level :all}}}
:worker

View File

@@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.profile :refer [schema:profile]]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -484,7 +483,7 @@
(defn delete-access-token
[{:keys [id] :as params}]
(us/assert! ::us/uuid id)
(assert (uuid? id))
(ptk/reify ::delete-access-token
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -31,27 +31,28 @@
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.modules :as mod]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
(def auth-page
(mf/lazy-component app.main.ui.auth/auth))
(mf/lazy #(mod/load 'app.main.ui.auth/auth-page*)))
(def verify-token-page
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def verify-token-page*
(mf/lazy #(mod/load 'app.main.ui.auth.verify-token/verify-token-page*)))
(def viewer-page*
(mf/lazy-component app.main.ui.viewer/viewer*))
(mf/lazy #(mod/load 'app.main.ui.viewer/viewer-page*)))
(def dashboard-page*
(mf/lazy-component app.main.ui.dashboard/dashboard*))
(mf/lazy #(mod/load 'app.main.ui.dashboard/dashboard-page*)))
(def settings-page*
(mf/lazy-component app.main.ui.settings/settings*))
(mf/lazy #(mod/load 'app.main.ui.settings/settings-page*)))
(def workspace-page*
(mf/lazy-component app.main.ui.workspace/workspace*))
(mf/lazy #(mod/load 'app.main.ui.workspace/workspace-page*)))
(mf/defc workspace-legacy-redirect*
{::mf/props :obj
@@ -189,7 +190,7 @@
[:? [:& auth-page {:route route}]]
:auth-verify-token
[:? [:& verify-token-page {:route route}]]
[:? [:& verify-token-page* {:route route}]]
(:settings-profile
:settings-password

View File

@@ -19,8 +19,7 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc auth
{::mf/props :obj}
(mf/defc auth*
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
is-register (or
@@ -69,3 +68,9 @@
(when (= section :auth-register)
[:& terms-register])]]))
(mf/defc auth-page*
{::mf/lazy-load true}
[props]
[:> auth* props])

View File

@@ -61,9 +61,9 @@
(rt/nav :auth-login)
(ntf/warn (tr "errors.unexpected-token"))))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])
(mf/defc verify-token*
[{:keys [route]}]
(let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)]
(mf/with-effect []
@@ -99,3 +99,8 @@
[:> static/invalid-token {}]
[:> loader* {:title (tr "labels.loading")
:overlay true}])))
(mf/defc verify-token-page*
{::mf/lazy-load true}
[props]
[:> verify-token* props])

View File

@@ -8,24 +8,28 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.util.modules :as modules]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.lazy :as lazy]))
[rumext.v2 :as mf]))
(def highlight-fn
(lazy/loadable app.util.code-highlight/highlight!))
(delay (modules/load-fn 'app.util.code-highlight/highlight!)))
(mf/defc code-block
{::mf/wrap-props false}
[{:keys [code type]}]
(let [block-ref (mf/use-ref)
code (str/trim code)]
code (str/trim code)]
(mf/with-effect [code type]
(when-let [node (mf/ref-val block-ref)]
(p/let [highlight-fn (lazy/load highlight-fn)]
(highlight-fn node))))
(->> @highlight-fn
(p/fmap (fn [f] (f)))
(p/fnly (fn [f cause]
(if cause
(js/console.error cause)
(f node)))))))
[:pre {:class (dm/str type " " (stl/css :code-display)) :ref block-ref} code]))

View File

@@ -247,7 +247,6 @@
(swap! storage/session dissoc :template))))))
(mf/defc dashboard*
{::mf/props :obj}
[{:keys [profile project-id team-id search-term plugin-url template section]}]
(let [team (mf/deref refs/team)
projects (mf/deref refs/projects)
@@ -313,3 +312,8 @@
:section section
:search-term search-term
:team team}]]]))
(mf/defc dashboard-page*
{::mf/lazy-load true}
[props]
[:> dashboard* props])

View File

@@ -78,3 +78,9 @@
:settings-notifications
[:& notifications-page* {:profile profile}])]]]]))
(mf/defc settings-page*
{::mf/lazy-load true}
[props]
[:> settings* props])

View File

@@ -624,7 +624,6 @@
;; --- Component: Viewer
(mf/defc viewer*
{::mf/props :obj}
[{:keys [file-id share-id page-id] :as props}]
(mf/with-effect [file-id page-id share-id]
(let [params {:file-id file-id
@@ -643,3 +642,8 @@
[:> loader* {:title (tr "labels.loading")
:overlay true}]))
(mf/defc viewer-page*
{::mf/lazy-load true}
[props]
[:> viewer* props])

View File

@@ -165,7 +165,7 @@
(dsh/lookup-page state file-id page-id))))
st/state))
(mf/defc workspace-page*
(mf/defc workspace-inner*
{::mf/private true}
[{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id]
@@ -252,10 +252,16 @@
:touch-action "none"}}
[:> context-menu*]
(if (and file-loaded? page-id)
[:> workspace-page*
[:> workspace-inner*
{:page-id page-id
:file-id file-id
:file file
:wglobal wglobal
:layout layout}]
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*
{::mf/lazy-load true}
[props]
[:> workspace* props])

View File

@@ -6,7 +6,7 @@
(ns app.main.ui.workspace.shapes.text.editor
(:require
["draft-js" :as draft]
["@penpot/draft-js" :as draft]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]

View File

@@ -33,6 +33,7 @@
[app.util.object :as obj]
[app.util.text.content :as content]
[app.util.text.content.styles :as styles]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn get-contrast-color [background-color]
@@ -268,7 +269,12 @@
"bottom" "flex-end"
nil))
;;
(defn- font-family-from-font-id [font-id]
(if (str/includes? font-id "gfont-noto-sans")
(let [lang (str/replace font-id #"gfont\-noto\-sans\-" "")]
(if (>= (count lang) 3) (str/capital lang) (str/upper lang)))
"Noto Color Emoji"))
;; Text Editor Wrapper
;; This is an SVG element that wraps the HTML editor.
;;
@@ -281,6 +287,10 @@
(let [shape-id (dm/get-prop shape :id)
modifiers (dm/get-in modifiers [shape-id :modifiers])
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
fallback-families (map (fn [font]
(font-family-from-font-id (:font-id font))) fallback-fonts)
clip-id (dm/str "text-edition-clip" shape-id)
text-modifier-ref
@@ -317,7 +327,7 @@
max-height (max height selrect-height)
valign (-> shape :content :vertical-align)
y (:y selrect)
y (if (> height selrect-height)
y (if (and valign (> height selrect-height))
(case valign
"bottom" (- y (- height selrect-height))
"center" (- y (/ (- height selrect-height) 2))
@@ -341,7 +351,8 @@
render-wasm?
(obj/merge!
#js {"--editor-container-width" (dm/str width "px")
"--editor-container-height" (dm/str height "px")})
"--editor-container-height" (dm/str height "px")
"--fallback-families" (dm/str (str/join ", " fallback-families))})
(not render-wasm?)
(obj/merge!

View File

@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.thumbnails :as thc]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
@@ -38,7 +37,6 @@
[app.util.i18n :as i18n :refer [c tr]]
[app.util.strings :refer [matches-search]]
[app.util.timers :as ts]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -97,10 +95,6 @@
(str (str/slice (:path asset) (count path)))
(cpn/merge-path-item (:name asset))))
(s/def ::asset-name ::us/not-empty-string)
(s/def ::name-group-form
(s/keys :req-un [::asset-name]))
(def initial-context-menu-state
{:open? false :top nil :left nil})

View File

@@ -38,30 +38,18 @@
(features/use-feature "render-wasm/v1")
has-invalid-shapes?
(if render-wasm-enabled?
false
(some (fn [shape]
(or (cfh/frame-shape? shape)
(cfh/text-shape? shape)))
shapes-with-children))
(some (if render-wasm-enabled?
cfh/frame-shape?
#(or (cfh/frame-shape? %) (cfh/text-shape? %)))
shapes-with-children)
head-not-group-like?
(and (= 1 total-selected)
(not is-group?)
(not is-bool?))
disabled-bool-btns
(if render-wasm-enabled?
false
(or (zero? total-selected)
has-invalid-shapes?
head-not-group-like?))
disabled-flatten
(if render-wasm-enabled?
false
(or (zero? total-selected)
has-invalid-shapes?))
disabled-bool-btns (or (zero? total-selected) has-invalid-shapes? head-not-group-like?)
disabled-flatten (or (zero? total-selected) has-invalid-shapes?)
on-change
(mf/use-fn

View File

@@ -32,6 +32,12 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
@@ -44,7 +50,7 @@
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value ::sm/text]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]

View File

@@ -32,6 +32,12 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
@@ -44,7 +50,7 @@
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value ::sm/text]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]

View File

@@ -32,6 +32,12 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
@@ -44,7 +50,7 @@
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value ::sm/text]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]

View File

@@ -205,6 +205,7 @@
;; TODO: Review this value vs default-value
:value (or value "")
:hint-message (:message hint)
:variant "comfortable"
:slot-start swatch
:hint-type (:type hint)})

View File

@@ -83,6 +83,7 @@
(mf/spread-props props {:on-change on-change
:default-value value
:hint-message (:message hint)
:variant "comfortable"
:hint-type (:type hint)})
props

View File

@@ -32,6 +32,12 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
@@ -44,7 +50,7 @@
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value ::sm/text]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]

View File

@@ -7,7 +7,7 @@
(ns app.plugins.comments
(:require
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.comments :as dc]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.comments :as dwc]
@@ -118,7 +118,8 @@
(fn [position]
(let [position (parser/parse-point position)]
(cond
(or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position))))
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :position "Not valid point")
(not (r/check-permission plugin-id "comment:write"))

View File

@@ -7,7 +7,7 @@
(ns app.plugins.flex
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.transforms :as dwt]
@@ -133,7 +133,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -148,7 +148,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -163,7 +163,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -178,7 +178,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -194,7 +194,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -209,7 +209,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -224,7 +224,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -239,7 +239,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -296,7 +296,7 @@
:set
(fn [_ value]
(cond
(us/safe-int? value)
(sm/valid-safe-int? value)
(u/display-not-valid :zIndex value)
(not (r/check-permission plugin-id "content:write"))
@@ -359,7 +359,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :verticalMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -374,7 +374,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :horizontalMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -389,7 +389,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :topMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -404,7 +404,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :rightMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -419,7 +419,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :bottomMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -434,7 +434,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :leftMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -449,7 +449,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxWidth value)
(not (r/check-permission plugin-id "content:write"))
@@ -464,7 +464,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :minWidth value)
(not (r/check-permission plugin-id "content:write"))
@@ -479,7 +479,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxHeight value)
(not (r/check-permission plugin-id "content:write"))
@@ -494,7 +494,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :minHeight value)
(not (r/check-permission plugin-id "content:write"))

View File

@@ -7,7 +7,7 @@
(ns app.plugins.grid
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.transforms :as dwt]
@@ -126,7 +126,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -141,7 +141,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -156,7 +156,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -171,7 +171,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -186,7 +186,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -201,7 +201,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -216,7 +216,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -231,7 +231,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -248,7 +248,7 @@
(u/display-not-valid :addRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRow-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -261,14 +261,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :addRowAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addRowAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRowAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -285,7 +285,7 @@
(u/display-not-valid :addColumn-type type)
(and (or (= :percent type) (= :flex type) (= :lex type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -297,14 +297,14 @@
:addColumnAtIndex
(fn [index type value]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :addColumnAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addColumnAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumnAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -317,7 +317,7 @@
:removeRow
(fn [index]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeRow index)
(not (r/check-permission plugin-id "content:write"))
@@ -329,7 +329,7 @@
:removeColumn
(fn [index]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeColumn index)
(not (r/check-permission plugin-id "content:write"))
@@ -342,14 +342,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :setColumn-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setColumn-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -362,14 +362,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :setRow-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setRow-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -393,10 +393,10 @@
(not (shape-proxy? child))
(u/display-not-valid :appendChild-child child)
(or (< row 0) (not (us/safe-int? row)))
(or (< row 0) (not (sm/valid-safe-int? row)))
(u/display-not-valid :appendChild-row row)
(or (< column 0) (not (us/safe-int? column)))
(or (< column 0) (not (sm/valid-safe-int? column)))
(u/display-not-valid :appendChild-column column)
(not (r/check-permission plugin-id "content:write"))
@@ -431,7 +431,7 @@
(let [cell (locate-cell self)
shape (u/proxy->shape self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :row-value value)
(nil? cell)
@@ -451,7 +451,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowSpan-value value)
(nil? cell)
@@ -471,7 +471,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :column-value value)
(nil? cell)
@@ -491,7 +491,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnSpan-value value)
(nil? cell)

View File

@@ -10,7 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.color :as cc]
[app.common.uuid :as uuid]
[app.main.data.comments :as dc]
@@ -299,7 +299,7 @@
(fn [orientation value board]
(let [shape (u/proxy->shape board)]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :addRulerGuide "Value not a safe number")
(not (contains? #{"vertical" "horizontal"} orientation))
@@ -345,8 +345,8 @@
(or (not (string? content)) (empty? content))
(u/display-not-valid :addCommentThread "Content not valid")
(or (not (us/safe-number? (:x position)))
(not (us/safe-number? (:y position))))
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :addCommentThread "Position not valid")
(and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))

View File

@@ -8,7 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.workspace.guides :as dwgu]
[app.main.store :as st]
[app.plugins.format :as format]
@@ -77,7 +77,7 @@
:set
(fn [self value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :position "Not valid position")
(not (r/check-permission plugin-id "content:write"))

View File

@@ -14,7 +14,6 @@
[app.common.path-names :as cpn]
[app.common.record :as crc]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.svg.path :as svg.path]
[app.common.types.color :as clr]
[app.common.types.component :as ctk]
@@ -325,7 +324,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(or (not (us/safe-int? value)) (< value 0))
(or (not (sm/valid-safe-int? value)) (< value 0))
(u/display-not-valid :borderRadius value)
(not (r/check-permission plugin-id "content:write"))
@@ -341,7 +340,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusTopLeft value)
(not (r/check-permission plugin-id "content:write"))
@@ -357,7 +356,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusTopRight value)
(not (r/check-permission plugin-id "content:write"))
@@ -373,7 +372,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusBottomRight value)
(not (r/check-permission plugin-id "content:write"))
@@ -389,7 +388,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusBottomLeft value)
(not (r/check-permission plugin-id "content:write"))
@@ -405,7 +404,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(or (not (us/safe-number? value)) (< value 0) (> value 1))
(or (not (sm/valid-safe-number? value)) (< value 0) (> value 1))
(u/display-not-valid :opacity value)
(not (r/check-permission plugin-id "content:write"))
@@ -492,7 +491,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :x value)
(not (r/check-permission plugin-id "content:write"))
@@ -510,7 +509,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :y value)
(not (r/check-permission plugin-id "content:write"))
@@ -555,7 +554,7 @@
:set
(fn [self value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :parentX value)
(not (r/check-permission plugin-id "content:write"))
@@ -582,7 +581,7 @@
:set
(fn [self value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :parentY value)
(not (r/check-permission plugin-id "content:write"))
@@ -609,7 +608,7 @@
:set
(fn [self value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :frameX value)
(not (r/check-permission plugin-id "content:write"))
@@ -636,7 +635,7 @@
:set
(fn [self value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :frameY value)
(not (r/check-permission plugin-id "content:write"))
@@ -795,10 +794,10 @@
:resize
(fn [width height]
(cond
(or (not (us/safe-number? width)) (<= width 0))
(or (not (sm/valid-safe-number? width)) (<= width 0))
(u/display-not-valid :resize width)
(or (not (us/safe-number? height)) (<= height 0))
(or (not (sm/valid-safe-number? height)) (<= height 0))
(u/display-not-valid :resize height)
(not (r/check-permission plugin-id "content:write"))
@@ -1048,10 +1047,10 @@
(not (cfh/text-shape? shape))
(u/display-not-valid :getRange-shape "shape is not text")
(or (not (us/safe-int? start)) (< start 0) (> start end))
(or (not (sm/valid-safe-int? start)) (< start 0) (> start end))
(u/display-not-valid :getRange-start start)
(not (us/safe-int? end))
(not (sm/valid-safe-int? end))
(u/display-not-valid :getRange-end end)
:else
@@ -1078,7 +1077,7 @@
:setParentIndex
(fn [index]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :setParentIndex index)
(not (r/check-permission plugin-id "content:write"))
@@ -1230,7 +1229,7 @@
(fn [orientation value]
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :addRulerGuide "Value not a safe number")
(not (contains? #{"vertical" "horizontal"} orientation))

View File

@@ -7,7 +7,7 @@
(ns app.plugins.viewport
(:require
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.workspace.viewport :as dwv]
[app.main.data.workspace.zoom :as dwz]
[app.main.store :as st]
@@ -37,10 +37,10 @@
(let [new-x (obj/get value "x")
new-y (obj/get value "y")]
(cond
(not (us/safe-number? new-x))
(not (sm/valid-safe-number? new-x))
(u/display-not-valid :center-x new-x)
(not (us/safe-number? new-y))
(not (sm/valid-safe-number? new-y))
(u/display-not-valid :center-y new-y)
:else
@@ -62,7 +62,7 @@
:set
(fn [value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :zoom value)
:else

View File

@@ -125,47 +125,40 @@
[:embed {:optional true} :boolean]
[:skip-children {:optional true} :boolean]
[:object-id
[:or
::sm/uuid
::sm/coll-of-uuid]]])
[:or [::sm/set ::sm/uuid] ::sm/uuid]]])
(def ^:private render-objects-decoder
(sm/lazy-decoder schema:render-objects
sm/string-transformer))
(def ^:private render-objects-validator
(sm/lazy-validator schema:render-objects))
(def ^:private coerce-render-objects-params
(sm/coercer schema:render-objects))
(defn- render-objects
[params]
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params} (render-objects-decoder params)]
(if-not (render-objects-validator params)
(do
(js/console.error "invalid arguments")
(sm/pretty-explain schema:render-objects params)
nil)
(try
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
(coerce-render-objects-params params)]
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
(if (uuid? object-id)
(mf/html
[:& object-svg
{:file-id file-id
:page-id page-id
:share-id share-id
:object-id object-id
:embed embed
:skip-children skip-children}])
(do
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
(if (uuid? object-id)
(mf/html
[:& object-svg
{:file-id file-id
:page-id page-id
:share-id share-id
:object-id object-id
:embed embed
:skip-children skip-children}])
(mf/html
[:& objects-svg
{:file-id file-id
:page-id page-id
:share-id share-id
:object-ids (into #{} object-id)
:embed embed
:skip-children skip-children}]))))))
(mf/html
[:& objects-svg
{:file-id file-id
:page-id page-id
:share-id share-id
:object-ids (into #{} object-id)
:embed embed
:skip-children skip-children}])))
(catch :default cause
(when-let [explain (-> cause ex-data ::sm/explain)]
(js/console.log "Unexpected error")
(js/console.log (sm/humanize-explain explain)))
(mf/html [:span "Unexpected error:" (ex-message cause)]))))
;; ---- COMPONENTS SPRITE
@@ -242,39 +235,37 @@
[:embed {:optional true} :boolean]
[:component-id {:optional true} ::sm/uuid]])
(def ^:private render-components-decoder
(sm/lazy-decoder schema:render-components
sm/string-transformer))
(def ^:private render-components-validator
(sm/lazy-validator schema:render-components))
(def ^:private coerce-render-components-params
(sm/coercer schema:render-components))
(defn render-components
[params]
(let [{:keys [file-id component-id embed] :as params} (render-components-decoder params)]
(if-not (render-components-validator params)
(do
(js/console.error "invalid arguments")
(sm/pretty-explain schema:render-components params)
nil)
(try
(let [{:keys [file-id component-id embed] :as params}
(coerce-render-components-params params)]
(do
(st/emit! (ptk/reify ::initialize-render-components
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (fetch-team :file-id file-id))
(st/emit! (ptk/reify ::initialize-render-components
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (fetch-team :file-id file-id))
(->> stream
(rx/filter (ptk/type? ::team-fetched))
(rx/observe-on :async)
(rx/map (constantly params))
(rx/map fetch-components-bundle))))))
(->> stream
(rx/filter (ptk/type? ::team-fetched))
(rx/observe-on :async)
(rx/map (constantly params))
(rx/map fetch-components-bundle))))))
(mf/html
[:& components-svg
{:component-id component-id
:embed embed}])))))
(mf/html
[:& components-svg
{:component-id component-id
:embed embed}]))
(catch :default cause
(when-let [explain (-> cause ex-data ::sm/explain)]
(js/console.log "Unexpected error")
(js/console.log (sm/humanize-explain explain)))
(mf/html [:span "Unexpected error:" (ex-message cause)]))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETUP

View File

@@ -40,6 +40,7 @@
[app.util.debug :as dbg]
[app.util.functions :as fns]
[app.util.globals :as ug]
[app.util.modules :as mod]
[app.util.text.content :as tc]
[beicon.v2.core :as rx]
[promesa.core :as p]
@@ -475,9 +476,9 @@
(dissoc :style)
(merge style)
(select-keys allowed-keys))
fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
fill-rule (-> (or (:fill-rule attrs) (:fillRule attrs)) sr/translate-fill-rule)
stroke-linecap (-> (or (:stroke-linecap attrs) (:strokeLinecap attrs)) sr/translate-stroke-linecap)
stroke-linejoin (-> (or (:stroke-linejoin attrs) (:strokeLinejoin attrs)) sr/translate-stroke-linejoin)
fill-none (= "none" (-> attrs :fill))]
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
@@ -785,19 +786,10 @@
hidden)))
shadows))
(defn set-shape-text-content
"This function sets shape text content and returns a stream that loads the needed fonts asynchronously"
[shape-id content]
(h/call wasm/internal-module "_clear_shape_text")
(set-shape-vertical-align (get content :vertical-align))
(defn fonts-from-text-content [content fallback-fonts-only?]
(let [paragraph-set (first (get content :children))
paragraphs (get paragraph-set :children)
fonts (fonts/get-content-fonts content)
total (count paragraphs)]
(loop [index 0
emoji? false
langs #{}]
@@ -814,20 +806,36 @@
emoji? (if emoji? emoji? (t/contains-emoji? text))
langs (t/collect-used-languages langs text)]
(t/write-shape-text spans paragraph text)
;; FIXME: this should probably be somewhere else
(when fallback-fonts-only? (t/write-shape-text spans paragraph text))
(recur (inc index)
emoji?
langs))))
(let [updated-fonts
(-> fonts
(-> #{}
(cond-> ^boolean emoji? (f/add-emoji-font))
(f/add-noto-fonts langs))
result (f/store-fonts shape-id updated-fonts)]
fallback-fonts (filter #(get % :is-fallback) updated-fonts)]
(h/call wasm/internal-module "_update_shape_text_layout")
(if fallback-fonts-only? updated-fonts fallback-fonts))))))
result)))))
(defn set-shape-text-content
"This function sets shape text content and returns a stream that loads the needed fonts asynchronously"
[shape-id content]
(h/call wasm/internal-module "_clear_shape_text")
(set-shape-vertical-align (get content :vertical-align))
(let [fonts (fonts/get-content-fonts content)
fallback-fonts (fonts-from-text-content content true)
all-fonts (concat fonts fallback-fonts)
result (f/store-fonts shape-id all-fonts)]
(f/load-fallback-fonts-for-editor! fallback-fonts)
(h/call wasm/internal-module "_update_shape_text_layout")
result))
(defn set-shape-grow-type
[grow-type]
@@ -1354,7 +1362,7 @@
(delay
(if (exists? js/dynamicImport)
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(->> (js/dynamicImport (str uri))
(->> (mod/import uri)
(p/mcat init-wasm-module)
(p/fmap
(fn [default]

View File

@@ -266,6 +266,12 @@
(store-font-id shape-id font-data asset-id emoji? fallback?)))
;; FIXME: This is a temporary function to load the fallback fonts for the editor.
;; Once we render the editor content within wasm, we can remove this function.
(defn load-fallback-fonts-for-editor!
[fonts]
(doseq [font fonts]
(fonts/ensure-loaded! (:font-id font) (:font-variant-id font))))
(defn store-fonts
[shape-id fonts]
@@ -277,7 +283,8 @@
:font-variant-id "regular"
:style 0
:weight 400
:is-emoji true}))
:is-emoji true
:is-fallback true}))
(def noto-fonts
{:japanese {:font-id "gfont-noto-sans-jp" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}

View File

@@ -67,15 +67,17 @@ function filterAllowedTypes(options) {
* @param {string} type
* @returns {boolean}
*/
return function filter(type) {
function filter(type) {
if (
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
type === "text/html"
type === "text/html"
) {
return false;
}
return allowedTypes.includes(type);
};
return filter;
}
/**
@@ -85,19 +87,22 @@ function filterAllowedTypes(options) {
* @returns {Function<AllowedTypesFilterFunction>}
*/
function filterAllowedItems(options) {
/**
* @param {DataTransferItem}
* @returns {boolean}
*/
return function filter(item) {
function filter(item) {
if (
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
item.type === "text/html"
item.type === "text/html"
) {
return false;
}
return allowedTypes.includes(item.type);
};
return filter;
}
/**

View File

@@ -6,10 +6,11 @@
(ns app.util.code-highlight
(:require
["@penpot/hljs" :as hljs]
["highlight.js$default" :as hljs]
[app.util.dom :as dom]))
(defn highlight!
{:lazy-loadable true}
[node]
(dom/set-data! node "highlighted" nil)
(hljs/highlightElement node))

View File

@@ -20,7 +20,7 @@ goog.provide("app.util.globals");
goog.scope(function () {
var self = app.util.globals;
self.global = goog.global;
self.global = globalThis;
function createMockedEventEmitter(k) {
/* Allow mocked objects to be event emitters, so other modules

View File

@@ -92,8 +92,9 @@
is executed in the critical part (application bootstrap) and used in
many parts of the application."
[data]
(reset! locale (or (get storage/global ::locale) (autodetect)))
(set! translations data))
(set! translations data)
(reset! locale (or (get storage/global ::locale) (autodetect))))
(defn set-locale!
[lname]

View File

@@ -0,0 +1,17 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.modules
(:refer-clojure :exclude [load resolve]))
(defmacro load
[thing]
`(-> (shadow.esm/load-by-name ~thing)
(.then (fn [f#] (cljs.core/js-obj "default" (f#))))))
(defmacro load-fn
[thing]
`(shadow.esm/load-by-name ~thing))

View File

@@ -0,0 +1,16 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.modules
(:refer-clojure :exclude [import])
(:require-macros [app.util.modules])
(:require
[shadow.esm :refer [dynamic-import]]))
(defn import
"Dynamic esm module import import"
[path]
(dynamic-import (str path)))

View File

@@ -38,7 +38,8 @@
(str v "px")
(and (= k :font-family) (seq v))
(str/quote v)
;; pick just first family, avoid quoting twice, and add var(--fallback-families)
(str/concat (str/quote (str/unquote (first (str/split v ",")))) ", var(--fallback-families)")
:else
v))
@@ -53,7 +54,7 @@
(str/slice v 0 -2)
(= k :font-family)
(str/unquote v)
(str/unquote (str/replace v ", var(--fallback-families)" ""))
:else
v))

View File

@@ -101,8 +101,8 @@
(rx/push! bus message))))
handle-error
(fn [error]
(on-error worker (.-data error)))]
(fn [event]
(on-error worker (.-data event)))]
(.addEventListener instance "message" handle-message)
(.addEventListener instance "error" handle-error)

View File

@@ -20,12 +20,12 @@
[app.render-wasm.api :as wasm.api]
[app.render-wasm.wasm :as wasm]
[app.util.http :as http]
[app.util.modules :as mod]
[app.worker.impl :as impl]
[beicon.v2.core :as rx]
[okulary.core :as l]
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.esm :refer (dynamic-import)]))
[rumext.v2 :as mf]))
(log/set-level! :trace)
@@ -101,7 +101,7 @@
(def init-wasm
(delay
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(-> (dynamic-import (str uri))
(-> (mod/import (str uri))
(p/then #(wasm.api/init-wasm-module %))
(p/then #(set! wasm/internal-module %))))))

View File

@@ -13,6 +13,7 @@ import {
isLikeParagraph,
} from "./Paragraph.js";
import { isDisplayBlock, normalizeStyles } from "./Style.js";
import { sanitizeFontFamily } from "./Style.js";
const DEFAULT_FONT_SIZE = "14px";
const DEFAULT_FONT_WEIGHT = 400;
@@ -87,16 +88,16 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE,
);
}
const fontFamily = textSpan.style.getPropertyValue("font-family");
let fontFamily = textSpan.style.getPropertyValue("font-family");
if (!fontFamily) {
console.warn("font-family", fontFamily);
const fontFamilyValue =
fontFamily =
styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY;
const quotedFontFamily = fontFamilyValue.startsWith('"')
? fontFamilyValue
: `"${fontFamilyValue}"`;
textSpan.style.setProperty("font-family", quotedFontFamily);
}
fontFamily = sanitizeFontFamily(fontFamily);
textSpan.style.setProperty("font-family", fontFamily);
const fontWeight = textSpan.style.getPropertyValue("font-weight");
if (!fontWeight) {
console.warn("font-weight", fontWeight);
@@ -144,18 +145,29 @@ export function htmlToText(html) {
tmp.innerHTML = html;
const blockTags = [
"P", "DIV", "SECTION", "ARTICLE", "HEADER", "FOOTER",
"UL", "OL", "LI", "TABLE", "TR", "TD", "TH", "PRE"
"P",
"DIV",
"SECTION",
"ARTICLE",
"HEADER",
"FOOTER",
"UL",
"OL",
"LI",
"TABLE",
"TR",
"TD",
"TH",
"PRE",
];
function walk(node) {
let text = "";
node.childNodes.forEach(child => {
node.childNodes.forEach((child) => {
if (child.nodeType === Node.TEXT_NODE) {
text += child.textContent;
} else if (child.nodeType === Node.ELEMENT_NODE) {
if (child.tagName === "BR") {
text += "\n";
}
@@ -178,7 +190,6 @@ export function htmlToText(html) {
return result.trim();
}
/**
* Maps any HTML into a valid content DOM element.
*
@@ -187,10 +198,14 @@ export function htmlToText(html) {
* @param {boolean} [allowHTMLPaste=false]
* @returns {DocumentFragment}
*/
export function mapContentFragmentFromHTML(html, styleDefaults, allowHTMLPaste) {
export function mapContentFragmentFromHTML(
html,
styleDefaults,
allowHTMLPaste,
) {
if (allowHTMLPaste) {
try {
const parser = new DOMParser()
const parser = new DOMParser();
const document = parser.parseFromString(html, "text/html");
return mapContentFragmentFromDocument(document, styleDefaults);
} catch (error) {

View File

@@ -19,10 +19,31 @@ const DEFAULT_FONT_WEIGHT = "400";
* @param {string} value
*/
export function sanitizeFontFamily(value) {
if (value && value.length > 0 && !value.startsWith('"')) {
return `"${value}"`;
} else {
// NOTE: This is a fix for a bug introduced earlier that have might modified the font-family in the model
// adding extra double quotes.
if (value && value.startsWith('""')) {
//remove the first and last quotes
value = value.slice(1).replace(/"([^"]*)$/, "$1");
// remove quotes from font-family in 1-word font-families
// and repeated values
value = [
...new Set(
value
.split(", ")
.map((x) => (x.includes(" ") ? x : x.replace(/"/g, ""))),
),
].join(", ");
}
if (!value || value === "") {
return "var(--fallback-families)";
} else if (value.endsWith(" var(--fallback-families)")) {
return value;
} else if (value.startsWith('"')) {
return `${value}, var(--fallback-families)`;
} else {
return `"${value}", var(--fallback-families)`;
}
}

View File

@@ -1,5 +0,0 @@
import h from "highlight.js";
export function highlightElement(node) {
return h.highlightElement(node);
}

View File

@@ -1,13 +0,0 @@
{
"name": "@penpot/hljs",
"version": "1.0.0",
"description": "Penpot Hightlight.js ESM wrapper",
"main": "index.js",
"packageManager": "yarn@4.3.1",
"author": "Andrey Antukh",
"license": "MPL-2.0",
"type": "module",
"dependencies": {
"highlight.js": "^11.10.0"
}
}

View File

@@ -1,21 +0,0 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"@penpot/hljs@workspace:.":
version: 0.0.0-use.local
resolution: "@penpot/hljs@workspace:."
dependencies:
highlight.js: "npm:^11.10.0"
languageName: unknown
linkType: soft
"highlight.js@npm:^11.10.0":
version: 11.10.0
resolution: "highlight.js@npm:11.10.0"
checksum: 10c0/cd8bf7ef06cbd72ddb83580ecabe769f08f062be8bb82d2eb492d31c17f7480d1f8d14a66fc81deee0601645435f19f04c470510563f847242a41ccff0ab873e
languageName: node
linkType: hard

View File

@@ -1,4 +0,0 @@
(ns tubax.saxjs
(:require ["sax" :as sax]))
(goog/exportSymbol "sax" sax)

View File

File diff suppressed because it is too large Load Diff

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