mirror of
https://github.com/penpot/penpot.git
synced 2026-01-04 12:28:52 -05:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7e28b71b | ||
|
|
be30174a49 | ||
|
|
8373654f80 | ||
|
|
471c636580 | ||
|
|
635c6efe42 | ||
|
|
0b39318b33 | ||
|
|
47cecb2ac4 | ||
|
|
5d6ceec803 | ||
|
|
16cf16c422 | ||
|
|
4e1eee197e | ||
|
|
91c8af9e38 | ||
|
|
58593a9428 | ||
|
|
17cf57f7ca | ||
|
|
f7cfe36f37 | ||
|
|
c26f909565 | ||
|
|
6db7fe5f7b | ||
|
|
a207114d95 | ||
|
|
b8299a5ea5 | ||
|
|
1fa461e996 | ||
|
|
2e3745099b | ||
|
|
6892cffe54 | ||
|
|
e0034dc205 | ||
|
|
bd9eab08b7 | ||
|
|
b5121657ee | ||
|
|
ca257d1caf | ||
|
|
e164692391 | ||
|
|
b58edea544 | ||
|
|
9a587c91a8 | ||
|
|
aae1571a5c | ||
|
|
ebaf30727c | ||
|
|
f5f255e2d5 | ||
|
|
e65c0d9f48 | ||
|
|
86c5ca4213 | ||
|
|
179d534237 | ||
|
|
162507264c | ||
|
|
7e0a8b6227 | ||
|
|
475d14edf4 | ||
|
|
979828ffe3 | ||
|
|
65bb795199 | ||
|
|
a0546b2e63 | ||
|
|
f291125377 | ||
|
|
0ce981a68c | ||
|
|
a8814dcaba | ||
|
|
229eeae6db | ||
|
|
d03788af93 | ||
|
|
017aad6454 | ||
|
|
767ec37b83 | ||
|
|
89f64e0c49 | ||
|
|
d108ad904e | ||
|
|
6564736d3e | ||
|
|
d01cd70c6b | ||
|
|
ea7768117c | ||
|
|
5bfb39cdf6 | ||
|
|
29f1c2bdad | ||
|
|
e79f9ba40f | ||
|
|
452aabdec6 | ||
|
|
860e32d965 | ||
|
|
495f9dfa84 | ||
|
|
133ca33cb5 | ||
|
|
1c69a9fd8a | ||
|
|
15faa57e01 | ||
|
|
f5510234cf | ||
|
|
5b0331611d | ||
|
|
f8f1c58f61 | ||
|
|
3f34aa92fa | ||
|
|
c99102e49b | ||
|
|
d583661e58 | ||
|
|
ffa326e08f | ||
|
|
03040ed40b | ||
|
|
5e89cd1cb3 | ||
|
|
bf202473e9 | ||
|
|
886c0c596f | ||
|
|
b15b394c65 | ||
|
|
caf78a6b4d | ||
|
|
6a161267ba | ||
|
|
a180c33a32 | ||
|
|
ea8febdb7d | ||
|
|
f765cc8dbc | ||
|
|
81b7972347 | ||
|
|
1281670c61 | ||
|
|
b8c6103858 | ||
|
|
b2c0bed84c | ||
|
|
9619fcbc1f | ||
|
|
e9c55e9eb4 | ||
|
|
488d034a58 | ||
|
|
8d66275187 | ||
|
|
59063e861c | ||
|
|
9da891e9b0 | ||
|
|
a6de12323e | ||
|
|
52d099c80e | ||
|
|
b2010e5fd8 | ||
|
|
5808bd3743 | ||
|
|
d5f5c440dd | ||
|
|
50df2279a7 | ||
|
|
71ba0242c7 |
23
CHANGES.md
23
CHANGES.md
@@ -1,11 +1,23 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
|
||||
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
|
||||
- Add limits for invitation RPC methods (hard limit 25 emails per request)
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
|
||||
@@ -71,10 +83,21 @@
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
|
||||
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
|
||||
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
|
||||
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
|
||||
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
|
||||
- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888)
|
||||
- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464)
|
||||
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
|
||||
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
|
||||
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
|
||||
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
|
||||
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
|
||||
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
|
||||
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
|
||||
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
|
||||
|
||||
## 2.1.5
|
||||
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
[{:id "wireframing-kit"
|
||||
:name "Wireframe library"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
|
||||
{:id "prototype-examples"
|
||||
:name "Prototype template"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/prototype-examples.penpot"}
|
||||
{:id "plants-app"
|
||||
:name "UI mockup example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Design system example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
|
||||
{:id "tutorial-for-beginners"
|
||||
:name "Tutorial for beginners"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
|
||||
{:id "lucide-icons"
|
||||
:name "Lucide Icons"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Lucide-icons.penpot"}
|
||||
{:id "font-awesome"
|
||||
:name "Font Awesome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/FontAwesome.penpot"}
|
||||
{:id "black-white-mobile-templates"
|
||||
:name "Black & White Mobile Templates"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Black-&-White-Mobile-Templates.penpot"}
|
||||
{:id "avataaars"
|
||||
:name "Avataaars"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Avataaars-by-Pablo-Stanley.penpot"}
|
||||
{:id "ux-notes"
|
||||
:name "UX Notes"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/UX-Notes.penpot"}
|
||||
{:id "whiteboarding-kit"
|
||||
:name "Whiteboarding Kit"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
|
||||
{:id "open-color-scheme"
|
||||
:name "Open Color Scheme"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
|
||||
{:id "welcome"
|
||||
:name "Welcome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]
|
||||
|
||||
@@ -24,7 +24,7 @@ export PENPOT_FLAGS="\
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-file-snapshot \
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
|
||||
@@ -567,7 +567,6 @@
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:props (:props info)
|
||||
:profile-id (:id profile)}))
|
||||
props (audit/profile->props profile)
|
||||
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
||||
|
||||
@@ -409,7 +409,10 @@
|
||||
:fn (mg/resource "app/migrations/sql/0128-mod-task-table.sql")}
|
||||
|
||||
{:name "0129-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0130-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN version integer NULL;
|
||||
@@ -27,9 +27,11 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def schema:password
|
||||
@@ -241,6 +243,7 @@
|
||||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
|
||||
@@ -350,7 +353,7 @@
|
||||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
(let [theme (when (= theme "light") theme)
|
||||
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
||||
params (-> claims
|
||||
@@ -380,8 +383,13 @@
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
||||
|
||||
props (audit/profile->props profile)]
|
||||
props (audit/profile->props profile)
|
||||
|
||||
create-welcome-file-when-needed
|
||||
(fn []
|
||||
(when (:create-welcome-file params)
|
||||
(let [cfg (dissoc cfg ::db/conn)]
|
||||
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
||||
(cond
|
||||
;; When profile is blocked, we just ignore it and return plain data
|
||||
(:is-blocked profile)
|
||||
@@ -418,6 +426,7 @@
|
||||
(if (:is-active profile)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "login"}
|
||||
@@ -427,10 +436,12 @@
|
||||
(when-not (eml/has-reports? conn (:email profile))
|
||||
(send-email-verification! cfg profile))
|
||||
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
(-> {:email (:email profile)}
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
:else
|
||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||
@@ -462,7 +473,8 @@
|
||||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
[:fullname [::sm/word-string {:max 100}]]
|
||||
[:theme {:optional true} [:string {:max 10}]]])
|
||||
[:theme {:optional true} [:string {:max 10}]]
|
||||
[:create-welcome-file {:optional true} :boolean]])
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
[:map {:title "create-comment-thread"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content [:string {:max 250}]]
|
||||
[:content [:string {:max 750}]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
|
||||
(defn get-minimal-file
|
||||
[cfg id & {:as opts}]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :revn :data-ref-id :data-backend])]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :data-ref-id :data-backend])]
|
||||
(db/get cfg :file {:id id} opts)))
|
||||
|
||||
(defn get-file-etag
|
||||
@@ -487,7 +487,7 @@
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]
|
||||
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]]
|
||||
[:object-id {:optional true} [:or ::sm/uuid [::sm/set ::sm/uuid]]]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
|
||||
(sv/defmethod ::get-page
|
||||
@@ -1058,7 +1058,7 @@
|
||||
(def ^:private schema:ignore-file-library-sync-status
|
||||
[:map {:title "ignore-file-library-sync-status"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:date ::dt/duration]])
|
||||
[:date ::dt/instant]])
|
||||
|
||||
;; TODO: improve naming
|
||||
(sv/defmethod ::ignore-file-library-sync-status
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
(db/update! conn :file
|
||||
{:data (:data snapshot)
|
||||
:revn (inc (:revn file))
|
||||
:version (:version snapshot)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:has-media-trimmed false
|
||||
@@ -170,7 +171,7 @@
|
||||
(update :data blob/encode)))))
|
||||
|
||||
(defn take-file-snapshot!
|
||||
[cfg {:keys [file-id label]}]
|
||||
[cfg {:keys [file-id label ::rpc/profile-id]}]
|
||||
(let [file (get-file cfg file-id)
|
||||
id (uuid/next)]
|
||||
|
||||
@@ -182,7 +183,9 @@
|
||||
{:id id
|
||||
:revn (:revn file)
|
||||
:data (:data file)
|
||||
:version (:version file)
|
||||
:features (:features file)
|
||||
:profile-id profile-id
|
||||
:file-id (:id file)
|
||||
:label label}
|
||||
{::db/return-keys false})
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"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
|
||||
DO UPDATE SET updated_at=?, media_id=?, deleted_at=?
|
||||
RETURNING *")
|
||||
|
||||
(defn- persist-thumbnail!
|
||||
@@ -251,17 +251,19 @@
|
||||
:content-type mtype
|
||||
:bucket "file-object-thumbnail"})))
|
||||
|
||||
|
||||
|
||||
(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)
|
||||
[{:keys [::sto/storage] :as cfg} file object-id media tag]
|
||||
(let [file-id (:id file)
|
||||
timestamp (dt/now)
|
||||
media (persist-thumbnail! storage media timestamp)
|
||||
[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)])]
|
||||
file-id object-id tag
|
||||
(:id media)
|
||||
timestamp
|
||||
(:id media)
|
||||
(:deleted-at file)])]
|
||||
[th1 th2])))]
|
||||
|
||||
(when (and (some? th1)
|
||||
@@ -294,8 +296,8 @@
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(db/run! cfg files/check-edition-permissions! profile-id file-id)
|
||||
|
||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))
|
||||
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
|
||||
(create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
|
||||
|
||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||
|
||||
|
||||
@@ -38,6 +38,20 @@
|
||||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
(declare ^:private update-file)
|
||||
(declare ^:private update-file*)
|
||||
(declare ^:private process-changes-and-validate)
|
||||
(declare ^:private take-snapshot?)
|
||||
(declare ^:private delete-old-snapshots!)
|
||||
|
||||
;; PUBLIC API; intended to be used outside of this module
|
||||
(declare update-file!)
|
||||
(declare update-file-data!)
|
||||
(declare persist-file!)
|
||||
(declare get-file)
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
(def ^:private
|
||||
@@ -97,41 +111,6 @@
|
||||
(or (contains? library-change-types type)
|
||||
(contains? file-change-types type)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn- wrap-with-pointer-map-context
|
||||
[f]
|
||||
(fn [cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [result (f cfg file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
result))))
|
||||
|
||||
(declare ^:private delete-old-snapshots!)
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
(declare ^:private take-snapshot?)
|
||||
(declare ^:private update-file)
|
||||
(declare ^:private update-file*)
|
||||
(declare ^:private update-file-data)
|
||||
|
||||
;; If features are specified from params and the final feature
|
||||
;; set is different than the persisted one, update it on the
|
||||
;; database.
|
||||
@@ -147,7 +126,8 @@
|
||||
::sm/result schema:update-file-result
|
||||
::doc/module :files
|
||||
::doc/added "1.17"}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
@@ -161,14 +141,30 @@
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
|
||||
params (assoc params
|
||||
:profile-id profile-id
|
||||
:features features
|
||||
:team team
|
||||
:file file)
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features)
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
cfg (assoc cfg ::timestamp (dt/now))
|
||||
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
@@ -177,89 +173,126 @@
|
||||
{:features features}
|
||||
{:id (:id team)})))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
(get :app.http/request)
|
||||
(errors/request->context))]
|
||||
(-> (update-file cfg params)
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
||||
|
||||
(defn update-file
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [file features changes changes-with-metadata] :as params}]
|
||||
(let [features (-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file)))
|
||||
|
||||
update-fn (cond-> update-file*
|
||||
(contains? features "fdata/pointer-map")
|
||||
(wrap-with-pointer-map-context))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))]
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(let [file (assoc file :features features)
|
||||
params (-> params
|
||||
(assoc :file file)
|
||||
(assoc :changes changes)
|
||||
(assoc ::created-at (dt/now)))]
|
||||
|
||||
(-> (update-fn cfg params)
|
||||
(vary-meta assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))))
|
||||
|
||||
(defn- update-file*
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg}
|
||||
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
|
||||
"Internal function, part of the update-file process, that encapsulates
|
||||
the changes application offload to a separated thread and emit all
|
||||
corresponding notifications.
|
||||
|
||||
Follow the inner implementation to `update-file-data!` function.
|
||||
|
||||
Only intended for internal use on this module."
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
{:keys [profile-id file features changes session-id skip-validate] :as params}]
|
||||
|
||||
(let [;; Retrieve the file data
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
|
||||
file (assoc file :features
|
||||
(-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file))))
|
||||
|
||||
;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
file (px/invoke! executor
|
||||
(fn []
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))]
|
||||
|
||||
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
|
||||
;; NOTE: if file was offloaded, we need to touch the referenced
|
||||
;; storage object because on this update operation the data will
|
||||
;; be overwritted.
|
||||
(when (= "objects-storage" (:data-backend file))
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(sto/touch-object! storage (:data-ref-id file))))
|
||||
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:features (db/create-array conn "text" (:features file))
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage))))
|
||||
|
||||
;; TODO: move this to asynchronous task
|
||||
(when (::snapshot-data file)
|
||||
(delete-old-snapshots! cfg file))
|
||||
|
||||
(persist-file! cfg file)
|
||||
|
||||
(let [params (assoc params :file file)
|
||||
response {:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)}
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
|
||||
;; Insert change (xlog)
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at timestamp
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:features features
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
|
||||
(vary-meta response assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))
|
||||
|
||||
(defn update-file!
|
||||
"A public api that allows apply a transformation to a file with all context setup."
|
||||
[cfg file-id update-fn & args]
|
||||
(let [file (get-file cfg file-id)
|
||||
file (apply update-file-data! cfg file update-fn args)]
|
||||
(persist-file! cfg file)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
"Get not-decoded file, only decodes the features set."
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn persist-file!
|
||||
"Function responsible of persisting already encoded file. Should be
|
||||
used together with `get-file` and `update-file-data!`.
|
||||
|
||||
It also updates the project modified-at attr."
|
||||
[{:keys [::db/conn ::timestamp]} file]
|
||||
(let [features (db/create-array conn "text" (:features file))
|
||||
;; The timestamp can be nil because this function is also
|
||||
;; intended to be used outside of this module
|
||||
modified-at (or timestamp (dt/now))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
{:id (:project-id file)}
|
||||
{::db/return-keys false})
|
||||
|
||||
(db/update! conn :file
|
||||
{:revn (:revn file)
|
||||
:data (:data file)
|
||||
@@ -267,20 +300,95 @@
|
||||
:features features
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:modified-at created-at
|
||||
:modified-at modified-at
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})
|
||||
{:id (:id file)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at created-at}
|
||||
{:id (:project-id file)})
|
||||
(defn- update-file-data!
|
||||
"Perform a file data transformation in with all update context setup.
|
||||
|
||||
(let [params (assoc params :file file)]
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
This function expected not-decoded file and transformation function. Returns
|
||||
an encoded file.
|
||||
|
||||
{:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)})))
|
||||
This function is not responsible of saving the file. It only saves
|
||||
fdata/pointer-map modified fragments."
|
||||
|
||||
[cfg {:keys [id] :as file} update-fn & args]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
file (apply update-fn cfg file args)
|
||||
|
||||
;; TODO: reuse operations if file is migrated
|
||||
;; TODO: move encoding to a separated thread
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)
|
||||
|
||||
file (cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))]
|
||||
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
|
||||
file)))
|
||||
|
||||
(defn- get-file-libraries
|
||||
"A helper for preload file libraries, mainly used for perform file
|
||||
semantical and structural validation"
|
||||
[{:keys [::db/conn] :as cfg} file]
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(defn- soft-validate-file-schema!
|
||||
[file]
|
||||
@@ -297,68 +405,19 @@
|
||||
(l/error :hint "file validation error"
|
||||
:cause cause))))
|
||||
|
||||
(defn- update-file-data
|
||||
[{:keys [::db/conn] :as cfg} file changes skip-validate]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
;; WARNING: this ruins performance; maybe we need to find
|
||||
(defn- process-changes-and-validate
|
||||
[cfg file changes skip-validate]
|
||||
(let [;; WARNING: this ruins performance; maybe we need to find
|
||||
;; some other way to do general validation
|
||||
libs (when (and (or (contains? cf/flags :file-validation)
|
||||
(contains? cf/flags :soft-file-validation))
|
||||
(not skip-validate))
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(get-file-libraries cfg file))
|
||||
|
||||
file (-> (files/check-version! file)
|
||||
(update :revn inc)
|
||||
(update :data cpc/process-changes changes)
|
||||
(update :data d/without-nils))
|
||||
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)]
|
||||
(update :data d/without-nils))]
|
||||
|
||||
(binding [pmap/*tracked* nil]
|
||||
(when (contains? cf/flags :soft-file-validation)
|
||||
@@ -375,15 +434,7 @@
|
||||
(not skip-validate))
|
||||
(val/validate-file-schema! file)))
|
||||
|
||||
(cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))))
|
||||
file))
|
||||
|
||||
(defn- take-snapshot?
|
||||
"Defines the rule when file `data` snapshot should be saved."
|
||||
@@ -425,8 +476,7 @@
|
||||
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
|
||||
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
|
||||
|
||||
(def ^:private
|
||||
sql:lagged-changes
|
||||
(def ^:private sql:lagged-changes
|
||||
"select s.id, s.revn, s.file_id,
|
||||
s.session_id, s.changes
|
||||
from file_change as s
|
||||
|
||||
@@ -396,8 +396,8 @@
|
||||
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn- clone-template
|
||||
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
||||
(defn clone-template
|
||||
[cfg {:keys [project-id profile-id] :as params} template]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||
;; NOTE: the importation process performs some operations that
|
||||
;; are not very friendly with virtual threads, and for avoid
|
||||
@@ -416,6 +416,7 @@
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))
|
||||
@@ -437,7 +438,8 @@
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
||||
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
||||
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
||||
template (tmpl/get-template-stream cfg template-id)]
|
||||
template (tmpl/get-template-stream cfg template-id)
|
||||
params (assoc params :profile-id profile-id)]
|
||||
|
||||
(when-not template
|
||||
(ex/raise :type :not-found
|
||||
|
||||
@@ -360,27 +360,31 @@
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]]))
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props)))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-props}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props))))
|
||||
[cfg {:keys [::rpc/profile-id props]}]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(update-profile-props cfg profile-id props))))
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
[:role schema:role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
"The number of invitations can be sent in a single rpc request"
|
||||
25)
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
@@ -920,6 +924,12 @@
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
@@ -994,6 +1004,12 @@
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
(def ^:private schema:verify-token
|
||||
[:map {:title "verify-token"}
|
||||
[:token [:string {:max 1000}]]])
|
||||
[:token [:string {:max 5000}]]])
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{::rpc/auth false
|
||||
@@ -82,16 +82,8 @@
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
|
||||
props (merge (:props profile)
|
||||
(:props claims))]
|
||||
(when (not= props (:props profile))
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}))
|
||||
|
||||
(let [profile (assoc profile :props props)]
|
||||
(assoc claims :profile profile))))
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
|
||||
64
backend/src/app/setup/welcome_file.clj
Normal file
64
backend/src/app/setup/welcome_file.clj
Normal file
@@ -0,0 +1,64 @@
|
||||
;; 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.setup.welcome-file
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files-update :as fupdate]
|
||||
[app.rpc.commands.management :as management]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.worker :as-alias wrk]))
|
||||
|
||||
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
|
||||
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
|
||||
|
||||
(def ^:private update-path
|
||||
[:data :pages-index page-id :objects shape-id
|
||||
:content :children 0 :children 0 :children 0])
|
||||
|
||||
(def ^:private sql:mark-file-object-thumbnails-deleted
|
||||
"UPDATE file_tagged_object_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(def ^:private sql:mark-file-thumbnail-deleted
|
||||
"UPDATE file_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(defn- update-welcome-shape
|
||||
[_ file name]
|
||||
(let [text (str "Welcome to Penpot, " name "!")]
|
||||
(-> file
|
||||
(update-in update-path assoc :text text)
|
||||
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
|
||||
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
|
||||
|
||||
(defn create-welcome-file
|
||||
[cfg {:keys [id fullname] :as profile}]
|
||||
(try
|
||||
(let [cfg (dissoc cfg ::db/conn)
|
||||
params {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)}
|
||||
template-stream (tmpl/get-template-stream cfg "welcome")
|
||||
file-id (-> (management/clone-template cfg params template-stream)
|
||||
first)]
|
||||
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
|
||||
(profile/update-profile-props cfg id {:welcome-file-id file-id})
|
||||
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
|
||||
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on create welcome file " :cause cause))))
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})))
|
||||
|
||||
|
||||
@@ -727,13 +727,15 @@
|
||||
deleted 0
|
||||
total 0]
|
||||
(if-let [email (first emails)]
|
||||
(if-let [profile (db/get* system :profile
|
||||
{:email (str/lower email)}
|
||||
{::db/remove-deleted false})]
|
||||
(if-let [profile (some-> (db/get* system :profile
|
||||
{:email (str/lower email)}
|
||||
{::db/remove-deleted false})
|
||||
(profile/decode-row))]
|
||||
(do
|
||||
(audit/insert! system
|
||||
{::audit/name "delete-profile"
|
||||
::audit/type "action"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/tracked-at deleted-at
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/context {:triggered-by "srepl"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
inactivity (the default threshold is 72h)."
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.logging :as l]
|
||||
@@ -35,16 +34,36 @@
|
||||
(declare ^:private decode-file)
|
||||
(declare ^:private persist-file!)
|
||||
|
||||
(def ^:private sql:get-snapshots
|
||||
"SELECT f.file_id AS id,
|
||||
f.data,
|
||||
f.revn,
|
||||
f.version,
|
||||
f.features,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file_change AS f
|
||||
WHERE f.file_id = ?
|
||||
AND f.label IS NOT NULL
|
||||
ORDER BY f.created_at ASC")
|
||||
|
||||
(def ^:private sql:mark-file-media-object-deleted
|
||||
"UPDATE file_media_object
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ? AND id != ALL(?::uuid[])
|
||||
RETURNING id")
|
||||
|
||||
(def ^:private xf:collect-used-media
|
||||
(comp (map :data) (mapcat bfc/collect-used-media)))
|
||||
|
||||
(defn- clean-file-media!
|
||||
"Performs the garbage collection of file media objects."
|
||||
[{:keys [::db/conn]} {:keys [id data] :as file}]
|
||||
(let [used (bfc/collect-used-media data)
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [used (into #{}
|
||||
xf:collect-used-media
|
||||
(cons file
|
||||
(->> (db/cursor conn [sql:get-snapshots id])
|
||||
(map (partial decode-file cfg)))))
|
||||
ids (db/create-array conn "uuid" used)
|
||||
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids])
|
||||
(into #{} (map :id)))]
|
||||
@@ -114,7 +133,13 @@
|
||||
file))
|
||||
|
||||
(def ^:private sql:get-files-for-library
|
||||
"SELECT f.id, f.data, f.modified_at, f.features, f.version
|
||||
"SELECT f.id,
|
||||
f.data,
|
||||
f.modified_at,
|
||||
f.features,
|
||||
f.version,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
|
||||
WHERE fl.library_file_id = ?
|
||||
@@ -170,11 +195,6 @@
|
||||
(l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused))
|
||||
file))
|
||||
|
||||
(def ^:private sql:get-changes
|
||||
"SELECT id, data FROM file_change
|
||||
WHERE file_id = ? AND data IS NOT NULL
|
||||
ORDER BY created_at ASC")
|
||||
|
||||
(def ^:private sql:mark-deleted-data-fragments
|
||||
"UPDATE file_data_fragment
|
||||
SET deleted_at = now()
|
||||
@@ -190,8 +210,7 @@
|
||||
|
||||
(defn- clean-data-fragments!
|
||||
[{:keys [::db/conn]} {:keys [id] :as file}]
|
||||
(let [used (into #{} xf:collect-pointers
|
||||
(cons file (db/cursor conn [sql:get-changes id])))
|
||||
(let [used (into #{} xf:collect-pointers [file])
|
||||
|
||||
unused (let [ids (db/create-array conn "uuid" used)]
|
||||
(->> (db/exec! conn [sql:mark-deleted-data-fragments id ids])
|
||||
@@ -220,7 +239,9 @@
|
||||
f.revn,
|
||||
f.version,
|
||||
f.features,
|
||||
f.modified_at
|
||||
f.modified_at,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
@@ -236,18 +257,8 @@
|
||||
|
||||
(defn- decode-file
|
||||
[cfg {:keys [id] :as file}]
|
||||
;; NOTE: a preventive check that does not allow proceed the gc for
|
||||
;; already offloaded file; if this exception happens, means that
|
||||
;; something external modified the file flag without preloading the
|
||||
;; file back again to the table
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(ex/raise :hint "unable to run file-gc on an already offloaded file"
|
||||
:type :internal
|
||||
:code :file-already-offloaded
|
||||
:file-id id))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> file
|
||||
(-> (feat.fdata/resolve-file-data cfg file)
|
||||
(update :features db/decode-pgarray #{})
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
@@ -256,7 +267,7 @@
|
||||
(fmg/migrate-file))))
|
||||
|
||||
(defn- persist-file!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [id] :as file}]
|
||||
(let [file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
@@ -272,11 +283,18 @@
|
||||
(update :features db/encode-pgarray conn "text")
|
||||
(update :data blob/encode))]
|
||||
|
||||
;; If file was already offloaded, we touch the underlying storage
|
||||
;; object for properly trigger storage-gc-touched task
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage)))
|
||||
|
||||
(db/update! conn :file
|
||||
{:has-media-trimmed true
|
||||
:features (:features file)
|
||||
:version (:version file)
|
||||
:data (:data file)}
|
||||
:data (:data file)
|
||||
:data-backend nil
|
||||
:data-ref-id nil}
|
||||
{:id id}
|
||||
{::db/return-keys true})))
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -26,8 +27,8 @@
|
||||
|
||||
(def xf:filter-offloded
|
||||
(comp
|
||||
(filter #(= "objects-storage" (:data-backend %)))
|
||||
(map :data-ref-id)))
|
||||
(filter feat.fdata/offloaded?)
|
||||
(keep :data-ref-id)))
|
||||
|
||||
(defn- delete-in-chunks
|
||||
[{:keys [::chunk-size ::threshold] :as cfg}]
|
||||
|
||||
@@ -141,21 +141,22 @@
|
||||
|
||||
;; --- INSTANT
|
||||
|
||||
(defn instant?
|
||||
[v]
|
||||
(instance? Instant v))
|
||||
|
||||
(defn instant
|
||||
([s]
|
||||
(if (int? s)
|
||||
(Instant/ofEpochMilli s)
|
||||
(Instant/parse s)))
|
||||
(cond
|
||||
(instant? s) s
|
||||
(int? s) (Instant/ofEpochMilli s)
|
||||
:else (Instant/parse s)))
|
||||
([s fmt]
|
||||
(case fmt
|
||||
:rfc1123 (Instant/from (.parse DateTimeFormatter/RFC_1123_DATE_TIME ^String s))
|
||||
:iso (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s))
|
||||
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
|
||||
|
||||
(defn instant?
|
||||
[v]
|
||||
(instance? Instant v))
|
||||
|
||||
(defn is-after?
|
||||
[da db]
|
||||
(.isAfter ^Instant da ^Instant db))
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
:enable-feature-fdata-pointer-map
|
||||
:enable-feature-fdata-objets-map
|
||||
:enable-feature-components-v2
|
||||
:enable-file-snapshot
|
||||
:enable-auto-file-snapshot
|
||||
:disable-file-validation])
|
||||
|
||||
(defn state-init
|
||||
@@ -304,16 +304,18 @@
|
||||
([params] (update-file* *system* params))
|
||||
([system {:keys [file-id changes session-id profile-id revn]
|
||||
:or {session-id (uuid/next) revn 0}}]
|
||||
(db/tx-run! system (fn [{:keys [::db/conn] :as system}]
|
||||
(let [file (files.update/get-file conn file-id)]
|
||||
(files.update/update-file system
|
||||
(-> system
|
||||
(assoc ::files.update/timestamp (dt/now))
|
||||
(db/tx-run! (fn [{:keys [::db/conn] :as system}]
|
||||
(let [file (files.update/get-file conn file-id)]
|
||||
(#'files.update/update-file* system
|
||||
{:id file-id
|
||||
:revn revn
|
||||
:file file
|
||||
:features (:features file)
|
||||
:changes changes
|
||||
:session-id session-id
|
||||
:profile-id profile-id}))))))
|
||||
:profile-id profile-id})))))))
|
||||
|
||||
(declare command!)
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
;; SCHEMAS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private
|
||||
schema:operation
|
||||
(def schema:operation
|
||||
[:multi {:dispatch :type
|
||||
:title "Operation"
|
||||
:decode/json #(update % :type keyword)
|
||||
@@ -61,9 +60,12 @@
|
||||
[:type [:= :set-remote-synced]]
|
||||
[:remote-synced {:optional true} [:maybe :boolean]]]]])
|
||||
|
||||
(sm/register! ::change
|
||||
(def schema:change
|
||||
[:schema
|
||||
[:multi {:dispatch :type :title "Change" ::smd/simplified true}
|
||||
[:multi {:dispatch :type
|
||||
:title "Change"
|
||||
:decode/json #(update % :type keyword)
|
||||
::smd/simplified true}
|
||||
[:set-option
|
||||
[:map {:title "SetOptionChange"}
|
||||
[:type [:= :set-option]]
|
||||
@@ -188,10 +190,9 @@
|
||||
[:type [:= :del-color]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
[:add-recent-color
|
||||
[:map {:title "AddRecentColorChange"}
|
||||
[:type [:= :add-recent-color]]
|
||||
[:color ::ctc/recent-color]]]
|
||||
[:map {:title "AddRecentColorChange"}]]
|
||||
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
@@ -256,8 +257,11 @@
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]]])
|
||||
|
||||
(sm/register! ::changes
|
||||
[:sequential {:gen/max 2} ::change])
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
|
||||
(sm/register! ::change schema:change)
|
||||
(sm/register! ::changes schema:changes)
|
||||
|
||||
(def check-change!
|
||||
(sm/check-fn ::change))
|
||||
@@ -541,6 +545,7 @@
|
||||
(d/update-in-when [pid :shapes] d/without-obj sid)
|
||||
(d/update-in-when [pid :shapes] d/vec-without-nils)
|
||||
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
|
||||
|
||||
(update-parent-id [objects id]
|
||||
(-> objects
|
||||
(d/update-when id assoc :parent-id parent-id)))
|
||||
@@ -651,18 +656,10 @@
|
||||
[data {:keys [id]}]
|
||||
(ctcl/delete-color data id))
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
(defmethod process-change :add-recent-color
|
||||
[data {:keys [color]}]
|
||||
;; Moves the color to the top of the list and then truncates up to 15
|
||||
(update
|
||||
data
|
||||
:recent-colors
|
||||
(fn [rc]
|
||||
(let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color)))
|
||||
rc (-> rc (conj color))]
|
||||
(cond-> rc
|
||||
(> (count rc) 15)
|
||||
(subvec 1))))))
|
||||
[data _]
|
||||
data)
|
||||
|
||||
;; -- Media
|
||||
|
||||
|
||||
@@ -607,13 +607,6 @@
|
||||
(reduce resize-parent changes all-parents)))
|
||||
|
||||
;; Library changes
|
||||
|
||||
(defn add-recent-color
|
||||
[changes color]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-recent-color :color color})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn add-color
|
||||
[changes color]
|
||||
(-> changes
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 51)
|
||||
(def version 54)
|
||||
|
||||
@@ -863,11 +863,9 @@
|
||||
(assoc shadow :color color)))
|
||||
|
||||
(update-object [object]
|
||||
(d/update-when object :shadow
|
||||
#(into []
|
||||
(comp (map fix-shadow)
|
||||
(filter valid-shadow?))
|
||||
%)))
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(d/update-when object :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
@@ -1010,13 +1008,44 @@
|
||||
|
||||
(defn migrate-up-51
|
||||
"This migration fixes library invalid colors"
|
||||
|
||||
[data]
|
||||
(let [update-colors
|
||||
(fn [colors]
|
||||
(into {} (filter #(-> % val valid-color?) colors)))]
|
||||
(update data :colors update-colors)))
|
||||
|
||||
(defn migrate-up-52
|
||||
"Fixes incorrect value on `layout-wrap-type` prop"
|
||||
[data]
|
||||
(letfn [(update-shape [shape]
|
||||
(if (= :no-wrap (:layout-wrap-type shape))
|
||||
(assoc shape :layout-wrap-type :nowrap)
|
||||
shape))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals update-shape))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
|
||||
(defn migrate-up-54
|
||||
"Fixes shapes with invalid colors in shadow: it first tries a non
|
||||
destructive fix, and if it is not possible, then, shadow is removed"
|
||||
[data]
|
||||
(letfn [(fix-shadow [shadow]
|
||||
(update shadow :color d/without-nils))
|
||||
|
||||
(update-shape [shape]
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(d/update-when shape :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -1059,4 +1088,7 @@
|
||||
{:id 48 :migrate-up migrate-up-48}
|
||||
{:id 49 :migrate-up migrate-up-49}
|
||||
{:id 50 :migrate-up migrate-up-50}
|
||||
{:id 51 :migrate-up migrate-up-51}])
|
||||
{:id 51 :migrate-up migrate-up-51}
|
||||
{:id 52 :migrate-up migrate-up-52}
|
||||
{:id 53 :migrate-up migrate-up-26}
|
||||
{:id 54 :migrate-up migrate-up-54}])
|
||||
|
||||
@@ -158,7 +158,11 @@
|
||||
|
||||
empty-parents
|
||||
;; Any parent whose children are all deleted, must be deleted too.
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
;; Unless we are during a component swap: in this case we are replacing a shape by
|
||||
;; other one, so must not delete empty parents.
|
||||
(if-not component-swap
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
#{})
|
||||
|
||||
components-to-delete
|
||||
(if components-v2
|
||||
|
||||
@@ -448,7 +448,7 @@
|
||||
(defn parse-email
|
||||
[s]
|
||||
(if (string? s)
|
||||
(re-matches email-re s)
|
||||
(first (re-seq email-re s))
|
||||
nil))
|
||||
|
||||
(defn email-string?
|
||||
@@ -476,7 +476,7 @@
|
||||
::oapi/type "string"
|
||||
::oapi/format "email"}})
|
||||
|
||||
(def non-empty-strings-xf
|
||||
(def xf:filter-word-strings
|
||||
(comp
|
||||
(filter string?)
|
||||
(remove str/empty?)
|
||||
@@ -489,11 +489,8 @@
|
||||
:min 0
|
||||
:max 1
|
||||
:compile
|
||||
(fn [{:keys [coerce kind max min] :as props} children _]
|
||||
(let [xform (if coerce
|
||||
(comp non-empty-strings-xf (map coerce))
|
||||
non-empty-strings-xf)
|
||||
kind (or (last children) kind)
|
||||
(fn [{:keys [kind max min] :as props} children _]
|
||||
(let [kind (or (last children) kind)
|
||||
|
||||
pred
|
||||
(cond
|
||||
@@ -501,9 +498,6 @@
|
||||
(nil? kind) any?
|
||||
:else (validator kind))
|
||||
|
||||
encode-child
|
||||
(encoder kind string-transformer)
|
||||
|
||||
pred
|
||||
(cond
|
||||
(and max min)
|
||||
@@ -531,31 +525,49 @@
|
||||
(fn [value]
|
||||
(every? pred value)))
|
||||
|
||||
decode
|
||||
|
||||
decode-string-child
|
||||
(decoder kind string-transformer)
|
||||
|
||||
decode-string
|
||||
(fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
||||
(into #{} xform v)))
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||
x (comp xf:filter-word-strings (map decode-string-child))]
|
||||
(into #{} x v)))
|
||||
|
||||
decode-json-child
|
||||
(decoder kind json-transformer)
|
||||
|
||||
decode-json
|
||||
(fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||
x (comp xf:filter-word-strings (map decode-json-child))]
|
||||
(into #{} x v)))
|
||||
|
||||
encode-string-child
|
||||
(encoder kind string-transformer)
|
||||
|
||||
encode-string
|
||||
(fn [o]
|
||||
(if (set? o)
|
||||
(str/join ", " (map encode-string-child o))
|
||||
o))
|
||||
|
||||
encode-json
|
||||
(fn [o]
|
||||
(if (set? o)
|
||||
(vec o)
|
||||
o))
|
||||
|
||||
encode-string
|
||||
(fn [o]
|
||||
(if (set? o)
|
||||
(str/join ", " (map encode-child o))
|
||||
o))]
|
||||
|
||||
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "set"
|
||||
:description "Set of Strings"
|
||||
:error/message "should be a set of strings"
|
||||
:gen/gen (-> kind sg/generator sg/set)
|
||||
:decode/string decode
|
||||
:decode/json decode
|
||||
:decode/string decode-string
|
||||
:decode/json decode-json
|
||||
:encode/string encode-string
|
||||
:encode/json encode-json
|
||||
::oapi/type "array"
|
||||
@@ -569,21 +581,14 @@
|
||||
:min 0
|
||||
:max 1
|
||||
:compile
|
||||
(fn [{:keys [coerce kind max min] :as props} children _]
|
||||
(let [xform (if coerce
|
||||
(comp non-empty-strings-xf (map coerce))
|
||||
non-empty-strings-xf)
|
||||
|
||||
kind (or (last children) kind)
|
||||
(fn [{:keys [kind max min] :as props} children _]
|
||||
(let [kind (or (last children) kind)
|
||||
pred
|
||||
(cond
|
||||
(fn? kind) kind
|
||||
(nil? kind) any?
|
||||
:else (validator kind))
|
||||
|
||||
encode-child
|
||||
(encoder kind string-transformer)
|
||||
|
||||
pred
|
||||
(cond
|
||||
(and max min)
|
||||
@@ -611,15 +616,31 @@
|
||||
(fn [value]
|
||||
(every? pred value)))
|
||||
|
||||
decode
|
||||
decode-string-child
|
||||
(decoder kind string-transformer)
|
||||
|
||||
decode-json-child
|
||||
(decoder kind json-transformer)
|
||||
|
||||
decode-string
|
||||
(fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
||||
(into [] xform v)))
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||
x (comp xf:filter-word-strings (map decode-string-child))]
|
||||
(into #{} x v)))
|
||||
|
||||
decode-json
|
||||
(fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||
x (comp xf:filter-word-strings (map decode-json-child))]
|
||||
(into #{} x v)))
|
||||
|
||||
encode-string-child
|
||||
(encoder kind string-transformer)
|
||||
|
||||
encode-string
|
||||
(fn [o]
|
||||
(if (vector? o)
|
||||
(str/join ", " (map encode-child o))
|
||||
(str/join ", " (map encode-string-child o))
|
||||
o))]
|
||||
|
||||
{:pred pred
|
||||
@@ -628,8 +649,8 @@
|
||||
:description "Set of Strings"
|
||||
:error/message "should be a set of strings"
|
||||
:gen/gen (-> kind sg/generator sg/set)
|
||||
:decode/string decode
|
||||
:decode/json decode
|
||||
:decode/string decode-string
|
||||
:decode/json decode-json
|
||||
:encode/string encode-string
|
||||
::oapi/type "array"
|
||||
::oapi/format "set"
|
||||
@@ -646,7 +667,7 @@
|
||||
:gen/gen (-> :string sg/generator sg/set)
|
||||
:decode/string (fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
||||
(into #{} non-empty-strings-xf v)))
|
||||
(into #{} xf:filter-word-strings v)))
|
||||
::oapi/type "array"
|
||||
::oapi/format "set"
|
||||
::oapi/items {:type "string"}
|
||||
@@ -662,7 +683,7 @@
|
||||
:gen/gen (-> :keyword sg/generator sg/set)
|
||||
:decode/string (fn [v]
|
||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
||||
(into #{} (comp non-empty-strings-xf (map keyword)) v)))
|
||||
(into #{} (comp xf:filter-word-strings (map keyword)) v)))
|
||||
::oapi/type "array"
|
||||
::oapi/format "set"
|
||||
::oapi/items {:type "string" :format "keyword"}
|
||||
|
||||
@@ -80,21 +80,23 @@
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:offset ::sm/safe-number]]]]])
|
||||
|
||||
(def schema:color-attrs
|
||||
[:map {:title "ColorAttrs"}
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:value {:optional true} [:maybe :string]]
|
||||
[:color {:optional true} [:maybe ::rgb-color]]
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:ref-id {:optional true} ::sm/uuid]
|
||||
[:ref-file {:optional true} ::sm/uuid]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
|
||||
(def schema:color
|
||||
[:and
|
||||
[:map {:title "Color"}
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:value {:optional true} [:maybe :string]]
|
||||
[:color {:optional true} [:maybe ::rgb-color]]
|
||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:ref-id {:optional true} ::sm/uuid]
|
||||
[:ref-file {:optional true} ::sm/uuid]
|
||||
[:gradient {:optional true} [:maybe schema:gradient]]
|
||||
[:image {:optional true} [:maybe schema:image-color]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]]
|
||||
[:and schema:color-attrs
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
(def schema:recent-color
|
||||
@@ -107,11 +109,11 @@
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
(sm/register! ::rgb-color type:rgb-color)
|
||||
|
||||
(sm/register! ::color schema:color)
|
||||
(sm/register! ::gradient schema:gradient)
|
||||
(sm/register! ::image-color schema:image-color)
|
||||
(sm/register! ::recent-color schema:recent-color)
|
||||
(sm/register! ::color-attrs schema:color-attrs)
|
||||
|
||||
(def check-color!
|
||||
(sm/check-fn schema:color))
|
||||
@@ -392,13 +394,22 @@
|
||||
|
||||
(process-shape-colors shape sync-color)))
|
||||
|
||||
(defn eq-recent-color?
|
||||
(defn- eq-recent-color?
|
||||
[c1 c2]
|
||||
(or (= c1 c2)
|
||||
(and (some? (:color c1))
|
||||
(some? (:color c2))
|
||||
(= (:color c1) (:color c2)))))
|
||||
|
||||
(defn add-recent-color
|
||||
"Moves the color to the top of the list and then truncates up to 15"
|
||||
[state file-id color]
|
||||
(update state file-id (fn [colors]
|
||||
(let [colors (d/removev (partial eq-recent-color? color) colors)
|
||||
colors (conj colors color)]
|
||||
(cond-> colors
|
||||
(> (count colors) 15)
|
||||
(subvec 1))))))
|
||||
|
||||
(defn stroke->color-att
|
||||
[stroke file-id shared-libs]
|
||||
|
||||
@@ -18,20 +18,20 @@
|
||||
;; SCHEMAS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(sm/register! ::flow
|
||||
[:map {:title "PageFlow"}
|
||||
(def schema:flow
|
||||
[:map {:title "Flow"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:starting-frame ::sm/uuid]])
|
||||
|
||||
(sm/register! ::guide
|
||||
[:map {:title "PageGuide"}
|
||||
(def schema:guide
|
||||
[:map {:title "Guide"}
|
||||
[:id ::sm/uuid]
|
||||
[:axis [::sm/one-of #{:x :y}]]
|
||||
[:position ::sm/safe-number]
|
||||
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sm/register! ::page
|
||||
(def schema:page
|
||||
[:map {:title "FilePage"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
@@ -42,11 +42,15 @@
|
||||
[:background {:optional true} ::ctc/rgb-color]
|
||||
[:saved-grids {:optional true} ::ctg/saved-grids]
|
||||
[:flows {:optional true}
|
||||
[:vector {:gen/max 2} ::flow]]
|
||||
[:vector {:gen/max 2} schema:flow]]
|
||||
[:guides {:optional true}
|
||||
[:map-of {:gen/max 2} ::sm/uuid ::guide]]
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]]]])
|
||||
|
||||
(sm/register! ::page schema:page)
|
||||
(sm/register! ::guide schema:guide)
|
||||
(sm/register! ::flow schema:flow)
|
||||
|
||||
(def check-page-guide!
|
||||
(sm/check-fn ::guide))
|
||||
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
|
||||
(sm/register! ::stroke schema:stroke)
|
||||
|
||||
(def check-stroke!
|
||||
(sm/check-fn schema:stroke))
|
||||
|
||||
(def schema:shape-base-attrs
|
||||
[:map {:title "ShapeMinimalRecord"}
|
||||
[:id ::sm/uuid]
|
||||
|
||||
@@ -1622,13 +1622,17 @@
|
||||
(defn remap-grid-cells
|
||||
"Remaps the shapes ids inside the cells"
|
||||
[shape ids-map]
|
||||
(let [do-remap-cells
|
||||
(let [remap-shape
|
||||
(fn [id]
|
||||
(get ids-map id id))
|
||||
|
||||
remap-cell
|
||||
(fn [cell]
|
||||
(-> cell
|
||||
(update :shapes #(into [] (keep ids-map) %))))
|
||||
(update :shapes #(into [] (keep remap-shape) %))))
|
||||
shape
|
||||
(-> shape
|
||||
(update :layout-grid-cells update-vals do-remap-cells))]
|
||||
(update :layout-grid-cells update-vals remap-cell))]
|
||||
shape))
|
||||
|
||||
(defn merge-cells
|
||||
|
||||
@@ -27,3 +27,6 @@
|
||||
[:color ::ctc/color]])
|
||||
|
||||
(sm/register! ::shadow schema:shadow)
|
||||
|
||||
(def check-shadow!
|
||||
(sm/check-fn schema:shadow))
|
||||
|
||||
@@ -26,16 +26,24 @@
|
||||
|
||||
(def ^:private
|
||||
schema:config
|
||||
(sm/define
|
||||
[:map {:title "config"}
|
||||
[:public-uri {:optional true} ::sm/uri]
|
||||
[:host {:optional true} :string]
|
||||
[:tenant {:optional true} :string]
|
||||
[:flags {:optional true} ::sm/set-of-keywords]
|
||||
[:redis-uri {:optional true} :string]
|
||||
[:tempdir {:optional true} :string]
|
||||
[:browser-pool-max {:optional true} :int]
|
||||
[:browser-pool-min {:optional true} :int]]))
|
||||
[:map {:title "config"}
|
||||
[:public-uri {:optional true} ::sm/uri]
|
||||
[:host {:optional true} :string]
|
||||
[:tenant {:optional true} :string]
|
||||
[:flags {:optional true} [::sm/set :keyword]]
|
||||
[:redis-uri {:optional true} :string]
|
||||
[:tempdir {:optional true} :string]
|
||||
[:browser-pool-max {:optional true} ::sm/int]
|
||||
[:browser-pool-min {:optional true} ::sm/int]])
|
||||
|
||||
(def ^:private decode-config
|
||||
(sm/decoder schema:config sm/string-transformer))
|
||||
|
||||
(def ^:private explain-config
|
||||
(sm/explainer schema:config))
|
||||
|
||||
(def ^:private valid-config?
|
||||
(sm/validator schema:config))
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
@@ -60,15 +68,15 @@
|
||||
[]
|
||||
(let [env (read-env "penpot")
|
||||
env (d/without-nils env)
|
||||
data (merge defaults env)]
|
||||
data (merge defaults env)
|
||||
data (decode-config data)]
|
||||
|
||||
(try
|
||||
(sm/conform! schema:config data)
|
||||
(catch :default cause
|
||||
(if-let [explain (some->> cause ex-data ::sm/explain)]
|
||||
(println (sm/humanize-explain explain))
|
||||
(js/console.error cause))
|
||||
(process/exit -1)))))
|
||||
(when-not (valid-config? data)
|
||||
(let [explain (explain-config data)]
|
||||
(println (sm/humanize-explain explain))
|
||||
(process/exit -1)))
|
||||
|
||||
data))
|
||||
|
||||
(def config
|
||||
(prepare-config))
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"highlight.js": "^11.9.0",
|
||||
"js-beautify": "^1.15.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"mousetrap": "^1.6.5",
|
||||
"opentype.js": "^1.3.4",
|
||||
|
||||
@@ -177,3 +177,15 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.pageName.click();
|
||||
await page.keyboard.press("ArrowLeft");
|
||||
|
||||
await expect(workspacePage.pageName).toHaveText("Page 1");
|
||||
});
|
||||
|
||||
18
frontend/resources/templates/challenge.mustache
Normal file
18
frontend/resources/templates/challenge.mustache
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Penpot - Challenge</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
var params = new URL(document.location.toString()).searchParams;
|
||||
var redirectPath = params.get("redirect");
|
||||
setTimeout(() => {
|
||||
location.href = "/#" + redirectPath;
|
||||
}, 100);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -415,6 +415,13 @@ async function generateTemplates() {
|
||||
|
||||
await fs.writeFile("./resources/public/index.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/challenge.mustache",
|
||||
{},
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./resources/public/challenge.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-body.mustache",
|
||||
{
|
||||
|
||||
@@ -143,4 +143,3 @@
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as s]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(declare update-profile-props)
|
||||
|
||||
;; --- SCHEMAS
|
||||
|
||||
(def ^:private
|
||||
@@ -49,14 +51,14 @@
|
||||
|
||||
(defn get-current-team-id
|
||||
[profile]
|
||||
(let [team-id (::current-team-id @storage)]
|
||||
(let [team-id (::current-team-id @s/storage)]
|
||||
(or team-id (:default-team-id profile))))
|
||||
|
||||
(defn set-current-team!
|
||||
[team-id]
|
||||
(if (nil? team-id)
|
||||
(swap! storage dissoc ::current-team-id)
|
||||
(swap! storage assoc ::current-team-id team-id)))
|
||||
(swap! s/storage dissoc ::current-team-id)
|
||||
(swap! s/storage assoc ::current-team-id team-id)))
|
||||
|
||||
;; --- EVENT: fetch-teams
|
||||
|
||||
@@ -76,9 +78,9 @@
|
||||
;; if not, dissoc it from storage.
|
||||
|
||||
(let [ids (into #{} (map :id) teams)]
|
||||
(when-let [ctid (::current-team-id @storage)]
|
||||
(when-let [ctid (::current-team-id @s/storage)]
|
||||
(when-not (contains? ids ctid)
|
||||
(swap! storage dissoc ::current-team-id)))))))
|
||||
(swap! s/storage dissoc ::current-team-id)))))))
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
@@ -129,21 +131,34 @@
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)
|
||||
email (:email profile)
|
||||
previous-profile (:profile @storage)
|
||||
previous-profile (:profile @s/storage)
|
||||
previous-email (:email previous-profile)]
|
||||
(when profile
|
||||
(swap! storage assoc :profile profile)
|
||||
(swap! s/storage assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(when (not= previous-email email)
|
||||
(set-current-team! nil)))))))
|
||||
|
||||
(defn- on-fetch-profile-exception
|
||||
[cause]
|
||||
(let [data (ex-data cause)]
|
||||
(if (and (= :authorization (:type data))
|
||||
(= :challenge-required (:code data)))
|
||||
(let [path (rt/get-current-path)
|
||||
href (->> path
|
||||
(js/encodeURIComponent)
|
||||
(str "/challenge.html?redirect="))]
|
||||
(rx/of (rt/nav-raw :href href)))
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn fetch-profile
|
||||
[]
|
||||
(ptk/reify ::fetch-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/map profile-fetched)))))
|
||||
(rx/map profile-fetched)
|
||||
(rx/catch on-fetch-profile-exception)))))
|
||||
|
||||
;; --- EVENT: login
|
||||
|
||||
@@ -152,9 +167,27 @@
|
||||
profile. The profile can proceed from standard login or from
|
||||
accepting invitation, or third party auth signup or singin."
|
||||
[profile]
|
||||
(letfn [(get-redirect-event []
|
||||
(let [team-id (get-current-team-id profile)]
|
||||
(rt/nav' :dashboard-projects {:team-id team-id})))]
|
||||
(letfn [(get-redirect-events []
|
||||
(let [team-id (get-current-team-id profile)
|
||||
welcome-file-id (dm/get-in profile [:props :welcome-file-id])
|
||||
redirect-href (:login-redirect @s/session)
|
||||
current-href (rt/get-current-href)]
|
||||
|
||||
(cond
|
||||
(some? redirect-href)
|
||||
(binding [s/*sync* true]
|
||||
(swap! s/session dissoc :login-redirect)
|
||||
(if (= current-href redirect-href)
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
|
||||
(some? welcome-file-id)
|
||||
(rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
|
||||
:file-id welcome-file-id})
|
||||
(update-profile-props {:welcome-file-id nil}))
|
||||
|
||||
:else
|
||||
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
@@ -171,10 +204,11 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (is-authenticated? profile)
|
||||
(->> (rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(get-redirect-event)
|
||||
(ws/initialize))
|
||||
(->> (rx/concat
|
||||
(rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(ws/initialize))
|
||||
(get-redirect-events))
|
||||
(rx/observe-on :async)))))))
|
||||
|
||||
(declare login-from-register)
|
||||
@@ -311,7 +345,7 @@
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(set-current-team! nil)))))
|
||||
(swap! s/storage (constantly {}))))))
|
||||
|
||||
(defn logout
|
||||
([] (logout {}))
|
||||
|
||||
@@ -79,12 +79,14 @@
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def default-workspace-local {:zoom 1})
|
||||
(log/set-level! :debug)
|
||||
@@ -335,6 +337,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state
|
||||
:recent-colors (:recent-colors @storage)
|
||||
:workspace-ready? false
|
||||
:current-file-id file-id
|
||||
:current-project-id project-id
|
||||
@@ -1549,15 +1552,40 @@
|
||||
shapes (->> (cfh/selected-with-children objects selected)
|
||||
(keep (d/getf objects)))]
|
||||
|
||||
(->> (rx/from shapes)
|
||||
(rx/merge-map (partial prepare-object objects frame-id))
|
||||
(rx/reduce collect-data initial)
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map (partial advance-copies state selected))
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))))))))
|
||||
;; The clipboard API doesn't handle well asynchronous calls because it expects to use
|
||||
;; the clipboard in an user interaction. If you do an async call the callback is outside
|
||||
;; the thread of the UI and so Safari blocks the copying event.
|
||||
;; We use the API `ClipboardItem` that allows promises to be passed and so the event
|
||||
;; will wait for the promise to resolve and everything should work as expected.
|
||||
;; This only works in the current versions of the browsers.
|
||||
(if (some? (unchecked-get ug/global "ClipboardItem"))
|
||||
(let [resolve-data-promise
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(->> (rx/from shapes)
|
||||
(rx/merge-map (partial prepare-object objects frame-id))
|
||||
(rx/reduce collect-data initial)
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map (partial advance-copies state selected))
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
|
||||
;; FIXME: this is to support Firefox versions below 116 that don't support `ClipboardItem`
|
||||
;; after the version 116 is less common we could remove this.
|
||||
;; https://caniuse.com/?search=ClipboardItem
|
||||
(->> (rx/from shapes)
|
||||
(rx/merge-map (partial prepare-object objects frame-id))
|
||||
(rx/reduce collect-data initial)
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map (partial advance-copies state selected))
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))
|
||||
|
||||
(declare ^:private paste-transit)
|
||||
(declare ^:private paste-text)
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.shape :refer [check-stroke!]]
|
||||
[app.common.types.shape.shadow :refer [check-shadow!]]
|
||||
[app.main.broadcast :as mbc]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as md]
|
||||
@@ -21,7 +24,6 @@
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.util.color :as uc]
|
||||
[app.util.storage :refer [storage]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@@ -165,6 +167,15 @@
|
||||
|
||||
(defn add-fill
|
||||
[ids color]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid color struct"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::add-fill
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -175,6 +186,15 @@
|
||||
|
||||
(defn remove-fill
|
||||
[ids color position]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid color struct"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::remove-fill
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -187,13 +207,21 @@
|
||||
|
||||
(defn remove-all-fills
|
||||
[ids color]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid color struct"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::remove-all-fills
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [remove-all (fn [shape _] (assoc shape :fills []))]
|
||||
(transform-fill state ids color remove-all)))))
|
||||
|
||||
|
||||
(defn change-hide-fill-on-export
|
||||
[ids hide-fill-on-export]
|
||||
(ptk/reify ::change-hide-fill-on-export
|
||||
@@ -272,17 +300,25 @@
|
||||
;; example using the color selection from
|
||||
;; multiple shapes) let's use the first stop
|
||||
;; color
|
||||
attrs (cond-> attrs
|
||||
(:gradient attrs) (get-in [:gradient :stops 0]))
|
||||
new-attrs (-> (merge (get-in shape [:shadow index :color]) attrs)
|
||||
(d/without-nils))]
|
||||
(assoc-in shape [:shadow index :color] new-attrs))))))))
|
||||
attrs (cond-> attrs
|
||||
(:gradient attrs)
|
||||
(dm/get-in [:gradient :stops 0]))
|
||||
|
||||
attrs' (-> (dm/get-in shape [:shadow index :color])
|
||||
(merge attrs)
|
||||
(d/without-nils))]
|
||||
(assoc-in shape [:shadow index :color] attrs'))))))))
|
||||
|
||||
(defn add-shadow
|
||||
[ids shadow]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid shadow struct"
|
||||
(check-shadow! shadow))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(sm/check-coll-of-uuid! ids))
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::add-shadow
|
||||
ptk/WatchEvent
|
||||
@@ -293,6 +329,15 @@
|
||||
|
||||
(defn add-stroke
|
||||
[ids stroke]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid stroke struct"
|
||||
(check-stroke! stroke))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::add-stroke
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -301,6 +346,11 @@
|
||||
|
||||
(defn remove-stroke
|
||||
[ids position]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::remove-stroke
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -314,6 +364,11 @@
|
||||
|
||||
(defn remove-all-strokes
|
||||
[ids]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid coll of uuid's"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::remove-all-strokes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -376,7 +431,7 @@
|
||||
:on-change handle-change-color}
|
||||
:allow-click-outside true})))))))
|
||||
|
||||
(defn color-att->text
|
||||
(defn- color-att->text
|
||||
[color]
|
||||
{:fill-color (when (:color color) (str/lower (:color color)))
|
||||
:fill-opacity (:opacity color)
|
||||
@@ -395,26 +450,57 @@
|
||||
(some? has-color?)
|
||||
(assoc-in [:fills index] parsed-new-color))))
|
||||
|
||||
(def ^:private schema:change-color-operation
|
||||
[:map
|
||||
[:prop [:enum :fill :stroke :shadow :content]]
|
||||
[:shape-id ::sm/uuid]
|
||||
[:index :int]])
|
||||
|
||||
(def ^:private schema:change-color-operations
|
||||
[:vector schema:change-color-operation])
|
||||
|
||||
(def ^:private check-change-color-operations!
|
||||
(sm/check-fn schema:change-color-operations))
|
||||
|
||||
(defn change-color-in-selected
|
||||
[new-color shapes-by-color old-color]
|
||||
[operations new-color old-color]
|
||||
|
||||
(dm/verify!
|
||||
"expected valid change color operations"
|
||||
(check-change-color-operations! operations))
|
||||
|
||||
(dm/verify!
|
||||
"expected a valid color struct for new-color param"
|
||||
(ctc/check-color! new-color))
|
||||
|
||||
(dm/verify!
|
||||
"expected a valid color struct for old-color param"
|
||||
(ctc/check-color! old-color))
|
||||
|
||||
(ptk/reify ::change-color-in-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(rx/concat
|
||||
(rx/of (dwu/start-undo-transaction undo-id))
|
||||
(->> (rx/from shapes-by-color)
|
||||
(rx/map (fn [shape] (case (:prop shape)
|
||||
:fill (change-fill [(:shape-id shape)] new-color (:index shape))
|
||||
:stroke (change-stroke [(:shape-id shape)] new-color (:index shape))
|
||||
:shadow (change-shadow [(:shape-id shape)] new-color (:index shape))
|
||||
:content (dwt/update-text-with-function
|
||||
(:shape-id shape)
|
||||
(partial change-text-color old-color new-color (:index shape)))))))
|
||||
(->> (rx/from operations)
|
||||
(rx/map (fn [{:keys [shape-id index] :as operation}]
|
||||
(case (:prop operation)
|
||||
:fill (change-fill [shape-id] new-color index)
|
||||
:stroke (change-stroke [shape-id] new-color index)
|
||||
:shadow (change-shadow [shape-id] new-color index)
|
||||
:content (dwt/update-text-with-function
|
||||
shape-id
|
||||
(partial change-text-color old-color new-color index))))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn apply-color-from-palette
|
||||
[color stroke?]
|
||||
|
||||
(dm/assert!
|
||||
"should be a valid color"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(ptk/reify ::apply-color-from-palette
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -437,9 +523,10 @@
|
||||
|
||||
result (cond-> result (not group?) (conj cur))]
|
||||
(recur (rest pending) result))))]
|
||||
|
||||
(if stroke?
|
||||
(rx/of (change-stroke ids (merge uc/empty-color color) 0))
|
||||
(rx/of (change-fill ids (merge uc/empty-color color) 0)))))))
|
||||
(rx/of (change-stroke ids color 0))
|
||||
(rx/of (change-fill ids color 0)))))))
|
||||
|
||||
(declare activate-colorpicker-color)
|
||||
(declare activate-colorpicker-gradient)
|
||||
@@ -448,15 +535,22 @@
|
||||
|
||||
(defn apply-color-from-colorpicker
|
||||
[color]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid color structure"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(ptk/reify ::apply-color-from-colorpicker
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(cond
|
||||
(:image color) (activate-colorpicker-image)
|
||||
(:color color) (activate-colorpicker-color)
|
||||
(= :linear (get-in color [:gradient :type])) (activate-colorpicker-gradient :linear-gradient)
|
||||
(= :radial (get-in color [:gradient :type])) (activate-colorpicker-gradient :radial-gradient))))))
|
||||
;; FIXME: revisit this
|
||||
(let [gradient-type (dm/get-in color [:gradient :type])]
|
||||
(rx/of
|
||||
(cond
|
||||
(:image color) (activate-colorpicker-image)
|
||||
(:color color) (activate-colorpicker-color)
|
||||
(= :linear gradient-type) (activate-colorpicker-gradient :linear-gradient)
|
||||
(= :radial gradient-type) (activate-colorpicker-gradient :radial-gradient)))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -596,7 +690,8 @@
|
||||
(update :current-color merge changes)
|
||||
(update :current-color materialize-color-components)
|
||||
(update :current-color #(if (not= type :image) (dissoc % :image) %))
|
||||
;; current color can be a library one I'm changing via colorpicker
|
||||
;; current color can be a library one
|
||||
;; I'm changing via colorpicker
|
||||
(d/dissoc-in [:current-color :id])
|
||||
(d/dissoc-in [:current-color :file-id]))]
|
||||
(if-let [stop (:editing-stop state)]
|
||||
@@ -614,7 +709,8 @@
|
||||
:colorpicker
|
||||
:type)
|
||||
formated-color (get-color-from-colorpicker-state (:colorpicker state))
|
||||
;; Type is set to color on closing the colorpicker, but we can can close it while still uploading an image fill
|
||||
;; Type is set to color on closing the colorpicker, but we
|
||||
;; can can close it while still uploading an image fill
|
||||
ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))]
|
||||
(when (and add-recent? (not ignore-color?))
|
||||
(rx/of (dwl/add-recent-color formated-color)))))))
|
||||
@@ -686,6 +782,7 @@
|
||||
|
||||
(defn select-color
|
||||
[position add-color]
|
||||
;; FIXME: revisit
|
||||
(ptk/reify ::select-color
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as s]
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@@ -115,8 +116,13 @@
|
||||
(update :id #(or % (uuid/next)))
|
||||
(assoc :name (or (get-in color [:image :name])
|
||||
(:color color)
|
||||
(uc/gradient-type->string (get-in color [:gradient :type])))))]
|
||||
(dm/assert! ::ctc/color color)
|
||||
(uc/gradient-type->string (get-in color [:gradient :type]))))
|
||||
(d/without-nils))]
|
||||
|
||||
(dm/assert!
|
||||
"expect valid color structure"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(ptk/reify ::add-color
|
||||
ev/Event
|
||||
(-data [_] color)
|
||||
@@ -132,16 +138,21 @@
|
||||
|
||||
(defn add-recent-color
|
||||
[color]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid recent color map"
|
||||
"expected valid recent color structure"
|
||||
(ctc/check-recent-color! color))
|
||||
|
||||
(ptk/reify ::add-recent-color
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-recent-color color))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [file-id (:current-file-id state)]
|
||||
(update state :recent-colors ctc/add-recent-color file-id color)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [recent-colors (:recent-colors state)]
|
||||
(swap! s/storage assoc :recent-colors recent-colors)))))
|
||||
|
||||
(def clear-color-for-rename
|
||||
(ptk/reify ::clear-color-for-rename
|
||||
@@ -149,7 +160,7 @@
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :color-for-rename] nil))))
|
||||
|
||||
(defn- do-update-color
|
||||
(defn- update-color*
|
||||
[it state color file-id]
|
||||
(let [data (get state :workspace-data)
|
||||
[path name] (cfh/parse-path-name (:name color))
|
||||
@@ -165,16 +176,20 @@
|
||||
|
||||
(defn update-color
|
||||
[color file-id]
|
||||
(let [color (d/without-nils color)]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(and (ctc/check-color! color)
|
||||
(uuid? file-id)))
|
||||
(dm/assert!
|
||||
"expected valid color data structure"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(ptk/reify ::update-color
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(do-update-color it state color file-id))))
|
||||
(dm/assert!
|
||||
"expected file-id"
|
||||
(uuid? file-id))
|
||||
|
||||
(ptk/reify ::update-color
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(update-color* it state color file-id)))))
|
||||
|
||||
(defn rename-color
|
||||
[file-id id new-name]
|
||||
@@ -189,9 +204,10 @@
|
||||
(if (str/empty? new-name)
|
||||
(rx/empty)
|
||||
(let [data (get state :workspace-data)
|
||||
object (get-in data [:colors id])
|
||||
object (assoc object :name new-name)]
|
||||
(do-update-color it state object file-id)))))))
|
||||
color (get-in data [:colors id])
|
||||
color (assoc color :name new-name)
|
||||
color (d/without-nils color)]
|
||||
(update-color* it state color file-id)))))))
|
||||
|
||||
(defn delete-color
|
||||
[{:keys [id] :as params}]
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
(let [id (d/nilv id (uuid/next))
|
||||
page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame-id (ctst/top-nested-frame objects position)
|
||||
selected (if ignore-selection? #{} (wsh/lookup-selected state))
|
||||
base (cfh/get-base-shape objects selected)
|
||||
|
||||
@@ -81,9 +80,16 @@
|
||||
selected-frame? (and (= 1 (count selected))
|
||||
(= :frame (dm/get-in objects [selected-id :type])))
|
||||
|
||||
base-id (:parent-id base)
|
||||
|
||||
frame-id (if (or selected-frame? (empty? selected)
|
||||
(not= :frame (dm/get-in objects [base-id :type])))
|
||||
(ctst/top-nested-frame objects position)
|
||||
base-id)
|
||||
|
||||
parent-id (if (or selected-frame? (empty? selected))
|
||||
frame-id
|
||||
(:parent-id base))
|
||||
base-id)
|
||||
|
||||
[new-shape new-children]
|
||||
(csvg.shapes-builder/create-svg-shapes id svg-data position objects frame-id parent-id selected true)
|
||||
|
||||
@@ -236,9 +236,10 @@
|
||||
=))
|
||||
|
||||
(def workspace-recent-colors
|
||||
(l/derived (fn [data]
|
||||
(get data :recent-colors []))
|
||||
workspace-data))
|
||||
(l/derived (fn [state]
|
||||
(when-let [file-id (:current-file-id state)]
|
||||
(dm/get-in state [:recent-colors file-id])))
|
||||
st/state))
|
||||
|
||||
(def workspace-recent-fonts
|
||||
(l/derived (fn [data]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn handle-response
|
||||
[{:keys [status body] :as response}]
|
||||
[{:keys [status body headers] :as response}]
|
||||
(cond
|
||||
(= 204 status)
|
||||
;; We need to send "something" so the streams listening downstream can act
|
||||
@@ -40,6 +40,13 @@
|
||||
{:type :validation
|
||||
:code :request-body-too-large}))
|
||||
|
||||
(and (= status 403)
|
||||
(or (= "cloudflare" (get headers "server"))
|
||||
(= "challenge" (get headers "cf-mitigated"))))
|
||||
(rx/throw (ex-info "http error"
|
||||
{:type :authorization
|
||||
:code :challenge-required}))
|
||||
|
||||
(and (>= status 400) (map? body))
|
||||
(rx/throw (ex-info "http error" body))
|
||||
|
||||
@@ -48,6 +55,7 @@
|
||||
(ex-info "http error"
|
||||
{:type :unexpected-error
|
||||
:status status
|
||||
:headers headers
|
||||
:data body}))))
|
||||
|
||||
(def default-options
|
||||
|
||||
@@ -42,9 +42,34 @@
|
||||
(mf/lazy-component app.main.ui.workspace/workspace))
|
||||
|
||||
(mf/defc main-page
|
||||
{::mf/props :obj}
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [route profile]}]
|
||||
(let [{:keys [data params]} route]
|
||||
(let [{:keys [data params]} route
|
||||
props (get profile :props)
|
||||
show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-questions)))
|
||||
|
||||
show-newsletter-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :newsletter-updates))
|
||||
(contains? props :onboarding-questions))
|
||||
|
||||
show-team-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
(case (:name data)
|
||||
(:auth-login
|
||||
@@ -84,42 +109,19 @@
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
(when-let [props (get profile :props)]
|
||||
(let [show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-questions)))
|
||||
|
||||
show-newsletter-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :newsletter-updates))
|
||||
(contains? props :onboarding-questions))
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-team-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? true}]
|
||||
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])))
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
|
||||
[:& dashboard-page {:route route :profile profile}]]
|
||||
:viewer
|
||||
@@ -154,6 +156,20 @@
|
||||
page-id (some-> params :query :page-id uuid)
|
||||
layout (some-> params :query :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? false}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
|
||||
[:& workspace-page {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as s]
|
||||
[beicon.v2.core :as rx]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -44,13 +45,28 @@
|
||||
[]
|
||||
(st/emit! (du/create-demo-profile)))
|
||||
|
||||
(defn- store-login-redirect
|
||||
[save-login-redirect]
|
||||
(binding [s/*sync* true]
|
||||
(if (some? save-login-redirect)
|
||||
;; Save the current login raw uri for later redirect user back to
|
||||
;; the same page, we need it to be synchronous because the user is
|
||||
;; going to be redirected instantly to the oidc provider uri
|
||||
(swap! s/session assoc :login-redirect (rt/get-current-href))
|
||||
;; Clean the login redirect
|
||||
(swap! s/session dissoc :login-redirect))))
|
||||
|
||||
(defn- login-with-oidc
|
||||
[event provider params]
|
||||
(dom/prevent-default event)
|
||||
|
||||
(store-login-redirect (:save-login-redirect params))
|
||||
|
||||
;; FIXME: this code should be probably moved outside of the UI
|
||||
(->> (rp/cmd! :login-with-oidc (assoc params :provider provider))
|
||||
(rx/subs! (fn [{:keys [redirect-uri] :as rsp}]
|
||||
(if redirect-uri
|
||||
(.replace js/location redirect-uri)
|
||||
(st/emit! (rt/nav-raw :uri redirect-uri))
|
||||
(log/error :hint "unexpected response from OIDC method"
|
||||
:resp (pr-str rsp))))
|
||||
(fn [cause]
|
||||
@@ -119,6 +135,7 @@
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form _event]
|
||||
(store-login-redirect (:save-login-redirect params))
|
||||
(reset! error nil)
|
||||
(let [params (with-meta (:clean-data @form)
|
||||
{:on-error on-error
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
form (fm/use-form :schema schema:register-form
|
||||
:initial initial)
|
||||
|
||||
submitted? (mf/use-state false)
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
@@ -176,7 +177,9 @@
|
||||
::mf/private true}
|
||||
[{:keys [params on-success-callback]}]
|
||||
(let [form (fm/use-form :schema schema:register-validate-form :initial params)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
@@ -208,7 +211,13 @@
|
||||
(mf/deps on-success on-error)
|
||||
(fn [form _]
|
||||
(reset! submitted? true)
|
||||
(let [params (:clean-data @form)]
|
||||
(let [create-welcome-file?
|
||||
(cf/external-feature-flag "onboarding-03" "test")
|
||||
|
||||
params
|
||||
(cond-> (:clean-data @form)
|
||||
create-welcome-file? (assoc :create-welcome-file true))]
|
||||
|
||||
(->> (rp/cmd! :register-profile params)
|
||||
(rx/finalize #(reset! submitted? false))
|
||||
(rx/subs! on-success on-error)))))]
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
on-focus (unchecked-get props "on-focus")
|
||||
on-blur (unchecked-get props "on-blur")
|
||||
placeholder (unchecked-get props "placeholder")
|
||||
max-length (unchecked-get props "max-length")
|
||||
on-change (unchecked-get props "on-change")
|
||||
on-esc (unchecked-get props "on-esc")
|
||||
on-ctrl-enter (unchecked-get props "on-ctrl-enter")
|
||||
@@ -88,7 +89,8 @@
|
||||
:on-blur on-blur
|
||||
:value value
|
||||
:placeholder placeholder
|
||||
:on-change on-change*}]))
|
||||
:on-change on-change*
|
||||
:max-length max-length}]))
|
||||
|
||||
(mf/defc reply-form
|
||||
[{:keys [thread] :as props}]
|
||||
@@ -128,7 +130,8 @@
|
||||
:on-focus on-focus
|
||||
:select-on-focus? false
|
||||
:on-ctrl-enter on-submit
|
||||
:on-change on-change}]
|
||||
:on-change on-change
|
||||
:max-length 750}]
|
||||
(when (or @show-buttons? (seq @content))
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
[:input.btn-secondary
|
||||
@@ -196,7 +199,8 @@
|
||||
:select-on-focus? false
|
||||
:on-esc on-esc
|
||||
:on-change on-change
|
||||
:on-ctrl-enter on-submit}]
|
||||
:on-ctrl-enter on-submit
|
||||
:max-length 750}]
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
|
||||
[:input {:on-click on-esc
|
||||
@@ -233,7 +237,8 @@
|
||||
:select-on-focus true
|
||||
:select-on-focus? false
|
||||
:on-ctrl-enter on-submit*
|
||||
:on-change on-change}]
|
||||
:on-change on-change
|
||||
:max-length 750}]
|
||||
[:div {:class (stl/css :buttons-wrapper)}
|
||||
[:input {:type "button"
|
||||
:value "Cancel"
|
||||
|
||||
@@ -42,9 +42,7 @@
|
||||
(let [search-term (get-in route [:params :query :search-term])
|
||||
team-id (get-in route [:params :path :team-id])
|
||||
project-id (get-in route [:params :path :project-id])]
|
||||
(cond->
|
||||
{:search-term search-term}
|
||||
|
||||
(cond-> {:search-term search-term}
|
||||
(uuid-str? team-id)
|
||||
(assoc :team-id (uuid team-id))
|
||||
|
||||
@@ -84,10 +82,10 @@
|
||||
|
||||
(mf/use-effect on-resize)
|
||||
|
||||
|
||||
[:div {:class (stl/css :dashboard-content)
|
||||
:style {:pointer-events (when file-menu-open? "none")}
|
||||
:on-click clear-selected-fn :ref container}
|
||||
:on-click clear-selected-fn
|
||||
:ref container}
|
||||
(case section
|
||||
:dashboard-projects
|
||||
[:*
|
||||
@@ -146,7 +144,8 @@
|
||||
(l/derived :current-team-id st/state))
|
||||
|
||||
(mf/defc dashboard
|
||||
[{:keys [route profile] :as props}]
|
||||
{::mf/props :obj}
|
||||
[{:keys [route profile]}]
|
||||
(let [section (get-in route [:data :name])
|
||||
params (parse-params route)
|
||||
|
||||
@@ -181,13 +180,13 @@
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
;; the :current-team-id on the state. We set the key to the
|
||||
;; team-id because we want to completely refresh all the
|
||||
;; components on team change. Many components assumes that the
|
||||
;; team is already set so don't put the team into mf/deps.
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
;; the :current-team-id on the state. We set the key to the
|
||||
;; team-id because we want to completely refresh all the
|
||||
;; components on team change. Many components assumes that the
|
||||
;; team is already set so don't put the team into mf/deps.
|
||||
(when (and team initialized?)
|
||||
[:main {:class (stl/css :dashboard)
|
||||
:key (:id team)}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
(mf/defc search-page
|
||||
[{:keys [team search-term] :as props}]
|
||||
(let [result (mf/deref refs/dashboard-search-result)
|
||||
(let [search-term (or search-term "")
|
||||
result (mf/deref refs/dashboard-search-result)
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)]
|
||||
|
||||
(mf/use-effect
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
@@ -30,7 +29,6 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -129,17 +127,10 @@
|
||||
]
|
||||
(filterv identity)))
|
||||
|
||||
(s/def ::emails (s/and ::us/set-of-valid-emails d/not-empty?))
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
(s/def ::invite-member-form
|
||||
(s/keys :req-un [::role ::emails ::team-id]))
|
||||
|
||||
(def ^:private schema:invite-member-form
|
||||
[:map {:title "InviteMemberForm"}
|
||||
[:role :keyword]
|
||||
[:emails [::sm/set {:kind ::sm/email :min 1}]]
|
||||
[:emails [::sm/set {:min 1} ::sm/email]]
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(mf/defc invite-members-modal
|
||||
@@ -181,6 +172,10 @@
|
||||
(st/emit! (ntf/error (tr "errors.profile-is-muted"))
|
||||
(modal/hide))
|
||||
|
||||
(and (= :validation type)
|
||||
(= :max-invitations-by-request code))
|
||||
(swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
|
||||
|
||||
(or (= :member-is-muted code)
|
||||
(= :email-has-permanent-bounces code)
|
||||
(= :email-has-complaints code))
|
||||
@@ -226,10 +221,9 @@
|
||||
:name :emails
|
||||
:auto-focus? true
|
||||
:trim true
|
||||
:valid-item-fn us/parse-email
|
||||
:valid-item-fn sm/parse-email
|
||||
:caution-item-fn current-members-emails
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit
|
||||
:invite-email invite-email}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
@@ -519,8 +519,10 @@
|
||||
@include bodySmallTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.custom-input-checkbox {
|
||||
// TODO: This fix is temporary, the error is caused by the
|
||||
// cascading order of the compiled css files.
|
||||
// https://tree.taiga.io/project/penpot/task/8658
|
||||
.custom-input-checkbox.custom-input-checkbox {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,9 @@
|
||||
[{:keys [default-project-id profile project-id team-id]}]
|
||||
(let [templates (mf/deref builtin-templates)
|
||||
templates (mf/with-memo [templates]
|
||||
(filterv #(not= (:id %) "tutorial-for-beginners") templates))
|
||||
(filterv #(and
|
||||
(not= (:id %) "welcome")
|
||||
(not= (:id %) "tutorial-for-beginners")) templates))
|
||||
|
||||
route (mf/deref refs/route)
|
||||
route-name (get-in route [:data :name])
|
||||
|
||||
@@ -111,14 +111,15 @@
|
||||
[:action-button-position {:optional true}
|
||||
[:enum "start" "end"]]
|
||||
[:default-selected {:optional true} :string]
|
||||
[:selected {:optional true} :string]
|
||||
[:tabs [:vector {:min 1} schema:tab]]])
|
||||
|
||||
(mf/defc tab-switcher*
|
||||
{::mf/props :obj
|
||||
::mf/schema schema:tab-switcher}
|
||||
[{:keys [class tabs on-change-tab default-selected action-button-position action-button] :rest props}]
|
||||
(let [selected* (mf/use-state #(get-selected-tab-id tabs default-selected))
|
||||
selected (deref selected*)
|
||||
[{:keys [class tabs on-change-tab default-selected selected action-button-position action-button] :rest props}]
|
||||
(let [selected* (mf/use-state #(or selected (get-selected-tab-id tabs default-selected)))
|
||||
selected (or selected (deref selected*))
|
||||
|
||||
tabs-nodes-refs (mf/use-ref nil)
|
||||
tabs-ref (mf/use-ref nil)
|
||||
@@ -175,8 +176,7 @@
|
||||
|
||||
class (dm/str class " " (stl/css :tabs))
|
||||
|
||||
props (mf/spread-props props {:class class
|
||||
:on-key-down on-key-down})]
|
||||
props (mf/spread-props props {:class class})]
|
||||
|
||||
(mf/with-effect [tabs]
|
||||
(mf/set-ref-val! tabs-ref tabs))
|
||||
@@ -188,6 +188,7 @@
|
||||
:tabs tabs
|
||||
:on-ref on-ref
|
||||
:selected selected
|
||||
:on-key-down on-key-down
|
||||
:on-click on-click}]]
|
||||
|
||||
(let [active-tab (get-tab tabs selected)
|
||||
|
||||
@@ -101,7 +101,17 @@
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
--tab-panel-outline-color: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
--tab-panel-outline-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||
}
|
||||
|
||||
@@ -369,7 +369,9 @@
|
||||
selected (:selected state)
|
||||
status (:status state)
|
||||
|
||||
|
||||
;; We've deprecated the merge option on non-binary files because it wasn't working
|
||||
;; and we're planning to remove this export in future releases.
|
||||
export-types (if binary? export-types [:all :detach])
|
||||
|
||||
start-export
|
||||
(mf/use-fn
|
||||
|
||||
@@ -260,6 +260,14 @@
|
||||
(when ^boolean obj
|
||||
(apply (.-f obj) args)))))))
|
||||
|
||||
(defn use-ref-value
|
||||
"Returns a ref that will be automatically updated when the value is changed"
|
||||
[v]
|
||||
(let [ref (mf/use-ref v)]
|
||||
(mf/with-effect [v]
|
||||
(mf/set-ref-val! ref v))
|
||||
ref))
|
||||
|
||||
(defn use-equal-memo
|
||||
[val]
|
||||
(let [ref (mf/use-ref nil)]
|
||||
@@ -294,19 +302,21 @@
|
||||
`key` for new values."
|
||||
[key default]
|
||||
(let [id (mf/use-id)
|
||||
state (mf/use-state (get @storage key default))
|
||||
state* (mf/use-state #(get @storage key default))
|
||||
state (deref state*)
|
||||
stream (mf/with-memo [id]
|
||||
(->> mbc/stream
|
||||
(rx/filter #(not= (:id %) id))
|
||||
(rx/filter #(= (:type %) key))
|
||||
(rx/map deref)))]
|
||||
|
||||
(mf/with-effect [@state key id]
|
||||
(mbc/emit! id key @state)
|
||||
(swap! storage assoc key @state))
|
||||
(mf/with-effect [state key id]
|
||||
(mbc/emit! id key state)
|
||||
(swap! storage assoc key state))
|
||||
|
||||
(use-stream stream (partial reset! state))
|
||||
state))
|
||||
(use-stream stream (partial reset! state*))
|
||||
|
||||
state*))
|
||||
|
||||
(defonce ^:private intersection-subject (rx/subject))
|
||||
(defonce ^:private intersection-observer
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.main.ui.hooks.resize
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as log]
|
||||
@@ -20,6 +21,15 @@
|
||||
|
||||
(def last-resize-type nil)
|
||||
|
||||
(defn- get-initial-state
|
||||
[initial file-id key]
|
||||
(let [saved (dm/get-in @storage [::state file-id key])]
|
||||
(d/nilv saved initial)))
|
||||
|
||||
(defn- update-persistent-state
|
||||
[data file-id key size]
|
||||
(update-in data [::state file-id] assoc key size))
|
||||
|
||||
(defn set-resize-type! [type]
|
||||
(set! last-resize-type type))
|
||||
|
||||
@@ -28,26 +38,28 @@
|
||||
(use-resize-hook key initial min-val max-val axis negate? resize-type nil))
|
||||
|
||||
([key initial min-val max-val axis negate? resize-type on-change-size]
|
||||
(let [current-file-id (mf/use-ctx ctx/current-file-id)
|
||||
size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial))
|
||||
parent-ref (mf/use-ref nil)
|
||||
(let [file-id (mf/use-ctx ctx/current-file-id)
|
||||
|
||||
dragging-ref (mf/use-ref false)
|
||||
current-size* (mf/use-state #(get-initial-state initial file-id key))
|
||||
current-size (deref current-size*)
|
||||
|
||||
parent-ref (mf/use-ref nil)
|
||||
dragging-ref (mf/use-ref false)
|
||||
start-size-ref (mf/use-ref nil)
|
||||
start-ref (mf/use-ref nil)
|
||||
start-ref (mf/use-ref nil)
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-callback
|
||||
(mf/deps @size-state)
|
||||
(mf/use-fn
|
||||
(mf/deps current-size)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! start-size-ref @size-state)
|
||||
(mf/set-ref-val! start-size-ref current-size)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! start-ref (dom/get-client-position event))
|
||||
(set! last-resize-type resize-type)))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! start-size-ref nil)
|
||||
@@ -56,40 +68,39 @@
|
||||
(set! last-resize-type nil)))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-callback
|
||||
(mf/deps min-val max-val negate?)
|
||||
(mf/use-fn
|
||||
(mf/deps min-val max-val negate? file-id key)
|
||||
(fn [event]
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(let [start (mf/ref-val start-ref)
|
||||
pos (dom/get-client-position event)
|
||||
pos (dom/get-client-position event)
|
||||
delta (-> (gpt/to-vec start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
start-size (mf/ref-val start-size-ref)
|
||||
new-size (-> (+ start-size delta) (max min-val) (min max-val))]
|
||||
(reset! size-state new-size)
|
||||
(swap! storage assoc-in [::saved-resize current-file-id key] new-size)
|
||||
(when on-change-size (on-change-size new-size))))))
|
||||
(reset! current-size* new-size)
|
||||
(swap! storage update-persistent-state file-id key new-size)))))
|
||||
|
||||
set-size
|
||||
(mf/use-callback
|
||||
(mf/deps on-change-size)
|
||||
(mf/use-fn
|
||||
(mf/deps on-change-size file-id key)
|
||||
(fn [new-size]
|
||||
(let [new-size (mth/clamp new-size min-val max-val)]
|
||||
(reset! size-state new-size)
|
||||
(swap! storage assoc-in [::saved-resize current-file-id key] new-size)
|
||||
(when on-change-size (on-change-size new-size)))))]
|
||||
(reset! current-size* new-size)
|
||||
(swap! storage update-persistent-state file-id key new-size))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(when on-change-size (on-change-size @size-state))))
|
||||
(mf/with-effect [on-change-size current-size]
|
||||
(when on-change-size
|
||||
(on-change-size current-size)))
|
||||
|
||||
{:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:parent-ref parent-ref
|
||||
:set-size set-size
|
||||
:size @size-state})))
|
||||
:size current-size})))
|
||||
|
||||
(defn use-resize-observer
|
||||
[callback]
|
||||
|
||||
@@ -217,9 +217,9 @@
|
||||
[:team-size
|
||||
[:enum "more-than-50" "31-50" "11-30" "2-10" "freelancer" "personal-project"]]
|
||||
[:role
|
||||
[:enum "designer" "developer" "student-teacher" "graphic-design" "marketing" "manager" "other"]]
|
||||
[:enum "ux" "developer" "student-teacher" "designer" "marketing" "manager" "other"]]
|
||||
[:responsability
|
||||
[:enum "team-leader" "team-member" "freelancer" "ceo-founder" "director" "student-teacher" "other"]]
|
||||
[:enum "team-leader" "team-member" "freelancer" "ceo-founder" "director" "other"]]
|
||||
|
||||
[:role-other {:optional true} [::sm/text {:max 512}]]
|
||||
[:responsability-other {:optional true} [::sm/text {:max 512}]]]
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -57,7 +57,7 @@
|
||||
(def ^:private schema:invite-form
|
||||
[:map {:title "InviteForm"}
|
||||
[:role :keyword]
|
||||
[:emails [::sm/set {:kind ::sm/email}]]])
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]])
|
||||
|
||||
(defn- get-available-roles
|
||||
[]
|
||||
@@ -66,18 +66,15 @@
|
||||
|
||||
(mf/defc team-form-step-2
|
||||
{::mf/props :obj}
|
||||
[{:keys [name on-back]}]
|
||||
(let [initial (mf/use-memo
|
||||
#(do {:role "editor"
|
||||
:name name}))
|
||||
[{:keys [name on-back go-to-team?]}]
|
||||
(let [initial (mf/with-memo []
|
||||
{:role "editor" :name name})
|
||||
|
||||
form (fm/use-form :schema schema:invite-form
|
||||
:initial initial)
|
||||
|
||||
params (:clean-data @form)
|
||||
emails (:emails params)
|
||||
|
||||
roles (mf/use-memo get-available-roles)
|
||||
error* (mf/use-state nil)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
@@ -85,12 +82,29 @@
|
||||
(let [team-id (:id response)]
|
||||
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
|
||||
:onboarding-viewed true})
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))
|
||||
(when go-to-team?
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (ntf/error (tr "errors.generic")))))
|
||||
(fn [cause]
|
||||
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||
(cond
|
||||
(and (= :validation type)
|
||||
(= :profile-is-muted code))
|
||||
(swap! error* (tr "errors.profile-is-muted"))
|
||||
|
||||
(and (= :validation type)
|
||||
(= :max-invitations-by-request code))
|
||||
(swap! error* (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
|
||||
|
||||
(or (= :member-is-muted code)
|
||||
(= :email-has-permanent-bounces code)
|
||||
(= :email-has-complaints code))
|
||||
(swap! error* (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
||||
|
||||
:else
|
||||
(swap! error* (tr "errors.generic"))))))
|
||||
|
||||
on-invite-later
|
||||
(mf/use-fn
|
||||
@@ -110,7 +124,7 @@
|
||||
|
||||
on-invite-now
|
||||
(mf/use-fn
|
||||
(fn [{:keys [name] :as params}]
|
||||
(fn [{:keys [name emails] :as params}]
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
|
||||
@@ -142,6 +156,10 @@
|
||||
[:& fm/form {:form form
|
||||
:class (stl/css :modal-form-invitations)
|
||||
:on-submit on-submit}
|
||||
|
||||
(when-let [content (deref error*)]
|
||||
[:& context-notification {:content content :level :error}])
|
||||
|
||||
[:div {:class (stl/css :role-select)}
|
||||
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
|
||||
[:& fm/select {:name :role :options roles}]]
|
||||
@@ -154,18 +172,22 @@
|
||||
:valid-item-fn sm/parse-email
|
||||
:caution-item-fn #{}
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit}]]
|
||||
;; :on-submit on-submit
|
||||
}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :back-button)
|
||||
:on-click on-back}
|
||||
(tr "labels.back")]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
|
||||
(let [params (:clean-data @form)
|
||||
emails (:emails params)]
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}])]
|
||||
|
||||
[:div {:class (stl/css :modal-hint)}
|
||||
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]
|
||||
|
||||
@@ -240,7 +262,7 @@
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
[{:keys [go-to-team?]}]
|
||||
(let [name* (mf/use-state nil)
|
||||
name (deref name*)
|
||||
|
||||
@@ -262,6 +284,6 @@
|
||||
[:& left-sidebar]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
(if name
|
||||
[:& team-form-step-2 {:name name :on-back on-back}]
|
||||
[:& team-form-step-2 {:name name :on-back on-back :go-to-team? go-to-team?}]
|
||||
[:& team-form-step-1 {:on-submit on-submit}])]]))
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
[app.main.ui.releases.v1-9]
|
||||
[app.main.ui.releases.v2-0]
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -92,4 +93,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.1")))
|
||||
(rc/render-release-notes (assoc params :version "2.2")))
|
||||
|
||||
51
frontend/src/app/main/ui/releases/v2_2.cljs
Normal file
51
frontend/src/app/main/ui/releases/v2_2.cljs
Normal file
@@ -0,0 +1,51 @@
|
||||
;; 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.main.ui.releases.v2-2
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.2"
|
||||
[{:keys [slide klass finish version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[: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)}
|
||||
"What's new in Penpot? "]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This Penpot 2.2 release focuses on internal changes that are laying out the ground for the upcoming plugin system and substantial performance improvements."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This version also adds full JSON API interoperability and the brand-new Penpot’s Storybook!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Self-hosted Penpot installations will benefit from better file data storage and Penpot admins can now use the improved automatic snapshotting process when recovering old files."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Thanks again to our awesome community for their amazing contributions to this release!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click finish} "Let's go"]]]]]])))
|
||||
|
||||
79
frontend/src/app/main/ui/releases/v2_2.scss
Normal file
79
frontend/src/app/main/ui/releases/v2_2.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: $s-324 1fr;
|
||||
height: $s-480;
|
||||
width: $s-888;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: $s-324;
|
||||
border-radius: $br-8 0 0 $br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: $s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr $s-32;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include flexCenter;
|
||||
@include headlineSmallTypography;
|
||||
height: $s-32;
|
||||
width: $s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
width: $s-440;
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: $s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -106,9 +106,9 @@
|
||||
(st/emit! (rt/navigated match))
|
||||
|
||||
:else
|
||||
;; We just recheck with an additional profile request; this avoids
|
||||
;; some race conditions that causes unexpected redirects on
|
||||
;; invitations workflows (and probably other cases).
|
||||
;; We just recheck with an additional profile request; this
|
||||
;; avoids some race conditions that causes unexpected redirects
|
||||
;; on invitations workflows (and probably other cases).
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/subs! (fn [{:keys [id] :as profile}]
|
||||
(cond
|
||||
|
||||
@@ -7,19 +7,21 @@
|
||||
(ns app.main.ui.static
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
["rxjs" :as rxjs]
|
||||
[app.common.data :as d]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
|
||||
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
|
||||
[app.main.ui.auth.register :as register]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.header :as header]
|
||||
[app.main.ui.viewer.header :as viewer.header]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
@@ -29,10 +31,14 @@
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; FIXME: this is a workaround until we export this class on beicon library
|
||||
(def TimeoutError rxjs/TimeoutError)
|
||||
|
||||
(mf/defc error-container*
|
||||
{::mf/props :obj}
|
||||
[{:keys [children]}]
|
||||
(let [profile-id (:profile-id @st/state)]
|
||||
(let [profile-id (:profile-id @st/state)
|
||||
on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
|
||||
[:section {:class (stl/css :exception-layout)}
|
||||
[:button
|
||||
{:class (stl/css :exception-header)
|
||||
@@ -44,7 +50,7 @@
|
||||
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
|
||||
(when-not profile-id
|
||||
[:button {:class (stl/css :login-header)
|
||||
:on-click rt/nav-root}
|
||||
:on-click on-nav-root}
|
||||
(tr "labels.login")])
|
||||
|
||||
[:div {:class (stl/css :exception-content)}
|
||||
@@ -65,8 +71,8 @@
|
||||
{::mf/props :obj}
|
||||
[{:keys [show-dialog]}]
|
||||
(let [current-section (mf/use-state :login)
|
||||
user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
|
||||
set-section
|
||||
(mf/use-fn
|
||||
@@ -85,29 +91,37 @@
|
||||
#(reset! current-section :login))
|
||||
|
||||
success-login
|
||||
(fn []
|
||||
(reset! show-dialog false)
|
||||
(.reload js/window.location true))
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! show-dialog false)
|
||||
(st/emit! (rt/reload true))))
|
||||
|
||||
success-register
|
||||
(fn [data]
|
||||
(reset! register-token (:token data))
|
||||
(reset! current-section :register-validate))
|
||||
(mf/use-fn
|
||||
(fn [data]
|
||||
(reset! register-token (:token data))
|
||||
(reset! current-section :register-validate)))
|
||||
|
||||
register-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :register-email-sent))
|
||||
(mf/use-fn
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :register-email-sent)))
|
||||
|
||||
recovery-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :recovery-email-sent))]
|
||||
(mf/use-fn
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :recovery-email-sent)))
|
||||
|
||||
on-nav-root
|
||||
(mf/use-fn #(st/emit! (rt/nav-root)))]
|
||||
|
||||
[:div {:class (stl/css :overlay)}
|
||||
[:div {:class (stl/css :dialog-login)}
|
||||
[:div {:class (stl/css :modal-close)}
|
||||
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
|
||||
[:button {:class (stl/css :modal-close-button)
|
||||
:on-click on-nav-root}
|
||||
i/close]]
|
||||
[:div {:class (stl/css :login)}
|
||||
[:div {:class (stl/css :logo)} i/logo]
|
||||
@@ -118,19 +132,21 @@
|
||||
[:div {:class (stl/css :logo-title)} (tr "labels.login")]
|
||||
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
|
||||
[:& login-methods {:on-recovery-request set-section-recovery
|
||||
:on-success-callback success-login}]
|
||||
:on-success-callback success-login
|
||||
:params {:save-login-redirect true}}]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :change-section)}
|
||||
(tr "auth.register")
|
||||
" "
|
||||
[:a {:data-section "register"
|
||||
:on-click set-section} (tr "auth.register-submit")]]]
|
||||
:on-click set-section}
|
||||
(tr "auth.register-submit")]]]
|
||||
|
||||
:register
|
||||
[:*
|
||||
[:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
|
||||
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
|
||||
[:& register-methods {:on-success-callback success-register :hide-separator true}]
|
||||
[:& register/register-methods {:on-success-callback success-register :hide-separator true}]
|
||||
#_[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :change-section)}
|
||||
@@ -140,12 +156,13 @@
|
||||
:on-click set-section} (tr "auth.login-here")]]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:& terms-register]]]
|
||||
[:& register/terms-register]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& register-validate-form {:params {:token @register-token}
|
||||
:on-success-callback register-email-sent}]
|
||||
[:& register/register-validate-form
|
||||
{:params {:token @register-token}
|
||||
:on-success-callback register-email-sent}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :register)}
|
||||
[:a {:data-section "register"
|
||||
@@ -154,7 +171,7 @@
|
||||
|
||||
:register-email-sent
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& register-success-page {:params {:email @user-email :hide-logo true}}]]
|
||||
[:& register/register-success-page {:params {:email @user-email :hide-logo true}}]]
|
||||
|
||||
:recovery-request
|
||||
[:& recovery-request-page {:go-back-callback set-section-login
|
||||
@@ -166,43 +183,57 @@
|
||||
|
||||
(mf/defc request-dialog
|
||||
{::mf/props :obj}
|
||||
[{:keys [title content button-text on-button-click cancel-text]}]
|
||||
(let [on-click (or on-button-click rt/nav-root)]
|
||||
[{:keys [title content button-text on-button-click cancel-text on-close]}]
|
||||
(let [on-click (or on-button-click on-close)]
|
||||
[:div {:class (stl/css :overlay)}
|
||||
[:div {:class (stl/css :dialog)}
|
||||
[:div {:class (stl/css :modal-close)}
|
||||
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
|
||||
[:button {:class (stl/css :modal-close-button) :on-click on-close}
|
||||
i/close]]
|
||||
[:div {:class (stl/css :dialog-title)} title]
|
||||
(for [txt content]
|
||||
[:div txt])
|
||||
(for [[index content] (d/enumerate content)]
|
||||
[:div {:key index} content])
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
(when cancel-text
|
||||
[:button {:class (stl/css :cancel-button) :on-click rt/nav-root} cancel-text])
|
||||
[:button {:class (stl/css :cancel-button)
|
||||
:on-click on-close}
|
||||
cancel-text])
|
||||
[:button {:on-click on-click} button-text]]]]))
|
||||
|
||||
|
||||
(mf/defc request-access
|
||||
{::mf/props :obj}
|
||||
[{:keys [file-id team-id is-default workspace?]}]
|
||||
(let [profile (:profile @st/state)
|
||||
(let [profile (mf/deref refs/profile)
|
||||
requested* (mf/use-state {:sent false :already-requested false})
|
||||
requested (deref requested*)
|
||||
show-dialog (mf/use-state true)
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn []
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))))
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
#(reset! requested* {:sent true :already-requested false}))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
#(reset! requested* {:sent true :already-requested true}))
|
||||
|
||||
on-request-access
|
||||
(mf/use-fn
|
||||
(mf/deps file-id team-id workspace?)
|
||||
(fn []
|
||||
(let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id})
|
||||
mdata {:on-success on-success :on-error on-error}]
|
||||
(st/emit! (dc/create-team-access-request (with-meta params mdata))))))]
|
||||
|
||||
(let [params (if (some? file-id)
|
||||
{:file-id file-id
|
||||
:is-viewer (not workspace?)}
|
||||
{:team-id team-id})
|
||||
mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
(st/emit! (dc/create-team-access-request
|
||||
(with-meta params mdata))))))]
|
||||
|
||||
[:*
|
||||
(if (some? file-id)
|
||||
@@ -214,17 +245,24 @@
|
||||
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
|
||||
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
|
||||
[:div {:class (stl/css :workspace-right)}]]
|
||||
|
||||
[:div {:class (stl/css :viewer)}
|
||||
[:& header/header {:project {:name (tr "not-found.no-permission.project-name")}
|
||||
:index 0
|
||||
:file {:name (tr "not-found.no-permission.penpot-file")}
|
||||
:page nil
|
||||
:frame nil
|
||||
:permissions {:is-logged true}
|
||||
:zoom 1
|
||||
:section :interactions
|
||||
:shown-thumbnails false
|
||||
:interactions-mode nil}]])
|
||||
;; FIXME: the viewer header was never designed to be reused
|
||||
;; from other parts of the application, and this code looks
|
||||
;; like a fast workaround reusing it as-is without a proper
|
||||
;; component adaptation for be able to use it easily it on
|
||||
;; viewer context or static error page context
|
||||
[:& viewer.header/header {:project
|
||||
{:name (tr "not-found.no-permission.project-name")}
|
||||
:index 0
|
||||
:file {:name (tr "not-found.no-permission.penpot-file")}
|
||||
:page nil
|
||||
:frame nil
|
||||
:permissions {:is-logged true}
|
||||
:zoom 1
|
||||
:section :interactions
|
||||
:shown-thumbnails false
|
||||
:interactions-mode nil}]])
|
||||
|
||||
[:div {:class (stl/css :dashboard)}
|
||||
[:div {:class (stl/css :dashboard-sidebar)}
|
||||
@@ -242,22 +280,27 @@
|
||||
[:& login-dialog {:show-dialog show-dialog}]
|
||||
|
||||
is-default
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project")
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]
|
||||
|
||||
(and (some? file-id) (:already-requested requested))
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
|
||||
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]
|
||||
|
||||
(:already-requested requested)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
|
||||
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]
|
||||
|
||||
(:sent requested)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
|
||||
:content [(tr "not-found.no-permission.done.remember")]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
:button-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]
|
||||
|
||||
(some? file-id)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.file")
|
||||
@@ -265,7 +308,8 @@
|
||||
(tr "not-found.no-permission.if-approves")]
|
||||
:button-text (tr "not-found.no-permission.ask")
|
||||
:on-button-click on-request-access
|
||||
:cancel-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
:cancel-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]
|
||||
|
||||
(some? team-id)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project")
|
||||
@@ -273,7 +317,8 @@
|
||||
(tr "not-found.no-permission.if-approves")]
|
||||
:button-text (tr "not-found.no-permission.ask")
|
||||
:on-button-click on-request-access
|
||||
:cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
|
||||
:cancel-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]))]))
|
||||
|
||||
(mf/defc not-found*
|
||||
[]
|
||||
@@ -379,64 +424,93 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-reset} (tr "labels.retry")]]]))
|
||||
|
||||
(defn- load-info
|
||||
"Load exception page info"
|
||||
[path-params]
|
||||
(let [default {:loaded true}
|
||||
stream (cond
|
||||
(:file-id path-params)
|
||||
(->> (rp/cmd! :get-file-info {:id (:file-id path-params)})
|
||||
(rx/map (fn [info]
|
||||
{:loaded true
|
||||
:file-id (:id info)})))
|
||||
|
||||
(:team-id path-params)
|
||||
(->> (rp/cmd! :get-team-info {:id (:team-id path-params)})
|
||||
(rx/map (fn [info]
|
||||
{:loaded true
|
||||
:team-id (:id info)
|
||||
:team-default (:is-default info)})))
|
||||
|
||||
:else
|
||||
(rx/of default))]
|
||||
|
||||
(->> stream
|
||||
(rx/timeout 3000)
|
||||
(rx/catch (fn [cause]
|
||||
(if (instance? TimeoutError cause)
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [data route] :as props}]
|
||||
|
||||
(let [file-info (mf/use-state {:pending true})
|
||||
team-info (mf/use-state {:pending true})
|
||||
type (:type data)
|
||||
path (:path route)
|
||||
(let [type (:type data)
|
||||
path (:path route)
|
||||
|
||||
workspace? (str/includes? path "workspace")
|
||||
dashboard? (str/includes? path "dashboard")
|
||||
view? (str/includes? path "view")
|
||||
query-params (:query-params route)
|
||||
path-params (:path-params route)
|
||||
|
||||
request-access? (and
|
||||
(or workspace? dashboard? view?)
|
||||
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info)))))
|
||||
workspace? (str/includes? path "workspace")
|
||||
dashboard? (str/includes? path "dashboard")
|
||||
view? (str/includes? path "view")
|
||||
|
||||
query-params (u/map->query-string (:query-params route))
|
||||
pparams (:path-params route)
|
||||
on-file-info (mf/use-fn
|
||||
(fn [info]
|
||||
(reset! file-info {:file-id (:id info)})))
|
||||
on-team-info (mf/use-fn
|
||||
(fn [info]
|
||||
(reset! team-info {:team-id (:id info) :is-default (:is-default info)})))]
|
||||
;; We stora the request access info int this state
|
||||
info* (mf/use-state nil)
|
||||
info (deref info*)
|
||||
|
||||
(mf/with-effect [type path query-params pparams @file-info @team-info]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
|
||||
loaded? (get info :loaded false)
|
||||
|
||||
(when (and (:file-id pparams) (:pending @file-info))
|
||||
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)})
|
||||
(rx/subs! on-file-info)))
|
||||
request-access?
|
||||
(and
|
||||
(= (:type data) :not-found)
|
||||
(or workspace? dashboard? view?)
|
||||
(or (:file-id info)
|
||||
(:team-id info)))]
|
||||
|
||||
(when (and (:team-id pparams) (:pending @team-info))
|
||||
(->> (rp/cmd! :get-team-info {:id (:team-id pparams)})
|
||||
(rx/subs! on-team-info))))
|
||||
(mf/with-effect [type path query-params path-params]
|
||||
(let [query-params (u/map->query-string query-params)
|
||||
event-params {::ev/name "exception-page"
|
||||
:type type
|
||||
:path path
|
||||
:query-params query-params}]
|
||||
(st/emit! (ptk/event ::ev/event event-params))))
|
||||
|
||||
(case (:type data)
|
||||
:not-found
|
||||
(mf/with-effect [path-params info]
|
||||
(when-not (:loaded info)
|
||||
(->> (load-info path-params)
|
||||
(rx/subs! (partial reset! info*)))))
|
||||
|
||||
(when loaded?
|
||||
(if request-access?
|
||||
[:& request-access {:file-id (:file-id @file-info)
|
||||
:team-id (:team-id @team-info)
|
||||
:is-default (:is-default @team-info)
|
||||
[:& request-access {:file-id (:file-id info)
|
||||
:team-id (:team-id info)
|
||||
:is-default (:team-default info)
|
||||
:workspace? workspace?}]
|
||||
[:> not-found* {}])
|
||||
|
||||
:authentication
|
||||
(if request-access?
|
||||
[:& request-access {:file-id (:file-id @file-info)
|
||||
:team-id (:team-id @team-info)
|
||||
:is-default (:is-default @team-info)
|
||||
:workspace? workspace?}]
|
||||
[:> not-found* {}])
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:> not-found* {}]
|
||||
|
||||
:bad-gateway
|
||||
[:> bad-gateway* props]
|
||||
:authentication
|
||||
[:> not-found* {}]
|
||||
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
:bad-gateway
|
||||
[:> bad-gateway* props]
|
||||
|
||||
[:> internal-error* props])))
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
|
||||
[:> internal-error* props])))))
|
||||
|
||||
@@ -244,7 +244,8 @@
|
||||
(fn [result]
|
||||
(reset! images-data* result)))))
|
||||
|
||||
[:div {:class (stl/css :element-options)}
|
||||
[:div {:class (stl/css-case :element-options true
|
||||
:viewer-code-block (= :viewer from))}
|
||||
[:div {:class (stl/css :attributes-block)}
|
||||
[:button {:class (stl/css :download-button)
|
||||
:on-click handle-copy-all-code}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.element-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - #{$s-128}); // TODO: Fix this hardcoded value
|
||||
height: calc(100vh - #{$s-160}); // TODO: Fix this hardcoded value
|
||||
overflow: hidden;
|
||||
padding-bottom: $s-16;
|
||||
overflow-y: auto;
|
||||
@@ -17,6 +17,10 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.viewer-code-block {
|
||||
height: calc(100vh - #{$s-108}); // TODO: Fix this hardcoded value
|
||||
}
|
||||
|
||||
.download-button {
|
||||
@extend .button-secondary;
|
||||
@include uppercaseTitleTipography;
|
||||
|
||||
@@ -427,7 +427,8 @@
|
||||
(let [childs (mapv #(get objects %) (:shapes (unchecked-get props "shape")))
|
||||
props (obj/merge! #js {} props
|
||||
#js {:childs childs
|
||||
:objects objects})]
|
||||
:objects objects
|
||||
:all-objects all-objects})]
|
||||
(when (not-empty childs)
|
||||
[:> group-wrapper props])))))
|
||||
|
||||
|
||||
@@ -136,7 +136,8 @@
|
||||
:else
|
||||
[:div {:class (stl/css :settings-bar-content)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:default-selected (dm/str section)
|
||||
:default-selected "layers"
|
||||
:selected (name section)
|
||||
:on-change-tab on-tab-change
|
||||
:class (stl/css :left-sidebar-tabs)
|
||||
:action-button-position "start"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
height: 100%;
|
||||
grid-auto-rows: max-content;
|
||||
// TODO: ugly hack :( Fix this! we shouldn't be hardcoding this height
|
||||
max-height: calc(100vh - $s-80);
|
||||
height: calc(100vh - $s-92);
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: auto;
|
||||
padding-top: $s-8;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
(cond-> color
|
||||
(:value color) (assoc :color (:value color) :opacity 1)
|
||||
(:value color) (dissoc :value)
|
||||
true (assoc :file-id file-id)))
|
||||
:always (assoc :file-id file-id)))
|
||||
|
||||
color-id (:id color)
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
(fn [event]
|
||||
(st/emit!
|
||||
(dwl/add-recent-color color)
|
||||
(dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event)))))
|
||||
(dc/apply-color-from-palette color (kbd/alt? event)))))
|
||||
|
||||
rename-color
|
||||
(mf/use-fn
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.title-bar :refer [title-bar]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -21,82 +22,96 @@
|
||||
(defn- prepare-colors
|
||||
[shapes file-id shared-libs]
|
||||
(let [data (into [] (remove nil? (ctc/extract-all-colors shapes file-id shared-libs)))
|
||||
grouped-colors (group-by :attrs data)
|
||||
groups (d/group-by :attrs #(dissoc % :attrs) data)
|
||||
all-colors (distinct (mapv :attrs data))
|
||||
|
||||
tmp (group-by #(some? (:id %)) all-colors)
|
||||
library-colors (get tmp true)
|
||||
colors (get tmp false)]
|
||||
{:grouped-colors grouped-colors
|
||||
{:groups groups
|
||||
:all-colors all-colors
|
||||
:colors colors
|
||||
:library-colors library-colors}))
|
||||
|
||||
(def xf:map-shape-id
|
||||
(map :shape-id))
|
||||
|
||||
(mf/defc color-selection-menu
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [shapes file-id shared-libs]}]
|
||||
(let [{:keys [grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
|
||||
(prepare-colors shapes file-id shared-libs))
|
||||
(let [{:keys [groups library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
|
||||
(prepare-colors shapes file-id shared-libs))
|
||||
|
||||
state* (mf/use-state true)
|
||||
open? (deref state*)
|
||||
state* (mf/use-state true)
|
||||
open? (deref state*)
|
||||
|
||||
has-colors? (or (some? (seq colors)) (some? (seq library-colors)))
|
||||
has-colors? (or (some? (seq colors)) (some? (seq library-colors)))
|
||||
|
||||
toggle-content (mf/use-fn #(swap! state* not))
|
||||
toggle-content (mf/use-fn #(swap! state* not))
|
||||
|
||||
expand-lib-color (mf/use-state false)
|
||||
expand-color (mf/use-state false)
|
||||
|
||||
grouped-colors* (mf/use-var nil)
|
||||
prev-colors* (mf/use-var [])
|
||||
groups-ref (h/use-ref-value groups)
|
||||
prev-colors-ref (mf/use-ref nil)
|
||||
|
||||
;; grouped-colors* (mf/use-var nil)
|
||||
;; prev-colors* (mf/use-var [])
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [new-color old-color from-picker?]
|
||||
(let [old-color (-> old-color (dissoc :name :path) d/without-nils)
|
||||
(prn "new-color" new-color)
|
||||
(prn "old-color" old-color)
|
||||
(let [old-color (-> old-color
|
||||
(dissoc :name :path)
|
||||
(d/without-nils))
|
||||
|
||||
;; When dragging on the color picker sometimes all
|
||||
;; the shapes hasn't updated the color to the prev
|
||||
;; value so we need this extra calculation
|
||||
shapes-by-old-color (get @grouped-colors* old-color)
|
||||
prev-color (d/seek #(get @grouped-colors* %) @prev-colors*)
|
||||
shapes-by-prev-color (get @grouped-colors* prev-color)
|
||||
shapes-by-color (or shapes-by-prev-color shapes-by-old-color)]
|
||||
groups (mf/ref-val groups-ref)
|
||||
prev-colors (mf/ref-val prev-colors-ref)
|
||||
|
||||
prev-color (d/seek (partial get groups) prev-colors)
|
||||
|
||||
cops-old (get groups old-color)
|
||||
cops-prev (get groups prev-colors)
|
||||
cops (or cops-prev cops-old)
|
||||
old-color (or prev-color old-color)]
|
||||
|
||||
(when from-picker?
|
||||
(swap! prev-colors* conj (-> new-color (dissoc :name :path) d/without-nils)))
|
||||
(let [color (-> new-color
|
||||
(dissoc :name :path)
|
||||
(d/without-nils))]
|
||||
(mf/set-ref-val! prev-colors-ref
|
||||
(conj prev-colors color))))
|
||||
|
||||
(st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color))))))
|
||||
(st/emit! (dc/change-color-in-selected cops new-color old-color)))))
|
||||
|
||||
on-open
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! prev-colors* [])))
|
||||
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! prev-colors* [])))
|
||||
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
|
||||
|
||||
on-detach
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(let [shapes-by-color (get @grouped-colors* color)
|
||||
new-color (assoc color :id nil :file-id nil)]
|
||||
(st/emit! (dc/change-color-in-selected new-color shapes-by-color color)))))
|
||||
(let [groups (mf/ref-val groups-ref)
|
||||
cops (get groups color)
|
||||
color' (dissoc color :id :file-id)]
|
||||
(st/emit! (dc/change-color-in-selected cops color' color)))))
|
||||
|
||||
select-only
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(let [shapes-by-color (get @grouped-colors* color)
|
||||
ids (into (d/ordered-set) (map :shape-id) shapes-by-color)]
|
||||
(let [groups (mf/ref-val groups-ref)
|
||||
cops (get groups color)
|
||||
ids (into (d/ordered-set) xf:map-shape-id cops)]
|
||||
(st/emit! (dws/select-shapes ids)))))]
|
||||
|
||||
(mf/with-effect [grouped-colors]
|
||||
(reset! grouped-colors* grouped-colors))
|
||||
|
||||
[:div {:class (stl/css :element-set)}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
[:& title-bar {:collapsable has-colors?
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
.height {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
padding-top: $s-1;
|
||||
}
|
||||
@@ -208,7 +207,6 @@
|
||||
.margin {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon {
|
||||
&.rotated svg {
|
||||
transform: rotate(90deg);
|
||||
|
||||
@@ -113,9 +113,9 @@
|
||||
handle-value-change
|
||||
(mf/use-fn
|
||||
(mf/deps color on-change)
|
||||
(fn [new-value]
|
||||
(fn [value]
|
||||
(let [color (-> color
|
||||
(assoc :color new-value)
|
||||
(assoc :color value)
|
||||
(dissoc :gradient))]
|
||||
(st/emit! (dwl/add-recent-color color)
|
||||
(on-change color)))))
|
||||
@@ -146,7 +146,9 @@
|
||||
:else
|
||||
color)
|
||||
|
||||
{:keys [x y]} (dom/get-client-position event)
|
||||
cpos (dom/get-client-position event)
|
||||
x (dm/get-prop cpos :x)
|
||||
y (dm/get-prop cpos :y)
|
||||
|
||||
props {:x x
|
||||
:y y
|
||||
@@ -154,14 +156,14 @@
|
||||
:disable-opacity disable-opacity
|
||||
:disable-image disable-image
|
||||
;; on-change second parameter means if the source is the color-picker
|
||||
:on-change #(on-change (merge uc/empty-color %) true)
|
||||
:on-change #(on-change % true)
|
||||
:on-close (fn [value opacity id file-id]
|
||||
(when on-close
|
||||
(on-close value opacity id file-id)))
|
||||
:data color}]
|
||||
|
||||
(when on-open
|
||||
(on-open (merge uc/empty-color color)))
|
||||
(when (fn? on-open)
|
||||
(on-open color))
|
||||
|
||||
(modal/show! :colorpicker props))))
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
}
|
||||
|
||||
.stroke-options {
|
||||
@include flexRow;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: $s-4;
|
||||
grid-template-columns: 1fr 2fr 2fr;
|
||||
|
||||
.stroke-width-input-element {
|
||||
@@ -28,11 +29,14 @@
|
||||
}
|
||||
}
|
||||
.stroke-caps-options {
|
||||
@include flexRow;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: $s-4;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
||||
|
||||
.cap-select {
|
||||
width: $s-124;
|
||||
width: 100%;
|
||||
}
|
||||
.stroke-cap-dropdown,
|
||||
.stroke-cap-dropdown-start {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
scrollbar-gutter: stable;
|
||||
max-width: var(--width);
|
||||
}
|
||||
|
||||
.pages-list {
|
||||
|
||||
@@ -80,9 +80,6 @@
|
||||
(= id :multiple)
|
||||
(= file-id :multiple)))
|
||||
|
||||
(def empty-color
|
||||
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image]))
|
||||
|
||||
(defn get-color-name
|
||||
[color]
|
||||
(or (:color-library-name color)
|
||||
|
||||
@@ -760,8 +760,10 @@
|
||||
(.back (.-history js/window)))
|
||||
|
||||
(defn reload-current-window
|
||||
[]
|
||||
(.reload (.-location js/window)))
|
||||
([]
|
||||
(.reload globals/location))
|
||||
([force?]
|
||||
(.reload globals/location force?)))
|
||||
|
||||
(defn scroll-by!
|
||||
([element x y]
|
||||
|
||||
29
frontend/src/app/util/functions.cljs
Normal file
29
frontend/src/app/util/functions.cljs
Normal file
@@ -0,0 +1,29 @@
|
||||
;; 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.util.functions
|
||||
"A functions helpers"
|
||||
(:require
|
||||
["lodash/debounce.js" :as lodash-debounce]))
|
||||
|
||||
;; NOTE: this is needed because depending on the type of the build and
|
||||
;; target execution evironment (browser, esm), the real export can be
|
||||
;; different. All this issue is about the commonjs and esm
|
||||
;; interop/conversion, because the js ecosystem decided that is should
|
||||
;; work this way.
|
||||
;;
|
||||
;; In this concrete case, lodash exposes commonjs module which works
|
||||
;; ok on browser build but for ESM build it is converted in the best
|
||||
;; effort to esm module, exporting the module.exports as the default
|
||||
;; property. This is why on ESM builds we need to look on .-default
|
||||
;; property.
|
||||
(def ^:private ext-debounce
|
||||
(or (.-default lodash-debounce)
|
||||
lodash-debounce))
|
||||
|
||||
(defn debounce
|
||||
[f timeout]
|
||||
(ext-debounce f timeout #{:leading false :trailing true}))
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.util.globals :as globals]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as e]
|
||||
[potok.v2.core :as ptk]
|
||||
[reitit.core :as r]))
|
||||
@@ -147,7 +148,41 @@
|
||||
(defn nav-root
|
||||
"Navigate to the root page."
|
||||
[]
|
||||
(set! (.-href globals/location) "/"))
|
||||
(ptk/reify ::nav-root
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(set! (.-href globals/location) "/"))))
|
||||
|
||||
(defn reload
|
||||
[force?]
|
||||
(ptk/reify ::reload
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(ts/asap (partial dom/reload-current-window force?)))))
|
||||
|
||||
(defn nav-raw
|
||||
[& {:keys [href uri]}]
|
||||
(ptk/reify ::nav-raw
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(cond
|
||||
(string? uri)
|
||||
(.replace globals/location uri)
|
||||
|
||||
(string? href)
|
||||
(set! (.-href globals/location) href)))))
|
||||
|
||||
(defn get-current-href
|
||||
[]
|
||||
(.-href globals/location))
|
||||
|
||||
(defn get-current-path
|
||||
[]
|
||||
(let [hash (.-hash globals/location)]
|
||||
(if (str/starts-with? hash "#")
|
||||
(subs hash 1)
|
||||
hash)))
|
||||
|
||||
|
||||
;; --- History API
|
||||
|
||||
|
||||
@@ -8,40 +8,150 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.transit :as t]
|
||||
[app.util.functions :as fns]
|
||||
[app.util.globals :as g]
|
||||
[app.util.timers :as tm]))
|
||||
[cuerdas.core :as str]
|
||||
[okulary.util :as ou]))
|
||||
|
||||
(defn- persist
|
||||
[storage prev curr]
|
||||
(run! (fn [key]
|
||||
(let [prev* (get prev key)
|
||||
curr* (get curr key)]
|
||||
(when (not= curr* prev*)
|
||||
(tm/schedule-on-idle
|
||||
#(if (some? curr*)
|
||||
(.setItem ^js storage (t/encode-str key) (t/encode-str curr*))
|
||||
(.removeItem ^js storage (t/encode-str key)))))))
|
||||
;; Using ex/ignoring because can receive a DOMException like this when
|
||||
;; importing the code as a library: Failed to read the 'localStorage'
|
||||
;; property from 'Window': Storage is disabled inside 'data:' URLs.
|
||||
(defonce ^:private local-storage-backend
|
||||
(ex/ignoring (unchecked-get g/global "localStorage")))
|
||||
|
||||
(into #{} (concat (keys curr)
|
||||
(keys prev)))))
|
||||
(defonce ^:private session-storage-backend
|
||||
(ex/ignoring (unchecked-get g/global "sessionStorage")))
|
||||
|
||||
(defn- load
|
||||
[storage]
|
||||
(when storage
|
||||
(let [len (.-length ^js storage)]
|
||||
(reduce (fn [res index]
|
||||
(let [key (.key ^js storage index)
|
||||
val (.getItem ^js storage key)]
|
||||
(try
|
||||
(assoc res (t/decode-str key) (t/decode-str val))
|
||||
(catch :default _e
|
||||
res))))
|
||||
{}
|
||||
(range len)))))
|
||||
(def ^:dynamic *sync*
|
||||
"Dynamic variable which determines the mode of operation of the
|
||||
storage mutatio actions. By default is asynchronous."
|
||||
false)
|
||||
|
||||
;; Using ex/ignoring because can receive a DOMException like this when importing the code as a library:
|
||||
;; Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs.
|
||||
(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage")))))
|
||||
(defn- encode-key
|
||||
[prefix k]
|
||||
(assert (keyword? k) "key must be keyword")
|
||||
(let [kns (namespace k)
|
||||
kn (name k)]
|
||||
(str prefix ":" kns "/" kn)))
|
||||
|
||||
(add-watch storage :persistence #(persist js/localStorage %3 %4))
|
||||
(defn- decode-key
|
||||
[prefix k]
|
||||
(when (str/starts-with? k prefix)
|
||||
(let [l (+ (count prefix) 1)
|
||||
k (subs k l)]
|
||||
(if (str/starts-with? k "/")
|
||||
(keyword (subs k 1))
|
||||
(let [[kns kn] (str/split k "/" 2)]
|
||||
(keyword kns kn))))))
|
||||
|
||||
(defn- lookup-by-index
|
||||
[backend prefix result index]
|
||||
(try
|
||||
(let [key (.key ^js backend index)
|
||||
key' (decode-key prefix key)]
|
||||
(if key'
|
||||
(let [val (.getItem ^js backend key)]
|
||||
(assoc! result key' (t/decode-str val)))
|
||||
result))
|
||||
(catch :default _
|
||||
result)))
|
||||
|
||||
(defn- load-data
|
||||
[backend prefix]
|
||||
(if (some? backend)
|
||||
(let [length (.-length ^js backend)]
|
||||
(loop [index 0
|
||||
result (transient {})]
|
||||
(if (< index length)
|
||||
(recur (inc index)
|
||||
(lookup-by-index backend prefix result index))
|
||||
(persistent! result))))
|
||||
{}))
|
||||
|
||||
(defn create-storage
|
||||
[backend prefix]
|
||||
(let [initial (load-data backend prefix)
|
||||
curr-data #js {:content initial}
|
||||
last-data #js {:content initial}
|
||||
watches (js/Map.)
|
||||
|
||||
update-key
|
||||
(fn [key val]
|
||||
(when (some? backend)
|
||||
(if (some? val)
|
||||
(.setItem ^js backend (encode-key prefix key) (t/encode-str val))
|
||||
(.removeItem ^js backend (encode-key prefix key)))))
|
||||
|
||||
on-change*
|
||||
(fn [curr-state]
|
||||
(let [prev-state (unchecked-get last-data "content")]
|
||||
(try
|
||||
(run! (fn [key]
|
||||
(let [prev-val (get prev-state key)
|
||||
curr-val (get curr-state key)]
|
||||
(when-not (identical? curr-val prev-val)
|
||||
(update-key key curr-val))))
|
||||
(into #{} (concat (keys curr-state)
|
||||
(keys prev-state))))
|
||||
(finally
|
||||
(unchecked-set last-data "content" curr-state)))))
|
||||
|
||||
on-change
|
||||
(fns/debounce on-change* 2000)]
|
||||
|
||||
(reify
|
||||
IAtom
|
||||
|
||||
IDeref
|
||||
(-deref [_] (unchecked-get curr-data "content"))
|
||||
|
||||
ILookup
|
||||
(-lookup [coll k]
|
||||
(-lookup coll k nil))
|
||||
(-lookup [_ k not-found]
|
||||
(let [state (unchecked-get curr-data "content")]
|
||||
(-lookup state k not-found)))
|
||||
|
||||
IReset
|
||||
(-reset! [self newval]
|
||||
(let [oldval (unchecked-get curr-data "content")]
|
||||
(unchecked-set curr-data "content" newval)
|
||||
(if *sync*
|
||||
(on-change* newval)
|
||||
(on-change newval))
|
||||
(when (> (.-size watches) 0)
|
||||
(-notify-watches self oldval newval))
|
||||
newval))
|
||||
|
||||
ISwap
|
||||
(-swap! [self f]
|
||||
(let [state (unchecked-get curr-data "content")]
|
||||
(-reset! self (f state))))
|
||||
(-swap! [self f x]
|
||||
(let [state (unchecked-get curr-data "content")]
|
||||
(-reset! self (f state x))))
|
||||
(-swap! [self f x y]
|
||||
(let [state (unchecked-get curr-data "content")]
|
||||
(-reset! self (f state x y))))
|
||||
(-swap! [self f x y more]
|
||||
(let [state (unchecked-get curr-data "content")]
|
||||
(-reset! self (apply f state x y more))))
|
||||
|
||||
IWatchable
|
||||
(-notify-watches [self oldval newval]
|
||||
(ou/doiter
|
||||
(.entries watches)
|
||||
(fn [n]
|
||||
(let [f (aget n 1)
|
||||
k (aget n 0)]
|
||||
(f k self oldval newval)))))
|
||||
|
||||
(-add-watch [self key f]
|
||||
(.set watches key f)
|
||||
self)
|
||||
|
||||
(-remove-watch [_ key]
|
||||
(.delete watches key)))))
|
||||
|
||||
(defonce storage (create-storage local-storage-backend "penpot"))
|
||||
(defonce session (create-storage session-storage-backend "penpot"))
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
(defn asap
|
||||
[f]
|
||||
(-> (p/resolved nil)
|
||||
(p/then f)))
|
||||
(p/then (fn [_] (f)))))
|
||||
|
||||
(defn interval
|
||||
[ms func]
|
||||
|
||||
@@ -103,6 +103,14 @@
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")]
|
||||
(.writeText ^js cboard data)))
|
||||
|
||||
(defn write-to-clipboard-promise
|
||||
[mimetype promise]
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")
|
||||
data (js/ClipboardItem.
|
||||
(-> (obj/create)
|
||||
(obj/set! mimetype promise)))]
|
||||
(.write ^js cboard #js [data])))
|
||||
|
||||
(defn read-from-clipboard
|
||||
[]
|
||||
(try
|
||||
|
||||
@@ -202,108 +202,61 @@
|
||||
|
||||
(defn make-local-external-references
|
||||
[file file-id]
|
||||
(let [detach-text
|
||||
(let [change-fill
|
||||
(fn [fill]
|
||||
(cond-> fill
|
||||
(not= file-id (:fill-color-ref-file fill))
|
||||
(assoc :fill-color-ref-file file-id)))
|
||||
|
||||
change-stroke
|
||||
(fn [stroke]
|
||||
(cond-> stroke
|
||||
(not= file-id (:stroke-color-ref-file stroke))
|
||||
(assoc :stroke-color-ref-file file-id)))
|
||||
|
||||
change-text
|
||||
(fn [content]
|
||||
(->> content
|
||||
(ct/transform-nodes
|
||||
#(cond-> %
|
||||
(not= file-id (:fill-color-ref-file %))
|
||||
(assoc :fill-color-ref-file file-id)
|
||||
(fn [node]
|
||||
(-> node
|
||||
(d/update-when :fills #(mapv change-fill %))
|
||||
(cond-> (not= file-id (:typography-ref-file node))
|
||||
(assoc :typography-ref-file file-id)))))))
|
||||
|
||||
(not= file-id (:typography-ref-file %))
|
||||
(assoc :typography-ref-file file-id)))))
|
||||
|
||||
detach-shape
|
||||
change-shape
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(not= file-id (:fill-color-ref-file shape))
|
||||
(assoc :fill-color-ref-file file-id)
|
||||
(-> shape
|
||||
(d/update-when :fills #(mapv change-fill %))
|
||||
(d/update-when :strokes #(mapv change-stroke %))
|
||||
(cond-> (not= file-id (:component-file shape))
|
||||
(assoc :component-file file-id))
|
||||
|
||||
(not= file-id (:stroke-color-ref-file shape))
|
||||
(assoc :stroke-color-ref-file file-id)
|
||||
(cond-> (= :text (:type shape))
|
||||
(update :content change-text))))
|
||||
|
||||
(not= file-id (:component-file shape))
|
||||
(assoc :component-file file-id)
|
||||
|
||||
(= :text (:type shape))
|
||||
(update :content detach-text)))
|
||||
|
||||
detach-objects
|
||||
change-objects
|
||||
(fn [objects]
|
||||
(->> objects
|
||||
(d/mapm #(detach-shape %2))))
|
||||
(d/mapm #(change-shape %2))))
|
||||
|
||||
detach-pages
|
||||
change-pages
|
||||
(fn [pages-index]
|
||||
(->> pages-index
|
||||
(d/mapm
|
||||
(fn [_ data]
|
||||
(-> data
|
||||
(update :objects detach-objects))))))]
|
||||
(update :objects change-objects))))))]
|
||||
(-> file
|
||||
(update-in [:data :pages-index] detach-pages))))
|
||||
|
||||
(defn collect-external-references
|
||||
[file]
|
||||
|
||||
(let [get-text-refs
|
||||
(fn [content]
|
||||
(->> content
|
||||
(ct/node-seq #(or (contains? % :fill-color-ref-id)
|
||||
(contains? % :typography-ref-id)))
|
||||
|
||||
(mapcat (fn [node]
|
||||
(cond-> []
|
||||
(contains? node :fill-color-ref-id)
|
||||
(conj {:id (:fill-color-ref-id node)
|
||||
:file-id (:fill-color-ref-file node)})
|
||||
|
||||
(contains? node :typography-ref-id)
|
||||
(conj {:id (:typography-ref-id node)
|
||||
:file-id (:typography-ref-file node)}))))
|
||||
|
||||
(into [])))
|
||||
|
||||
get-shape-refs
|
||||
(fn [[_ shape]]
|
||||
(cond-> []
|
||||
(contains? shape :fill-color-ref-id)
|
||||
(conj {:id (:fill-color-ref-id shape)
|
||||
:file-id (:fill-color-ref-file shape)})
|
||||
|
||||
(contains? shape :stroke-color-ref-id)
|
||||
(conj {:id (:stroke-color-ref-id shape)
|
||||
:file-id (:stroke-color-ref-file shape)})
|
||||
|
||||
(contains? shape :component-id)
|
||||
(conj {:id (:component-id shape)
|
||||
:file-id (:component-file shape)})
|
||||
|
||||
(= :text (:type shape))
|
||||
(into (get-text-refs (:content shape)))))]
|
||||
|
||||
(->> (get-in file [:data :pages-index])
|
||||
(vals)
|
||||
(mapcat :objects)
|
||||
(mapcat get-shape-refs)
|
||||
(filter (comp some? :file-id))
|
||||
(filter (comp some? :id))
|
||||
(group-by :file-id)
|
||||
(d/mapm #(mapv :id %2)))))
|
||||
(update-in [:data :pages-index] change-pages))))
|
||||
|
||||
(defn merge-assets [target-file assets-files]
|
||||
(let [external-refs (collect-external-references target-file)
|
||||
|
||||
merge-file-assets
|
||||
(let [merge-file-assets
|
||||
(fn [target file]
|
||||
(let [colors (-> (get-in file [:data :colors])
|
||||
(select-keys (get external-refs (:id file))))
|
||||
typographies (-> (get-in file [:data :typographies])
|
||||
(select-keys (get external-refs (:id file))))
|
||||
media (-> (get-in file [:data :media])
|
||||
(select-keys (get external-refs (:id file))))
|
||||
components (-> (ctkl/components (:data file))
|
||||
(select-keys (get external-refs (:id file))))]
|
||||
(let [colors (get-in file [:data :colors])
|
||||
typographies (get-in file [:data :typographies])
|
||||
media (get-in file [:data :media])
|
||||
components (ctkl/components (:data file))]
|
||||
(cond-> target
|
||||
(d/not-empty? colors)
|
||||
(update-in [:data :colors] merge colors)
|
||||
@@ -323,16 +276,20 @@
|
||||
(defn process-export
|
||||
[file-id export-type files]
|
||||
|
||||
(case export-type
|
||||
:all files
|
||||
:merge (let [file-list (-> files (d/without-keys [file-id]) vals)]
|
||||
(-> (select-keys files [file-id])
|
||||
(update file-id merge-assets file-list)
|
||||
(update file-id make-local-external-references file-id)
|
||||
(update file-id dissoc :libraries)))
|
||||
:detach (-> (select-keys files [file-id])
|
||||
(update file-id ctf/detach-external-references file-id)
|
||||
(update file-id dissoc :libraries))))
|
||||
(let [result
|
||||
(case export-type
|
||||
:all files
|
||||
:merge (let [file-list (-> files (d/without-keys [file-id]) vals)]
|
||||
(-> (select-keys files [file-id])
|
||||
(update file-id merge-assets file-list)
|
||||
(update file-id make-local-external-references file-id)
|
||||
(update file-id dissoc :libraries)))
|
||||
:detach (-> (select-keys files [file-id])
|
||||
(update file-id ctf/detach-external-references file-id)
|
||||
(update file-id dissoc :libraries)))]
|
||||
|
||||
;;(.log js/console (clj->js result))
|
||||
result))
|
||||
|
||||
(defn collect-files
|
||||
[file-id export-type features]
|
||||
|
||||
@@ -506,6 +506,6 @@
|
||||
(rx/mapcat rp/handle-response)
|
||||
(rx/subs! (fn [_]
|
||||
(println "Snapshot restored " (or snapshot-id label)))
|
||||
#_(.reload js/location))
|
||||
(fn [cause]
|
||||
(js/console.log "EE:" cause))))))
|
||||
#_(.reload js/location)
|
||||
(fn [cause]
|
||||
(js/console.log "EE:" cause)))))))
|
||||
|
||||
@@ -923,6 +923,9 @@ msgstr "Are you sure?"
|
||||
msgid "errors.auth-provider-not-allowed"
|
||||
msgstr "Auth provider not allowed for this profile"
|
||||
|
||||
msgid "errors.maximum-invitations-by-request-reached"
|
||||
msgstr "The maximum (%s) number of emails that can be invited in a single request has been reached"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs:61
|
||||
msgid "errors.auth-provider-not-configured"
|
||||
msgstr "Authentication provider not configured."
|
||||
|
||||
@@ -6094,3 +6094,6 @@ msgstr "Actualizar"
|
||||
#, unused
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Pulsar para cerrar la ruta"
|
||||
|
||||
msgid "errors.maximum-invitations-by-request-reached"
|
||||
msgstr "Se ha alcanzado el número máximo (%s) de correos electrónicos que se pueden invitar en una sola solicitud"
|
||||
|
||||
@@ -6647,6 +6647,7 @@ __metadata:
|
||||
js-beautify: "npm:^1.15.1"
|
||||
jsdom: "npm:^24.1.0"
|
||||
jszip: "npm:^3.10.1"
|
||||
lodash: "npm:^4.17.21"
|
||||
luxon: "npm:^3.4.4"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:^12.0.2"
|
||||
|
||||
Reference in New Issue
Block a user