Compare commits

..

78 Commits

Author SHA1 Message Date
Andrey Antukh
4ad34ab5c8 📎 Update version number. 2021-11-24 13:06:36 +01:00
Andrey Antukh
33c7847dfc 🐛 Fix team deletion flow on dashboard. 2021-11-24 13:05:54 +01:00
alonso.torres
7a04f15710 🐛 Fix problems with team management. 2021-11-24 13:05:48 +01:00
Andrey Antukh
b8043a2432 📎 Update ci config. 2021-11-18 17:19:55 +01:00
Andrey Antukh
ed5de525aa 📎 Increase default db pool size to 50. 2021-11-18 17:19:55 +01:00
Andrey Antukh
8105d9388b ♻️ Refactor rlimit usage (backend). 2021-11-18 17:19:55 +01:00
Andrey Antukh
8151dcc05f 📎 Improve services defmethod linter hook. 2021-11-18 17:19:55 +01:00
Andrey Antukh
25b1c5fe90 📎 Minor update on feedback module. 2021-11-17 14:46:18 +01:00
Andrey Antukh
ea218839e4 Minor change on error pruning mechanism. 2021-11-17 11:10:28 +01:00
Andrey Antukh
4c18a1881b 📎 Minor change on feedback subject template. 2021-11-17 11:10:04 +01:00
Andrey Antukh
0bdbbd35e3 📎 Fix linter issues. 2021-11-12 12:37:38 +01:00
Andrey Antukh
401afe7c1a 📎 Change loggling level on oauth ns. 2021-11-12 12:37:34 +01:00
Andrés Moya
c5adeecd90 🐛 Fix problems importing files 2021-11-12 12:34:26 +01:00
Andrey Antukh
da6c62414b Merge remote-tracking branch 'origin/beta-release-info' 2021-11-11 13:54:01 +01:00
Andrey Antukh
6650fe863f 📎 Fix linter issues. 2021-11-11 13:28:02 +01:00
Andrey Antukh
76c00c42b5 📎 Update changelog. 2021-11-11 13:25:51 +01:00
Andrey Antukh
f8609419a1 Merge remote-tracking branch 'origin/develop' 2021-11-11 13:23:49 +01:00
Andrey Antukh
250e79eda1 Disable default project loading on demo users. 2021-11-11 13:23:07 +01:00
Andrey Antukh
f7401daeae 📎 Update label on version.txt 2021-11-11 13:22:43 +01:00
Andrey Antukh
7390e372e0 📎 Add missing translations. 2021-11-11 13:22:29 +01:00
Andrey Antukh
239c521ad9 📎 Minor change on gulpfile. 2021-11-11 12:21:21 +01:00
Andrey Antukh
77b4f09cfb 📎 Update onboarding texts. 2021-11-11 12:13:16 +01:00
Andrey Antukh
bb178af278 🐛 Fix import template on recently created team. 2021-11-11 11:49:23 +01:00
Andrey Antukh
3c39661174 📎 Enable _blank target on all markdown links. 2021-11-11 11:31:27 +01:00
Andrés Moya
1fffc1e828 💄 Change placeholder text 2021-11-11 11:09:17 +01:00
Andrey Antukh
ed50cd1fa8 📎 Remove :insecure-register default flag (backend). 2021-11-11 11:00:23 +01:00
Andrey Antukh
ef6a02e8ef ⬆️ Update clk-kondo dependency on devenv. 2021-11-10 23:21:41 +01:00
Andrey Antukh
e7003dde83 Add :insecure-register flag.
This allows on-premise users skip the email validation.
2021-11-10 23:21:41 +01:00
Andrey Antukh
bf2a393fd3 🎉 Add generic retry middleware for rpc methods. 2021-11-10 23:21:41 +01:00
elhombretecla
bb2cfd52f4 Add new wording 2021-11-10 14:52:48 +01:00
Andrés Moya
6a6f88c6ef 📚 Update changelog 2021-11-10 12:17:23 +01:00
elhombretecla
0a2b1a4fbe 🎉 Add new beta onboarding info 2021-11-10 11:53:14 +01:00
Andrey Antukh
5fd48c9e98 📎 Update changelog. 2021-11-10 11:26:28 +01:00
alonso.torres
022d32cd44 🐛 Fix project files count not refreshing correctly after import 2021-11-10 11:08:32 +01:00
alonso.torres
af10cf71db 🐛 Add placeholder to create shareable link 2021-11-10 11:08:32 +01:00
alonso.torres
1bf1de8ce8 🐛 Fix problem in viewer with dropdowns when comments active 2021-11-10 11:08:32 +01:00
alonso.torres
b80ddfa580 🐛 Remove change style on hover for options 2021-11-10 11:08:32 +01:00
alonso.torres
aa276ab308 🐛 Fix viewer comment position when zoom applied 2021-11-10 11:08:32 +01:00
alonso.torres
f50943d470 🐛 Fix max/min values for opacity fields 2021-11-10 11:08:32 +01:00
alonso.torres
959c998664 🐛 Fix a worker error when transforming a rectangle into path 2021-11-10 11:08:32 +01:00
alonso.torres
b6b6b6043c 🐛 Add shortcuts to boolean icons popups 2021-11-10 11:08:32 +01:00
alonso.torres
8e0807d502 🐛 Fix problem when flattening booleans losing styles 2021-11-10 11:08:32 +01:00
alonso.torres
78d027b25e 🐛 Fix problem with text rendering on export 2021-11-10 11:08:32 +01:00
alonso.torres
503f0bee69 🐛 Add ellipsis in long labels for input fields 2021-11-10 11:08:32 +01:00
Andrés Moya
50d756b189 🐛 Disallow to create a redundant component 2021-11-05 16:55:38 +01:00
Andrey Antukh
7c3d71e572 Merge pull request #1320 from penpot/scroll
Preserve Scroll posiition
2021-11-04 15:20:26 +01:00
Andrey Antukh
bf895d26b0 📎 Port from develop fixes to frontend build script. 2021-11-04 11:00:22 +01:00
Andrey Antukh
5530e8581a Merge remote-tracking branch 'origin/main' into develop 2021-11-04 10:48:47 +01:00
Andrés Moya
f913816d87 🎉 Add preserve scroll option 2021-11-04 10:39:16 +01:00
Andrés Moya
3d59d31b0a 🐛 Fix horizontal scrollbar hidden 2021-11-04 10:37:56 +01:00
alonso.torres
9a66f26bd9 🐛 Fix problem with inner stroke 2021-11-04 10:36:51 +01:00
Andrey Antukh
d5b6605ce8 🐛 Fix issue on translation files. 2021-11-04 10:33:05 +01:00
Andrey Antukh
38e5184be4 📎 Minor fix on frontend build script. 2021-11-04 10:17:19 +01:00
Andrey Antukh
369ec9f814 📎 Fix on previous commit. 2021-11-04 09:43:40 +01:00
Andrey Antukh
620b454c49 📎 Minor changes on build script resource management. 2021-11-04 09:43:03 +01:00
Andrey Antukh
2e5040e65d Don't load initial project on profile creation. 2021-11-04 09:23:14 +01:00
Andrey Antukh
71fe7ef125 📎 Add better auditlog event for profile email change event. 2021-11-04 09:23:14 +01:00
Andrey Antukh
e0e8fd7ddc 📎 Increment version number. 2021-11-04 09:23:14 +01:00
Andrey Antukh
01b4b4933e Update devenv nginx config. 2021-11-04 09:23:14 +01:00
Andrey Antukh
fced22bc60 🎉 Add new onboarding flow. 2021-11-04 09:23:14 +01:00
Andrey Antukh
898ae64a57 ⬆️ Update frontend dependencies. 2021-11-04 09:23:14 +01:00
Andrey Antukh
8d50852cbe Minor imrovements on general purpose specs naming. 2021-11-04 09:23:14 +01:00
Andrey Antukh
a11c7b10ac 🔥 Remove deprecated fixtures related code. 2021-11-04 09:23:14 +01:00
Andrey Antukh
fe9033b8be Merge branch 'main' into develop 2021-11-03 16:41:55 +01:00
alonso.torres
e26f9e4a71 🐛 Fix problem with arrow lines 2021-11-03 16:41:03 +01:00
alonso.torres
c477328da4 🐛 Fix problem with view mode comments 2021-11-03 13:45:43 +01:00
alonso.torres
214c64c49e 🐛 Fix problem when exporting texts with gradients or opacity 2021-11-03 10:56:42 +01:00
Andrés Moya
bce0e9194c Merge branch 'main' into develop 2021-11-02 11:09:11 +01:00
Andrey Antukh
a0f98e3823 Merge pull request #1306 from penpot/hotfix-validate-url
 Auto add http prefix to interaction url
2021-10-29 14:11:38 +02:00
Andrés Moya
bff6768adf 🐛 Fix linter error 2021-10-29 13:38:47 +02:00
Andrés Moya
8ce2eb448c Auto add http prefix to interaction url 2021-10-29 13:38:47 +02:00
alonso.torres
7c5d00f8a4 🐛 Fix problem with export 2021-10-28 17:56:51 +02:00
Andrés Moya
30cd499014 Enhance border radius options form 2021-10-28 17:32:57 +02:00
Andrey Antukh
99d173789e Merge pull request #1304 from penpot/bugfix
Bugfix
2021-10-28 17:31:40 +02:00
alonso.torres
ae72db8129 🐛 Fix pages dropdown in viewer 2021-10-28 17:18:17 +02:00
alonso.torres
9437cc1806 🐛 Fix undo stacking when changing color from color-picker 2021-10-28 17:18:17 +02:00
alonso.torres
0e76aa0265 🐛 Fix problem with exporting before the document is saved 2021-10-28 17:18:17 +02:00
Andrey Antukh
756e654d32 📎 Fix linter issues. 2021-10-27 16:16:44 +02:00
134 changed files with 2387 additions and 1406 deletions

View File

@@ -36,17 +36,23 @@ jobs:
- run:
name: common lint
working_directory: "./common"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
- run:
name: frontend lint
working_directory: "./frontend"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
- run:
name: backend lint
working_directory: "./backend"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
# run backend test
- run:

View File

@@ -51,18 +51,28 @@
(defn service-defmethod
[{:keys [:node]}]
(let [[rnode rtype & other] (:children node)
(let [[rnode rtype ?meta & other] (:children node)
rsym (gensym (name (:k rtype)))
result (api/list-node
[(api/token-node (symbol "do"))
(api/list-node
[(api/token-node (symbol "declare"))
(api/token-node rsym)])
(if (= :map (:tag ?meta))
(api/list-node
[(api/token-node (symbol "reset-meta!"))
(api/token-node rsym)
?meta])
(api/list-node
[(api/token-node (symbol "comment"))
(api/token-node rsym)]))
(api/list-node
(into [(api/token-node (symbol "defmethod"))
(api/token-node rsym)
rtype]
other))])]
(cons ?meta other)))])]
;; (prn "==============" rtype (into {} ?meta))
;; (prn (api/sexpr result))
{:node result}))

View File

@@ -1,6 +1,5 @@
# CHANGELOG
## :rocket: Next
### :boom: Breaking changes
@@ -10,6 +9,54 @@
### :heart: Community contributions by (Thank you!)
# 1.10.1-beta
### :bug: Bugs fixed
- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353)
## 1.10.0-beta
### :boom: Breaking changes
- The initial project / data mechanism (not documented) has been
disabled. Is the mechanism used for creating initial project on user
signup. With the new onboarding approach, this subsystem is no
longer needed and is disabled.
### :sparkles: New features
- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190).
- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250).
- Add new onboarding modals.
### :bug: Bugs fixed
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189).
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191).
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087).
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200).
- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226).
- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237).
- Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224)
- Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223)
- Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217)
- Add shortcuts to boolean icons popups [Taiga #2220](https://tree.taiga.io/project/penpot/issue/2220)
- Fix a worker error when transforming a rectangle into path
- Fix max/min values for opacity fields [Taiga #2183](https://tree.taiga.io/project/penpot/issue/2183)
- Fix viewer comment position when zoom applied [Taiga #2240](https://tree.taiga.io/project/penpot/issue/2240)
- Remove change style on hover for options [Taiga #2172](https://tree.taiga.io/project/penpot/issue/2172)
- Fix problem in viewer with dropdowns when comments active [#1303](https://github.com/penpot/penpot/issues/1303)
- Add placeholder to create shareable link
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
### :heart: Community contributions by (Thank you!)
- To the translation community for the hard work on making penpot
available on so many languages.
## 1.9.0-alpha
### :boom: Breaking changes

View File

@@ -68,10 +68,6 @@
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:fn-fixtures
{:exec-fn app.cli.fixtures/run
:args {}}
:kaocha
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}}
:main-opts ["-m" "kaocha.runner"]}

View File

@@ -1 +1 @@
[PENPOT FEEDBACK]: {{subject|abbreviate:19}} (from {{email}})
[PENPOT FEEDBACK]: {{subject}}

View File

@@ -1,15 +1,9 @@
#!/bin/sh
#!/usr/bin/env bash
export PENPOT_ASSERTS_ENABLED=true
export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts"
set -ex
if [ ! -e ~/.fixtures-loaded ]; then
echo "Loading fixtures..."
clojure -Adev -X:fn-fixtures
touch ~/.fixtures-loaded
fi
if [ "$1" = "--watch" ]; then
echo "Start Watch..."
@@ -27,6 +21,3 @@ if [ "$1" = "--watch" ]; then
else
clojure -A:dev -M -m app.main
fi

View File

@@ -1,258 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.cli.fixtures
"A initial fixtures."
(:require
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.db :as db]
[app.main :as main]
[app.rpc.mutations.profile :as profile]
[app.util.blob :as blob]
[buddy.hashers :as hashers]
[integrant.core :as ig]))
(defn- mk-uuid
[prefix & args]
(uuid/namespaced uuid/zero (apply str prefix (interpose "-" args))))
;; --- Profiles creation
(def password (hashers/derive "123123"))
(def preset-small
{:num-teams 5
:num-profiles 5
:num-profiles-per-team 5
:num-projects-per-team 5
:num-files-per-project 5
:num-draft-files-per-profile 10})
(defn- rng-ids
[rng n max]
(let [stream (->> (.longs rng 0 max)
(.iterator)
(iterator-seq))]
(reduce (fn [acc item]
(if (= (count acc) n)
(reduced acc)
(conj acc item)))
#{}
stream)))
(defn- rng-vec
[rng vdata n]
(let [ids (rng-ids rng n (count vdata))]
(mapv #(nth vdata %) ids)))
(defn- rng-nth
[rng vdata]
(let [stream (->> (.longs rng 0 (count vdata))
(.iterator)
(iterator-seq))]
(nth vdata (first stream))))
(defn- collect
[f items]
(reduce #(conj %1 (f %2)) [] items))
(defn- register-profile
[conn params]
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn)))
(defn impl-run
[pool opts]
(let [rng (java.util.Random. 1)]
(letfn [(create-profile [conn index]
(let [id (mk-uuid "profile" index)
_ (l/info :action "create profile"
:index index
:id id)
prof (register-profile conn
{:id id
:fullname (str "Profile " index)
:password "123123"
:is-demo true
:email (str "profile" index "@example.com")})
team-id (:default-team-id prof)
owner-id id]
(let [project-ids (collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))]
(run! (partial create-files conn owner-id) project-ids))
prof))
(create-profiles [conn]
(l/info :action "create profiles")
(collect (partial create-profile conn)
(range (:num-profiles opts))))
(create-team [conn index]
(let [id (mk-uuid "team" index)
name (str "Team" index)]
(l/info :action "create team"
:index index
:id id)
(db/insert! conn :team {:id id
:name name})
id))
(create-teams [conn]
(l/info :action "create teams")
(collect (partial create-team conn)
(range (:num-teams opts))))
(create-file [conn owner-id project-id index]
(let [id (mk-uuid "file" project-id index)
name (str "file" index)
data (cp/make-file-data id)]
(l/info :action "create file"
:index index
:id id)
(db/insert! conn :file
{:id id
:data (blob/encode data)
:project-id project-id
:name name})
(db/insert! conn :file-profile-rel
{:file-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id))
(create-files [conn owner-id project-id]
(l/info :action "create files")
(run! (partial create-file conn owner-id project-id)
(range (:num-files-per-project opts))))
(create-project [conn team-id owner-id index]
(let [id (if index
(mk-uuid "project" team-id index)
(mk-uuid "project" team-id))
name (if index
(str "project " index)
"Drafts")
is-default (nil? index)]
(l/info :action "create project"
:index index
:id id)
(db/insert! conn :project
{:id id
:team-id team-id
:is-default is-default
:name name})
(db/insert! conn :project-profile-rel
{:project-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id))
(create-projects [conn team-id profile-ids]
(l/info :action "create projects")
(let [owner-id (rng-nth rng profile-ids)
project-ids (conj
(collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))
(create-project conn team-id owner-id nil))]
(run! (partial create-files conn owner-id) project-ids)))
(assign-profile-to-team [conn team-id owner? profile-id]
(db/insert! conn :team-profile-rel
{:team-id team-id
:profile-id profile-id
:is-owner owner?
:is-admin true
:can-edit true}))
(setup-team [conn team-id profile-ids]
(l/info :action "setup team"
:team-id team-id
:profile-ids (pr-str profile-ids))
(assign-profile-to-team conn team-id true (first profile-ids))
(run! (partial assign-profile-to-team conn team-id false)
(rest profile-ids))
(create-projects conn team-id profile-ids))
(assign-teams-and-profiles [conn teams profiles]
(l/info :action "assign teams and profiles")
(loop [team-id (first teams)
teams (rest teams)]
(when-not (nil? team-id)
(let [n-profiles-team (:num-profiles-per-team opts)
selected-profiles (rng-vec rng profiles n-profiles-team)]
(setup-team conn team-id selected-profiles)
(recur (first teams)
(rest teams))))))
(create-draft-file [conn owner index]
(let [owner-id (:id owner)
id (mk-uuid "file" "draft" owner-id index)
name (str "file" index)
project-id (:default-project-id owner)
data (cp/make-file-data id)]
(l/info :action "create draft file"
:index index
:id id)
(db/insert! conn :file
{:id id
:data (blob/encode data)
:project-id project-id
:name name})
(db/insert! conn :file-profile-rel
{:file-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id))
(create-draft-files [conn profile]
(run! (partial create-draft-file conn profile)
(range (:num-draft-files-per-profile opts))))
]
(db/with-atomic [conn pool]
(let [profiles (create-profiles conn)
teams (create-teams conn)]
(assign-teams-and-profiles conn teams (map :id profiles))
(run! (partial create-draft-files conn) profiles))))))
(defn run-in-system
[system preset]
(let [pool (:app.db/pool system)
preset (if (map? preset)
preset
(case preset
(nil "small" :small) preset-small
;; "medium" preset-medium
;; "big" preset-big
preset-small))]
(impl-run pool preset)))
(defn run
[{:keys [preset] :or {preset :small}}]
(let [config (select-keys main/system-config
[:app.db/pool
:app.telemetry/migrations
:app.migrations/migrations
:app.migrations/all
:app.metrics/metrics])
_ (ig/load-namespaces config)
system (-> (ig/prep config)
(ig/init))]
(try
(run-in-system system preset)
(catch Exception e
(l/error :hint "unhandled exception" :cause e))
(finally
(ig/halt! system)))))

View File

@@ -62,8 +62,9 @@
:assets-path "/internal/assets/"
:rlimits-password 10
:rlimits-image 2
:rlimit-password 10
:rlimit-image 2
:rlimit-font 5
:smtp-default-reply-to "Penpot <no-reply@example.com>"
:smtp-default-from "Penpot <no-reply@example.com>"
@@ -85,7 +86,7 @@
;; a server prop key where initial project is stored.
:initial-project-skey "initial-project"})
(s/def ::flags ::us/words)
(s/def ::flags ::us/set-of-keywords)
;; DEPRECATED PROPERTIES: should be removed in 1.10
(s/def ::registration-enabled ::us/boolean)
@@ -151,8 +152,9 @@
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::registration-domain-whitelist ::us/set-of-str)
(s/def ::rlimits-image ::us/integer)
(s/def ::rlimits-password ::us/integer)
(s/def ::rlimit-font ::us/integer)
(s/def ::rlimit-image ::us/integer)
(s/def ::rlimit-password ::us/integer)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-host ::us/string)
@@ -237,8 +239,9 @@
::redis-uri
::registration-domain-whitelist
::registration-enabled
::rlimits-image
::rlimits-password
::rlimit-font
::rlimit-image
::rlimit-password
::sentry-dsn
::sentry-debug
::sentry-attach-stack-trace
@@ -268,10 +271,16 @@
::telemetry-with-taiga
::tenant]))
(def default-flags
[:enable-backend-asserts
:enable-backend-api-doc
:enable-secure-session-cookies])
(defn- parse-flags
[config]
(-> (:flags config)
(flags/parse flags/default)))
(flags/parse flags/default
default-flags
(:flags config)))
(defn read-env
[prefix]

View File

@@ -113,7 +113,7 @@
(.setIdleTimeout 120000) ;; 2min
(.setMaxLifetime 1800000) ;; 30min
(.setMinimumIdle (:min-pool-size cfg 0))
(.setMaximumPoolSize (:max-pool-size cfg 30))
(.setMaximumPoolSize (:max-pool-size cfg 50))
(.setConnectionInitSql initsql)
(.setInitializationFailTimeout -1))

View File

@@ -45,7 +45,7 @@
(defn handler
[rpc]
(let [context (prepare-context rpc)]
(if (contains? cf/flags :api-doc)
(if (contains? cf/flags :backend-api-doc)
(fn [_]
{:status 200
:body (-> (io/resource "api-doc.tmpl")

View File

@@ -20,11 +20,19 @@
(get headers "x-real-ip")
(get request :remote-addr)))
(defn- simple-prune
([s] (simple-prune s (* 1024 1024)))
([s max-length]
(if (> (count s) max-length)
(str (subs s 0 max-length) " [...]")
s)))
(defn- stringify-data
[data]
(binding [clojure.pprint/*print-right-margin* 200]
(let [result (with-out-str (clojure.pprint/pprint data))]
(str/prune result (* 1024 1024) "[...]"))))
(simple-prune result (* 1024 1024)))))
(defn get-error-context
[request error]

View File

@@ -61,6 +61,7 @@
destination (cf/get :feedback-destination)]
(eml/send! {::eml/conn pool
::eml/factory eml/feedback
:from destination
:to destination
:profile profile
:reply-to (:from params)

View File

@@ -58,8 +58,7 @@
{:token (get data "access_token")
:type (get data "token_type")})))
(catch Exception e
(l/error :hint "unexpected error on retrieve-access-token"
:cause e)
(l/warn :hint "unexpected error on retrieve-access-token" :cause e)
nil)))
(defn- qualify-props
@@ -86,8 +85,7 @@
:props (->> (dissoc info :name :email)
(qualify-props provider))})))
(catch Exception e
(l/error :hint "unexpected exception on retrieve-user-info"
:cause e)
(l/warn :hint "unexpected exception on retrieve-user-info" :cause e)
nil)))
(s/def ::backend ::us/not-empty-string)
@@ -203,6 +201,7 @@
(sxf request)))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens :generate info)
params (d/without-nils

View File

@@ -53,12 +53,13 @@
(defn- add-cookies
[response {:keys [id] :as session}]
(let [cors? (contains? cfg/flags :cors)]
(let [cors? (contains? cfg/flags :cors)
secure? (contains? cfg/flags :secure-session-cookies)]
(assoc response :cookies {cookie-name {:path "/"
:http-only true
:value id
:same-site (if cors? :none :strict)
:secure true}})))
:secure secure?}})))
(defn- clear-cookies
[response]

View File

@@ -127,24 +127,6 @@
:audit (ig/ref :app.loggers.audit/collector)
:public-uri (cf/get :public-uri)}
;; RLimit definition for password hashing
:app.rlimits/password
(cf/get :rlimits-password)
;; RLimit definition for image processing
:app.rlimits/image
(cf/get :rlimits-image)
;; RLimit definition for font processing
:app.rlimits/font
(cf/get :rlimits-font 2)
;; A collection of rlimits as hash-map.
:app.rlimits/all
{:password (ig/ref :app.rlimits/password)
:image (ig/ref :app.rlimits/image)
:font (ig/ref :app.rlimits/font)}
:app.rpc/rpc
{:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http.session/session)
@@ -152,7 +134,6 @@
:metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:rlimits (ig/ref :app.rlimits/all)
:public-uri (cf/get :public-uri)
:audit (ig/ref :app.loggers.audit/collector)}

View File

@@ -12,7 +12,6 @@
[app.common.media :as cm]
[app.common.spec :as us]
[app.config :as cf]
[app.rlimits :as rlm]
[app.util.svg :as svg]
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
@@ -51,7 +50,6 @@
:code :media-type-not-allowed
:hint "Seems like you are uploading an invalid media object"))))
(defmulti process :cmd)
(defmulti process-error class)
@@ -66,17 +64,11 @@
(throw error))
(defn run
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
(us/assert map? rlimits)
(let [rlimit (get rlimits rlimit)]
(when-not rlimit
(ex/raise :type :internal
:code :rlimit-not-configured
:hint ":image rlimit not configured"))
(try
(rlm/execute rlimit (process params))
(catch Throwable e
(process-error e)))))
[params]
(try
(process params)
(catch Throwable e
(process-error e))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Thumbnails Generation

View File

@@ -1,45 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rlimits
"Resource usage limits (in other words: semaphores)."
(:require
[app.common.spec :as us]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
java.util.concurrent.Semaphore))
(s/def ::rlimit #(instance? Semaphore %))
(s/def ::rlimits (s/map-of ::us/keyword ::rlimit))
(derive ::password ::instance)
(derive ::image ::instance)
(derive ::font ::instance)
(defmethod ig/pre-init-spec ::instance [_]
(s/spec int?))
(defmethod ig/init-key ::instance
[_ permits]
(Semaphore. (int permits)))
(defn acquire!
[sem]
(.acquire ^Semaphore sem))
(defn release!
[sem]
(.release ^Semaphore sem))
(defmacro execute
[rlinst & body]
`(try
(acquire! ~rlinst)
~@body
(finally
(release! ~rlinst))))

View File

@@ -13,10 +13,10 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.metrics :as mtx]
[app.rlimits :as rlm]
[app.util.retry :as retry]
[app.util.rlimit :as rlimit]
[app.util.services :as sv]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
(defn- default-handler
@@ -73,33 +73,19 @@
[cfg f mdata]
(mtx/wrap-summary f (::mobj cfg) [(::sv/name mdata)]))
;; Wrap the rpc handler with a semaphore if it is specified in the
;; metadata asocciated with the handler.
(defn- wrap-with-rlimits
[cfg f mdata]
(if-let [key (:rlimit mdata)]
(let [rlinst (get-in cfg [:rlimits key])]
(when-not rlinst
(ex/raise :type :internal
:code :rlimit-not-configured
:hint (str/fmt "%s rlimit not configured" key)))
(l/trace :action "add rlimit"
:handler (::sv/name mdata))
(fn [cfg params]
(rlm/execute rlinst (f cfg params))))
f))
(defn- wrap-impl
[{:keys [audit] :as cfg} f mdata]
(let [f (wrap-with-rlimits cfg f mdata)
f (wrap-with-metrics cfg f mdata)
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(let [f (as-> f $
(rlimit/wrap-rlimit cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(wrap-with-metrics cfg $ mdata))
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(l/trace :action "register" :name (::sv/name mdata))
(with-meta
(fn [params]
;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request.
(when (and auth? (not (uuid? (:profile-id params))))
@@ -187,7 +173,7 @@
(defmethod ig/pre-init-spec ::rpc [_]
(s/keys :req-un [::storage ::session ::tokens ::audit
::mtx/metrics ::rlm/rlimits ::db/pool]))
::mtx/metrics ::db/pool]))
(defmethod ig/init-key ::rpc
[_ cfg]

View File

@@ -12,6 +12,7 @@
[app.rpc.queries.comments :as comments]
[app.rpc.queries.files :as files]
[app.util.blob :as blob]
[app.util.retry :as retry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -32,6 +33,9 @@
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
(sv/defmethod ::create-comment-thread
{::retry/enabled true
::retry/max-retries 3
::retry/matches retry/conflict-db-insert?}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-read-permissions! conn profile-id file-id)
@@ -43,7 +47,7 @@
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(defn- create-comment-thread*
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id position content] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
@@ -78,24 +82,6 @@
(select-keys thread [:id :file-id :page-id])))
(defn- create-comment-thread
[conn params]
(loop [sp (db/savepoint conn)
rc 0]
(let [res (ex/try (create-comment-thread* conn params))]
(cond
(and (instance? Throwable res)
(< rc 3))
(do
(db/rollback! conn sp)
(recur (db/savepoint conn)
(inc rc)))
(instance? Throwable res)
(throw res)
:else res))))
(defn- retrieve-page-name
[conn {:keys [file-id page-id]}]
(let [{:keys [data]} (db/get-by-id conn :file file-id)

View File

@@ -13,7 +13,6 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile]
[app.setup.initial-data :as sid]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
@@ -34,7 +33,7 @@
params {:id id
:email email
:fullname fullname
:is-demo true
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
:props {:onboarding-viewed true}}]
@@ -46,8 +45,7 @@
(db/with-atomic [conn pool]
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn)
(sid/load-initial-project! conn))
(#'profile/create-profile-relations conn))
(with-meta {:email email
:password password}

View File

@@ -9,10 +9,12 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -37,6 +39,7 @@
::font-id ::font-family ::font-weight ::font-style]))
(sv/defmethod ::create-font-variant
{::rlimit/permits (cf/get :rlimit-font)}
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
@@ -45,10 +48,9 @@
(defn create-font-variant
[{:keys [conn storage] :as cfg} {:keys [data] :as params}]
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
(let [data (media/run {:cmd :generate-fonts :input data})
storage (media/configure-assets-storage storage conn)
otf (when-let [fdata (get data "font/otf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/otf"}))

View File

@@ -10,11 +10,13 @@
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.util.http :as http]
[app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -47,6 +49,7 @@
:opt-un [::id]))
(sv/defmethod ::upload-file-media-object
{::rlimit/permits (cf/get :rlimit-image)}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file conn file-id)]
@@ -89,21 +92,20 @@
:content-type mtype
:expired-at (dt/in-future {:minutes 30})}))))
(defn create-file-media-object
[{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}]
(media/validate-media-type (:content-type content))
(let [storage (media/configure-assets-storage storage conn)
source-path (fs/path (:tempfile content))
source-mtype (:content-type content)
source-info (media/run cfg {:cmd :info :input {:path source-path :mtype source-mtype}})
source-info (media/run {:cmd :info :input {:path source-path :mtype source-mtype}})
thumb (when (and (not (svg-image? source-info))
(big-enough-for-thumbnail? source-info))
(media/run cfg (assoc thumbnail-options
:cmd :generic-thumbnail
:input {:mtype (:mtype source-info)
:path source-path})))
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input {:mtype (:mtype source-info)
:path source-path})))
image (if (= (:mtype source-info) "image/svg+xml")
(let [data (slurp source-path)]

View File

@@ -16,11 +16,10 @@
[app.loggers.audit :as audit]
[app.media :as media]
[app.metrics :as mtx]
[app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.setup.initial-data :as sid]
[app.storage :as sto]
[app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
@@ -126,13 +125,12 @@
;; --- MUTATION: Register Profile
(s/def ::accept-terms-and-privacy ::us/boolean)
(s/def ::token ::us/not-empty-string)
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile {:auth false :rlimit :password}
(sv/defmethod ::register-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
@@ -148,16 +146,17 @@
(defn register-profile
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [profile (->> params
(create-profile conn)
(create-profile-relations conn)
(decode-profile-row))]
(sid/load-initial-project! conn profile)
(let [is-active (or (:is-active params)
(contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(decode-profile-row))]
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
@@ -187,6 +186,15 @@
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
:before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(let [vtoken (tokens :generate
@@ -231,7 +239,7 @@
backend (:backend params "penpot")
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params (or (not= "penpot" backend) is-demo))
is-active (:is-active params false)
email (str/lower (:email params))
params {:id id
@@ -256,28 +264,15 @@
:code :email-already-exists
:cause e)))))))
(defn create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:is-default true})
project (projects/create-project conn {:profile-id (:id profile)
:team-id (:id team)
:name "Drafts"
:is-default true})
params {:team-id (:id team)
:profile-id (:id profile)
:project-id (:id project)
:role :owner}]
(teams/create-team-role conn params)
(projects/create-project-role conn params)
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
(assoc :default-project-id (:id project)))))
(assoc :default-project-id (:default-project-id team)))))
;; --- MUTATION: Login
@@ -288,7 +283,8 @@
(s/keys :req-un [::email ::password]
:opt-un [::scope ::invitation-token]))
(sv/defmethod ::login {:auth false :rlimit :password}
(sv/defmethod ::login
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
@@ -378,7 +374,8 @@
(s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password]))
(sv/defmethod ::update-profile-password {:rlimit :password}
(sv/defmethod ::update-profile-password
{::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} {:keys [password] :as params}]
(db/with-atomic [conn pool]
(let [profile (validate-password! conn params)]
@@ -411,11 +408,12 @@
(s/keys :req-un [::profile-id ::file]))
(sv/defmethod ::update-profile-photo
{::rlimit/permits (cf/get :rlimit-image)}
[{:keys [pool storage] :as cfg} {:keys [profile-id file] :as params}]
(db/with-atomic [conn pool]
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
(media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
(media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
(let [profile (db/get-by-id conn :profile profile-id)
storage (media/configure-assets-storage storage conn)
@@ -561,7 +559,8 @@
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile {:auth false :rlimit :password}
(sv/defmethod ::recover-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})]

View File

@@ -10,6 +10,7 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.media :as media]
@@ -18,6 +19,7 @@
[app.rpc.queries.profile :as profile]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -32,6 +34,7 @@
;; --- Mutation: Create Team
(declare create-team)
(declare create-team-entry)
(declare create-team-role)
(declare create-team-default-project)
@@ -42,15 +45,21 @@
(sv/defmethod ::create-team
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
params (assoc params
:team-id (:id team)
:role :owner)]
(create-team-role conn params)
(create-team-default-project conn params)
team)))
(create-team conn params)))
(defn create-team
"This is a complete team creation process, it creates the team
object and all related objects (default role and default project)."
[conn params]
(let [team (create-team-entry conn params)
params (assoc params
:team-id (:id team)
:role :owner)
project (create-team-default-project conn params)]
(create-team-role conn params)
(assoc team :default-project-id (:id project))))
(defn- create-team-entry
[conn {:keys [id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
@@ -59,23 +68,24 @@
:name name
:is-default is-default})))
(defn create-team-role
(defn- create-team-role
[conn {:keys [team-id profile-id role] :as params}]
(let [params {:team-id team-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :team-profile-rel))))
(defn create-team-default-project
(defn- create-team-default-project
[conn {:keys [team-id profile-id] :as params}]
(let [project {:id (uuid/next)
:team-id team-id
:name "Drafts"
:is-default true}]
(projects/create-project conn project)
:is-default true}
project (projects/create-project conn project)]
(projects/create-project-role conn {:project-id (:id project)
:profile-id profile-id
:role :owner})))
:role :owner})
project))
;; --- Mutation: Update Team
@@ -100,10 +110,10 @@
(sv/defmethod ::leave-team
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id id)
(let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)]
(when (some :is-owner perms)
(when (:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "reasing owner before leave"))
@@ -161,8 +171,7 @@
(sv/defmethod ::update-team-member-role
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)
(let [perms (teams/get-permissions conn profile-id team-id)
;; We retrieve all team members instead of query the
;; database for a single member. This is just for
;; convenience, if this bocomes a bottleneck or problematic,
@@ -170,8 +179,8 @@
members (teams/retrieve-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)
is-owner? (some :is-owner perms)
is-admin? (some :is-admin perms)]
is-owner? (:is-owner perms)
is-admin? (:is-admin perms)]
;; If no member is found, just 404
(when-not member
@@ -224,9 +233,9 @@
(sv/defmethod ::delete-team-member
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)]
(when-not (or (some :is-owner perms)
(some :is-admin perms))
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
@@ -251,12 +260,13 @@
(s/keys :req-un [::profile-id ::team-id ::file]))
(sv/defmethod ::update-team-photo
{::rlimit/permits (cf/get :rlimit-image)}
[{:keys [pool storage] :as cfg} {:keys [profile-id file team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
(media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
(media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
(let [team (teams/retrieve-team conn profile-id team-id)
storage (media/configure-assets-storage storage conn)
@@ -276,16 +286,13 @@
(defn upload-photo
[{:keys [storage] :as cfg} {:keys [file]}]
(let [thumb (media/run cfg
{:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input {:path (fs/path (:tempfile file))
:mtype (:content-type file)}})]
(let [thumb (media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input {:path (fs/path (:tempfile file))
:mtype (:content-type file)}})]
(sto/put-object storage
{:content (sto/content (:data thumb) (:size thumb))
:content-type (:mtype thumb)})))
@@ -293,28 +300,18 @@
;; --- Mutation: Invite Member
(declare create-team-invitation)
(s/def ::email ::us/email)
(s/def ::invite-team-member
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sv/defmethod ::invite-team-member
[{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
member (profile/retrieve-profile-data-by-email conn email)
team (db/get-by-id conn :team team-id)
itoken (tokens :generate
{:iss :team-invitation
:exp (dt/in-future "48h")
:profile-id (:id profile)
:role role
:team-id team-id
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
team (db/get-by-id conn :team team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
@@ -326,24 +323,71 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
;; Secondly check if the invited member email is part of the
;; global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (:public-uri cfg)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})
(create-team-invitation
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role))
nil)))
(defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email)
itoken (tokens :generate
{:iss :team-invitation
:exp (dt/in-future "48h")
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
;; Secondly check if the invited member email is part of the
;; global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (:public-uri cfg)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})))
;; --- Mutation: Create Team & Invite Members
(s/def ::emails ::us/set-of-emails)
(s/def ::create-team-and-invite-members
(s/and ::create-team (s/keys :req-un [::emails ::role])))
(sv/defmethod ::create-team-and-invite-members
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(doseq [email emails]
(create-team-invitation
(assoc cfg
:conn conn
:team team
:profile profile
:email email
:role role)))
team)))

View File

@@ -34,10 +34,15 @@
(when (profile/retrieve-profile-data-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
claims)
(with-meta claims
{::audit/name "update-profile-email"
::audit/props {:email email}
::audit/profile-id profile-id}))
(defn- annotate-profile-activation
"A helper for properly increase the profile-activation metric once the

View File

@@ -87,13 +87,9 @@
(defn retrieve-profile
[conn id]
(let [profile (some->> (retrieve-profile-data conn id)
(strip-private-attrs)
(populate-additional-data conn))]
(when (nil? profile)
(ex/raise :type :not-found
:hint "Object doest not exists."))
(let [profile (->> (retrieve-profile-data conn id)
(strip-private-attrs)
(populate-additional-data conn))]
(update profile :props filter-profile-props)))
(def ^:private sql:profile-by-email

View File

@@ -79,12 +79,14 @@
where f.project_id = p.id
and deleted_at is null) as count
from project as p
inner join team as t on (t.id = p.team_id)
left join team_project_profile_rel as tpp
on (tpp.project_id = p.id and
tpp.team_id = p.team_id and
tpp.profile_id = ?)
where p.team_id = ?
and p.deleted_at is null
and t.deleted_at is null
order by p.modified_at desc")
(defn retrieve-projects
@@ -108,26 +110,26 @@
(def sql:all-projects
"select p1.*, t.name as team_name, t.is_default as is_default_team
from project as p1
inner join team as t
on t.id = p1.team_id
inner join team as t on (t.id = p1.team_id)
where t.id in (select team_id
from team_profile_rel as tpr
where tpr.profile_id = ?
and (tpr.can_edit = true or
tpr.is_owner = true or
tpr.is_admin = true))
and t.deleted_at is null
and p1.deleted_at is null
union
select p2.*, t.name as team_name, t.is_default as is_default_team
from project as p2
inner join team as t
on t.id = p2.team_id
inner join team as t on (t.id = p2.team_id)
where p2.id in (select project_id
from project_profile_rel as ppr
where ppr.profile_id = ?
and (ppr.can_edit = true or
ppr.is_owner = true or
ppr.is_admin = true))
and t.deleted_at is null
and p2.deleted_at is null
order by team_name, name;")

View File

@@ -0,0 +1,43 @@
;; 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) UXBOX Labs SL
(ns app.util.retry
"A fault tolerance helpers. Allow retry some operations that we know
we can retry."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.util.async :as aa]
[app.util.services :as sv]))
(defn conflict-db-insert?
"Check if exception matches a insertion conflict on postgresql."
[e]
(and (instance? org.postgresql.util.PSQLException e)
(= "23505" (.getSQLState e))))
(defn wrap-retry
[_ f {:keys [::max-retries ::matches ::sv/name]
:or {max-retries 3
matches (constantly false)}
:as mdata}]
(when (::enabled mdata)
(l/debug :hint "wrapping retry" :name name))
(if (::enabled mdata)
(fn [cfg params]
(loop [retry 1]
(when (> retry 1)
(l/debug :hint "retrying controlled function" :retry retry :name name))
(let [res (ex/try (f cfg params))]
(if (ex/exception? res)
(if (and (matches res) (< retry max-retries))
(do
(aa/thread-sleep (* 100 retry))
(recur (inc retry)))
(throw res))
res))))
f))

View File

@@ -0,0 +1,36 @@
;; 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) UXBOX Labs SL
(ns app.util.rlimit
"Resource usage limits (in other words: semaphores)."
(:require
[app.common.logging :as l]
[app.util.services :as sv])
(:import
java.util.concurrent.Semaphore))
(defn acquire!
[sem]
(.acquire ^Semaphore sem))
(defn release!
[sem]
(.release ^Semaphore sem))
(defn wrap-rlimit
[_cfg f mdata]
(if-let [permits (::permits mdata)]
(let [sem (Semaphore. permits)]
(l/debug :hint "wrapping rlimit" :handler (::sv/name mdata) :permits permits)
(fn [cfg params]
(try
(acquire! sem)
(f cfg params)
(finally
(release! sem)))))
f))

View File

@@ -43,7 +43,7 @@
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= 2 (count result)))
(t/is project-id (get-in result [0 :id]))
(t/is (= "test project" (get-in result [0 :name])))))
@@ -55,15 +55,15 @@
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= 3 (count result)))
(t/is (not= project-id (get-in result [0 :id])))
(t/is (= "Drafts" (get-in result [0 :name])))
(t/is (= "Default" (get-in result [0 :team-name])))
(t/is (= true (get-in result [0 :is-default-team])))
(t/is project-id (get-in result [1 :id]))
(t/is (= "test project" (get-in result [1 :name])))
(t/is (= "team1" (get-in result [1 :team-name])))
(t/is (= false (get-in result [1 :is-default-team])))))
(t/is project-id (get-in result [2 :id]))
(t/is (= "test project" (get-in result [2 :name])))
(t/is (= "team1" (get-in result [2 :team-name])))
(t/is (= false (get-in result [2 :is-default-team])))))
;; rename project
(let [data {::th/type :rename-project
@@ -95,7 +95,7 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
;; query a list of projects after delete"
;; query a list of projects after delete
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile)}
@@ -103,7 +103,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
(t/is (= 1 (count result)))))
))
(t/deftest permissions-checks-create-project

View File

@@ -130,7 +130,7 @@
(let [result (task {:max-age (dt/duration {:minutes 1})})]
(t/is (nil? result)))
;; query the list of projects of a after hard deletion
;; query the list of projects after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}

View File

@@ -126,7 +126,8 @@
:password "123123"
:is-demo false}
params)]
(->> (#'profile/create-profile conn params)
(->> params
(#'profile/create-profile conn)
(#'profile/create-profile-relations conn)))))
(defn create-project*
@@ -159,15 +160,10 @@
([i params] (create-team* *pool* i params))
([conn i {:keys [profile-id] :as params}]
(us/assert uuid? profile-id)
(let [id (mk-uuid "team" i)
team (#'teams/create-team conn {:id id
:profile-id profile-id
:name (str "team" i)})]
(#'teams/create-team-role conn
{:team-id id
:profile-id profile-id
:role :owner})
team)))
(let [id (mk-uuid "team" i)]
(teams/create-team conn {:id id
:profile-id profile-id
:name (str "team" i)}))))
(defn create-file-media-object*
([params] (create-file-media-object* *pool* params))
@@ -350,3 +346,11 @@
(defn reset-mock!
[m]
(reset! m @(mk/make-mock {})))
(defn pause
[]
(let [^java.io.Console cnsl (System/console)]
(println "[waiting RETURN]")
(.readLine cnsl)
nil))

View File

@@ -392,7 +392,8 @@
(defmethod read-action-opts :navigate
[interaction-src]
(select-keys interaction-src [:destination]))
(select-keys interaction-src [:destination
:preserve-scroll]))
(defmethod read-action-opts :open-overlay
[interaction-src]
@@ -430,7 +431,8 @@
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
{:keys [delay]} (read-event-opts interaction-src)
{:keys [destination overlay-pos-type overlay-position url
close-click-outside background-overlay]} (read-action-opts interaction-src)
close-click-outside background-overlay preserve-scroll]}
(read-action-opts interaction-src)
interactions (-> (lookup-shape file from-id)
:interactions
@@ -443,7 +445,8 @@
:overlay-position overlay-position
:url url
:close-click-outside close-click-outside
:background-overlay background-overlay})))]
:background-overlay background-overlay
:preserve-scroll preserve-scroll})))]
(commit-change
file
{:type :mod-obj

View File

@@ -10,30 +10,28 @@
[cuerdas.core :as str]))
(def default
#{:backend-asserts
:api-doc
:registration
:demo-users})
"A common flags that affects both: backend and frontend."
[:enable-registration
:enable-demo-users])
(defn parse
([flags] (parse flags #{}))
([flags default]
(loop [flags (seq flags)
result default]
(let [item (first flags)]
(if (nil? item)
result
(let [sname (name item)]
(cond
(str/starts-with? sname "enable-")
(recur (rest flags)
(conj result (keyword (subs sname 7))))
[& flags]
(loop [flags (apply concat flags)
result #{}]
(let [item (first flags)]
(if (nil? item)
result
(let [sname (name item)]
(cond
(str/starts-with? sname "enable-")
(recur (rest flags)
(conj result (keyword (subs sname 7))))
(str/starts-with? sname "disable-")
(recur (rest flags)
(disj result (keyword (subs sname 8))))
(str/starts-with? sname "disable-")
(recur (rest flags)
(disj result (keyword (subs sname 8))))
:else
(recur (rest flags) result))))))))
:else
(recur (rest flags) result)))))))

View File

@@ -6,6 +6,7 @@
(ns app.common.geom.shapes.intersect
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gpp]
@@ -172,22 +173,23 @@
"Checks if the given rect overlaps with the path in any point"
[shape rect]
(let [;; If paths are too complex the intersection is too expensive
;; we fallback to check its bounding box otherwise the performance penalty
;; is too big
;; TODO: Look for ways to optimize this operation
simple? (> (count (:content shape)) 100)
(when (d/not-empty? (:content shape))
(let [ ;; If paths are too complex the intersection is too expensive
;; we fallback to check its bounding box otherwise the performance penalty
;; is too big
;; TODO: Look for ways to optimize this operation
simple? (> (count (:content shape)) 100)
rect-points (gpr/rect->points rect)
rect-lines (points->lines rect-points)
path-lines (if simple?
(points->lines (:points shape))
(gpp/path->lines shape))
start-point (-> shape :content (first) :params (gpt/point))]
rect-points (gpr/rect->points rect)
rect-lines (points->lines rect-points)
path-lines (if simple?
(points->lines (:points shape))
(gpp/path->lines shape))
start-point (-> shape :content (first) :params (gpt/point))]
(or (is-point-inside-nonzero? (first rect-points) path-lines)
(is-point-inside-nonzero? start-point rect-lines)
(intersects-lines? rect-lines path-lines))))
(or (is-point-inside-nonzero? (first rect-points) path-lines)
(is-point-inside-nonzero? start-point rect-lines)
(intersects-lines? rect-lines path-lines)))))
(defn is-point-inside-ellipse?
"checks if a point is inside an ellipse"

View File

@@ -11,6 +11,7 @@
[app.common.spec :as us]
[app.common.types.interactions :as cti]
[app.common.types.page-options :as cto]
[app.common.types.radius :as ctr]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
@@ -191,12 +192,6 @@
(s/def :internal.shape/page-id uuid?)
(s/def :internal.shape/proportion ::us/safe-number)
(s/def :internal.shape/proportion-lock boolean?)
(s/def :internal.shape/rx ::us/safe-number)
(s/def :internal.shape/ry ::us/safe-number)
(s/def :internal.shape/r1 ::us/safe-number)
(s/def :internal.shape/r2 ::us/safe-number)
(s/def :internal.shape/r3 ::us/safe-number)
(s/def :internal.shape/r4 ::us/safe-number)
(s/def :internal.shape/stroke-color string?)
(s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient))
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
@@ -285,12 +280,12 @@
:internal.shape/constraints-h
:internal.shape/constraints-v
:internal.shape/fixed-scroll
:internal.shape/rx
:internal.shape/ry
:internal.shape/r1
:internal.shape/r2
:internal.shape/r3
:internal.shape/r4
::ctr/rx
::ctr/ry
::ctr/r1
::ctr/r2
::ctr/r3
::ctr/r4
:internal.shape/x
:internal.shape/y
:internal.shape/exports

View File

@@ -42,7 +42,8 @@
(let [head-p (gsp/command->point head)
head (cond
(and (= :close-path (:command head))
(< (gpt/distance last-p last-move) 0.01))
(or (nil? last-p) ;; Ignore consecutive close-paths
(< (gpt/distance last-p last-move) 0.01)))
nil
(= :close-path (:command head))

View File

@@ -177,18 +177,11 @@
(map #(get objects %))
(map #(convert-to-path % objects)))
bool-type (:bool-type shape)
head (if (= bool-type :difference) (first children) (last children))
head (cond-> head
(and (contains? head :svg-attrs) (nil? (:fill-color head)))
(assoc :fill-color "#000000"))
head-data (select-keys head style-properties)
content (pb/content-bool (:bool-type shape) (mapv :content children))]
content (pb/content-bool bool-type (mapv :content children))]
(-> shape
(assoc :type :path)
(assoc :content content)
(merge head-data)
(d/without-keys dissoc-attrs))))
(defn convert-to-path

View File

@@ -111,16 +111,6 @@
(s/def ::point gpt/point?)
(s/def ::id ::uuid)
(s/def ::words
(s/conformer
(fn [s]
(cond
(set? s) s
(string? s) (into #{} (map keyword) (str/words s))
:else ::s/invalid))
(fn [s]
(str/join " " (map name s)))))
(defn bytes?
"Test if a first parameter is a byte
array or not."
@@ -134,7 +124,6 @@
(s/def ::bytes bytes?)
(s/def ::safe-integer
#(and
(int? %)
@@ -149,8 +138,28 @@
(<= % max-safe-int)))
;; --- SPEC: set of Keywords
(s/def ::set-of-keywords
(s/conformer
(fn [s]
(let [xform (comp
(map (fn [s]
(cond
(string? s) (keyword s)
(keyword? s) s
:else nil)))
(filter identity))]
(cond
(set? s) (into #{} xform s)
(string? s) (into #{} xform (str/words s))
:else ::s/invalid)))
(fn [s]
(str/join " " (map name s)))))
;; --- SPEC: email
(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
(s/def ::email
(s/conformer
@@ -162,6 +171,23 @@
::s/invalid))
str))
(s/def ::set-of-emails
(s/conformer
(fn [v]
(cond
(string? v)
(into #{} (re-seq email-re v))
(or (set? v) (sequential? v))
(->> (str/join " " v)
(re-seq email-re)
(into #{}))
:else ::s/invalid))
(fn [v]
(str/join " " v))))
;; --- SPEC: set-of-str
(s/def ::set-of-str

View File

@@ -64,11 +64,12 @@
(s/def ::url ::us/string)
(s/def ::close-click-outside ::us/boolean)
(s/def ::background-overlay ::us/boolean)
(s/def ::preserve-scroll ::us/boolean)
(defmulti action-opts-spec :action-type)
(defmethod action-opts-spec :navigate [_]
(s/keys :req-un [::destination]))
(s/keys :opt-un [::destination ::preserve-scroll]))
(defmethod action-opts-spec :open-overlay [_]
(s/keys :req-un [::destination
@@ -85,7 +86,7 @@
::background-overlay]))
(defmethod action-opts-spec :close-overlay [_]
(s/keys :req-un [::destination]))
(s/keys :opt-un [::destination]))
(defmethod action-opts-spec :prev-screen [_]
(s/keys :req-un []))
@@ -151,7 +152,8 @@
:navigate
(assoc interaction
:action-type action-type
:destination (get interaction :destination))
:destination (get interaction :destination)
:preserve-scroll false)
(:open-overlay :toggle-overlay)
(let [overlay-pos-type (get interaction :overlay-pos-type :center)
@@ -196,6 +198,10 @@
(and (has-destination interaction)
(some? (:destination interaction))))
(defn has-preserve-scroll
[interaction]
(= (:action-type interaction) :navigate))
(defn set-destination
[interaction destination]
(us/verify ::interaction interaction)
@@ -210,6 +216,13 @@
(assoc :overlay-pos-type :center
:overlay-position (gpt/point 0 0))))
(defn set-preserve-scroll
[interaction preserve-scroll]
(us/verify ::interaction interaction)
(us/verify ::us/boolean preserve-scroll)
(assert (has-preserve-scroll interaction))
(assoc interaction :preserve-scroll preserve-scroll))
(defn has-url
[interaction]
(= (:action-type interaction) :open-url))

View File

@@ -0,0 +1,90 @@
;; 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) UXBOX Labs SL
(ns app.common.types.radius
(:require
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
(s/def ::rx ::us/safe-number)
(s/def ::ry ::us/safe-number)
(s/def ::r1 ::us/safe-number)
(s/def ::r2 ::us/safe-number)
(s/def ::r3 ::us/safe-number)
(s/def ::r4 ::us/safe-number)
;; Rectangle shapes may define the radius of the corners in two modes:
;; - radius-1 all corners have the same radius (although we store two
;; values :rx and :ry because svg uses it this way).
;; - radius-4 each corner (top-left, top-right, bottom-right, bottom-left)
;; has an independent value. SVG does not allow this directly, so we
;; emulate it with paths.
;; A shape never will have both :rx and :r1 simultaneously
;; All operations take into account that the shape may not be a rectangle, and so
;; it hasn't :rx nor :r1. In this case operations must leave shape untouched.
(defn radius-mode
[shape]
(cond (:rx shape) :radius-1
(:r1 shape) :radius-4
:else nil))
(defn radius-1?
[shape]
(and (:rx shape) (not= (:rx shape) 0)))
(defn radius-4?
[shape]
(and (:r1 shape)
(or (not= (:r1 shape) 0)
(not= (:r2 shape) 0)
(not= (:r3 shape) 0)
(not= (:r4 shape) 0))))
(defn all-equal?
[shape]
(= (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape)))
(defn switch-to-radius-1
[shape]
(let [r (if (all-equal? shape) (:r1 shape) 0)]
(cond-> shape
(:r1 shape)
(-> (assoc :rx r :ry r)
(dissoc :r1 :r2 :r3 :r4)))))
(defn switch-to-radius-4
[shape]
(cond-> shape
(:rx shape)
(-> (assoc :r1 (:rx shape)
:r2 (:rx shape)
:r3 (:rx shape)
:r4 (:rx shape))
(dissoc :rx :ry))))
(defn set-radius-1
[shape value]
(cond-> shape
(:r1 shape)
(-> (dissoc :r1 :r2 :r3 :r4)
(assoc :rx 0 :ry 0))
(:rx shape)
(assoc :rx value :ry value)))
(defn set-radius-4
[shape attr value]
(cond-> shape
(:rx shape)
(-> (dissoc :rx :rx)
(assoc :r1 0 :r2 0 :r3 0 :r4 0))
(attr shape)
(assoc attr value)))

View File

@@ -5,7 +5,7 @@ ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v14.17.6 \
CLOJURE_VERSION=1.10.3.967 \
CLJKONDO_VERSION=2021.09.15 \
CLJKONDO_VERSION=2021.10.19 \
BABASHKA_VERSION=0.6.1 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8

View File

@@ -50,7 +50,7 @@ services:
- PENPOT_SMTP_PASSWORD=
- PENPOT_SMTP_SSL=false
- PENPOT_SMTP_TLS=false
- PENPOT_FLAGS="enable-cors"
- PENPOT_FLAGS="enable-cors enable-insecure-register enable-terms-and-privacy-checkbox"
# LDAP setup
- PENPOT_LDAP_HOST=ldap

View File

@@ -89,6 +89,16 @@ http {
error_page 301 302 307 = @handle_redirect;
}
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;
proxy_buffering off;
}
location /internal/assets {
internal;
alias /home/penpot/penpot/backend/assets;

View File

@@ -104,7 +104,7 @@
(def browser-pool-factory
(letfn [(create []
(let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]})
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox" "--font-render-hinting=none"]})
(p/then (fn [browser]
(let [id (deref pool-browser-id)]
(log/info :origin "factory" :action "create" :browser-id id)

View File

@@ -23,7 +23,7 @@
[app.renderer.bitmap :refer [create-cookie]]
[promesa.core :as p]))
(log/set-level "app.http.export-svg" :trace)
(log/set-level "app.renderer.svg" :trace)
(defn- xml->clj
[data]
@@ -129,7 +129,7 @@
svgpath (path/join basepath (str basename ".svg"))]
(-> (sh/run-cmd! (str "potrace --flat -b svg " pbmpath " -o " svgpath))
(p/then (constantly svgpath)))))
(generate-color-layer [ppmpath color]
(log/trace :fn :generate-color-layer :ppmpath ppmpath :color color)
(let [basepath (path/dirname ppmpath)
@@ -146,15 +146,61 @@
{:color color
:svgdata data}))))))
(join-color-layers [{:keys [x y width height] :as node} layers]
(log/trace :fn :join-color-layers)
(set-path-color [id color mapping node]
(let [color-mapping (get mapping color)]
(cond
(and (some? color-mapping)
(= "transparent" (get color-mapping "type")))
(update node "attributes" assoc
"fill" (get color-mapping "hex")
"fill-opacity" (get color-mapping "opacity"))
(and (some? color-mapping)
(= "gradient" (get color-mapping "type")))
(update node "attributes" assoc
"fill" (str "url(#gradient-" id "-" (subs color 1) ")"))
:else
(update node "attributes" assoc "fill" color))))
(get-stops [data]
(->> (get-in data ["gradient" "stops"])
(mapv (fn [stop-data]
{"type" "element"
"name" "stop"
"attributes" {"offset" (get stop-data "offset")
"stop-color" (get stop-data "color")
"stop-opacity" (get stop-data "opacity")}}))))
(data->gradient-def [id [color data]]
(let [id (str "gradient-" id "-" (subs color 1))]
(if (= type "linear")
{"type" "element"
"name" "linearGradient"
"attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"}
"elements" (get-stops data)}
{"type" "element"
"name" "radialGradient"
"attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"}
"elements" (get-stops data)}
)))
(get-gradients [id mapping]
(->> mapping
(filter (fn [[color data]]
(= (get data "type") "gradient")))
(mapv (partial data->gradient-def id))))
(join-color-layers [{:keys [id x y width height mapping] :as node} layers]
(log/trace :fn :join-color-layers :mapping mapping)
(loop [result (-> (:svgdata (first layers))
(assoc "elements" []))
layers (seq layers)]
(if-let [{:keys [color svgdata]} (first layers)]
(recur (->> (get svgdata "elements")
(filter #(= (get % "name") "g"))
(map #(update % "attributes" assoc "fill" color))
(map (partial set-path-color id color mapping))
(update result "elements" d/concat))
(rest layers))
@@ -166,22 +212,33 @@
(parse-viewbox))
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
(/ width (:width vbox))
(/ height (:height vbox)))]
(/ height (:height vbox)))
gradient-defs (get-gradients id mapping)
elements
(->> (get result "elements")
(mapv (fn [group]
(let [paths (get group "elements")]
(if (= 1 (count paths))
(let [path (first paths)]
(update path "attributes"
(fn [attrs]
(-> attrs
(d/merge (get group "attributes"))
(update "transform" #(str transform " " %))))))
(update-in group ["attributes" "transform"] #(str transform " " %)))))))
elements (cond->> elements
(not (empty? gradient-defs))
(d/concat [{"type" "element" "name" "defs" "attributes" {}
"elements" gradient-defs}]))]
(-> result
(assoc "name" "g")
(assoc "attributes" {})
(update "elements" (fn [elements]
(mapv (fn [group]
(let [paths (get group "elements")]
(if (= 1 (count paths))
(let [path (first paths)]
(update path "attributes"
(fn [attrs]
(-> attrs
(d/merge (get group "attributes"))
(update "transform" #(str transform " " %))))))
(update-in group ["attributes" "transform"] #(str transform " " %)))))
elements))))))))
(assoc "elements" elements))))))
(convert-to-svg [ppmpath {:keys [colors] :as node}]
(log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors)
@@ -201,25 +258,28 @@
:svgdata svgdata))))
(extract-element-attrs [^js element]
(let [^js attrs (.. element -attributes)
^js colors (.. element -dataset -colors)]
#js {:id (.. attrs -id -value)
:x (.. attrs -x -value)
:y (.. attrs -y -value)
:width (.. attrs -width -value)
:height (.. attrs -height -value)
:colors (.split colors ",")}))
(let [^js attrs (.. element -attributes)
^js colors (.. element -dataset -colors)
^js mapping (.. element -dataset -mapping)]
#js {:id (.. attrs -id -value)
:x (.. attrs -x -value)
:y (.. attrs -y -value)
:width (.. attrs -width -value)
:height (.. attrs -height -value)
:colors (.split colors ",")
:mapping (js/JSON.parse mapping)}))
(extract-single-node [[shot node]]
(log/trace :fn :extract-single-node)
(p/let [attrs (bw/eval! node extract-element-attrs)]
{:id (unchecked-get attrs "id")
:x (unchecked-get attrs "x")
:y (unchecked-get attrs "y")
:width (unchecked-get attrs "width")
:height (unchecked-get attrs "height")
:colors (vec (unchecked-get attrs "colors"))
{:id (unchecked-get attrs "id")
:x (unchecked-get attrs "x")
:y (unchecked-get attrs "y")
:width (unchecked-get attrs "width")
:height (unchecked-get attrs "height")
:colors (vec (unchecked-get attrs "colors"))
:mapping (js->clj (unchecked-get attrs "mapping"))
:data shot}))
(resolve-text-node [page node]
@@ -313,3 +373,4 @@
".svg"))
:length (alength content)
:mime-type "image/svg+xml"}))

View File

@@ -23,7 +23,7 @@
:dev
{:extra-deps
{thheller/shadow-cljs {:mvn/version "2.15.9"}
{thheller/shadow-cljs {:mvn/version "2.15.12"}
cider/cider-nrepl {:mvn/version "0.26.0"}}}
:shadow-cljs

View File

@@ -25,6 +25,17 @@ paths.resources = "./resources/";
paths.output = "./resources/public/";
paths.dist = "./target/dist/";
/***********************************************
* Marked Extensions
***********************************************/
const renderer = {
link(href, title, text) {
return `<a href="${href}" target="_blank">${text}</a>`;
}
};
marked.use({renderer});
/***********************************************
* Helpers

View File

@@ -23,7 +23,7 @@
"start": "npm-run-all --parallel watch-gulp watch-main"
},
"devDependencies": {
"autoprefixer": "^10.2.4",
"autoprefixer": "^10.3.7",
"gettext-parser": "^4.0.4",
"gulp": "4.0.2",
"gulp-concat": "^2.6.1",
@@ -35,36 +35,36 @@
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^1.5.0",
"map-stream": "0.0.7",
"marked": "^3.0.4",
"marked": "^3.0.8",
"mkdirp": "^1.0.4",
"nodemon": "^2.0.13",
"nodemon": "^2.0.14",
"npm-run-all": "^4.1.5",
"postcss": "^8.3.5",
"postcss": "^8.3.11",
"postcss-clean": "^1.2.2",
"rimraf": "^3.0.0",
"sass": "^1.35.1",
"shadow-cljs": "2.15.9"
"sass": "^1.43.4",
"shadow-cljs": "2.15.12"
},
"dependencies": {
"@sentry/browser": "^6.12.0",
"@sentry/tracing": "^6.12.0",
"date-fns": "^2.22.1",
"@sentry/browser": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"date-fns": "^2.25.0",
"draft-js": "^0.11.7",
"highlight.js": "^11.0.1",
"highlight.js": "^11.3.1",
"js-beautify": "^1.14.0",
"jszip": "^3.6.0",
"luxon": "^2.0.2",
"mousetrap": "^1.6.5",
"opentype.js": "^1.3.3",
"opentype.js": "^1.3.4",
"randomcolor": "^0.6.2",
"react": "~17.0.2",
"react-dom": "~17.0.2",
"react-virtualized": "^9.22.3",
"rxjs": "~7.2.0",
"rxjs": "~7.4.0",
"sax": "^1.2.4",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
"ua-parser-js": "^0.7.28",
"ua-parser-js": "^1.0.2",
"xregexp": "^5.0.1"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154394" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697997087 -10.666460420698968 24.47353642154394 45.833972589785844" height="45.833972589785844" style="-webkit-print-color-adjust: exact;"><g id="shape-68c61310-2040-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-68c61311-2040-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324019C32.728545198285246,-6.330271155262835,31.92939323677365,-3.376021339875024,31.573755956590503,-2.4511143742279273C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500309,31.43334627304739,-10.431571776022338C29.909377991244583,-10.594250374600506,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908017C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227184,25.941143250794084,-7.1652527066157745C24.72968451746874,-7.572893837298125,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884185583,10.659070475242515,0.4358613513782075,10.659070475242515,0.4358613513782075L10.492289692115264,34.903869918353394C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655593702,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324019ZZ" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g><g id="shape-68c61312-2040-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.69085378511636,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987821,26.762725789724755,14.45390600122937,27.19052918719808,15.656068507365035C27.618298301069444,16.85823063295993,26.994616697437777,18.18217399373725,25.79747312881318,18.61320913307327C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g><g id="shape-68c61313-2040-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.332660542501344C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.24527363442894,19.819776888222805,23.267580687635473,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154485" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697995268 -10.66646042069624 24.47353642154485 45.833972589782206" height="45.833972589782206" style="-webkit-print-color-adjust: exact;"><g id="shape-b0db2ad1-203d-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-b0db2ad2-203d-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324474C32.728545198285246,-6.3302711552623805,31.92939323677365,-3.376021339874569,31.573755956590503,-2.4511143742274726C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500764,31.43334627304739,-10.431571776021883C29.909377991244583,-10.59425037460096,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908472C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227639,25.941143250794084,-7.165252706616229C24.72968451746874,-7.57289383729767,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884181035,10.659070475242515,0.4358613513777527,10.659070475242515,0.4358613513777527L10.492289692115264,34.90386991835294C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655594157,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324474ZZ" style="fill: rgb(250, 181, 245);"/></g><g id="shape-b0db51e0-203d-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.690853785115905,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987367,26.762725789724755,14.45390600122937,27.19052918719808,15.65606850736549C27.618298301069444,16.85823063295993,26.994616697437777,18.182173993736797,25.79747312881318,18.613209133072814C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: white;"/></g><g id="shape-b0db51e1-203d-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.33266054250089C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.245273634429395,19.819776888222805,23.267580687635927,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: white;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="206.95680443620125" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-1.9573043775739962 -2.7988168413976156 206.95680443620125 158.15679986166066" height="158.15679986166066" style="-webkit-print-color-adjust: exact;"><g id="shape-575c9093-25c2-11ec-9877-05429cda5971"><g id="shape-575c9094-25c2-11ec-9877-05429cda5971"><defs><mask id="outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" x="-1.9573043775739962" y="-2.7988168413976156" width="206.95680443620125" height="158.15679986166066" maskUnits="userSpaceOnUse"><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none; stroke: white; stroke-width: 4;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: black;"/></mask></defs><g class="outer-stroke-shape"><defs><rect width="201" height="152" x="1" id="stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" transform="matrix(0.9999999978661108,-1.745329248269963e-8,1.7453292541199837e-7,1.0000000006480119,-0.000013047912574393195,0.0000017222602934907627)" ry="3" rx="3" y="0" data-style="fill:none;stroke-width:2;stroke:#E3E3E3;stroke-opacity:1;stroke-dasharray:"/></defs><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" mask="url(#outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35)" style="stroke-width: 4; stroke: rgb(227, 227, 227); stroke-opacity: 1; fill: none;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none;"/></g></g><g id="shape-575c9095-25c2-11ec-9877-05429cda5971"><path d="M0,91 h202 a0,0 0 0 1 0,0 v59 a3,3 0 0 1 -3,3 h-196 a3,3 0 0 1 -3,-3 v-59 a0,0 0 0 1 0,0 z" x="0" y="91" transform="matrix(1.0000000024760176,-1.0471975568525078e-7,3.490658523073093e-8,1.0000000103484785,-0.000004508681172410434,0.000009314180957176177)" width="202" height="62" style="fill: none; stroke-width: 2; stroke: rgb(227, 227, 227); stroke-opacity: 1;"/></g><g id="shape-575c9096-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="103" transform="matrix(0.9999999995870766,-4.810081334728633e-16,-4.810081268554315e-16,1.000000030940994,3.1795153176972235e-8,-0.0000034653912877047333)" width="134" height="18" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-575c9097-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="131" transform="matrix(1.0000000022017677,6.948317137937551e-23,6.617444834426139e-23,1.0000000667740845,-2.234794180822064e-7,-0.000009114662532283546)" width="183" height="11" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="81.5877720738863" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.43500565160593396 0.14985215967590193 81.5877720738863 128.8502825397186" height="128.8502825397186" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39c3-25c6-11ec-9877-05429cda5971" width="90" height="140" fill="none"><g id="shape-305b39c4-25c6-11ec-9877-05429cda5971"><path d="M18.180074555850297,34.13571661820788C16.328443710368447,37.802933917027985,15.363633970810042,41.85364142381468,15.363541785663983,45.96187246179579C15.363449600517924,49.407753224720636,16.04230101673238,52.81989422415609,17.360917347203213,56.00341606115035C18.679625862816465,59.187030083290665,20.612379636906553,62.07970778419531,23.049017419827578,64.51634556711724C25.485655202748603,66.95298335003963,28.378332903652336,68.88573712412926,31.561946925794473,70.20444563974297C34.74556094793297,71.52306197021062,38.157794132515846,72.20182120128084,41.603674895442964,72.20182120128084C48.56282376312447,72.20172901613478,55.23693615892262,69.43709648322647,60.15777926017472,64.51616119682512C65.07871454658016,59.59522591042378,67.84316270919953,52.92102132947775,67.8430705240462,45.96187246179579C67.84316270919953,41.85373360896074,66.87872171022173,37.802933917027985,65.027275235032,34.13571661820788L18.180074555850297,34.13571661820788ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c5-25c6-11ec-9877-05429cda5971"><path d="M40.358622311596264,76.81790020937888C29.539497004501754,76.81799239452494,19.163413509784732,80.76582127821257,11.513152601448382,87.79300278375013C3.862873256086459,94.81972336355693,-0.4349826275210944,104.35074562355658,-0.43500383010177757,114.28922622951995C-0.4349070356984157,119.34558149560962,0.6793145450319571,124.34939122839569,2.840650607504358,129.00013185144462L77.87714712656452,129.00013185144462C80.03842787794929,124.34939122839569,81.15276192455713,119.34558149560962,81.15276192455713,114.28922622951995C81.15276192455713,109.36838312826512,80.09761074177368,104.49547630300549,78.04750527664146,99.94890489510135C75.99739981150924,95.40325533865871,72.99244060259662,91.27234675472573,69.20436857717868,87.79281841345801C65.41629655176075,84.31329007218983,60.919228567337996,81.55317461144296,55.96990025592095,79.67010863112546C51.02047975936148,77.78704265080796,45.715777709559916,76.81790020937888,40.358622311596264,76.81790020937888ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c6-25c6-11ec-9877-05429cda5971"><path d="M22.63741074228892,55.77654841648564C22.63741074228892,55.77654841648564,28.038261899520876,66.05546876716517,39.71092947477882,65.88123884095012C51.38359705003313,65.70700891473462,57.30695361562539,56.473468121347196,57.30695361562539,56.473468121347196" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c7-25c6-11ec-9877-05429cda5971"><path d="M33.141447219852125,53.60300704066367C36.667529059923254,53.60300704066367,39.52591388646397,50.77716357071449,39.52591388646397,47.29136663950885C39.52591388646397,43.805569708302755,36.667529059923254,40.97972623835358,33.141447219852125,40.97972623835358C29.615457564927056,40.97972623835358,26.75698055324392,43.805569708302755,26.75698055324392,47.29136663950885C26.75698055324392,50.77716357071449,29.615457564927056,53.60300704066367,33.141447219852125,53.60300704066367ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1; stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c8-25c6-11ec-9877-05429cda5971"><path d="M52.22921139562823,5.9902455968044706C47.897615748537646,2.9522656685448965,42.82503589672706,1.0223696339853632,37.493231413951435,0.383799439228369C32.16142693117581,-0.2547744429339218,26.74730111289682,0.4191478335183092,21.76469396372704,2.341603604932061L64.9989743951628,32.66471824116161C65.42634473269572,27.547336408323645,64.47748302341643,22.41059569479512,62.24254634026147,17.742063338540447C60.007609657106514,13.073623167432288,56.56080704272608,9.028299273180437,52.22921139562823,5.9902455968044706ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="102.12322708597458" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.2830548330293823 0.00013327541864782688 102.12322708597458 54.02153523420793" height="54.02153523420793" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39ca-25c6-11ec-9877-05429cda5971" width="133" height="70" fill="none"><g id="shape-305b39cb-25c6-11ec-9877-05429cda5971"><path d="M92.06001883709541,52.780900512351764C92.66931429164106,49.56773051235177,86.02942338254798,47.840623239624165,83.94800520073477,47.06227823962445C89.18168701891409,45.781289603260575,101.50092247346038,49.98474823962442,101.82747470073446,46.54621233053331C102.1541065189158,43.10765323962414,96.1049138370945,39.43658051235161,92.18095065527814,37.78441233053354C88.2568870189134,36.13216687598788,82.26786429164349,34.273371421442334,83.30053701891302,33.860271421442576C84.33313247346086,33.4472486941695,93.48809610982426,36.99089869416957,94.37155520073429,34.2504214214423C95.25488292800583,31.510021421442616,89.671905200732,29.82478051235148,85.57220065527872,28.284194148715414C74.89666429164208,24.27258051235185,76.95853247345804,0.00013505780589184724,76.95853247345804,0.00013505780589184724L-0.28305389017623384,0.486489603260452C-0.28305389017623384,1.519162330533618,10.851173382550769,39.85655778507862,33.94041883709724,49.143364148715136C49.09020065527875,55.740090512351344,63.87718701891572,49.16363278507879,75.95035520072997,51.06297323962417C82.07081883709907,52.068059603260735,91.45064610982081,55.99406278507877,92.06001883709541,52.780900512351764ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39cc-25c6-11ec-9877-05429cda5971"><path d="M39.658059746187064,27.905789603260928C42.341277928004274,26.911598694169697,45.32354156436486,28.28403960326068,46.31920065527811,30.971275966897338C47.314859746184084,33.658435057805946,45.946823382550065,36.64278505780612,43.263605200732854,37.6369759668969C40.580387018912006,38.631089603260534,37.59812338255142,37.258648694170006,36.60246429163817,34.57148960326049C35.6068052007322,31.884253239624286,36.974841564369854,28.89990323962411,39.658059746187064,27.905789603260928ZZ" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39cd-25c6-11ec-9877-05429cda5971"><path d="M59.423496109822736,14.73643051235149C57.15994610982307,9.680012330533373,49.1343233825537,2.492953239624512,37.54835974618618,6.785685057806404C25.962164291642694,11.078494148715436,26.02405974618887,21.21628960326052,27.201927928006626,26.674912330533516" style="stroke-width: 2; stroke: white;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -396,6 +396,11 @@ ul.slider-dots {
text-align: right;
top: 26%;
width: 18px;
pointer-events: none;
max-width: 4rem;
overflow: hidden;
text-overflow: ellipsis;
}
.after {

View File

@@ -46,7 +46,7 @@ $width-settings-bar: 16rem;
}
.handoff-layout {
.viewer-preview {
.viewer-section {
flex-wrap: nowrap;
}
.settings-bar {

View File

@@ -336,7 +336,7 @@
.viewer-comments-container {
width: 100%;
height: 100%;
z-index: 1000;
z-index: 1;
position: absolute;
top: 0px;
left: 0px;

View File

@@ -335,6 +335,20 @@
padding: 3rem;
justify-content: center;
&.drafts {
background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg");
background-position: 15% bottom, 85% top;
background-repeat: no-repeat;
.text {
p {
max-width: 360px;
text-align: center;
font-size: $fs16;
}
}
}
svg {
width: 36px;
height: 36px;
@@ -346,5 +360,10 @@
color: $color-gray-30;
font-size: $fs16;
}
img.ph-files {
height: 150px;
margin-right: calc(100% - 148px);
}
}

View File

@@ -63,7 +63,7 @@
display: flex;
flex-direction: column;
width: 448px;
background-color: $color-dashboard;
background-color: $color-white;
.modal-header {
align-items: center;
@@ -705,7 +705,7 @@
background-color: $color-white;
box-shadow: 0 10px 10px rgba(0,0,0,.2);
display: flex;
min-height: 370px;
min-height: 420px;
flex-direction: row;
font-family: "sourcesanspro", sans-serif;
min-width: 620px;
@@ -824,21 +824,93 @@
}
&.final {
// TODO: Juan revisa TODA esta parte
padding: $size-5 0 0 0;
flex-direction: column;
.modal-top {
padding-top: 40px;
color: $color-gray-60;
display: flex;
flex-direction: column;
align-items: center;
h1 {
font-family: 'worksans', sans-serif;
font-weight: 700;
font-size: 27px;
margin-bottom: $size-3;
}
p {
font-family: 'worksans', sans-serif;
font-weight: 500;
font-size: $fs18;
}
}
.modal-columns {
display: flex;
margin: 17px;
.modal-left {
background-image: url("/images/on-solo.svg");
background-position: left top;
background-size: 11%;
}
.modal-left:hover {
background-image: url("/images/on-solo-hover.svg");
background-size: 15%;
}
.modal-right {
background-image: url("/images/on-teamup.svg");
background-position: right top;
background-size: 28%;
}
.modal-right:hover {
background-image: url("/images/on-teamup-hover.svg");
background-size: 32%;
}
.modal-right,
.modal-left {
background-repeat: no-repeat;
border-radius: $br-medium;
transition: all ease .3s;
&:hover {
background-color: $color-primary;
}
}
}
.modal-left {
margin-right: 35px;
}
.modal-left,
.modal-right {
justify-content: center;
align-items: center;
background-color: $color-white;
color: $color-black;
flex: 1;
flex-direction: column;
overflow: visible;
padding: $size-6 40px;
// overflow: visible;
// padding: $size-6 40px;
text-align: center;
border: 1px solid $color-gray-10;
border-radius: 2px;
min-height: 180px;
width: 233px;
cursor: pointer;
h2 {
font-weight: 900;
font-weight: 700;
margin-bottom: $size-5;
font-size: $fs24;
}
@@ -847,12 +919,6 @@
font-size: $fs14;
}
.btn-primary {
margin-bottom: 0;
margin-top: auto;
width: 200px;
}
img {
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
border-radius: $br-medium;
@@ -861,26 +927,6 @@
width: 150px;
}
}
.modal-left {
border-right: 1px solid $color-gray-10;
form {
align-items: center;
display: flex;
flex-direction: column;
margin-top: auto;
.custom-input {
margin-bottom: $size-4;
input {
width: 200px;
}
}
}
}
}
}
@@ -899,3 +945,193 @@
.relnotes .onboarding {
height: 420px;
}
.onboarding-templates {
position: fixed;
top: 0;
right: 0;
width: 348px;
height: 100vh;
.modal-close-button {
width: 34px;
height: 34px;
margin-right: 13px;
margin-top: 13px;
svg {
width: 24px;
height: 24px;
}
}
.modal-header {
height: unset;
border-radius: unset;
justify-content: flex-end;
}
.modal-content {
border: 0px;
padding: 0px 25px;
background-color: $color-white;
flex-grow: 1;
p, h3 {
color: $color-gray-60;
text-align: center;
}
h3 {
font-size: $fs18;
font-weight: bold;
}
p {
font-size: $fs16;
}
.templates {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8%;
}
.template-item {
width: 275px;
border: 1px solid $color-gray-10;
display: flex;
flex-direction: column;
text-align: left;
border-radius: $br-small;
&:not(:last-child) {
margin-bottom: 22px;
}
}
.template-item-content {
// height: 144px;
flex-grow: 1;
img {
border-radius: $br-small $br-small 0 0;
}
}
.template-item-title {
padding: 6px 12px;
height: 64px;
border-top: 1px solid $color-gray-10;
.label {
color: $color-black;
padding: 0px 4px;
font-size: $fs16;
display: flex;
}
.action {
color: $color-primary-dark;
cursor: pointer;
font-size: $fs14;
font-weight: 600;
display: flex;
justify-content: flex-end;
margin-top: $size-2;
}
}
}
}
.onboarding-team {
display: flex;
min-width: 620px;
min-height: 420px;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
.title {
display: flex;
flex-direction: column;
align-items: center;
width: 408px;
color: $color-gray-60;
h2 {
font-weight: 700;
padding-bottom: 10px;
}
p {
text-align: center;
font-size: $fs18;
}
}
form {
display: flex;
flex-direction: column;
margin-top: $size-6;
.buttons {
margin-top: 30px;
display: flex;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 13px;
}
input { margin-bottom: unset; }
input[type=submit] {
}
.btn-primary {
width: 117px;
}
}
.team-row {
.custom-input {
width: 459px;
}
}
.invite-row {
display: flex;
justify-content: space-between;
> *:not(:last-child) {
margin-right: 13px;
}
.custom-input {
width: 321px;
}
.custom-select {
width: 118px;
}
}
.skip-action {
display: flex;
justify-content: flex-end;
margin-top: 15px;
.action {
color: $color-primary-dark;
font-weight: 500;
font-size: $fs16;
cursor: pointer;
}
}
}
}

View File

@@ -70,12 +70,6 @@
width: 100%;
align-items: center;
}
&:hover {
.element-set-title {
color: $color-gray-10;
}
}
}
.element-list {
@@ -207,6 +201,9 @@
border-color: $color-gray-40;
}
&.error {
border-color: $color-danger;
}
}
.input-select {

View File

@@ -6,7 +6,6 @@
height: 48px;
padding: 0 $size-4 0 55px;
position: relative;
z-index: 12;
justify-content: space-between;
a {

View File

@@ -6,7 +6,7 @@
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 12;
z-index: 10;
&.invisible {
visibility: hidden;

View File

@@ -6,8 +6,8 @@
grid-template-columns: 1fr;
}
.viewer-preview {
height: calc(100vh - 40px);
.viewer-section {
height: calc(100vh - 48px);
grid-row: 1 / span 2;
grid-column: 1 / span 1;

View File

@@ -8,7 +8,7 @@ EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
yarn install || exit 1;
npx gulp clean || exit 1;
clojure -J-Xms1G -J-Xmx1G -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1
clojure -J-Xms100M -J-Xmx800M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1
npx gulp build || exit 1;
npx gulp dist:clean || exit 1;
npx gulp dist:copy || exit 1;

View File

@@ -57,8 +57,8 @@
(defn- parse-flags
[global]
(let [flags (obj/get global "penpotFlags" "")
flags (into #{} (map keyword) (str/words flags))]
(flags/parse flags flags/default)))
flags (sequence (map keyword) (str/words flags))]
(flags/parse flags/default flags)))
(defn- parse-version
[global]

View File

@@ -187,10 +187,12 @@
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-files
(fn [state]
(let [state (remove-project-files state)]
(reduce #(assoc %1 (:id %2) %2) state files))))))))
(-> state
(update :dashboard-files
(fn [state]
(let [state (remove-project-files state)]
(reduce #(assoc %1 (:id %2) %2) state files))))
(assoc-in [:dashboard-projects project-id :count] (count files)))))))
(defn fetch-files
[{:keys [project-id] :as params}]
@@ -300,6 +302,28 @@
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: create-team-with-invitations
;; NOTE: right now, it only handles a single email, in a near future
;; this will be changed to the ability to specify multiple emails.
(defn create-team-with-invitations
[{:keys [name email role] :as params}]
(us/assert string? name)
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params {:name name
:emails #{email}
:role role}]
(->> (rp/mutation! :create-team-and-invite-members params)
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: update-team
(defn update-team

View File

@@ -302,6 +302,21 @@
(update [_ state]
(assoc-in state [:viewer-local :interactions-show?] false))))
(defn set-nav-scroll
[scroll]
(ptk/reify ::set-nav-scroll
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :nav-scroll] scroll))))
(defn reset-nav-scroll
[]
(ptk/reify ::reset-nav-scroll
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:viewer-local :nav-scroll]))))
;; --- Navigation inside page
(defn go-to-frame-by-index

View File

@@ -125,70 +125,73 @@
create a group that contains all ids. Then, make a component with it,
and link all shapes to their corresponding one in the component."
[shapes objects page-id file-id]
(let [[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dwg/prepare-create-group objects page-id shapes "Component-1" true))
(if (and (= (count shapes) 1)
(:component-id (first shapes)))
empty-changes
(let [[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dwg/prepare-create-group objects page-id shapes "Component-1" true))
[new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id)
[new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val (:component-file updated-shape)}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
:val (:component-file updated-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
[group rchanges uchanges]))
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
[group rchanges uchanges])))
(defn duplicate-component
"Clone the root shape of the component and all children. Generate new

View File

@@ -201,6 +201,9 @@
(s/def ::shapes-changes-persisted
(s/keys :req-un [::revn ::cp/changes]))
(defn shapes-persited-event? [event]
(= (ptk/type event) ::changes-persisted))
(defn shapes-changes-persisted
[file-id {:keys [revn changes] :as params}]
(us/verify ::us/uuid file-id)

View File

@@ -61,12 +61,6 @@
(def dashboard-search-result
(l/derived :dashboard-search-result st/state))
(def dashboard-team
(l/derived (fn [state]
(let [team-id (:current-team-id state)]
(get-in state [:teams team-id])))
st/state))
(def dashboard-team-stats
(l/derived :dashboard-team-stats st/state))

View File

@@ -72,7 +72,10 @@
:dashboard-team-settings)
[:*
#_[:div.modal-wrapper
[:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]]
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding/onboarding-team-modal]
]
[:& dashboard {:route route}]]
:viewer

View File

@@ -169,7 +169,9 @@
(let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
(not= "penpot" (:auth-backend data))
;; The :is-active flag is true, when insecure-register is enabled
;; or the user used external auth provider.
(:is-active data)
(st/emit! (du/login-from-register))
:else
@@ -178,9 +180,14 @@
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean)
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
:opt-un [::accept-newsletter-subscription]))
(if (contains? @cf/flags :terms-and-privacy-checkbox)
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
:opt-un [::accept-newsletter-subscription]))
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname]
:opt-un [::accept-terms-and-privacy
::accept-newsletter-subscription])))
(mf/defc register-validate-form
[{:keys [params] :as props}]
@@ -207,23 +214,17 @@
:label (tr "auth.fullname")
:type "text"}]]
[:div.fields-row
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"
:type "checkbox"}
[:span
(tr "auth.terms-privacy-agreement")
[:div
[:a {:href "https://penpot.app/terms.html" :target "_blank"} (tr "auth.terms-of-service")]
[:span ",\u00A0"]
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]]
;; (when (contains? @cf/flags :newsletter-registration-check)
;; [:div.fields-row
;; [:& fm/input {:name :accept-newsletter-subscription
;; :class "check-primary"
;; :label (tr "auth.newsletter-subscription")
;; :type "checkbox"}]])
(when (contains? @cf/flags :terms-and-privacy-checkbox)
[:div.fields-row
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"
:type "checkbox"}
[:span
(tr "auth.terms-privacy-agreement")
[:div
[:a {:href "https://penpot.app/terms.html" :target "_blank"} (tr "auth.terms-of-service")]
[:span ",\u00A0"]
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]])
[:& fm/submit-button
{:label (tr "auth.register-submit")

View File

@@ -24,6 +24,7 @@
max-val-str (obj/get props "max")
wrap-value? (obj/get props "data-wrap")
on-change (obj/get props "onChange")
title (obj/get props "title")
;; We need a ref pointing to the input dom element, but the user
;; of this component may provide one (that is forwarded here).
@@ -33,9 +34,18 @@
value (d/parse-integer value-str 0)
min-val (when (string? min-val-str)
min-val (cond
(number? min-val-str)
min-val-str
(string? min-val-str)
(d/parse-integer min-val-str))
max-val (when (string? max-val-str)
max-val (cond
(number? max-val-str)
max-val-str
(string? max-val-str)
(d/parse-integer max-val-str))
num? (fn [val] (and (number? val)
@@ -144,6 +154,7 @@
(obj/set! "type" "text")
(obj/set! "ref" ref)
(obj/set! "defaultValue" value-str)
(obj/set! "title" title)
(obj/set! "onWheel" handle-mouse-wheel)
(obj/set! "onKeyDown" handle-key-down)
(obj/set! "onBlur" handle-blur))]

View File

@@ -11,6 +11,10 @@
(def render-ctx (mf/create-context nil))
(def def-ctx (mf/create-context false))
;; This content is used to replace complex colors to simple ones
;; for text shapes in the export process
(def text-plain-colors-ctx (mf/create-context false))
(def current-route (mf/create-context nil))
(def current-team-id (mf/create-context nil))
(def current-project-id (mf/create-context nil))

View File

@@ -104,11 +104,11 @@
(when (and (:onboarding-viewed props)
(not= version (:main @cf/version))
(not= "0.0" (:main @cf/version)))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)})))))))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
:version (:main @cf/version)})))))))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing

View File

@@ -120,6 +120,6 @@
[:*
[:& header {:team team :project project}]
[:section.dashboard-container
[:& grid {:project-id (:id project)
[:& grid {:project project
:files files}]]]))

View File

@@ -15,6 +15,7 @@
[app.main.ui.dashboard.file-menu :refer [file-menu]]
[app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
[app.main.ui.icons :as i]
[app.main.worker :as wrk]
[app.util.dom :as dom]
@@ -195,24 +196,10 @@
:on-edit on-edit
:on-menu-close on-menu-close}])]]]))
(mf/defc empty-placeholder
[{:keys [dragging?] :as props}]
(if-not dragging?
[:div.grid-empty-placeholder
[:div.icon i/file-html]
[:div.text (tr "dashboard.empty-files")]]
[:div.grid-row.no-wrap
[:div.grid-item]]))
(mf/defc loading-placeholder
[]
[:div.grid-empty-placeholder
[:div.icon i/loader]
[:div.text (tr "dashboard.loading-files")]])
(mf/defc grid
[{:keys [files project-id] :as props}]
(let [dragging? (mf/use-state false)
[{:keys [files project] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
on-finish-import
(mf/use-callback
@@ -272,7 +259,7 @@
:navigate? true}])]
:else
[:& empty-placeholder])]))
[:& empty-placeholder {:default? (:is-default project)}])]))
(mf/defc line-grid-row
[{:keys [files selected-files on-load-more dragging?] :as props}]
@@ -330,8 +317,11 @@
(tr "dashboard.show-all-files")]])]))
(mf/defc line-grid
[{:keys [project-id team-id files on-load-more] :as props}]
[{:keys [project team files on-load-more] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
team-id (:id team)
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
@@ -413,5 +403,6 @@
:dragging? @dragging?}]
:else
[:& empty-placeholder {:dragging? @dragging?}])]))
[:& empty-placeholder {:dragging? @dragging?
:default? (:is-default project)}])]))

View File

@@ -331,10 +331,11 @@
[:div.modal-footer
[:div.action-buttons
[:input.cancel-button
{:type "button"
:value (tr "labels.cancel")
:on-click handle-cancel}]
(when (or (= :analyzing (:status @state)) pending-import?)
[:input.cancel-button
{:type "button"
:value (tr "labels.cancel")
:on-click handle-cancel}])
(when (= :analyzing (:status @state))
[:input.accept-button

View File

@@ -0,0 +1,34 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.dashboard.placeholder
(:require
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc empty-placeholder
[{:keys [dragging? default?] :as props}]
(cond
(true? dragging?)
[:div.grid-row.no-wrap
[:div.grid-item]]
(true? default?)
[:div.grid-empty-placeholder.drafts
[:div.text
[:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]]
:else
[:div.grid-empty-placeholder
[:img.ph-files {:src "images/ph-file.svg"}]]))
(mf/defc loading-placeholder
[]
[:div.grid-empty-placeholder
[:div.icon i/loader]
[:div.text (tr "dashboard.loading-files")]])

View File

@@ -73,7 +73,6 @@
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn}))
file-input (mf/use-ref nil)
on-import-files

View File

@@ -33,10 +33,8 @@
(tr "dashboard.new-project")]]))
(mf/defc project-item
[{:keys [project first? files] :as props}]
[{:keys [project first? team files] :as props}]
(let [locale (mf/deref i18n/locale)
team-id (:team-id project)
file-count (or (:count project) 0)
dstate (mf/deref refs/dashboard-local)
@@ -100,7 +98,8 @@
on-import
(mf/use-callback
(fn []
(st/emit! (dd/fetch-recent-files)
(st/emit! (dd/fetch-files {:project-id (:id project)})
(dd/fetch-recent-files)
(dd/clear-selected-files))))]
[:div.dashboard-project-row {:class (when first? "first")}
@@ -145,9 +144,8 @@
i/actions]]
[:& line-grid
{:project-id (:id project)
:project project
:team-id team-id
{:project project
:team team
:on-load-more on-nav
:files files}]]))
@@ -186,7 +184,8 @@
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:& project-item {:project project
:files files
:team team
:files files
:first? (= project (first projects))
:key (:id project)}]))]])))

View File

@@ -230,9 +230,12 @@
(mf/defc leave-and-reassign-modal
{::mf/register modal/components
::mf/register-as ::leave-and-reassign}
[{:keys [members profile team accept]}]
[{:keys [team accept]}]
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
members (some->> members (filterv #(not= (:id %) (:id profile))))
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
options (into [{:value ""
:label (tr "modals.leave-and-reassign.select-memeber-to-promote")}]
(map #(hash-map :label (:name %) :value (str (:id %))) members))
@@ -290,7 +293,7 @@
on-leaved-success
(fn []
(st/emit! (modal/hide)
(dd/go-to-projects (:default-team-id profile))))
(du/fetch-teams)))
leave-fn
(st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
@@ -298,7 +301,13 @@
leave-and-reassign-fn
(fn [member-id]
(let [params {:reassign-to member-id}]
(st/emit! (dd/leave-team (with-meta params {:on-success on-leaved-success})))))
(st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/leave-team (with-meta params {:on-success on-leaved-success})))))
delete-fn
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/delete-team (with-meta team {:on-success on-leaved-success}))))
on-leave-clicked
(st/emitf (modal/show
@@ -309,15 +318,13 @@
:on-accept leave-fn}))
on-leave-as-owner-clicked
(st/emitf (modal/show
{:type ::leave-and-reassign
:profile profile
:team team
:members members
:accept leave-and-reassign-fn}))
delete-fn
(st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success})))
(fn []
(st/emit! (dd/fetch-team-members)
(modal/show
{:type ::leave-and-reassign
:profile profile
:team team
:accept leave-and-reassign-fn})))
on-delete-clicked
(st/emitf
@@ -335,14 +342,14 @@
[:li {:on-click on-rename-clicked} (tr "labels.rename")]
(cond
(:is-owner team)
(get-in team [:permissions :is-owner])
[:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
(> (count members) 1)
[:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])
(when (:is-owner team)
(when (get-in team [:permissions :is-owner])
[:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))

View File

@@ -124,6 +124,7 @@
(mf/defc team-member
{::mf/wrap [mf/memo]}
[{:keys [team member profile] :as props}]
(let [show? (mf/use-state false)
set-role
@@ -174,8 +175,8 @@
[:span.label (tr "labels.viewer")])
(when (and (not (:is-owner member))
(or (:is-admin team)
(:is-owner team)))
(or (get-in team [:permissions :is-admin])
(get-in team [:permissions :is-owner])))
[:span.icon {:on-click #(reset! show? true)} i/arrow-down])]
[:& dropdown {:show @show?
@@ -191,8 +192,8 @@
[:hr]
[:li {:on-click set-owner} (tr "dashboard.promote-to-owner")]])
[:hr]
(when (and (or (:is-owner team)
(:is-admin team))
(when (and (or (get-in team [:permissions :is-owner])
(get-in team [:permissions :is-admin]))
(not= (:id profile)
(:id member)))
[:li {:on-click delete} (tr "labels.remove")])]]]]))

View File

@@ -72,8 +72,9 @@
#(doseq [key keys]
(events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref}
(mf/element (get components (:type data)) (:props data))]))
(when-let [component (get components (:type data))]
[:div.modal-wrapper {:ref wrapper-ref}
(mf/element component (:props data))])))
(def modal-ref

View File

@@ -12,19 +12,25 @@
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
[app.main.ui.releases.v1-7]
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.timers :as tm]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
@@ -38,10 +44,11 @@
[:div.modal-right
[:div.modal-title
[:h2 (tr "onboarding.welcome.title")]]
[:span.release "Alpha version " (:main @cf/version)]
[:span.release "Beta version " (:main @cf/version)]
[:div.modal-content
[:p (tr "onboarding.welcome.desc1")]
[:p (tr "onboarding.welcome.desc2")]]
[:p (tr "onboarding.welcome.desc2")]
[:p (tr "onboarding.welcome.desc3")]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} (tr "labels.continue")]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
@@ -159,7 +166,7 @@
skip
(mf/use-callback
(st/emitf (modal/hide)
(modal/show {:type :onboarding-team})
(modal/show {:type :onboarding-choice})
(du/mark-onboarding-as-viewed)))]
(mf/use-layout-effect
@@ -187,57 +194,233 @@
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-choice-modal
{::mf/register modal/components
::mf/register-as :onboarding-choice}
[]
(let [;; When user choices the option of `fly solo`, we proceed to show
;; the onboarding templates modal.
on-fly-solo
(fn []
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
on-team-up
(fn []
(st/emit! (modal/show {:type :onboarding-team})))
]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
[:h1 (tr "onboarding.choice.title")]
[:p (tr "onboarding.choice.desc")]]
[:div.modal-columns
[:div.modal-left
[:div.content-button {:on-click on-fly-solo}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
[:div.content-button {:on-click on-team-up}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [close (mf/use-fn (st/emitf (modal/hide)))
form (fm/use-form :spec ::team-form
(let [form (fm/use-form :spec ::team-form
:initial {})
on-submit
(mf/use-callback
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.team-row
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team-input-placeholder")}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.next")}]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-form
(s/keys :req-un [::role ::email]))
;; This is the final step of team creation, consists in provide a
;; shortcut for invite users.
(mf/defc onboarding-team-invitations-modal
{::mf/register modal/components
::mf/register-as :onboarding-team-invitations}
[{:keys [name] :as props}]
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name}))
form (fm/use-form :spec ::invite-form
:initial initial)
roles (mf/use-memo #(get-available-roles))
on-success
(mf/use-callback
(fn [_form response]
(st/emit! (modal/hide)
(rt/nav :dashboard-projects {:team-id (:id response)}))))
(let [project-id (:default-project-id response)
team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/show {:type :onboarding-templates
:project-id project-id}))))))
on-error
(mf/use-callback
(fn [_form _response]
(st/emit! (dm/error "Error on creating team."))))
on-submit
;; The SKIP branch only creates the team, without invitations
on-skip
(mf/use-callback
(fn [form _event]
(fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name (get-in @form [:clean-data :name])}]
(st/emit! (dd/create-team (with-meta params mdata))))))]
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))))))
;; The SUBMIT branch creates the team with the invitations
on-submit
(mf/use-callback
(fn [form _]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params (:clean-data @form)]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-left
[:img {:src "images/onboarding-team.jpg" :border "0" :alt (tr "onboarding.team.create.title")}]
[:h2 (tr "onboarding.team.create.title")]
[:p (tr "onboarding.team.create.desc1")]
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team.create.input-placeholder")}]
[:& fm/form {:form form
:on-submit on-submit}
[:div.invite-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "onboarding.team.create.button")}]]]
[:div.modal-right
[:img {:src "images/onboarding-start.jpg" :border "0" :alt (tr "onboarding.team.start.title")}]
[:h2 (tr "onboarding.team.start.title")]
[:p (tr "onboarding.team.start.desc1")]
[:button.btn-primary.btn-large {:on-click close} (tr "onboarding.team.start.button")]]
{:label (tr "labels.create")}]]
[:div.skip-action
{:on-click on-skip}
[:div.action "Skip and invite later"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc cf/public-uri :path path))
on-finish-import
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-recent-files)
(dd/clear-selected-files)))
open-import-modal
(fn [file]
(st/emit! (modal/show
{:type :import
:project-id project-id
:files [file]
:on-finish-import on-finish-import})))
on-click
(fn []
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []
(reset! downloading? false)))))
]
[:div.template-item
[:div.template-item-content
[:img {:src image}]]
[:div.template-item-title
[:div.label name]
(if @downloading?
[:div.action "Fetching..."]
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
(mf/defc onboarding-templates-modal
{::mf/register modal/components
::mf/register-as :onboarding-templates}
;; NOTE: the project usually comes empty, it only comes fullfilled
;; when a user creates a new team just after signup.
[{:keys [project-id] :as props}]
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
profile (mf/deref refs/profile)
project-id (or project-id (:default-project-id profile))]
[:div.modal-overlay
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
{:on-click close-fn} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
[:p (tr "onboarding.templates.subtitle")]
[:div.templates
[:& template-item
{:path "/github/penpot-files/Penpot-Design-system.penpot"
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:name "Penpot Design System"
:project-id project-id}]
[:& template-item
{:path "/github/penpot-files/Material-Design-Kit.penpot"
:image "https://penpot.app/images/libraries/cover-material.jpg"
:name "Material Design Kit"
:project-id project-id}]]]]]))
;;; --- RELEASE NOTES MODAL
@@ -299,5 +482,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "1.9")))
(rc/render-release-notes (assoc params :version "1.10")))

View File

@@ -0,0 +1,31 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.releases.v1-10
(:require
[app.main.ui.releases.common :as c]
[rumext.alpha :as mf]))
(defmethod c/render-release-notes "1.10"
[{:keys [klass finish version]}]
(mf/html
[:div.modal-overlay
[:div.animated {:class @klass}
[:div.modal-container.onboarding.feature
[:div.modal-left
[:img {:src "images/beta-on.jpg" :border "0" :alt "Penpot is now BETA"}]]
[:div.modal-right
[:div.modal-title
[:h2 "Penpot is now BETA"]]
[:span.release "Beta version " version]
[:div.modal-content
[:p "Penpots officially beta!"]
[:p "We carefully analyzed everything important to us before taking this step. And now were ready to move forward onto the beta version. Have a play around if you havent yet."]
[:a {:href "https://penpot.app/why-beta.html" :target "_blank"} "Learn why we made this decision."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Explore Penpot Beta 1.10"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]]))

View File

@@ -16,6 +16,7 @@
[app.main.exports :as exports]
[app.main.repo :as repo]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.export :as ed]
[app.main.ui.shapes.filters :as filters]
@@ -124,14 +125,15 @@
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:svg {:id (str "screenshot-text-" (:id object))
:view-box (str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]))]))
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
[:svg {:id (str "screenshot-text-" (:id object))
:view-box (str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]]))]))
(defn- adapt-root-frame
[objects object-id]

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.shapes.attrs
(:require
[app.common.pages.spec :as spec]
[app.common.types.radius :as ctr]
[app.main.ui.context :as muc]
[app.util.object :as obj]
[app.util.svg :as usvg]
@@ -53,7 +54,13 @@
(min r-bottom-left r-left-bottom)]))
(defn add-border-radius [attrs shape]
(if (or (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape))
(case (ctr/radius-mode shape)
:radius-1
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)})
:radius-4
(let [[r1 r2 r3 r4] (truncate-radius shape)
top (- (:width shape) r1 r2)
right (- (:height shape) r2 r3)
@@ -69,10 +76,7 @@
"v" (- left) " "
"a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " "
"z")}))
(if (or (:rx shape) (:ry shape))
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)})
attrs)))
attrs))
(defn add-fill [attrs shape render-id]
(let [fill-attrs (cond

View File

@@ -69,6 +69,7 @@
stroke-opacity (when-not (:stroke-color-gradient shape)
(:stroke-opacity shape))]
[:*
(when (or (= cap-start :line-arrow) (= cap-end :line-arrow))
[:marker {:id (str marker-id-prefix "-line-arrow")
@@ -160,22 +161,23 @@
(mf/defc stroke-defs
[{:keys [shape render-id]}]
(when (or (not= (:type shape) :path)
(not (gsh/open-path? shape)))
(let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))]
(cond
(and (= :inner (:stroke-alignment shape :center))
(and (not open-path?)
(= :inner (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& inner-stroke-clip-path {:shape shape
:render-id render-id}]
(and (= :outer (:stroke-alignment shape :center))
(and (not open-path?)
(= :outer (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& outer-stroke-mask {:shape shape
:render-id render-id}]
(and (or (some? (:stroke-cap-start shape))
(some? (:stroke-cap-end shape)))
(= (:stroke-alignment shape) :center))
(or (some? (:stroke-cap-start shape))
(some? (:stroke-cap-end shape)))
[:& cap-markers {:shape shape
:render-id render-id}])))

View File

@@ -242,7 +242,8 @@
:penpot:overlay-position-y ((d/nilf get-in) interaction [:overlay-position :y])
:penpot:url (:url interaction)
:penpot:close-click-outside ((d/nilf str) (:close-click-outside interaction))
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction))}])]))
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction))
:penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])]))
(mf/defc export-data
[{:keys [shape]}]

View File

@@ -10,24 +10,10 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]
[app.main.ui.shapes.export :as ed]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(mf/defc linear-gradient [{:keys [id gradient shape]}]
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))]
[:> :linearGradient #js {:id id
:x1 (:start-x gradient)
:y1 (:start-y gradient)
:x2 (:end-x gradient)
:y2 (:end-y gradient)
:gradientTransform transform
:penpot:gradient "true"}
(for [{:keys [offset color opacity]} (:stops gradient)]
[:stop {:key (str id "-stop-" offset)
:offset (or offset 0)
:stop-color color
:stop-opacity opacity}])]))
(defn add-metadata [props gradient]
(-> props
(obj/set! "penpot:gradient" "true")
@@ -38,6 +24,30 @@
(obj/set! "penpot:end-y" (:end-y gradient))
(obj/set! "penpot:width" (:width gradient))))
(mf/defc linear-gradient [{:keys [id gradient shape]}]
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))
base-props #js {:id id
:x1 (:start-x gradient)
:y1 (:start-y gradient)
:x2 (:end-x gradient)
:y2 (:end-y gradient)
:gradientTransform transform}
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
props (cond-> base-props
include-metadata?
(add-metadata gradient))]
[:> :linearGradient props
(for [{:keys [offset color opacity]} (:stops gradient)]
[:stop {:key (str id "-stop-" offset)
:offset (or offset 0)
:stop-color color
:stop-opacity opacity}])]))
(mf/defc radial-gradient [{:keys [id gradient shape]}]
(let [{:keys [x y width height]} (:selrect shape)
transform (if (= :path (:type shape))
@@ -73,7 +83,11 @@
:gradientUnits "userSpaceOnUse"
:gradientTransform transform}
props (-> base-props (add-metadata gradient))]
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
props (cond-> base-props
include-metadata?
(add-metadata gradient))]
[:> :radialGradient props
(for [{:keys [offset color opacity]} (:stops gradient)]
[:stop {:key (str id "-stop-" offset)

View File

@@ -8,9 +8,12 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as geom]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.text.styles :as sts]
[app.util.color :as uc]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(mf/defc render-text
@@ -73,12 +76,111 @@
(obj/set! "key" index))]
[:> render-node props]))])))))
(defn- next-color
"Given a set of colors try to get a color not yet used"
[colors]
(assert (set? colors))
(loop [current-rgb [0 0 0]]
(let [current-hex (uc/rgb->hex current-rgb)]
(if (contains? colors current-hex)
(recur (uc/next-rgb current-rgb))
current-hex))))
(defn- remap-colors
"Returns a new content replacing the original colors by their mapped 'simple color'"
[content color-mapping]
(cond-> content
(and (:fill-opacity content) (< (:fill-opacity content) 1.0))
(-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)]))
(assoc :fill-opacity 1.0))
(some? (:fill-color-gradient content))
(-> (assoc :fill-color (get color-mapping (:fill-color-gradient content)))
(assoc :fill-opacity 1.0)
(dissoc :fill-color-gradient))
(contains? content :children)
(update :children #(mapv (fn [node] (remap-colors node color-mapping)) %))))
(defn- fill->color
"Given a content node returns the information about that node fill color"
[{:keys [fill-color fill-opacity fill-color-gradient]}]
(cond
(some? fill-color-gradient)
{:type :gradient
:gradient fill-color-gradient}
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
{:type :transparent
:hex fill-color
:opacity fill-opacity}
(string? fill-color)
{:type :solid
:hex fill-color
:map-to fill-color}))
(defn- retrieve-colors
"Given a text shape returns a triple with the values:
- colors used as fills
- a mapping from simple solid colors to complex ones (transparents/gradients)
- the inverse of the previous mapping (to restore the value in the SVG)"
[shape]
(let [colors (->> (:content shape)
(tree-seq map? :children)
(into #{"#000000"} (comp (map :fill-color) (filter string?))))]
(apply str (interpose "," colors))))
(let [color-data
(->> (:content shape)
(tree-seq map? :children)
(map fill->color)
(filter some?))
colors (->> color-data
(into #{"#000000"}
(comp (filter #(= :solid (:type %)))
(map :hex))))
[colors color-data]
(loop [colors colors
head (first color-data)
tail (rest color-data)
result []]
(if (nil? head)
[colors result]
(if (= :solid (:type head))
(recur colors
(first tail)
(rest tail)
(conj result head))
(let [next-color (next-color colors)
head (assoc head :map-to next-color)
colors (conj colors next-color)]
(recur colors
(first tail)
(rest tail)
(conj result head))))))
color-mapping-inverse
(->> color-data
(remove #(= :solid (:type %)))
(group-by :map-to)
(d/mapm #(first %2)))
color-mapping
(merge
(->> color-data
(filter #(= :transparent (:type %)))
(map #(vector [(:hex %) (:opacity %)] (:map-to %)))
(into {}))
(->> color-data
(filter #(= :gradient (:type %)))
(map #(vector (:gradient %) (:map-to %)))
(into {})))]
[colors color-mapping color-mapping-inverse]))
(mf/defc text-shape
{::mf/wrap-props false
@@ -88,11 +190,19 @@
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
;; We add 8px to add a padding for the exporter
;; width (+ width 8)
]
[colors color-mapping color-mapping-inverse] (retrieve-colors shape)
plain-colors? (mf/use-ctx muc/text-plain-colors-ctx)
content (cond-> content
plain-colors?
(remap-colors color-mapping))]
[:foreignObject {:x x
:y y
:id id
:data-colors (retrieve-colors shape)
:data-colors (->> colors (str/join ","))
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
:transform (geom/transform-matrix shape)
:width (if (#{:auto-width} grow-type) 100000 width)
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)

View File

@@ -46,7 +46,7 @@
(defn generate-paragraph-styles
[shape data]
(let [line-height (:line-height data)
(let [line-height (:line-height data 1.2)
text-align (:text-align data "start")
grow-type (:grow-type shape)
@@ -60,10 +60,10 @@
(defn generate-text-styles
[data]
(let [letter-spacing (:letter-spacing data)
(let [letter-spacing (:letter-spacing data 0)
text-decoration (:text-decoration data)
text-transform (:text-transform data)
line-height (:line-height data)
line-height (:line-height data 1.2)
font-id (:font-id data (:font-id txt/default-text-attrs))
font-variant-id (:font-variant-id data)

View File

@@ -127,10 +127,14 @@
[:div.share-link-section
[:label (tr "labels.link")]
[:div.custom-input.with-icon
[:input {:type "text" :value (or @link "") :read-only true}]
[:div.help-icon {:title (tr "labels.copy")
:on-click copy-link}
i/copy]]
[:input {:type "text"
:value (or @link "")
:placeholder (tr "common.share-link.placeholder")
:read-only true}]
(when (some? @link)
[:div.help-icon {:title (tr "labels.copy")
:on-click copy-link}
i/copy])]
[:div.hint (tr "common.share-link.permissions-hint")]]]

View File

@@ -43,6 +43,8 @@
local (mf/deref refs/viewer-local)
nav-scroll (:nav-scroll local)
page-id (or page-id (-> file :data :pages first))
page (mf/use-memo
@@ -91,6 +93,14 @@
(fn []
(events/unlistenByKey key1)))))
(mf/use-layout-effect
(mf/deps nav-scroll)
(fn []
(when (number? nav-scroll)
(let [viewer-section (dom/get-element "viewer-section")]
(st/emit! (dv/reset-nav-scroll))
(dom/set-scroll-pos! viewer-section nav-scroll)))))
[:div {:class (dom/classnames
:force-visible (:show-thumbnails local)
:viewer-layout (not= section :handoff)
@@ -110,7 +120,7 @@
:show? (:show-thumbnails local false)
:page page
:index index}]
[:section.viewer-preview
[:section.viewer-section {:id "viewer-section"}
(cond
(empty? frames)
[:section.empty-state

View File

@@ -6,10 +6,8 @@
(ns app.main.ui.viewer.comments
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.refs :as refs]
@@ -80,6 +78,7 @@
(mf/defc comments-layer
[{:keys [zoom file users frame page] :as props}]
(let [profile (mf/deref refs/profile)
threads-map (mf/deref threads-ref)
modifier1 (-> (gpt/point (:x frame) (:y frame))
(gpt/negate)
@@ -88,16 +87,12 @@
modifier2 (-> (gpt/point (:x frame) (:y frame))
(gmt/translate-matrix))
threads-map (->> (mf/deref threads-ref)
(d/mapm #(update %2 :position gpt/transform modifier1)))
cstate (mf/deref refs/comments-local)
mframe (geom/transform-shape frame)
threads (->> (vals threads-map)
(dcm/apply-filters cstate profile)
(filter (fn [{:keys [position]}]
(frame-contains? mframe position))))
(frame-contains? frame position))))
on-bubble-click
(mf/use-callback
@@ -110,14 +105,15 @@
on-click
(mf/use-callback
(mf/deps cstate frame page file)
(mf/deps cstate frame page file zoom)
(fn [event]
(dom/stop-propagation event)
(if (some? (:open cstate))
(st/emit! (dcm/close-thread))
(let [event (.-nativeEvent ^js event)
position (-> (dom/get-offset-position event)
(gpt/transform modifier2))
viewport-point (dom/get-offset-position event)
viewport-point (-> viewport-point (update :x #(/ % zoom)) (update :y #(/ % zoom)))
position (gpt/transform viewport-point modifier2)
params {:position position
:page-id (:id page)
:file-id (:id file)}]
@@ -140,14 +136,16 @@
[:div.viewer-comments-container
[:div.threads
(for [item threads]
[:& cmt/thread-bubble {:thread item
:zoom zoom
:on-click on-bubble-click
:open? (= (:id item) (:open cstate))
:key (:seqn item)}])
(let [item (update item :position gpt/transform modifier1)]
[:& cmt/thread-bubble {:thread item
:zoom zoom
:on-click on-bubble-click
:open? (= (:id item) (:open cstate))
:key (:seqn item)}]))
(when-let [id (:open cstate)]
(when-let [thread (get threads-map id)]
(when-let [thread (-> (get threads-map id)
(update :position gpt/transform modifier1))]
[:& cmt/thread-comments {:thread thread
:users users
:zoom zoom}]))

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.viewer.handoff.attributes.layout
(:require
[app.common.math :as mth]
[app.common.types.radius :as ctr]
[app.main.ui.components.copy-button :refer [copy-button]]
[app.util.code-gen :as cg]
[app.util.i18n :refer [t]]
@@ -58,20 +59,17 @@
[:div.attributes-value (mth/precision y 2) "px"]
[:& copy-button {:data (copy-data selrect :y)}]])
(when (and (:rx shape) (not= (:rx shape) 0))
(when (ctr/radius-1? shape)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
[:& copy-button {:data (copy-data shape :rx)}]])
(when (and (:r1 shape)
(or (not= (:r1 shape) 0)
(not= (:r2 shape) 0)
(not= (:r3 shape) 0)
(not= (:r4 shape) 0)))
(when (ctr/radius-4? shape)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
[:div.attributes-value
(mth/precision (:r1 shape) 2) ", "
(mth/precision (:r2 shape) 2) ", "
(mth/precision (:r3 shape) 2) ", "
(mth/precision (:r4 shape) 2) "px"]

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