Compare commits

..

30 Commits

Author SHA1 Message Date
Alejandro Alonso
879caf66eb Merge pull request #7493 from penpot/niwinz-staging-hotfix-2
🐛 Fix corrner case on error report validation
2025-10-15 09:51:52 +02:00
Andrey Antukh
4daf086214 📎 Backport circleci config from develop 2025-10-15 09:33:42 +02:00
Andrey Antukh
c8b3a41117 📎 Backport all github workflows from develop 2025-10-15 09:33:42 +02:00
Andrey Antukh
c9dcc8a4ee 🐛 Add migration for clearing :objects nil from components
on the local library
2025-10-15 09:33:40 +02:00
Andrey Antukh
17376dfa3f Add mark-file-as-trimmed srepl helper 2025-10-14 19:04:02 +02:00
Andrey Antukh
8d65e1cc94 Simplify reset-password srepl helper 2025-10-14 19:04:02 +02:00
Andrey Antukh
d4de367499 🐛 Fix ::sm/set schema validation
It has several corner cases where set specific type
is not checked. It also now checks for ordered type
specifically when ordered is specified
2025-10-14 19:04:02 +02:00
Andrey Antukh
25521b18ff Make the restriction errors report as warning to logging 2025-10-14 19:04:02 +02:00
Andrey Antukh
39bdf026ca 🐛 Fix corrner case on error report validationt 2025-10-14 19:03:59 +02:00
Andrey Antukh
1b6a833166 🐛 Add migration for fix fills on position-data 2025-10-14 18:11:42 +02:00
Andrey Antukh
928dcf5cb8 🐛 Fix migrations set decoding on binfile import
that causes incorrect migration ordering on the table
2025-10-14 18:11:42 +02:00
Andrey Antukh
b3ae54775b 📎 Update changelog 2025-10-14 12:26:56 +02:00
Andrey Antukh
11ff64b362 Merge tag '2.10.1-RC3' 2025-10-06 12:12:27 +02:00
Alonso Torres
d5b743c604 🐛 Fix problem with text in plugins (#7446) 2025-10-06 11:19:33 +02:00
Andrey Antukh
e38dd21307 ⬆️ Update exported dependencies 2025-10-06 10:56:04 +02:00
Andrey Antukh
8c91109c63 📚 Update changelog 2025-10-06 09:38:01 +02:00
Andrey Antukh
c3eabbdb25 📎 Enable the fdata/objects-map feature by default
Using config flags
2025-10-06 09:38:01 +02:00
Andrey Antukh
67661674e2 Make deleted fonts fixer to run with more granular stragegy
Instead of running it on all the file, only run it to local library
and the current page, reducing considerably the overhead of analyzing
the whole file on each file load.

It stills executes for page each time the page is loaded, and add
some kind of local cache for not doing repeated work each time page
loads is pending to be implemented in other commit.
2025-10-06 09:38:01 +02:00
Andrey Antukh
c70e7f3876 Add logging to frontend repo namespace 2025-10-06 09:38:01 +02:00
Andrey Antukh
0295f0f7c8 Add better workspace file indexing strategy
Improve file indexes initialization on workspace.

Instead of initialize indexes for all pages only initialize
indexes for the loaded page.
2025-10-06 09:38:01 +02:00
Andrey Antukh
54bb879cb6 📎 Add several missing imports on repl related namespaces 2025-10-06 09:38:01 +02:00
Andrey Antukh
b72704e54b Set explicit no-buffering for sse responses 2025-10-06 09:19:48 +02:00
Yamila Moreno
6c6ec7a620 Merge pull request #7433 from penpot/yms-improve-self-host-documentation
📚 Improve self-host documentation
2025-10-03 13:24:37 +02:00
Yamila Moreno
21a7d30c5e 📚 Improve self-host documentation 2025-10-03 13:21:06 +02:00
Andrey Antukh
b90aba0f95 Merge tag '2.10.0' 2025-10-02 12:37:58 +02:00
andrés gonzález
d1607fbe54 💄 Update Help Center images (#7266) 2025-09-29 11:54:47 +02:00
andrés gonzález
cce1dd86a2 💄 Change variants video source to peertube (#7387) 2025-09-26 10:21:41 +02:00
andrés gonzález
58c6c94cb8 📚 Update boards info at the user guide (#7383) 2025-09-25 16:36:35 +02:00
andrés gonzález
ecee7ecfc7 📚 Update workspace info at the user guide (#7376) 2025-09-25 12:24:59 +02:00
andrés gonzález
b770145436 💄 Update variants video at the user guide (#7363) 2025-09-24 13:41:20 +02:00
618 changed files with 11754 additions and 44055 deletions

View File

@@ -13,6 +13,7 @@
- [ ] Add a detailed explanation of how to reproduce the issue and/or verify the fix, if applicable.
- [ ] Include screenshots or videos, if applicable.
- [ ] Add or modify existing integration tests in case of bugs or new features, if applicable.
- [ ] Refactor any modified SCSS files following the refactor guide.
- [ ] Check CI passes successfully.
- [ ] Update the `CHANGES.md` file, referencing the related GitHub issue, if applicable.

View File

@@ -21,10 +21,10 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
# 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
# uses: ./.github/workflows/release.yml
# secrets: inherit
# with:
# gh_ref: ${{ github.ref_name }}
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
uses: ./.github/workflows/release.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}

View File

@@ -36,39 +36,39 @@ jobs:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# # --- Publicly release the docker images ---
# - name: Login to private registry
# uses: docker/login-action@v3
# with:
# registry: ${{ secrets.DOCKER_REGISTRY }}
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
# --- Publicly release the docker images ---
- name: Login to private registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# - name: Login to DockerHub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.PUB_DOCKER_USERNAME }}
# password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# - name: Publish docker images to DockerHub
# env:
# TAG: ${{ steps.vars.outputs.gh_ref }}
# REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
# HUB: ${{ secrets.PUB_DOCKER_HUB }}
# run: |
# IMAGES=("frontend" "backend" "exporter")
# EXTRA_TAGS=("main" "latest")
- name: Publish docker images to DockerHub
env:
TAG: ${{ steps.vars.outputs.gh_ref }}
REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
HUB: ${{ secrets.PUB_DOCKER_HUB }}
run: |
IMAGES=("frontend" "backend" "exporter")
EXTRA_TAGS=("main" "latest")
# for image in "${IMAGES[@]}"; do
# docker pull "$REGISTRY/penpotapp/$image:$TAG"
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
# docker push "penpotapp/$image:$TAG"
for image in "${IMAGES[@]}"; do
docker pull "$REGISTRY/penpotapp/$image:$TAG"
docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
docker push "penpotapp/$image:$TAG"
# for tag in "${EXTRA_TAGS[@]}"; do
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
# docker push "penpotapp/$image:$tag"
# done
# done
for tag in "${EXTRA_TAGS[@]}"; do
docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
docker push "penpotapp/$image:$tag"
done
done
# --- Release notes extraction ---
- name: Extract release notes from CHANGES.md

View File

@@ -1,49 +1,16 @@
# CHANGELOG
## 2.11.0 (Unreleased)
### :rocket: Epics and highlights
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`
- The `PENPOT_STORAGE_ASSETS_S3_BUCKET` becomes `PENPOT_OBJECTS_STORAGE_S3_BUCKET`
- The `PENPOT_STORAGE_ASSETS_S3_REGION` becomes `PENPOT_OBJECTS_STORAGE_S3_REGION`
- The `PENPOT_STORAGE_ASSETS_S3_ENDPOINT` becomes `PENPOT_OBJECTS_STORAGE_S3_ENDPOINT`
- The `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` replaced (see below)
- Add `PENPOT_NETTY_IO_THREADS` and `PENPOT_EXECUTOR_THREADS` variables to provide the
control over concurrency of the shared resources used by netty. Penpot uses the netty IO
threads for AWS S3 SDK and Redis/Valkey communication, and the EXEC threads to perform
out of HTTP serving threads tasks such that cache invalidation, S3 response completion,
configuration reloading and many other auxiliar tasks. By default they use a half number
if available cpus with a minumum of 2 for both executors. You should not touch that
variables unless you are know what you are doing.
- Replace the `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` with a more general configuration
`PENPOT_NETTY_IO_THREADS` used to configure a shared netty resources across different
services which use netty internally (redis connection, S3 SDK client). This
configuration is not very commonly used so don't expected real impact on any user.
### :heart: Community contributions (Thank you!)
## 2.10.1
### :sparkles: New features & Enhancements
- Show current Penpot version [Taiga #11603](https://tree.taiga.io/project/penpot/us/11603)
- Switch several variant copies at the same time [Taiga #11411](https://tree.taiga.io/project/penpot/us/11411)
- Invitations management improvements [Taiga #3479](https://tree.taiga.io/project/penpot/us/3479)
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
### :bug: Bugs fixed
- Fix selection problems when devtools open [Taiga #11950](https://tree.taiga.io/project/penpot/issue/11950)
- Fix long font names overlap [Taiga #11844](https://tree.taiga.io/project/penpot/issue/11844)
- Fix paste behavior according to the selected element [Taiga #11979](https://tree.taiga.io/project/penpot/issue/11979)
- Fix problem with export size [Github #7160](https://github.com/penpot/penpot/issues/7160)
- Fix multi level library dependencies [Taiga #12155](https://tree.taiga.io/project/penpot/issue/12155)
- Fix component context menu options order in assets tab [Taiga #11941](https://tree.taiga.io/project/penpot/issue/11941)
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
## 2.10.0
@@ -80,7 +47,6 @@
- Retrieve variants with nested components [Taiga #10277](https://tree.taiga.io/project/penpot/us/10277)
- Create variants in bulk from existing components [Taiga #7926](https://tree.taiga.io/project/penpot/us/7926)
- Alternative ways of creating variants - Button Design Tab [Taiga #10316](https://tree.taiga.io/project/penpot/us/10316)
- Fix problem with component swapping panel [Taiga #12175](https://tree.taiga.io/project/penpot/issue/12175)
### :bug: Bugs fixed
@@ -94,7 +60,7 @@
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
- Fix misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
- Misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
- Fix pasting RTF text crashes penpot [Taiga #11717](https://tree.taiga.io/project/penpot/issue/11717)
- Fix navigation arrows in Libraries & Templates carousel [Taiga #10609](https://tree.taiga.io/project/penpot/issue/10609)
- Fix applying tokens with zero value to size [Taiga #11618](https://tree.taiga.io/project/penpot/issue/11618)
@@ -189,7 +155,7 @@
**Penpot Library**
The initial prototype is completly reworked to provide a more consistent API
The initial prototype is completly reworked for provide a more consistent API
and to have proper validation and params decoding. All the details can be found
on [its own changelog](library/CHANGES.md)

View File

@@ -77,14 +77,17 @@ Provide your team or organization with a completely owned collaborative design t
### Integrations ###
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
### Building Design Systems: design tokens, components and variants ###
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
### Whats great for design ###
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
### Design Tokens ###
With Penpots standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
<br />
<p align="center">
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
</p>
<br />

View File

@@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.8.1.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.7.0.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -28,30 +28,29 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.6"
:git/sha "94dc017"
{:git/tag "v11.4"
:git/sha "ce50d42"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1070"}
{:mvn/version "1.3.1002"}
metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.4.0"}
nrepl/nrepl {:mvn/version "1.3.1"}
org.postgresql/postgresql {:mvn/version "42.7.7"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.jsoup/jsoup {:mvn/version "1.20.1"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -61,12 +60,12 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
software.amazon.awssdk/s3 {:mvn/version "2.33.8"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -81,14 +80,12 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
:ns-default build}
:test
{:main-opts ["-m" "kaocha.runner"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
"--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:outdated

View File

@@ -1 +1 @@
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}
Invitation to join {{team}}

View File

@@ -31,7 +31,8 @@ export PENPOT_FLAGS="\
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
enable-subscriptions \
disable-subscriptions-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
@@ -77,14 +78,10 @@ export JAVA_OPTS="\
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:+UseShenandoahGC \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockExperimentalVMOptions \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
-XX:ShenandoahGCMode=generational \
-XX:+UseCompactObjectHeaders \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";

View File

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

View File

@@ -34,7 +34,8 @@
[clojure.set :as set]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io]))
[datoteka.io :as io]
[promesa.exec :as px]))
(set! *warn-on-reflection* true)
@@ -475,7 +476,7 @@
(vary-meta dissoc ::fmg/migrated))))
(defn encode-file
[cfg {:keys [id features] :as file}]
[{:keys [::wrk/executor] :as cfg} {:keys [id features] :as file}]
(let [file (if (and (contains? features "fdata/objects-map")
(:data file))
(fdata/enable-objects-map file)
@@ -492,7 +493,7 @@
(-> file
(d/update-when :features into-array)
(d/update-when :data blob/encode))))
(d/update-when :data (fn [data] (px/invoke! executor #(blob/encode data)))))))
(defn- file->params
[file]

View File

@@ -96,7 +96,7 @@
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:http-server-worker-threads {:optional true} ::sm/int]
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
@@ -214,21 +214,20 @@
[:media-uri {:optional true} :string]
[:assets-path {:optional true} :string]
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
;; DEPRECATED
;; Legacy, will be removed in 2.5
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]
[:storage-assets-s3-bucket {:optional true} :string]
[:storage-assets-s3-region {:optional true} :keyword]
[:storage-assets-s3-endpoint {:optional true} ::sm/uri]
[:storage-assets-s3-io-threads {:optional true} ::sm/int]
[:objects-storage-backend {:optional true} :keyword]
[:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(defn- parse-flags
[config]

View File

@@ -19,7 +19,9 @@
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.objects-map :as omap.legacy]
[app.util.pointer-map :as pmap]))
[app.util.pointer-map :as pmap]
[app.worker :as wrk]
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OFFLOAD
@@ -95,10 +97,10 @@
(assoc file :data data)))
(defn decode-file-data
[_system {:keys [data] :as file}]
[{:keys [::wrk/executor]} {:keys [data] :as file}]
(cond-> file
(bytes? data)
(assoc :data (blob/decode data))))
(assoc :data (px/invoke! executor #(blob/decode data)))))
(defn load-pointer
"A database loader pointer helper"

View File

@@ -26,7 +26,9 @@
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.worker :as wrk]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
@@ -53,8 +55,6 @@
[:map
[::port ::sm/int]
[::host ::sm/text]
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
@@ -65,41 +65,31 @@
(assert (sm/check schema:server-params params)))
(defmethod ig/init-key ::server
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :as cfg}]
(l/info :hint "starting http server" :port port :host host)
(let [on-dispatch
(fn [_ start-at-ns]
(let [timing (- (System/nanoTime) start-at-ns)
timing (int (/ timing 1000000))]
(mtx/run! metrics
:id :http-server-dispatch-timing
:val timing)))
(let [options {:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/dispatch executor
:ring/compat :ring2
:socket/backlog 4069}
options
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)
:ring/compat :ring2
:events/on-dispatch on-dispatch
:socket/backlog 4069}
handler (cond
(some? router)
(router-handler router)
handler
(cond
(some? router)
(router-handler router)
(some? handler)
handler
(some? handler)
handler
:else
(throw (UnsupportedOperationException. "handler or router are required")))
:else
(throw (UnsupportedOperationException. "handler or router are required")))
server
(yt/server handler (d/without-nils options))]
options (d/without-nils options)
server (yt/server handler options)]
(assoc cfg ::server (yt/start! server))))

View File

@@ -17,9 +17,11 @@
[app.main :as-alias main]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.data.json :as j]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
@@ -38,8 +40,8 @@
[_ cfg]
(letfn [(handler [request]
(let [data (-> request yreq/body slurp)]
(handle-request cfg data)
{::yres/status 200}))]
(px/run! :vthread (partial handle-request cfg data)))
{::yres/status 200})]
["/sns" {:handler handler
:allowed-methods #{:post}}]))

View File

@@ -61,8 +61,7 @@
::yres/body data}
(binding [l/*context* (request->context request)]
(l/err :hint "restriction error"
:cause err)
(l/wrn :hint "restriction error" :cause err)
{::yres/status 400
::yres/body data}))))

View File

@@ -44,7 +44,8 @@
(def default-headers
{"Content-Type" "text/event-stream;charset=UTF-8"
"Cache-Control" "no-cache, no-store, max-age=0, must-revalidate"
"Pragma" "no-cache"})
"Pragma" "no-cache"
"X-Accel-Buffering" "no"})
(defn response
[handler & {:keys [buf] :or {buf 32} :as opts}]
@@ -54,7 +55,7 @@
::yres/body (yres/stream-body
(fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/spawn-listener
listener (events/start-listener
channel
(partial write! output)
(partial pu/close! output))]

View File

@@ -42,7 +42,6 @@
[app.svgo :as-alias svgo]
[app.util.cron]
[app.worker :as-alias wrk]
[app.worker.executor]
[clojure.test :as test]
[clojure.tools.namespace.repl :as repl]
[cuerdas.core :as str]
@@ -149,11 +148,23 @@
::mdef/labels []
::mdef/type :histogram}
:http-server-dispatch-timing
{::mdef/name "penpot_http_server_dispatch_timing"
::mdef/help "Histogram of dispatch handler"
::mdef/labels []
::mdef/type :histogram}})
:executors-active-threads
{::mdef/name "penpot_executors_active_threads"
::mdef/help "Current number of threads available in the executor service."
::mdef/labels ["name"]
::mdef/type :gauge}
:executors-completed-tasks
{::mdef/name "penpot_executors_completed_tasks_total"
::mdef/help "Approximate number of completed tasks by the executor."
::mdef/labels ["name"]
::mdef/type :counter}
:executors-running-threads
{::mdef/name "penpot_executors_running_threads"
::mdef/help "Current number of threads with state RUNNING."
::mdef/labels ["name"]
::mdef/type :gauge}})
(def system-config
{::db/pool
@@ -165,12 +176,14 @@
::db/max-size (cf/get :database-max-pool-size 60)
::mtx/metrics (ig/ref ::mtx/metrics)}
;; Default netty IO pool (shared between several services)
::wrk/netty-io-executor
{:threads (cf/get :netty-io-threads)}
;; Default thread pool for IO operations
::wrk/executor
{}
::wrk/netty-executor
{:threads (cf/get :executor-threads)}
::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)
::wrk/name "default"}
:app.migrations/migrations
{::db/pool (ig/ref ::db/pool)}
@@ -184,19 +197,14 @@
::rds/redis
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/netty-executor
(ig/ref ::wrk/netty-executor)
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
::wrk/executor (ig/ref ::wrk/executor)}
::mbus/msgbus
{::wrk/executor (ig/ref ::wrk/netty-executor)
{::wrk/executor (ig/ref ::wrk/executor)
::rds/redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/netty-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
::sto.gc-deleted/handler
{::db/pool (ig/ref ::db/pool)
@@ -224,10 +232,9 @@
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::mtx/metrics (ig/ref ::mtx/metrics)}
::wrk/executor (ig/ref ::wrk/executor)}
::ldap/provider
{:host (cf/get :ldap-host)
@@ -305,17 +312,17 @@
::rpc/climit
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/netty-executor)
::wrk/executor (ig/ref ::wrk/executor)
::climit/config (cf/get :rpc-climit-config)
::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/netty-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/netty-executor)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
::sto/storage (ig/ref ::sto/storage)
@@ -469,14 +476,13 @@
(cf/get :objects-storage-s3-bucket))
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
(cf/get :objects-storage-s3-io-threads))
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
::wrk/executor (ig/ref ::wrk/executor)}
:app.storage.fs/backend
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
(cf/get :objects-storage-fs-directory))}})
(def worker-config
{::wrk/cron
{::wrk/registry (ig/ref ::wrk/registry)

View File

@@ -216,7 +216,8 @@
(rds/add-listener sconn (create-listener rcv-ch))
(px/thread
{:name "penpot/msgbus"}
{:name "penpot/msgbus/io-loop"
:virtual true}
(try
(loop []
(let [timeout-ch (sp/timeout-chan 1000)

View File

@@ -21,7 +21,8 @@
[clojure.java.io :as io]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p])
[promesa.core :as p]
[promesa.exec :as px])
(:import
clojure.lang.MapEntry
io.lettuce.core.KeyValue
@@ -44,10 +45,8 @@
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.HashedWheelTimer
io.netty.util.Timer
io.netty.util.concurrent.EventExecutorGroup
java.lang.AutoCloseable
java.time.Duration))
@@ -112,15 +111,20 @@
(defmethod ig/expand-key ::redis
[k v]
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s")))})
(let [cpus (px/get-available-processors)
threads (max 1 (int (* cpus 0.2)))]
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s"))
(assoc ::io-threads (max 3 threads))
(assoc ::worker-threads (max 3 threads)))}))
(def ^:private schema:redis-params
[:map {:title "redis-params"}
::wrk/netty-io-executor
::wrk/netty-executor
::wrk/executor
::mtx/metrics
[::uri ::sm/uri]
[::worker-threads ::sm/int]
[::io-threads ::sm/int]
[::timeout ::ct/duration]])
(defmethod ig/assert-key ::redis
@@ -137,30 +141,17 @@
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [::uri ::mtx/metrics ::wrk/netty-io-executor ::wrk/netty-executor] :as params}]
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
(l/inf :hint "initialize redis resources"
:uri (str uri))
:uri (str uri)
:io-threads io-threads
:worker-threads worker-threads)
(let [timer (HashedWheelTimer.)
resources (.. (DefaultClientResources/builder)
(eventExecutorGroup ^EventExecutorGroup netty-executor)
;; We provide lettuce with a shared event loop
;; group instance instead of letting lettuce to
;; create its own
(eventLoopGroupProvider
(reify io.lettuce.core.resource.EventLoopGroupProvider
(allocate [_ _] netty-io-executor)
(threadPoolSize [_]
(.executorCount ^NioEventLoopGroup netty-io-executor))
(release [_ _ _ _ _]
;; Do nothing
)
(shutdown [_ _ _ _]
;; Do nothing
)))
(ioThreadPoolSize ^long io-threads)
(computationThreadPoolSize ^long worker-threads)
(timer ^Timer timer)
(build))
@@ -175,7 +166,7 @@
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))
cache (cache/create :executor netty-executor
cache (cache/create :executor executor
:on-remove on-remove
:keepalive "5m")]
(reify

View File

@@ -21,6 +21,7 @@
[clojure.set :as set]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.exec :as px]
[promesa.exec.bulkhead :as pbh])
(:import
clojure.lang.ExceptionInfo
@@ -288,9 +289,13 @@
(get-limits cfg)))
(defn invoke!
"Run a function in context of climit."
[{:keys [::rpc/climit] :as cfg} f params]
"Run a function in context of climit.
Intended to be used in virtual threads."
[{:keys [::executor ::rpc/climit] :as cfg} f params]
(let [f (if climit
(build-exec-chain cfg f)
(let [f (if (some? executor)
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
f)]
(build-exec-chain cfg f))
f)]
(f cfg params)))

View File

@@ -6,7 +6,6 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -63,7 +62,7 @@
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password")
(let [result (auth/verify-password password (:password profile))]
(let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trc :hint "updating profile password"
:id (str (:id profile))
@@ -157,7 +156,7 @@
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (auth/derive-password password)]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
nil))]
@@ -379,7 +378,7 @@
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password auth/derive-password))
(update :password #(profile/derive-password cfg %)))
profile (->> (create-profile! conn params)
(create-profile-rels! conn))]
(vary-meta profile assoc :created true))))

View File

@@ -28,6 +28,7 @@
[app.tasks.file-gc]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]
[yetti.response :as yres]))
(set! *warn-on-reflection* true)
@@ -93,7 +94,7 @@
;; --- Command: import-binfile
(defn- import-binfile
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
(let [team (teams/get-team pool
:profile-id profile-id
:project-id project-id)
@@ -104,9 +105,13 @@
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
result (case (int version)
1 (bf.v1/import-files! cfg)
3 (bf.v3/import-files! cfg))]
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
(db/update! pool :project
{:modified-at (ct/now)}

View File

@@ -7,7 +7,6 @@
(ns app.rpc.commands.demo
"A demo specific mutations."
(:require
[app.auth :refer [derive-password]]
[app.common.exceptions :as ex]
[app.common.time :as ct]
[app.config :as cf]
@@ -15,6 +14,7 @@
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[buddy.core.codecs :as bc]
@@ -46,7 +46,7 @@
:fullname fullname
:is-active true
:deleted-at (ct/in-future (cf/get-deletion-delay))
:password (derive-password password)
:password (profile/derive-password cfg password)
:props {}}
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(->> (auth/create-profile! conn params)

View File

@@ -39,7 +39,8 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.exec :as px]))
;; --- FEATURES
@@ -250,7 +251,7 @@
(feat.fmigr/resolve-applied-migrations cfg file))))))
(defn get-file
[{:keys [::db/conn] :as cfg} id
[{:keys [::db/conn ::wrk/executor] :as cfg} id
& {:keys [project-id
migrate?
include-deleted?
@@ -272,8 +273,13 @@
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(feat.fmigr/resolve-applied-migrations cfg)
(feat.fdata/resolve-file-data cfg)
(decode-row))
(feat.fdata/resolve-file-data cfg))
;; NOTE: we perform the file decoding in a separate thread
;; because it has heavy and synchronous operations for
;; decoding file body that are not very friendly with virtual
;; threads.
file (px/invoke! executor #(decode-row file))
file (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file options)
@@ -608,16 +614,8 @@
{:components components
:variant-ids variant-ids}))
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
(def ^:private sql:team-shared-files
"with file_library_agg as (
select flr.file_id,
coalesce(array_agg(flr.library_file_id) filter (where flr.library_file_id is not null), '{}') as library_file_ids
from file_library_rel flr
group by flr.file_id
)
select f.id,
"select f.id,
f.revn,
f.vern,
f.data,
@@ -630,12 +628,10 @@
f.version,
f.is_shared,
ft.media_id,
p.team_id,
fla.library_file_ids
p.team_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
left join file_library_agg as fla on fla.file_id = f.id
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
@@ -679,8 +675,6 @@
(dissoc :media-id)
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map (fn [row]
(update row :library-file-ids db/decode-pgarray #{})))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))
@@ -1077,7 +1071,6 @@
[:library-id ::sm/uuid]])
(sv/defmethod ::link-file-to-library
"Link a file to a library. Returns the recursive list of libraries used by that library"
{::doc/added "1.17"
::webhooks/event? true
::sm/params schema:link-file-to-library}
@@ -1091,8 +1084,7 @@
(fn [{:keys [::db/conn]}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(link-file-to-library conn params)
(bfc/get-libraries cfg [library-id]))))
(link-file-to-library conn params))))
;; --- MUTATION COMMAND: unlink-file-from-library

View File

@@ -37,7 +37,9 @@
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[clojure.set :as set]))
[app.worker :as wrk]
[clojure.set :as set]
[promesa.exec :as px]))
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
@@ -206,7 +208,7 @@
Follow the inner implementation to `update-file-data!` function.
Only intended for internal use on this module."
[{:keys [::db/conn ::timestamp] :as cfg}
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
(let [;; Retrieve the file data
@@ -219,11 +221,15 @@
;; We create a new lexycal scope for clearly delimit the result of
;; executing this update file operation and all its side effects
(let [file (binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))]
(let [file (px/invoke! executor
(fn []
;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))))]
(feat.fmigr/upsert-migrations! conn file)
(persist-file! cfg file)

View File

@@ -26,7 +26,9 @@
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]))
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
@@ -103,7 +105,7 @@
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
@@ -155,7 +157,7 @@
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))]
(let [data (generate-missing! data)
(let [data (px/invoke! executor (partial generate-missing! data))
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))

View File

@@ -28,7 +28,9 @@
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.storage.tmp :as tmp]
[app.util.services :as sv]))
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
;; --- COMMAND: Duplicate File
@@ -311,14 +313,15 @@
;; Update the modification date of the all affected projects
;; ensuring that the destination project is the most recent one.
(loop [project-ids (into (list project-id) source)
modified-at (ct/now)]
(when-let [project-id (first project-ids)]
(db/update! conn :project
{:modified-at modified-at}
{:id project-id})
(recur (rest project-ids)
(ct/plus modified-at 10))))
(doseq [project-id (into (list project-id) source)]
;; NOTE: as this is executed on virtual thread, sleeping does
;; not causes major issues, and allows an easy way to set a
;; trully different modification date to each file.
(px/sleep 10)
(db/update! conn :project
{:modified-at (ct/now)}
{:id project-id}))
nil))
@@ -393,7 +396,12 @@
;; --- COMMAND: Clone Template
(defn clone-template
[{:keys [::db/pool] :as cfg} {:keys [project-id profile-id] :as params} template]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
;; NOTE: the importation process performs some operations
;; that are not very friendly with virtual threads, and for
;; avoid unexpected blocking of other concurrent operations
;; we dispatch that operation to a dedicated executor.
(let [template (tmp/tempfile-from template
:prefix "penpot.template."
:suffix ""
@@ -411,8 +419,8 @@
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
result (if (= format :binfile-v3)
(bf.v3/import-files! cfg)
(bf.v1/import-files! cfg))]
(px/invoke! executor (partial bf.v3/import-files! cfg))
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]

View File

@@ -24,8 +24,10 @@
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[datoteka.io :as io]))
[datoteka.io :as io]
[promesa.exec :as px]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@@ -151,9 +153,9 @@
(assoc ::image (process-main-image info)))))
(defn- create-file-media-object
[{:keys [::sto/storage ::db/conn] :as cfg}
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
{:keys [id file-id is-local name content]}]
(let [result (process-image content)
(let [result (px/invoke! executor (partial process-image content))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]

View File

@@ -30,13 +30,16 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.exec :as px]))
(declare check-profile-existence!)
(declare decode-row)
(declare derive-password)
(declare filter-props)
(declare get-profile)
(declare strip-private-attrs)
(declare verify-password)
(def schema:props-notifications
[:map {:title "props-notifications"}
@@ -189,7 +192,7 @@
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)]
(when (and (not= (:password profile) "!")
(not (:valid (auth/verify-password old-password (:password profile)))))
(not (:valid (verify-password cfg old-password (:password profile)))))
(ex/raise :type :validation
:code :old-password-not-match))
profile))
@@ -198,7 +201,7 @@
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
(when-not (db/read-only? conn)
(db/update! conn :profile
{:password (auth/derive-password password)}
{:password (derive-password cfg password)}
{:id id})
nil))
@@ -300,11 +303,12 @@
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage] :as cfg} {:keys [file] :as params}]
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
(let [params (-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/label "upload-photo")
(assoc ::climit/executor executor)
(climit/invoke! generate-thumbnail! file))]
(sto/put-object! storage params)))
@@ -544,6 +548,15 @@
[props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[{:keys [::wrk/executor]} password]
(when password
(px/invoke! executor (partial auth/derive-password password))))
(defn verify-password
[{:keys [::wrk/executor]} password password-data]
(px/invoke! executor (partial auth/verify-password password password-data)))
(defn decode-row
[{:keys [props] :as row}]
(cond-> row

View File

@@ -224,112 +224,62 @@
(def ^:private xf:map-email (map :email))
(defn- create-team-invitations
"Unified function to handle both create and resend team invitations.
Accepts either:
- emails (set) + role (single role for all emails)
- invitations (vector of {:email :role} maps)"
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
(let [;; Normalize input to a consistent format: [{:email :role}]
invitation-data (cond
;; Case 1: emails + single role (create invitations style)
(and emails role)
(map (fn [email] {:email email :role role}) emails)
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
(let [emails (set emails)
;; Case 2: invitations with individual roles (resend invitations style)
(some? invitations)
invitations
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
:else
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
invitation-emails (into #{} (map :email) invitation-data)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
invitations (into #{}
(comp
;; We don't re-send invitations to
;; already existing members
(remove #(contains? team-members (:email %)))
invitations (into #{}
(comp
;; We don't re-send inviation to
;; already existing members
(remove team-members)
;; We don't send invitations to
;; join-requested members
(remove #(contains? join-requests (:email %)))
(map (fn [{:keys [email role]}]
(create-invitation cfg
(-> params
(assoc :email email)
(assoc :role role)))))
(remove nil?))
invitation-data)]
(remove join-requests)
(map (fn [email] (assoc params :email email)))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> join-requests
(filter #(contains? invitation-emails (key %)))
(map (fn [[email member]]
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
(add-member-to-team conn profile team role member))))
(doall))
(filter #(contains? emails (key %)))
(map val)
(run! (partial add-member-to-team conn profile team role)))
invitations))
(def ^:private schema:create-team-invitations
[:and
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
;; Support both formats:
;; 1. emails (set) + role (single role for all)
;; 2. invitations (vector of {:email :role} maps)
[:emails {:optional true} [::sm/set ::sm/email]]
[:role {:optional true} types.team/schema:role]
[:invitations {:optional true} [:vector [:map
[:email ::sm/email]
[:role types.team/schema:role]]]]]
;; Ensure exactly one format is provided
[:fn (fn [params]
(let [has-emails-role (and (contains? params :emails)
(contains? params :role))
has-invitations (contains? params :invitations)]
(and (or has-emails-role has-invitations)
(not (and has-emails-role has-invitations)))))]])
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role types.team/schema:role]
[:emails [::sm/set ::sm/email]]])
(def ^:private max-invitations-by-request-threshold
"The number of invitations can be sent in a single rpc request"
25)
(sv/defmethod ::create-team-invitations
"A rpc call that allows to send single or multiple invitations to join the team.
Supports two parameter formats:
1. emails (set) + role (single role for all emails)
2. invitations (vector of {:email :role} maps for individual roles)"
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"
::doc/module :teams
::sm/params schema:create-team-invitations}
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
(let [perms (teams/get-permissions cfg profile-id team-id)
profile (db/get-by-id cfg :profile profile-id)
;; Determine which format is being used
using-emails-format? (and emails role)
;; Handle both parameter formats
emails (if using-emails-format?
(into #{} (map profile/clean-email) emails)
#{})
;; Calculate total invitation count for both formats
invitation-count (if using-emails-format?
(count emails)
(count (:invitations params)))]
emails (into #{} (map profile/clean-email) emails)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when (> invitation-count max-invitations-by-request-threshold)
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
@@ -338,7 +288,7 @@
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/incr invitation-count)
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
@@ -354,12 +304,7 @@
(-> params
(assoc :profile profile)
(assoc :team team)
;; Pass parameters in the correct format for the unified function
(cond-> using-emails-format?
;; If using emails+role format, ensure both are present
(assoc :emails emails :role role)
;; If using invitations format, the :invitations key is already in params
(not using-emails-format?) identity)))]
(assoc :emails emails)))]
(with-meta {:total (count invitations)
:invitations invitations}

View File

@@ -7,7 +7,7 @@
(ns app.srepl.cli
"PREPL API for external usage (CLI or ADMIN)"
(:require
[app.auth :refer [derive-password]]
[app.auth :as auth]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
@@ -54,7 +54,7 @@
(some-> (get-current-system)
(db/tx-run!
(fn [{:keys [::db/conn] :as system}]
(let [password (derive-password password)
(let [password (cmd.profile/derive-password system password)
params {:id (uuid/next)
:email email
:fullname fullname
@@ -74,7 +74,7 @@
(assoc :fullname fullname)
(some? password)
(assoc :password (derive-password password))
(assoc :password (auth/derive-password password))
(some? is-active)
(assoc :is-active is-active))]
@@ -124,7 +124,7 @@
(defmethod exec-command "derive-password"
[{:keys [password]}]
(derive-password password))
(auth/derive-password password))
(defmethod exec-command "authenticate"
[{:keys [token]}]

View File

@@ -16,7 +16,7 @@
[app.common.features :as cfeat]
[app.common.files.validate :as cfv]
[app.common.logging :as l]
[app.common.pprint :as p]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.time :as ct]
@@ -58,7 +58,7 @@
(defn print-tasks
[]
(let [tasks (:app.worker/registry main/system)]
(p/pprint (keys tasks) :level 200)))
(pp/pprint (keys tasks) :level 200)))
(defn run-task!
([tname]
@@ -130,18 +130,18 @@
(defn reset-password!
"Reset a password to a specific one for a concrete user or all users
if email is `:all` keyword."
[& {:keys [email password] :or {password "123123"} :as params}]
(when-not email
(throw (IllegalArgumentException. "email is mandatory")))
[& {:keys [email password]}]
(assert (string? email) "expected email")
(assert (string? password) "expected password")
(some-> main/system
(db/tx-run!
(fn [{:keys [::db/conn] :as system}]
(let [password (derive-password password)]
(if (= email :all)
(db/exec! conn ["update profile set password=?" password])
(let [email (str/lower email)]
(db/exec! conn ["update profile set password=? where email=?" password email]))))))))
(let [password (derive-password password)
email (str/lower email)]
(-> (db/exec-one! conn ["update profile set password=? where email=?" password email])
(db/get-update-count)
(pos?)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FEATURES
@@ -549,6 +549,17 @@
:elapsed elapsed))))))
(defn mark-file-as-trimmed
[id]
(let [id (h/parse-uuid id)]
(db/tx-run! main/system (fn [cfg]
(-> (db/update! cfg :file
{:has-media-trimmed true}
{:id id}
{::db/return-keys false})
(db/get-update-count)
(pos?))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -27,9 +27,7 @@
(defn get-legacy-backend
[]
(when-let [name (cf/get :assets-storage-backend)]
(l/wrn :hint "using deprecated configuration, please read 2.11 release notes"
:href "https://github.com/penpot/penpot/releases/tag/2.11.0")
(let [name (cf/get :assets-storage-backend)]
(case name
:assets-fs :fs
:assets-s3 :s3

View File

@@ -31,13 +31,13 @@
java.time.Duration
java.util.Collection
java.util.Optional
java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber
software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
@@ -87,11 +87,12 @@
(def ^:private schema:config
[:map {:title "s3-backend-config"}
::wrk/netty-io-executor
::wrk/executor
[::region {:optional true} :keyword]
[::bucket {:optional true} ::sm/text]
[::prefix {:optional true} ::sm/text]
[::endpoint {:optional true} ::sm/uri]])
[::endpoint {:optional true} ::sm/uri]
[::io-threads {:optional true} ::sm/int]])
(defmethod ig/expand-key ::backend
[k v]
@@ -109,7 +110,6 @@
presigner (build-s3-presigner params)]
(assoc params
::sto/type :s3
::counter (AtomicLong. 0)
::client @client
::presigner presigner
::close-fn #(.close ^java.lang.AutoCloseable client)))))
@@ -121,7 +121,7 @@
(defmethod ig/halt-key! ::backend
[_ {:keys [::close-fn]}]
(when (fn? close-fn)
(close-fn)))
(px/run! close-fn)))
(def ^:private schema:backend
[:map {:title "s3-backend"}
@@ -198,16 +198,19 @@
(Region/of (name region)))
(defn- build-s3-client
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
thr-num (or io-threads (min 16 (px/get-available-processors)))
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroup (SdkEventLoopGroup/create netty-io-executor))
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int thr-num))))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
@@ -259,7 +262,7 @@
(.close ^InputStream input))))
(defn- make-request-body
[counter content]
[executor content]
(let [size (impl/get-size content)]
(reify
AsyncRequestBody
@@ -269,19 +272,16 @@
(^void subscribe [_ ^Subscriber subscriber]
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
input (io/input-stream content)]
(px/thread-call (partial write-input-stream delegate input)
{:name (str "penpot/storage/" (.getAndIncrement ^AtomicLong counter))})
(px/run! executor (partial write-input-stream delegate input))
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
^Subscriber subscriber))))))
(defn- put-object
[{:keys [::client ::bucket ::prefix ::counter]} {:keys [id] :as object} content]
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
(let [path (dm/str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
rbody (make-request-body counter content)
rbody (make-request-body executor content)
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)

View File

@@ -44,7 +44,7 @@
[_ cfg]
(fs/create-dir default-tmp-dir)
(px/fn->thread (partial io-loop cfg)
{:name "penpot/storage/tmp-cleaner"}))
{:name "penpot/storage/tmp-cleaner" :virtual true}))
(defmethod ig/halt-key! ::cleaner
[_ thread]

View File

@@ -27,7 +27,7 @@
(sp/put! channel [type data])
nil)))
(defn spawn-listener
(defn start-listener
[channel on-event on-close]
(assert (sp/chan? channel) "expected active events channel")
@@ -51,7 +51,7 @@
[f on-event]
(binding [*channel* (sp/chan :buf 32)]
(let [listener (spawn-listener *channel* on-event (constantly nil))]
(let [listener (start-listener *channel* on-event (constantly nil))]
(try
(f)
(finally

View File

@@ -112,7 +112,7 @@
(if (db/read-only? pool)
(l/wrn :hint "not started (db is read-only)")
(px/fn->thread dispatcher :name "penpot/worker-dispatcher"))))
(px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual false))))
(defmethod ig/halt-key! ::wrk/dispatcher
[_ thread]

View File

@@ -7,79 +7,97 @@
(ns app.worker.executor
"Async tasks abstraction (impl)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.metrics :as mtx]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[promesa.exec :as px])
(:import
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.concurrent.DefaultEventExecutorGroup
java.util.concurrent.ExecutorService
java.util.concurrent.ThreadFactory))
java.util.concurrent.ThreadPoolExecutor))
(set! *warn-on-reflection* true)
(sm/register!
{:type ::wrk/executor
:pred #(instance? ExecutorService %)
:pred #(instance? ThreadPoolExecutor %)
:type-properties
{:title "executor"
:description "Instance of ExecutorService"}})
(sm/register!
{:type ::wrk/netty-io-executor
:pred #(instance? NioEventLoopGroup %)
:type-properties
{:title "executor"
:description "Instance of NioEventLoopGroup"}})
(sm/register!
{:type ::wrk/netty-executor
:pred #(instance? DefaultEventExecutorGroup %)
:type-properties
{:title "executor"
:description "Instance of DefaultEventExecutorGroup"}})
:description "Instance of ThreadPoolExecutor"}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IO Executor
;; EXECUTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/assert-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_NETTY_IO_THREADS environment variable"))
(defmethod ig/init-key ::wrk/executor
[_ _]
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 60000)]
(l/inf :hint "executor started")
executor))
(defmethod ig/init-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/netty-io/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start netty io executor" :threads nthreads)
(NioEventLoopGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/halt-key! ::wrk/netty-io-executor
(defmethod ig/halt-key! ::wrk/executor
[_ instance]
(px/shutdown! instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IO Offload Executor
;; MONITOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/assert-key ::wrk/netty-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_EXEC_THREADS environment variable"))
(defn- get-stats
[^ThreadPoolExecutor executor]
{:active (.getPoolSize ^ThreadPoolExecutor executor)
:running (.getActiveCount ^ThreadPoolExecutor executor)
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
(defmethod ig/init-key ::wrk/netty-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/exec/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start default executor" :threads nthreads)
(DefaultEventExecutorGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/expand-key ::wrk/monitor
[k v]
{k (-> (d/without-nils v)
(assoc ::interval (ct/duration "2s")))})
(defmethod ig/halt-key! ::wrk/netty-executor
[_ instance]
(px/shutdown! instance))
(defmethod ig/init-key ::wrk/monitor
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
(letfn [(monitor! [executor prev-completed]
(let [labels (into-array String [(d/name name)])
stats (get-stats executor)
completed (:completed stats)
completed-inc (- completed prev-completed)
completed-inc (if (neg? completed-inc) 0 completed-inc)]
(mtx/run! metrics
:id :executor-active-threads
:labels labels
:val (:active stats))
(mtx/run! metrics
:id :executor-running-threads
:labels labels
:val (:running stats))
(mtx/run! metrics
:id :executors-completed-tasks
:labels labels
:inc completed-inc)
completed-inc))]
(px/thread
{:name "penpot/executors-monitor" :virtual true}
(l/inf :hint "monitor started" :name name)
(try
(loop [completed 0]
(px/sleep interval)
(recur (long (monitor! executor completed))))
(catch InterruptedException _cause
(l/trc :hint "monitor: interrupted" :name name))
(catch Throwable cause
(l/err :hint "monitor: unexpected error" :name name :cause cause))
(finally
(l/inf :hint "monitor: terminated" :name name))))))
(defmethod ig/halt-key! ::wrk/monitor
[_ thread]
(px/interrupt! thread))

View File

@@ -248,7 +248,7 @@
(defn- start-thread!
[{:keys [::rds/redis ::id ::queue ::wrk/tenant] :as cfg}]
(px/thread
{:name (str "penpot/worker-runner/" id)}
{:name (format "penpot/worker/runner:%s" id)}
(l/inf :hint "started" :id id :queue queue)
(try
(dm/with-open [rconn (rds/connect redis)]
@@ -303,7 +303,7 @@
(l/wrn :hint "not started (db is read-only)" :queue queue :parallelism parallelism)
(doall
(->> (range parallelism)
(map #(assoc cfg ::id (str queue "/" %)))
(map #(assoc cfg ::id %))
(map start-thread!))))))
(defmethod ig/halt-key! ::wrk/runner

View File

@@ -113,6 +113,7 @@
:app.auth.oidc.providers/generic
:app.setup/templates
:app.auth.oidc/routes
:app.worker/monitor
:app.http.oauth/handler
:app.notifications/handler
:app.loggers.mattermost/reporter

View File

@@ -7,25 +7,25 @@
org.clojure/clojurescript {:mvn/version "1.12.42"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
selmer/selmer {:mvn/version "1.12.62"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
metosin/malli {:mvn/version "0.19.1"}
metosin/malli {:mvn/version "0.18.0"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"}
integrant/integrant {:mvn/version "0.13.1"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
@@ -47,7 +47,7 @@
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.29"}
fipp/fipp {:mvn/version "0.6.27"}
me.flowthing/pp {:mvn/version "2024-11-13.77"}
@@ -59,7 +59,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "3.2.0"}
thheller/shadow-cljs {:mvn/version "3.1.5"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -68,7 +68,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
:ns-default build}
:test

View File

@@ -50,13 +50,6 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(long (.getInt ~target (unchecked-int ~offset))))))
(defmacro read-long
[target offset]
(if (:ns &env)
`(.getInt64 ~target ~offset true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.getLong ~target (unchecked-int ~offset)))))
(defmacro read-float
[target offset]
(if (:ns &env)
@@ -82,40 +75,6 @@
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
(defmacro read-bytes
"Get a byte array from buffer. It is potentially unsafe because on
JS/CLJS it returns a subarray without doing any copy of data."
[target offset size]
(if (:ns &env)
`(new js/Uint8Array
(.-buffer ~target)
(+ (.-byteOffset ~target) ~offset)
~size)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
bbuf (with-meta (gensym "bbuf") {:tag bytes})]
`(let [~bbuf (byte-array ~size)]
(.get ~target
(unchecked-int ~offset)
~bbuf
0
~size)
~bbuf))))
;; FIXME: implement in cljs
(defmacro write-bytes
([target offset src size]
`(write-bytes ~target ~offset ~src 0 ~size))
([target offset src src-offset size]
(if (:ns &env)
(throw (ex-info "not implemented" {}))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
src (with-meta src {:tag 'bytes})]
`(.put ~target
(unchecked-int ~offset)
~src
(unchecked-int ~src-offset)
(unchecked-int ~size))))))
(defmacro write-byte
[target offset value]
(if (:ns &env)
@@ -185,15 +144,13 @@
(.setUint32 ~target (+ ~offset 12) (aget barray# 3) true))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
value (with-meta value {:tag 'java.util.UUID})
prev (with-meta (gensym "prev-") {:tag 'java.nio.ByteOrder})]
`(let [~prev (.order ~target)]
(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ~prev)))))))
value (with-meta value {:tag 'java.util.UUID})]
`(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
(defn wrap
[data]
@@ -203,7 +160,7 @@
(defn allocate
[size]
#?(:clj (let [buffer (ByteBuffer/allocate (unchecked-int size))]
#?(:clj (let [buffer (ByteBuffer/allocate (int size))]
(.order buffer ByteOrder/LITTLE_ENDIAN))
:cljs (new js/DataView (new js/ArrayBuffer size))))
@@ -224,14 +181,6 @@
(.set dst-view src-view)
(js/DataView. dst-buff))))
;; FIXME: cljs impl
#?(:clj
(defn copy-bytes
[src src-offset size dst dst-offset]
(let [tmp (byte-array size)]
(.get ^ByteBuffer src src-offset tmp 0 size)
(.put ^ByteBuffer dst dst-offset tmp 0 size))))
(defn equals?
[buffer-a buffer-b]
#?(:clj
@@ -259,18 +208,3 @@
[o]
#?(:clj (instance? ByteBuffer o)
:cljs (instance? js/DataView o)))
(defn slice
[buffer offset size]
#?(:cljs
(let [offset (+ (.-byteOffset buffer) offset)]
(new js/DataView (.-buffer buffer) offset size))
:clj
(-> (.slice ^ByteBuffer buffer (unchecked-int offset) (unchecked-int size))
(.order ByteOrder/LITTLE_ENDIAN))))
(defn size
[o]
#?(:cljs (.-byteLength ^js o)
:clj (.capacity ^ByteBuffer o)))

View File

@@ -51,7 +51,6 @@
"styles/v2"
"layout/grid"
"plugins/runtime"
"tokens/numeric-input"
"design-tokens/v1"
"text-editor/v2"
"render-wasm/v1"
@@ -76,7 +75,6 @@
#{"styles/v2"
"plugins/runtime"
"text-editor/v2"
"tokens/numeric-input"
"render-wasm/v1"})
;; Features that are mainly backend only or there are a proper
@@ -100,7 +98,6 @@
"design-tokens/v1"
"fdata/shape-data-type"
"fdata/path-data"
"tokens/numeric-input"
"variants/v1"}
(into frontend-only-features)
(into backend-only-features)))
@@ -125,7 +122,6 @@
:feature-text-editor-v2 "text-editor/v2"
:feature-render-wasm "render-wasm/v1"
:feature-variants "variants/v1"
:feature-token-input "tokens/numeric-input"
nil))
(defn migrate-legacy-features

View File

@@ -323,7 +323,7 @@
[:main-instance-page ::sm/uuid]]]
[:mod-component
[:map {:title "ModComponentChange"}
[:map {:title "ModCompoenentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
@@ -366,33 +366,9 @@
[:type [:= :del-typography]]
[:id ::sm/uuid]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]] ;; TODO: we should define a plain object schema for tokens-lib
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-set-attrs]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-theme-attrs]]]]
[:set-active-token-themes
[:map {:title "SetActiveTokenThemes"}
[:type [:= :set-active-token-themes]]
[:update-active-token-themes
[:map {:title "UpdateActiveTokenThemes"}
[:type [:= :update-active-token-themes]]
[:theme-paths [:set :string]]]]
[:rename-token-set-group
@@ -417,6 +393,39 @@
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:theme-name :string]
[:group :string]
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
;; FIXME: we should not pass private types as part of changes
;; protocol, the changes protocol should reflect a
;; method/protocol for perform surgical operations on file data,
;; this has nothing todo with internal types of a file data
;; structure.
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
[:maybe [:fn ctob/token-set?]]]]]
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-name :string]
[:token-id ::sm/uuid]
[:token [:maybe ctob/schema:token-attrs]]]]
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
@@ -969,63 +978,64 @@
[data {:keys [id]}]
(ctyl/delete-typography data id))
;; -- Design Tokens
;; -- Tokens
(defmethod process-change :set-tokens-lib
[data {:keys [tokens-lib]}]
(assoc data :tokens-lib tokens-lib))
(defmethod process-change :set-token
[data {:keys [set-id token-id attrs]}]
[data {:keys [set-name token-id token]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-token lib' set-id token-id)
(not token)
(ctob/delete-token-from-set lib' set-name token-id)
(not (ctob/get-token lib' set-id token-id))
(ctob/add-token lib' set-id (ctob/make-token attrs))
(not (ctob/get-token-in-set lib' set-name token-id))
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
:else
(ctob/update-token lib' set-id token-id
(fn [prev-token]
(ctob/make-token (merge prev-token attrs)))))))))
(ctob/update-token-in-set lib' set-name token-id (fn [prev-token]
(ctob/make-token (merge prev-token token)))))))))
(defmethod process-change :set-token-set
[data {:keys [id attrs]}]
[data {:keys [set-name group? token-set]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-set lib' id)
(not token-set)
(if group?
(ctob/delete-set-group lib' set-name)
(ctob/delete-set lib' set-name))
(not (ctob/get-set lib' id))
(ctob/add-set lib' (ctob/make-token-set attrs))
(not (ctob/get-set lib' set-name))
(ctob/add-set lib' token-set)
:else
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
(ctob/update-set lib' set-name (fn [_] token-set)))))))
(defmethod process-change :set-token-theme
[data {:keys [id attrs]}]
[data {:keys [group theme-name theme]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-theme lib' id)
(not theme)
(ctob/delete-theme lib' group theme-name)
(not (ctob/get-theme lib' id))
(ctob/add-theme lib' (ctob/make-token-theme attrs))
(not (ctob/get-theme lib' group theme-name))
(ctob/add-theme lib' (ctob/make-token-theme theme))
:else
(ctob/update-theme lib'
id
group theme-name
(fn [prev-token-theme]
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
(defmethod process-change :set-active-token-themes
(defmethod process-change :update-active-token-themes
[data {:keys [theme-paths]}]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-paths))))
@@ -1049,7 +1059,7 @@
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Design Tokens configuration
;; === Base font size
(defmethod process-change :set-base-font-size
[data {:keys [base-font-size]}]

View File

@@ -21,8 +21,7 @@
[app.common.types.path :as path]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]))
[app.common.uuid :as uuid]))
;; Auxiliary functions to help create a set of changes (undo + redo)
;; TODO: this is a duplicate schema
@@ -718,7 +717,6 @@
(reduce resize-parent changes all-parents)))
;; Library changes
(defn add-color
[changes color]
(-> changes
@@ -800,6 +798,160 @@
(update :undo-changes conj {:type :add-typography :typography prev-typography})
(apply-changes-local))))
(defn update-active-token-themes
[changes active-theme-paths prev-active-theme-paths]
(-> changes
(update :redo-changes conj {:type :update-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :update-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local)))
(defn set-token-theme [changes group theme-name theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme group theme-name))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:theme-name theme-name
:group group
:theme theme})
(update :undo-changes conj (if prev-theme
{:type :set-token-theme
:group group
:theme-name (or
;; Undo of edit
(:name theme)
;; Undo of delete
theme-name)
:theme prev-theme}
;; Undo of create
{:type :set-token-theme
:group group
:theme-name theme-name
:theme nil}))
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
(apply-changes-local))))
(defn move-token-set
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
(-> changes
(update :redo-changes conj {:type :move-token-set
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn move-token-set-group
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
(-> changes
(update :redo-changes conj {:type :move-token-set-group
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set-group
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-name token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-set set-name)
(ctob/get-token token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-name set-name
:token-id token-id
:token token})
(update :undo-changes conj (if prev-token
{:type :set-token
:set-name set-name
:token-id (or
;; Undo of edit
(:id token)
;; Undo of delete
token-id)
:token prev-token}
;; Undo of create token
{:type :set-token
:set-name set-name
:token-id token-id
:token nil}))
(apply-changes-local))))
(defn rename-token-set
[changes name new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name name
:token-set (ctob/rename prev-token-set new-name)
:group? false})
(update :undo-changes conj {:type :set-token-set
:set-name new-name
:token-set prev-token-set
:group? false})
(apply-changes-local))))
(defn set-token-set
[changes set-name group? token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set set-name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name set-name
:token-set token-set
:group? group?})
(update :undo-changes conj (if prev-token-set
{:type :set-token-set
:set-name (if token-set
;; Undo of edit
(ctob/get-name token-set)
;; Undo of delete
set-name)
:token-set prev-token-set
:group? group?}
;; Undo of create
{:type :set-token-set
:set-name set-name
:token-set nil
:group? group?}))
(apply-changes-local))))
(defn add-component
([changes id path name updated-shapes main-instance-id main-instance-page]
(add-component changes id path name updated-shapes main-instance-id main-instance-page nil nil nil))
@@ -929,144 +1081,6 @@
:id id
:delta delta})))
;; Design Tokens changes
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-id token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-token set-id token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy token)})
(update :undo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy prev-token)})
(apply-changes-local))))
(defn set-token-set
[changes id token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy token-set)})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn rename-token-set
[changes id new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy (ctob/rename prev-token-set new-name))})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn set-token-theme [changes id theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme id))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:id id
:attrs (datafy theme)})
(update :undo-changes conj {:type :set-token-theme
:id id
:attrs (datafy prev-theme)})
(apply-changes-local))))
(defn set-active-token-themes
[changes active-theme-paths]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-active-theme-paths (d/nilv (some-> (get library-data :tokens-lib)
(ctob/get-active-theme-paths))
#{})]
(-> changes
(update :redo-changes conj {:type :set-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :set-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
(apply-changes-local))))
(defn move-token-set
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
(-> changes
(update :redo-changes conj {:type :move-token-set
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn move-token-set-group
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
(-> changes
(update :redo-changes conj {:type :move-token-set-group
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set-group
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))
;; Misc changes
(defn reorder-children
[changes id children]
(assert-page-id! changes)
@@ -1149,3 +1163,15 @@
[changes]
(::page-id (meta changes)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))

View File

@@ -692,9 +692,129 @@
(walk/postwalk process-form data)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHAPES ORGANIZATION
;; SHAPES ORGANIZATION (PATH MANAGEMENT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-path
"Decompose a string in the form 'one / two / three' into
a vector of strings, normalizing spaces."
[path]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path "/")
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join " / " path-vec))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn parse-path-name
"Parse a string in the form 'group / subgroup / name'.
Retrieve the path and the name in separated values, normalizing spaces."
[path-name]
(let [path-name-split (split-path path-name)
path (str/join " / " (butlast path-name-split))
name (or (last path-name-split) "")]
[path name]))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn compact-name
"Append the first item of the path and the name."
[path name]
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))
(defn get-frame-objects
"Retrieves a new objects map only with the objects under frame-id (with frame-id)"
[objects frame-id]

View File

@@ -231,7 +231,7 @@
shape))
(update-container [container]
(update container :objects d/update-vals fix-line-paths))]
(d/update-when container :objects d/update-vals fix-line-paths))]
(-> data
(update :pages-index d/update-vals update-container)
@@ -285,7 +285,9 @@
(let [[deleted objects] (clean-objects objects)]
(if (and (pos? deleted) (< n 1000))
(recur (inc n) objects)
(assoc container :objects objects)))))]
(-> container
(assoc :objects objects)
(d/without-nils))))))]
(-> data
(update :pages-index d/update-vals clean-container)
@@ -383,21 +385,20 @@
(dissoc :fill-color :fill-opacity))))
(update-container [container]
(if (contains? container :objects)
(loop [objects (:objects container)
shapes (->> (vals objects)
(filter cfh/image-shape?))]
(if-let [shape (first shapes)]
(let [{:keys [id frame-id] :as shape'} (process-shape shape)]
(if (identical? shape shape')
(recur objects (rest shapes))
(recur (-> objects
(assoc id shape')
(d/update-when frame-id dissoc :thumbnail))
(rest shapes))))
(assoc container :objects objects)))
container))]
(loop [objects (:objects container)
shapes (->> (vals objects)
(filter cfh/image-shape?))]
(if-let [shape (first shapes)]
(let [{:keys [id frame-id] :as shape'} (process-shape shape)]
(if (identical? shape shape')
(recur objects (rest shapes))
(recur (-> objects
(assoc id shape')
(d/update-when frame-id dissoc :thumbnail))
(rest shapes))))
(-> container
(assoc :objects objects)
(d/without-nils)))))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
@@ -1433,74 +1434,6 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def ^:private valid-stroke?
(sm/lazy-validator cts/schema:stroke))
(defmethod migrate-data "0007-clear-invalid-strokes-and-fills-v2"
[data _]
(letfn [(clear-color-image [image]
(select-keys image types.color/image-attrs))
(clear-color-gradient [gradient]
(select-keys gradient types.color/gradient-attrs))
(clear-stroke [stroke]
(-> stroke
(select-keys cts/stroke-attrs)
(d/update-when :stroke-color-gradient clear-color-gradient)
(d/update-when :stroke-image clear-color-image)
(d/update-when :stroke-style #(if (#{:svg :none} %) :solid %))))
(fix-strokes [strokes]
(->> (map clear-stroke strokes)
(filterv valid-stroke?)))
;; Fixes shapes with nested :fills in the :fills attribute
;; introduced in a migration `0006-fix-old-texts-fills` when
;; types.text/transform-nodes with identity pred was broken
(remove-nested-fills [[fill :as fills]]
(if (and (= 1 (count fills))
(contains? fill :fills))
(:fills fill)
fills))
(clear-fill [fill]
(-> fill
(select-keys types.fills/fill-attrs)
(d/update-when :fill-image clear-color-image)
(d/update-when :fill-color-gradient clear-color-gradient)))
(fix-fills [fills]
(->> fills
(remove-nested-fills)
(map clear-fill)
(filterv valid-fill?)))
(fix-object [object]
(-> object
(d/update-when :strokes fix-strokes)
(d/update-when :fills fix-fills)))
(fix-text-content [content]
(->> content
(types.text/transform-nodes types.text/is-content-node? fix-object)
(types.text/transform-nodes types.text/is-paragraph-set-node? #(dissoc % :fills))))
(update-shape [object]
(-> object
(fix-object)
;; The text shape also can has strokes and fils on the
;; text fragments so we need to fix them there
(cond-> (cfh/text-shape? object)
(update :content fix-text-content))))
(update-container [container]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0008-fix-library-colors-v4"
[data _]
(letfn [(clear-color-opacity [color]
@@ -1605,6 +1538,84 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def ^:private valid-stroke?
(sm/lazy-validator cts/schema:stroke))
(defmethod migrate-data "0013-clear-invalid-strokes-and-fills"
[data _]
(letfn [(clear-color-image [image]
(select-keys image types.color/image-attrs))
(clear-color-gradient [gradient]
(select-keys gradient types.color/gradient-attrs))
(clear-stroke [stroke]
(-> stroke
(select-keys cts/stroke-attrs)
(d/update-when :stroke-color-gradient clear-color-gradient)
(d/update-when :stroke-image clear-color-image)
(d/update-when :stroke-style #(if (#{:svg :none} %) :solid %))))
(fix-strokes [strokes]
(->> (map clear-stroke strokes)
(filterv valid-stroke?)))
;; Fixes shapes with nested :fills in the :fills attribute
;; introduced in a migration `0006-fix-old-texts-fills` when
;; types.text/transform-nodes with identity pred was broken
(remove-nested-fills [[fill :as fills]]
(if (and (= 1 (count fills))
(contains? fill :fills))
(:fills fill)
fills))
(clear-fill [fill]
(-> fill
(select-keys types.fills/fill-attrs)
(d/update-when :fill-image clear-color-image)
(d/update-when :fill-color-gradient clear-color-gradient)))
(fix-fills [fills]
(->> fills
(remove-nested-fills)
(map clear-fill)
(filterv valid-fill?)))
(fix-object [object]
(-> object
(d/update-when :strokes fix-strokes)
(d/update-when :fills fix-fills)))
(fix-text-content [content]
(->> content
(types.text/transform-nodes types.text/is-content-node? fix-object)
(types.text/transform-nodes types.text/is-paragraph-set-node? #(dissoc % :fills))))
(update-shape [object]
(-> object
(fix-object)
(d/update-when :position-data #(mapv fix-object %))
;; The text shape can also have strokes and fills on
;; the text fragments, so we need to fix them there.
(cond-> (cfh/text-shape? object)
(update :content fix-text-content))))
(update-container [container]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0014-clear-components-nil-objects"
[data _]
;; Because of a bug in migrations, several files have migrations
;; applied in an incorrect order and because of other bug on old
;; migrations, some files have components with `:objects` with `nil`
;; as value; this migration fixes it.
(d/update-when data :components d/update-vals d/without-nils))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@@ -1667,10 +1678,11 @@
"0004-clean-shadow-color"
"0005-deprecate-image-type"
"0006-fix-old-texts-fills"
"0007-clear-invalid-strokes-and-fills-v2"
"0008-fix-library-colors-v4"
"0009-clean-library-colors"
"0009-add-partial-text-touched-flags"
"0010-fix-swap-slots-pointing-non-existent-shapes"
"0011-fix-invalid-text-touched-flags"
"0012-fix-position-data"]))
"0012-fix-position-data"
"0013-clear-invalid-strokes-and-fills"
"0014-clear-components-nil-objects"]))

View File

@@ -320,31 +320,6 @@
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids detach-shape))))))
(defmethod repair-error :ref-shape-is-not-head
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a normal copy, removing nested copy status
(log/debug :hint " -> unhead shape")
(ctk/unhead-shape shape))]
(log/dbg :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :ref-shape-is-head
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a nested head, adding component info
(log/debug :hint " -> reroot shape")
(ctk/rehead-shape shape (:component-file args) (:component-id args)))]
(log/dbg :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :shape-ref-cycle
[_ {:keys [shape args] :as error} file-data _]

View File

@@ -11,7 +11,6 @@
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -48,8 +47,6 @@
:should-be-component-root
:should-not-be-component-root
:ref-shape-not-found
:ref-shape-is-head
:ref-shape-is-not-head
:shape-ref-in-main
:root-main-not-allowed
:nested-main-not-allowed
@@ -83,7 +80,7 @@
[:file-id ::sm/uuid]
[:page-id {:optional true} [:maybe ::sm/uuid]]])
(def check-error!
(def check-error
(sm/check-fn schema:error))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -99,21 +96,17 @@
(defn- report-error
[code hint shape file page & {:as args}]
(let [error {:code code
:hint hint
:shape shape
:file-id (:id file)
:page-id (:id page)
:shape-id (:id shape)
:args args}]
(let [error (d/without-nils
{:code code
:hint hint
:shape shape
:file-id (:id file)
:page-id (:id page)
:shape-id (:id shape)
:args args})]
(dm/assert!
"expected a valid `*errors*` dynamic binding"
(some? *errors*))
(dm/assert!
"expected valid error"
(check-error! error))
(assert (some? *errors*) "expected a valid `*errors*` dynamic binding")
(assert (check-error error))
(vswap! *errors* conj error)))
@@ -308,28 +301,6 @@
"Shape inside main instance should not have shape-ref"
shape file page)))
(defn- check-ref-is-not-head
"Validate that the referenced shape is not a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(ctk/instance-head? ref-shape))
(report-error :ref-shape-is-head
(str/ffmt "Referenced shape % is a component, so the copy must also be" (:shape-ref shape))
shape file page))))
(defn- check-ref-is-head
"Validate that the referenced shape is a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(not (ctk/instance-head? ref-shape)))
(report-error :ref-shape-is-not-head
(str/ffmt "Referenced shape % of a head copy must also be a head" (:shape-ref shape))
shape file page
:component-file (:component-file ref-shape)
:component-id (:component-id ref-shape)))))
(defn- check-empty-swap-slot
"Validate that this shape does not have any swap slot."
[shape file page]
@@ -407,7 +378,6 @@
(check-component-not-main-head shape file page libraries)
(check-component-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-duplicate-swap-slot shape file page)
(check-valid-touched shape file page)
@@ -425,8 +395,7 @@
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
;; so we only validate the shape-ref if the ancestor is from a valid library
(when library-exists
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries))
(check-component-ref shape file page libraries))
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
(defn- check-shape-main-not-root
@@ -444,7 +413,6 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-not-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-valid-touched shape file page)
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
@@ -512,7 +480,7 @@
(report-error :variant-bad-name
(str/ffmt "Variant % has an invalid name" (:id shape))
shape file page))
(when-not (= (:name parent) (cpn/merge-path-item (:path component) (:name component)))
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
(report-error :variant-component-bad-name
(str/ffmt "Component % has an invalid name" (:id shape))
shape file page))
@@ -571,7 +539,7 @@
;; mains can't be nested into mains
(if (or (= context :not-component) (= context :main-top))
(report-error :nested-main-not-allowed
"Component main not allowed inside other component"
"Nested main component only allowed inside other component"
shape file page)
(check-shape-main-root-nested shape file page libraries))
@@ -636,20 +604,6 @@
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))))
(defn- check-main-inside-main
[component file]
(let [component-page (ctf/get-component-page (:data file) component)
main-instance (ctst/get-shape component-page (:main-instance-id component))
main-parents? (->> main-instance
:id
(cfh/get-parents (:objects component-page))
(some ctk/main-instance?)
boolean)]
(when main-parents?
(report-error :nested-main-not-allowed
"Component main not allowed inside other component"
main-instance file component-page))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
[component file]
@@ -657,8 +611,6 @@
(report-error :component-nil-objects-not-allowed
"Objects list cannot be nil"
component file nil))
(when-not (:deleted component)
(check-main-inside-main component file))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)
(check-ref-cycles component file))

View File

@@ -120,7 +120,6 @@
:tiered-file-data-storage
:token-units
:token-base-font-size
:token-color
:token-typography-types
:token-typography-composite
:transit-readable-response
@@ -159,7 +158,8 @@
:enable-component-thumbnails
:enable-render-wasm-dpr
:enable-token-units
:enable-token-typography-types])
:enable-token-typography-types
:enable-feature-fdata-objects-map])
(defn parse
[& flags]

View File

@@ -88,11 +88,8 @@
([shape]
(get-shape-filter-bounds shape false))
([shape ignore-shadow-margin?]
(if (or (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
;; If no shadows or blur, we return the selrect as is
(and (empty? (-> shape :shadow))
(zero? (-> shape :blur :value (or 0)))))
(if (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
(dm/get-prop shape :selrect)
(let [filters (shape->filters shape)
blur-value (or (-> shape :blur :value) 0)

View File

@@ -49,7 +49,6 @@
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[cuerdas.core :as str]
[promesa.exec :as px]
@@ -222,42 +221,36 @@
#?(:clj (inst-ms (java.time.Instant/now))
:cljs (js/Date.now)))
(defn emit-log
[props cause context logger level sync?]
(let [props (cond-> props sync? deref)
ts (current-timestamp)
gcontext *context*
logfn (fn []
(let [props (if sync? props (deref props))
props (into (d/ordered-map) props)
context (if (and (empty? gcontext)
(empty? context))
{}
(d/without-nils (merge gcontext context)))
lrecord {::id (uuid/next)
::timestamp ts
::message (delay (build-message props))
::props props
::context context
::level level
::logger logger}
lrecord (cond-> lrecord
(some? cause)
(assoc ::cause cause
::trace (delay (build-stack-trace cause))))]
(swap! log-record (constantly lrecord))))]
(if sync?
(logfn)
(px/exec! *default-executor* logfn))))
(defmacro log!
"Emit a new log record to the global log-record state (asynchronously). "
[& props]
(let [{:keys [::level ::logger ::context ::sync? cause] :or {sync? false}} props
props (into [] msg-props-xf props)]
`(when (enabled? ~logger ~level)
(emit-log (delay ~props) ~cause ~context ~logger ~level ~sync?))))
(let [props# (cond-> (delay ~props) ~sync? deref)
ts# (current-timestamp)
context# *context*
logfn# (fn []
(let [props# (if ~sync? props# (deref props#))
props# (into (d/ordered-map) props#)
cause# ~cause
context# (d/without-nils
(merge context# ~context))
lrecord# {::id (uuid/next)
::timestamp ts#
::message (delay (build-message props#))
::props props#
::context context#
::level ~level
::logger ~logger}
lrecord# (cond-> lrecord#
(some? cause#)
(assoc ::cause cause#
::trace (delay (build-stack-trace cause#))))]
(swap! log-record (constantly lrecord#))))]
(if ~sync?
(logfn#)
(px/exec! *default-executor* logfn#))))))
#?(:clj
(defn slf4j-log-handler
@@ -283,8 +276,7 @@
(when (enabled? logger level)
(let [hstyles (str/ffmt "font-weight: 600; color: %" (level->color level))
mstyles (str/ffmt "font-weight: 300; color: %" (level->color level))
ts (ct/format-inst (ct/now) "kk:mm:ss.SSSS")
header (str/concat "%c" (level->name level) " " ts " [" logger "] ")
header (str/concat "%c" (level->name level) " [" logger "] ")
message (str/concat header "%c" @message)]
(js/console.group message hstyles mstyles)

View File

@@ -11,13 +11,11 @@
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[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]
@@ -987,7 +985,7 @@
(defn generate-rename-component
"Generate the changes for rename the component with the given id, in the current file library."
[changes id new-name library-data]
(let [[path name] (cpn/split-group-name new-name)]
(let [[path name] (cfh/parse-path-name new-name)]
(-> changes
(pcb/with-library-data library-data)
(pcb/update-component id #(assoc % :path path :name name)))))
@@ -1735,17 +1733,6 @@
[(conj roperations roperation)
(conj uoperations uoperation)]))
(defn- check-detached-main
[changes dest-shape origin-shape]
;; Only for direct updates (from main to copy). Check if the main shape
;; has been detached. If so, the copy shape must be unheaded (i.e. converted
;; into a normal copy and not a nested instance).
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
(ctk/subcopy-head? dest-shape)
(not (ctk/instance-head? origin-shape)))
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
changes))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1786,8 +1773,6 @@
(seq roperations)
(add-update-attr-changes dest-shape container roperations uoperations)
:always
(check-detached-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(let [attr-group (get ctk/sync-attrs attr)
@@ -1811,6 +1796,7 @@
(= :content attr)
(touched attr-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
@@ -2236,7 +2222,7 @@
variant-id (when (ctk/is-variant? root) (:parent-id root))
props (when (ctk/is-variant? root) (get variant-props (:component-id root)))
[path name] (cpn/split-group-name name)
[path name] (cfh/parse-path-name name)
[root-shape updated-shapes]
(ctn/convert-shape-in-component root objects file-id)
@@ -2528,10 +2514,9 @@
frames)))
(defn- duplicate-variant
[changes library component base-pos parent page-id into-new-variant?]
[changes library component base-pos parent-id page-id]
(let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
objects (:objects component-page)
component-shape (get objects (:main-instance-id component))
component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract base-pos orig-pos)
new-component-id (uuid/next)
@@ -2541,27 +2526,11 @@
new-component-id
{:apply-changes-local-library? true
:delta delta
:new-variant-id (if into-new-variant? nil (:id parent))
:page-id page-id})
value (when into-new-variant?
(str ctv/value-prefix
(-> (cfv/extract-properties-values (:data library) objects (:id parent))
last
:value
count
inc)))]
:new-variant-id parent-id
:page-id page-id})]
[shape
(cond-> changes
into-new-variant?
(clvp/generate-make-shapes-variant [shape] parent)
;; If it has the same parent, update the value of the last property
(and into-new-variant? (= (:variant-id component) (:id parent)))
(clvp/generate-update-property-value new-component-id (-> component :variant-properties count dec) value)
:always
(pcb/change-parent (:id parent) [shape] 0))]))
(-> changes
(pcb/change-parent parent-id [shape]))]))
(defn generate-duplicate-component-change
@@ -2573,13 +2542,11 @@
pos (as-> (gsh/move main delta) $
(gpt/point (:x $) (:y $)))
parent (get objects parent-id)
;; When we duplicate a variant alone, we will instanciate it
;; When we duplicate a variant along with its variant-container, we will duplicate it
in-variant-container? (contains? ids-map (:variant-id main))
restore-component
#(let [{:keys [shape changes]}
(prepare-restore-component changes
@@ -2592,42 +2559,29 @@
frame-id)]
[shape changes])
[_shape changes]
(cond
(nil? component)
(if (nil? component)
(restore-component)
(if (and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent-id
(:id page))
(and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
false)
(ctk/is-variant-container? parent)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
true)
:else
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{}))]
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{})))]
changes))
(defn generate-duplicate-shape-change
@@ -2786,8 +2740,7 @@
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects)
(pcb/with-library-data library-data))
(pcb/with-objects all-objects))
changes
(->> shapes
(reduce #(generate-duplicate-shape-change %1

View File

@@ -185,17 +185,15 @@
interactions)))
(vals objects))
id-to-delete? (set ids-to-delete)
changes
(->> (:flows page)
(reduce
(fn [changes [id flow]]
(if (id-to-delete? (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes))
(reduce (fn [changes {:keys [id] :as flow}]
(if (contains? ids-to-delete (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes
(:flows page))
all-parents

View File

@@ -17,16 +17,18 @@
Use this for managing sets active state without having to modify a
user created theme (\"no themes selected\" state in the ui)."
[changes tokens-lib update-theme-fn]
(let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
(let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme' (-> (some-> hidden-theme
(ctob/set-sets active-token-set-names))
(update-theme-fn))]
prev-hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme (-> (some-> prev-hidden-theme (ctob/set-sets active-token-set-names))
(update-theme-fn))]
(-> changes
(pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')})
(pcb/set-token-theme (ctob/get-id hidden-theme)
hidden-theme'))))
(pcb/update-active-token-themes #{(ctob/theme-path hidden-theme)} prev-active-token-themes)
(pcb/set-token-theme (:group prev-hidden-theme)
(:name prev-hidden-theme)
hidden-theme))))
(defn generate-toggle-token-set
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
@@ -137,12 +139,3 @@
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
changes))
(defn generate-delete-token-set-group
"Create changes for deleting a token set group."
[changes tokens-lib path]
(let [sets (ctob/get-sets-at-path tokens-lib path)]
(reduce (fn [changes set]
(pcb/set-token-set changes (ctob/get-id set) nil))
changes
sets)))

View File

@@ -7,8 +7,8 @@
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctcl]
[app.common.types.variant :as ctv]
@@ -127,7 +127,7 @@
(defn- generate-make-shape-no-variant
[changes shape]
(let [new-name (ctv/variant-name-to-name shape)
[cpath cname] (cpn/split-group-name new-name)]
[cpath cname] (cfh/parse-path-name new-name)]
(-> changes
(pcb/update-component (:component-id shape)
#(-> (dissoc % :variant-id :variant-properties)
@@ -146,8 +146,8 @@
(defn- create-new-properties-from-variant
[shape min-props data container-name base-properties]
(let [component (ctcl/get-component data (:component-id shape) true)
component-full-name (cpn/merge-path-item (:path component) (:name component))
add-name? (not= component-full-name container-name)
add-name? (not= (:name component) container-name)
props (ctv/merge-properties base-properties
(:variant-properties component))
new-props (- min-props
@@ -188,7 +188,7 @@
(map #(assoc % :value "")))
num-base-props (count base-props)
[cpath cname] (cpn/split-group-name (:name variant-container))
[cpath cname] (cfh/parse-path-name (:name variant-container))
container-name (:name variant-container)
create-new-properties

View File

@@ -1,134 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.path-names
(:require
[cuerdas.core :as str]))
"Functions to manipulate entity names that represent groups with paths,
e.g. 'Group / Subgroup / Name'.
Some naming conventions:
- Path string: the full string with groups and name, e.g. 'Group / Subgroup / Name'.
- Path: a vector of strings with the full path, e.g. ['Group' 'Subgroup' 'Name'].
- Group string: the group part of the path string, e.g. 'Group / Subgroup'.
- Group: a vector of strings with the group part of the path, e.g. ['Group' 'Subgroup'].
- Name: the final name part of the path, e.g. 'Name'."
(defn split-path
"Decompose a path string in the form 'one / two / three' into a vector
of strings, trimming spaces (e.g. ['one' 'two' 'three'])."
[path-str & {:keys [separator] :or {separator "/"}}]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path-str separator)
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector.
(e.g. ['one' 'two' 'three'] -> 'one / two / three')"
[path & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(if with-spaces?
(str/join (str " " separator " ") path)
(str/join separator path)))
(defn split-group-name
"Parse a path string. Retrieve the group and the name in separated values,
normalizing spaces (e.g. 'group / subgroup / name' -> ['group / subgroup' 'name'])."
[path-str & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(let [path (split-path path-str :separator separator)
group-str (join-path (butlast path) :separator separator :with-spaces? with-spaces?)
name (or (last path) "")]
[group-str name]))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))

View File

@@ -420,54 +420,62 @@
:min 0
:max 1
:compile
(fn [{:keys [kind max min] :as props} children _]
(fn [{:keys [kind max min ordered] :as props} children _]
(let [kind (or (last children) kind)
pred
child-pred
(cond
(fn? kind) kind
(nil? kind) any?
:else (validator kind))
type-pred
(if ordered
d/ordered-set?
set?)
pred
(cond
(and max min)
(fn [value]
(let [size (count value)]
(and (set? value)
(<= min size max)
(every? pred value))))
(and (type-pred value)
(every? child-pred value)
(<= min (count value) max)))
min
(fn [value]
(let [size (count value)]
(and (set? value)
(<= min size)
(every? pred value))))
(and (type-pred value)
(every? child-pred value)
(<= min (count value))))
max
(fn [value]
(let [size (count value)]
(and (set? value)
(<= size max)
(every? pred value))))
(and (type-pred value)
(every? child-pred value)
(<= (count value) max)))
:else
(fn [value]
(every? pred value)))
(and (type-pred value)
(every? child-pred value))))
empty-set
(if ordered
(d/ordered-set)
#{})
decode
(fn [v]
(cond
(string? v)
(let [v (str/split v #"[\s,]+")]
(into #{} xf:filter-word-strings v))
(into empty-set xf:filter-word-strings v))
(set? v)
v
(coll? v)
(into #{} v)
(into empty-set v)
:else
v))

View File

@@ -12,7 +12,6 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.path-names :as cpn]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
@@ -37,7 +36,7 @@
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
[path name] (cpn/split-group-name (:name updated-root))]
[path name] (cfh/parse-path-name (:name updated-root))]
(thi/set-id! label (:component-id updated-root))
(ctf/update-file-data
@@ -73,10 +72,6 @@
[file id]
(ctkl/get-component (:data file) id))
(defn get-components
[file]
(ctkl/components (:data file)))
(defn- set-children-labels!
[file shape-label children-labels]
(doseq [[label id]

View File

@@ -108,8 +108,7 @@
page (if (some? page-label)
(:id (get-page file page-label))
(current-page-id file))
libraries (or libraries
{(:id file) file})]
libraries (or libraries {})]
(ctf/dump-tree file page libraries params)))

View File

@@ -28,10 +28,12 @@
(ctf/update-file-data file #(update % :tokens-lib f)))
(defn get-token
[file set-id token-id]
[file set-name token-id]
(let [tokens-lib (:tokens-lib (:data file))]
(when tokens-lib
(ctob/get-token tokens-lib set-id token-id))))
(-> tokens-lib
(ctob/get-set set-name)
(ctob/get-token token-id)))))
(defn token-data-eq?
"Compare token data without comparing unstable fields."

View File

@@ -130,6 +130,7 @@
ms-or-obj
(integer? ms-or-obj)
(Duration/ofMillis ms-or-obj)
:else
@@ -432,4 +433,4 @@
#?(:cljs
(extend-protocol cljs.core/IEncodeJS
js/Date
(-clj->js [x] x)))
(-clj->js [x] x)))

View File

@@ -145,12 +145,9 @@
(defn component-attr?
"Check if some attribute is one that is involved in component syncrhonization.
Note that design tokens also are involved, although they go by an alternate
route and thus they are not part of :sync-attrs.
Also when detaching a nested copy it also needs to trigger a synchronization,
even though :shape-ref is not a synced attribute per se"
route and thus they are not part of :sync-attrs."
[attr]
(or (get sync-attrs attr)
(= :shape-ref attr)
(= :applied-tokens attr)))
(defn instance-root?
@@ -220,16 +217,19 @@
(and (= shape-id (:main-instance-id component))
(= page-id (:main-instance-page component))))
(defn is-variant?
"Check if this shape or component is a variant component"
[item]
(some? (:variant-id item)))
(defn is-variant-container?
"Check if this shape is a variant container"
[shape]
(:is-variant-container shape))
(defn set-touched-group
[touched group]
(when group
@@ -310,22 +310,6 @@
:shape-ref
:touched))
(defn unhead-shape
"Make the shape not be a component head, but keep its :shape-ref and :touched if it was a nested copy"
[shape]
(dissoc shape
:component-root
:component-file
:component-id
:main-instance))
(defn rehead-shape
"Make the shape a component head, by adding component info"
[shape component-file component-id]
(assoc shape
:component-file component-file
:component-id component-id))
(defn- extract-ids [shape]
(if (map? shape)
(let [current-id (:id shape)

View File

@@ -7,6 +7,7 @@
(ns app.common.types.container
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@@ -76,8 +77,11 @@
(defn get-shape
[container shape-id]
(assert (check-container container))
(assert (uuid? shape-id)
"expected valid uuid for `shape-id`")
(-> container
(get :objects)
(get shape-id)))
@@ -490,40 +494,29 @@
all-main?
(every? ctk/main-instance? top-children)
ascendants (cfh/get-parents-with-self objects parent-id)
any-main-ascendant (some ctk/main-instance? ascendants)
any-variant-container-ascendant (some ctk/is-variant-container? ascendants)
get-variant-id (fn [shape]
(when (:component-id shape)
(-> (get-component-from-shape shape libraries)
:variant-id)))
descendants (mapcat #(cfh/get-children-with-self objects %) children-ids)
any-variant-container-descendant (some ctk/is-variant-container? descendants)
descendants-variant-ids-set (->> descendants
(map get-variant-id)
set)
any-main-descendant
(some
(fn [shape]
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
children)]
children)
;; Are all the top-children a main-instance of a cutted component?
all-comp-cut?
(when all-main?
(->> top-children
(map #(ctkl/get-component (dm/get-in libraries [(:component-file %) :data])
(:component-id %)
true))
(every? :deleted)))]
(if (or no-changes?
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
;; If we are moving (not pasting) into a main component, no descendant can be main
(or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent)))
;; Don't allow variant-container inside variant container nor main
(or (not any-variant-container-descendant)
(and (not any-variant-container-ascendant) (not any-main-ascendant)))
;; If the parent is a variant-container, all the items should be main
(or (not (ctk/is-variant-container? parent)) all-main?)
;; If we are pasting, the parent can't be a "brother" of any of the pasted items,
;; so not have the same variant-id of any descendant
(or (not pasting?)
(not (ctk/is-variant? parent))
(not (contains? descendants-variant-ids-set (:variant-id parent))))))
;; If we are moving into a main component, no descendant can be main
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))
;; If we are moving into a variant-container, all the items should be main
;; so if we are pasting, only allow main instances that are cut-and-pasted
(or (not (ctk/is-variant-container? parent))
(and (not pasting?) all-main?)
all-comp-cut?)))
[parent-id (get-frame parent-id)]
(recur (:parent-id parent) objects children pasting? libraries))))))

View File

@@ -106,7 +106,7 @@
[:version :int]
[:features ::cfeat/features]
[:migrations {:optional true}
[::sm/set :string]]])
[::sm/set {:ordered true} :string]]])
(sm/register! ::data schema:data)
(sm/register! ::file schema:file)

View File

@@ -26,27 +26,6 @@
(mu/keys)
(into #{})))
(defn find-token-value-references
"Returns set of token references found in `token-value`.
Used for checking if a token has a reference in the value.
Token references are strings delimited by curly braces.
E.g.: {foo.bar.baz} -> foo.bar.baz"
[token-value]
(if (string? token-value)
(some->> (re-seq #"\{([^}]*)\}" token-value)
(map second)
(into #{}))
#{}))
(defn token-value-self-reference?
"Check if the token is self referencing with its `token-name` in `token-value`.
Simple 1 level check, doesn't account for circular self refernces across multiple tokens."
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
self-reference?))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -73,24 +52,7 @@
:typography "typography"})
(def dtcg-token-type->token-type
(-> (set/map-invert token-type->dtcg-token-type)
;; Allow these properties to be imported with singular key names for backwards compability
(assoc "fontWeight" :font-weight
"fontSize" :font-size
"fontFamily" :font-family)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
(set/map-invert token-type->dtcg-token-type))
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
@@ -255,8 +217,7 @@
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys
#{:line-height}))
typography-token-keys))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
@@ -328,7 +289,6 @@
(font-size-keys shape-attr) #{shape-attr :typography}
(letter-spacing-keys shape-attr) #{shape-attr :typography}
(font-family-keys shape-attr) #{shape-attr :typography}
(= :line-height shape-attr) #{:line-height :typography}
(= :text-transform shape-attr) #{:text-case :typography}
(text-decoration-keys shape-attr) #{shape-attr :typography}
(font-weight-keys shape-attr) #{shape-attr :typography}
@@ -508,30 +468,3 @@
(when (font-weight-values weight)
(cond-> {:weight weight}
italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
:height #{:sizing :dimensions}
:max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions}
:x #{:spacing :dimensions}
:y #{:spacing :dimensions}
:rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions}
:column-gap #{:spacing :dimensions}
:horizontal-padding #{:spacing :dimensions}
:vertical-padding #{:spacing :dimensions}
:sided-paddings #{:spacing :dimensions}
:horizontal-margin #{:spacing :dimensions}
:vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}})

View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
(ns app.common.types.variant
(:require
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.math :as math]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[cuerdas.core :as str]))
@@ -50,6 +50,7 @@
(def property-max-length 60)
(def value-prefix "Value ")
(defn properties-to-name
"Transform the properties into a name, with the values separated by comma"
[properties]
@@ -58,6 +59,7 @@
(remove str/empty?)
(str/join ", ")))
(defn next-property-number
"Returns the next property number, to avoid duplicates on the property names"
[properties]
@@ -90,7 +92,7 @@
([path properties]
(path-to-properties path properties 0))
([path properties min-props]
(let [cpath (cpn/split-path path)
(let [cpath (cfh/split-path path)
total-props (max (count cpath) min-props)
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
;; Add empty strings to the end of cpath to reach the minimum number of properties
@@ -98,6 +100,7 @@
remaining (drop (count properties) cpath)]
(add-new-props assigned remaining))))
(defn properties-map->formula
"Transforms a map of properties to a formula of properties omitting the empty ones"
[properties]
@@ -107,6 +110,7 @@
(str name "=" value))))
(str/join ", ")))
(defn properties-formula->map
"Transforms a formula of properties to a map of properties"
[s]
@@ -117,6 +121,7 @@
{:name (str/trim k)
:value (str/trim v)}))))
(defn valid-properties-formula?
"Checks if a formula is valid"
[s]
@@ -133,18 +138,21 @@
(let [upd-names (set (map :name upd-props))]
(filterv #(not (contains? upd-names (:name %))) prev-props)))
(defn find-properties-to-update
"Compares two property maps to find which properties should be updated"
[prev-props upd-props]
(filterv #(some (fn [prop] (and (= (:name %) (:name prop))
(not= (:value %) (:value prop)))) prev-props) upd-props))
(defn find-properties-to-add
"Compares two property maps to find which properties should be added"
[prev-props upd-props]
(let [prev-names (set (map :name prev-props))]
(filterv #(not (contains? prev-names (:name %))) upd-props)))
(defn- split-base-name-and-number
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
[item]
@@ -184,6 +192,7 @@
:value (:value prop)}))
[])))
(defn find-index-for-property-name
"Finds the index of a name in a property map"
[props name]
@@ -309,4 +318,4 @@
"Transforms a variant-name (its properties values) into a standard name:
the real name of the shape joined by the properties values separated by '/'"
[variant]
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
(cfh/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))

View File

@@ -22,7 +22,7 @@
#?(:cljs
(defn weak-map
"Create a WeakMap-like instance what uses clojure equality
"Create a WeakMap like instance what uses clojure equality
semantics."
[]
(new wm/WeakEqMap #js {:hash hash :equals =})))

View File

@@ -14,6 +14,7 @@
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.pprint :refer [pprint]]
[clojure.test :as t]
[common-tests.types.shape-decode-encode-test :refer [json-roundtrip]]))

View File

@@ -446,35 +446,3 @@
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))
(t/deftest test-detach-copy-in-main
(let [;; ==== Setup
file (-> (setup-file)
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested-h-ellipse
:copy-nested-ellipse]))
page (thf/current-page file)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :nested-h-ellipse))
file' (-> (thf/apply-changes file changes)
(tho/propagate-component-changes :c-board-with-ellipse)
(tho/propagate-component-changes :c-big-board))
;; ==== Get
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
copy-nested2-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse)]
;; ==== Check
;; When the nested copy inside the main is detached, their copies are unheaded.
(t/is (not (ctk/subcopy-head? nested2-h-ellipse)))
(t/is (not (ctk/subcopy-head? copy-nested2-h-ellipse)))))

View File

@@ -144,21 +144,20 @@
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib #(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
:name "test-token-set"))
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
:sets #{"test-token-set"}))
(ctob/set-active-themes #{"/test-theme"})
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(tho/add-frame :frame-1
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic

View File

@@ -27,66 +27,65 @@
(-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib #(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
:name "test-token-set"))
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
:sets #{"test-token-set"}))
(ctob/set-active-themes #{"/test-theme"})
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-radius)
:name "token-radius"
:type :border-radius
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-rotation)
:name "token-rotation"
:type :rotation
:value 30))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-opacity)
:name "token-opacity"
:type :opacity
:value 0.7))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-stroke-width)
:name "token-stroke-width"
:type :stroke-width
:value 2))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-color)
:name "token-color"
:type :color
:value "#00ff00"))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-dimensions)
:name "token-dimensions"
:type :dimensions
:value 100))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-font-size)
:name "token-font-size"
:type :font-size
:value 24))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
:name "token-letter-spacing"
:type :letter-spacing
:value 2))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-font-family)
:name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-radius)
:name "token-radius"
:type :border-radius
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-rotation)
:name "token-rotation"
:type :rotation
:value 30))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-opacity)
:name "token-opacity"
:type :opacity
:value 0.7))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-stroke-width)
:name "token-stroke-width"
:type :stroke-width
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-color)
:name "token-color"
:type :color
:value "#00ff00"))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-dimensions)
:name "token-dimensions"
:type :dimensions
:value 100))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-font-size)
:name "token-font-size"
:type :font-size
:value 24))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
:name "token-letter-spacing"
:type :letter-spacing
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-font-family)
:name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(tho/add-frame :frame1
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic
@@ -132,17 +131,17 @@
frame1 (ths/get-shape file :frame1)
text1 (ths/get-shape file :text1)
circle1 (ths/get-shape file :circle1)
token-radius (tht/get-token file (thi/id :test-token-set) (thi/id :token-radius))
token-rotation (tht/get-token file (thi/id :test-token-set) (thi/id :token-rotation))
token-opacity (tht/get-token file (thi/id :test-token-set) (thi/id :token-opacity))
token-stroke-width (tht/get-token file (thi/id :test-token-set) (thi/id :token-stroke-width))
token-color (tht/get-token file (thi/id :test-token-set) (thi/id :token-color))
token-dimensions (tht/get-token file (thi/id :test-token-set) (thi/id :token-dimensions))
token-font-size (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-size))
token-letter-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-letter-spacing))
token-font-family (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-family))
token-sizing (tht/get-token file (thi/id :test-token-set) (thi/id :token-sizing))
token-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-spacing))
token-radius (tht/get-token file "test-token-set" (thi/id :token-radius))
token-rotation (tht/get-token file "test-token-set" (thi/id :token-rotation))
token-opacity (tht/get-token file "test-token-set" (thi/id :token-opacity))
token-stroke-width (tht/get-token file "test-token-set" (thi/id :token-stroke-width))
token-color (tht/get-token file "test-token-set" (thi/id :token-color))
token-dimensions (tht/get-token file "test-token-set" (thi/id :token-dimensions))
token-font-size (tht/get-token file "test-token-set" (thi/id :token-font-size))
token-letter-spacing (tht/get-token file "test-token-set" (thi/id :token-letter-spacing))
token-font-family (tht/get-token file "test-token-set" (thi/id :token-font-family))
token-sizing (tht/get-token file "test-token-set" (thi/id :token-sizing))
token-spacing (tht/get-token file "test-token-set" (thi/id :token-spacing))
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)

View File

@@ -13,7 +13,6 @@
[app.common.test-helpers.tokens :as tht]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
@@ -34,7 +33,6 @@
(pcb/with-library-data (:data file))
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
_ (prn "changes" changes)
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
@@ -85,133 +83,127 @@
(t/deftest set-token-theme-test
(t/testing "delete token theme"
(let [theme-id (uuid/next)
(let [theme-name "foo"
group "main"
file (setup-file #(-> %
(ctob/add-theme (ctob/make-token-theme :id theme-id
:name "foo"
:group "main"))))
(ctob/add-theme (ctob/make-token-theme :name theme-name
:group group))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme theme-id nil))
(pcb/set-token-theme group theme-name nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
;; Redo
(t/is (nil? (ctob/get-theme redo-lib theme-id)))
(t/is (nil? (ctob/get-theme redo-lib group theme-name)))
;; Undo
(t/is (some? (ctob/get-theme undo-lib theme-id)))))
(t/is (some? (ctob/get-theme undo-lib group theme-name)))))
(t/testing "add token theme"
(let [theme-id (uuid/next)
theme (ctob/make-token-theme :id theme-id
:name "foo"
:group "main")
(let [theme-name "foo"
group "main"
theme (ctob/make-token-theme :name theme-name
:group group)
file (setup-file identity)
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme theme-id theme))
(pcb/set-token-theme group theme-name theme))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
;; Redo
(t/is (some? (ctob/get-theme redo-lib theme-id)))
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
;; Undo
(t/is (nil? (ctob/get-theme undo-lib theme-id)))))
(t/is (nil? (ctob/get-theme undo-lib group theme-name)))))
(t/testing "update token theme"
(let [theme-id (uuid/next)
prev-theme-name "foo"
prev-theme (ctob/make-token-theme :id theme-id
:name prev-theme-name
:group "main")
(let [theme-name "foo"
group "main"
prev-theme (ctob/make-token-theme :name theme-name
:group group)
file (setup-file #(ctob/add-theme % prev-theme))
new-theme-name "foo1"
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme theme-id (ctob/rename prev-theme new-theme-name)))
(pcb/set-token-theme group new-theme-name prev-theme))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
redo-theme (ctob/get-theme redo-lib theme-id)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)
undo-theme (ctob/get-theme undo-lib theme-id)]
undo-lib (tht/get-tokens-lib undo)]
;; Redo
(t/is (= new-theme-name (ctob/get-name redo-theme)))
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
(t/is (nil? (ctob/get-theme redo-lib group new-theme-name)))
;; Undo
(t/is (= prev-theme-name (ctob/get-name undo-theme)))))
(t/is (some? (ctob/get-theme undo-lib group theme-name)))
(t/is (nil? (ctob/get-theme undo-lib group new-theme-name)))))
(t/testing "toggling token theme updates using changes history"
(let [theme-id (uuid/next)
theme (ctob/make-token-theme :id theme-id
:name "foo-theme"
:group "main")
(let [theme-name "foo-theme"
group "main"
set-name "bar-set"
token-set (ctob/make-token-set :name set-name)
theme (ctob/make-token-theme :name theme-name
:group group)
file (setup-file #(-> %
(ctob/add-theme theme)
(ctob/add-set token-set)))
theme' (assoc theme :sets #{set-name})
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme theme-id theme'))
(pcb/set-token-theme group theme-name theme'))
changed-file (-> file
(thf/apply-changes changes)
(thf/apply-undo-changes changes)
(thf/apply-changes changes))
changed-lib (tht/get-tokens-lib changed-file)]
(t/is (= #{set-name}
(-> changed-lib (ctob/get-theme theme-id) :sets))))))
(-> changed-lib (ctob/get-theme group theme-name) :sets))))))
(t/deftest set-token-test
(t/testing "delete token"
(let [set-name "foo"
set-id (uuid/next)
token-id (uuid/next)
file (setup-file #(-> %
(ctob/add-set (ctob/make-token-set :id set-id
:name set-name))
(ctob/add-token set-id (ctob/make-token {:name "to.delete.color.red"
:id token-id
:value "red"
:type :color}))))
(ctob/add-set (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name (ctob/make-token {:name "to.delete.color.red"
:id token-id
:value "red"
:type :color}))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-id token-id nil))
(pcb/set-token set-name token-id nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (nil? (ctob/get-token redo-lib set-id token-id)))
(t/is (nil? (ctob/get-token-in-set redo-lib set-name token-id)))
;; Undo
(t/is (some? (ctob/get-token undo-lib set-id token-id)))))
(t/is (some? (ctob/get-token-in-set undo-lib set-name token-id)))))
(t/testing "add token"
(let [set-name "foo"
set-id (uuid/next)
token (ctob/make-token {:name "to.add.color.red"
:value "red"
:type :color})
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :id set-id
:name set-name))))
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :name set-name))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-id (:id token) token))
(pcb/set-token set-name (:id token) token))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (= token (ctob/get-token redo-lib set-id (:id token))))
(t/is (= token (ctob/get-token-in-set redo-lib set-name (:id token))))
;; Undo
(t/is (nil? (ctob/get-token undo-lib set-id (:id token))))))
(t/is (nil? (ctob/get-token-in-set undo-lib set-name (:id token))))))
(t/testing "update token"
(let [set-name "foo"
set-id (uuid/next)
prev-token (ctob/make-token {:name "to.update.color.red"
:value "red"
:type :color})
@@ -219,29 +211,27 @@
(assoc :name "color.red.changed")
(assoc :value "blue"))
file (setup-file #(-> %
(ctob/add-set (ctob/make-token-set :id set-id
:name set-name))
(ctob/add-token set-id prev-token)))
(ctob/add-set (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name prev-token)))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-id (:id prev-token) token))
(pcb/set-token set-name (:id prev-token) token))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (tht/token-data-eq? token (ctob/get-token redo-lib set-id (:id token))))
(t/is (tht/token-data-eq? token (ctob/get-token-in-set redo-lib set-name (:id token))))
;; Undo
(t/is (tht/token-data-eq? prev-token (ctob/get-token undo-lib set-id (:id prev-token)))))))
(t/is (tht/token-data-eq? prev-token (ctob/get-token-in-set undo-lib set-name (:id prev-token)))))))
(t/deftest set-token-set-test
(t/testing "delete token set"
(let [set-name "foo"
set-id (uuid/next)
file (setup-file #(ctob/add-set % (ctob/make-token-set :id set-id :name set-name)))
file (setup-file #(ctob/add-set % (ctob/make-token-set :name set-name)))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-id nil))
(pcb/set-token-set set-name false nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
@@ -253,12 +243,11 @@
(t/testing "add token set"
(let [set-name "foo"
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name set-name)
token-set (ctob/make-token-set :name set-name)
file (setup-file identity)
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-id token-set))
(pcb/set-token-set set-name false token-set))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
@@ -270,26 +259,28 @@
(t/testing "update token set"
(let [set-name "foo"
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name set-name)
file (setup-file #(-> (ctob/add-set % token-set)))
token-name "bar"
token (ctob/make-token {:name token-name
:value "red"
:type :color})
file (setup-file #(-> (ctob/add-set % (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name token)))
prev-token-set (-> file tht/get-tokens-lib (ctob/get-set set-name))
new-set-name "foo1"
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-id (ctob/rename token-set new-set-name)))
(pcb/set-token-set set-name false (ctob/rename prev-token-set new-set-name)))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
redo-token-set (ctob/get-set redo-lib set-id)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)
undo-token-set (ctob/get-set undo-lib set-id)]
undo-lib (tht/get-tokens-lib undo)]
(t/is (= (ctob/get-name redo-token-set) new-set-name))
;; Undo
(t/is (= (ctob/get-name undo-token-set) set-name)))))
(t/is (some? (ctob/get-token-in-set undo-lib set-name (:id token))))
(t/is (nil? (ctob/get-token-in-set undo-lib new-set-name (:id token))))
;; Redo
(t/is (nil? (ctob/get-token-in-set redo-lib set-name (:id token))))
(t/is (some? (ctob/get-token-in-set redo-lib new-set-name (:id token)))))))
(t/deftest generate-toggle-token-set-group-test
(t/testing "toggling set group with no active sets inside will activate all child sets"
@@ -370,13 +361,13 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar" "foo" "baz"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "at bottom"
(let [file (setup-file #(-> %
@@ -389,13 +380,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar" "baz" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "dropping out of set group"
(let [file (setup-file #(-> %
@@ -407,13 +398,13 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "into set group"
(let [file (setup-file #(-> %
@@ -425,13 +416,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["foo/bar" "foo/foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "edge-cases:"
(t/testing "prevent overriding set to identical path"
@@ -463,13 +454,13 @@
:collapsed-paths #{["foo"]}})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["foo/bar" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets))))))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))))))
(t/deftest generate-move-token-group-test
(t/testing "Ignore dropping set group to the same position"
@@ -505,14 +496,14 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar/bar" "foo/foo" "baz/baz"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "to bottom"
(let [file (setup-file #(-> %
@@ -524,14 +515,14 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar" "foo/foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/testing "into set group"
(let [file (setup-file #(-> %
@@ -543,13 +534,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-set-names))
(ctob/get-ordered-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-set-names))]
(ctob/get-ordered-set-names))]
(t/is (= ["bar/foo/foo" "bar/bar"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-set-names lib) undo-sets))))
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))
(t/testing "edge-cases:"
(t/testing "prevent overriding set to identical path"

View File

@@ -0,0 +1,19 @@
;; 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 common-tests.pages-helpers-test
(:require
[app.common.files.helpers :as cfh]
[clojure.pprint :refer [pprint]]
[clojure.test :as t]))
(t/deftest parse-path-name
(t/is (= ["foo" "bar"] (cfh/parse-path-name "foo/bar")))
(t/is (= ["" "foo"] (cfh/parse-path-name "foo")))
(t/is (= ["" "foo"] (cfh/parse-path-name "/foo")))
(t/is (= ["" ""] (cfh/parse-path-name "")))
(t/is (= ["" ""] (cfh/parse-path-name nil))))

View File

@@ -1,33 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.path-names-test
(:require
[app.common.path-names :as cpn]
[clojure.test :as t]))
(t/deftest split-group-name
(t/is (= ["foo" "bar"] (cpn/split-group-name "foo/bar")))
(t/is (= ["" "foo"] (cpn/split-group-name "foo")))
(t/is (= ["" "foo"] (cpn/split-group-name "/foo")))
(t/is (= ["" ""] (cpn/split-group-name "")))
(t/is (= ["" ""] (cpn/split-group-name nil))))
(t/deftest split-and-join-path
(let [name "group/subgroup/name"
path (cpn/split-path name :separator "/")
name' (cpn/join-path path :separator "/" :with-spaces? false)]
(t/is (= (first path) "group"))
(t/is (= (second path) "subgroup"))
(t/is (= (nth path 2) "name"))
(t/is (= name' name))))
(t/deftest split-and-join-path-with-spaces
(let [name "group / subgroup / name"
path (cpn/split-path name :separator "/")]
(t/is (= (first path) "group"))
(t/is (= (second path) "subgroup"))
(t/is (= (nth path 2) "name"))))

View File

@@ -30,7 +30,7 @@
[common-tests.logic.swap-as-override-test]
[common-tests.logic.token-test]
[common-tests.media-test]
[common-tests.path-names-test]
[common-tests.pages-helpers-test]
[common-tests.record-test]
[common-tests.schema-test]
[common-tests.svg-path-test]
@@ -82,7 +82,7 @@
'common-tests.logic.swap-as-override-test
'common-tests.logic.token-test
'common-tests.media-test
'common-tests.path-names-test
'common-tests.pages-helpers-test
'common-tests.record-test
'common-tests.schema-test
'common-tests.svg-path-test

View File

@@ -6,6 +6,7 @@
(ns common-tests.schema-test
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[clojure.test :as t]))
@@ -35,6 +36,77 @@
(t/is (true? (sm/validate schema #{})))
(t/is (false? (sm/validate schema #{"a"})))))
(t/testing "validate 2"
(let [candidate-1 ["a@b.com" "a@c.net"]
candidate-2 (into #{} candidate-1)
candidate-3 (into (d/ordered-set) candidate-1)
candidate-4 #{"a@b.com"}
candidate-5 (d/ordered-set "a@b.com")
schema-1 [::sm/set ::sm/email]
schema-2 [::sm/set {:ordered true} ::sm/email]
schema-3 [::sm/set {:ordered true :min 1} ::sm/email]
schema-4 [::sm/set {:min 1} ::sm/email]
schema-5 [::sm/set {:ordered true :max 1} ::sm/email]
schema-6 [::sm/set {:ordered true :min 1 :max 2} ::sm/email]
schema-7 [::sm/set {:min 1 :max 2} ::sm/email]]
(t/is (false? (sm/validate schema-1 [])))
(t/is (false? (sm/validate schema-1 candidate-1)))
(t/is (true? (sm/validate schema-1 candidate-2)))
(t/is (true? (sm/validate schema-1 candidate-3)))
(t/is (false? (sm/validate schema-2 [])))
(t/is (false? (sm/validate schema-2 candidate-1)))
(t/is (false? (sm/validate schema-2 candidate-2)))
(t/is (true? (sm/validate schema-2 candidate-3)))
(t/is (false? (sm/validate schema-3 [])))
(t/is (false? (sm/validate schema-3 candidate-1)))
(t/is (false? (sm/validate schema-3 candidate-2)))
(t/is (true? (sm/validate schema-3 candidate-3)))
(t/is (false? (sm/validate schema-3 candidate-4)))
(t/is (true? (sm/validate schema-3 candidate-5)))
(t/is (false? (sm/validate schema-3 (d/ordered-set))))
(t/is (false? (sm/validate schema-4 [])))
(t/is (false? (sm/validate schema-4 candidate-1)))
(t/is (true? (sm/validate schema-4 candidate-2)))
(t/is (true? (sm/validate schema-4 candidate-3)))
(t/is (true? (sm/validate schema-4 candidate-4)))
(t/is (true? (sm/validate schema-4 candidate-5)))
(t/is (false? (sm/validate schema-4 (d/ordered-set))))
(t/is (false? (sm/validate schema-4 #{})))
(t/is (false? (sm/validate schema-5 [])))
(t/is (false? (sm/validate schema-5 candidate-1)))
(t/is (false? (sm/validate schema-5 candidate-2)))
(t/is (false? (sm/validate schema-5 candidate-3)))
(t/is (false? (sm/validate schema-5 candidate-4)))
(t/is (true? (sm/validate schema-5 candidate-5)))
(t/is (true? (sm/validate schema-5 (d/ordered-set))))
(t/is (false? (sm/validate schema-5 #{})))
(t/is (false? (sm/validate schema-6 [])))
(t/is (false? (sm/validate schema-6 candidate-1)))
(t/is (false? (sm/validate schema-6 candidate-2)))
(t/is (true? (sm/validate schema-6 candidate-3)))
(t/is (false? (sm/validate schema-6 candidate-4)))
(t/is (true? (sm/validate schema-6 candidate-5)))
(t/is (false? (sm/validate schema-6 (d/ordered-set))))
(t/is (false? (sm/validate schema-6 #{})))
(t/is (false? (sm/validate schema-6 (conj candidate-3 "r@r.com"))))
(t/is (false? (sm/validate schema-7 [])))
(t/is (false? (sm/validate schema-7 candidate-1)))
(t/is (true? (sm/validate schema-7 candidate-2)))
(t/is (true? (sm/validate schema-7 candidate-3)))
(t/is (true? (sm/validate schema-7 candidate-4)))
(t/is (true? (sm/validate schema-7 candidate-5)))
(t/is (false? (sm/validate schema-7 (d/ordered-set))))
(t/is (false? (sm/validate schema-7 #{})))
(t/is (false? (sm/validate schema-7 (conj candidate-2 "r@r.com"))))
(t/is (false? (sm/validate schema-7 (conj candidate-3 "r@r.com"))))))
(t/testing "generate"
(let [schema [::sm/set ::sm/email]
value (sg/generate schema)]

View File

@@ -1,26 +0,0 @@
{
"fonts": {
"string-font-family": {
"$value": "Arial, Helvetica, sans-serif",
"$type": "fontFamilies",
"$description": "A font family defined as a string"
},
"array-font-family": {
"$value": ["Inter", "system-ui", "sans-serif"],
"$type": "fontFamilies",
"$description": "A font family defined as an array"
},
"single-font-family": {
"$value": "Georgia",
"$type": "fontFamilies"
},
"complex-font-family": {
"$value": "Times New Roman, serif",
"$type": "fontFamilies"
},
"font-with-spaces": {
"$value": "Source Sans Pro, Arial, sans-serif",
"$type": "fontFamilies"
}
}
}

View File

@@ -1,53 +0,0 @@
{
"test": {
"typo": {
"$value": {
"fontWeight": "100",
"fontSize": "16px",
"letterSpacing": "0.1em"
},
"$type": "typography"
},
"typo2": {
"$value": "{typo}",
"$type": "typography"
},
"font-weight": {
"$value": "200",
"$type": "fontWeights"
},
"typo-to-single": {
"$value": "{font-weight}",
"$type": "typography"
},
"test-empty": {
"$value": {},
"$type": "typography"
},
"font-size": {
"$value": "18px",
"$type": "fontSizes"
},
"typo-complex": {
"$value": {
"fontWeight": "bold",
"fontSize": "24px",
"letterSpacing": "0.05em",
"lineHeights": "100%",
"fontFamilies": ["Arial", "sans-serif"],
"textCase": "uppercase"
},
"$type": "typography",
"$description": "A complex typography token"
},
"typo-with-string-font-family": {
"$value": {
"fontWeight": "600",
"fontSize": "20px",
"fontFamilies": "Roboto, Helvetica, sans-serif"
},
"$type": "typography",
"$description": "Typography token with string font family"
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -113,12 +113,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@@ -183,8 +183,8 @@ RUN set -eux; \
FROM base AS setup-utils
ENV CLJKONDO_VERSION=2025.07.28 \
BABASHKA_VERSION=1.12.208 \
ENV CLJKONDO_VERSION=2025.01.16 \
BABASHKA_VERSION=1.12.207 \
CLJFMT_VERSION=0.13.1
RUN set -ex; \

View File

@@ -118,7 +118,6 @@ http {
location /api {
proxy_pass http://127.0.0.1:6060/api;
proxy_buffering off;
proxy_http_version 1.1;
}

View File

@@ -39,7 +39,7 @@ tmux new-window -t penpot:3 -n 'exporter'
tmux select-window -t penpot:3
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot 'yarn run watch' enter
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l

View File

@@ -87,9 +87,14 @@ RUN set -ex; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
ca-certificates \
curl \
fontconfig \
fontforge \
python3 \
python3-tabulate \
tzdata \
woff-tools \
woff2 \
\
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
@@ -108,11 +113,6 @@ RUN set -ex; \
libxml2 \
libzip4t64 \
libzstd1 \
python3 \
python3-tabulate \
tzdata \
woff-tools \
woff2 \
; \
find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \
rm -rf /var/lib /var/cache; \
@@ -126,9 +126,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
ARG BUNDLE_PATH="./bundle-backend/"
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
USER penpot:penpot
WORKDIR /opt/penpot/backend

View File

@@ -89,8 +89,7 @@ RUN set -eux; \
mkdir -p /opt/penpot; \
chown -R penpot:penpot /opt/penpot;
ARG BUNDLE_PATH="./bundle-exporter/"
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
ADD --chown=penpot:penpot ./bundle-exporter/ /opt/penpot/exporter
WORKDIR /opt/penpot/exporter
USER penpot:penpot

View File

@@ -6,18 +6,14 @@ USER root
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /opt/data/assets; \
chown -R penpot:penpot /opt/data; \
mkdir -p /etc/nginx/overrides/http.d/; \
mkdir -p /etc/nginx/overrides/server.d/; \
mkdir -p /etc/nginx/overrides/location.d/;
chown -R penpot:penpot /opt/data;
ARG BUNDLE_PATH="./bundle-frontend/"
ADD $BUNDLE_PATH /var/www/app/
ADD ./bundle-frontend/ /var/www/app/
ADD ./files/config.js /var/www/app/js/config.js
ADD ./files/nginx.conf.template /tmp/nginx.conf.template
ADD ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
ADD ./files/nginx.conf /etc/nginx/nginx.conf.template
ADD ./files/nginx-proxies.conf /etc/nginx/nginx-proxies.conf
ADD ./files/resolvers.conf /etc/nginx/overrides.d/resolvers.conf.template
ADD ./files/nginx-mime.types /etc/nginx/mime.types
ADD ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
RUN chown -R 1001:0 /var/cache/nginx; \

View File

@@ -5,8 +5,10 @@
#########################################
if [[ $PENPOT_FLAGS == *"enable-air-gapped-conf"* ]]; then
rm /etc/nginx/overrides/location.d/external-locations.conf;
export INCLUDE_PROXIES=""
export PENPOT_FLAGS="$PENPOT_FLAGS disable-google-fonts-provider disable-dashboard-templates-section"
else
export INCLUDE_PROXIES="include /etc/nginx/nginx-proxies.conf;"
fi
#########################################
@@ -31,13 +33,14 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE,\$INCLUDE_PROXIES" \
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
< /tmp/resolvers.conf.template > /etc/nginx/overrides/http.d/resolvers.conf
< /etc/nginx/overrides.d/resolvers.conf.template > /etc/nginx/overrides.d/resolvers.conf
exec "$@";

View File

@@ -1,10 +1,10 @@
worker_processes auto;
pid /tmp/nginx.pid;
include /etc/nginx/overrides/main.d/*.conf;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 2048;
multi_accept on;
# multi_accept on;
}
http {
@@ -33,11 +33,6 @@ http {
error_log /dev/stderr;
access_log /dev/stdout;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
gzip on;
gzip_vary on;
gzip_proxied any;
@@ -46,7 +41,7 @@ http {
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
@@ -62,14 +57,7 @@ http {
proxy_cache_valid any 48h;
proxy_cache_key "$host$request_uri";
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
include /etc/nginx/overrides/http.d/*.conf;
include /etc/nginx/overrides.d/*.conf;
server {
listen 8080 default_server;
@@ -78,6 +66,13 @@ http {
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
charset utf-8;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
etag off;
root /var/www/app/;
@@ -124,10 +119,12 @@ http {
location /api {
proxy_pass $PENPOT_BACKEND_URI/api;
proxy_buffering off;
}
location /readyz {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_BACKEND_URI$request_uri;
}
@@ -137,10 +134,8 @@ http {
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
}
include /etc/nginx/overrides/server.d/*.conf;
location / {
include /etc/nginx/overrides/location.d/*.conf;
$INCLUDE_PROXIES
location ~ ^/js/config.js$ {
add_header Cache-Control "no-store, no-cache, max-age=0" always;

View File

@@ -16,7 +16,7 @@
title=title or metadata.title,
desc=desc or metadata.desc or description or metadata.description,
url="https://help.penpot.app" + page.url,
img="https://help.penpot.app/img/th-help-center.jpg",
img="https://help.penpot.app/img/thumb-help-center.jpg",
img_alt=alt,
twitterHandle=twitter or metadata.twitter,
name=name

View File

@@ -7,7 +7,7 @@ eleventyNavigation:
---
<div class="main-illus">
<img src="/img/home-contributing.png" alt="User guide" border="0">
<img src="/img/home-contribution.webp" alt="User guide" border="0">
</div>
<h1 id="contributing-guide">Contributing guide.</h1>

View File

@@ -832,22 +832,22 @@ a[href].post-tag:visited {
padding-left: 14.6rem;
}
.illus-userguide {
background-image: url(/img/home-userguide.png);
background-image: url(/img/home-user-guide.webp);
}
.illus-faq {
background-image: url(/img/home-faq.png);
background-image: url(/img/home-faq.webp);
}
.illus-techguide {
background-image: url(/img/home-techguide.png);
background-image: url(/img/home-technical-guide.webp);
}
.illus-plugins {
background-image: url(/img/home-plugins.png);
background-image: url(/img/home-plugins.webp);
}
.illus-contributing {
background-image: url(/img/home-contributing.png);
background-image: url(/img/home-contribution.webp);
}
.illus-contact {
background-image: url(/img/home-contact.png);
background-image: url(/img/home-contact.webp);
}
.illus-libraries {
padding-left: 13rem;
@@ -924,7 +924,7 @@ a[href].post-tag:visited {
background-size: auto 75%;
background-position: center top;
background-repeat: no-repeat;
background-image: url(/img/home-contact.png);
background-image: url(/img/home-contact.webp);
}
.contact-block p,
.contact-block h2,
@@ -992,22 +992,22 @@ a[href].post-tag:visited {
}
.plugins .illus-getting-started {
background-image: url(/img/plugins/getting_started.png);
background-image: url(/img/plugins/plugins-getting-started.webp);
}
.plugins .illus-create-plugin {
background-image: url(/img/plugins/create_plugin.png);
background-image: url(/img/plugins/plugins-create.webp);
}
.plugins .illus-deployment {
background-image: url(/img/plugins/deployment.png);
background-image: url(/img/plugins/plugins-deploy.webp);
}
.plugins .illus-api {
background-image: url(/img/plugins/api.png);
background-image: url(/img/plugins/plugins-api.webp);
}
.plugins .illus-examples {
background-image: url(/img/plugins/examples.png);
background-image: url(/img/plugins/plugins-examples-templates.webp);
}
.plugins .illus-faq {
background-image: url(/img/plugins/faqs.png);
background-image: url(/img/plugins/plugins-faqs.webp);
}
table, tr, th, td {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

BIN
docs/img/home-contact.webp Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/img/home-faq.webp Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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