Compare commits
78 Commits
1.9.0-alph
...
1.10.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad34ab5c8 | ||
|
|
33c7847dfc | ||
|
|
7a04f15710 | ||
|
|
b8043a2432 | ||
|
|
ed5de525aa | ||
|
|
8105d9388b | ||
|
|
8151dcc05f | ||
|
|
25b1c5fe90 | ||
|
|
ea218839e4 | ||
|
|
4c18a1881b | ||
|
|
0bdbbd35e3 | ||
|
|
401afe7c1a | ||
|
|
c5adeecd90 | ||
|
|
da6c62414b | ||
|
|
6650fe863f | ||
|
|
76c00c42b5 | ||
|
|
f8609419a1 | ||
|
|
250e79eda1 | ||
|
|
f7401daeae | ||
|
|
7390e372e0 | ||
|
|
239c521ad9 | ||
|
|
77b4f09cfb | ||
|
|
bb178af278 | ||
|
|
3c39661174 | ||
|
|
1fffc1e828 | ||
|
|
ed50cd1fa8 | ||
|
|
ef6a02e8ef | ||
|
|
e7003dde83 | ||
|
|
bf2a393fd3 | ||
|
|
bb2cfd52f4 | ||
|
|
6a6f88c6ef | ||
|
|
0a2b1a4fbe | ||
|
|
5fd48c9e98 | ||
|
|
022d32cd44 | ||
|
|
af10cf71db | ||
|
|
1bf1de8ce8 | ||
|
|
b80ddfa580 | ||
|
|
aa276ab308 | ||
|
|
f50943d470 | ||
|
|
959c998664 | ||
|
|
b6b6b6043c | ||
|
|
8e0807d502 | ||
|
|
78d027b25e | ||
|
|
503f0bee69 | ||
|
|
50d756b189 | ||
|
|
7c3d71e572 | ||
|
|
bf895d26b0 | ||
|
|
5530e8581a | ||
|
|
f913816d87 | ||
|
|
3d59d31b0a | ||
|
|
9a66f26bd9 | ||
|
|
d5b6605ce8 | ||
|
|
38e5184be4 | ||
|
|
369ec9f814 | ||
|
|
620b454c49 | ||
|
|
2e5040e65d | ||
|
|
71fe7ef125 | ||
|
|
e0e8fd7ddc | ||
|
|
01b4b4933e | ||
|
|
fced22bc60 | ||
|
|
898ae64a57 | ||
|
|
8d50852cbe | ||
|
|
a11c7b10ac | ||
|
|
fe9033b8be | ||
|
|
e26f9e4a71 | ||
|
|
c477328da4 | ||
|
|
214c64c49e | ||
|
|
bce0e9194c | ||
|
|
a0f98e3823 | ||
|
|
bff6768adf | ||
|
|
8ce2eb448c | ||
|
|
7c5d00f8a4 | ||
|
|
30cd499014 | ||
|
|
99d173789e | ||
|
|
ae72db8129 | ||
|
|
9437cc1806 | ||
|
|
0e76aa0265 | ||
|
|
756e654d32 |
@@ -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:
|
||||
|
||||
@@ -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}))
|
||||
|
||||
|
||||
|
||||
49
CHANGES.md
@@ -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
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -1 +1 @@
|
||||
[PENPOT FEEDBACK]: {{subject|abbreviate:19}} (from {{email}})
|
||||
[PENPOT FEEDBACK]: {{subject}}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)))))
|
||||
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"}))
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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})]
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;")
|
||||
|
||||
|
||||
43
backend/src/app/util/retry.clj
Normal 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))
|
||||
|
||||
36
backend/src/app/util/rlimit.clj
Normal 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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))))))
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
90
common/src/app/common/types/radius.cljc
Normal 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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/resources/images/beta-on.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
1
frontend/resources/images/on-solo-hover.svg
Normal 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 |
1
frontend/resources/images/on-solo.svg
Normal 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 |
1
frontend/resources/images/on-teamup-hover.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
frontend/resources/images/on-teamup.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
frontend/resources/images/ph-file.svg
Normal 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 |
1
frontend/resources/images/ph-left.svg
Normal 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 |
1
frontend/resources/images/ph-right.svg
Normal 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 |
@@ -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 {
|
||||
|
||||
@@ -46,7 +46,7 @@ $width-settings-bar: 16rem;
|
||||
}
|
||||
|
||||
.handoff-layout {
|
||||
.viewer-preview {
|
||||
.viewer-section {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.settings-bar {
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
.viewer-comments-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
height: 48px;
|
||||
padding: 0 $size-4 0 55px;
|
||||
position: relative;
|
||||
z-index: 12;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 12;
|
||||
z-index: 10;
|
||||
|
||||
&.invisible {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,6 +120,6 @@
|
||||
[:*
|
||||
[:& header {:team team :project project}]
|
||||
[:section.dashboard-container
|
||||
[:& grid {:project-id (:id project)
|
||||
[:& grid {:project project
|
||||
:files files}]]]))
|
||||
|
||||
|
||||
@@ -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)}])]))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
34
frontend/src/app/main/ui/dashboard/placeholder.cljs
Normal 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")]])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}]))]])))
|
||||
|
||||
|
||||
@@ -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")])]))
|
||||
|
||||
|
||||
|
||||
@@ -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")])]]]]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))
|
||||
|
||||
31
frontend/src/app/main/ui/releases/v1_10.cljs
Normal 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 "Penpot’s officially beta!"]
|
||||
[:p "We carefully analyzed everything important to us before taking this step. And now we’re ready to move forward onto the beta version. Have a play around if you haven’t 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"}]]]]))
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}])))
|
||||
|
||||
|
||||
@@ -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]}]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")]]]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}]))
|
||||
|
||||
@@ -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"]
|
||||
|
||||