mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eee1cdbf3 | ||
|
|
54c506100d | ||
|
|
7c64ed84f1 | ||
|
|
5fef0b64f4 | ||
|
|
3294058e16 | ||
|
|
5e8c164a44 | ||
|
|
3472359168 | ||
|
|
1b17742fc3 | ||
|
|
98038b10a0 | ||
|
|
91ca55742a | ||
|
|
e83c90203e | ||
|
|
b4a7a15045 | ||
|
|
b847754e3b | ||
|
|
9c5f7373de | ||
|
|
3e8c665b7f | ||
|
|
cf6bea1974 | ||
|
|
b8bff31aca | ||
|
|
54c4e9af6c | ||
|
|
07d859f9bd | ||
|
|
d008d82a11 | ||
|
|
abff7d324d | ||
|
|
39613944bb | ||
|
|
9194e257b6 | ||
|
|
b54b99becf | ||
|
|
b357cf505d | ||
|
|
632165d6dc | ||
|
|
4d463537dd | ||
|
|
ae6cb551cb | ||
|
|
0f181df767 | ||
|
|
8d104de41c | ||
|
|
a94f1d6fe4 | ||
|
|
c616e3c932 | ||
|
|
bbb64b8be9 | ||
|
|
2c3ae851ea | ||
|
|
7117ea1f7e | ||
|
|
0fc7d8529e | ||
|
|
a8fae53564 | ||
|
|
23bd57e9bb | ||
|
|
11f2d7e711 | ||
|
|
3a71068a48 | ||
|
|
bebdc78ce6 | ||
|
|
22939aa689 | ||
|
|
6901acb37e | ||
|
|
e0fe7181f1 | ||
|
|
a65282c01b | ||
|
|
5b35176584 | ||
|
|
857429290d | ||
|
|
3c842d2b81 | ||
|
|
38fd385d5d | ||
|
|
9bf5b1a8cd | ||
|
|
7c80c605d1 | ||
|
|
0cb2e6d07d | ||
|
|
d907812513 | ||
|
|
d8bf48e49e | ||
|
|
b667f1bb2c | ||
|
|
8e9fa66e2f | ||
|
|
17fb5283cc | ||
|
|
03c9f6b1e1 | ||
|
|
908229b7a8 | ||
|
|
2a752e3625 | ||
|
|
c6fabc349e | ||
|
|
3ea3923751 | ||
|
|
293ab3c80e | ||
|
|
89fa8ce66e | ||
|
|
3bf5648b5b | ||
|
|
57346ab685 | ||
|
|
8bd9c0d031 | ||
|
|
88f46f2ab2 | ||
|
|
edd91f00af | ||
|
|
16fa0b0330 | ||
|
|
c975e0bcee | ||
|
|
25001e5b80 | ||
|
|
c3a0db2431 | ||
|
|
e27c0b2086 | ||
|
|
ec8c847440 | ||
|
|
380c77a704 | ||
|
|
caaf695352 | ||
|
|
56f4348586 | ||
|
|
56ba32b66d | ||
|
|
4dacba6836 | ||
|
|
ddfe5fbcb8 | ||
|
|
7948f565e3 | ||
|
|
2bca2b005e | ||
|
|
4cb57c9748 | ||
|
|
bb76700c18 | ||
|
|
33bdf5e83f | ||
|
|
f0eff95e18 | ||
|
|
2a6b9f06b3 | ||
|
|
f531a5c323 | ||
|
|
acc1fac8de | ||
|
|
36e66c4dd9 | ||
|
|
8c2038e43b | ||
|
|
0135b477ca | ||
|
|
8bf1b9c28e | ||
|
|
002772ff0e | ||
|
|
4838571ec2 | ||
|
|
8e71d219ca | ||
|
|
cbac4587cf | ||
|
|
e636bdd0b0 | ||
|
|
a7a3344030 | ||
|
|
137e576e63 |
30
CHANGES.md
30
CHANGES.md
@@ -1,5 +1,33 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.0.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix chrome scrollbar styling [Taiga Issue #7852](https://tree.taiga.io/project/penpot/issue/7852)
|
||||
- Fix incorrect password encoding on create-profile manage scritp [Github #3651](https://github.com/penpot/penpot/issues/3651)
|
||||
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### :sparkles: Enhancements
|
||||
|
||||
- Fix locking contention on cron subsystem (causes backend start blocking)
|
||||
- Fix locking contention on file object thumbails backend RPC calls
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458)
|
||||
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
|
||||
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
||||
|
||||
|
||||
## 2.0.0 - I Just Can't Get Enough
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -14,6 +42,8 @@
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
token (tokens/generate (::setup/props cfg) info)
|
||||
params (d/without-nils
|
||||
{:token token
|
||||
:provider (:provider (:path-params request))
|
||||
:fullname (:fullname info)})
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
|
||||
@@ -113,8 +113,7 @@
|
||||
(s/def ::worker-default-parallelism ::us/integer)
|
||||
(s/def ::worker-webhook-parallelism ::us/integer)
|
||||
|
||||
(s/def ::authenticated-cookie-domain ::us/string)
|
||||
(s/def ::authenticated-cookie-name ::us/string)
|
||||
(s/def ::auth-data-cookie-domain ::us/string)
|
||||
(s/def ::auth-token-cookie-name ::us/string)
|
||||
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
||||
|
||||
@@ -222,7 +221,6 @@
|
||||
::audit-log-http-handler-concurrency
|
||||
::auth-token-cookie-name
|
||||
::auth-token-cookie-max-age
|
||||
::authenticated-cookie-name
|
||||
::authenticated-cookie-domain
|
||||
::database-password
|
||||
::database-uri
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
;; A cookie that we can use to check from other sites of the same
|
||||
;; domain if a user is authenticated.
|
||||
(def default-authenticated-cookie-name "authenticated")
|
||||
(def default-auth-data-cookie-name "auth-data")
|
||||
|
||||
;; Default value for cookie max-age
|
||||
(def default-cookie-max-age (dt/duration {:days 7}))
|
||||
@@ -133,9 +134,9 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare ^:private assign-auth-token-cookie)
|
||||
(declare ^:private assign-authenticated-cookie)
|
||||
(declare ^:private assign-auth-data-cookie)
|
||||
(declare ^:private clear-auth-token-cookie)
|
||||
(declare ^:private clear-authenticated-cookie)
|
||||
(declare ^:private clear-auth-data-cookie)
|
||||
(declare ^:private gen-token)
|
||||
|
||||
(defn create-fn
|
||||
@@ -153,7 +154,7 @@
|
||||
(l/trace :hint "create" :profile-id (str profile-id))
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-authenticated-cookie session)))))
|
||||
(assign-auth-data-cookie session)))))
|
||||
|
||||
(defn delete-fn
|
||||
[{:keys [::manager]}]
|
||||
@@ -167,7 +168,7 @@
|
||||
(assoc :status 204)
|
||||
(assoc :body nil)
|
||||
(clear-auth-token-cookie)
|
||||
(clear-authenticated-cookie)))))
|
||||
(clear-auth-data-cookie)))))
|
||||
|
||||
(defn- gen-token
|
||||
[props {:keys [profile-id created-at]}]
|
||||
@@ -229,7 +230,7 @@
|
||||
(let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-authenticated-cookie session)))
|
||||
(assign-auth-data-cookie session)))
|
||||
response))))
|
||||
|
||||
(def soft-auth
|
||||
@@ -262,11 +263,11 @@
|
||||
:secure secure?}]
|
||||
(update response :cookies assoc name cookie)))
|
||||
|
||||
(defn- assign-authenticated-cookie
|
||||
[response {updated-at :updated-at}]
|
||||
(defn- assign-auth-data-cookie
|
||||
[response {profile-id :profile-id updated-at :updated-at}]
|
||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||
domain (cf/get :authenticated-cookie-domain)
|
||||
cname (cf/get :authenticated-cookie-name "authenticated")
|
||||
domain (cf/get :auth-data-cookie-domain)
|
||||
cname default-auth-data-cookie-name
|
||||
|
||||
created-at (or updated-at (dt/now))
|
||||
renewal (dt/plus created-at default-renewal-max-age)
|
||||
@@ -274,14 +275,17 @@
|
||||
|
||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
strict? (contains? cf/flags :strict-session-cookies)
|
||||
cors? (contains? cf/flags :cors)
|
||||
|
||||
cookie {:domain domain
|
||||
:expires expires
|
||||
:path "/"
|
||||
:comment comment
|
||||
:value true
|
||||
:same-site :strict
|
||||
:value (u/map->query-string {:profile-id profile-id})
|
||||
:same-site (if cors? :none (if strict? :strict :lax))
|
||||
:secure secure?}]
|
||||
|
||||
(cond-> response
|
||||
(string? domain)
|
||||
(update :cookies assoc cname cookie))))
|
||||
@@ -291,10 +295,10 @@
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||
|
||||
(defn- clear-authenticated-cookie
|
||||
(defn- clear-auth-data-cookie
|
||||
[response]
|
||||
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
|
||||
domain (cf/get :authenticated-cookie-domain)]
|
||||
(let [cname default-auth-data-cookie-name
|
||||
domain (cf/get :auth-data-cookie-domain)]
|
||||
(cond-> response
|
||||
(string? domain)
|
||||
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
|
||||
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
|
||||
|
||||
:object-update
|
||||
(ig/ref :app.tasks.object-update/handler)
|
||||
:process-webhook-event
|
||||
(ig/ref ::webhooks/process-event-handler)
|
||||
:run-webhook
|
||||
@@ -376,7 +378,10 @@
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.orphan-teams-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.object-update/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.file-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
row_number() OVER (ORDER BY created_at DESC) AS rown
|
||||
FROM team
|
||||
WHERE deleted_at IS NULL
|
||||
AND (features <@ '{components/v2}' OR features IS NULL)
|
||||
AND (not (features @> '{components/v2}') OR features IS NULL)
|
||||
ORDER BY created_at DESC")
|
||||
|
||||
(defn- get-teams
|
||||
@@ -37,7 +37,7 @@
|
||||
;; Run teams migration
|
||||
(run! (fn [{:keys [id rown]}]
|
||||
(try
|
||||
(-> (assoc system ::db/rollback true)
|
||||
(-> (assoc system ::db/rollback false)
|
||||
(feat/migrate-team! id
|
||||
:rown rown
|
||||
:label "v2-migration"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
@@ -91,67 +92,77 @@
|
||||
:timeout (:timeout config)
|
||||
:type :semaphore))
|
||||
|
||||
(defmacro ^:private measure-and-log!
|
||||
[metrics mlabels stats id action limit-id limit-label profile-id elapsed]
|
||||
`(let [mpermits# (:max-permits ~stats)
|
||||
mqueue# (:max-queue ~stats)
|
||||
permits# (:permits ~stats)
|
||||
queue# (:queue ~stats)
|
||||
queue# (- queue# mpermits#)
|
||||
queue# (if (neg? queue#) 0 queue#)
|
||||
level# (if (pos? queue#) :warn :trace)]
|
||||
|
||||
(mtx/run! ~metrics
|
||||
:id :rpc-climit-queue
|
||||
:val queue#
|
||||
:labels ~mlabels)
|
||||
(defn measure!
|
||||
[metrics mlabels stats elapsed]
|
||||
(let [mpermits (:max-permits stats)
|
||||
permits (:permits stats)
|
||||
queue (:queue stats)
|
||||
queue (- queue mpermits)
|
||||
queue (if (neg? queue) 0 queue)]
|
||||
|
||||
(mtx/run! ~metrics
|
||||
:id :rpc-climit-permits
|
||||
:val permits#
|
||||
:labels ~mlabels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-queue
|
||||
:val queue
|
||||
:labels mlabels)
|
||||
|
||||
(l/log level#
|
||||
:hint ~action
|
||||
:req ~id
|
||||
:id ~limit-id
|
||||
:label ~limit-label
|
||||
:profile-id (str ~profile-id)
|
||||
:permits permits#
|
||||
:queue queue#
|
||||
:max-permits mpermits#
|
||||
:max-queue mqueue#
|
||||
~@(if (some? elapsed)
|
||||
[:elapsed `(dt/format-duration ~elapsed)]
|
||||
[]))))
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-permits
|
||||
:val permits
|
||||
:labels mlabels)
|
||||
|
||||
(when elapsed
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val (inst-ms elapsed)
|
||||
:labels mlabels))))
|
||||
|
||||
(defn log!
|
||||
[action req-id stats limit-id limit-label params elapsed]
|
||||
(let [mpermits (:max-permits stats)
|
||||
queue (:queue stats)
|
||||
queue (- queue mpermits)
|
||||
queue (if (neg? queue) 0 queue)
|
||||
level (if (pos? queue) :warn :trace)]
|
||||
|
||||
(l/log level
|
||||
:hint action
|
||||
:req req-id
|
||||
:id limit-id
|
||||
:label limit-label
|
||||
:queue queue
|
||||
:elapsed (some-> elapsed dt/format-duration)
|
||||
:params (-> (select-keys params [::rpc/profile-id :file-id :profile-id])
|
||||
(set/rename-keys {::rpc/profile-id :profile-id})
|
||||
(update-vals str)))))
|
||||
|
||||
(def ^:private idseq (AtomicLong. 0))
|
||||
|
||||
(defn- invoke
|
||||
[limiter metrics limit-id limit-key limit-label profile-id f params]
|
||||
[limiter metrics limit-id limit-key limit-label handler params]
|
||||
(let [tpoint (dt/tpoint)
|
||||
mlabels (into-array String [(id->str limit-id)])
|
||||
limit-id (id->str limit-id limit-key)
|
||||
stats (pbh/get-stats limiter)
|
||||
id (.incrementAndGet ^AtomicLong idseq)]
|
||||
req-id (.incrementAndGet ^AtomicLong idseq)]
|
||||
|
||||
(try
|
||||
(measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil)
|
||||
(measure! metrics mlabels stats nil)
|
||||
(log! "enqueued" req-id stats limit-id limit-label params nil)
|
||||
(px/invoke! limiter (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val (inst-ms elapsed)
|
||||
:labels mlabels)
|
||||
(apply f params))))
|
||||
|
||||
(measure! metrics mlabels stats elapsed)
|
||||
(log! "acquired" req-id stats limit-id limit-label params elapsed)
|
||||
|
||||
(handler params))))
|
||||
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(if (= :bulkhead-error type)
|
||||
(let [elapsed (tpoint)]
|
||||
(measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed)
|
||||
(log! "rejected" req-id stats limit-id limit-label params elapsed)
|
||||
(ex/raise :type :concurrency-limit
|
||||
:code code
|
||||
:hint "concurrency limit reached"
|
||||
@@ -161,7 +172,9 @@
|
||||
(finally
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed))))))
|
||||
|
||||
(measure! metrics mlabels stats nil)
|
||||
(log! "finished" req-id stats limit-id limit-label params elapsed))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MIDDLEWARE
|
||||
@@ -219,10 +232,8 @@
|
||||
(let [limit-key (key-fn params)
|
||||
cache-key [limit-id limit-key]
|
||||
limiter (cache/get cache cache-key (partial create-limiter config))
|
||||
profile-id (if (= key-fn ::rpc/profile-id)
|
||||
limit-key
|
||||
(get params ::rpc/profile-id))]
|
||||
(invoke limiter metrics limit-id limit-key label profile-id handler [cfg params])))))
|
||||
handler (partial handler cfg)]
|
||||
(invoke limiter metrics limit-id limit-key label handler params)))))
|
||||
|
||||
(do
|
||||
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
|
||||
@@ -237,15 +248,15 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- build-exec-chain
|
||||
[{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f]
|
||||
[{:keys [::label ::rpc/climit ::mtx/metrics] :as cfg} f]
|
||||
(let [config (get climit ::config)
|
||||
cache (get climit ::cache)]
|
||||
(reduce (fn [handler [limit-id limit-key :as ckey]]
|
||||
(if-let [config (get config limit-id)]
|
||||
(fn [& params]
|
||||
(let [limiter (cache/get cache ckey (partial create-limiter config))]
|
||||
(invoke limiter metrics limit-id limit-key label profile-id handler params)))
|
||||
|
||||
(fn [cfg params]
|
||||
(let [limiter (cache/get cache ckey (partial create-limiter config))
|
||||
handler (partial handler cfg)]
|
||||
(invoke limiter metrics limit-id limit-key label handler params)))
|
||||
(do
|
||||
(l/wrn :hint "config not found" :label label :id limit-id)
|
||||
f)))
|
||||
@@ -255,9 +266,9 @@
|
||||
(defn invoke!
|
||||
"Run a function in context of climit.
|
||||
Intended to be used in virtual threads."
|
||||
[{:keys [::executor] :as cfg} f & params]
|
||||
[{:keys [::executor] :as cfg} f params]
|
||||
(let [f (if (some? executor)
|
||||
(fn [& params] (px/await! (px/submit! executor (fn [] (apply f params)))))
|
||||
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
|
||||
f)
|
||||
f (build-exec-chain cfg f)]
|
||||
(apply f params)))
|
||||
(f cfg params)))
|
||||
|
||||
@@ -77,10 +77,19 @@
|
||||
(when (seq events)
|
||||
(db/insert-many! pool :audit-log event-columns events))))
|
||||
|
||||
(def valid-event-types
|
||||
#{"action" "identify"})
|
||||
|
||||
(def schema:event
|
||||
[:map {:title "Event"}
|
||||
[:name [:string {:max 250}]]
|
||||
[:type [:string {:max 250}]]
|
||||
[:name
|
||||
[:and {:gen/elements ["update-file", "get-profile"]}
|
||||
[:string {:max 250}]
|
||||
[:re #"[\d\w-]{1,50}"]]]
|
||||
[:type
|
||||
[:and {:gen/elements valid-event-types}
|
||||
[:string {:max 250}]
|
||||
[::sm/one-of {:format "string"} valid-event-types]]]
|
||||
[:props
|
||||
[:map-of :keyword :any]]
|
||||
[:context {:optional true}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.db.sql :as sql]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-create :as files.create]
|
||||
@@ -23,6 +24,7 @@
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
@@ -100,7 +102,9 @@
|
||||
:revn revn
|
||||
:data nil
|
||||
:changes (blob/encode changes)})
|
||||
nil)))
|
||||
(rph/with-meta (rph/wrap nil)
|
||||
{::audit/replace-props {:file-id id
|
||||
:revn revn}}))))
|
||||
|
||||
;; --- MUTATION COMMAND: persist-temp-file
|
||||
|
||||
|
||||
@@ -228,51 +228,52 @@
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; MUTATION COMMAND: create-file-object-thumbnail
|
||||
(def sql:get-file-object-thumbnail
|
||||
"SELECT * FROM file_tagged_object_thumbnail
|
||||
WHERE file_id = ? AND object_id = ? AND tag = ?
|
||||
FOR UPDATE")
|
||||
|
||||
(defn- create-file-object-thumbnail!
|
||||
[{:keys [::db/conn ::sto/storage]} file-id object-id media tag]
|
||||
(def sql:create-file-object-thumbnail
|
||||
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (file_id, object_id, tag)
|
||||
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
|
||||
RETURNING *")
|
||||
|
||||
(let [thumb (db/get* conn :file-tagged-object-thumbnail
|
||||
{:file-id file-id
|
||||
:object-id object-id
|
||||
:tag tag}
|
||||
{::db/remove-deleted false
|
||||
::sql/for-update true})
|
||||
|
||||
path (:path media)
|
||||
(defn- persist-thumbnail!
|
||||
[storage media created-at]
|
||||
(let [path (:path media)
|
||||
mtype (:mtype media)
|
||||
hash (sto/calculate-hash path)
|
||||
data (-> (sto/content path)
|
||||
(sto/wrap-with-hash hash))
|
||||
tnow (dt/now)
|
||||
(sto/wrap-with-hash hash))]
|
||||
|
||||
media (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at tnow
|
||||
:content-type mtype
|
||||
:bucket "file-object-thumbnail"})]
|
||||
(sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at created-at
|
||||
:content-type mtype
|
||||
:bucket "file-object-thumbnail"})))
|
||||
|
||||
(if (some? thumb)
|
||||
(do
|
||||
;; We mark the old media id as touched if it does not matches
|
||||
(when (not= (:id media) (:media-id thumb))
|
||||
(sto/touch-object! storage (:media-id thumb)))
|
||||
(db/update! conn :file-tagged-object-thumbnail
|
||||
{:media-id (:id media)
|
||||
:deleted-at nil
|
||||
:updated-at tnow}
|
||||
{:file-id file-id
|
||||
:object-id object-id
|
||||
:tag tag}))
|
||||
(db/insert! conn :file-tagged-object-thumbnail
|
||||
{:file-id file-id
|
||||
:object-id object-id
|
||||
:created-at tnow
|
||||
:updated-at tnow
|
||||
:tag tag
|
||||
:media-id (:id media)}))))
|
||||
|
||||
|
||||
(defn- create-file-object-thumbnail!
|
||||
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
|
||||
(let [tsnow (dt/now)
|
||||
media (persist-thumbnail! storage media tsnow)
|
||||
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
|
||||
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
|
||||
file-id object-id tag (:id media)
|
||||
tsnow (:id media)])]
|
||||
[th1 th2])))]
|
||||
|
||||
(when (and (some? th1)
|
||||
(not= (:media-id th1)
|
||||
(:media-id th2)))
|
||||
(sto/touch-object! storage (:media-id th1) :async true))
|
||||
|
||||
th2))
|
||||
|
||||
(def ^:private
|
||||
schema:create-file-object-thumbnail
|
||||
@@ -296,16 +297,10 @@
|
||||
(media/validate-media-type! media)
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(when-not (db/read-only? conn)
|
||||
(let [cfg (-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 5)
|
||||
(assoc ::rtry/label "create-file-object-thumbnail"))]
|
||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))))))
|
||||
(db/run! cfg files/check-edition-permissions! profile-id file-id)
|
||||
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
|
||||
|
||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||
|
||||
|
||||
@@ -243,12 +243,13 @@
|
||||
;; NOTE: we use the climit here in a dynamic invocation because we
|
||||
;; don't want saturate the process-image limit with IO (download
|
||||
;; of external image)
|
||||
|
||||
(-> cfg
|
||||
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
|
||||
[:process-image/global]])
|
||||
(assoc ::climit/profile-id (:profile-id params))
|
||||
(assoc ::climit/label "create-file-media-object-from-url")
|
||||
(climit/invoke! db/run! cfg create-file-media-object params))))
|
||||
(climit/invoke! #(db/run! %1 create-file-media-object %2) params))))
|
||||
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
(let [email (str/lower email)
|
||||
email (if (str/starts-with? email "mailto:")
|
||||
(subs email 7)
|
||||
email)
|
||||
email (if (or (str/starts-with? email "<")
|
||||
(str/ends-with? email ">"))
|
||||
(str/trim email "<>")
|
||||
email)]
|
||||
email))
|
||||
|
||||
@@ -233,7 +237,7 @@
|
||||
:file-mtype (:mtype file)}}))))
|
||||
|
||||
(defn- generate-thumbnail!
|
||||
[file]
|
||||
[_ file]
|
||||
(let [input (media/run {:cmd :info :input file})
|
||||
thumb (media/run {:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
@@ -250,15 +254,15 @@
|
||||
:content-type (:mtype thumb)}))
|
||||
|
||||
(defn upload-photo
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}]
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
|
||||
(let [params (-> cfg
|
||||
(assoc ::climit/id :process-image/global)
|
||||
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
|
||||
[:process-image/global]])
|
||||
(assoc ::climit/label "upload-photo")
|
||||
(assoc ::climit/executor executor)
|
||||
(climit/invoke! generate-thumbnail! file))]
|
||||
(sto/put-object! storage params)))
|
||||
|
||||
|
||||
;; --- MUTATION: Request Email Change
|
||||
|
||||
(declare ^:private request-email-change!)
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
team (-> (db/get conn :team {:id (:team-id project)})
|
||||
(teams/decode-row))
|
||||
|
||||
members (into #{} (->> (teams/get-team-members conn (:team-id project))
|
||||
(map :id)))
|
||||
|
||||
perms (assoc perms :in-team (contains? members profile-id))
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -37,12 +38,13 @@
|
||||
:or {is-active true}}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn))))))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.s3 :as ss3]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
@@ -170,15 +171,28 @@
|
||||
(impl/put-object object content))
|
||||
object)))
|
||||
|
||||
(def ^:private default-touch-delay
|
||||
"A default delay for the asynchronous touch operation"
|
||||
(dt/duration "5m"))
|
||||
|
||||
(defn touch-object!
|
||||
"Mark object as touched."
|
||||
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||
[{:keys [::db/pool-or-conn] :as storage} object-or-id & {:keys [async]}]
|
||||
(us/assert! ::storage storage)
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
||||
rs (db/update! pool-or-conn :storage-object
|
||||
{:touched-at (dt/now)}
|
||||
{:id id})]
|
||||
(pos? (db/get-update-count rs))))
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
|
||||
(if async
|
||||
(wrk/submit! ::wrk/conn pool-or-conn
|
||||
::wrk/task :object-update
|
||||
::wrk/delay default-touch-delay
|
||||
:object :storage-object
|
||||
:id id
|
||||
:key :touched-at
|
||||
:val (dt/now))
|
||||
(-> (db/update! pool-or-conn :storage-object
|
||||
{:touched-at (dt/now)}
|
||||
{:id id})
|
||||
(db/get-update-count)
|
||||
(pos?)))))
|
||||
|
||||
(defn get-object-data
|
||||
"Return an input stream instance of the object content."
|
||||
|
||||
32
backend/src/app/tasks/object_update.clj
Normal file
32
backend/src/app/tasks/object_update.clj
Normal file
@@ -0,0 +1,32 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.tasks.object-update
|
||||
"A task used for perform simple object properties update
|
||||
in an asynchronous flow."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- update-object
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id object key val] :as props}]
|
||||
(l/trc :hint "update object prop"
|
||||
:id (str id)
|
||||
:object (d/name object)
|
||||
:key (d/name key)
|
||||
:val val)
|
||||
(db/update! conn object {key val} {:id id} {::db/return-keys false}))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as params}]
|
||||
(db/tx-run! cfg update-object props)))
|
||||
@@ -119,11 +119,13 @@
|
||||
:next.jdbc/update-count))]
|
||||
(l/trc :hint "submit task"
|
||||
:name task
|
||||
:task-id (str id)
|
||||
:queue queue
|
||||
:label label
|
||||
:dedupe (boolean dedupe)
|
||||
:deleted (or deleted 0)
|
||||
:in (dt/format-duration duration))
|
||||
:delay (dt/format-duration duration)
|
||||
:replace (or deleted 0))
|
||||
|
||||
|
||||
(db/exec-one! conn [sql:insert-new-task id task props queue
|
||||
label priority max-retries interval])
|
||||
|
||||
@@ -27,14 +27,15 @@
|
||||
"insert into scheduled_task (id, cron_expr)
|
||||
values (?, ?)
|
||||
on conflict (id)
|
||||
do update set cron_expr=?")
|
||||
do nothing")
|
||||
|
||||
(defn- synchronize-cron-entries!
|
||||
[{:keys [::db/pool ::entries]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(doseq [{:keys [id cron]} entries]
|
||||
(l/trc :hint "register cron task" :id id :cron (str cron))
|
||||
(db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)]))))
|
||||
[{:keys [::db/conn ::entries]}]
|
||||
(doseq [{:keys [id cron]} entries]
|
||||
(let [result (db/exec-one! conn [sql:upsert-cron-task id (str cron)])
|
||||
updated? (pos? (db/get-update-count result))]
|
||||
(l/dbg :hint "register task" :id id :cron (str cron)
|
||||
:status (if updated? "created" "exists")))))
|
||||
|
||||
(defn- lock-scheduled-task!
|
||||
[conn id]
|
||||
@@ -45,7 +46,7 @@
|
||||
(declare ^:private schedule-cron-task)
|
||||
|
||||
(defn- execute-cron-task
|
||||
[cfg {:keys [id] :as task}]
|
||||
[cfg {:keys [id cron] :as task}]
|
||||
(px/thread
|
||||
{:name (str "penpot/cron-task/" id)}
|
||||
(let [tpoint (dt/tpoint)]
|
||||
@@ -54,20 +55,25 @@
|
||||
(db/exec-one! conn ["SET LOCAL statement_timeout=0;"])
|
||||
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"])
|
||||
(when (lock-scheduled-task! conn id)
|
||||
(l/dbg :hint "start task" :task-id id)
|
||||
(db/update! conn :scheduled-task
|
||||
{:cron-expr (str cron)
|
||||
:modified-at (dt/now)}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
(l/dbg :hint "start" :id id)
|
||||
((:fn task) task)
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/dbg :hint "end task" :task-id id :elapsed elapsed)))))
|
||||
(l/dbg :hint "end" :id id :elapsed elapsed)))))
|
||||
|
||||
(catch InterruptedException _
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/debug :hint "task interrupted" :task-id id :elapsed elapsed)))
|
||||
(l/debug :hint "task interrupted" :id id :elapsed elapsed)))
|
||||
|
||||
(catch Throwable cause
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(binding [l/*context* (get-error-context cause task)]
|
||||
(l/err :hint "unhandled exception on running task"
|
||||
:task-id id
|
||||
:id id
|
||||
:elapsed elapsed
|
||||
:cause cause))))
|
||||
(finally
|
||||
@@ -86,7 +92,7 @@
|
||||
(let [ts (ms-until-valid cron)
|
||||
ft (px/schedule! ts (partial execute-cron-task cfg task))]
|
||||
|
||||
(l/dbg :hint "schedule task" :task-id id
|
||||
(l/dbg :hint "schedule" :id id
|
||||
:ts (dt/format-duration ts)
|
||||
:at (dt/format-instant (dt/in-future ts)))
|
||||
|
||||
@@ -135,7 +141,8 @@
|
||||
cfg (assoc cfg ::entries entries ::running running)]
|
||||
|
||||
(l/inf :hint "started" :tasks (count entries))
|
||||
(synchronize-cron-entries! cfg)
|
||||
|
||||
(db/tx-run! cfg synchronize-cron-entries!)
|
||||
|
||||
(->> (filter some? entries)
|
||||
(run! (partial schedule-cron-task cfg)))
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
:else
|
||||
(try
|
||||
(l/trc :hint "start task"
|
||||
(l/dbg :hint "start"
|
||||
:name (:name task)
|
||||
:task-id (str task-id)
|
||||
:queue queue
|
||||
@@ -149,7 +149,7 @@
|
||||
result (handle-task task)
|
||||
elapsed (dt/format-duration (tpoint))]
|
||||
|
||||
(l/trc :hint "end task"
|
||||
(l/dbg :hint "end"
|
||||
:name (:name task)
|
||||
:task-id (str task-id)
|
||||
:queue queue
|
||||
@@ -228,9 +228,9 @@
|
||||
(recur))))
|
||||
|
||||
(catch InterruptedException _
|
||||
(l/debug :hint "interrupted"
|
||||
:id id
|
||||
:queue queue))
|
||||
(l/dbg :hint "interrupted"
|
||||
:id id
|
||||
:queue queue))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected exception"
|
||||
:id id
|
||||
|
||||
@@ -1158,7 +1158,7 @@
|
||||
;; check that the unknown frame thumbnail is deleted
|
||||
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||
(t/is (= 2 (count rows)))
|
||||
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
|
||||
(t/is (= 1 (count (remove :deleted-at rows)))))
|
||||
|
||||
(let [res (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (= 3 (:processed res))))
|
||||
|
||||
@@ -277,8 +277,6 @@
|
||||
(t/is (thrown? org.postgresql.util.PSQLException
|
||||
(th/db-delete! :storage-object {:id (:media-id row1)}))))))
|
||||
|
||||
|
||||
|
||||
(t/deftest get-file-object-thumbnail
|
||||
(let [storage (::sto/storage th/*system*)
|
||||
profile (th/create-profile* 1)
|
||||
@@ -317,3 +315,44 @@
|
||||
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result "test-key-2"))))))
|
||||
|
||||
(t/deftest create-file-object-thumbnail
|
||||
(th/db-delete! :task {:name "object-update"})
|
||||
(let [storage (::sto/storage th/*system*)
|
||||
profile (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})
|
||||
data {::th/type :create-file-object-thumbnail
|
||||
::rpc/profile-id (:id profile)
|
||||
:file-id (:id file)
|
||||
:object-id "test-key-2"
|
||||
:media {:filename "sample.jpg"
|
||||
:mtype "image/jpeg"}}]
|
||||
|
||||
(let [data (update data :media
|
||||
(fn [media]
|
||||
(-> media
|
||||
(assoc :path (th/tempfile "backend_tests/test_files/sample2.jpg"))
|
||||
(assoc :size 7923))))
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (map? (:result out))))
|
||||
|
||||
(let [data (update data :media
|
||||
(fn [media]
|
||||
(-> media
|
||||
(assoc :path (th/tempfile "backend_tests/test_files/sample.jpg"))
|
||||
(assoc :size 312043))))
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (map? (:result out))))
|
||||
|
||||
(let [[row1 :as rows]
|
||||
(->> (th/db-query :task {:name "object-update"})
|
||||
(map #(update % :props db/decode-transit-pgobject)))]
|
||||
|
||||
;; (app.common.pprint/pprint rows)
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (> (inst-ms (dt/diff (:created-at row1) (:scheduled-at row1)))
|
||||
(inst-ms (dt/duration "4m")))))))
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
|
||||
(t/deftest clean-email
|
||||
(t/is "foo@example.com" (profile/clean-email "mailto:foo@example.com"))
|
||||
(t/is "foo@example.com" (profile/clean-email "mailto:<foo@example.com>"))
|
||||
(t/is "foo@example.com" (profile/clean-email "<foo@example.com>"))
|
||||
(t/is "foo@example.com" (profile/clean-email "foo@example.com>"))
|
||||
(t/is "foo@example.com" (profile/clean-email "<foo@example.com")))
|
||||
|
||||
;; Test with wrong credentials
|
||||
(t/deftest profile-login-failed-1
|
||||
(let [profile (th/create-profile* 1)
|
||||
|
||||
@@ -274,6 +274,13 @@
|
||||
(catch #?(:clj Throwable :cljs :default) _cause
|
||||
[0 0 0])))
|
||||
|
||||
(defn hex->lum
|
||||
[color]
|
||||
(let [[r g b] (hex->rgb color)]
|
||||
(mth/sqrt (+ (* 0.241 r)
|
||||
(* 0.691 g)
|
||||
(* 0.068 b)))))
|
||||
|
||||
(defn- int->hex
|
||||
"Convert integer to hex string"
|
||||
[v]
|
||||
@@ -455,3 +462,19 @@
|
||||
|
||||
:else
|
||||
[r g (inc b)]))
|
||||
|
||||
(defn reduce-range
|
||||
[value range]
|
||||
(/ (mth/floor (* value range)) range))
|
||||
|
||||
(defn sort-colors
|
||||
[a b]
|
||||
(let [[ah _ av] (hex->hsv (:color a))
|
||||
[bh _ bv] (hex->hsv (:color b))
|
||||
ah (reduce-range (/ ah 60) 8)
|
||||
bh (reduce-range (/ bh 60) 8)
|
||||
av (/ av 255)
|
||||
bv (/ bv 255)
|
||||
a (+ (* ah 100) (* av 10))
|
||||
b (+ (* bh 100) (* bv 10))]
|
||||
(compare a b)))
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 46)
|
||||
(def version 48)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -898,6 +900,43 @@
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(defn migrate-up-47
|
||||
[data]
|
||||
(letfn [(fix-shape [page shape]
|
||||
(let [file {:id (:id data) :data data}
|
||||
component-file (:component-file shape)
|
||||
;; On cloning a file, the component-file of the shapes point to the old file id
|
||||
;; this is a workaround to be able to found the components in that case
|
||||
libraries {component-file {:id component-file :data data}}
|
||||
ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true})
|
||||
ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape))
|
||||
shape-swap-slot (ctk/get-swap-slot shape)
|
||||
ref-swap-slot (ctk/get-swap-slot ref-shape)]
|
||||
(if (and (some? shape-swap-slot)
|
||||
(= shape-swap-slot ref-swap-slot)
|
||||
(ctk/main-instance? ref-parent))
|
||||
(ctk/remove-swap-slot shape)
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals (partial fix-shape page)))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(defn migrate-up-48
|
||||
[data]
|
||||
(letfn [(fix-shape [shape]
|
||||
(let [swap-slot (ctk/get-swap-slot shape)]
|
||||
(if (and (some? swap-slot)
|
||||
(not (ctk/subcopy-head? shape)))
|
||||
(ctk/remove-swap-slot shape)
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -935,4 +974,6 @@
|
||||
{:id 43 :migrate-up migrate-up-43}
|
||||
{:id 44 :migrate-up migrate-up-44}
|
||||
{:id 45 :migrate-up migrate-up-45}
|
||||
{:id 46 :migrate-up migrate-up-46}])
|
||||
{:id 46 :migrate-up migrate-up-46}
|
||||
{:id 47 :migrate-up migrate-up-47}
|
||||
{:id 48 :migrate-up migrate-up-48}])
|
||||
|
||||
@@ -460,6 +460,34 @@
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
|
||||
(defmethod repair-error :misplaced-slot
|
||||
[_ {:keys [shape page-id] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
;; Remove the swap slot
|
||||
(log/debug :hint (str " -> remove swap-slot"))
|
||||
(ctk/remove-swap-slot shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :missing-slot
|
||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
;; Set the desired swap slot
|
||||
(let [slot (:swap-slot args)]
|
||||
(when (some? slot)
|
||||
(log/debug :hint (str " -> set swap-slot to " slot))
|
||||
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
|
||||
|
||||
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :default
|
||||
[_ error file _]
|
||||
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.files.validate
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
@@ -50,7 +51,9 @@
|
||||
:not-head-copy-not-allowed
|
||||
:not-component-not-allowed
|
||||
:component-nil-objects-not-allowed
|
||||
:instance-head-not-frame})
|
||||
:instance-head-not-frame
|
||||
:misplaced-slot
|
||||
:missing-slot})
|
||||
|
||||
(def ^:private
|
||||
schema:error
|
||||
@@ -285,6 +288,14 @@
|
||||
"Shape inside main instance should not have shape-ref"
|
||||
shape file page)))
|
||||
|
||||
(defn- check-empty-swap-slot
|
||||
"Validate that this shape does not have any swap slot."
|
||||
[shape file page]
|
||||
(when (some? (ctk/get-swap-slot shape))
|
||||
(report-error :misplaced-slot
|
||||
"This shape should not have swap slot"
|
||||
shape file page)))
|
||||
|
||||
(defn- check-shape-main-root-top
|
||||
"Root shape of a top main instance:
|
||||
|
||||
@@ -296,6 +307,7 @@
|
||||
(check-component-main-head shape file page libraries)
|
||||
(check-component-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-top) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-main-root-nested
|
||||
@@ -307,6 +319,7 @@
|
||||
(check-component-main-head shape file page libraries)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-nested) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-copy-root-top
|
||||
@@ -321,6 +334,7 @@
|
||||
(check-component-not-main-head shape file page libraries)
|
||||
(check-component-root shape file page)
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
|
||||
|
||||
(defn- check-shape-copy-root-nested
|
||||
@@ -343,6 +357,7 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-any) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-copy-not-root
|
||||
@@ -351,6 +366,7 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-not-component
|
||||
@@ -360,6 +376,7 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
|
||||
|
||||
(defn- check-shape
|
||||
@@ -454,6 +471,8 @@
|
||||
;; PUBLIC API: VALIDATION FUNCTIONS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare check-swap-slots)
|
||||
|
||||
(defn validate-file
|
||||
"Validate full referential integrity and semantic coherence on file data.
|
||||
|
||||
@@ -464,6 +483,8 @@
|
||||
|
||||
(doseq [page (filter :id (ctpl/pages-seq data))]
|
||||
(check-shape uuid/zero file page libraries)
|
||||
(when (str/includes? (:name file) "check-swap-slot")
|
||||
(check-swap-slots uuid/zero file page libraries))
|
||||
(->> (get-orphan-shapes page)
|
||||
(run! #(check-shape % file page libraries))))
|
||||
|
||||
@@ -517,3 +538,41 @@
|
||||
:hint "error on validating file referential integrity"
|
||||
:file-id (:id file)
|
||||
:details errors)))
|
||||
|
||||
|
||||
(declare compare-slots)
|
||||
|
||||
;; Optional check to look for missing swap slots.
|
||||
;; Search for copies that do not point the shape-ref to the near component but don't have swap slot
|
||||
;; (looking for position relative to the parent, in the copy and the main).
|
||||
;;
|
||||
;; This check cannot be generally enabled, because files that have been migrated from components v1
|
||||
;; may have copies with shapes that do not match by position, but have not been swapped. So we enable
|
||||
;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to
|
||||
;; the name of the file.
|
||||
(defn- check-swap-slots
|
||||
[shape-id file page libraries]
|
||||
(let [shape (ctst/get-shape page shape-id)]
|
||||
(if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape))
|
||||
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true)
|
||||
container (:container (meta ref-shape))]
|
||||
(when (some? ref-shape)
|
||||
(compare-slots shape ref-shape file page container)))
|
||||
(doall (for [child-id (:shapes shape)]
|
||||
(check-swap-slots child-id file page libraries))))))
|
||||
|
||||
(defn- compare-slots
|
||||
[shape-copy shape-main file container-copy container-main]
|
||||
(if (and (not= (:shape-ref shape-copy) (:id shape-main))
|
||||
(nil? (ctk/get-swap-slot shape-copy)))
|
||||
(report-error :missing-slot
|
||||
"Shape has been swapped, should have swap slot"
|
||||
shape-copy file container-copy
|
||||
:swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main)))
|
||||
(when (nil? (ctk/get-swap-slot shape-copy))
|
||||
(let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))]
|
||||
(doall (for [[child-copy-id child-main-id] children-id-pairs]
|
||||
(let [child-copy (ctst/get-shape container-copy child-copy-id)
|
||||
child-main (ctst/get-shape container-main child-main-id)]
|
||||
(when (and (some? child-copy) (some? child-main))
|
||||
(compare-slots child-copy child-main file container-copy container-main)))))))))
|
||||
|
||||
@@ -130,6 +130,15 @@
|
||||
(and (some? (:component-id shape))
|
||||
(nil? (:component-root shape))))
|
||||
|
||||
(defn subcopy-head?
|
||||
"Check if this shape is the head of a subinstance that is a copy."
|
||||
[shape]
|
||||
;; This is redundant with the previous one, but may give more security
|
||||
;; in case of bugs.
|
||||
(and (some? (:component-id shape))
|
||||
(nil? (:component-root shape))
|
||||
(some? (:shape-ref shape))))
|
||||
|
||||
(defn instance-of?
|
||||
[shape file-id component-id]
|
||||
(and (some? (:component-id shape))
|
||||
@@ -227,7 +236,6 @@
|
||||
:shape-ref
|
||||
:touched))
|
||||
|
||||
|
||||
(defn- extract-ids [shape]
|
||||
(if (map? shape)
|
||||
(let [current-id (:id shape)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
version: "3.5"
|
||||
version: "3.8"
|
||||
|
||||
networks:
|
||||
penpot:
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -28,9 +28,16 @@ body {
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: thin;
|
||||
// transition: all .4s ease;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.global-zeroclipboard-container {
|
||||
transition: none;
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
|
||||
// SCROLLBAR
|
||||
.new-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(170, 181, 186, 0.3) transparent;
|
||||
&:hover {
|
||||
scrollbar-color: rgba(170, 181, 186, 0.7) transparent;
|
||||
}
|
||||
|
||||
// These rules do not apply in chrome - 121 or higher
|
||||
// We keep them to preserve backward compatibility.
|
||||
::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
@@ -585,6 +593,9 @@
|
||||
width: 100%;
|
||||
z-index: $z-index-modal;
|
||||
background-color: var(--overlay-color);
|
||||
&.onboarding-a-b-test {
|
||||
background-color: var(--overlay-color-onboarding-a-b-test);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container-base {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// Dark background
|
||||
--db-primary: #18181a;
|
||||
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)};
|
||||
--db-primary-90: #{color.change(#18181a, $alpha: 0.9)};
|
||||
--db-secondary: #000000;
|
||||
--db-secondary-30: #{color.change(#000000, $alpha: 0.3)};
|
||||
--db-secondary-80: #{color.change(#000000, $alpha: 0.8)};
|
||||
@@ -35,6 +36,7 @@
|
||||
// Light background
|
||||
--lb-primary: #ffffff;
|
||||
--lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)};
|
||||
--lb-primary-90: #{color.change(#ffffff, $alpha: 0.9)};
|
||||
--lb-secondary: #e8eaee;
|
||||
--lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)};
|
||||
--lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)};
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
--color-info-foreground: var(--status-color-info-500);
|
||||
|
||||
--overlay-color: var(--db-primary-60);
|
||||
--overlay-color-onboarding-a-b-test: var(--db-primary-90);
|
||||
|
||||
--shadow-color: var(--db-secondary-30);
|
||||
--radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset;
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
--color-info-foreground: var(--status-color-info-500);
|
||||
|
||||
--overlay-color: var(--lb-primary-60);
|
||||
--overlay-color-onboarding-a-b-test: var(--lb-primary-90);
|
||||
|
||||
--shadow-color: var(--lf-secondary-40);
|
||||
--radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset;
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.public-DraftStyleDefault-block {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
&.align-top {
|
||||
.DraftEditor-root {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
|
||||
@@ -130,6 +130,10 @@
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
|
||||
(defn external-feature-flag [flag value]
|
||||
(when-let [fn (obj/get global "externalFeatureFlag")]
|
||||
(fn flag value)))
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
(defn ^boolean check-browser? [candidate]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.libs.file-builder
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.builder :as fb]
|
||||
[app.common.media :as cm]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
@@ -73,7 +74,7 @@
|
||||
|
||||
manifest-stream
|
||||
(->> files-stream
|
||||
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % false))
|
||||
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features))
|
||||
(rx/map (fn [a]
|
||||
(vector "manifest.json" a))))
|
||||
|
||||
@@ -144,7 +145,7 @@
|
||||
(str (:current-page-id file)))
|
||||
|
||||
(addPage [_ name options]
|
||||
(set! file (fb/add-page file {:name name :options options}))
|
||||
(set! file (fb/add-page file {:name name :options (parse-data options)}))
|
||||
(str (:current-page-id file)))
|
||||
|
||||
(closePage [_]
|
||||
@@ -253,7 +254,7 @@
|
||||
|
||||
(export [_]
|
||||
(->> (export-file file)
|
||||
(rx/subs
|
||||
(rx/subs!
|
||||
(fn [value]
|
||||
(when (not (contains? value :type))
|
||||
(let [[file export-blob] value]
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{::ev/name "signing"
|
||||
{::ev/name "signin"
|
||||
::ev/type "identify"
|
||||
:email (:email profile)
|
||||
:auth-backend (:auth-backend profile)
|
||||
|
||||
@@ -1711,8 +1711,14 @@
|
||||
(process-entry [[type data]]
|
||||
(case type
|
||||
:text
|
||||
(if (str/empty? data)
|
||||
(cond
|
||||
(str/empty? data)
|
||||
(rx/empty)
|
||||
|
||||
(re-find #"<svg\s" data)
|
||||
(rx/of (paste-svg-text data))
|
||||
|
||||
:else
|
||||
(rx/of (paste-text data)))
|
||||
|
||||
:transit
|
||||
@@ -1757,8 +1763,7 @@
|
||||
text-data (some-> pdata wapi/extract-text)
|
||||
transit-data (ex/ignoring (some-> text-data t/decode-str))]
|
||||
(cond
|
||||
(and (string? text-data)
|
||||
(str/includes? text-data "<svg "))
|
||||
(and (string? text-data) (re-find #"<svg\s" text-data))
|
||||
(rx/of (paste-svg-text text-data))
|
||||
|
||||
(seq image-data)
|
||||
|
||||
@@ -143,18 +143,12 @@
|
||||
(map-indexed vector)
|
||||
(filter #(#{(:id group)} (second %)))
|
||||
(ffirst)
|
||||
inc)
|
||||
|
||||
;; Shapes that are in a component (including root) must be detached,
|
||||
;; because cannot be easyly synchronized back to the main component.
|
||||
shapes-to-detach (filter ctk/in-component-copy?
|
||||
(cfh/get-children-with-self objects (:id group)))]
|
||||
inc)]
|
||||
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/change-parent parent-id children index-in-parent)
|
||||
(pcb/remove-objects [(:id group)])
|
||||
(pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape))))
|
||||
(pcb/remove-objects [(:id group)]))))
|
||||
|
||||
(defn remove-frame-changes
|
||||
[it page-id frame objects]
|
||||
|
||||
@@ -233,6 +233,10 @@
|
||||
; If the initial shape was component-root, first level subinstances are converted in top instances
|
||||
(pcb/update-shapes [shape-id] #(assoc % :component-root true))
|
||||
|
||||
:always
|
||||
; First level subinstances of a detached component can't have swap-slot
|
||||
(pcb/update-shapes [shape-id] ctk/remove-swap-slot)
|
||||
|
||||
:always
|
||||
; Near shape-refs need to be advanced one level
|
||||
(generate-advance-nesting-level nil container libraries (:id shape)))
|
||||
@@ -1491,9 +1495,22 @@
|
||||
container
|
||||
{:type :reg-objects
|
||||
:shapes all-parents})]))))
|
||||
(let [roperation {:type :set
|
||||
(let [;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
;; so it's calculated again
|
||||
reset-pos-data?
|
||||
(and (cfh/text-shape? origin-shape)
|
||||
(= attr :position-data)
|
||||
(not= (get origin-shape attr) (get dest-shape attr))
|
||||
(touched :geometry-group))
|
||||
|
||||
roperation {:type :set
|
||||
:attr attr
|
||||
:val (get origin-shape attr)
|
||||
:val (cond
|
||||
;; If position data changes and the geometry group is touched
|
||||
;; we need to put to nil so we can regenerate it
|
||||
reset-pos-data? nil
|
||||
:else (get origin-shape attr))
|
||||
:ignore-touched true}
|
||||
uoperation {:type :set
|
||||
:attr attr
|
||||
|
||||
@@ -486,6 +486,7 @@
|
||||
duplicating-component? (or duplicating-component? (ctk/instance-head? obj))
|
||||
is-component-main? (ctk/main-instance? obj)
|
||||
subinstance-head? (ctk/subinstance-head? obj)
|
||||
instance-root? (ctk/instance-root? obj)
|
||||
|
||||
into-component? (and duplicating-component?
|
||||
(ctn/in-any-component? objects parent))
|
||||
@@ -508,7 +509,9 @@
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
|
||||
(cond-> (and subinstance-head? remove-swap-slot?)
|
||||
(cond-> (and (not instance-root?)
|
||||
subinstance-head?
|
||||
remove-swap-slot?)
|
||||
(ctk/remove-swap-slot))
|
||||
|
||||
(dissoc :shapes
|
||||
@@ -581,8 +584,9 @@
|
||||
true
|
||||
(and remove-swap-slot?
|
||||
;; only remove swap slot of children when the current shape
|
||||
;; is not a subinstance head
|
||||
(not subinstance-head?))))
|
||||
;; is not a subinstance head nor a instance root
|
||||
(not subinstance-head?)
|
||||
(not instance-root?))))
|
||||
changes
|
||||
(map (d/getf objects) (:shapes obj)))))))
|
||||
|
||||
|
||||
@@ -133,7 +133,10 @@
|
||||
(defn- fetch-gfont-css
|
||||
[url]
|
||||
(->> (http/send! {:method :get :uri url :mode :cors :response-type :text})
|
||||
(rx/map :body)))
|
||||
(rx/map :body)
|
||||
(rx/catch (fn [err]
|
||||
(.warn js/console "Cannot find the font" (obj/get err "message"))
|
||||
(rx/empty)))))
|
||||
|
||||
(defmethod load-font :google
|
||||
[{:keys [id ::on-loaded] :as font}]
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
[:& (mf/provider ctx/current-profile) {:value profile}
|
||||
(if edata
|
||||
[:& static/exception-page {:data edata}]
|
||||
[:& static/exception-page {:data edata :route route}]
|
||||
[:*
|
||||
[:& msgs/notifications-hub]
|
||||
(when route
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
(dom/set-html-title (tr "title.default")))
|
||||
|
||||
[:main {:class (stl/css :auth-section)}
|
||||
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
[:div {:class (stl/css :login-illustration)}
|
||||
i/login-illustration]
|
||||
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: absolute;
|
||||
top: $s-20;
|
||||
left: $s-20;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
margin-block-end: $s-52;
|
||||
}
|
||||
|
||||
.login-illustration {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -55,14 +65,6 @@
|
||||
}
|
||||
|
||||
.logo-btn {
|
||||
position: absolute;
|
||||
top: $s-20;
|
||||
left: $s-20;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
margin-block-end: $s-52;
|
||||
|
||||
svg {
|
||||
width: $s-120;
|
||||
height: $s-40;
|
||||
|
||||
@@ -54,9 +54,10 @@
|
||||
(let [props (.-props tab)
|
||||
id (.-id props)
|
||||
title (.-title props)
|
||||
sid (d/name id)]
|
||||
sid (d/name id)
|
||||
tooltip (if (string? title) title nil)]
|
||||
[:div {:key (str/concat "tab-" sid)
|
||||
:title title
|
||||
:title tooltip
|
||||
:data-id sid
|
||||
:on-click on-click
|
||||
:class (stl/css-case
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
@extend .button-icon;
|
||||
stroke: var(--tab-foreground-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
@include headlineSmallTypography;
|
||||
text-align: center;
|
||||
@@ -53,17 +54,21 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.current,
|
||||
&.current:hover {
|
||||
background: var(--tab-background-color-selected);
|
||||
border-color: var(--tab-border-color-selected);
|
||||
color: var(--tab-foreground-color-selected);
|
||||
|
||||
svg {
|
||||
stroke: var(--tab-foreground-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--tab-foreground-color-hover);
|
||||
|
||||
svg {
|
||||
stroke: var(--tab-foreground-color-hover);
|
||||
}
|
||||
@@ -78,6 +83,7 @@
|
||||
min-width: $s-24;
|
||||
padding: 0 $s-6;
|
||||
border-radius: $br-5;
|
||||
|
||||
svg {
|
||||
@include flexCenter;
|
||||
height: $s-16;
|
||||
@@ -87,6 +93,7 @@
|
||||
fill: none;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
stroke: var(--icon-foreground-hover);
|
||||
@@ -107,3 +114,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
//Firefox doesn't respect scrollbar-gutter
|
||||
@supports (-moz-appearance: none) {
|
||||
.tab-container-content {
|
||||
padding-right: $s-8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
(def current-page-id (mf/create-context nil))
|
||||
(def current-file-id (mf/create-context nil))
|
||||
(def current-vbox (mf/create-context nil))
|
||||
(def current-svg-root-id (mf/create-context nil))
|
||||
|
||||
(def active-frames (mf/create-context nil))
|
||||
(def render-thumbnails (mf/create-context nil))
|
||||
|
||||
@@ -7,15 +7,11 @@
|
||||
(ns app.main.ui.dashboard.projects
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.grid :refer [line-grid]]
|
||||
@@ -100,80 +96,6 @@
|
||||
(def builtin-templates
|
||||
(l/derived :builtin-templates st/state))
|
||||
|
||||
(mf/defc tutorial-project
|
||||
[{:keys [close-tutorial default-project-id] :as props}]
|
||||
(let [state (mf/use-state {:status :waiting
|
||||
:file nil})
|
||||
|
||||
templates (mf/deref builtin-templates)
|
||||
template (d/seek #(= (:id %) "tutorial-for-beginners") templates)
|
||||
|
||||
on-template-cloned-success
|
||||
(mf/use-fn
|
||||
(mf/deps default-project-id)
|
||||
(fn [response]
|
||||
(swap! state #(assoc % :status :success :file (:first response)))
|
||||
(st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"})
|
||||
(du/update-profile-props {:viewed-tutorial? true}))))
|
||||
|
||||
on-template-cloned-error
|
||||
(mf/use-fn
|
||||
(fn [cause]
|
||||
(swap! state assoc :status :error)
|
||||
(errors/print-error! cause)
|
||||
(st/emit! (msg/error (tr "dashboard.libraries-and-templates.import-error")))))
|
||||
|
||||
download-tutorial
|
||||
(mf/use-fn
|
||||
(mf/deps template default-project-id)
|
||||
(fn []
|
||||
(let [mdata {:on-success on-template-cloned-success
|
||||
:on-error on-template-cloned-error}
|
||||
params {:project-id default-project-id
|
||||
:template-id (:id template)}]
|
||||
(swap! state #(assoc % :status :importing))
|
||||
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
||||
{::ev/origin "get-started-hero-block"})))))]
|
||||
[:article {:class (stl/css :tutorial)}
|
||||
[:div {:class (stl/css :thumbnail)}]
|
||||
[:div {:class (stl/css :text)}
|
||||
[:h2 {:class (stl/css :title)} (tr "dasboard.tutorial-hero.title")]
|
||||
[:p {:class (stl/css :info)} (tr "dasboard.tutorial-hero.info")]
|
||||
[:button {:class (stl/css :btn-primary :action)
|
||||
:on-click download-tutorial}
|
||||
(case (:status @state)
|
||||
:waiting (tr "dasboard.tutorial-hero.start")
|
||||
:importing [:span.loader i/loader-pencil]
|
||||
:success "")]]
|
||||
|
||||
[:button {:class (stl/css :close)
|
||||
:on-click close-tutorial
|
||||
:aria-label (tr "labels.close")}
|
||||
close-icon]]))
|
||||
|
||||
(mf/defc interface-walkthrough
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [close-walkthrough] :as props}]
|
||||
(let [handle-walkthrough-link
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:section "dashboard"})))]
|
||||
[:article {:class (stl/css :walkthrough)}
|
||||
[:div {:class (stl/css :thumbnail)}]
|
||||
[:div {:class (stl/css :text)}
|
||||
[:h2 {:class (stl/css :title)} (tr "dasboard.walkthrough-hero.title")]
|
||||
[:p {:class (stl/css :info)} (tr "dasboard.walkthrough-hero.info")]
|
||||
[:a {:class (stl/css :btn-primary :action)
|
||||
:href " https://design.penpot.app/walkthrough"
|
||||
:target "_blank"
|
||||
:on-click handle-walkthrough-link}
|
||||
(tr "dasboard.walkthrough-hero.start")]]
|
||||
[:button {:class (stl/css :close)
|
||||
:on-click close-walkthrough
|
||||
:aria-label (tr "labels.close")}
|
||||
close-icon]]))
|
||||
|
||||
(mf/defc project-item
|
||||
[{:keys [project first? team files] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
@@ -365,7 +287,7 @@
|
||||
(l/derived :dashboard-recent-files st/state))
|
||||
|
||||
(mf/defc projects-section
|
||||
[{:keys [team projects profile default-project-id] :as props}]
|
||||
[{:keys [team projects profile] :as props}]
|
||||
(let [projects (->> (vals projects)
|
||||
(sort-by :modified-at)
|
||||
(reverse))
|
||||
@@ -378,8 +300,6 @@
|
||||
(:team-hero? props true)
|
||||
(not (:is-default team)))
|
||||
|
||||
tutorial-viewed? (:viewed-tutorial? props true)
|
||||
walkthrough-viewed? (:viewed-walkthrough? props true)
|
||||
is-my-penpot (= (:default-team-id profile) (:id team))
|
||||
|
||||
team-id (:id team)
|
||||
@@ -391,28 +311,6 @@
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero"
|
||||
::ev/origin "dashboard"}))))
|
||||
|
||||
close-tutorial
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-tutorial? true})
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-tutorial"
|
||||
::ev/origin "get-started-hero"
|
||||
:type "tutorial"
|
||||
:section "dashboard"}))))
|
||||
|
||||
close-walkthrough
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-walkthrough? true})
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-walkthrough"
|
||||
::ev/origin "get-started-hero"
|
||||
:type "walkthrough"
|
||||
:section "dashboard"}))))
|
||||
|
||||
show-hero? (and is-my-penpot
|
||||
(or (not tutorial-viewed?)
|
||||
(not walkthrough-viewed?)))
|
||||
|
||||
show-team-hero? (and (not is-my-penpot) team-hero?)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
@@ -433,22 +331,9 @@
|
||||
(when team-hero?
|
||||
[:& team-hero {:team team :close-fn close-banner}])
|
||||
|
||||
(when (and (contains? cf/flags :dashboard-templates-section)
|
||||
show-hero?)
|
||||
[:div {:class (stl/css :hero-projects)}
|
||||
(when (and (not tutorial-viewed?) (:is-default team))
|
||||
[:& tutorial-project
|
||||
{:close-tutorial close-tutorial
|
||||
:default-project-id default-project-id}])
|
||||
|
||||
(when (and (not walkthrough-viewed?) (:is-default team))
|
||||
[:& interface-walkthrough
|
||||
{:close-walkthrough close-walkthrough}])])
|
||||
|
||||
[:div {:class (stl/css-case :dashboard-container true
|
||||
:no-bg true
|
||||
:dashboard-projects true
|
||||
:with-hero show-hero?
|
||||
:with-team-hero show-team-hero?)}
|
||||
(for [{:keys [id] :as project} projects]
|
||||
(let [files (when recent-map
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
height: calc(100vh - $s-64);
|
||||
}
|
||||
|
||||
.with-hero,
|
||||
.with-team-hero {
|
||||
height: calc(100vh - $s-280);
|
||||
}
|
||||
@@ -86,8 +85,6 @@
|
||||
color: var(--title-foreground-color-hover);
|
||||
cursor: pointer;
|
||||
height: $s-16;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
@@ -242,88 +239,3 @@
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-projects {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: $s-32;
|
||||
margin: 0 $s-16 $s-16 $s-20;
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tutorial,
|
||||
.walkthrough {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
position: relative;
|
||||
border-radius: $br-8;
|
||||
min-height: $s-216;
|
||||
background-color: $db-tertiary;
|
||||
padding: $s-8;
|
||||
|
||||
.thumbnail {
|
||||
width: $s-200;
|
||||
height: $s-200;
|
||||
border-radius: $br-6;
|
||||
padding: $s-32;
|
||||
display: block;
|
||||
background-color: var(--color-canvas);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $br-4;
|
||||
margin-bottom: 0;
|
||||
width: $s-232;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: $s-32;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $df-primary;
|
||||
font-size: $fs-24;
|
||||
font-weight: $fw400;
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
color: $df-secondary;
|
||||
margin-bottom: $s-20;
|
||||
font-size: $fs-16;
|
||||
}
|
||||
.invite {
|
||||
height: $s-32;
|
||||
}
|
||||
.action {
|
||||
width: $s-180;
|
||||
height: $s-40;
|
||||
}
|
||||
}
|
||||
.walkthrough {
|
||||
.thumbnail {
|
||||
background-image: url("/images/walkthrough-cover.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
.tutorial {
|
||||
.thumbnail {
|
||||
background-image: url("/images/hands-on-tutorial.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.loader {
|
||||
display: flex;
|
||||
svg#loader-pencil {
|
||||
width: $s-32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
[:div {:class (stl/css :img-container)}
|
||||
[:a {:id id
|
||||
:tab-index (if (or (not is-visible) collapsed) "-1" "0")
|
||||
:href "https://penpot.app/libraries-templates.html"
|
||||
:href "https://penpot.app/libraries-templates"
|
||||
:target "_blank"
|
||||
:on-click on-click
|
||||
:on-key-down on-key-down}
|
||||
|
||||
@@ -142,7 +142,9 @@
|
||||
(modal/show! {:type :onboarding-newsletter})
|
||||
|
||||
(contains? cf/flags :onboarding-team)
|
||||
(modal/show! {:type :onboarding-team}))))]
|
||||
(modal/show! {:type :onboarding-team}))))
|
||||
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
(mf/with-effect [@slide]
|
||||
(when (not= :start @slide)
|
||||
@@ -151,8 +153,8 @@
|
||||
(fn []
|
||||
(reset! klass nil)
|
||||
(tm/dispose! sem))))
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
|
||||
(case @slide
|
||||
:start [:& onboarding-welcome {:next #(navigate :opensource)}]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.onboarding.newsletter
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
@@ -35,9 +36,11 @@
|
||||
(st/emit! (when (or @newsletter-updates @newsletter-news)
|
||||
(msg/success message))
|
||||
(modal/show {:type :onboarding-team})
|
||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))]
|
||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:img {:src "images/deco-newsletter.png"
|
||||
|
||||
@@ -287,9 +287,11 @@
|
||||
(modal/show! {:type :onboarding-team})
|
||||
|
||||
:else
|
||||
(modal/hide!)))))]
|
||||
(modal/hide!)))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-container)
|
||||
:ref container}
|
||||
(case @step
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dmc]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
@@ -84,14 +85,16 @@
|
||||
::ev/origin "onboarding"
|
||||
:step 1}))))
|
||||
|
||||
teams (mf/deref refs/teams)]
|
||||
teams (mf/deref refs/teams)
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
(mf/with-effect [teams]
|
||||
(when (> (count teams) 1)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
(when (< (count teams) 2)
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
@@ -212,9 +215,11 @@
|
||||
(if (> (count emails) 0)
|
||||
(on-invite-now form)
|
||||
(on-invite-later form))
|
||||
(modal/hide!))))]
|
||||
(modal/hide!))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
[:h2 "Libraries & templates"]]
|
||||
[:div.modal-content
|
||||
[:p "We’ve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpot’s Design System are among them (and a lot more to come!)."]
|
||||
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates.html"} "Explore libraries & templates"]]]
|
||||
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates"} "Explore libraries & templates"]]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click finish} "Start!"]
|
||||
[:& c/navigation-bullets
|
||||
|
||||
@@ -8,196 +8,203 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; TODO: Review all copies and alt text
|
||||
(defmethod c/render-release-notes "2.0"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-intro-image.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
(let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-intro-image.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Welcome to Penpot 2.0! "]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Welcome to Penpot 2.0! "]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"CSS Grid Layout: "]
|
||||
"Bring your designs to life, knowing that what you create is what developers code."]
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"CSS Grid Layout: "]
|
||||
"Bring your designs to life, knowing that what you create is what developers code."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"Sleeker UI: "]
|
||||
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"Sleeker UI: "]
|
||||
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"New Components System: "]
|
||||
"Managing and using your design components got a whole lot better."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"New Components System: "]
|
||||
"Managing and using your design components got a whole lot better."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all - we’ve fined tuned performance and "
|
||||
"accessibility to give you a better and more fluid design experience."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all - we’ve fined tuned performance and "
|
||||
"accessibility to give you a better and more fluid design experience."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
" Ready to dive in? Let 's get started!"]]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
" Ready to dive in? Let 's get started!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-css-grid.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's CSS Grid Layout"}]
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-css-grid.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's CSS Grid Layout"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"CSS Grid Layout - Design Meets Development"]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"CSS Grid Layout - Design Meets Development"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The much-awaited Grid Layout introduces 2-dimensional"
|
||||
" layout capabilities to Penpot, allowing for the creation"
|
||||
" of adaptive layouts by leveraging the power of CSS properties."]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The much-awaited Grid Layout introduces 2-dimensional"
|
||||
" layout capabilities to Penpot, allowing for the creation"
|
||||
" of adaptive layouts by leveraging the power of CSS properties."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a host of new features, including columns and"
|
||||
" rows management, flexible units such as FR (fractions),"
|
||||
" the ability to create and name areas, and tons of new "
|
||||
"and unique possibilities within a design tool."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a host of new features, including columns and"
|
||||
" rows management, flexible units such as FR (fractions),"
|
||||
" the ability to create and name areas, and tons of new "
|
||||
"and unique possibilities within a design tool."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Designers will learn CSS basics while working, "
|
||||
"and as always with Penpot, developers can pick"
|
||||
" up the design as code to take it from there."]]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Designers will learn CSS basics while working, "
|
||||
"and as always with Penpot, developers can pick"
|
||||
" up the design as code to take it from there."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-new-ui.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's UI Makeover"}]
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-new-ui.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's UI Makeover"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We've completely overhauled Penpot's user interface. "
|
||||
"The improvements in consistency, the introduction of "
|
||||
"new microinteractions, and attention to countless details"
|
||||
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Furthermore, we’ve made several accessibility improvements, "
|
||||
"with better color contrast, keyboard navigation,"
|
||||
" and adherence to other best practices."]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We've completely overhauled Penpot's user interface. "
|
||||
"The improvements in consistency, the introduction of "
|
||||
"new microinteractions, and attention to countless details"
|
||||
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Furthermore, we’ve made several accessibility improvements, "
|
||||
"with better color contrast, keyboard navigation,"
|
||||
" and adherence to other best practices."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
2
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-components.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's new components system"}]
|
||||
2
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-components.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's new components system"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"New Components System"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The new Penpot components system improves"
|
||||
" control over instances, including their "
|
||||
"inheritances and properties overrides. "
|
||||
"Main components are now accessible as design"
|
||||
" elements, allowing a better updating "
|
||||
"workflow through instant changes synchronization."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all, there are new capabilities "
|
||||
"such as component swapping and annotations "
|
||||
"that will help you to better manage your design systems."]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"New Components System"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The new Penpot components system improves"
|
||||
" control over instances, including their "
|
||||
"inheritances and properties overrides. "
|
||||
"Main components are now accessible as design"
|
||||
" elements, allowing a better updating "
|
||||
"workflow through instant changes synchronization."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all, there are new capabilities "
|
||||
"such as component swapping and annotations "
|
||||
"that will help you to better manage your design systems."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
3
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-html.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt " Penpot's HTML code generator"}]
|
||||
3
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-html.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt " Penpot's HTML code generator"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"And much more"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"In addition to all of this, we’ve included several other requested improvements:"]
|
||||
[:ul {:class (stl/css :feature-list)}
|
||||
[:li "Access HTML markup code directly in inspect mode"]
|
||||
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
||||
[:li "Enjoy new color themes with options for both dark and light modes"]
|
||||
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"And much more"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"In addition to all of this, we’ve included several other requested improvements:"]
|
||||
[:ul {:class (stl/css :feature-list)}
|
||||
[:li "Access HTML markup code directly in inspect mode"]
|
||||
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
||||
[:li "Enjoy new color themes with options for both dark and light modes"]
|
||||
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]]))))
|
||||
|
||||
|
||||
@@ -122,7 +122,11 @@
|
||||
(add! :stroke-cap-end)))
|
||||
|
||||
(cond-> text?
|
||||
(-> (add! :grow-type)
|
||||
(-> (add! :x)
|
||||
(add! :y)
|
||||
(add! :width)
|
||||
(add! :height)
|
||||
(add! :grow-type)
|
||||
(add! :content (comp json/encode uuid->string))
|
||||
(add! :position-data (comp json/encode uuid->string))))
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
(mf/fnc group-shape
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")
|
||||
render-id (mf/use-ctx muc/render-id)
|
||||
@@ -36,21 +37,31 @@
|
||||
|
||||
mask-props (if ^boolean masked-group?
|
||||
#js {:mask (mask-url render-id mask)}
|
||||
#js {})]
|
||||
#js {})
|
||||
|
||||
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
|
||||
|
||||
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
|
||||
;; be stored in the context. When rendering the styles we add its id as prefix.
|
||||
[svg-wrapper svg-wrapper-props]
|
||||
(if (and (contains? shape :svg-attrs) (not current-svg-root-id))
|
||||
[(mf/provider muc/current-svg-root-id) #js {:value (:id shape)}]
|
||||
[mf/Fragment #js {}])]
|
||||
|
||||
;; We need to separate mask and clip into two because a bug in
|
||||
;; Firefox breaks when the group has clip+mask+foreignObject
|
||||
;; Clip and mask separated will work in every platform Firefox
|
||||
;; bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805
|
||||
[:> wrapper clip-props
|
||||
[:> wrapper mask-props
|
||||
(when ^boolean masked-group?
|
||||
[:& render-mask {:mask mask}])
|
||||
[:> svg-wrapper svg-wrapper-props
|
||||
[:> wrapper clip-props
|
||||
[:> wrapper mask-props
|
||||
(when ^boolean masked-group?
|
||||
[:& render-mask {:mask mask}])
|
||||
|
||||
(for [item childs]
|
||||
[:& shape-wrapper
|
||||
{:shape item
|
||||
:key (dm/str (dm/get-prop item :id))}])]]))))
|
||||
(for [item childs]
|
||||
[:& shape-wrapper
|
||||
{:shape item
|
||||
:key (dm/str (dm/get-prop item :id))}])]]]))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,13 @@
|
||||
(obj/unset! "disable-shadows?")
|
||||
(obj/set! "ref" ref)
|
||||
(obj/set! "id" (dm/fmt "shape-%" shape-id))
|
||||
|
||||
;; TODO: This is added for backward compatibility.
|
||||
(cond-> (and (cfh/text-shape? shape) (empty? (:position-data shape)))
|
||||
(-> (obj/set! "x" (:x shape))
|
||||
(obj/set! "y" (:y shape))
|
||||
(obj/set! "width" (:width shape))
|
||||
(obj/set! "height" (:height shape))))
|
||||
(obj/set! "style" styles))
|
||||
|
||||
wrapper-props
|
||||
|
||||
@@ -104,9 +104,20 @@
|
||||
svg-root? (and (map? content) (= tag :svg))
|
||||
svg-tag? (map? content)
|
||||
svg-leaf? (string? content)
|
||||
valid-tag? (contains? csvg/svg-tags tag)]
|
||||
valid-tag? (contains? csvg/svg-tags tag)
|
||||
|
||||
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
|
||||
|
||||
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
|
||||
;; be stored in the context and with this we scoped the styles:
|
||||
style-content
|
||||
(when (= tag :style)
|
||||
(dm/str "#shape-" current-svg-root-id "{ " (->> shape :content :content (str/join "\n")) " }"))]
|
||||
|
||||
(cond
|
||||
(= tag :style)
|
||||
[:style style-content]
|
||||
|
||||
^boolean svg-root?
|
||||
[:& svg-root {:shape shape}
|
||||
(for [item childs]
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
@@ -16,6 +18,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc error-container
|
||||
@@ -146,15 +149,19 @@
|
||||
|
||||
(mf/defc exception-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [data] :as props}]
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:& not-found]
|
||||
[{:keys [data route] :as props}]
|
||||
(let [type (:type data)
|
||||
path (:path route)
|
||||
query-params (u/map->query-string (:query-params route))]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:& not-found]
|
||||
|
||||
:bad-gateway
|
||||
[:& bad-gateway]
|
||||
:bad-gateway
|
||||
[:& bad-gateway]
|
||||
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
|
||||
[:> internal-error props]))
|
||||
[:> internal-error props])))
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
:on-zoom-fit handle-zoom-fit
|
||||
:on-fullscreen toggle-fullscreen}]
|
||||
|
||||
(when (:can-edit permissions)
|
||||
(when (:in-team permissions)
|
||||
[:span {:on-click go-to-workspace
|
||||
:class (stl/css :edit-btn)}
|
||||
i/curve])
|
||||
@@ -191,7 +191,9 @@
|
||||
:on-click toggle-fullscreen}
|
||||
i/expand]
|
||||
|
||||
(when (:is-admin permissions)
|
||||
(when (and
|
||||
(:in-team permissions)
|
||||
(:is-admin permissions))
|
||||
[:button {:on-click open-share-dialog
|
||||
:class (stl/css :share-btn)}
|
||||
(tr "labels.share")])
|
||||
@@ -301,8 +303,8 @@
|
||||
;; If the user doesn't have permission we disable the link
|
||||
[:a {:class (stl/css :home-link)
|
||||
:on-click go-to-dashboard
|
||||
:style {:cursor (when-not (:can-edit permissions) "auto")
|
||||
:pointer-events (when-not (:can-edit permissions) "none")}}
|
||||
:style {:cursor (when-not (:in-team permissions) "auto")
|
||||
:pointer-events (when-not (:in-team permissions) "none")}}
|
||||
[:span {:class (stl/css :logo-icon)}
|
||||
i/logo-icon]]
|
||||
|
||||
@@ -321,7 +323,7 @@
|
||||
:title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))}
|
||||
i/play]
|
||||
|
||||
(when (or (:can-edit permissions)
|
||||
(when (or (:in-team permissions)
|
||||
(= (:who-comment permissions) "all"))
|
||||
[:button {:on-click navigate
|
||||
:data-value "comments"
|
||||
@@ -330,7 +332,7 @@
|
||||
:title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))}
|
||||
i/comments])
|
||||
|
||||
(when (or (= (:type permissions) :membership)
|
||||
(when (or (:in-team permissions)
|
||||
(and (= (:type permissions) :share-link)
|
||||
(= (:who-inspect permissions) "all")))
|
||||
[:button {:on-click go-to-inspect
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.workspace.colorpicker.libraries
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.colors :as c]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.events :as ev]
|
||||
@@ -24,7 +25,7 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc libraries
|
||||
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
|
||||
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
|
||||
(let [selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent)
|
||||
current-colors (mf/use-state [])
|
||||
|
||||
@@ -43,10 +44,17 @@
|
||||
(parse-uuid event)))))
|
||||
|
||||
check-valid-color?
|
||||
(fn [color]
|
||||
(and (or (not disable-gradient) (not (:gradient color)))
|
||||
(or (not disable-opacity) (= 1 (:opacity color)))
|
||||
(or (not disable-image) (not (:image color)))))
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(and (or (not disable-gradient) (not (:gradient color)))
|
||||
(or (not disable-opacity) (= 1 (:opacity color)))
|
||||
(or (not disable-image) (not (:image color))))))
|
||||
|
||||
;; Sort colors by hue and lightness
|
||||
get-sorted-colors
|
||||
(mf/use-fn
|
||||
(fn [colors]
|
||||
(sort c/sort-colors (into [] (filter check-valid-color?) colors))))
|
||||
|
||||
toggle-palette
|
||||
(mf/use-fn
|
||||
@@ -89,13 +97,15 @@
|
||||
(sort-by :name)
|
||||
(map #(assoc % :file-id file-id)))))]
|
||||
|
||||
(reset! current-colors (into [] (filter check-valid-color?) colors))))
|
||||
(if (not= @selected :recent)
|
||||
(reset! current-colors (get-sorted-colors colors))
|
||||
(reset! current-colors (into [] (filter check-valid-color? colors))))))
|
||||
|
||||
;; If the file colors change and the file option is selected updates the state
|
||||
(mf/with-effect [file-colors]
|
||||
(when (= @selected :file)
|
||||
(let [colors (vals file-colors)]
|
||||
(reset! current-colors (into [] (filter check-valid-color?) colors)))))
|
||||
(reset! current-colors (get-sorted-colors colors)))))
|
||||
|
||||
[:div {:class (stl/css :libraries)}
|
||||
[:div {:class (stl/css :select-wrapper)}
|
||||
|
||||
@@ -181,14 +181,14 @@
|
||||
[:span {:class (stl/css :resalted-area)}]]]
|
||||
[:div {:class (stl/css :constraints-center)}
|
||||
[:button {:class (stl/css-case :constraint-btn true
|
||||
:active (= constraints-h :center))
|
||||
:data-value "centerh"
|
||||
:active (= constraints-v :center))
|
||||
:data-value "centerv"
|
||||
:on-click on-constraint-button-clicked}
|
||||
[:span {:class (stl/css :resalted-area)}]]
|
||||
[:button {:class (stl/css-case :constraint-btn-special true
|
||||
:constraint-btn-rotated true
|
||||
:active (= constraints-v :center))
|
||||
:data-value "centerv"
|
||||
:active (= constraints-h :center))
|
||||
:data-value "centerh"
|
||||
:on-click on-constraint-button-clicked}
|
||||
[:span {:class (stl/css :resalted-area)}]]]
|
||||
[:div {:class (stl/css :constraints-right)}
|
||||
|
||||
@@ -69,33 +69,34 @@
|
||||
|
||||
on-add
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(mf/deps ids fills)
|
||||
(fn [_]
|
||||
(st/emit! (dc/add-fill ids {:color default-color
|
||||
:opacity 1}))
|
||||
|
||||
(when (not (some? (seq fills))) (open-content))))
|
||||
(when (or (= :multiple fills)
|
||||
(not (some? (seq fills))))
|
||||
(open-content))))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [index]
|
||||
(fn [color]
|
||||
(st/emit! (dc/change-fill ids color index)))))
|
||||
(fn [index]
|
||||
(fn [color]
|
||||
(st/emit! (dc/change-fill ids color index))))
|
||||
|
||||
on-reorder
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [new-index]
|
||||
(fn [index]
|
||||
(st/emit! (dc/reorder-fills ids index new-index)))))
|
||||
(fn [new-index]
|
||||
(fn [index]
|
||||
(st/emit! (dc/reorder-fills ids index new-index))))
|
||||
|
||||
on-remove
|
||||
(fn [index]
|
||||
(fn []
|
||||
(st/emit! (dc/remove-fill ids {:color default-color
|
||||
:opacity 1} index))
|
||||
(when (= 1 (count (seq fills))) (close-content))))
|
||||
(when (or (= :multiple fills)
|
||||
(= 1 (count (seq fills))))
|
||||
(close-content))))
|
||||
|
||||
on-remove-all
|
||||
(fn [_]
|
||||
(st/emit! (dc/remove-all-fills ids {:color clr/black
|
||||
|
||||
@@ -272,9 +272,21 @@
|
||||
(def has-position? #{:frame :rect :image :text})
|
||||
|
||||
(defn parse-position
|
||||
[props svg-data]
|
||||
(let [values (->> (select-keys svg-data [:x :y :width :height])
|
||||
(d/mapm (fn [_ val] (d/parse-double val))))]
|
||||
[props node svg-data]
|
||||
(let [x (get-meta node :x d/parse-double)
|
||||
y (get-meta node :y d/parse-double)
|
||||
width (get-meta node :width d/parse-double)
|
||||
height (get-meta node :height d/parse-double)
|
||||
|
||||
values (->> (select-keys svg-data [:x :y :width :height])
|
||||
(d/mapm (fn [_ val] (d/parse-double val))))
|
||||
|
||||
values
|
||||
(cond-> values
|
||||
(some? x) (assoc :x x)
|
||||
(some? y) (assoc :y y)
|
||||
(some? width) (assoc :width width)
|
||||
(some? height) (assoc :height height))]
|
||||
(d/merge props values)))
|
||||
|
||||
(defn parse-circle
|
||||
@@ -392,7 +404,7 @@
|
||||
center (gpt/point center-x center-y)]
|
||||
(cond-> props
|
||||
(has-position? type)
|
||||
(parse-position svg-data)
|
||||
(parse-position node svg-data)
|
||||
|
||||
(= type :svg-raw)
|
||||
(add-svg-position node)
|
||||
|
||||
@@ -352,7 +352,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers te "
|
||||
"deel of voeg by vanaf ons [Biblioteke en sjablone](https://penpot.app/"
|
||||
"libraries-templates.html)."
|
||||
"libraries-templates)."
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-github-submit"
|
||||
|
||||
@@ -306,7 +306,7 @@ msgstr "تكرير %s الملفات"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"أوه لا! ليس لديك ملفات بعد! إذا كنت تريد تجربة بعض القوالب ، فانتقل إلى "
|
||||
"[المكتبات والقوالب] (https://penpot.app/libraries-templates.html)"
|
||||
"[المكتبات والقوالب] (https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "تنزيل ملفات ٪s Penpot (.penpot)"
|
||||
|
||||
@@ -311,7 +311,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a "
|
||||
"la secció [Biblioteques i "
|
||||
"plantilles](https://penpot.app/libraries-templates.html)"
|
||||
"plantilles](https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Baixa %s fitxers Penpot (.penpot)"
|
||||
|
||||
@@ -310,7 +310,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo "
|
||||
"je přidat z našich [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Stáhnout soubory %s Penpot (.penpot)"
|
||||
|
||||
@@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Files added to Libraries will appear here. Try sharing your files or add "
|
||||
"from our [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Download %s Penpot files (.penpot)"
|
||||
|
||||
@@ -412,7 +412,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar "
|
||||
"con alguna plantilla ve a [Bibliotecas y "
|
||||
"plantillas](https://penpot.app/libraries-templates.html)."
|
||||
"plantillas](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||
|
||||
@@ -258,7 +258,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir "
|
||||
"sus archivos o agréguelos desde nuestras [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||
|
||||
@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
|
||||
"baduzu joan [Liburutegi eta "
|
||||
"txantiloiak](https://penpot.app/libraries-templates.html) atalera."
|
||||
"txantiloiak](https://penpot.app/libraries-templates) atalera."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"
|
||||
|
||||
@@ -308,7 +308,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"وای نه! شما هنوز هیچ فایلی ندارید! اگر میخواهید چند الگو را امتحان کنید، "
|
||||
"به [کتابخانهها و الگوها] بروید "
|
||||
"(https://penpot.app/libraries-templates.html)"
|
||||
"(https://penpot.app/libraries-templates)"
|
||||
|
||||
#, fuzzy
|
||||
msgid "dashboard.export-binary-multi"
|
||||
|
||||
@@ -298,7 +298,7 @@ msgstr "Tvítak %s fílur"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Áh nei! Tú hevur ongar fílur enn! Um tú vilt royna við nøkrum skapilónum, "
|
||||
"vitja [Libraries & templates](https://penpot.app/libraries-templates.html)"
|
||||
"vitja [Libraries & templates](https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Heinta %s Penpot fílur (.penpot)"
|
||||
|
||||
@@ -397,7 +397,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh non ! Vous n'avez pas encore de fichiers ! Si vous voulez essayer avec "
|
||||
"des modèles, allez sur [Bibliothèques et modèles] "
|
||||
"(https://penpot.app/libraries-templates.html)."
|
||||
"(https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Télécharger %s fichiers Penpot (.penpot)"
|
||||
|
||||
@@ -306,7 +306,7 @@ msgstr "Duplicar % ficheiros"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Ai non! Ainda non tes ficheiros! Se queres facer a proba con algún modelo "
|
||||
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates.html)"
|
||||
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s ficheiros Penpot (.penpot)"
|
||||
|
||||
@@ -381,7 +381,7 @@ msgstr "שכפול %s קבצים"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"קבצים שנוספו לספריות יתווספו לכאן. כדאי לנסות לשתף את הקבצים שלך או להוסיף "
|
||||
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates.html)."
|
||||
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "הורדת %s קובצי Penpot (.penpot)"
|
||||
|
||||
@@ -308,7 +308,7 @@ msgstr "Kopiraj %s datoteka"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na "
|
||||
"[Biblioteke i predlošci](https://penpot.app/libraries-templates.html)"
|
||||
"[Biblioteke i predlošci](https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Preuzmi %s Penpot datoteke (.penpot)"
|
||||
|
||||
@@ -401,7 +401,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan "
|
||||
"berkas Anda atau menambahkan dari [Pustaka & "
|
||||
"templat](https://penpot.app/libraries-templates.html) kami."
|
||||
"templat](https://penpot.app/libraries-templates) kami."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Unduh %s berkas Penpot (.penpot)"
|
||||
|
||||
@@ -304,7 +304,7 @@ msgstr "Duplicare %s file"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh no! Non hai ancora nessun file! Se desideri provare alcuni template vai "
|
||||
"su [Librerie e template](https://penpot.app/libraries-templates.html)"
|
||||
"su [Librerie e template](https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Scarica %s file Penpot (.penpot)"
|
||||
|
||||
@@ -267,7 +267,7 @@ msgstr "%s ファイルを複製"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"まだファイルがありません。もしいくつかのテンプレートを試してみたいなら、[Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates.html) をチェックしてみてください。"
|
||||
"templates](https://penpot.app/libraries-templates) をチェックしてみてください。"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "PDFでエクスポート"
|
||||
|
||||
@@ -297,7 +297,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Čia bus rodomi prie bibliotekų pridėti failai. Pabandykite bendrinti failus "
|
||||
"arba pridėti iš mūsų [Bibliotekos ir šablonai] "
|
||||
"(https://penpot.app/libraries-templates.html)"
|
||||
"(https://penpot.app/libraries-templates)"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "Eksportuokite darbalaukius į PDF"
|
||||
|
||||
@@ -404,7 +404,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Šeit tiks parādītas bibliotēkām pievienotās datnes. Mēģini koplietot datnes "
|
||||
"vai pievienot tās no mūsu [bibliotēkām un veidnēm](https://penpot.app/"
|
||||
"libraries-templates.html)."
|
||||
"libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Lejupielādēt %s Penpot datnes (.penpot)"
|
||||
|
||||
@@ -225,7 +225,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"ഇതുവരെയും ഇവിടെ ഫയലുകളില്ല. നിങ്ങൾക്ക് ചില ടെമ്പ്ലേറ്റുകൾ "
|
||||
"പരീക്ഷിക്കണമെന്നുണ്ടെങ്കിൽ [ലൈബ്രറികളുടെയും ടെമ്പ്ലേറ്റുകളുടെയും "
|
||||
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates.html) പോകാവുന്നതാണ്"
|
||||
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates) പോകാവുന്നതാണ്"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "ആർട്ട്ബോർഡുകൾ പിഡിഎഫായി എക്സ്പോർട്ട് ചെയ്യുക"
|
||||
|
||||
@@ -336,7 +336,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Fail yang ditambahkan pada Perpustakaan akan dipaparkan di sini. Cuba kongsi "
|
||||
"fail anda atau tambahkan daripada [Perpustakaan & templat](https://penpot."
|
||||
"app/libraries-templates.html) kami."
|
||||
"app/libraries-templates) kami."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Muat turun %s fail Penpot (.penpot)"
|
||||
|
||||
@@ -417,7 +417,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Bestanden die aan bibliotheken zijn toegevoegd, worden hier weergegeven. "
|
||||
"Probeer je bestanden te delen of toe te voegen vanuit onze [Bibliotheken & "
|
||||
"sjablonen] (https://penpot.app/libraries-templates.html)."
|
||||
"sjablonen] (https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "%s Penpot-bestanden downloaden (.penpot)"
|
||||
|
||||
@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Tutaj pojawią się pliki dodane do Bibliotek. Spróbuj udostępnić swoje pliki "
|
||||
"lub dodać z naszych [Bibliotek i "
|
||||
"szablonów](https://penpot.app/libraries-templates.html)."
|
||||
"szablonów](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Pobierz %s plików Penpot (.penpot)"
|
||||
|
||||
@@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Arquivos adicionados na biblioteca de ativos vão aparecer aqui. Tente "
|
||||
"compartilhar seus arquivos ou adicione das nossas [Bibliotecas & "
|
||||
"modelos](https://penpot.app/libraries-templates.html)."
|
||||
"modelos](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Baixar %s arquivos Penpot (.penpot)"
|
||||
|
||||
@@ -400,7 +400,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh não! Ainda não tens ficheiros! Se quiseres experimentar podes começar "
|
||||
"com os nossos templates em [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descarrega %s ficheiros Penpot (.penpot)"
|
||||
|
||||
@@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Fișierele adăugate la Biblioteci vor apărea aici. Încercați să partajați "
|
||||
"fișierele dvs. sau adăugați-le din [Biblioteci și "
|
||||
"șabloane](https://penpot.app/libraries-templates.html)."
|
||||
"șabloane](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descărcați %s fișiere Penpot (.penpot)"
|
||||
|
||||
@@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Файлы, добавленные в Библиотеки, появятся здесь. Попробуйте поделиться "
|
||||
"своими файлами или добавить их из наших [Библиотек и "
|
||||
"шаблонов](https://penpot.app/libraries-templates.html)."
|
||||
"шаблонов](https://penpot.app/libraries-templates)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Скачать файлы Penpot (.penpot) (%s)"
|
||||
|
||||
@@ -311,7 +311,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Kütüphanelere eklenen dosyalar burada görünecektir. Dosyalarınızı "
|
||||
"paylaşmayı deneyin veya [Kütüphaneler ve "
|
||||
"şablonlarımızdan](https://penpot.app/libraries-templates.html) ekleyin."
|
||||
"şablonlarımızdan](https://penpot.app/libraries-templates) ekleyin."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "%s Penpot dosyasını indir (.penpot)"
|
||||
|
||||
@@ -295,7 +295,7 @@ msgstr "複製 %s 個檔案"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"添加在資料庫的檔案會在此處列出。請分享你的檔案或由我們的 [資料庫 & "
|
||||
"模板區段](https://penpot.app/libraries-templates.html) 添加。"
|
||||
"模板區段](https://penpot.app/libraries-templates) 添加。"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "下載 %s 個Penpot 檔案 (.penpot)"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.0
|
||||
2.0.3
|
||||
|
||||
Reference in New Issue
Block a user