Compare commits

...

101 Commits
2.0.0 ... 2.0.3

Author SHA1 Message Date
Alejandro Alonso
5eee1cdbf3 Merge remote-tracking branch 'origin/staging' 2024-06-05 12:26:23 +02:00
Pablo Alba
54c506100d 🐛 Fix swap slot is not removed on parent detach (2) 2024-06-04 11:17:06 +02:00
Pablo Alba
7c64ed84f1 🐛 Fix swap slot is not removed on parent detach 2024-06-04 10:20:26 +02:00
Alejandro
5fef0b64f4 Merge pull request #4665 from penpot/niwinz-sanitize-audit
 Add stricter validation for audit events
2024-06-04 10:12:27 +02:00
Andrey Antukh
3294058e16 Add stricter validation for audit events 2024-06-04 09:54:41 +02:00
Andrés Moya
5e8c164a44 🐛 Add migration to remove all misplaced slots 2024-06-03 17:17:14 +02:00
Andrés Moya
3472359168 🐛 Add validate and repair for :misplaced-slot 2024-06-03 17:17:02 +02:00
Aitor Moreno
1b17742fc3 Merge pull request #4654 from penpot/superalex-a-b-test-onboarding-questions
🎉 Add a/b test for onboarding questions
2024-05-31 09:50:00 +02:00
Alejandro Alonso
98038b10a0 🎉 Add a/b test for onboarding questions 2024-05-30 14:54:40 +02:00
Alejandro Alonso
91ca55742a Merge remote-tracking branch 'origin/staging' 2024-05-30 12:00:03 +02:00
Andrés Moya
e83c90203e 🐛 Migration to remove bad swap-slots 2024-05-29 15:09:04 +02:00
Pablo Alba
b4a7a15045 Revert "🐛 swap slot is not copied on copy-paste of a main"
This reverts commit 2a752e3625.
2024-05-29 15:09:04 +02:00
Alejandro Alonso
b847754e3b Support external feature flags 2024-05-29 13:18:47 +02:00
Alejandro
9c5f7373de Merge pull request #4641 from penpot/hiru-validate-swap-slots
🔧 Add temporary validation to catch a bug
2024-05-28 16:33:10 +02:00
Andrés Moya
3e8c665b7f 🔧 Add optional validation to check missing swap slots 2024-05-28 16:19:30 +02:00
Alejandro Alonso
cf6bea1974 Merge remote-tracking branch 'origin/staging' 2024-05-28 10:42:30 +02:00
Alejandro Alonso
b8bff31aca 📎 Increase version 2024-05-28 10:42:01 +02:00
Alejandro Alonso
54c4e9af6c Merge remote-tracking branch 'origin/staging' 2024-05-28 08:10:21 +02:00
Alejandro Alonso
07d859f9bd 🐛 Fix penpot.app links 2024-05-28 08:08:53 +02:00
Alejandro
d008d82a11 Merge pull request #4634 from penpot/niwinz-xdomain-cookie
 Improve auth-data xdomain cookie
2024-05-27 11:34:58 +02:00
Andrey Antukh
abff7d324d Improve auth-data xdomain cookie 2024-05-27 10:58:05 +02:00
Alejandro Alonso
39613944bb Merge remote-tracking branch 'origin/staging' 2024-05-24 13:15:59 +02:00
Eva Marco
9194e257b6 🐛 Fix project name ellipsis 2024-05-24 13:15:09 +02:00
Alejandro
b54b99becf Merge pull request #4623 from penpot/niwinz-bugfix-2
🐛 Fix incorrect password encoding on create-profile manage command
2024-05-23 16:21:15 +02:00
Andrey Antukh
b357cf505d 🐛 Fix incorrect password encoding on create-profile manage command 2024-05-23 16:13:30 +02:00
Andrey Antukh
632165d6dc Merge branch 'AbdelatifAitBara-fix-docker-compose' into staging 2024-05-23 15:40:11 +02:00
AbdelatifAitBara
4d463537dd 🐛 Fix docker-compose version
Fixes #4545
2024-05-23 15:39:34 +02:00
Alejandro Alonso
ae6cb551cb Merge remote-tracking branch 'origin/staging' 2024-05-22 13:03:20 +02:00
Aitor Moreno
0f181df767 Merge pull request #4604 from penpot/eva-fix-scrollbar-chrome
🐛  Fix scrollbar with on chrome after 121 release
2024-05-22 11:19:21 +02:00
Eva Marco
8d104de41c 🐛 Fix scrollbar with on chrome after 121 release 2024-05-21 13:43:54 +02:00
Alejandro
a94f1d6fe4 Merge pull request #4547 from penpot/alotor-fix-problem-svg-styles
🐛 Fix style scoping problem with imported SVG
2024-05-10 10:54:05 +02:00
Alejandro
c616e3c932 Merge pull request #4540 from penpot/azazeln28-fix-color-palette-sorting-by-hue-and-value
🐛  Color palette sorting
2024-05-10 10:26:42 +02:00
Alejandro Alonso
bbb64b8be9 Merge remote-tracking branch 'origin/staging' 2024-05-07 12:47:17 +02:00
Aitor Moreno
2c3ae851ea Merge pull request #4541 from penpot/superalex-delete-bottle-tutorial-and-walkthrough-from-onboarding-dashboard
 Delete Bottle tutorial and walkthrough from onboarding das…
2024-05-06 15:14:51 +02:00
alonso.torres
7117ea1f7e 🐛 Fix style scoping problem with imported SVG 2024-05-06 09:16:57 +02:00
Alejandro Alonso
0fc7d8529e Delete Bottle tutorial and walkthrough from onboarding dashboard 2024-04-30 16:49:23 +02:00
AzazelN28
a8fae53564 🐛 color palette sorting 2024-04-30 10:03:53 +02:00
Alejandro
23bd57e9bb Merge pull request #4531 from penpot/alotor-fix-font-bug
🐛 Fix problem with exporter texts
2024-04-26 12:49:45 +02:00
Alejandro
11f2d7e711 Merge pull request #4530 from penpot/palba-fix-wrong-permissions
🐛 Fix inspect permission on shared prototype for owners
2024-04-26 12:43:34 +02:00
alonso.torres
3a71068a48 🐛 Add warning when font cannot be found 2024-04-26 12:16:05 +02:00
alonso.torres
bebdc78ce6 🐛 Fix problem with exporter texts 2024-04-26 12:15:37 +02:00
Pablo Alba
22939aa689 🐛 Fix inspect permission on shared prototype for owners 2024-04-26 11:55:56 +02:00
Pablo Alba
6901acb37e 🐛 Fix ungrouping detach components 2024-04-26 11:41:43 +02:00
Pablo Alba
e0fe7181f1 Merge pull request #4522 from penpot/superalex-fix-update-temp-file-audit-log-stored-dat
🐛 Fix update-temp-file audit_log stored data
2024-04-26 11:02:10 +02:00
Alejandro Alonso
a65282c01b 🐛 Fix update-temp-file audit_log stored data 2024-04-26 06:30:00 +02:00
Alejandro Alonso
5b35176584 Merge remote-tracking branch 'origin/staging' 2024-04-25 10:29:04 +02:00
Aitor Moreno
857429290d Merge pull request #4501 from penpot/superalex-add-exception-audit-log-event
 Add exception page audit log event
2024-04-24 14:07:53 +02:00
Alejandro Alonso
3c842d2b81 Add exception page audit log event 2024-04-24 13:17:57 +02:00
Alejandro
38fd385d5d Merge pull request #4467 from penpot/azazeln28-fix-color-palette-sorting
🐛 Fix color palette sorting
2024-04-24 08:22:33 +02:00
Alejandro
9bf5b1a8cd Merge pull request #4491 from jordisala1991/hotfix/features-file-builder
🐛 Fix default features for files exported with penpot lib
2024-04-23 12:39:17 +02:00
Eva Marco
7c80c605d1 🐛 Fix background color on scrollbar for firefox 2024-04-23 11:25:18 +02:00
Jordi Sala Morales
0cb2e6d07d 🐛 Fix default features for files exported with penpot lib
Avoid having false as features, since it should be an array.
2024-04-23 07:40:16 +00:00
Alejandro
d907812513 Merge pull request #4481 from jordisala1991/hotfix/page-options
🐛 Apply parse-data for addPage options
2024-04-22 14:53:53 +02:00
Alejandro
d8bf48e49e Merge pull request #4486 from penpot/azazeln28-add-provider-to-validate-uri
📎 Add provider to validate uri
2024-04-22 13:43:26 +02:00
Jordi Sala Morales
b667f1bb2c 🐛 Fix adding pages with customized options 2024-04-22 11:23:47 +00:00
Alejandro
8e9fa66e2f Merge pull request #4482 from penpot/palba-fix-remove-swap-slot
Fixes on swap slots
2024-04-22 13:20:28 +02:00
AzazelN28
17fb5283cc 📎 Add provider to validate uri 2024-04-22 13:13:42 +02:00
Alejandro
03c9f6b1e1 Merge pull request #4487 from penpot/superalex-fix-libraries-templates-links
🐛 Fix librares and templates links
2024-04-22 13:13:06 +02:00
Alejandro Alonso
908229b7a8 🐛 Fix librares and templates links 2024-04-22 13:04:48 +02:00
Pablo Alba
2a752e3625 🐛 swap slot is not copied on copy-paste of a main 2024-04-22 11:32:31 +02:00
Pablo Alba
c6fabc349e 🐛 Fix swap-slot is removed on copy-paste of a chained copy 2024-04-22 11:21:34 +02:00
Alejandro Alonso
3ea3923751 Merge remote-tracking branch 'origin/staging' 2024-04-22 09:07:06 +02:00
AzazelN28
293ab3c80e 🐛 Fix color palette sorting 2024-04-19 10:31:54 +02:00
Alejandro
89fa8ce66e Merge pull request #4466 from penpot/azazeln28-fix-seo-issues
📎 Fix SEO issues
2024-04-19 09:27:27 +02:00
AzazelN28
3bf5648b5b 📎 Fix SEO issues 2024-04-18 11:39:48 +02:00
alonso.torres
57346ab685 📚 Update changelog to reflect breaking change 2024-04-17 19:57:02 +02:00
Alejandro
8bd9c0d031 Merge pull request #4444 from penpot/palba-bugfixing-008
🐛 Bugfixing
2024-04-17 06:46:57 +02:00
Alejandro
88f46f2ab2 Merge pull request #4458 from penpot/niwinz-staging-bugfix-2
 Minor enhancements
2024-04-17 06:40:42 +02:00
Andrey Antukh
edd91f00af Add minor improvement to worker module logging 2024-04-16 23:27:00 +02:00
Andrey Antukh
16fa0b0330 Improve email clean mechanism 2024-04-16 17:24:50 +02:00
Alejandro
c975e0bcee Merge pull request #4455 from penpot/niwinz-staging-bugfix-2
 Make some storage operations asynchronous
2024-04-16 17:01:52 +02:00
Andrey Antukh
25001e5b80 📎 Add minor logging improvements on worker module 2024-04-16 16:42:44 +02:00
Andrey Antukh
c3a0db2431 Add the ability to schedule storage object touching as a task 2024-04-16 16:42:44 +02:00
Andrey Antukh
e27c0b2086 Add a task for asynchronous object update operation 2024-04-16 16:42:44 +02:00
Pablo Alba
ec8c847440 🐛 Fix wrong permissions on shared prototype for owners 2024-04-16 13:12:21 +02:00
Alejandro Alonso
380c77a704 Merge remote-tracking branch 'origin/staging' 2024-04-16 12:41:24 +02:00
Andrey Antukh
caaf695352 📚 Update changelog 2024-04-16 12:39:13 +02:00
Alejandro
56f4348586 Merge pull request #4452 from penpot/niwinz-staging-bugfix-1
 Reduce lock contention on uploading file object thumbnail
2024-04-16 11:52:24 +02:00
Andrey Antukh
56ba32b66d Reduce lock contention on uploading file object thumbnail 2024-04-16 11:37:35 +02:00
Alejandro
4dacba6836 Merge pull request #4450 from penpot/niwinz-staging-bugfix-1
 Make cron task schedule sync more lock resilent
2024-04-16 09:28:03 +02:00
Jordi Sala Morales
ddfe5fbcb8 Avoid non existent function warning 2024-04-16 08:47:35 +02:00
Andrey Antukh
7948f565e3 Make cron task schedule sync more lock resilent 2024-04-16 08:39:04 +02:00
Alejandro Alonso
2bca2b005e Merge remote-tracking branch 'origin/staging' 2024-04-15 20:57:17 +02:00
Alejandro
4cb57c9748 Merge pull request #4446 from penpot/superalex-update-changes-2
📚 Update CHANGES for 2.0.1
2024-04-15 20:57:02 +02:00
Alejandro Alonso
bb76700c18 📚 Update CHANGES for 2.0.1 2024-04-15 20:55:51 +02:00
Andrey Antukh
33bdf5e83f Merge remote-tracking branch 'origin/staging' 2024-04-15 20:27:29 +02:00
Alejandro Alonso
f0eff95e18 🐛 Fix v2 components migration script 2024-04-15 20:26:51 +02:00
Alejandro Alonso
2a6b9f06b3 Merge remote-tracking branch 'origin/staging' 2024-04-15 16:46:54 +02:00
Alejandro
f531a5c323 Merge pull request #4442 from penpot/niwinz-staging-bugfixes-14
🐛 Bugfixes
2024-04-15 16:24:11 +02:00
Pablo Alba
acc1fac8de 🐛 Fix color picker names are not displayed correctly on their tooltips 2024-04-15 16:07:05 +02:00
Andrey Antukh
36e66c4dd9 Merge remote-tracking branch 'origin/staging' 2024-04-15 14:27:46 +02:00
Andrey Antukh
8c2038e43b 🐛 Fix incorrect name on audit event 2024-04-15 14:27:24 +02:00
Andrey Antukh
0135b477ca Add improved traceability of climit module 2024-04-15 14:27:24 +02:00
Alejandro
8bf1b9c28e Merge pull request #4427 from penpot/palba-bugfixing-007
🐛 Bugfixing
2024-04-15 12:48:28 +02:00
Alejandro
002772ff0e Merge pull request #4429 from penpot/alotor-bugfix-44
Alotor bugfix 44
2024-04-15 12:38:56 +02:00
alonso.torres
4838571ec2 🐛 Fix problem with position-data overriding in copies 2024-04-15 10:13:01 +02:00
alonso.torres
8e71d219ca 🐛 Fix editor when several colors are in a single word 2024-04-15 10:13:01 +02:00
alonso.torres
cbac4587cf 🐛 Fix crash when removing multiple text fills 2024-04-15 10:13:01 +02:00
alonso.torres
e636bdd0b0 🐛 Fix problem copy/paste svg text 2024-04-15 10:13:01 +02:00
Pablo Alba
a7a3344030 🐛 Inverted highlight constraint for vertical and horizontal constraints 2024-04-12 12:53:03 +02:00
Pablo Alba
137e576e63 🐛 Fix scrollbar appears on top of UI buttons 2024-04-12 12:39:48 +02:00
100 changed files with 951 additions and 664 deletions

View File

@@ -1,5 +1,33 @@
# CHANGELOG
## 2.0.3
### :bug: Bugs fixed
- Fix chrome scrollbar styling [Taiga Issue #7852](https://tree.taiga.io/project/penpot/issue/7852)
- Fix incorrect password encoding on create-profile manage scritp [Github #3651](https://github.com/penpot/penpot/issues/3651)
## 2.0.2
### :sparkles: Enhancements
- Fix locking contention on cron subsystem (causes backend start blocking)
- Fix locking contention on file object thumbails backend RPC calls
### :bug: Bugs fixed
- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458)
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
## 2.0.1
### :bug: Bugs fixed
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
## 2.0.0 - I Just Can't Get Enough
### :rocket: Epics and highlights
@@ -14,6 +42,8 @@
### :boom: Breaking changes & Deprecations
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
### :heart: Community contributions (Thank you!)
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)

View File

@@ -540,6 +540,7 @@
token (tokens/generate (::setup/props cfg) info)
params (d/without-nils
{:token token
:provider (:provider (:path-params request))
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")

View File

@@ -113,8 +113,7 @@
(s/def ::worker-default-parallelism ::us/integer)
(s/def ::worker-webhook-parallelism ::us/integer)
(s/def ::authenticated-cookie-domain ::us/string)
(s/def ::authenticated-cookie-name ::us/string)
(s/def ::auth-data-cookie-domain ::us/string)
(s/def ::auth-token-cookie-name ::us/string)
(s/def ::auth-token-cookie-max-age ::dt/duration)
@@ -222,7 +221,6 @@
::audit-log-http-handler-concurrency
::auth-token-cookie-name
::auth-token-cookie-max-age
::authenticated-cookie-name
::authenticated-cookie-domain
::database-password
::database-uri

View File

@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uri :as u]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
@@ -33,7 +34,7 @@
;; A cookie that we can use to check from other sites of the same
;; domain if a user is authenticated.
(def default-authenticated-cookie-name "authenticated")
(def default-auth-data-cookie-name "auth-data")
;; Default value for cookie max-age
(def default-cookie-max-age (dt/duration {:days 7}))
@@ -133,9 +134,9 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private assign-auth-token-cookie)
(declare ^:private assign-authenticated-cookie)
(declare ^:private assign-auth-data-cookie)
(declare ^:private clear-auth-token-cookie)
(declare ^:private clear-authenticated-cookie)
(declare ^:private clear-auth-data-cookie)
(declare ^:private gen-token)
(defn create-fn
@@ -153,7 +154,7 @@
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))
(assign-auth-data-cookie session)))))
(defn delete-fn
[{:keys [::manager]}]
@@ -167,7 +168,7 @@
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie)))))
(clear-auth-data-cookie)))))
(defn- gen-token
[props {:keys [profile-id created-at]}]
@@ -229,7 +230,7 @@
(let [session (update! manager session)]
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))
(assign-auth-data-cookie session)))
response))))
(def soft-auth
@@ -262,11 +263,11 @@
:secure secure?}]
(update response :cookies assoc name cookie)))
(defn- assign-authenticated-cookie
[response {updated-at :updated-at}]
(defn- assign-auth-data-cookie
[response {profile-id :profile-id updated-at :updated-at}]
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
domain (cf/get :authenticated-cookie-domain)
cname (cf/get :authenticated-cookie-name "authenticated")
domain (cf/get :auth-data-cookie-domain)
cname default-auth-data-cookie-name
created-at (or updated-at (dt/now))
renewal (dt/plus created-at default-renewal-max-age)
@@ -274,14 +275,17 @@
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
secure? (contains? cf/flags :secure-session-cookies)
strict? (contains? cf/flags :strict-session-cookies)
cors? (contains? cf/flags :cors)
cookie {:domain domain
:expires expires
:path "/"
:comment comment
:value true
:same-site :strict
:value (u/map->query-string {:profile-id profile-id})
:same-site (if cors? :none (if strict? :strict :lax))
:secure secure?}]
(cond-> response
(string? domain)
(update :cookies assoc cname cookie))))
@@ -291,10 +295,10 @@
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
(defn- clear-authenticated-cookie
(defn- clear-auth-data-cookie
[response]
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
domain (cf/get :authenticated-cookie-domain)]
(let [cname default-auth-data-cookie-name
domain (cf/get :auth-data-cookie-domain)]
(cond-> response
(string? domain)
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))

View File

@@ -349,6 +349,8 @@
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
:object-update
(ig/ref :app.tasks.object-update/handler)
:process-webhook-event
(ig/ref ::webhooks/process-event-handler)
:run-webhook
@@ -376,7 +378,10 @@
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.orphan-teams-gc/handler
{::db/pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)}
:app.tasks.object-update/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.file-gc/handler
{::db/pool (ig/ref ::db/pool)

View File

@@ -18,7 +18,7 @@
row_number() OVER (ORDER BY created_at DESC) AS rown
FROM team
WHERE deleted_at IS NULL
AND (features <@ '{components/v2}' OR features IS NULL)
AND (not (features @> '{components/v2}') OR features IS NULL)
ORDER BY created_at DESC")
(defn- get-teams
@@ -37,7 +37,7 @@
;; Run teams migration
(run! (fn [{:keys [id rown]}]
(try
(-> (assoc system ::db/rollback true)
(-> (assoc system ::db/rollback false)
(feat/migrate-team! id
:rown rown
:label "v2-migration"

View File

@@ -20,6 +20,7 @@
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.edn :as edn]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]
@@ -91,67 +92,77 @@
:timeout (:timeout config)
:type :semaphore))
(defmacro ^:private measure-and-log!
[metrics mlabels stats id action limit-id limit-label profile-id elapsed]
`(let [mpermits# (:max-permits ~stats)
mqueue# (:max-queue ~stats)
permits# (:permits ~stats)
queue# (:queue ~stats)
queue# (- queue# mpermits#)
queue# (if (neg? queue#) 0 queue#)
level# (if (pos? queue#) :warn :trace)]
(mtx/run! ~metrics
:id :rpc-climit-queue
:val queue#
:labels ~mlabels)
(defn measure!
[metrics mlabels stats elapsed]
(let [mpermits (:max-permits stats)
permits (:permits stats)
queue (:queue stats)
queue (- queue mpermits)
queue (if (neg? queue) 0 queue)]
(mtx/run! ~metrics
:id :rpc-climit-permits
:val permits#
:labels ~mlabels)
(mtx/run! metrics
:id :rpc-climit-queue
:val queue
:labels mlabels)
(l/log level#
:hint ~action
:req ~id
:id ~limit-id
:label ~limit-label
:profile-id (str ~profile-id)
:permits permits#
:queue queue#
:max-permits mpermits#
:max-queue mqueue#
~@(if (some? elapsed)
[:elapsed `(dt/format-duration ~elapsed)]
[]))))
(mtx/run! metrics
:id :rpc-climit-permits
:val permits
:labels mlabels)
(when elapsed
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels mlabels))))
(defn log!
[action req-id stats limit-id limit-label params elapsed]
(let [mpermits (:max-permits stats)
queue (:queue stats)
queue (- queue mpermits)
queue (if (neg? queue) 0 queue)
level (if (pos? queue) :warn :trace)]
(l/log level
:hint action
:req req-id
:id limit-id
:label limit-label
:queue queue
:elapsed (some-> elapsed dt/format-duration)
:params (-> (select-keys params [::rpc/profile-id :file-id :profile-id])
(set/rename-keys {::rpc/profile-id :profile-id})
(update-vals str)))))
(def ^:private idseq (AtomicLong. 0))
(defn- invoke
[limiter metrics limit-id limit-key limit-label profile-id f params]
[limiter metrics limit-id limit-key limit-label handler params]
(let [tpoint (dt/tpoint)
mlabels (into-array String [(id->str limit-id)])
limit-id (id->str limit-id limit-key)
stats (pbh/get-stats limiter)
id (.incrementAndGet ^AtomicLong idseq)]
req-id (.incrementAndGet ^AtomicLong idseq)]
(try
(measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil)
(measure! metrics mlabels stats nil)
(log! "enqueued" req-id stats limit-id limit-label params nil)
(px/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed)
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels mlabels)
(apply f params))))
(measure! metrics mlabels stats elapsed)
(log! "acquired" req-id stats limit-id limit-label params elapsed)
(handler params))))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(let [elapsed (tpoint)]
(measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed)
(log! "rejected" req-id stats limit-id limit-label params elapsed)
(ex/raise :type :concurrency-limit
:code code
:hint "concurrency limit reached"
@@ -161,7 +172,9 @@
(finally
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed))))))
(measure! metrics mlabels stats nil)
(log! "finished" req-id stats limit-id limit-label params elapsed))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MIDDLEWARE
@@ -219,10 +232,8 @@
(let [limit-key (key-fn params)
cache-key [limit-id limit-key]
limiter (cache/get cache cache-key (partial create-limiter config))
profile-id (if (= key-fn ::rpc/profile-id)
limit-key
(get params ::rpc/profile-id))]
(invoke limiter metrics limit-id limit-key label profile-id handler [cfg params])))))
handler (partial handler cfg)]
(invoke limiter metrics limit-id limit-key label handler params)))))
(do
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
@@ -237,15 +248,15 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- build-exec-chain
[{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f]
[{:keys [::label ::rpc/climit ::mtx/metrics] :as cfg} f]
(let [config (get climit ::config)
cache (get climit ::cache)]
(reduce (fn [handler [limit-id limit-key :as ckey]]
(if-let [config (get config limit-id)]
(fn [& params]
(let [limiter (cache/get cache ckey (partial create-limiter config))]
(invoke limiter metrics limit-id limit-key label profile-id handler params)))
(fn [cfg params]
(let [limiter (cache/get cache ckey (partial create-limiter config))
handler (partial handler cfg)]
(invoke limiter metrics limit-id limit-key label handler params)))
(do
(l/wrn :hint "config not found" :label label :id limit-id)
f)))
@@ -255,9 +266,9 @@
(defn invoke!
"Run a function in context of climit.
Intended to be used in virtual threads."
[{:keys [::executor] :as cfg} f & params]
[{:keys [::executor] :as cfg} f params]
(let [f (if (some? executor)
(fn [& params] (px/await! (px/submit! executor (fn [] (apply f params)))))
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
f)
f (build-exec-chain cfg f)]
(apply f params)))
(f cfg params)))

View File

@@ -77,10 +77,19 @@
(when (seq events)
(db/insert-many! pool :audit-log event-columns events))))
(def valid-event-types
#{"action" "identify"})
(def schema:event
[:map {:title "Event"}
[:name [:string {:max 250}]]
[:type [:string {:max 250}]]
[:name
[:and {:gen/elements ["update-file", "get-profile"]}
[:string {:max 250}]
[:re #"[\d\w-]{1,50}"]]]
[:type
[:and {:gen/elements valid-event-types}
[:string {:max 250}]
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword :any]]
[:context {:optional true}

View File

@@ -16,6 +16,7 @@
[app.db.sql :as sql]
[app.features.components-v2 :as feat.compv2]
[app.features.fdata :as fdata]
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.files-create :as files.create]
@@ -23,6 +24,7 @@
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -100,7 +102,9 @@
:revn revn
:data nil
:changes (blob/encode changes)})
nil)))
(rph/with-meta (rph/wrap nil)
{::audit/replace-props {:file-id id
:revn revn}}))))
;; --- MUTATION COMMAND: persist-temp-file

View File

@@ -228,51 +228,52 @@
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMAND: create-file-object-thumbnail
(def sql:get-file-object-thumbnail
"SELECT * FROM file_tagged_object_thumbnail
WHERE file_id = ? AND object_id = ? AND tag = ?
FOR UPDATE")
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media tag]
(def sql:create-file-object-thumbnail
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
RETURNING *")
(let [thumb (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:tag tag}
{::db/remove-deleted false
::sql/for-update true})
path (:path media)
(defn- persist-thumbnail!
[storage media created-at]
(let [path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
tnow (dt/now)
(sto/wrap-with-hash hash))]
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at tnow
:content-type mtype
:bucket "file-object-thumbnail"})]
(sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at created-at
:content-type mtype
:bucket "file-object-thumbnail"})))
(if (some? thumb)
(do
;; We mark the old media id as touched if it does not matches
(when (not= (:id media) (:media-id thumb))
(sto/touch-object! storage (:media-id thumb)))
(db/update! conn :file-tagged-object-thumbnail
{:media-id (:id media)
:deleted-at nil
:updated-at tnow}
{:file-id file-id
:object-id object-id
:tag tag}))
(db/insert! conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:created-at tnow
:updated-at tnow
:tag tag
:media-id (:id media)}))))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media)
tsnow (:id media)])]
[th1 th2])))]
(when (and (some? th1)
(not= (:media-id th1)
(:media-id th2)))
(sto/touch-object! storage (:media-id th1) :async true))
th2))
(def ^:private
schema:create-file-object-thumbnail
@@ -296,16 +297,10 @@
(media/validate-media-type! media)
(media/validate-media-size! media)
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 5)
(assoc ::rtry/label "create-file-object-thumbnail"))]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))))))
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

@@ -243,12 +243,13 @@
;; NOTE: we use the climit here in a dynamic invocation because we
;; don't want saturate the process-image limit with IO (download
;; of external image)
(-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/profile-id (:profile-id params))
(assoc ::climit/label "create-file-media-object-from-url")
(climit/invoke! db/run! cfg create-file-media-object params))))
(climit/invoke! #(db/run! %1 create-file-media-object %2) params))))
;; --- Clone File Media object (Upload and create from url)

View File

@@ -46,6 +46,10 @@
(let [email (str/lower email)
email (if (str/starts-with? email "mailto:")
(subs email 7)
email)
email (if (or (str/starts-with? email "<")
(str/ends-with? email ">"))
(str/trim email "<>")
email)]
email))
@@ -233,7 +237,7 @@
:file-mtype (:mtype file)}}))))
(defn- generate-thumbnail!
[file]
[_ file]
(let [input (media/run {:cmd :info :input file})
thumb (media/run {:cmd :profile-thumbnail
:format :jpeg
@@ -250,15 +254,15 @@
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}]
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
(let [params (-> cfg
(assoc ::climit/id :process-image/global)
(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)))
;; --- MUTATION: Request Email Change
(declare ^:private request-email-change!)

View File

@@ -38,6 +38,11 @@
team (-> (db/get conn :team {:id (:team-id project)})
(teams/decode-row))
members (into #{} (->> (teams/get-team-members conn (:team-id project))
(map :id)))
perms (assoc perms :in-team (contains? members profile-id))
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))

View File

@@ -12,6 +12,7 @@
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.profile :as cmd.profile]
[app.util.json :as json]
[app.util.time :as dt]
[cuerdas.core :as str]))
@@ -37,12 +38,13 @@
:or {is-active true}}]
(when-let [system (get-current-system)]
(db/with-atomic [conn (:app.db/pool system)]
(let [params {:id (uuid/next)
:email email
:fullname fullname
:is-active is-active
:password password
:props {}}]
(let [password (cmd.profile/derive-password system password)
params {:id (uuid/next)
:email email
:fullname fullname
:is-active is-active
:password password
:props {}}]
(->> (cmd.auth/create-profile! conn params)
(cmd.auth/create-profile-rels! conn))))))

View File

@@ -16,6 +16,7 @@
[app.storage.impl :as impl]
[app.storage.s3 :as ss3]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]
@@ -170,15 +171,28 @@
(impl/put-object object content))
object)))
(def ^:private default-touch-delay
"A default delay for the asynchronous touch operation"
(dt/duration "5m"))
(defn touch-object!
"Mark object as touched."
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
[{:keys [::db/pool-or-conn] :as storage} object-or-id & {:keys [async]}]
(us/assert! ::storage storage)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
rs (db/update! pool-or-conn :storage-object
{:touched-at (dt/now)}
{:id id})]
(pos? (db/get-update-count rs))))
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
(if async
(wrk/submit! ::wrk/conn pool-or-conn
::wrk/task :object-update
::wrk/delay default-touch-delay
:object :storage-object
:id id
:key :touched-at
:val (dt/now))
(-> (db/update! pool-or-conn :storage-object
{:touched-at (dt/now)}
{:id id})
(db/get-update-count)
(pos?)))))
(defn get-object-data
"Return an input stream instance of the object content."

View File

@@ -0,0 +1,32 @@
;; 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.tasks.object-update
"A task used for perform simple object properties update
in an asynchronous flow."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(defn- update-object
[{:keys [::db/conn] :as cfg} {:keys [id object key val] :as props}]
(l/trc :hint "update object prop"
:id (str id)
:object (d/name object)
:key (d/name key)
:val val)
(db/update! conn object {key val} {:id id} {::db/return-keys false}))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as params}]
(db/tx-run! cfg update-object props)))

View File

@@ -119,11 +119,13 @@
:next.jdbc/update-count))]
(l/trc :hint "submit task"
:name task
:task-id (str id)
:queue queue
:label label
:dedupe (boolean dedupe)
:deleted (or deleted 0)
:in (dt/format-duration duration))
:delay (dt/format-duration duration)
:replace (or deleted 0))
(db/exec-one! conn [sql:insert-new-task id task props queue
label priority max-retries interval])

View File

@@ -27,14 +27,15 @@
"insert into scheduled_task (id, cron_expr)
values (?, ?)
on conflict (id)
do update set cron_expr=?")
do nothing")
(defn- synchronize-cron-entries!
[{:keys [::db/pool ::entries]}]
(db/with-atomic [conn pool]
(doseq [{:keys [id cron]} entries]
(l/trc :hint "register cron task" :id id :cron (str cron))
(db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)]))))
[{:keys [::db/conn ::entries]}]
(doseq [{:keys [id cron]} entries]
(let [result (db/exec-one! conn [sql:upsert-cron-task id (str cron)])
updated? (pos? (db/get-update-count result))]
(l/dbg :hint "register task" :id id :cron (str cron)
:status (if updated? "created" "exists")))))
(defn- lock-scheduled-task!
[conn id]
@@ -45,7 +46,7 @@
(declare ^:private schedule-cron-task)
(defn- execute-cron-task
[cfg {:keys [id] :as task}]
[cfg {:keys [id cron] :as task}]
(px/thread
{:name (str "penpot/cron-task/" id)}
(let [tpoint (dt/tpoint)]
@@ -54,20 +55,25 @@
(db/exec-one! conn ["SET LOCAL statement_timeout=0;"])
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"])
(when (lock-scheduled-task! conn id)
(l/dbg :hint "start task" :task-id id)
(db/update! conn :scheduled-task
{:cron-expr (str cron)
:modified-at (dt/now)}
{:id id}
{::db/return-keys false})
(l/dbg :hint "start" :id id)
((:fn task) task)
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "end task" :task-id id :elapsed elapsed)))))
(l/dbg :hint "end" :id id :elapsed elapsed)))))
(catch InterruptedException _
(let [elapsed (dt/format-duration (tpoint))]
(l/debug :hint "task interrupted" :task-id id :elapsed elapsed)))
(l/debug :hint "task interrupted" :id id :elapsed elapsed)))
(catch Throwable cause
(let [elapsed (dt/format-duration (tpoint))]
(binding [l/*context* (get-error-context cause task)]
(l/err :hint "unhandled exception on running task"
:task-id id
:id id
:elapsed elapsed
:cause cause))))
(finally
@@ -86,7 +92,7 @@
(let [ts (ms-until-valid cron)
ft (px/schedule! ts (partial execute-cron-task cfg task))]
(l/dbg :hint "schedule task" :task-id id
(l/dbg :hint "schedule" :id id
:ts (dt/format-duration ts)
:at (dt/format-instant (dt/in-future ts)))
@@ -135,7 +141,8 @@
cfg (assoc cfg ::entries entries ::running running)]
(l/inf :hint "started" :tasks (count entries))
(synchronize-cron-entries! cfg)
(db/tx-run! cfg synchronize-cron-entries!)
(->> (filter some? entries)
(run! (partial schedule-cron-task cfg)))

View File

@@ -139,7 +139,7 @@
:else
(try
(l/trc :hint "start task"
(l/dbg :hint "start"
:name (:name task)
:task-id (str task-id)
:queue queue
@@ -149,7 +149,7 @@
result (handle-task task)
elapsed (dt/format-duration (tpoint))]
(l/trc :hint "end task"
(l/dbg :hint "end"
:name (:name task)
:task-id (str task-id)
:queue queue
@@ -228,9 +228,9 @@
(recur))))
(catch InterruptedException _
(l/debug :hint "interrupted"
:id id
:queue queue))
(l/dbg :hint "interrupted"
:id id
:queue queue))
(catch Throwable cause
(l/err :hint "unexpected exception"
:id id

View File

@@ -1158,7 +1158,7 @@
;; check that the unknown frame thumbnail is deleted
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(t/is (= 1 (count (remove :deleted-at rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 3 (:processed res))))

View File

@@ -277,8 +277,6 @@
(t/is (thrown? org.postgresql.util.PSQLException
(th/db-delete! :storage-object {:id (:media-id row1)}))))))
(t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
@@ -317,3 +315,44 @@
(let [result (:result out)]
(t/is (contains? result "test-key-2"))))))
(t/deftest create-file-object-thumbnail
(th/db-delete! :task {:name "object-update"})
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
data {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-2"
:media {:filename "sample.jpg"
:mtype "image/jpeg"}}]
(let [data (update data :media
(fn [media]
(-> media
(assoc :path (th/tempfile "backend_tests/test_files/sample2.jpg"))
(assoc :size 7923))))
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (map? (:result out))))
(let [data (update data :media
(fn [media]
(-> media
(assoc :path (th/tempfile "backend_tests/test_files/sample.jpg"))
(assoc :size 312043))))
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (map? (:result out))))
(let [[row1 :as rows]
(->> (th/db-query :task {:name "object-update"})
(map #(update % :props db/decode-transit-pgobject)))]
;; (app.common.pprint/pprint rows)
(t/is (= 1 (count rows)))
(t/is (> (inst-ms (dt/diff (:created-at row1) (:scheduled-at row1)))
(inst-ms (dt/duration "4m")))))))

View File

@@ -27,6 +27,14 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest clean-email
(t/is "foo@example.com" (profile/clean-email "mailto:foo@example.com"))
(t/is "foo@example.com" (profile/clean-email "mailto:<foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "<foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "<foo@example.com")))
;; Test with wrong credentials
(t/deftest profile-login-failed-1
(let [profile (th/create-profile* 1)

View File

@@ -274,6 +274,13 @@
(catch #?(:clj Throwable :cljs :default) _cause
[0 0 0])))
(defn hex->lum
[color]
(let [[r g b] (hex->rgb color)]
(mth/sqrt (+ (* 0.241 r)
(* 0.691 g)
(* 0.068 b)))))
(defn- int->hex
"Convert integer to hex string"
[v]
@@ -455,3 +462,19 @@
:else
[r g (inc b)]))
(defn reduce-range
[value range]
(/ (mth/floor (* value range)) range))
(defn sort-colors
[a b]
(let [[ah _ av] (hex->hsv (:color a))
[bh _ bv] (hex->hsv (:color b))
ah (reduce-range (/ ah 60) 8)
bh (reduce-range (/ bh 60) 8)
av (/ av 255)
bv (/ bv 255)
a (+ (* ah 100) (* av 10))
b (+ (* bh 100) (* bv 10))]
(compare a b)))

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 46)
(def version 48)

View File

@@ -22,6 +22,8 @@
[app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.text :as txt]
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.types.shape.shadow :as ctss]
[app.common.uuid :as uuid]
@@ -898,6 +900,43 @@
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defn migrate-up-47
[data]
(letfn [(fix-shape [page shape]
(let [file {:id (:id data) :data data}
component-file (:component-file shape)
;; On cloning a file, the component-file of the shapes point to the old file id
;; this is a workaround to be able to found the components in that case
libraries {component-file {:id component-file :data data}}
ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true})
ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape))
shape-swap-slot (ctk/get-swap-slot shape)
ref-swap-slot (ctk/get-swap-slot ref-shape)]
(if (and (some? shape-swap-slot)
(= shape-swap-slot ref-swap-slot)
(ctk/main-instance? ref-parent))
(ctk/remove-swap-slot shape)
shape)))
(update-page [page]
(d/update-when page :objects update-vals (partial fix-shape page)))]
(-> data
(update :pages-index update-vals update-page))))
(defn migrate-up-48
[data]
(letfn [(fix-shape [shape]
(let [swap-slot (ctk/get-swap-slot shape)]
(if (and (some? swap-slot)
(not (ctk/subcopy-head? shape)))
(ctk/remove-swap-slot shape)
shape)))
(update-page [page]
(d/update-when page :objects update-vals fix-shape))]
(-> data
(update :pages-index update-vals update-page))))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -935,4 +974,6 @@
{:id 43 :migrate-up migrate-up-43}
{:id 44 :migrate-up migrate-up-44}
{:id 45 :migrate-up migrate-up-45}
{:id 46 :migrate-up migrate-up-46}])
{:id 46 :migrate-up migrate-up-46}
{:id 47 :migrate-up migrate-up-47}
{:id 48 :migrate-up migrate-up-48}])

View File

@@ -460,6 +460,34 @@
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
(defmethod repair-error :misplaced-slot
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape
(fn [shape]
;; Remove the swap slot
(log/debug :hint (str " -> remove swap-slot"))
(ctk/remove-swap-slot shape))]
(log/dbg :hint "repairing shape :misplaced-slot" :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 :missing-slot
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
(fn [shape]
;; Set the desired swap slot
(let [slot (:swap-slot args)]
(when (some? slot)
(log/debug :hint (str " -> set swap-slot to " slot))
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
(log/dbg :hint "repairing shape :missing-slot" :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 :default
[_ error file _]
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))

View File

@@ -6,6 +6,7 @@
(ns app.common.files.validate
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
@@ -50,7 +51,9 @@
:not-head-copy-not-allowed
:not-component-not-allowed
:component-nil-objects-not-allowed
:instance-head-not-frame})
:instance-head-not-frame
:misplaced-slot
:missing-slot})
(def ^:private
schema:error
@@ -285,6 +288,14 @@
"Shape inside main instance should not have shape-ref"
shape file page)))
(defn- check-empty-swap-slot
"Validate that this shape does not have any swap slot."
[shape file page]
(when (some? (ctk/get-swap-slot shape))
(report-error :misplaced-slot
"This shape should not have swap slot"
shape file page)))
(defn- check-shape-main-root-top
"Root shape of a top main instance:
@@ -296,6 +307,7 @@
(check-component-main-head shape file page libraries)
(check-component-root shape file page)
(check-component-not-ref shape file page)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :main-top) (:shapes shape)))
(defn- check-shape-main-root-nested
@@ -307,6 +319,7 @@
(check-component-main-head shape file page libraries)
(check-component-not-root shape file page)
(check-component-not-ref shape file page)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :main-nested) (:shapes shape)))
(defn- check-shape-copy-root-top
@@ -321,6 +334,7 @@
(check-component-not-main-head shape file page libraries)
(check-component-root shape file page)
(check-component-ref shape file page libraries)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
(defn- check-shape-copy-root-nested
@@ -343,6 +357,7 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-not-ref shape file page)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :main-any) (:shapes shape)))
(defn- check-shape-copy-not-root
@@ -351,6 +366,7 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-ref shape file page libraries)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
(defn- check-shape-not-component
@@ -360,6 +376,7 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-not-ref shape file page)
(check-empty-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
(defn- check-shape
@@ -454,6 +471,8 @@
;; PUBLIC API: VALIDATION FUNCTIONS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare check-swap-slots)
(defn validate-file
"Validate full referential integrity and semantic coherence on file data.
@@ -464,6 +483,8 @@
(doseq [page (filter :id (ctpl/pages-seq data))]
(check-shape uuid/zero file page libraries)
(when (str/includes? (:name file) "check-swap-slot")
(check-swap-slots uuid/zero file page libraries))
(->> (get-orphan-shapes page)
(run! #(check-shape % file page libraries))))
@@ -517,3 +538,41 @@
:hint "error on validating file referential integrity"
:file-id (:id file)
:details errors)))
(declare compare-slots)
;; Optional check to look for missing swap slots.
;; Search for copies that do not point the shape-ref to the near component but don't have swap slot
;; (looking for position relative to the parent, in the copy and the main).
;;
;; This check cannot be generally enabled, because files that have been migrated from components v1
;; may have copies with shapes that do not match by position, but have not been swapped. So we enable
;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to
;; the name of the file.
(defn- check-swap-slots
[shape-id file page libraries]
(let [shape (ctst/get-shape page shape-id)]
(if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape))
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true)
container (:container (meta ref-shape))]
(when (some? ref-shape)
(compare-slots shape ref-shape file page container)))
(doall (for [child-id (:shapes shape)]
(check-swap-slots child-id file page libraries))))))
(defn- compare-slots
[shape-copy shape-main file container-copy container-main]
(if (and (not= (:shape-ref shape-copy) (:id shape-main))
(nil? (ctk/get-swap-slot shape-copy)))
(report-error :missing-slot
"Shape has been swapped, should have swap slot"
shape-copy file container-copy
:swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main)))
(when (nil? (ctk/get-swap-slot shape-copy))
(let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))]
(doall (for [[child-copy-id child-main-id] children-id-pairs]
(let [child-copy (ctst/get-shape container-copy child-copy-id)
child-main (ctst/get-shape container-main child-main-id)]
(when (and (some? child-copy) (some? child-main))
(compare-slots child-copy child-main file container-copy container-main)))))))))

View File

@@ -130,6 +130,15 @@
(and (some? (:component-id shape))
(nil? (:component-root shape))))
(defn subcopy-head?
"Check if this shape is the head of a subinstance that is a copy."
[shape]
;; This is redundant with the previous one, but may give more security
;; in case of bugs.
(and (some? (:component-id shape))
(nil? (:component-root shape))
(some? (:shape-ref shape))))
(defn instance-of?
[shape file-id component-id]
(and (some? (:component-id shape))
@@ -227,7 +236,6 @@
:shape-ref
:touched))
(defn- extract-ids [shape]
(if (map? shape)
(let [current-id (:id shape)

View File

@@ -1,5 +1,5 @@
---
version: "3.5"
version: "3.8"
networks:
penpot:

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -28,9 +28,16 @@ body {
* {
box-sizing: border-box;
scrollbar-width: thin;
// transition: all .4s ease;
}
@-moz-document url-prefix() {
* {
scrollbar-width: auto;
}
}
.global-zeroclipboard-container {
transition: none;

View File

@@ -6,6 +6,14 @@
// SCROLLBAR
.new-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(170, 181, 186, 0.3) transparent;
&:hover {
scrollbar-color: rgba(170, 181, 186, 0.7) transparent;
}
// These rules do not apply in chrome - 121 or higher
// We keep them to preserve backward compatibility.
::-webkit-scrollbar {
background-color: transparent;
cursor: pointer;
@@ -585,6 +593,9 @@
width: 100%;
z-index: $z-index-modal;
background-color: var(--overlay-color);
&.onboarding-a-b-test {
background-color: var(--overlay-color-onboarding-a-b-test);
}
}
.modal-container-base {

View File

@@ -11,6 +11,7 @@
// Dark background
--db-primary: #18181a;
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)};
--db-primary-90: #{color.change(#18181a, $alpha: 0.9)};
--db-secondary: #000000;
--db-secondary-30: #{color.change(#000000, $alpha: 0.3)};
--db-secondary-80: #{color.change(#000000, $alpha: 0.8)};
@@ -35,6 +36,7 @@
// Light background
--lb-primary: #ffffff;
--lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)};
--lb-primary-90: #{color.change(#ffffff, $alpha: 0.9)};
--lb-secondary: #e8eaee;
--lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)};
--lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)};

View File

@@ -37,6 +37,7 @@
--color-info-foreground: var(--status-color-info-500);
--overlay-color: var(--db-primary-60);
--overlay-color-onboarding-a-b-test: var(--db-primary-90);
--shadow-color: var(--db-secondary-30);
--radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset;

View File

@@ -37,6 +37,8 @@
--color-info-foreground: var(--status-color-info-500);
--overlay-color: var(--lb-primary-60);
--overlay-color-onboarding-a-b-test: var(--lb-primary-90);
--shadow-color: var(--lf-secondary-40);
--radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset;

View File

@@ -21,6 +21,10 @@
flex-direction: column;
}
.public-DraftStyleDefault-block {
white-space: pre;
}
&.align-top {
.DraftEditor-root {
justify-content: flex-start;

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />

View File

@@ -130,6 +130,10 @@
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
(defn external-feature-flag [flag value]
(when-let [fn (obj/get global "externalFeatureFlag")]
(fn flag value)))
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]

View File

@@ -7,6 +7,7 @@
(ns app.libs.file-builder
(:require
[app.common.data :as d]
[app.common.features :as cfeat]
[app.common.files.builder :as fb]
[app.common.media :as cm]
[app.common.types.components-list :as ctkl]
@@ -73,7 +74,7 @@
manifest-stream
(->> files-stream
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % false))
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features))
(rx/map (fn [a]
(vector "manifest.json" a))))
@@ -144,7 +145,7 @@
(str (:current-page-id file)))
(addPage [_ name options]
(set! file (fb/add-page file {:name name :options options}))
(set! file (fb/add-page file {:name name :options (parse-data options)}))
(str (:current-page-id file)))
(closePage [_]
@@ -253,7 +254,7 @@
(export [_]
(->> (export-file file)
(rx/subs
(rx/subs!
(fn [value]
(when (not (contains? value :type))
(let [[file export-blob] value]

View File

@@ -163,7 +163,7 @@
(ptk/reify ::logged-in
ev/Event
(-data [_]
{::ev/name "signing"
{::ev/name "signin"
::ev/type "identify"
:email (:email profile)
:auth-backend (:auth-backend profile)

View File

@@ -1711,8 +1711,14 @@
(process-entry [[type data]]
(case type
:text
(if (str/empty? data)
(cond
(str/empty? data)
(rx/empty)
(re-find #"<svg\s" data)
(rx/of (paste-svg-text data))
:else
(rx/of (paste-text data)))
:transit
@@ -1757,8 +1763,7 @@
text-data (some-> pdata wapi/extract-text)
transit-data (ex/ignoring (some-> text-data t/decode-str))]
(cond
(and (string? text-data)
(str/includes? text-data "<svg "))
(and (string? text-data) (re-find #"<svg\s" text-data))
(rx/of (paste-svg-text text-data))
(seq image-data)

View File

@@ -143,18 +143,12 @@
(map-indexed vector)
(filter #(#{(:id group)} (second %)))
(ffirst)
inc)
;; Shapes that are in a component (including root) must be detached,
;; because cannot be easyly synchronized back to the main component.
shapes-to-detach (filter ctk/in-component-copy?
(cfh/get-children-with-self objects (:id group)))]
inc)]
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/change-parent parent-id children index-in-parent)
(pcb/remove-objects [(:id group)])
(pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape))))
(pcb/remove-objects [(:id group)]))))
(defn remove-frame-changes
[it page-id frame objects]

View File

@@ -233,6 +233,10 @@
; If the initial shape was component-root, first level subinstances are converted in top instances
(pcb/update-shapes [shape-id] #(assoc % :component-root true))
:always
; First level subinstances of a detached component can't have swap-slot
(pcb/update-shapes [shape-id] ctk/remove-swap-slot)
:always
; Near shape-refs need to be advanced one level
(generate-advance-nesting-level nil container libraries (:id shape)))
@@ -1491,9 +1495,22 @@
container
{:type :reg-objects
:shapes all-parents})]))))
(let [roperation {:type :set
(let [;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data?
(and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= (get origin-shape attr) (get dest-shape attr))
(touched :geometry-group))
roperation {:type :set
:attr attr
:val (get origin-shape attr)
:val (cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data? nil
:else (get origin-shape attr))
:ignore-touched true}
uoperation {:type :set
:attr attr

View File

@@ -486,6 +486,7 @@
duplicating-component? (or duplicating-component? (ctk/instance-head? obj))
is-component-main? (ctk/main-instance? obj)
subinstance-head? (ctk/subinstance-head? obj)
instance-root? (ctk/instance-root? obj)
into-component? (and duplicating-component?
(ctn/in-any-component? objects parent))
@@ -508,7 +509,9 @@
:parent-id parent-id
:frame-id frame-id)
(cond-> (and subinstance-head? remove-swap-slot?)
(cond-> (and (not instance-root?)
subinstance-head?
remove-swap-slot?)
(ctk/remove-swap-slot))
(dissoc :shapes
@@ -581,8 +584,9 @@
true
(and remove-swap-slot?
;; only remove swap slot of children when the current shape
;; is not a subinstance head
(not subinstance-head?))))
;; is not a subinstance head nor a instance root
(not subinstance-head?)
(not instance-root?))))
changes
(map (d/getf objects) (:shapes obj)))))))

View File

@@ -133,7 +133,10 @@
(defn- fetch-gfont-css
[url]
(->> (http/send! {:method :get :uri url :mode :cors :response-type :text})
(rx/map :body)))
(rx/map :body)
(rx/catch (fn [err]
(.warn js/console "Cannot find the font" (obj/get err "message"))
(rx/empty)))))
(defmethod load-font :google
[{:keys [id ::on-loaded] :as font}]

View File

@@ -170,7 +170,7 @@
[:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/current-profile) {:value profile}
(if edata
[:& static/exception-page {:data edata}]
[:& static/exception-page {:data edata :route route}]
[:*
[:& msgs/notifications-hub]
(when route

View File

@@ -48,7 +48,8 @@
(dom/set-html-title (tr "title.default")))
[:main {:class (stl/css :auth-section)}
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :login-illustration)}
i/login-illustration]

View File

@@ -24,6 +24,16 @@
}
}
.logo-container {
position: absolute;
top: $s-20;
left: $s-20;
display: flex;
justify-content: flex-start;
width: $s-120;
margin-block-end: $s-52;
}
.login-illustration {
display: flex;
justify-content: center;
@@ -55,14 +65,6 @@
}
.logo-btn {
position: absolute;
top: $s-20;
left: $s-20;
display: flex;
justify-content: flex-start;
width: $s-120;
margin-block-end: $s-52;
svg {
width: $s-120;
height: $s-40;

View File

@@ -54,9 +54,10 @@
(let [props (.-props tab)
id (.-id props)
title (.-title props)
sid (d/name id)]
sid (d/name id)
tooltip (if (string? title) title nil)]
[:div {:key (str/concat "tab-" sid)
:title title
:title tooltip
:data-id sid
:on-click on-click
:class (stl/css-case

View File

@@ -46,6 +46,7 @@
@extend .button-icon;
stroke: var(--tab-foreground-color);
}
.content {
@include headlineSmallTypography;
text-align: center;
@@ -53,17 +54,21 @@
overflow: hidden;
text-overflow: ellipsis;
}
&.current,
&.current:hover {
background: var(--tab-background-color-selected);
border-color: var(--tab-border-color-selected);
color: var(--tab-foreground-color-selected);
svg {
stroke: var(--tab-foreground-color-selected);
}
}
&:hover {
color: var(--tab-foreground-color-hover);
svg {
stroke: var(--tab-foreground-color-hover);
}
@@ -78,6 +83,7 @@
min-width: $s-24;
padding: 0 $s-6;
border-radius: $br-5;
svg {
@include flexCenter;
height: $s-16;
@@ -87,6 +93,7 @@
fill: none;
color: transparent;
}
&:hover {
svg {
stroke: var(--icon-foreground-hover);
@@ -107,3 +114,10 @@
display: flex;
flex-direction: column;
}
//Firefox doesn't respect scrollbar-gutter
@supports (-moz-appearance: none) {
.tab-container-content {
padding-right: $s-8;
}
}

View File

@@ -17,6 +17,7 @@
(def current-page-id (mf/create-context nil))
(def current-file-id (mf/create-context nil))
(def current-vbox (mf/create-context nil))
(def current-svg-root-id (mf/create-context nil))
(def active-frames (mf/create-context nil))
(def render-thumbnails (mf/create-context nil))

View File

@@ -7,15 +7,11 @@
(ns app.main.ui.dashboard.projects
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.errors :as errors]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [line-grid]]
@@ -100,80 +96,6 @@
(def builtin-templates
(l/derived :builtin-templates st/state))
(mf/defc tutorial-project
[{:keys [close-tutorial default-project-id] :as props}]
(let [state (mf/use-state {:status :waiting
:file nil})
templates (mf/deref builtin-templates)
template (d/seek #(= (:id %) "tutorial-for-beginners") templates)
on-template-cloned-success
(mf/use-fn
(mf/deps default-project-id)
(fn [response]
(swap! state #(assoc % :status :success :file (:first response)))
(st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"})
(du/update-profile-props {:viewed-tutorial? true}))))
on-template-cloned-error
(mf/use-fn
(fn [cause]
(swap! state assoc :status :error)
(errors/print-error! cause)
(st/emit! (msg/error (tr "dashboard.libraries-and-templates.import-error")))))
download-tutorial
(mf/use-fn
(mf/deps template default-project-id)
(fn []
(let [mdata {:on-success on-template-cloned-success
:on-error on-template-cloned-error}
params {:project-id default-project-id
:template-id (:id template)}]
(swap! state #(assoc % :status :importing))
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
{::ev/origin "get-started-hero-block"})))))]
[:article {:class (stl/css :tutorial)}
[:div {:class (stl/css :thumbnail)}]
[:div {:class (stl/css :text)}
[:h2 {:class (stl/css :title)} (tr "dasboard.tutorial-hero.title")]
[:p {:class (stl/css :info)} (tr "dasboard.tutorial-hero.info")]
[:button {:class (stl/css :btn-primary :action)
:on-click download-tutorial}
(case (:status @state)
:waiting (tr "dasboard.tutorial-hero.start")
:importing [:span.loader i/loader-pencil]
:success "")]]
[:button {:class (stl/css :close)
:on-click close-tutorial
:aria-label (tr "labels.close")}
close-icon]]))
(mf/defc interface-walkthrough
{::mf/wrap [mf/memo]}
[{:keys [close-walkthrough] :as props}]
(let [handle-walkthrough-link
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough"
::ev/origin "get-started-hero-block"
:section "dashboard"})))]
[:article {:class (stl/css :walkthrough)}
[:div {:class (stl/css :thumbnail)}]
[:div {:class (stl/css :text)}
[:h2 {:class (stl/css :title)} (tr "dasboard.walkthrough-hero.title")]
[:p {:class (stl/css :info)} (tr "dasboard.walkthrough-hero.info")]
[:a {:class (stl/css :btn-primary :action)
:href " https://design.penpot.app/walkthrough"
:target "_blank"
:on-click handle-walkthrough-link}
(tr "dasboard.walkthrough-hero.start")]]
[:button {:class (stl/css :close)
:on-click close-walkthrough
:aria-label (tr "labels.close")}
close-icon]]))
(mf/defc project-item
[{:keys [project first? team files] :as props}]
(let [locale (mf/deref i18n/locale)
@@ -365,7 +287,7 @@
(l/derived :dashboard-recent-files st/state))
(mf/defc projects-section
[{:keys [team projects profile default-project-id] :as props}]
[{:keys [team projects profile] :as props}]
(let [projects (->> (vals projects)
(sort-by :modified-at)
(reverse))
@@ -378,8 +300,6 @@
(:team-hero? props true)
(not (:is-default team)))
tutorial-viewed? (:viewed-tutorial? props true)
walkthrough-viewed? (:viewed-walkthrough? props true)
is-my-penpot (= (:default-team-id profile) (:id team))
team-id (:id team)
@@ -391,28 +311,6 @@
(ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero"
::ev/origin "dashboard"}))))
close-tutorial
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:viewed-tutorial? true})
(ptk/data-event ::ev/event {::ev/name "dont-show-tutorial"
::ev/origin "get-started-hero"
:type "tutorial"
:section "dashboard"}))))
close-walkthrough
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:viewed-walkthrough? true})
(ptk/data-event ::ev/event {::ev/name "dont-show-walkthrough"
::ev/origin "get-started-hero"
:type "walkthrough"
:section "dashboard"}))))
show-hero? (and is-my-penpot
(or (not tutorial-viewed?)
(not walkthrough-viewed?)))
show-team-hero? (and (not is-my-penpot) team-hero?)]
(mf/with-effect [team]
@@ -433,22 +331,9 @@
(when team-hero?
[:& team-hero {:team team :close-fn close-banner}])
(when (and (contains? cf/flags :dashboard-templates-section)
show-hero?)
[:div {:class (stl/css :hero-projects)}
(when (and (not tutorial-viewed?) (:is-default team))
[:& tutorial-project
{:close-tutorial close-tutorial
:default-project-id default-project-id}])
(when (and (not walkthrough-viewed?) (:is-default team))
[:& interface-walkthrough
{:close-walkthrough close-walkthrough}])])
[:div {:class (stl/css-case :dashboard-container true
:no-bg true
:dashboard-projects true
:with-hero show-hero?
:with-team-hero show-team-hero?)}
(for [{:keys [id] :as project} projects]
(let [files (when recent-map

View File

@@ -20,7 +20,6 @@
height: calc(100vh - $s-64);
}
.with-hero,
.with-team-hero {
height: calc(100vh - $s-280);
}
@@ -86,8 +85,6 @@
color: var(--title-foreground-color-hover);
cursor: pointer;
height: $s-16;
display: inline-flex;
align-items: center;
}
.info-wrapper {
@@ -242,88 +239,3 @@
width: 0;
}
}
.hero-projects {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: $s-32;
margin: 0 $s-16 $s-16 $s-20;
@media (max-width: 1366px) {
grid-template-columns: 1fr;
}
.tutorial,
.walkthrough {
display: grid;
grid-template-columns: auto 1fr;
position: relative;
border-radius: $br-8;
min-height: $s-216;
background-color: $db-tertiary;
padding: $s-8;
.thumbnail {
width: $s-200;
height: $s-200;
border-radius: $br-6;
padding: $s-32;
display: block;
background-color: var(--color-canvas);
}
img {
border-radius: $br-4;
margin-bottom: 0;
width: $s-232;
}
.text {
padding: $s-32;
display: flex;
flex-direction: column;
}
.title {
color: $df-primary;
font-size: $fs-24;
font-weight: $fw400;
margin-bottom: $s-8;
}
.info {
flex: 1;
color: $df-secondary;
margin-bottom: $s-20;
font-size: $fs-16;
}
.invite {
height: $s-32;
}
.action {
width: $s-180;
height: $s-40;
}
}
.walkthrough {
.thumbnail {
background-image: url("/images/walkthrough-cover.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
}
.tutorial {
.thumbnail {
background-image: url("/images/hands-on-tutorial.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.loader {
display: flex;
svg#loader-pencil {
width: $s-32;
}
}
}
}

View File

@@ -155,7 +155,7 @@
[:div {:class (stl/css :img-container)}
[:a {:id id
:tab-index (if (or (not is-visible) collapsed) "-1" "0")
:href "https://penpot.app/libraries-templates.html"
:href "https://penpot.app/libraries-templates"
:target "_blank"
:on-click on-click
:on-key-down on-key-down}

View File

@@ -142,7 +142,9 @@
(modal/show! {:type :onboarding-newsletter})
(contains? cf/flags :onboarding-team)
(modal/show! {:type :onboarding-team}))))]
(modal/show! {:type :onboarding-team}))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/with-effect [@slide]
(when (not= :start @slide)
@@ -151,8 +153,8 @@
(fn []
(reset! klass nil)
(tm/dispose! sem))))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
(case @slide
:start [:& onboarding-welcome {:next #(navigate :opensource)}]

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.onboarding.newsletter
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cf]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
@@ -35,9 +36,11 @@
(st/emit! (when (or @newsletter-updates @newsletter-news)
(msg/success message))
(modal/show {:type :onboarding-team})
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))]
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-left)}
[:img {:src "images/deco-newsletter.png"

View File

@@ -287,9 +287,11 @@
(modal/show! {:type :onboarding-team})
:else
(modal/hide!)))))]
(modal/hide!)))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div {:class (stl/css :modal-container)
:ref container}
(case @step

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data.macros :as dmc]
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.messages :as msg]
@@ -84,14 +85,16 @@
::ev/origin "onboarding"
:step 1}))))
teams (mf/deref refs/teams)]
teams (mf/deref refs/teams)
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/with-effect [teams]
(when (> (count teams) 1)
(st/emit! (modal/hide))))
(when (< (count teams) 2)
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]
[:div {:class (stl/css :separator)}]
@@ -212,9 +215,11 @@
(if (> (count emails) 0)
(on-invite-now form)
(on-invite-later form))
(modal/hide!))))]
(modal/hide!))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]

View File

@@ -99,7 +99,7 @@
[:h2 "Libraries & templates"]]
[:div.modal-content
[:p "Weve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpots Design System are among them (and a lot more to come!)."]
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates.html"} "Explore libraries & templates"]]]
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates"} "Explore libraries & templates"]]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
[:& c/navigation-bullets

View File

@@ -8,196 +8,203 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
;; TODO: Review all copies and alt text
(defmethod c/render-release-notes "2.0"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-intro-image.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
(let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-intro-image.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Welcome to Penpot 2.0! "]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Welcome to Penpot 2.0! "]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"CSS Grid Layout: "]
"Bring your designs to life, knowing that what you create is what developers code."]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"CSS Grid Layout: "]
"Bring your designs to life, knowing that what you create is what developers code."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"Sleeker UI: "]
"Weve polished Penpot to make your experience smoother and more enjoyable."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"Sleeker UI: "]
"Weve polished Penpot to make your experience smoother and more enjoyable."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"New Components System: "]
"Managing and using your design components got a whole lot better."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"New Components System: "]
"Managing and using your design components got a whole lot better."]
[:p {:class (stl/css :feature-content)}
"And thats not all - weve fined tuned performance and "
"accessibility to give you a better and more fluid design experience."]
[:p {:class (stl/css :feature-content)}
"And thats not all - weve fined tuned performance and "
"accessibility to give you a better and more fluid design experience."]
[:p {:class (stl/css :feature-content)}
" Ready to dive in? Let 's get started!"]]
[:p {:class (stl/css :feature-content)}
" Ready to dive in? Let 's get started!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-css-grid.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's CSS Grid Layout"}]
0
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-css-grid.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's CSS Grid Layout"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"CSS Grid Layout - Design Meets Development"]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"CSS Grid Layout - Design Meets Development"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The much-awaited Grid Layout introduces 2-dimensional"
" layout capabilities to Penpot, allowing for the creation"
" of adaptive layouts by leveraging the power of CSS properties."]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The much-awaited Grid Layout introduces 2-dimensional"
" layout capabilities to Penpot, allowing for the creation"
" of adaptive layouts by leveraging the power of CSS properties."]
[:p {:class (stl/css :feature-content)}
"Its a host of new features, including columns and"
" rows management, flexible units such as FR (fractions),"
" the ability to create and name areas, and tons of new "
"and unique possibilities within a design tool."]
[:p {:class (stl/css :feature-content)}
"Its a host of new features, including columns and"
" rows management, flexible units such as FR (fractions),"
" the ability to create and name areas, and tons of new "
"and unique possibilities within a design tool."]
[:p {:class (stl/css :feature-content)}
"Designers will learn CSS basics while working, "
"and as always with Penpot, developers can pick"
" up the design as code to take it from there."]]
[:p {:class (stl/css :feature-content)}
"Designers will learn CSS basics while working, "
"and as always with Penpot, developers can pick"
" up the design as code to take it from there."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
1
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-new-ui.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's UI Makeover"}]
1
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-new-ui.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's UI Makeover"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"We've completely overhauled Penpot's user interface. "
"The improvements in consistency, the introduction of "
"new microinteractions, and attention to countless details"
" will significantly enhance the productivity and enjoyment of using Penpot."]
[:p {:class (stl/css :feature-content)}
"Furthermore, weve made several accessibility improvements, "
"with better color contrast, keyboard navigation,"
" and adherence to other best practices."]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"We've completely overhauled Penpot's user interface. "
"The improvements in consistency, the introduction of "
"new microinteractions, and attention to countless details"
" will significantly enhance the productivity and enjoyment of using Penpot."]
[:p {:class (stl/css :feature-content)}
"Furthermore, weve made several accessibility improvements, "
"with better color contrast, keyboard navigation,"
" and adherence to other best practices."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
2
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-components.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's new components system"}]
2
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-components.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's new components system"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New Components System"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The new Penpot components system improves"
" control over instances, including their "
"inheritances and properties overrides. "
"Main components are now accessible as design"
" elements, allowing a better updating "
"workflow through instant changes synchronization."]
[:p {:class (stl/css :feature-content)}
"And thats not all, there are new capabilities "
"such as component swapping and annotations "
"that will help you to better manage your design systems."]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New Components System"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The new Penpot components system improves"
" control over instances, including their "
"inheritances and properties overrides. "
"Main components are now accessible as design"
" elements, allowing a better updating "
"workflow through instant changes synchronization."]
[:p {:class (stl/css :feature-content)}
"And thats not all, there are new capabilities "
"such as component swapping and annotations "
"that will help you to better manage your design systems."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
3
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-html.gif"
:class (stl/css :start-image)
:border "0"
:alt " Penpot's HTML code generator"}]
3
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-html.gif"
:class (stl/css :start-image)
:border "0"
:alt " Penpot's HTML code generator"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"And much more"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"In addition to all of this, weve included several other requested improvements:"]
[:ul {:class (stl/css :feature-list)}
[:li "Access HTML markup code directly in inspect mode"]
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
[:li "Enjoy new color themes with options for both dark and light modes"]
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"And much more"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"In addition to all of this, weve included several other requested improvements:"]
[:ul {:class (stl/css :feature-list)}
[:li "Access HTML markup code directly in inspect mode"]
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
[:li "Enjoy new color themes with options for both dark and light modes"]
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
[:div {:class (stl/css :navigation)}
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]]))))

View File

@@ -122,7 +122,11 @@
(add! :stroke-cap-end)))
(cond-> text?
(-> (add! :grow-type)
(-> (add! :x)
(add! :y)
(add! :width)
(add! :height)
(add! :grow-type)
(add! :content (comp json/encode uuid->string))
(add! :position-data (comp json/encode uuid->string))))

View File

@@ -17,6 +17,7 @@
(mf/fnc group-shape
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
childs (unchecked-get props "childs")
render-id (mf/use-ctx muc/render-id)
@@ -36,21 +37,31 @@
mask-props (if ^boolean masked-group?
#js {:mask (mask-url render-id mask)}
#js {})]
#js {})
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
;; be stored in the context. When rendering the styles we add its id as prefix.
[svg-wrapper svg-wrapper-props]
(if (and (contains? shape :svg-attrs) (not current-svg-root-id))
[(mf/provider muc/current-svg-root-id) #js {:value (:id shape)}]
[mf/Fragment #js {}])]
;; We need to separate mask and clip into two because a bug in
;; Firefox breaks when the group has clip+mask+foreignObject
;; Clip and mask separated will work in every platform Firefox
;; bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805
[:> wrapper clip-props
[:> wrapper mask-props
(when ^boolean masked-group?
[:& render-mask {:mask mask}])
[:> svg-wrapper svg-wrapper-props
[:> wrapper clip-props
[:> wrapper mask-props
(when ^boolean masked-group?
[:& render-mask {:mask mask}])
(for [item childs]
[:& shape-wrapper
{:shape item
:key (dm/str (dm/get-prop item :id))}])]]))))
(for [item childs]
[:& shape-wrapper
{:shape item
:key (dm/str (dm/get-prop item :id))}])]]]))))

View File

@@ -94,6 +94,13 @@
(obj/unset! "disable-shadows?")
(obj/set! "ref" ref)
(obj/set! "id" (dm/fmt "shape-%" shape-id))
;; TODO: This is added for backward compatibility.
(cond-> (and (cfh/text-shape? shape) (empty? (:position-data shape)))
(-> (obj/set! "x" (:x shape))
(obj/set! "y" (:y shape))
(obj/set! "width" (:width shape))
(obj/set! "height" (:height shape))))
(obj/set! "style" styles))
wrapper-props

View File

@@ -104,9 +104,20 @@
svg-root? (and (map? content) (= tag :svg))
svg-tag? (map? content)
svg-leaf? (string? content)
valid-tag? (contains? csvg/svg-tags tag)]
valid-tag? (contains? csvg/svg-tags tag)
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
;; be stored in the context and with this we scoped the styles:
style-content
(when (= tag :style)
(dm/str "#shape-" current-svg-root-id "{ " (->> shape :content :content (str/join "\n")) " }"))]
(cond
(= tag :style)
[:style style-content]
^boolean svg-root?
[:& svg-root {:shape shape}
(for [item childs]

View File

@@ -9,6 +9,8 @@
(:require
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.main.data.events :as ev]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
@@ -16,6 +18,7 @@
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.webapi :as wapi]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(mf/defc error-container
@@ -146,15 +149,19 @@
(mf/defc exception-page
{::mf/props :obj}
[{:keys [data] :as props}]
(case (:type data)
:not-found
[:& not-found]
[{:keys [data route] :as props}]
(let [type (:type data)
path (:path route)
query-params (u/map->query-string (:query-params route))]
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
(case (:type data)
:not-found
[:& not-found]
:bad-gateway
[:& bad-gateway]
:bad-gateway
[:& bad-gateway]
:service-unavailable
[:& service-unavailable]
:service-unavailable
[:& service-unavailable]
[:> internal-error props]))
[:> internal-error props])))

View File

@@ -180,7 +180,7 @@
:on-zoom-fit handle-zoom-fit
:on-fullscreen toggle-fullscreen}]
(when (:can-edit permissions)
(when (:in-team permissions)
[:span {:on-click go-to-workspace
:class (stl/css :edit-btn)}
i/curve])
@@ -191,7 +191,9 @@
:on-click toggle-fullscreen}
i/expand]
(when (:is-admin permissions)
(when (and
(:in-team permissions)
(:is-admin permissions))
[:button {:on-click open-share-dialog
:class (stl/css :share-btn)}
(tr "labels.share")])
@@ -301,8 +303,8 @@
;; If the user doesn't have permission we disable the link
[:a {:class (stl/css :home-link)
:on-click go-to-dashboard
:style {:cursor (when-not (:can-edit permissions) "auto")
:pointer-events (when-not (:can-edit permissions) "none")}}
:style {:cursor (when-not (:in-team permissions) "auto")
:pointer-events (when-not (:in-team permissions) "none")}}
[:span {:class (stl/css :logo-icon)}
i/logo-icon]]
@@ -321,7 +323,7 @@
:title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))}
i/play]
(when (or (:can-edit permissions)
(when (or (:in-team permissions)
(= (:who-comment permissions) "all"))
[:button {:on-click navigate
:data-value "comments"
@@ -330,7 +332,7 @@
:title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))}
i/comments])
(when (or (= (:type permissions) :membership)
(when (or (:in-team permissions)
(and (= (:type permissions) :share-link)
(= (:who-inspect permissions) "all")))
[:button {:on-click go-to-inspect

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.colorpicker.libraries
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as c]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.events :as ev]
@@ -24,7 +25,7 @@
[rumext.v2 :as mf]))
(mf/defc libraries
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
(let [selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent)
current-colors (mf/use-state [])
@@ -43,10 +44,17 @@
(parse-uuid event)))))
check-valid-color?
(fn [color]
(and (or (not disable-gradient) (not (:gradient color)))
(or (not disable-opacity) (= 1 (:opacity color)))
(or (not disable-image) (not (:image color)))))
(mf/use-fn
(fn [color]
(and (or (not disable-gradient) (not (:gradient color)))
(or (not disable-opacity) (= 1 (:opacity color)))
(or (not disable-image) (not (:image color))))))
;; Sort colors by hue and lightness
get-sorted-colors
(mf/use-fn
(fn [colors]
(sort c/sort-colors (into [] (filter check-valid-color?) colors))))
toggle-palette
(mf/use-fn
@@ -89,13 +97,15 @@
(sort-by :name)
(map #(assoc % :file-id file-id)))))]
(reset! current-colors (into [] (filter check-valid-color?) colors))))
(if (not= @selected :recent)
(reset! current-colors (get-sorted-colors colors))
(reset! current-colors (into [] (filter check-valid-color? colors))))))
;; If the file colors change and the file option is selected updates the state
(mf/with-effect [file-colors]
(when (= @selected :file)
(let [colors (vals file-colors)]
(reset! current-colors (into [] (filter check-valid-color?) colors)))))
(reset! current-colors (get-sorted-colors colors)))))
[:div {:class (stl/css :libraries)}
[:div {:class (stl/css :select-wrapper)}

View File

@@ -181,14 +181,14 @@
[:span {:class (stl/css :resalted-area)}]]]
[:div {:class (stl/css :constraints-center)}
[:button {:class (stl/css-case :constraint-btn true
:active (= constraints-h :center))
:data-value "centerh"
:active (= constraints-v :center))
:data-value "centerv"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]
[:button {:class (stl/css-case :constraint-btn-special true
:constraint-btn-rotated true
:active (= constraints-v :center))
:data-value "centerv"
:active (= constraints-h :center))
:data-value "centerh"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]]
[:div {:class (stl/css :constraints-right)}

View File

@@ -69,33 +69,34 @@
on-add
(mf/use-fn
(mf/deps ids)
(mf/deps ids fills)
(fn [_]
(st/emit! (dc/add-fill ids {:color default-color
:opacity 1}))
(when (not (some? (seq fills))) (open-content))))
(when (or (= :multiple fills)
(not (some? (seq fills))))
(open-content))))
on-change
(mf/use-fn
(mf/deps ids)
(fn [index]
(fn [color]
(st/emit! (dc/change-fill ids color index)))))
(fn [index]
(fn [color]
(st/emit! (dc/change-fill ids color index))))
on-reorder
(mf/use-fn
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-fills ids index new-index)))))
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-fills ids index new-index))))
on-remove
(fn [index]
(fn []
(st/emit! (dc/remove-fill ids {:color default-color
:opacity 1} index))
(when (= 1 (count (seq fills))) (close-content))))
(when (or (= :multiple fills)
(= 1 (count (seq fills))))
(close-content))))
on-remove-all
(fn [_]
(st/emit! (dc/remove-all-fills ids {:color clr/black

View File

@@ -272,9 +272,21 @@
(def has-position? #{:frame :rect :image :text})
(defn parse-position
[props svg-data]
(let [values (->> (select-keys svg-data [:x :y :width :height])
(d/mapm (fn [_ val] (d/parse-double val))))]
[props node svg-data]
(let [x (get-meta node :x d/parse-double)
y (get-meta node :y d/parse-double)
width (get-meta node :width d/parse-double)
height (get-meta node :height d/parse-double)
values (->> (select-keys svg-data [:x :y :width :height])
(d/mapm (fn [_ val] (d/parse-double val))))
values
(cond-> values
(some? x) (assoc :x x)
(some? y) (assoc :y y)
(some? width) (assoc :width width)
(some? height) (assoc :height height))]
(d/merge props values)))
(defn parse-circle
@@ -392,7 +404,7 @@
center (gpt/point center-x center-y)]
(cond-> props
(has-position? type)
(parse-position svg-data)
(parse-position node svg-data)
(= type :svg-raw)
(add-svg-position node)

View File

@@ -352,7 +352,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers te "
"deel of voeg by vanaf ons [Biblioteke en sjablone](https://penpot.app/"
"libraries-templates.html)."
"libraries-templates)."
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-github-submit"

View File

@@ -306,7 +306,7 @@ msgstr "تكرير %s الملفات"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"أوه لا! ليس لديك ملفات بعد! إذا كنت تريد تجربة بعض القوالب ، فانتقل إلى "
"[المكتبات والقوالب] (https://penpot.app/libraries-templates.html)"
"[المكتبات والقوالب] (https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "تنزيل ملفات ٪s Penpot (.penpot)"

View File

@@ -311,7 +311,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a "
"la secció [Biblioteques i "
"plantilles](https://penpot.app/libraries-templates.html)"
"plantilles](https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "Baixa %s fitxers Penpot (.penpot)"

View File

@@ -310,7 +310,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo "
"je přidat z našich [Libraries & "
"templates](https://penpot.app/libraries-templates.html)."
"templates](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Stáhnout soubory %s Penpot (.penpot)"

View File

@@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Files added to Libraries will appear here. Try sharing your files or add "
"from our [Libraries & "
"templates](https://penpot.app/libraries-templates.html)."
"templates](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Download %s Penpot files (.penpot)"

View File

@@ -412,7 +412,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar "
"con alguna plantilla ve a [Bibliotecas y "
"plantillas](https://penpot.app/libraries-templates.html)."
"plantillas](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Descargar %s archivos Penpot (.penpot)"

View File

@@ -258,7 +258,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir "
"sus archivos o agréguelos desde nuestras [Libraries & "
"templates](https://penpot.app/libraries-templates.html)."
"templates](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Descargar %s archivos Penpot (.penpot)"

View File

@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
"baduzu joan [Liburutegi eta "
"txantiloiak](https://penpot.app/libraries-templates.html) atalera."
"txantiloiak](https://penpot.app/libraries-templates) atalera."
msgid "dashboard.export-binary-multi"
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"

View File

@@ -308,7 +308,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"وای نه! شما هنوز هیچ فایلی ندارید! اگر می‌خواهید چند الگو را امتحان کنید، "
"به [کتابخانه‌ها و الگوها] بروید "
"(https://penpot.app/libraries-templates.html)"
"(https://penpot.app/libraries-templates)"
#, fuzzy
msgid "dashboard.export-binary-multi"

View File

@@ -298,7 +298,7 @@ msgstr "Tvítak %s fílur"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Áh nei! Tú hevur ongar fílur enn! Um tú vilt royna við nøkrum skapilónum, "
"vitja [Libraries & templates](https://penpot.app/libraries-templates.html)"
"vitja [Libraries & templates](https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "Heinta %s Penpot fílur (.penpot)"

View File

@@ -397,7 +397,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Oh non ! Vous n'avez pas encore de fichiers ! Si vous voulez essayer avec "
"des modèles, allez sur [Bibliothèques et modèles] "
"(https://penpot.app/libraries-templates.html)."
"(https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Télécharger %s fichiers Penpot (.penpot)"

View File

@@ -306,7 +306,7 @@ msgstr "Duplicar % ficheiros"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Ai non! Ainda non tes ficheiros! Se queres facer a proba con algún modelo "
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates.html)"
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "Descargar %s ficheiros Penpot (.penpot)"

View File

@@ -381,7 +381,7 @@ msgstr "שכפול %s קבצים"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"קבצים שנוספו לספריות יתווספו לכאן. כדאי לנסות לשתף את הקבצים שלך או להוסיף "
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates.html)."
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "הורדת %s קובצי Penpot (.penpot)"

View File

@@ -308,7 +308,7 @@ msgstr "Kopiraj %s datoteka"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na "
"[Biblioteke i predlošci](https://penpot.app/libraries-templates.html)"
"[Biblioteke i predlošci](https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "Preuzmi %s Penpot datoteke (.penpot)"

View File

@@ -401,7 +401,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan "
"berkas Anda atau menambahkan dari [Pustaka & "
"templat](https://penpot.app/libraries-templates.html) kami."
"templat](https://penpot.app/libraries-templates) kami."
msgid "dashboard.export-binary-multi"
msgstr "Unduh %s berkas Penpot (.penpot)"

View File

@@ -304,7 +304,7 @@ msgstr "Duplicare %s file"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Oh no! Non hai ancora nessun file! Se desideri provare alcuni template vai "
"su [Librerie e template](https://penpot.app/libraries-templates.html)"
"su [Librerie e template](https://penpot.app/libraries-templates)"
msgid "dashboard.export-binary-multi"
msgstr "Scarica %s file Penpot (.penpot)"

View File

@@ -267,7 +267,7 @@ msgstr "%s ファイルを複製"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"まだファイルがありません。もしいくつかのテンプレートを試してみたいなら、[Libraries & "
"templates](https://penpot.app/libraries-templates.html) をチェックしてみてください。"
"templates](https://penpot.app/libraries-templates) をチェックしてみてください。"
msgid "dashboard.export-frames"
msgstr "PDFでエクスポート"

View File

@@ -297,7 +297,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Čia bus rodomi prie bibliotekų pridėti failai. Pabandykite bendrinti failus "
"arba pridėti iš mūsų [Bibliotekos ir šablonai] "
"(https://penpot.app/libraries-templates.html)"
"(https://penpot.app/libraries-templates)"
msgid "dashboard.export-frames"
msgstr "Eksportuokite darbalaukius į PDF"

View File

@@ -404,7 +404,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Šeit tiks parādītas bibliotēkām pievienotās datnes. Mēģini koplietot datnes "
"vai pievienot tās no mūsu [bibliotēkām un veidnēm](https://penpot.app/"
"libraries-templates.html)."
"libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Lejupielādēt %s Penpot datnes (.penpot)"

View File

@@ -225,7 +225,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"ഇതുവരെയും ഇവിടെ ഫയലുകളില്ല. നിങ്ങൾക്ക് ചില ടെമ്പ്ലേറ്റുകൾ "
"പരീക്ഷിക്കണമെന്നുണ്ടെങ്കിൽ [ലൈബ്രറികളുടെയും ടെമ്പ്ലേറ്റുകളുടെയും "
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates.html) പോകാവുന്നതാണ്"
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates) പോകാവുന്നതാണ്"
msgid "dashboard.export-frames"
msgstr "ആർട്ട്ബോർഡുകൾ പിഡിഎഫായി എക്സ്പോർട്ട് ചെയ്യുക"

View File

@@ -336,7 +336,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Fail yang ditambahkan pada Perpustakaan akan dipaparkan di sini. Cuba kongsi "
"fail anda atau tambahkan daripada [Perpustakaan & templat](https://penpot."
"app/libraries-templates.html) kami."
"app/libraries-templates) kami."
msgid "dashboard.export-binary-multi"
msgstr "Muat turun %s fail Penpot (.penpot)"

View File

@@ -417,7 +417,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Bestanden die aan bibliotheken zijn toegevoegd, worden hier weergegeven. "
"Probeer je bestanden te delen of toe te voegen vanuit onze [Bibliotheken & "
"sjablonen] (https://penpot.app/libraries-templates.html)."
"sjablonen] (https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "%s Penpot-bestanden downloaden (.penpot)"

View File

@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Tutaj pojawią się pliki dodane do Bibliotek. Spróbuj udostępnić swoje pliki "
"lub dodać z naszych [Bibliotek i "
"szablonów](https://penpot.app/libraries-templates.html)."
"szablonów](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Pobierz %s plików Penpot (.penpot)"

View File

@@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Arquivos adicionados na biblioteca de ativos vão aparecer aqui. Tente "
"compartilhar seus arquivos ou adicione das nossas [Bibliotecas & "
"modelos](https://penpot.app/libraries-templates.html)."
"modelos](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Baixar %s arquivos Penpot (.penpot)"

View File

@@ -400,7 +400,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Oh não! Ainda não tens ficheiros! Se quiseres experimentar podes começar "
"com os nossos templates em [Libraries & "
"templates](https://penpot.app/libraries-templates.html)."
"templates](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Descarrega %s ficheiros Penpot (.penpot)"

View File

@@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Fișierele adăugate la Biblioteci vor apărea aici. Încercați să partajați "
"fișierele dvs. sau adăugați-le din [Biblioteci și "
"șabloane](https://penpot.app/libraries-templates.html)."
"șabloane](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Descărcați %s fișiere Penpot (.penpot)"

View File

@@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Файлы, добавленные в Библиотеки, появятся здесь. Попробуйте поделиться "
"своими файлами или добавить их из наших [Библиотек и "
"шаблонов](https://penpot.app/libraries-templates.html)."
"шаблонов](https://penpot.app/libraries-templates)."
msgid "dashboard.export-binary-multi"
msgstr "Скачать файлы Penpot (.penpot) (%s)"

View File

@@ -311,7 +311,7 @@ msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"Kütüphanelere eklenen dosyalar burada görünecektir. Dosyalarınızı "
"paylaşmayı deneyin veya [Kütüphaneler ve "
"şablonlarımızdan](https://penpot.app/libraries-templates.html) ekleyin."
"şablonlarımızdan](https://penpot.app/libraries-templates) ekleyin."
msgid "dashboard.export-binary-multi"
msgstr "%s Penpot dosyasını indir (.penpot)"

View File

@@ -295,7 +295,7 @@ msgstr "複製 %s 個檔案"
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
"添加在資料庫的檔案會在此處列出。請分享你的檔案或由我們的 [資料庫 & "
"模板區段](https://penpot.app/libraries-templates.html) 添加。"
"模板區段](https://penpot.app/libraries-templates) 添加。"
msgid "dashboard.export-binary-multi"
msgstr "下載 %s 個Penpot 檔案 .penpot"

View File

@@ -1 +1 @@
2.0.0
2.0.3