mirror of
https://github.com/penpot/penpot.git
synced 2026-01-28 08:13:29 -05:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72c07b657 | ||
|
|
708492afeb | ||
|
|
1305ab3cc6 | ||
|
|
29cc6b4f9c | ||
|
|
cc7f0b145c | ||
|
|
e69c0c3e27 | ||
|
|
a209966427 | ||
|
|
d5abbd4220 | ||
|
|
70a23a14c4 | ||
|
|
93c81ea49c | ||
|
|
ddc41027ab | ||
|
|
4f931fbe6a | ||
|
|
b49a4734ff | ||
|
|
2aaa2f3033 | ||
|
|
202b9f3075 | ||
|
|
be0814cdac | ||
|
|
80d719353c | ||
|
|
fa3fc12594 | ||
|
|
422a9db07b | ||
|
|
a4145a30f5 | ||
|
|
38e5c161e7 | ||
|
|
a7c1f7ba69 | ||
|
|
e9755d437e | ||
|
|
e5db66351e | ||
|
|
89153eef23 | ||
|
|
b7a8677036 | ||
|
|
9ff2160c77 | ||
|
|
4c77b32171 | ||
|
|
34141ce9af | ||
|
|
58c867885c | ||
|
|
ccb6e25914 | ||
|
|
965d2d4036 | ||
|
|
9f8d7c9e41 | ||
|
|
8d352c1f82 | ||
|
|
faead09174 | ||
|
|
ae3ce1220b | ||
|
|
6e3673136a | ||
|
|
28caa1d47d | ||
|
|
ea6f0abf7c | ||
|
|
45cdfff128 | ||
|
|
8c38e41261 | ||
|
|
3197dfddd9 | ||
|
|
d900516302 | ||
|
|
fa68a25bea | ||
|
|
2cc2d34719 | ||
|
|
4640d043e3 | ||
|
|
bc957893f4 | ||
|
|
b8107ee497 | ||
|
|
6b3a988526 | ||
|
|
5cb39874a2 | ||
|
|
9fc671cc17 | ||
|
|
3fb3b45fdc | ||
|
|
0816adbaec | ||
|
|
1d69941882 | ||
|
|
8f600f334f | ||
|
|
cf55d12991 | ||
|
|
78919df886 |
26
CHANGES.md
26
CHANGES.md
@@ -1,6 +1,27 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.6.0 (Unreleased)
|
||||
## 2.6.2 (Unreleased)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Increase the height of the right sidebar dropdowns [Taiga #10615](https://tree.taiga.io/project/penpot/issue/10615)
|
||||
- Fix scroll on token themes modal [Taiga #10745](https://tree.taiga.io/project/penpot/issue/10745)
|
||||
- Fix unexpected exception on path editor on merge segments when undo stack is empty
|
||||
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
|
||||
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
|
||||
- Fix several issues with internal srepl helpers
|
||||
- Fix unexpected exception on template import from libraries
|
||||
- Fix incorrect uuid parsing from different parts of code
|
||||
|
||||
## 2.6.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix webhooks not shown in list [Taiga #10763](https://tree.taiga.io/project/penpot/issue/10763)
|
||||
- Fix colorpicker scroll when dropdown displayed [Taiga #10696](https://tree.taiga.io/project/penpot/issue/10696)
|
||||
- Clean internal workspace state on exit or url changed [Taiga #10619](https://tree.taiga.io/project/penpot/issue/10619)
|
||||
|
||||
## 2.6.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -25,6 +46,7 @@
|
||||
- [DESIGN TOKENS] Import and export tokens from a JSON file.
|
||||
- [DESIGN TOKENS] Apply Themes and Sets at document level.
|
||||
- Add more descriptive tooltip to boards for first time users [Taiga #9426](https://tree.taiga.io/project/penpot/us/9426)
|
||||
- First State of a Project Changes Consolidation [Taia #10605](https://tree.taiga.io/project/penpot/us/10605)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -47,6 +69,8 @@
|
||||
- Fix available size of resize handler [Taiga #10639](https://tree.taiga.io/project/penpot/issue/10639)
|
||||
- Internal error when install a plugin by penpothub - Try plugin [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
|
||||
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
|
||||
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -30,7 +30,8 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions-old";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
|
||||
@@ -23,7 +23,8 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions-old";
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
[["" {:middleware [[mw/server-timing]
|
||||
[mw/params]
|
||||
[mw/format-response]
|
||||
[mw/parse-request]
|
||||
[session/soft-auth cfg]
|
||||
[actoken/soft-auth cfg]
|
||||
[mw/parse-request]
|
||||
[mw/errors errors/handle]
|
||||
[mw/restrict-methods]]}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(let [session-id (some-> params :session-id uuid/parse*)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
(contains? cf/flags :login-with-password))
|
||||
(ex/raise :type :restriction
|
||||
:code :login-disabled
|
||||
:hint "login is disabled in this instance"))
|
||||
:hint "login is disabled"))
|
||||
|
||||
(letfn [(check-password [cfg profile password]
|
||||
(if (= (:password profile) "!")
|
||||
@@ -79,7 +79,8 @@
|
||||
:code :wrong-credentials))
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
:code :profile-blocked
|
||||
:hint "profile is marked as blocked"))
|
||||
(when-not (check-password cfg profile password)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
@@ -183,11 +184,11 @@
|
||||
(defn- validate-register-attempt!
|
||||
[cfg params]
|
||||
|
||||
(when (or
|
||||
(not (contains? cf/flags :registration))
|
||||
(not (contains? cf/flags :login-with-password)))
|
||||
(when (or (not (contains? cf/flags :registration))
|
||||
(not (contains? cf/flags :login-with-password)))
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
:code :registration-disabled
|
||||
:hint "registration disabled"))
|
||||
|
||||
(when (contains? params :invitation-token)
|
||||
(let [invitation (tokens/verify (::setup/props cfg)
|
||||
@@ -201,12 +202,14 @@
|
||||
(when (and (email.blacklist/enabled? cfg)
|
||||
(email.blacklist/contains? cfg (:email params)))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-domain-is-not-allowed))
|
||||
:code :email-domain-is-not-allowed
|
||||
:hint "email domain in blacklist"))
|
||||
|
||||
(when (and (email.whitelist/enabled? cfg)
|
||||
(not (email.whitelist/contains? cfg (:email params))))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-domain-is-not-allowed))
|
||||
:code :email-domain-is-not-allowed
|
||||
:hint "email domain not in whitelist"))
|
||||
|
||||
;; Perform a basic validation of email & password
|
||||
(when (= (str/lower (:email params))
|
||||
@@ -219,13 +222,13 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-permanent-bounces
|
||||
:email (:email params)
|
||||
:hint "looks like the email has bounce reports"))
|
||||
:hint "email has bounce reports"))
|
||||
|
||||
(when (eml/has-complaint-reports? cfg (:email params))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-complaints
|
||||
:email (:email params)
|
||||
:hint "looks like the email has complaint reports")))
|
||||
:hint "email has complaint reports")))
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||
|
||||
@@ -328,7 +328,7 @@
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
;; This operation is needed for backward comapatibility with frontends that
|
||||
;; does not support pointer-map resolution mechanism; this just resolves the
|
||||
@@ -490,7 +490,7 @@
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [page-id (or page-id (-> file :data :pages first))
|
||||
@@ -737,7 +737,7 @@
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
{:name (:name file)
|
||||
|
||||
@@ -91,9 +91,6 @@
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
@@ -107,7 +104,7 @@
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features)))]
|
||||
(assoc :features features))]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
@@ -120,7 +117,7 @@
|
||||
;; to lost team features updating
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
;; the features defined on team row, we update it
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :team
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
|
||||
@@ -209,100 +209,116 @@
|
||||
This method allows send flash notifications to specified target destinations.
|
||||
The message can be a free text or a preconfigured one.
|
||||
|
||||
The destination can be: all, profile-id, team-id, or a coll of them."
|
||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
The destination can be: all, profile-id, team-id, or a coll of them.
|
||||
It also can be:
|
||||
|
||||
{:email \"some@example.com\"}
|
||||
[[:email \"some@example.com\"], ...]
|
||||
|
||||
Command examples:
|
||||
|
||||
(notify! :dest :all :code :maintenance)
|
||||
(notify! :dest :all :code :upgrade-version)
|
||||
"
|
||||
[& {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
|
||||
(when-not (contains? #{:success :error :info :warning} level)
|
||||
(ex/raise :type :assertion
|
||||
:code :incorrect-level
|
||||
:hint (str "level '" level "' not supported")))
|
||||
|
||||
(letfn [(send [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
(let [message {:type :notification
|
||||
:code code
|
||||
:level level
|
||||
:version (:full cf/version)
|
||||
:subs-id dest
|
||||
:message message}
|
||||
message (->> (dissoc params :dest :code :message :level)
|
||||
(merge message))]
|
||||
(mbus/pub! msgbus
|
||||
:topic (str dest)
|
||||
:message message)))
|
||||
(let [{:keys [::mbus/msgbus ::db/pool]} main/system
|
||||
|
||||
(resolve-profile [email]
|
||||
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
|
||||
send
|
||||
(fn [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
(let [message {:type :notification
|
||||
:code code
|
||||
:level level
|
||||
:version (:full cf/version)
|
||||
:subs-id dest
|
||||
:message message}
|
||||
message (->> (dissoc params :dest :code :message :level)
|
||||
(merge message))]
|
||||
(mbus/pub! msgbus
|
||||
:topic dest
|
||||
:message message)))
|
||||
|
||||
(resolve-team [team-id]
|
||||
(->> (db/query pool :team-profile-rel
|
||||
{:team-id team-id}
|
||||
{:columns [:profile-id]})
|
||||
(map :profile-id)))
|
||||
resolve-profile
|
||||
(fn [email]
|
||||
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
|
||||
|
||||
(resolve-dest [dest]
|
||||
(cond
|
||||
(= :all dest)
|
||||
[uuid/zero]
|
||||
resolve-team
|
||||
(fn [team-id]
|
||||
(->> (db/query pool :team-profile-rel
|
||||
{:team-id team-id}
|
||||
{:columns [:profile-id]})
|
||||
(map :profile-id)))
|
||||
|
||||
(uuid? dest)
|
||||
[dest]
|
||||
resolve-dest
|
||||
(fn resolve-dest [dest]
|
||||
(cond
|
||||
(= :all dest)
|
||||
[uuid/zero]
|
||||
|
||||
(string? dest)
|
||||
(some-> dest h/parse-uuid resolve-dest)
|
||||
(uuid? dest)
|
||||
[dest]
|
||||
|
||||
(nil? dest)
|
||||
(resolve-dest uuid/zero)
|
||||
(string? dest)
|
||||
(some-> dest h/parse-uuid resolve-dest)
|
||||
|
||||
(map? dest)
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
(nil? dest)
|
||||
[uuid/zero]
|
||||
|
||||
(and (vector? dest)
|
||||
(every? vector? dest))
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
(map? dest)
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(and (vector? dest)
|
||||
(keyword? (first dest)))
|
||||
(let [[op param] dest]
|
||||
(and (vector? dest)
|
||||
(every? vector? dest))
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(and (vector? dest)
|
||||
(keyword? (first dest)))
|
||||
(let [[op param] dest]
|
||||
(cond
|
||||
(= op :email)
|
||||
(cond
|
||||
(= op :email)
|
||||
(cond
|
||||
(and (coll? param)
|
||||
(every? string? param))
|
||||
(sequence (comp
|
||||
(keep resolve-profile)
|
||||
(mapcat identity))
|
||||
param)
|
||||
(and (coll? param)
|
||||
(every? string? param))
|
||||
(sequence (comp
|
||||
(keep resolve-profile)
|
||||
(mapcat identity))
|
||||
param)
|
||||
|
||||
(string? param)
|
||||
(resolve-profile param))
|
||||
(string? param)
|
||||
(resolve-profile param))
|
||||
|
||||
(= op :team-id)
|
||||
(cond
|
||||
(coll? param)
|
||||
(sequence (comp
|
||||
(mapcat resolve-team)
|
||||
(keep h/parse-uuid))
|
||||
param)
|
||||
(= op :team-id)
|
||||
(cond
|
||||
(coll? param)
|
||||
(sequence (comp
|
||||
(mapcat resolve-team)
|
||||
(keep h/parse-uuid))
|
||||
param)
|
||||
|
||||
(uuid? param)
|
||||
(resolve-team param)
|
||||
(uuid? param)
|
||||
(resolve-team param)
|
||||
|
||||
(string? param)
|
||||
(some-> param h/parse-uuid resolve-team))
|
||||
(string? param)
|
||||
(some-> param h/parse-uuid resolve-team))
|
||||
|
||||
(= op :profile-id)
|
||||
(if (coll? param)
|
||||
(sequence (keep h/parse-uuid) param)
|
||||
(resolve-dest param))))))]
|
||||
(= op :profile-id)
|
||||
(if (coll? param)
|
||||
(sequence (keep h/parse-uuid) param)
|
||||
(resolve-dest param))))))]
|
||||
|
||||
(->> (resolve-dest dest)
|
||||
(filter some?)
|
||||
@@ -321,14 +337,23 @@
|
||||
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[file-id label]
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
[file-id & {:keys [label id]}]
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
snapshot-id (some-> id h/parse-uuid)]
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [snapshot (->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first))]
|
||||
(fsnap/restore-file-snapshot! system file-id (:id snapshot)))))))
|
||||
(cond
|
||||
(uuid? snapshot-id)
|
||||
(fsnap/restore-file-snapshot! system file-id snapshot-id)
|
||||
|
||||
(string? label)
|
||||
(->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first)
|
||||
(fsnap/restore-file-snapshot! system file-id))
|
||||
|
||||
:else
|
||||
(throw (ex-info "snapshot id or label should be provided" {})))))))
|
||||
|
||||
(defn list-file-snapshots!
|
||||
[file-id & {:as _}]
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 122785 (:size mobj1)))
|
||||
(t/is (= 3302 (:size mobj2)))))))
|
||||
(t/is (= 3299 (:size mobj2)))))))
|
||||
|
||||
(t/deftest media-object-upload
|
||||
(let [prof (th/create-profile* 1)
|
||||
@@ -85,7 +85,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 312043 (:size mobj1)))
|
||||
(t/is (= 3887 (:size mobj2)))))))
|
||||
(t/is (= 3901 (:size mobj2)))))))
|
||||
|
||||
|
||||
(t/deftest media-object-upload-idempotency
|
||||
@@ -163,7 +163,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 122785 (:size mobj1)))
|
||||
(t/is (= 3302 (:size mobj2)))))))
|
||||
(t/is (= 3299 (:size mobj2)))))))
|
||||
|
||||
(t/deftest media-object-upload-command
|
||||
(let [prof (th/create-profile* 1)
|
||||
@@ -200,7 +200,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 312043 (:size mobj1)))
|
||||
(t/is (= 3887 (:size mobj2)))))))
|
||||
(t/is (= 3901 (:size mobj2)))))))
|
||||
|
||||
|
||||
(t/deftest media-object-upload-idempotency-command
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
(defn undo
|
||||
[stack]
|
||||
(update stack :index dec))
|
||||
(update stack :index #(max 0 (dec %))))
|
||||
|
||||
(defn redo
|
||||
[{index :index items :items :as stack}]
|
||||
|
||||
@@ -85,12 +85,11 @@
|
||||
;; be applied (per example backend can operate in both modes with or
|
||||
;; without migration applied)
|
||||
(def no-migration-features
|
||||
(-> #{"fdata/objects-map"
|
||||
"fdata/pointer-map"
|
||||
"layout/grid"
|
||||
(-> #{"layout/grid"
|
||||
"fdata/shape-data-type"
|
||||
"design-tokens/v1"}
|
||||
(into frontend-only-features)))
|
||||
(into frontend-only-features)
|
||||
(into backend-only-features)))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::features}
|
||||
@@ -158,7 +157,6 @@
|
||||
team-features (into #{} xf-remove-ephimeral (:features team))]
|
||||
(-> enabled-features
|
||||
(set/intersection no-migration-features)
|
||||
(set/difference frontend-only-features)
|
||||
(set/union team-features))))
|
||||
|
||||
(defn check-client-features!
|
||||
@@ -167,6 +165,8 @@
|
||||
frontend client"
|
||||
[enabled-features client-features]
|
||||
(when (set? client-features)
|
||||
;; Check if client declares support for features enabled on
|
||||
;; backend side
|
||||
(let [not-supported (-> enabled-features
|
||||
(set/difference client-features)
|
||||
(set/difference frontend-only-features)
|
||||
@@ -176,14 +176,6 @@
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "client declares no support for '%' features"
|
||||
(str/join "," not-supported)))))
|
||||
|
||||
(let [not-supported (set/difference client-features supported-features)]
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "backend does not support '%' features requested by client"
|
||||
(str/join "," not-supported))))))
|
||||
|
||||
enabled-features)
|
||||
@@ -194,57 +186,49 @@
|
||||
supported by the current backend"
|
||||
[enabled-features]
|
||||
(let [not-supported (set/difference enabled-features supported-features)]
|
||||
(when (seq not-supported)
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "features '%' not supported"
|
||||
(str/join "," not-supported)))))
|
||||
enabled-features)
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "feature '%' not supported on this backend" not-supported)))
|
||||
enabled-features))
|
||||
|
||||
(defn check-file-features!
|
||||
"Function used for check feature compability between currently
|
||||
enabled features set on backend with the provided featured set by
|
||||
the penpot file"
|
||||
([enabled-features file-features]
|
||||
(check-file-features! enabled-features file-features #{}))
|
||||
([enabled-features file-features client-features]
|
||||
(let [file-features (into #{} xf-remove-ephimeral file-features)
|
||||
;; We should ignore all features that does not match with the
|
||||
;; `no-migration-features` set because we can't enable them
|
||||
;; as-is, because they probably need migrations
|
||||
client-features (set/intersection client-features no-migration-features)]
|
||||
(let [not-supported (-> enabled-features
|
||||
(set/union client-features)
|
||||
(set/difference file-features)
|
||||
;; NOTE: we don't want to raise a feature-mismatch
|
||||
;; exception for features which don't require an
|
||||
;; explicit file migration process or has no real
|
||||
;; effect on file data structure
|
||||
(set/difference no-migration-features))]
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "enabled features '%' not present in file (missing migration)"
|
||||
(str/join "," not-supported)))))
|
||||
[enabled-features file-features]
|
||||
(let [file-features (into #{} xf-remove-ephimeral file-features)
|
||||
not-supported (-> enabled-features
|
||||
(set/difference file-features)
|
||||
;; NOTE: we don't want to raise a feature-mismatch
|
||||
;; exception for features which don't require an
|
||||
;; explicit file migration process or has no real
|
||||
;; effect on file data structure
|
||||
(set/difference no-migration-features))]
|
||||
|
||||
(check-supported-features! file-features)
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
|
||||
not-supported)))
|
||||
|
||||
(let [not-supported (-> file-features
|
||||
(set/difference enabled-features)
|
||||
(set/difference client-features)
|
||||
(set/difference backend-only-features)
|
||||
(set/difference frontend-only-features))]
|
||||
(check-supported-features! file-features)
|
||||
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "file features '%' not enabled"
|
||||
(str/join "," not-supported))))))
|
||||
(let [not-supported (-> file-features
|
||||
(set/difference enabled-features)
|
||||
(set/difference backend-only-features)
|
||||
(set/difference frontend-only-features))]
|
||||
|
||||
enabled-features))
|
||||
;; Check if file has a feature but that feature is not enabled
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "file feature '%' not enabled" not-supported))))
|
||||
|
||||
enabled-features))
|
||||
|
||||
(defn check-teams-compatibility!
|
||||
[{source-features :features} {destination-features :features}]
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:export-file-v3
|
||||
:render-wasm-dpr
|
||||
:hide-release-modal})
|
||||
:hide-release-modal
|
||||
:subscriptions-old})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -390,14 +390,22 @@
|
||||
(register! :merge (mu/-merge))
|
||||
(register! :union (mu/-union))
|
||||
|
||||
(def uuid-rx
|
||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
(defn parse-uuid
|
||||
(defn- parse-uuid
|
||||
[s]
|
||||
(if (string? s)
|
||||
(some->> (re-matches uuid-rx s) uuid/uuid)
|
||||
s))
|
||||
(if (uuid? s)
|
||||
s
|
||||
(if (str/empty? s)
|
||||
nil
|
||||
(try
|
||||
(uuid/parse s)
|
||||
(catch #?(:clj Exception :cljs :default) _cause
|
||||
s)))))
|
||||
|
||||
(defn- encode-uuid
|
||||
[v]
|
||||
(if (uuid? v)
|
||||
(str v)
|
||||
v))
|
||||
|
||||
(register!
|
||||
{:type ::uuid
|
||||
@@ -409,8 +417,8 @@
|
||||
:gen/gen (sg/uuid)
|
||||
:decode/string parse-uuid
|
||||
:decode/json parse-uuid
|
||||
:encode/string str
|
||||
:encode/json str
|
||||
:encode/string encode-uuid
|
||||
:encode/json encode-uuid
|
||||
::oapi/type "string"
|
||||
::oapi/format "uuid"}})
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
(let [smallest (-> params :shrunk :smallest vec)]
|
||||
(println)
|
||||
(println "Condition failed with the following params:")
|
||||
(println "Seed:" (:seed params))
|
||||
(println)
|
||||
(pp/pprint smallest)))
|
||||
|
||||
|
||||
@@ -543,14 +543,23 @@
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; But if we are pasting, those are new items, so it is considered a change
|
||||
no-changes?
|
||||
(and (->> children (every? #(= parent-id (:parent-id %))))
|
||||
(and (every? #(= parent-id (:parent-id %)) children)
|
||||
(not pasting?))
|
||||
all-main?
|
||||
(->> children (every? #(ctk/main-instance? %)))]
|
||||
(every? ctk/main-instance? children)
|
||||
|
||||
any-main-descendant
|
||||
(some
|
||||
(fn [shape]
|
||||
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
|
||||
children)]
|
||||
|
||||
(if (or no-changes?
|
||||
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
|
||||
;; If we are moving into a variant-container, all the items should be main
|
||||
(or all-main? (not (ctk/is-variant-container? parent)))))
|
||||
(or all-main? (not (ctk/is-variant-container? parent)))
|
||||
;; If we are moving into a main component, no descendant can be main
|
||||
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))))
|
||||
[parent-id (get-frame parent-id)]
|
||||
(recur (:parent-id parent) objects children pasting? libraries))))))
|
||||
|
||||
|
||||
@@ -109,13 +109,27 @@
|
||||
(def check-animation!
|
||||
(sm/check-fn schema:animation))
|
||||
|
||||
(def schema:interaction-attrs
|
||||
[:map {:title "InteractionAttrs"}
|
||||
[:action-type {:optional true} [::sm/one-of action-types]]
|
||||
[:event-type {:optional true} [::sm/one-of event-types]]
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:preserve-scroll {:optional true} :boolean]
|
||||
[:animation {:optional true} schema:animation]
|
||||
[:overlay-position {:optional true} ::gpt/point]
|
||||
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
|
||||
[:close-click-outside {:optional true} :boolean]
|
||||
[:background-overlay {:optional true} :boolean]
|
||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]
|
||||
[:url {:optional true} :string]])
|
||||
|
||||
(def schema:navigate-interaction
|
||||
[:map
|
||||
[:action-type [:= :navigate]]
|
||||
[:event-type [::sm/one-of event-types]]
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:preserve-scroll {:optional true} :boolean]
|
||||
[:animation {:optional true} ::animation]])
|
||||
[:animation {:optional true} schema:animation]])
|
||||
|
||||
(def schema:open-overlay-interaction
|
||||
[:map
|
||||
@@ -126,7 +140,7 @@
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:close-click-outside {:optional true} :boolean]
|
||||
[:background-overlay {:optional true} :boolean]
|
||||
[:animation {:optional true} ::animation]
|
||||
[:animation {:optional true} schema:animation]
|
||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def schema:toggle-overlay-interaction
|
||||
@@ -138,7 +152,7 @@
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:close-click-outside {:optional true} :boolean]
|
||||
[:background-overlay {:optional true} :boolean]
|
||||
[:animation {:optional true} ::animation]
|
||||
[:animation {:optional true} schema:animation]
|
||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def schema:close-overlay-interaction
|
||||
@@ -146,7 +160,7 @@
|
||||
[:action-type [:= :close-overlay]]
|
||||
[:event-type [::sm/one-of event-types]]
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:animation {:optional true} ::animation]
|
||||
[:animation {:optional true} schema:animation]
|
||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def schema:prev-scren-interaction
|
||||
@@ -161,21 +175,21 @@
|
||||
[:url :string]])
|
||||
|
||||
(def schema:interaction
|
||||
[:multi {:dispatch :action-type
|
||||
:title "Interaction"
|
||||
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
|
||||
(sg/generator schema:open-overlay-interaction)
|
||||
(sg/generator schema:close-overlay-interaction)
|
||||
(sg/generator schema:toggle-overlay-interaction)
|
||||
(sg/generator schema:prev-scren-interaction)
|
||||
(sg/generator schema:open-url-interaction))
|
||||
:decode/json #(update % :action-type keyword)}
|
||||
[:navigate schema:navigate-interaction]
|
||||
[:open-overlay schema:open-overlay-interaction]
|
||||
[:toggle-overlay schema:toggle-overlay-interaction]
|
||||
[:close-overlay schema:close-overlay-interaction]
|
||||
[:prev-screen schema:prev-scren-interaction]
|
||||
[:open-url schema:open-url-interaction]])
|
||||
[:and {:title "Interaction"
|
||||
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
|
||||
(sg/generator schema:open-overlay-interaction)
|
||||
(sg/generator schema:close-overlay-interaction)
|
||||
(sg/generator schema:toggle-overlay-interaction)
|
||||
(sg/generator schema:prev-scren-interaction)
|
||||
(sg/generator schema:open-url-interaction))}
|
||||
schema:interaction-attrs
|
||||
[:multi {:dispatch :action-type}
|
||||
[:navigate schema:navigate-interaction]
|
||||
[:open-overlay schema:open-overlay-interaction]
|
||||
[:toggle-overlay schema:toggle-overlay-interaction]
|
||||
[:close-overlay schema:close-overlay-interaction]
|
||||
[:prev-screen schema:prev-scren-interaction]
|
||||
[:open-url schema:open-url-interaction]]])
|
||||
|
||||
(sm/register! ::interaction schema:interaction)
|
||||
|
||||
|
||||
@@ -17,9 +17,14 @@
|
||||
java.util.UUID
|
||||
java.nio.ByteBuffer)))
|
||||
|
||||
(def regex
|
||||
#"^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$")
|
||||
|
||||
(defn uuid
|
||||
"Creates an UUID instance from string, expectes valid uuid strings,
|
||||
the existense of validation is implementation detail"
|
||||
the existense of validation is implementation detail.
|
||||
|
||||
UNSAFE: this can accept invalid uuids or incomplete uuids"
|
||||
[s]
|
||||
#?(:clj (UUID/fromString s)
|
||||
:cljs (c/uuid s)))
|
||||
@@ -27,8 +32,21 @@
|
||||
(defn parse
|
||||
"Parse string uuid representation into proper UUID instance, validates input"
|
||||
[s]
|
||||
#?(:clj (UUID/fromString s)
|
||||
:cljs (c/parse-uuid s)))
|
||||
(if (and (string? s) ^boolean (re-matches regex s))
|
||||
#?(:clj (UUID/fromString s)
|
||||
:cljs (uuid s))
|
||||
|
||||
(let [message (str "invalid string '" s "' for uuid")]
|
||||
(throw #?(:clj (IllegalArgumentException. message)
|
||||
:cljs (js/Error. message))))))
|
||||
|
||||
(defn parse*
|
||||
"Exception safe version of `parse`."
|
||||
[s]
|
||||
(try
|
||||
(parse s)
|
||||
(catch #?(:clj Exception :cljs :default) _cause
|
||||
nil)))
|
||||
|
||||
(defn next
|
||||
[]
|
||||
|
||||
@@ -156,10 +156,13 @@ http {
|
||||
}
|
||||
|
||||
location / {
|
||||
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
|
||||
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
|
||||
location ~ ^/github/penpot-files/(.+)$ {
|
||||
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
|
||||
proxy_pass https://raw.githubusercontent.com;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_set_header User-Agent "curl/7.74.0";
|
||||
proxy_hide_header Cookies;
|
||||
proxy_set_header User-Agent "curl/8.5.0";
|
||||
proxy_set_header Host "raw.githubusercontent.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
|
||||
@@ -87,7 +87,7 @@ services:
|
||||
networks:
|
||||
- penpot
|
||||
|
||||
labels:
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
|
||||
# ## HTTPS: example of labels for the case where penpot will be exposed to the
|
||||
|
||||
@@ -135,10 +135,13 @@ http {
|
||||
}
|
||||
|
||||
location / {
|
||||
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
|
||||
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
|
||||
location ~ ^/github/penpot-files/(.+)$ {
|
||||
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
|
||||
proxy_pass https://raw.githubusercontent.com;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_set_header User-Agent "curl/7.74.0";
|
||||
proxy_hide_header Cookies;
|
||||
proxy_set_header User-Agent "curl/8.5.0";
|
||||
proxy_set_header Host "raw.githubusercontent.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
|
||||
@@ -151,9 +151,20 @@ Postgres database and another one for the assets uploaded by your users (images
|
||||
clips). There may be more volumes if you enable other features, as explained in the file
|
||||
itself.
|
||||
|
||||
## Configure the proxy
|
||||
## Configure the proxy and HTTPS
|
||||
|
||||
Your host configuration needs to make a proxy to http://localhost:9001.
|
||||
We strongly recommend to use Penpot under HTTPS/SSL, which will require specific server configurations for DNS and SSL certificates.
|
||||
Besides, your host configuration needs to make a proxy to http://localhost:9001.
|
||||
|
||||
<p class="advice">
|
||||
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
|
||||
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
|
||||
This is a configuration NOT recommended for production environments; as some browser APIs do
|
||||
not work properly under non-https environments, this unsecure configuration
|
||||
may limit the usage of Penpot; as an example, the clipboard does not work with HTTP.
|
||||
</p>
|
||||
|
||||
Below, you can see three examples with three different proxys:
|
||||
|
||||
### Example with NGINX
|
||||
|
||||
|
||||
@@ -848,7 +848,7 @@ title: Shortcuts
|
||||
</table>
|
||||
|
||||
<h2 id="viewer-section"> View mode </h2>
|
||||
<p>The View mode is the area to present and share designs and play the proptotype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p>
|
||||
<p>The View mode is the area to present and share designs and play the prototype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p>
|
||||
|
||||
<h3 id="generic-viewer">Generic</h3>
|
||||
<table cellspacing="0" cellpadding="1" border="1" width="100%">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
|
||||
@@ -511,9 +511,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
// Creates nesting by renaming set with double click
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "light-renamed" })
|
||||
.click({ button: "right" });
|
||||
await expect(tokenContextMenuForSet).toBeVisible();
|
||||
await tokenContextMenuForSet.getByText("Rename").click();
|
||||
.dblclick();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "nested/light");
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
@@ -558,6 +556,45 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("User can create & edit sets and set groups with an identical name", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabButton = tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
.click();
|
||||
|
||||
await createSet(tokenThemesSetsSidebar, "core/colors");
|
||||
await createSet(tokenThemesSetsSidebar, "core");
|
||||
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "core" })
|
||||
.nth(0)
|
||||
.dblclick();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "core-group-renamed");
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core-group-renamed",
|
||||
"colors",
|
||||
"core",
|
||||
]);
|
||||
|
||||
await page.keyboard.press(`ControlOrMeta+z`);
|
||||
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
|
||||
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "core" })
|
||||
.nth(1)
|
||||
.dblclick();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "core-set-renamed");
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"core-set-renamed",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Fold/Unfold set", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
@@ -239,15 +239,15 @@
|
||||
(str (:last-id file)))
|
||||
|
||||
(lookupShape [_ shape-id]
|
||||
(clj->js (fb/lookup-shape file (uuid/uuid shape-id))))
|
||||
(clj->js (fb/lookup-shape file (uuid/parse shape-id))))
|
||||
|
||||
(updateObject [_ id new-obj]
|
||||
(let [old-obj (fb/lookup-shape file (uuid/uuid id))
|
||||
(let [old-obj (fb/lookup-shape file (uuid/parse id))
|
||||
new-obj (d/deep-merge old-obj (parse-data new-obj))]
|
||||
(set! file (fb/update-object file old-obj new-obj))))
|
||||
|
||||
(deleteObject [_ id]
|
||||
(set! file (fb/delete-object file (uuid/uuid id))))
|
||||
(set! file (fb/delete-object file (uuid/parse id))))
|
||||
|
||||
(getId [_]
|
||||
(:id file))
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
[file ^string page-id]
|
||||
|
||||
;; Better to expose the api as a promise to be consumed from JS
|
||||
(let [page-id (uuid/uuid page-id)
|
||||
(let [page-id (uuid/parse page-id)
|
||||
file-data (.-file file)
|
||||
data (get-in file-data [:data :pages-index page-id])]
|
||||
(p/create
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.main.data.profile :as dp]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.errors]
|
||||
[app.main.features :as feat]
|
||||
[app.main.rasterizer :as thr]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui :as ui]
|
||||
@@ -67,7 +66,6 @@
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (ev/initialize)
|
||||
(feat/initialize)
|
||||
(dp/refresh-profile))
|
||||
|
||||
;; Watch for profile deletion events
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.features :as features]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -182,8 +181,8 @@
|
||||
(let [file-id (or file-id (:current-file-id state))
|
||||
uchg (vec undo-changes)
|
||||
rchg (vec redo-changes)
|
||||
features (features/get-team-enabled-features state)
|
||||
permissions (:permissions state)]
|
||||
features (get state :features)
|
||||
permissions (get state :permissions)]
|
||||
|
||||
;; Prevent commit changes by a viewer team member (it really should never happen)
|
||||
(when (:can-edit permissions)
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"Retrieves the mentions in the content as an array of uuids"
|
||||
[content]
|
||||
(->> (re-seq r-mentions content)
|
||||
(mapv (fn [[_ _ id]] (uuid/uuid id)))))
|
||||
(mapv (fn [[_ _ id]] (uuid/parse id)))))
|
||||
|
||||
(defn update-mentions
|
||||
"Updates the params object with the mentiosn"
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as-alias dps]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
@@ -73,7 +72,7 @@
|
||||
(st/emit! (ntf/hide)))
|
||||
|
||||
(defn handle-notification
|
||||
[{:keys [message code level] :as params}]
|
||||
[{:keys [message code] :as params}]
|
||||
(ptk/reify ::show-notification
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -81,9 +80,6 @@
|
||||
:upgrade-version
|
||||
(rx/of (ntf/dialog
|
||||
:content (tr "notifications.by-code.upgrade-version")
|
||||
:controls :inline-actions
|
||||
:type :inline
|
||||
:level level
|
||||
:accept {:label (tr "labels.refresh")
|
||||
:callback force-reload!}
|
||||
:tag :notification))
|
||||
@@ -91,16 +87,14 @@
|
||||
:maintenance
|
||||
(rx/of (ntf/dialog
|
||||
:content (tr "notifications.by-code.maintenance")
|
||||
:controls :inline-actions
|
||||
:type level
|
||||
:accept {:label (tr "labels.accept")
|
||||
:callback hide-notifications!}
|
||||
:tag :notification))
|
||||
|
||||
(rx/of (ntf/dialog
|
||||
:content message
|
||||
:controls :close
|
||||
:type level
|
||||
:accept {:label (tr "labels.close")
|
||||
:callback hide-notifications!}
|
||||
:tag :notification))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -112,7 +106,7 @@
|
||||
(ptk/reify ::show-shared-dialog
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
(let [features (get state :features)
|
||||
file (dsh/lookup-file state)
|
||||
data (get file :data)]
|
||||
|
||||
@@ -169,8 +163,8 @@
|
||||
(ptk/reify ::export-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
team-id (:current-team-id state)]
|
||||
(let [features (get state :features)
|
||||
team-id (get state :current-team-id)]
|
||||
(->> (rx/from files)
|
||||
(rx/mapcat
|
||||
(fn [file]
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.sse :as sse]
|
||||
@@ -57,8 +56,7 @@
|
||||
(rx/filter (fn [{:keys [topic] :as msg}]
|
||||
(or (= topic uuid/zero)
|
||||
(= topic profile-id))))
|
||||
(rx/map process-message)
|
||||
(rx/ignore)))
|
||||
(rx/map process-message)))
|
||||
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
@@ -497,7 +495,7 @@
|
||||
base-name (tr "dashboard.new-file-prefix")
|
||||
name (or name
|
||||
(cfh/generate-unique-name base-name unames :immediate-suffix? true))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
features (-> (get state :features)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
params (-> params
|
||||
(assoc :name name)
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -47,7 +46,7 @@
|
||||
(ptk/reify ::export-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
(let [features (get state :features)
|
||||
team-id (:current-team-id state)
|
||||
evname (if (= format :legacy-zip)
|
||||
"export-standard-files"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
(def ^:private schema:notification
|
||||
[:map {:title "Notification"}
|
||||
[:level [::sm/one-of #{:success :error :info :warning}]]
|
||||
[:level {:optional true} [::sm/one-of #{:success :error :info :warning}]]
|
||||
[:status {:optional true}
|
||||
[::sm/one-of #{:visible :hide}]]
|
||||
[:position {:optional true}
|
||||
@@ -129,15 +129,11 @@
|
||||
:timeout timeout})))
|
||||
|
||||
(defn dialog
|
||||
[& {:keys [content controls actions accept cancel position tag level links]
|
||||
:or {controls :none position :floating level :info}}]
|
||||
[& {:keys [content accept cancel tag links]}]
|
||||
(show (d/without-nils
|
||||
{:content content
|
||||
:level level
|
||||
:links links
|
||||
:position position
|
||||
:controls controls
|
||||
:actions actions
|
||||
:type :inline
|
||||
:accept accept
|
||||
:cancel cancel
|
||||
:links links
|
||||
:tag tag})))
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
(let [permissions (get team :permissions)
|
||||
features (get team :features)]
|
||||
(rx/of #(assoc % :permissions permissions)
|
||||
(features/initialize (or features #{}))
|
||||
(features/initialize features)
|
||||
(fetch-members team-id))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
@@ -199,7 +199,7 @@
|
||||
(ptk/reify ::webhooks-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:team-id team-id] assoc :webhooks webhooks))))
|
||||
(update-in state [:teams team-id] assoc :webhooks webhooks))))
|
||||
|
||||
(defn fetch-webhooks
|
||||
[]
|
||||
@@ -255,12 +255,12 @@
|
||||
(dm/assert! (string? name))
|
||||
(ptk/reify ::create-team
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(watch [it _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
features (features/get-enabled-features state)
|
||||
params {:name name :features features}]
|
||||
features features/global-enabled-features
|
||||
params {:name name :features features}]
|
||||
(->> (rp/cmd! :create-team (with-meta params (meta it)))
|
||||
(rx/tap on-success)
|
||||
(rx/map team-created)
|
||||
@@ -272,11 +272,11 @@
|
||||
[{:keys [name emails role] :as params}]
|
||||
(ptk/reify ::create-team-with-invitations
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(watch [it _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
features (features/get-enabled-features state)
|
||||
features features/global-enabled-features
|
||||
params {:name name
|
||||
:emails emails
|
||||
:role role
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [team-id (:id team)
|
||||
team {:members users}]
|
||||
team (assoc team :members users)]
|
||||
(-> state
|
||||
(assoc :share-links share-links)
|
||||
(assoc :current-team-id team-id)
|
||||
@@ -248,7 +248,7 @@
|
||||
|
||||
(defn fetch-comments
|
||||
[{:keys [thread-id]}]
|
||||
(dm/assert! (uuid thread-id))
|
||||
(assert (uuid? thread-id))
|
||||
(letfn [(fetched [comments state]
|
||||
(update state :comments assoc thread-id (d/index-by :id comments)))]
|
||||
(ptk/reify ::retrieve-comments
|
||||
@@ -413,7 +413,7 @@
|
||||
(watch [_ state _]
|
||||
(let [params (rt/get-params state)
|
||||
index (some-> params :index parse-long)
|
||||
page-id (some-> params :page-id parse-uuid)
|
||||
page-id (some-> params :page-id uuid/parse)
|
||||
|
||||
total (count (get-in state [:viewer :pages page-id :frames]))]
|
||||
|
||||
|
||||
@@ -205,30 +205,29 @@
|
||||
(d/index-by :id))))))
|
||||
|
||||
(defn- fetch-libraries
|
||||
[file-id]
|
||||
[file-id features]
|
||||
(ptk/reify ::fetch-libries
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)]
|
||||
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
||||
(rx/mapcat
|
||||
(fn [libraries]
|
||||
(rx/concat
|
||||
(rx/of (libraries-fetched file-id libraries))
|
||||
(rx/merge
|
||||
(->> (rx/from libraries)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
|
||||
(rx/mapcat resolve-file)
|
||||
(rx/map library-resolved))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
||||
(rx/map dwl/library-thumbnails-fetched)))
|
||||
(rx/of (check-libraries-synchronozation file-id libraries))))))))))
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
||||
(rx/mapcat
|
||||
(fn [libraries]
|
||||
(rx/concat
|
||||
(rx/of (libraries-fetched file-id libraries))
|
||||
(rx/merge
|
||||
(->> (rx/from libraries)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
|
||||
(rx/mapcat resolve-file)
|
||||
(rx/map library-resolved))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
||||
(rx/map dwl/library-thumbnails-fetched)))
|
||||
(rx/of (check-libraries-synchronozation file-id libraries)))))))))
|
||||
|
||||
(defn- workspace-initialized
|
||||
[file-id]
|
||||
@@ -246,28 +245,16 @@
|
||||
(fbs/fix-broken-shapes)))))
|
||||
|
||||
(defn- bundle-fetched
|
||||
[{:keys [features file thumbnails]}]
|
||||
[{:keys [file file-id thumbnails] :as bundle}]
|
||||
(ptk/reify ::bundle-fetched
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{:features features
|
||||
:file file
|
||||
:thumbnails thumbnails})
|
||||
(-deref [_] bundle)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [file-id (:id file)]
|
||||
(-> state
|
||||
(assoc :thumbnails thumbnails)
|
||||
(update :files assoc file-id file))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
file-id (:id file)]
|
||||
(rx/of (dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id))))))
|
||||
(-> state
|
||||
(assoc :thumbnails thumbnails)
|
||||
(update :files assoc file-id file)))))
|
||||
|
||||
(defn zoom-to-frame
|
||||
[]
|
||||
@@ -296,46 +283,30 @@
|
||||
|
||||
(defn- fetch-bundle
|
||||
"Multi-stage file bundle fetch coordinator"
|
||||
[file-id]
|
||||
[file-id features]
|
||||
(ptk/reify ::fetch-bundle
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
render-wasm? (contains? features "render-wasm/v1")
|
||||
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
team-id (:current-team-id state)]
|
||||
|
||||
(->> (rx/concat
|
||||
;; Firstly load wasm module if it is enabled and fonts
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
(rx/of (df/fetch-fonts team-id)))
|
||||
|
||||
;; Then fetch file and thumbnails
|
||||
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
|
||||
(get-file-object-thumbnails file-id))
|
||||
(rx/take 1)
|
||||
(rx/mapcat
|
||||
(fn [[file thumbnails]]
|
||||
(->> (resolve-file file)
|
||||
(rx/map (fn [file]
|
||||
{:file file
|
||||
:features features
|
||||
:thumbnails thumbnails})))))
|
||||
(rx/map bundle-fetched)))
|
||||
(watch [_ _ stream]
|
||||
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
|
||||
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
|
||||
(get-file-object-thumbnails file-id))
|
||||
(rx/take 1)
|
||||
(rx/mapcat
|
||||
(fn [[file thumbnails]]
|
||||
(->> (resolve-file file)
|
||||
(rx/map (fn [file]
|
||||
{:file file
|
||||
:file-id file-id
|
||||
:features features
|
||||
:thumbnails thumbnails})))))
|
||||
(rx/map bundle-fetched)
|
||||
(rx/take-until stopper-s))))))
|
||||
|
||||
(defn initialize-workspace
|
||||
[file-id]
|
||||
[team-id file-id]
|
||||
(assert (uuid? team-id) "expected valud uuid for `team-id`")
|
||||
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
||||
|
||||
(ptk/reify ::initialize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -347,31 +318,58 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(log/debug :hint "initialize-workspace" :file-id (dm/str file-id))
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)]
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)
|
||||
features (features/get-enabled-features state team-id)
|
||||
render-wasm? (contains? features "render-wasm/v1")]
|
||||
|
||||
(log/debug :hint "initialize-workspace"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
|
||||
(->> (rx/merge
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(fetch-bundle file-id))
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(df/fetch-fonts team-id)))
|
||||
|
||||
;; Once the essential data is fetched, lets proceed to
|
||||
;; fetch teh file bunldle
|
||||
(rx/of (fetch-bundle file-id features)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::bundle-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat (fn [{:keys [file]}]
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:file-id file-id}))))))
|
||||
(rx/mapcat
|
||||
(fn [{:keys [file]}]
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id features)
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
(rx/map dwc/set-workspace-visited))
|
||||
|
||||
(when-let [component-id (some-> rparams :component-id parse-uuid)]
|
||||
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
@@ -384,7 +382,7 @@
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
|
||||
(when-let [comment-id (some-> rparams :comment-id parse-uuid)]
|
||||
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
@@ -410,7 +408,7 @@
|
||||
(unchecked-set ug/global "name" name)))))
|
||||
|
||||
(defn finalize-workspace
|
||||
[file-id]
|
||||
[_team-id file-id]
|
||||
(ptk/reify ::finalize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -425,12 +423,12 @@
|
||||
:workspace-tokens
|
||||
:workspace-undo)
|
||||
(update :workspace-global dissoc :read-only?)
|
||||
(assoc-in [:workspace-global :options-mode] :design)))
|
||||
(assoc-in [:workspace-global :options-mode] :design)
|
||||
(update :files d/update-vals #(dissoc % :data))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (:current-project-id state)]
|
||||
|
||||
(rx/of (dwn/finalize file-id)
|
||||
(dpj/finalize-project project-id)
|
||||
(dwsl/finalize-shape-layout)
|
||||
@@ -444,14 +442,13 @@
|
||||
(ptk/reify ::reload-current-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)]
|
||||
(rx/of (initialize-workspace file-id))))))
|
||||
(let [file-id (:current-file-id state)
|
||||
team-id (:current-team-id state)]
|
||||
(rx/of (initialize-workspace team-id file-id))))))
|
||||
|
||||
;; Make this event callable through dynamic resolution
|
||||
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
|
||||
|
||||
|
||||
|
||||
(def ^:private xf:collect-file-media
|
||||
"Resolve and collect all file media on page objects"
|
||||
(comp (map second)
|
||||
@@ -488,18 +485,25 @@
|
||||
(defn initialize-page
|
||||
[file-id page-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? page-id) "expected valid uuid for `page-id`")
|
||||
|
||||
(ptk/reify ::initialize-page
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(if-let [page (dsh/lookup-page state file-id page-id)]
|
||||
(rx/concat (rx/of (initialize-page* file-id page-id page)
|
||||
(dwth/watch-state-changes file-id page-id)
|
||||
(dwl/watch-component-changes))
|
||||
(let [profile (:profile state)
|
||||
props (get profile :props)]
|
||||
(when (not (:workspace-visited props))
|
||||
(rx/of (select-frame-tool file-id page-id)))))
|
||||
(rx/concat
|
||||
(rx/of (initialize-page* file-id page-id page)
|
||||
(dwth/watch-state-changes file-id page-id)
|
||||
(dwl/watch-component-changes))
|
||||
(let [profile (:profile state)
|
||||
props (get profile :props)]
|
||||
(when (not (:workspace-visited props))
|
||||
(rx/of (select-frame-tool file-id page-id)))))
|
||||
|
||||
;; NOTE: this redirect is necessary for cases where user
|
||||
;; explicitly passes an non-existing page-id on the url
|
||||
;; params, so on check it we can detect that there are no data
|
||||
;; for the page and redirect user to an existing page
|
||||
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
|
||||
|
||||
(defn finalize-page
|
||||
@@ -1410,7 +1414,7 @@
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
selected (->> (dsh/lookup-selected state)
|
||||
(cfh/clean-loops objects))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
features (-> (get state :features)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
|
||||
file-id (:current-file-id state)
|
||||
@@ -1648,9 +1652,10 @@
|
||||
objects (dsh/lookup-page-objects state)]
|
||||
(when-let [shape (get objects selected)]
|
||||
(let [props (cts/extract-props shape)
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
features (-> (get state :features)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
version (-> (dsh/lookup-file state) :version)
|
||||
version (-> (dsh/lookup-file state)
|
||||
(get :version))
|
||||
|
||||
copy-data {:type :copied-props
|
||||
:features features
|
||||
@@ -1784,8 +1789,8 @@
|
||||
(ptk/reify ::paste-transit-shapes
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
features (features/get-team-enabled-features state)]
|
||||
(let [file-id (:current-file-id state)
|
||||
features (get state :features)]
|
||||
|
||||
(when-not (paste-data-valid? pdata)
|
||||
(ex/raise :type :validation
|
||||
@@ -1856,7 +1861,7 @@
|
||||
(ptk/reify ::paste-transit-props
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
(let [features (get state :features)
|
||||
selected (dsh/lookup-selected state)]
|
||||
|
||||
(when (paste-data-valid? pdata)
|
||||
@@ -2440,13 +2445,6 @@
|
||||
(js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref))
|
||||
(js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref))))))
|
||||
|
||||
(defn set-shape-ref
|
||||
[id shape-ref]
|
||||
(ptk/reify ::set-shape-ref
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (update-shape (uuid/uuid id) {:shape-ref (uuid/uuid shape-ref)})))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Exports
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -1398,7 +1398,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)]
|
||||
(let [features (get state :features)]
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id})
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
(->> (rx/from initmsg)
|
||||
(rx/map dws/send))
|
||||
|
||||
|
||||
;; Subscribe to notifications of the subscription
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.persistence :as dwp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.thumbnails :as th]
|
||||
@@ -97,7 +98,8 @@
|
||||
(ptk/reify ::restore-version
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)]
|
||||
(let [file-id (:current-file-id state)
|
||||
team-id (:current-team-id state)]
|
||||
(rx/concat
|
||||
(rx/of ::dwp/force-persist
|
||||
(dw/remove-layout-flag :document-history))
|
||||
@@ -106,7 +108,7 @@
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/tap #(th/clear-queue!))
|
||||
(rx/map #(dw/initialize-workspace file-id)))
|
||||
(rx/map #(dw/initialize-workspace team-id file-id)))
|
||||
(case origin
|
||||
:version
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
|
||||
@@ -200,21 +202,23 @@
|
||||
|
||||
(ptk/reify ::restore-version-from-plugins
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
|
||||
::dwp/force-persist)
|
||||
(watch [_ state _]
|
||||
(let [file (dsh/lookup-file state file-id)
|
||||
team-id (or (:team-id file) (:current-file-id state))]
|
||||
(rx/concat
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
|
||||
::dwp/force-persist)
|
||||
|
||||
;; FIXME: we should abstract this
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/map #(dw/initialize-workspace file-id)))
|
||||
;; FIXME: we should abstract this
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/map #(dw/initialize-workspace team-id file-id)))
|
||||
|
||||
(->> (rx/of 1)
|
||||
(rx/tap resolve)
|
||||
(rx/ignore))))))
|
||||
(->> (rx/of 1)
|
||||
(rx/tap resolve)
|
||||
(rx/ignore)))))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"A thin, frontend centric abstraction layer and collection of
|
||||
helpers for `app.common.features` namespace."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as log]
|
||||
[app.config :as cf]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm :as wasm]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
@@ -26,38 +26,32 @@
|
||||
(cfeat/get-enabled-features cf/flags))
|
||||
|
||||
(defn get-enabled-features
|
||||
[state]
|
||||
(-> (get state :features-runtime #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union global-enabled-features)))
|
||||
|
||||
(defn get-team-enabled-features
|
||||
[state]
|
||||
(let [runtime-features (:features-runtime state #{})
|
||||
team-features (->> (:features-team state #{})
|
||||
(into #{} cfeat/xf-remove-ephimeral))]
|
||||
"An explicit lookup of enabled features for the current team"
|
||||
[state team-id]
|
||||
(let [team (dm/get-in state [:teams team-id])]
|
||||
(-> global-enabled-features
|
||||
(set/union runtime-features)
|
||||
(set/union (get state :features-runtime #{}))
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))))
|
||||
|
||||
(def features-ref
|
||||
(l/derived get-team-enabled-features st/state =))
|
||||
(set/union (get team :features)))))
|
||||
|
||||
(defn active-feature?
|
||||
"Given a state and feature, check if feature is enabled"
|
||||
"Given a state and feature, check if feature is enabled."
|
||||
[state feature]
|
||||
(assert (contains? cfeat/supported-features feature) "not supported feature")
|
||||
(or (contains? (get state :features-runtime) feature)
|
||||
(if (contains? cfeat/no-migration-features feature)
|
||||
(or (contains? global-enabled-features feature)
|
||||
(contains? (get state :features-team) feature))
|
||||
(contains? (get state :features-team state) feature))))
|
||||
(assert (contains? cfeat/supported-features feature) "feature not supported")
|
||||
(let [runtime-features (get state :features-runtime)
|
||||
enabled-features (get state :features)]
|
||||
(or (contains? runtime-features feature)
|
||||
(if (contains? cfeat/no-migration-features feature)
|
||||
(or (contains? global-enabled-features feature)
|
||||
(contains? enabled-features feature))
|
||||
(contains? enabled-features feature)))))
|
||||
|
||||
(def ^:private features-ref
|
||||
(l/derived (l/key :features) st/state))
|
||||
|
||||
(defn use-feature
|
||||
"A react hook that checks if feature is currently enabled"
|
||||
[feature]
|
||||
(assert (contains? cfeat/supported-features feature) "Not supported feature")
|
||||
(let [enabled-features (mf/deref features-ref)]
|
||||
(contains? enabled-features feature)))
|
||||
|
||||
@@ -71,14 +65,16 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assert (contains? cfeat/supported-features feature) "not supported feature")
|
||||
(update state :features-runtime (fn [features]
|
||||
(if (contains? features feature)
|
||||
(do
|
||||
(log/trc :hint "feature disabled" :feature feature)
|
||||
(disj features feature))
|
||||
(do
|
||||
(log/trc :hint "feature enabled" :feature feature)
|
||||
(conj features feature))))))))
|
||||
(-> state
|
||||
(update :features-runtime (fn [features]
|
||||
(if (contains? features feature)
|
||||
(do
|
||||
(log/trc :hint "feature disabled" :feature feature)
|
||||
(disj features feature))
|
||||
(do
|
||||
(log/trc :hint "feature enabled" :feature feature)
|
||||
(conj features feature)))))
|
||||
(update :features-runtime set/intersection cfeat/no-migration-features)))))
|
||||
|
||||
(defn enable-feature
|
||||
[feature]
|
||||
@@ -90,46 +86,28 @@
|
||||
state
|
||||
(do
|
||||
(log/trc :hint "feature enabled" :feature feature)
|
||||
(update state :features-runtime (fnil conj #{}) feature))))))
|
||||
(-> state
|
||||
(update :features-runtime (fnil conj #{}) feature)
|
||||
(update :features-runtime set/intersection cfeat/no-migration-features)))))))
|
||||
|
||||
(defn initialize
|
||||
([] (initialize #{}))
|
||||
([team-features]
|
||||
(assert (set? team-features) "expected a set of features")
|
||||
(assert (every? string? team-features) "expected a set of strings")
|
||||
[features]
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [features (-> global-enabled-features
|
||||
(set/union (get state :features-runtime #{}))
|
||||
(set/union features))]
|
||||
(assoc state :features features)))
|
||||
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [runtime-features (get state :features/runtime #{})
|
||||
team-features (into #{}
|
||||
cfeat/xf-supported-features
|
||||
team-features)]
|
||||
(-> state
|
||||
(assoc :features-runtime runtime-features)
|
||||
(assoc :features-team team-features))))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [features (get state :features)]
|
||||
(if (contains? features "render-wasm/v1")
|
||||
(wasm/initialize true)
|
||||
(wasm/initialize false))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when *assert*
|
||||
(->> (rx/from cfeat/no-migration-features)
|
||||
;; text editor v2 isn't enabled by default even in devenv
|
||||
;; wasm render v1 isn't enabled by default even in devenv
|
||||
(rx/filter #(not (or (contains? cfeat/backend-only-features %)
|
||||
(= "text-editor/v2" %)
|
||||
(= "render-wasm/v1" %)
|
||||
(= "design-tokens/v1" %))))
|
||||
(rx/observe-on :async)
|
||||
(rx/map enable-feature))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [features (get-team-enabled-features state)]
|
||||
(if (contains? features "render-wasm/v1")
|
||||
(wasm/initialize true)
|
||||
(wasm/initialize false))
|
||||
|
||||
(log/inf :hint "initialized"
|
||||
:enabled (str/join "," features)
|
||||
:runtime (str/join "," (:features-runtime state))))))))
|
||||
(log/inf :hint "initialized"
|
||||
:enabled (str/join "," features)
|
||||
:runtime (str/join "," (:features-runtime state)))))))
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.ui
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.team :as dtm]
|
||||
@@ -69,7 +70,7 @@
|
||||
(mf/defc dashboard-legacy-redirect*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [section team-id project-id search-term plugin-url template-url]}]
|
||||
[{:keys [section team-id project-id search-term plugin-url template]}]
|
||||
(let [section (case section
|
||||
:dashboard-legacy-search
|
||||
:dashboard-search
|
||||
@@ -97,7 +98,7 @@
|
||||
:project-id project-id
|
||||
:search-term search-term
|
||||
:plugin plugin-url
|
||||
:template-url template-url}]
|
||||
:template template}]
|
||||
(st/emit! (rt/nav section (d/without-nils params)))))
|
||||
|
||||
[:> loader*
|
||||
@@ -212,11 +213,11 @@
|
||||
:dashboard-webhooks
|
||||
:dashboard-settings)
|
||||
(let [params (get params :query)
|
||||
team-id (some-> params :team-id uuid)
|
||||
project-id (some-> params :project-id uuid)
|
||||
team-id (some-> params :team-id uuid/parse*)
|
||||
project-id (some-> params :project-id uuid/parse*)
|
||||
search-term (some-> params :search-term)
|
||||
plugin-url (some-> params :plugin)
|
||||
template-url (some-> params :template)]
|
||||
template (some-> params :template)]
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
@@ -243,13 +244,13 @@
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:project-id project-id
|
||||
:template-url template-url}]]])
|
||||
:template template}]]])
|
||||
|
||||
:workspace
|
||||
(let [params (get params :query)
|
||||
team-id (some-> params :team-id uuid)
|
||||
file-id (some-> params :file-id uuid)
|
||||
page-id (some-> params :page-id uuid)
|
||||
team-id (some-> params :team-id uuid/parse*)
|
||||
file-id (some-> params :file-id uuid/parse*)
|
||||
page-id (some-> params :page-id uuid/parse*)
|
||||
layout (some-> params :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
@@ -276,15 +277,15 @@
|
||||
:viewer
|
||||
(let [params (get params :query)
|
||||
index (some-> (:index params) parse-long)
|
||||
share-id (some-> (:share-id params) parse-uuid)
|
||||
share-id (some-> (:share-id params) uuid/parse*)
|
||||
section (or (some-> (:section params) keyword)
|
||||
:interactions)
|
||||
|
||||
file-id (some-> (:file-id params) parse-uuid)
|
||||
page-id (some-> (:page-id params) parse-uuid)
|
||||
file-id (some-> (:file-id params) uuid/parse*)
|
||||
page-id (some-> (:page-id params) uuid/parse*)
|
||||
imode (or (some-> (:interactions-mode params) keyword)
|
||||
:show-on-click)
|
||||
frame-id (some-> (:frame-id params) parse-uuid)
|
||||
frame-id (some-> (:frame-id params) uuid/parse*)
|
||||
share (:share params)]
|
||||
|
||||
[:? {}
|
||||
@@ -300,9 +301,9 @@
|
||||
|
||||
|
||||
:workspace-legacy
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
file-id (some-> params :path :file-id uuid)
|
||||
page-id (some-> params :query :page-id uuid)
|
||||
(let [project-id (some-> params :path :project-id uuid/parse*)
|
||||
file-id (some-> params :path :file-id uuid/parse*)
|
||||
page-id (some-> params :query :page-id uuid/parse*)
|
||||
layout (some-> params :query :layout keyword)]
|
||||
|
||||
[:> workspace-legacy-redirect*
|
||||
@@ -321,18 +322,18 @@
|
||||
:dashboard-legacy-team-invitations
|
||||
:dashboard-legacy-team-webhooks
|
||||
:dashboard-legacy-team-settings)
|
||||
(let [team-id (some-> params :path :team-id uuid)
|
||||
project-id (some-> params :path :project-id uuid)
|
||||
(let [team-id (some-> params :path :team-id uuid/parse*)
|
||||
project-id (some-> params :path :project-id uuid/parse*)
|
||||
search-term (some-> params :query :search-term)
|
||||
plugin-url (some-> params :query :plugin)
|
||||
template-url (some-> params :template)]
|
||||
template (some-> params :template)]
|
||||
[:> dashboard-legacy-redirect*
|
||||
{:team-id team-id
|
||||
:section section
|
||||
:project-id project-id
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:template-url template-url}])
|
||||
:template template}])
|
||||
|
||||
:viewer-legacy
|
||||
(let [{:keys [query-params path-params]} route
|
||||
@@ -370,6 +371,6 @@
|
||||
(if edata
|
||||
[:> static/exception-page* {:data edata :route route}]
|
||||
[:> error-boundary* {:fallback static/internal-error*}
|
||||
[:& notifications/current-notification]
|
||||
[:> notifications/current-notification*]
|
||||
(when route
|
||||
[:> page* {:route route :profile profile}])])]]))
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
(dom/stop-propagation event)
|
||||
(let [id (-> (dom/get-current-target event)
|
||||
(dom/get-data "user-id")
|
||||
(uuid/uuid))
|
||||
(uuid/parse))
|
||||
|
||||
user (d/seek #(= (:id %) id) members)]
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
[item item item]))
|
||||
|
||||
(mf/defc select
|
||||
[{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled]}]
|
||||
[{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled data-direction]}]
|
||||
(let [label-index (mf/with-memo [options]
|
||||
(into {} (map as-key-value) options))
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
[:span {:class (stl/css :current-label)} current-label]
|
||||
[:span {:class (stl/css :dropdown-button)} i/arrow]
|
||||
[:& dropdown {:show is-open? :on-close close-dropdown}
|
||||
[:ul {:ref dropdown-element* :data-direction @dropdown-direction*
|
||||
[:ul {:ref dropdown-element* :data-direction (or data-direction @dropdown-direction*)
|
||||
:class (dm/str dropdown-class " " (stl/css :custom-select-dropdown))}
|
||||
(for [[index item] (d/enumerate options)]
|
||||
(if (= :separator item)
|
||||
|
||||
@@ -34,12 +34,10 @@
|
||||
[app.main.ui.workspace.plugins]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
@@ -211,17 +209,22 @@
|
||||
(swap! storage/session dissoc :plugin-url))))))
|
||||
|
||||
(defn use-templates-import
|
||||
[can-edit? template-url project]
|
||||
[can-edit? template project]
|
||||
(let [project-id (get project :id)
|
||||
team-id (get project :team-id)]
|
||||
(mf/with-layout-effect [can-edit? template-url project-id team-id]
|
||||
(when (and (some? template-url)
|
||||
(mf/with-layout-effect [can-edit? template project-id team-id]
|
||||
(when (and (some? template)
|
||||
(some? project-id)
|
||||
(some? team-id))
|
||||
(if can-edit?
|
||||
(let [valid-url? (and (str/ends-with? template-url ".penpot")
|
||||
(str/starts-with? template-url cf/templates-uri))
|
||||
template-name (when valid-url? (subs template-url (count cf/templates-uri)))
|
||||
(let [valid-url? (str/ends-with? template ".penpot")
|
||||
|
||||
;; Backwards compatibility, ideally the template should be only the .penpot file name, not the full url
|
||||
template-name (if (str/starts-with? template "http")
|
||||
(subs template (count cf/templates-uri))
|
||||
template)
|
||||
|
||||
template-url (str "/github/penpot-files/" template-name)
|
||||
on-import #(st/emit! (dpj/fetch-files project-id)
|
||||
(dd/fetch-recent-files team-id)
|
||||
(dd/fetch-projects team-id)
|
||||
@@ -230,30 +233,22 @@
|
||||
:name template-name
|
||||
:url template-url}))]
|
||||
(if valid-url?
|
||||
(do
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "install-template-from-link" :name template-name :url template-url}))
|
||||
(->> (http/send! {:method :get
|
||||
:uri template-url
|
||||
:response-type :blob
|
||||
:omit-default-headers true})
|
||||
(rx/subs!
|
||||
(fn [result]
|
||||
(if (or (< (:status result) 200) (>= (:status result) 300))
|
||||
(st/emit! (notif/error (tr "dashboard.import.error")))
|
||||
(st/emit! (modal/show
|
||||
{:type :import
|
||||
:project-id project-id
|
||||
:entries [{:name template-name :uri (wapi/create-uri (:body result))}]
|
||||
:on-finish-import on-import})))))))
|
||||
(st/emit!
|
||||
(ptk/event ::ev/event {::ev/name "install-template-from-link" :name template-name :url template-url})
|
||||
(modal/show
|
||||
{:type :import
|
||||
:project-id project-id
|
||||
:entries [{:name template-name :uri template-url}]
|
||||
:on-finish-import on-import}))
|
||||
(st/emit! (notif/error (tr "dashboard.import.bad-url")))))
|
||||
(st/emit! (notif/error (tr "dashboard.import.no-perms"))))
|
||||
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :template-url))))))
|
||||
(swap! storage/session dissoc :template))))))
|
||||
|
||||
(mf/defc dashboard*
|
||||
{::mf/props :obj}
|
||||
[{:keys [profile project-id team-id search-term plugin-url template-url section]}]
|
||||
[{:keys [profile project-id team-id search-term plugin-url template section]}]
|
||||
(let [team (mf/deref refs/team)
|
||||
projects (mf/deref refs/projects)
|
||||
|
||||
@@ -263,7 +258,7 @@
|
||||
(filterv #(= team-id (:team-id %)))))
|
||||
|
||||
can-edit? (dm/get-in team [:permissions :can-edit])
|
||||
template-url (or template-url (:template-url storage/session))
|
||||
template (or template (:template storage/session))
|
||||
plugin-url (or plugin-url (:plugin-url storage/session))
|
||||
|
||||
default-project
|
||||
@@ -289,7 +284,7 @@
|
||||
(events/unlistenByKey key))))
|
||||
|
||||
(use-plugin-register plugin-url team-id (:id default-project))
|
||||
(use-templates-import can-edit? template-url default-project)
|
||||
(use-templates-import can-edit? template default-project)
|
||||
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> modal-container*]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.media :as cm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.modal :as modal]
|
||||
@@ -121,7 +122,7 @@
|
||||
(fn [event]
|
||||
(let [id (-> (dom/get-current-target event)
|
||||
(dom/get-data "id")
|
||||
(parse-uuid))
|
||||
(uuid/parse))
|
||||
item (get fonts id)]
|
||||
(on-upload* item))))
|
||||
|
||||
@@ -132,7 +133,7 @@
|
||||
(let [target (dom/get-current-target event)
|
||||
id (-> target
|
||||
(dom/get-data "id")
|
||||
(parse-uuid))
|
||||
(uuid/parse))
|
||||
name (dom/get-value target)]
|
||||
(when-not (str/blank? name)
|
||||
(swap! fonts* df/rename-and-regroup id name installed-fonts)))))
|
||||
@@ -143,7 +144,7 @@
|
||||
(let [target (dom/get-current-target event)
|
||||
id (-> target
|
||||
(dom/get-data "id")
|
||||
(parse-uuid))
|
||||
(uuid/parse))
|
||||
name (dom/get-value target)]
|
||||
(swap! fonts* update id assoc :font-family-tmp name))))
|
||||
|
||||
@@ -153,7 +154,7 @@
|
||||
(fn [event]
|
||||
(let [id (-> (dom/get-current-target event)
|
||||
(dom/get-data "id")
|
||||
(parse-uuid))]
|
||||
(uuid/parse))]
|
||||
(swap! fonts* dissoc id))))
|
||||
|
||||
on-upload-all
|
||||
@@ -344,7 +345,7 @@
|
||||
(fn [event]
|
||||
(let [id (-> (dom/get-current-target event)
|
||||
(dom/get-data "id")
|
||||
(parse-uuid))
|
||||
(uuid/parse))
|
||||
options {:type :confirm
|
||||
:title (tr "modals.delete-font-variant.title")
|
||||
:message (tr "modals.delete-font-variant.message")
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.project :as dpj]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.rasterizer :as thr]
|
||||
[app.main.refs :as refs]
|
||||
@@ -29,7 +28,7 @@
|
||||
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
||||
[app.main.ui.dashboard.placeholder :refer [empty-grid-placeholder* loading-placeholder*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as i]
|
||||
@@ -60,7 +59,7 @@
|
||||
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:features (features/get-team-enabled-features @st/state)})
|
||||
:features (get @st/state :features)})
|
||||
(rx/mapcat (fn [{:keys [fonts] :as result}]
|
||||
(->> (fonts/render-font-styles fonts)
|
||||
(rx/map (fn [styles]
|
||||
@@ -512,7 +511,7 @@
|
||||
:ref node-ref}
|
||||
(cond
|
||||
(nil? files)
|
||||
[:& loading-placeholder]
|
||||
[:> loading-placeholder*]
|
||||
|
||||
(seq files)
|
||||
(for [[index slice] (d/enumerate (partition-all limit files))]
|
||||
@@ -529,12 +528,13 @@
|
||||
:can-edit can-edit}])])
|
||||
|
||||
:else
|
||||
[:& empty-placeholder
|
||||
[:> empty-grid-placeholder*
|
||||
{:limit limit
|
||||
:can-edit can-edit
|
||||
:create-fn create-fn
|
||||
:origin origin
|
||||
:project-id project-id
|
||||
:team-id team-id
|
||||
:on-finish-import on-finish-import}])]))
|
||||
|
||||
(mf/defc line-grid-row
|
||||
@@ -646,7 +646,7 @@
|
||||
:on-drop on-drop}
|
||||
(cond
|
||||
(nil? files)
|
||||
[:& loading-placeholder]
|
||||
[:> loading-placeholder*]
|
||||
|
||||
(seq files)
|
||||
[:& line-grid-row {:files files
|
||||
@@ -657,10 +657,11 @@
|
||||
:limit limit}]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder
|
||||
{:dragging? @dragging?
|
||||
[:> empty-grid-placeholder*
|
||||
{:is-dragging @dragging?
|
||||
:limit limit
|
||||
:can-edit can-edit
|
||||
:create-fn create-fn
|
||||
:project-id project-id
|
||||
:team-id team-id
|
||||
:on-finish-import on-finish-import}])]))
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
@@ -162,29 +161,32 @@
|
||||
|
||||
(defn- analyze-entries
|
||||
[state entries]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :analyze-import
|
||||
:files entries
|
||||
:features @features/features-ref})
|
||||
(rx/mapcat #(rx/delay emit-delay (rx/of %)))
|
||||
(rx/filter some?)
|
||||
(rx/subs!
|
||||
(fn [message]
|
||||
(swap! state update-with-analyze-result message)))))
|
||||
(let [features (get @st/state :features)]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :analyze-import
|
||||
:files entries
|
||||
:features features})
|
||||
(rx/mapcat #(rx/delay emit-delay (rx/of %)))
|
||||
(rx/filter some?)
|
||||
(rx/subs!
|
||||
(fn [message]
|
||||
(swap! state update-with-analyze-result message))))))
|
||||
|
||||
(defn- import-files
|
||||
[state project-id entries]
|
||||
(st/emit! (ptk/data-event ::ev/event {::ev/name "import-files"
|
||||
:num-files (count entries)}))
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :import-files
|
||||
:project-id project-id
|
||||
:files entries
|
||||
:features @features/features-ref})
|
||||
(rx/filter (comp uuid? :file-id))
|
||||
(rx/subs!
|
||||
(fn [message]
|
||||
(swap! state update-entry-status message)))))
|
||||
|
||||
(let [features (get @st/state :features)]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :import-files
|
||||
:project-id project-id
|
||||
:files entries
|
||||
:features features})
|
||||
(rx/filter (comp uuid? :file-id))
|
||||
(rx/subs!
|
||||
(fn [message]
|
||||
(swap! state update-entry-status message))))))
|
||||
|
||||
(mf/defc import-entry*
|
||||
{::mf/props :obj
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.dashboard.placeholder
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.import :as udi]
|
||||
@@ -16,50 +15,92 @@
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc empty-placeholder-projects*
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-create on-finish-import project-id] :as props}]
|
||||
(mf/defc empty-project-placeholder*
|
||||
{::mf/private true}
|
||||
[{:keys [on-create on-finish-import project-id]}]
|
||||
(let [file-input (mf/use-ref nil)
|
||||
on-add-library (mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
|
||||
::ev/origin "dashboard"
|
||||
:section "empty-placeholder-projects"}))
|
||||
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
|
||||
on-import-files (mf/use-fn #(dom/click (mf/ref-val file-input)))]
|
||||
|
||||
on-add-library
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
|
||||
::ev/origin "dashboard"
|
||||
:section "empty-placeholder-projects"}))
|
||||
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
|
||||
|
||||
on-import
|
||||
(mf/use-fn #(dom/click (mf/ref-val file-input)))]
|
||||
|
||||
[:div {:class (stl/css :empty-project-container)}
|
||||
[:div {:class (stl/css :empty-project-card) :on-click on-create :title (tr "dashboard.add-file")}
|
||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.create")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.start")]]
|
||||
[:div {:class (stl/css :empty-project-card)
|
||||
:on-click on-create
|
||||
:title (tr "dashboard.add-file")}
|
||||
[:div {:class (stl/css :empty-project-card-title)}
|
||||
(tr "dashboard.empty-project.create")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||
(tr "dashboard.empty-project.start")]]
|
||||
|
||||
[:div {:class (stl/css :empty-project-card) :on-click on-import-files :title (tr "dashboard.empty-project.import")}
|
||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.import")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.import-penpot")]]
|
||||
[:div {:class (stl/css :empty-project-card)
|
||||
:on-click on-import
|
||||
:title (tr "dashboard.empty-project.import")}
|
||||
[:div {:class (stl/css :empty-project-card-title)}
|
||||
(tr "dashboard.empty-project.import")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||
(tr "dashboard.empty-project.import-penpot")]]
|
||||
|
||||
[:div {:class (stl/css :empty-project-card) :on-click on-add-library :title (tr "dashboard.empty-project.go-to-libraries")}
|
||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.add-library")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.explore")]]
|
||||
[:div {:class (stl/css :empty-project-card)
|
||||
:on-click on-add-library
|
||||
:title (tr "dashboard.empty-project.go-to-libraries")}
|
||||
[:div {:class (stl/css :empty-project-card-title)}
|
||||
(tr "dashboard.empty-project.add-library")]
|
||||
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||
(tr "dashboard.empty-project.explore")]]
|
||||
|
||||
[:& udi/import-form {:ref file-input
|
||||
:project-id project-id
|
||||
:on-finish-import on-finish-import}]]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging? limit origin create-fn can-edit project-id on-finish-import]}]
|
||||
(defn- make-has-other-files-or-projects-ref
|
||||
"Return a ref that resolves to true or false if there are at least some
|
||||
file or some project (a part of the default) exists; this determines
|
||||
if we need to show a complete placeholder or the small one."
|
||||
[team-id]
|
||||
(l/derived (fn [state]
|
||||
(or (let [projects (get state :projects)]
|
||||
(some (fn [[_ project]]
|
||||
(and (= (:team-id project) team-id)
|
||||
(not (:is-default project))))
|
||||
projects))
|
||||
(let [files (get state :files)]
|
||||
(some (fn [[_ file]]
|
||||
(= (:team-id file) team-id))
|
||||
files))))
|
||||
st/state))
|
||||
|
||||
(mf/defc empty-grid-placeholder*
|
||||
[{:keys [is-dragging limit origin create-fn can-edit team-id project-id on-finish-import]}]
|
||||
(let [on-click
|
||||
(mf/use-fn
|
||||
(mf/deps create-fn)
|
||||
(fn [_]
|
||||
(create-fn "dashboard:empty-folder-placeholder")))
|
||||
show-text (mf/use-state nil)
|
||||
on-mouse-enter (mf/use-fn #(reset! show-text true))
|
||||
on-mouse-leave (mf/use-fn #(reset! show-text nil))]
|
||||
|
||||
show-text* (mf/use-state nil)
|
||||
show-text? (deref show-text*)
|
||||
|
||||
on-mouse-enter (mf/use-fn #(reset! show-text* true))
|
||||
on-mouse-leave (mf/use-fn #(reset! show-text* nil))
|
||||
|
||||
has-other* (mf/with-memo [team-id]
|
||||
(make-has-other-files-or-projects-ref team-id))
|
||||
has-other? (mf/deref has-other*)]
|
||||
|
||||
(cond
|
||||
(true? dragging?)
|
||||
(true? is-dragging)
|
||||
[:ul
|
||||
{:class (stl/css :grid-row :no-wrap)
|
||||
:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
|
||||
@@ -79,22 +120,24 @@
|
||||
:tag-name "span"}])]
|
||||
|
||||
:else
|
||||
(if (cf/external-feature-flag "add-file-02" "test")
|
||||
[:> empty-placeholder-projects* {:on-create on-click :on-finish-import on-finish-import :project-id project-id}]
|
||||
(if-not has-other?
|
||||
[:> empty-project-placeholder*
|
||||
{:on-create on-click
|
||||
:on-finish-import on-finish-import
|
||||
:project-id project-id}]
|
||||
[:div {:class (stl/css :grid-empty-placeholder)}
|
||||
(if (cf/external-feature-flag "add-file-01" "test")
|
||||
[:button {:class (stl/css :create-new)
|
||||
:on-click on-click
|
||||
:on-mouse-enter on-mouse-enter
|
||||
:on-mouse-leave on-mouse-leave}
|
||||
(if @show-text (tr "dashboard.empty-project.create") i/add)]
|
||||
[:button {:class (stl/css :create-new)
|
||||
:on-click on-click}
|
||||
i/add])]))))
|
||||
[:button {:class (stl/css :create-new)
|
||||
:on-click on-click
|
||||
:on-mouse-enter on-mouse-enter
|
||||
:on-mouse-leave on-mouse-leave}
|
||||
(if show-text?
|
||||
(tr "dashboard.empty-project.create")
|
||||
i/add)]]))))
|
||||
|
||||
(mf/defc loading-placeholder
|
||||
(mf/defc loading-placeholder*
|
||||
[]
|
||||
[:> loader* {:width 32
|
||||
:title (tr "labels.loading")
|
||||
:class (stl/css :placeholder-loader)}
|
||||
[:span {:class (stl/css :placeholder-text)} (tr "dashboard.loading-files")]])
|
||||
[:span {:class (stl/css :placeholder-text)}
|
||||
(tr "dashboard.loading-files")]])
|
||||
|
||||
@@ -371,6 +371,7 @@
|
||||
show-team-hero?
|
||||
can-invite))}
|
||||
(for [{:keys [id] :as project} projects]
|
||||
;; FIXME: refactor this, looks inneficient
|
||||
(let [files (when recent-map
|
||||
(->> (vals recent-map)
|
||||
(filterv #(= id (:project-id %)))
|
||||
|
||||
@@ -284,7 +284,6 @@
|
||||
(let [team-id (-> (dom/get-current-target event)
|
||||
(dom/get-data "value")
|
||||
(uuid/parse))]
|
||||
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
|
||||
|
||||
handle-select-default
|
||||
@@ -963,13 +962,14 @@
|
||||
(dom/open-new-window "https://penpot.app/pricing")))]
|
||||
|
||||
[:*
|
||||
[:button {:class (stl/css :upgrade-plan-section)
|
||||
:on-click on-power-up-click}
|
||||
[:div {:class (stl/css :penpot-free)}
|
||||
[:span (tr "dashboard.upgrade-plan.penpot-free")]
|
||||
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]]
|
||||
[:div {:class (stl/css :power-up)}
|
||||
(tr "dashboard.upgrade-plan.power-up")]]
|
||||
(when (contains? cf/flags :subscriptions-old)
|
||||
[:button {:class (stl/css :upgrade-plan-section)
|
||||
:on-click on-power-up-click}
|
||||
[:div {:class (stl/css :penpot-free)}
|
||||
[:span (tr "dashboard.upgrade-plan.penpot-free")]
|
||||
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]]
|
||||
[:div {:class (stl/css :power-up)}
|
||||
(tr "dashboard.upgrade-plan.power-up")]])
|
||||
(when (and team profile)
|
||||
[:& comments-section
|
||||
{:profile profile
|
||||
|
||||
@@ -1045,7 +1045,7 @@
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-webhooks)))
|
||||
|
||||
[:*
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as IconStories from "./icon.stories"
|
||||
|
||||
# Iconography
|
||||
|
||||
See the [list of all available icons](?path=/story/foundations-icons--all-icons).
|
||||
See the [list of all available icons](?path=/story/foundations-assets-icon--all).
|
||||
|
||||
## Variants
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
[:class {:optional true} :string]
|
||||
[:variant {:optional true}
|
||||
[:maybe [:enum "default" "error"]]]
|
||||
[:accept-label {:optional true} :string]
|
||||
[:cancel-label {:optional true} :string]
|
||||
[:on-accept {:optional true} [:fn fn?]]
|
||||
[:on-cancel {:optional true} [:fn fn?]]])
|
||||
[:accept-label {:optional true} [:maybe :string]]
|
||||
[:cancel-label {:optional true} [:maybe :string]]
|
||||
[:on-accept {:optional true} [:maybe [:fn fn?]]]
|
||||
[:on-cancel {:optional true} [:maybe [:fn fn?]]]])
|
||||
|
||||
(mf/defc actionable*
|
||||
{::mf/schema schema:actionable}
|
||||
@@ -45,9 +45,13 @@
|
||||
|
||||
[:> :aside props
|
||||
[:div {:class (stl/css :notification-message)} children]
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-cancel}
|
||||
cancel-label]
|
||||
[:> button* {:variant (if (= variant "default") "primary" "destructive")
|
||||
:on-click on-accept}
|
||||
accept-label]]))
|
||||
|
||||
(when cancel-label
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-cancel}
|
||||
cancel-label])
|
||||
|
||||
(when accept-label
|
||||
[:> button* {:variant (if (= variant "default") "primary" "destructive")
|
||||
:on-click on-accept}
|
||||
accept-label])]))
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ref:notification
|
||||
(def ^:private ref:notification
|
||||
(l/derived :notification st/state))
|
||||
|
||||
(mf/defc current-notification
|
||||
(mf/defc current-notification*
|
||||
[]
|
||||
(let [notification (mf/deref ref:notification)
|
||||
on-close (mf/use-fn #(st/emit! (ntf/hide)))
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
(binding [storage/*sync* true]
|
||||
(when (some? template)
|
||||
(swap! storage/session assoc
|
||||
:template-url template))
|
||||
:template template))
|
||||
(when (some? plugin)
|
||||
(swap! storage/session assoc
|
||||
:plugin-url plugin))))
|
||||
|
||||
@@ -202,9 +202,8 @@
|
||||
cancel-text])
|
||||
[:button {:on-click on-click} button-text]]]]))
|
||||
|
||||
(mf/defc request-access
|
||||
{::mf/props :obj}
|
||||
[{:keys [file-id team-id is-default workspace?]}]
|
||||
(mf/defc request-access*
|
||||
[{:keys [file-id team-id is-default is-workspace]}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
requested* (mf/use-state {:sent false :already-requested false})
|
||||
requested (deref requested*)
|
||||
@@ -227,11 +226,11 @@
|
||||
|
||||
on-request-access
|
||||
(mf/use-fn
|
||||
(mf/deps file-id team-id workspace?)
|
||||
(mf/deps file-id team-id is-workspace)
|
||||
(fn []
|
||||
(let [params (if (some? file-id)
|
||||
{:file-id file-id
|
||||
:is-viewer (not workspace?)}
|
||||
:is-viewer (not is-workspace)}
|
||||
{:team-id team-id})
|
||||
mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
@@ -240,7 +239,7 @@
|
||||
|
||||
[:*
|
||||
(if (some? file-id)
|
||||
(if workspace?
|
||||
(if is-workspace
|
||||
[:div {:class (stl/css :workspace)}
|
||||
[:div {:class (stl/css :workspace-left)}
|
||||
i/logo-icon
|
||||
@@ -341,7 +340,7 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click handle-retry} (tr "labels.retry")]]]))
|
||||
|
||||
(mf/defc service-unavailable
|
||||
(mf/defc service-unavailable*
|
||||
[]
|
||||
(let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))]
|
||||
[:> error-container* {}
|
||||
@@ -350,58 +349,55 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-click} (tr "labels.retry")]]]))
|
||||
|
||||
(defn generate-report
|
||||
(defn- generate-report
|
||||
[data]
|
||||
(try
|
||||
(let [team-id (:current-team-id @st/state)
|
||||
profile-id (:profile-id @st/state)
|
||||
|
||||
trace (:app.main.errors/trace data)
|
||||
instance (:app.main.errors/instance data)
|
||||
content (with-out-str
|
||||
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
|
||||
(println "Prof ID:" (str (or profile-id "--")))
|
||||
(println "Team ID:" (str (or team-id "--")))
|
||||
instance (:app.main.errors/instance data)]
|
||||
(with-out-str
|
||||
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
|
||||
(println "Prof ID:" (str (or profile-id "--")))
|
||||
(println "Team ID:" (str (or team-id "--")))
|
||||
|
||||
(when-let [file-id (:file-id data)]
|
||||
(println "File ID:" (str file-id)))
|
||||
(when-let [file-id (:file-id data)]
|
||||
(println "File ID:" (str file-id)))
|
||||
|
||||
(println)
|
||||
(println)
|
||||
|
||||
(println "Data:")
|
||||
(loop [data data]
|
||||
(-> (d/without-qualified data)
|
||||
(dissoc :explain)
|
||||
(d/update-when :data (constantly "(...)"))
|
||||
(pp/pprint {:level 8 :length 10}))
|
||||
(println "Data:")
|
||||
(loop [data data]
|
||||
(-> (d/without-qualified data)
|
||||
(dissoc :explain)
|
||||
(d/update-when :data (constantly "(...)"))
|
||||
(pp/pprint {:level 8 :length 10}))
|
||||
|
||||
(println)
|
||||
(println)
|
||||
|
||||
(when-let [explain (:explain data)]
|
||||
(print explain))
|
||||
(when-let [explain (:explain data)]
|
||||
(print explain))
|
||||
|
||||
(when (and (= :server-error (:type data))
|
||||
(contains? data :data))
|
||||
(recur (:data data))))
|
||||
(when (and (= :server-error (:type data))
|
||||
(contains? data :data))
|
||||
(recur (:data data))))
|
||||
|
||||
(println "Trace:")
|
||||
(println trace)
|
||||
(println)
|
||||
(println "Trace:")
|
||||
(println trace)
|
||||
(println)
|
||||
|
||||
(println "Last events:")
|
||||
(pp/pprint @st/last-events {:length 200})
|
||||
(println "Last events:")
|
||||
(pp/pprint @st/last-events {:length 200})
|
||||
|
||||
(println))]
|
||||
(wapi/create-blob content "text/plain"))
|
||||
(println)))
|
||||
(catch :default cause
|
||||
(.error js/console "error on generating report.txt" cause)
|
||||
nil)))
|
||||
|
||||
(mf/defc internal-error*
|
||||
{::mf/props :obj}
|
||||
[{:keys [data on-reset] :as props}]
|
||||
[{:keys [on-reset report] :as props}]
|
||||
(let [report-uri (mf/use-ref nil)
|
||||
report (mf/use-memo (mf/deps data) #(generate-report data))
|
||||
on-reset (or on-reset #(st/emit! (rt/assign-exception nil)))
|
||||
|
||||
on-download
|
||||
@@ -413,8 +409,8 @@
|
||||
|
||||
(mf/with-effect [report]
|
||||
(when (some? report)
|
||||
|
||||
(let [uri (wapi/create-uri report)]
|
||||
(let [report (wapi/create-blob report "text/plain")
|
||||
uri (wapi/create-uri report)]
|
||||
(mf/set-ref-val! report-uri uri)
|
||||
(fn []
|
||||
(wapi/revoke-uri uri)))))
|
||||
@@ -455,6 +451,38 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
(let [type (get data :type)
|
||||
report (mf/with-memo [data]
|
||||
(generate-report data))
|
||||
props (mf/spread-props props {:report report})]
|
||||
|
||||
(mf/with-effect [data route report]
|
||||
(let [params (:query-params route)
|
||||
params (u/map->query-string params)]
|
||||
(st/emit! (ptk/data-event ::ev/event
|
||||
{::ev/name "exception-page"
|
||||
:type (get data :type :unknown)
|
||||
:hint (get data :hint)
|
||||
:path (get route :path)
|
||||
:report report
|
||||
:params params}))))
|
||||
(case type
|
||||
:not-found
|
||||
[:> not-found* {}]
|
||||
|
||||
:authentication
|
||||
[:> not-found* {}]
|
||||
|
||||
:bad-gateway
|
||||
[:> bad-gateway* props]
|
||||
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc exception-page*
|
||||
{::mf/props :obj}
|
||||
@@ -477,42 +505,23 @@
|
||||
|
||||
request-access?
|
||||
(and
|
||||
(or (= (:type data) :not-found)
|
||||
(= (:type data) :authentication))
|
||||
(or (= type :not-found)
|
||||
(= type :authentication))
|
||||
(or workspace? dashboard? view?)
|
||||
(or (:file-id info)
|
||||
(:team-id info)))]
|
||||
|
||||
(mf/with-effect [type path params]
|
||||
(st/emit! (ptk/data-event ::ev/event
|
||||
{::ev/name "exception-page"
|
||||
:type type
|
||||
:path path
|
||||
:params (u/map->query-string params)})))
|
||||
|
||||
(mf/with-effect [params info]
|
||||
(when-not (:loaded info)
|
||||
(->> (load-info params)
|
||||
(rx/subs! (partial reset! info*)))))
|
||||
(rx/subs! (partial reset! info*)
|
||||
(partial reset! info* {:loaded true})))))
|
||||
|
||||
(when loaded?
|
||||
(if request-access?
|
||||
[:& request-access {:file-id (:file-id info)
|
||||
:team-id (:team-id info)
|
||||
:is-default (:team-default info)
|
||||
:workspace? workspace?}]
|
||||
[:> request-access* {:file-id (:file-id info)
|
||||
:team-id (:team-id info)
|
||||
:is-default (:team-default info)
|
||||
:is-workspace workspace?}]
|
||||
[:> exception-section* props]))))
|
||||
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:> not-found* {}]
|
||||
|
||||
:authentication
|
||||
[:> not-found* {}]
|
||||
|
||||
:bad-gateway
|
||||
[:> bad-gateway* props]
|
||||
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
|
||||
[:> internal-error* props])))))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.event :as ev]
|
||||
@@ -104,7 +105,7 @@
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
checked? (dom/checked? target)
|
||||
page-id (parse-uuid (dom/get-data target "page-id"))
|
||||
page-id (uuid/parse (dom/get-data target "page-id"))
|
||||
dif-pages? (not= page-id (first (:pages options)))
|
||||
no-one-page (< 1 (count (:pages options)))
|
||||
should-change? (or ^boolean no-one-page
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.plugins :as dpl]
|
||||
[app.main.data.workspace :as dw]
|
||||
@@ -45,9 +46,10 @@
|
||||
(mf/defc workspace-content*
|
||||
{::mf/private true}
|
||||
[{:keys [file layout page wglobal]}]
|
||||
|
||||
(let [palete-size (mf/use-state nil)
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
page-id (:id page)
|
||||
page-id (get page :id)
|
||||
|
||||
{:keys [vport] :as wlocal} (mf/deref refs/workspace-local)
|
||||
{:keys [options-mode]} wglobal
|
||||
@@ -120,10 +122,46 @@
|
||||
:overlay true
|
||||
:file-loading true}])
|
||||
|
||||
(defn- make-team-ref
|
||||
[team-id]
|
||||
(l/derived (fn [state]
|
||||
(let [teams (get state :teams)]
|
||||
(get teams team-id)))
|
||||
st/state))
|
||||
|
||||
(defn- make-file-ref
|
||||
[file-id]
|
||||
(l/derived (fn [state]
|
||||
;; NOTE: for ensure ordering of execution, we need to
|
||||
;; wait the file initialization completly success until
|
||||
;; mark this file availablea and unlock the rendering
|
||||
;; of the following components
|
||||
(when (= (get state :current-file-id) file-id)
|
||||
(let [files (get state :files)
|
||||
file (get files file-id)]
|
||||
(-> file
|
||||
(dissoc :data)
|
||||
(assoc ::has-data (contains? file :data))))))
|
||||
st/state))
|
||||
|
||||
(defn- make-page-ref
|
||||
[file-id page-id]
|
||||
(l/derived (fn [state]
|
||||
(let [current-page-id (get state :current-page-id)]
|
||||
;; NOTE: for ensure ordering of execution, we need to
|
||||
;; wait the page initialization completly success until
|
||||
;; mark this file availablea and unlock the rendering
|
||||
;; of the following components
|
||||
(when (= current-page-id page-id)
|
||||
(dsh/lookup-page state file-id page-id))))
|
||||
st/state))
|
||||
|
||||
(mf/defc workspace-page*
|
||||
{::mf/private true}
|
||||
[{:keys [page-id file-id file layout wglobal]}]
|
||||
(let [page (mf/deref refs/workspace-page)]
|
||||
(let [page-ref (mf/with-memo [file-id page-id]
|
||||
(make-page-ref file-id page-id))
|
||||
page (mf/deref page-ref)]
|
||||
|
||||
(mf/with-effect []
|
||||
(let [focus-out #(st/emit! (dw/workspace-focus-lost))
|
||||
@@ -133,8 +171,7 @@
|
||||
(mf/with-effect [file-id page-id]
|
||||
(st/emit! (dw/initialize-page file-id page-id))
|
||||
(fn []
|
||||
(when page-id
|
||||
(st/emit! (dw/finalize-page file-id page-id)))))
|
||||
(st/emit! (dw/finalize-page file-id page-id))))
|
||||
|
||||
(if (some? page)
|
||||
[:> workspace-content* {:file file
|
||||
@@ -143,18 +180,9 @@
|
||||
:layout layout}]
|
||||
[:> workspace-loader*])))
|
||||
|
||||
(def ^:private ref:file-without-data
|
||||
(l/derived (fn [file]
|
||||
(-> file
|
||||
(dissoc :data)
|
||||
(assoc ::has-data (contains? file :data))))
|
||||
refs/file
|
||||
=))
|
||||
|
||||
(mf/defc workspace*
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [project-id file-id page-id layout-name]}]
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [team-id project-id file-id page-id layout-name]}]
|
||||
|
||||
(let [file-id (hooks/use-equal-memo file-id)
|
||||
page-id (hooks/use-equal-memo page-id)
|
||||
@@ -162,8 +190,15 @@
|
||||
layout (mf/deref refs/workspace-layout)
|
||||
wglobal (mf/deref refs/workspace-global)
|
||||
|
||||
team (mf/deref refs/team)
|
||||
file (mf/deref ref:file-without-data)
|
||||
team-ref (mf/with-memo [team-id]
|
||||
(make-team-ref team-id))
|
||||
file-ref (mf/with-memo [file-id]
|
||||
(make-file-ref file-id))
|
||||
|
||||
team (mf/deref team-ref)
|
||||
file (mf/deref file-ref)
|
||||
|
||||
file-loaded? (get file ::has-data)
|
||||
|
||||
file-name (:name file)
|
||||
permissions (:permissions team)
|
||||
@@ -187,14 +222,14 @@
|
||||
(when file-name
|
||||
(dom/set-html-title (tr "title.workspace" file-name))))
|
||||
|
||||
(mf/with-effect [file-id]
|
||||
(st/emit! (dw/initialize-workspace file-id))
|
||||
(mf/with-effect [team-id file-id]
|
||||
(st/emit! (dw/initialize-workspace team-id file-id))
|
||||
(fn []
|
||||
(st/emit! ::dps/force-persist
|
||||
(dw/finalize-workspace file-id))))
|
||||
(dw/finalize-workspace team-id file-id))))
|
||||
|
||||
(mf/with-effect [file page-id]
|
||||
(when-not page-id
|
||||
(mf/with-effect [file-id page-id file-loaded?]
|
||||
(when (and file-loaded? (not page-id))
|
||||
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
|
||||
|
||||
[:> (mf/provider ctx/current-project-id) {:value project-id}
|
||||
@@ -208,8 +243,7 @@
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
[:> context-menu*]
|
||||
|
||||
(if (::has-data file)
|
||||
(if (and file-loaded? page-id)
|
||||
[:> workspace-page*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
.colorpicker {
|
||||
border-radius: $br-8;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorpicker-tabs {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as mdc]
|
||||
@@ -62,7 +63,7 @@
|
||||
(if (or (= event "recent")
|
||||
(= event "file"))
|
||||
(keyword event)
|
||||
(parse-uuid event)))))
|
||||
(uuid/parse event)))))
|
||||
|
||||
valid-color?
|
||||
(mf/use-fn
|
||||
@@ -124,6 +125,7 @@
|
||||
[:div {:class (stl/css :select-wrapper)}
|
||||
[:& select
|
||||
{:class (stl/css :shadow-type-select)
|
||||
:data-direction "up"
|
||||
:default-value (or (d/name selected) "recent")
|
||||
:options options
|
||||
:on-change on-library-change}]]
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
(fn [event]
|
||||
(let [library-id (some-> (dom/get-current-target event)
|
||||
(dom/get-data "library-id")
|
||||
(parse-uuid))]
|
||||
(uuid/parse))]
|
||||
(reset! selected library-id)
|
||||
(st/emit! (dwl/link-file-to-library file-id library-id)))))
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
(fn [event]
|
||||
(let [library-id (some-> (dom/get-current-target event)
|
||||
(dom/get-data "library-id")
|
||||
(parse-uuid))]
|
||||
(uuid/parse))]
|
||||
(when (= library-id @selected)
|
||||
(reset! selected :file))
|
||||
(st/emit! (dwl/unlink-file-from-library file-id library-id)
|
||||
@@ -451,7 +451,7 @@
|
||||
(when-not updating?
|
||||
(let [library-id (some-> (dom/get-target event)
|
||||
(dom/get-data "library-id")
|
||||
(parse-uuid))]
|
||||
(uuid/parse))]
|
||||
(st/emit!
|
||||
(dwl/set-updating-library true)
|
||||
(dwl/sync-file file-id library-id))))))]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as mdc]
|
||||
@@ -87,7 +88,7 @@
|
||||
value (dom/get-attribute node "data-palette")]
|
||||
(on-select (if (or (= "file" value) (= "recent" value))
|
||||
(keyword value)
|
||||
(parse-uuid value))))))
|
||||
(uuid/parse value))))))
|
||||
|
||||
on-select-text-palette-menu
|
||||
(mf/use-fn
|
||||
|
||||
@@ -251,14 +251,14 @@
|
||||
(mf/deps index update-interaction)
|
||||
(fn [event]
|
||||
(let [value event
|
||||
value (when (not= value "") (uuid/uuid value))]
|
||||
value (when (not= value "") (uuid/parse value))]
|
||||
(update-interaction index #(ctsi/set-destination % value)))))
|
||||
|
||||
change-position-relative-to
|
||||
(mf/use-fn
|
||||
(mf/deps index update-interaction)
|
||||
(fn [event]
|
||||
(let [value (uuid/uuid event)]
|
||||
(let [value (uuid/parse event)]
|
||||
(update-interaction index #(ctsi/set-position-relative-to % value)))))
|
||||
|
||||
change-preserve-scroll
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
.custom-select-dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
margin-top: $s-2;
|
||||
max-height: 70vh;
|
||||
width: $s-252;
|
||||
.dropdown-element {
|
||||
@extend .dropdown-element-base;
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
(mf/deps on-pin-snapshot)
|
||||
(fn [event]
|
||||
(let [node (dom/get-current-target event)
|
||||
id (-> (dom/get-data node "id") uuid/uuid)]
|
||||
id (-> (dom/get-data node "id") uuid/parse)]
|
||||
(when on-pin-snapshot (on-pin-snapshot id)))))
|
||||
|
||||
handle-restore-snapshot
|
||||
@@ -161,7 +161,7 @@
|
||||
(mf/deps on-restore-snapshot)
|
||||
(fn [event]
|
||||
(let [node (dom/get-current-target event)
|
||||
id (-> (dom/get-data node "id") uuid/uuid)]
|
||||
id (-> (dom/get-data node "id") uuid/parse)]
|
||||
(when on-restore-snapshot (on-restore-snapshot id)))))
|
||||
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
border: $s-1 solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
|
||||
border-radius: $s-8;
|
||||
overflow-y: auto;
|
||||
max-height: $s-452;
|
||||
}
|
||||
|
||||
.sets-count-empty-button {
|
||||
|
||||
@@ -65,6 +65,11 @@
|
||||
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
|
||||
(dt/create-token-set name))))
|
||||
|
||||
(defn group-edition-id
|
||||
"Prefix editing groups `edition-id` so it can be differentiated from sets with the same id."
|
||||
[edition-id]
|
||||
(str "group-" edition-id))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COMPONENTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -166,16 +171,18 @@
|
||||
{:position (dom/get-client-position event)
|
||||
:is-group true
|
||||
:id id
|
||||
:edition-id (group-edition-id id)
|
||||
:path tree-path})))))
|
||||
|
||||
on-collapse-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-toggle-collapse tree-path)))
|
||||
|
||||
on-double-click
|
||||
(mf/use-fn (mf/deps id) #(on-start-edition id))
|
||||
(mf/use-fn (mf/deps id) #(on-start-edition (group-edition-id id)))
|
||||
|
||||
on-checkbox-click
|
||||
(mf/use-fn
|
||||
@@ -267,6 +274,7 @@
|
||||
{:position (dom/get-client-position event)
|
||||
:is-group false
|
||||
:id id
|
||||
:edition-id id
|
||||
:path tree-path})))))
|
||||
|
||||
on-double-click
|
||||
@@ -398,7 +406,7 @@
|
||||
:is-active (is-token-set-group-active path)
|
||||
:is-selected false
|
||||
:is-draggable is-draggable
|
||||
:is-editing (= edition-id id)
|
||||
:is-editing (= edition-id (group-edition-id id))
|
||||
:is-collapsed (collapsed? path)
|
||||
:on-select on-select
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
(mf/defc menu*
|
||||
{::mf/private true}
|
||||
[{:keys [is-group id path]}]
|
||||
[{:keys [is-group id edition-id path]}]
|
||||
(let [create-set-at-path
|
||||
(mf/use-fn (mf/deps path) #(st/emit! (dt/start-token-set-creation path)))
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn []
|
||||
(st/emit! (dt/start-token-set-edition id))))
|
||||
(st/emit! (dt/start-token-set-edition edition-id))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
(mf/defc token-set-context-menu*
|
||||
[]
|
||||
(let [{:keys [position is-group id path]}
|
||||
(let [{:keys [position is-group id edition-id path]}
|
||||
(mf/deref ref:token-sets-context-menu)
|
||||
|
||||
position-top
|
||||
@@ -78,4 +78,5 @@
|
||||
:on-context-menu prevent-default}
|
||||
[:> menu* {:is-group is-group
|
||||
:id id
|
||||
:edition-id edition-id
|
||||
:path path}]]]))
|
||||
|
||||
@@ -468,10 +468,7 @@
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [point (gpt/point (.-clientX event) (.-clientY event))
|
||||
viewport-coord (uwvv/point->viewport point)
|
||||
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
|
||||
asset-name (dnd/get-data event "text/asset-name")
|
||||
asset-type (dnd/get-data event "text/asset-type")]
|
||||
viewport-coord (uwvv/point->viewport point)]
|
||||
(cond
|
||||
(dnd/has-type? event "penpot/shape")
|
||||
(let [shape (dnd/get-data event "penpot/shape")
|
||||
@@ -516,25 +513,6 @@
|
||||
(assoc params :blobs (map wapi/data-uri->blob data)))]
|
||||
(st/emit! (dwm/upload-media-workspace params)))
|
||||
|
||||
;; Will trigger when the user drags an SVG asset from the assets panel
|
||||
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
|
||||
(let [path (cfg/resolve-file-media {:id asset-id})
|
||||
params {:file-id (:id file)
|
||||
:position viewport-coord
|
||||
:uris [path]
|
||||
:name asset-name
|
||||
:mtype asset-type}]
|
||||
(st/emit! (dwm/upload-media-workspace params)))
|
||||
|
||||
;; Will trigger when the user drags an image from the assets SVG
|
||||
(dnd/has-type? event "text/asset-id")
|
||||
(let [params {:file-id (:id file)
|
||||
:object-id asset-id
|
||||
:name asset-name}]
|
||||
(st/emit! (dwm/clone-media-object
|
||||
(with-meta params
|
||||
{:on-success #(st/emit! (dwm/image-uploaded % viewport-coord))}))))
|
||||
|
||||
;; Will trigger when the user drags a file from their file explorer into the viewport
|
||||
;; Or the user pastes an image
|
||||
;; Or the user uploads an image using the image tool
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
[]
|
||||
(let [worker (uw/init cf/worker-uri err/on-error)]
|
||||
(uw/ask! worker {:cmd :configure
|
||||
:key :public-uri
|
||||
:val cf/public-uri})
|
||||
:config {:public-uri cf/public-uri
|
||||
:build-data cf/build-date
|
||||
:version cf/version}})
|
||||
|
||||
(set! instance worker)))
|
||||
|
||||
(defn ask!
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.main.data.exports.files :as exports.files]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.versions :as dwv]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.worker :as uw]
|
||||
@@ -237,7 +236,7 @@
|
||||
|
||||
:else
|
||||
(let [file (u/locate-file id)
|
||||
features (features/get-team-enabled-features @st/state)
|
||||
features (:features @st/state)
|
||||
team-id (:current-team-id @st/state)
|
||||
format (case format
|
||||
"zip" :legacy-zip
|
||||
|
||||
@@ -969,7 +969,7 @@
|
||||
|
||||
:else
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
library-id (uuid/uuid library-id)]
|
||||
library-id (uuid/parse library-id)]
|
||||
(->> st/stream
|
||||
(rx/filter (ptk/type? ::dwl/attach-library-finished))
|
||||
(rx/take 1)
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
(u/display-not-valid :getShapeById shape-id)
|
||||
|
||||
:else
|
||||
(let [shape-id (uuid/uuid shape-id)
|
||||
(let [shape-id (uuid/parse shape-id)
|
||||
shape (u/locate-shape file-id id shape-id)]
|
||||
(when (some? shape)
|
||||
(shape/shape-proxy plugin-id file-id id shape-id)))))
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
(defn parse-id
|
||||
[id]
|
||||
(when id (uuid/uuid id)))
|
||||
(when id (uuid/parse id)))
|
||||
|
||||
(defn parse-keyword
|
||||
[kw]
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (features/initialize (or features #{}))))))
|
||||
(rx/of (features/initialize features)))))
|
||||
|
||||
(defn- fetch-team
|
||||
[& {:keys [file-id]}]
|
||||
@@ -98,7 +98,7 @@
|
||||
(ptk/reify ::fetch-objects-bundle
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)]
|
||||
(let [features (get state :features)]
|
||||
(->> (rx/zip
|
||||
(repo/cmd! :get-font-variants {:file-id file-id :share-id share-id})
|
||||
(repo/cmd! :get-page {:file-id file-id
|
||||
@@ -237,7 +237,7 @@
|
||||
(ptk/reify ::fetch-components-bundle
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)]
|
||||
(let [features (get state :features)]
|
||||
(->> (repo/cmd! :get-file {:id file-id :features features})
|
||||
(rx/map (fn [file] #(assoc % :file file))))))))
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
|
||||
(defn ^:export init
|
||||
[]
|
||||
(st/emit! (features/initialize))
|
||||
(init-ui))
|
||||
|
||||
(defn reinit
|
||||
|
||||
@@ -104,7 +104,9 @@
|
||||
|
||||
(defn send!
|
||||
[ws msg]
|
||||
(-send ws (t/encode-str msg)))
|
||||
(if *assert*
|
||||
(-send ws (t/encode-str msg {:type :json-verbose}))
|
||||
(-send ws (t/encode-str msg))))
|
||||
|
||||
(defn close!
|
||||
[ws]
|
||||
|
||||
@@ -135,15 +135,15 @@
|
||||
(rx/debounce 1)
|
||||
|
||||
(rx/subs! (fn [[messages dropped last]]
|
||||
;; Send back the dropped messages replies
|
||||
;; Send back the dropped messages replies
|
||||
(doseq [msg dropped]
|
||||
(drop-message msg))
|
||||
|
||||
;; Process the message
|
||||
;; Process the message
|
||||
(doseq [msg (vals messages)]
|
||||
(handle-message msg))
|
||||
|
||||
;; After process the buffer we send a clear
|
||||
;; After process the buffer we send a clear
|
||||
(when-not (= last ::clear)
|
||||
(rx/push! buffer ::clear)))))))
|
||||
|
||||
|
||||
@@ -50,8 +50,16 @@
|
||||
(handler (assoc message :cmd :snaps/update-page-index))))
|
||||
|
||||
(defmethod handler :configure
|
||||
[{:keys [key val]}]
|
||||
(log/info :hint "configure worker" :key key :val (dm/str val))
|
||||
(case key
|
||||
:public-uri
|
||||
(set! cf/public-uri val)))
|
||||
[{:keys [config]}]
|
||||
(log/info :hint "configure worker" :keys (keys config))
|
||||
|
||||
(when-let [public-uri (get config :public-uri)]
|
||||
(set! cf/public-uri public-uri))
|
||||
|
||||
(when-let [version (get config :version)]
|
||||
(set! cf/version version))
|
||||
|
||||
(when-let [build-date (get config :build-data)]
|
||||
(set! cf/build-date build-date))
|
||||
|
||||
nil)
|
||||
|
||||
@@ -43,14 +43,14 @@
|
||||
|
||||
(defn read-json-key
|
||||
[m]
|
||||
(or (sm/parse-uuid m)
|
||||
(or (uuid/parse m)
|
||||
(json/read-kebab-key m)))
|
||||
|
||||
(defn read-json-val
|
||||
[m]
|
||||
(cond
|
||||
(and (string? m)
|
||||
(re-matches sm/uuid-rx m))
|
||||
(re-matches uuid/regex m))
|
||||
(uuid/uuid m)
|
||||
|
||||
(and (string? m)
|
||||
@@ -521,8 +521,8 @@
|
||||
id (resolve old-id)
|
||||
path (get-in node [:attrs :penpot:path] "")
|
||||
type (parser/get-type content)
|
||||
main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
|
||||
main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
|
||||
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
|
||||
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
|
||||
data (-> (parser/parse-data type content)
|
||||
(assoc :path path)
|
||||
(assoc :id id)
|
||||
@@ -547,12 +547,12 @@
|
||||
old-id (parser/get-id node)
|
||||
id (resolve old-id)
|
||||
path (get-in node [:attrs :penpot:path] "")
|
||||
main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
|
||||
main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
|
||||
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
|
||||
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
|
||||
main-instance-x (-> (get-in node [:attrs :penpot:main-instance-x] "") (d/parse-double))
|
||||
main-instance-y (-> (get-in node [:attrs :penpot:main-instance-y] "") (d/parse-double))
|
||||
main-instance-parent (resolve (uuid (get-in node [:attrs :penpot:main-instance-parent] "")))
|
||||
main-instance-frame (resolve (uuid (get-in node [:attrs :penpot:main-instance-frame] "")))
|
||||
main-instance-parent (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-parent] "")))
|
||||
main-instance-frame (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-frame] "")))
|
||||
type (parser/get-type content)
|
||||
|
||||
data (-> (parser/parse-data type content)
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
(def url-regex
|
||||
#"url\(#([^\)]*)\)")
|
||||
|
||||
(def uuid-regex
|
||||
#"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}")
|
||||
|
||||
(def uuid-regex-prefix
|
||||
#"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}-")
|
||||
|
||||
@@ -84,7 +81,7 @@
|
||||
(defn get-id
|
||||
[node]
|
||||
(let [attr-id (get-in node [:attrs :id])
|
||||
id (when (string? attr-id) (re-find uuid-regex attr-id))]
|
||||
id (when (string? attr-id) (re-find uuid/regex attr-id))]
|
||||
(when (some? id)
|
||||
(uuid/uuid id))))
|
||||
|
||||
@@ -189,10 +186,10 @@
|
||||
[m]
|
||||
(letfn [(convert [value]
|
||||
(cond
|
||||
(and (string? value) (re-matches uuid-regex value))
|
||||
(and (string? value) (re-matches uuid/regex value))
|
||||
(uuid/uuid value)
|
||||
|
||||
(and (keyword? value) (re-matches uuid-regex (d/name value)))
|
||||
(and (keyword? value) (re-matches uuid/regex (d/name value)))
|
||||
(uuid/uuid (d/name value))
|
||||
|
||||
(vector? value)
|
||||
@@ -429,11 +426,11 @@
|
||||
(defn add-library-refs
|
||||
[props node]
|
||||
|
||||
(let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid)
|
||||
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid)
|
||||
component-id (get-meta node :component-id uuid/uuid)
|
||||
component-file (get-meta node :component-file uuid/uuid)
|
||||
shape-ref (get-meta node :shape-ref uuid/uuid)
|
||||
(let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/parse)
|
||||
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/parse)
|
||||
component-id (get-meta node :component-id uuid/parse)
|
||||
component-file (get-meta node :component-file uuid/parse)
|
||||
shape-ref (get-meta node :shape-ref uuid/parse)
|
||||
component-root? (get-meta node :component-root str->bool)
|
||||
main-instance? (get-meta node :main-instance str->bool)
|
||||
touched (get-meta node :touched parse-touched)]
|
||||
@@ -463,8 +460,8 @@
|
||||
[props node svg-data]
|
||||
|
||||
(let [fill (:fill svg-data)
|
||||
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
|
||||
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
|
||||
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/parse)
|
||||
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/parse)
|
||||
meta-fill-color (get-meta node :fill-color)
|
||||
meta-fill-opacity (get-meta node :fill-opacity)
|
||||
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient")
|
||||
@@ -627,7 +624,7 @@
|
||||
(let [attrs (-> node :attrs remove-penpot-prefix)]
|
||||
{:id (uuid/next)
|
||||
:name (-> attrs :name)
|
||||
:starting-frame (-> attrs :starting-frame uuid)}))
|
||||
:starting-frame (-> attrs :starting-frame uuid/parse)}))
|
||||
|
||||
(defn parse-flows [node]
|
||||
(let [flows-node (get-data node :penpot:flows)]
|
||||
@@ -638,7 +635,7 @@
|
||||
id (uuid/next)]
|
||||
[id
|
||||
{:id id
|
||||
:frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid))
|
||||
:frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid/parse))
|
||||
:axis (-> attrs :axis keyword)
|
||||
:position (-> attrs :position d/parse-double)}]))
|
||||
|
||||
@@ -775,8 +772,8 @@
|
||||
(parse-gradient node (get-meta fill-node :fill-color)))
|
||||
:fill-image (when fill-image-id
|
||||
(get images fill-image-id))
|
||||
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
|
||||
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
|
||||
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/parse)
|
||||
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/parse)
|
||||
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})))
|
||||
(mapv d/without-nils)
|
||||
(filterv #(not= (:fill-color %) "none")))]
|
||||
@@ -800,8 +797,8 @@
|
||||
(parse-gradient node (get-meta stroke-node :stroke-color)))
|
||||
:stroke-image (when stroke-image-id
|
||||
(get images stroke-image-id))
|
||||
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
|
||||
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
|
||||
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/parse)
|
||||
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/parse)
|
||||
:stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
|
||||
:stroke-style (get-meta stroke-node :stroke-style keyword)
|
||||
:stroke-width (get-meta stroke-node :stroke-width d/parse-double)
|
||||
@@ -993,7 +990,7 @@
|
||||
align-self
|
||||
justify-self
|
||||
shapes]} (-> cell-node :attrs remove-penpot-prefix)
|
||||
id (uuid/uuid id)]
|
||||
id (uuid/parse id)]
|
||||
[id (d/without-nils
|
||||
{:id id
|
||||
:area-name area-name
|
||||
@@ -1006,7 +1003,7 @@
|
||||
:justify-self (keyword justify-self)
|
||||
:shapes (if (and (some? shapes) (d/not-empty? shapes))
|
||||
(->> (str/split shapes " ")
|
||||
(mapv uuid/uuid))
|
||||
(mapv uuid/parse))
|
||||
[])})])))
|
||||
(into {}))))
|
||||
|
||||
@@ -1154,7 +1151,7 @@
|
||||
(assoc :delay (get-meta node :delay d/parse-double))
|
||||
|
||||
(ctsi/has-destination interaction)
|
||||
(assoc :destination (get-meta node :destination uuid/uuid)
|
||||
(assoc :destination (get-meta node :destination uuid/parse)
|
||||
:preserve-scroll (get-meta node :preserve-scroll str->bool))
|
||||
|
||||
(ctsi/has-url interaction)
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.shortcuts]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.debug :as dbg]
|
||||
@@ -180,7 +179,7 @@
|
||||
[state name]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
result (or (d/seek (fn [shape] (= name (:name shape))) (vals objects))
|
||||
(get objects (uuid/uuid name)))]
|
||||
(get objects (uuid/parse name)))]
|
||||
result))
|
||||
|
||||
(defn ^:export dump-object
|
||||
@@ -223,12 +222,12 @@
|
||||
(defn ^:export select-by-object-id
|
||||
[object-id]
|
||||
(let [[_ page-id shape-id _] (str/split object-id #"/")]
|
||||
(st/emit! (dcm/go-to-workspace :page-id (uuid/uuid page-id)))
|
||||
(st/emit! (dws/select-shape (uuid/uuid shape-id)))))
|
||||
(st/emit! (dcm/go-to-workspace :page-id (uuid/parse page-id)))
|
||||
(st/emit! (dws/select-shape (uuid/parse shape-id)))))
|
||||
|
||||
(defn ^:export select-by-id
|
||||
[shape-id]
|
||||
(st/emit! (dws/select-shape (uuid/uuid shape-id))))
|
||||
(st/emit! (dws/select-shape (uuid/parse shape-id))))
|
||||
|
||||
(defn dump-tree'
|
||||
([state] (dump-tree' state false false false))
|
||||
@@ -256,7 +255,7 @@
|
||||
file (dsh/lookup-file state)
|
||||
libraries (get state :files)
|
||||
shape-id (if (some? shape-id)
|
||||
(uuid/uuid shape-id)
|
||||
(uuid/parse shape-id)
|
||||
(first (dsh/lookup-selected state)))]
|
||||
(if (some? shape-id)
|
||||
(ctf/dump-subtree file page-id shape-id libraries {:show-ids show-ids
|
||||
@@ -370,7 +369,7 @@
|
||||
(let [file (dsh/lookup-file @st/state)
|
||||
libraries (get @st/state :files)]
|
||||
(try
|
||||
(->> (if-let [shape-id (some-> shape-id parse-uuid)]
|
||||
(->> (if-let [shape-id (some-> shape-id uuid/parse)]
|
||||
(let [page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)])]
|
||||
(cfv/validate-shape shape-id file page libraries))
|
||||
(cfv/validate-file file libraries))
|
||||
@@ -393,7 +392,7 @@
|
||||
(ptk/reify ::repair-current-file
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
(let [features (:features state)
|
||||
sid (:session-id state)
|
||||
|
||||
file (dsh/lookup-file state)
|
||||
@@ -427,10 +426,15 @@
|
||||
[]
|
||||
(st/emit! (dw/find-components-norefs)))
|
||||
|
||||
(defn- set-shape-ref*
|
||||
[id shape-ref]
|
||||
(ptk/reify ::set-shape-ref
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [shape-id (uuid/parse id)
|
||||
shape-ref (uuid/parse shape-ref)]
|
||||
(rx/of (dw/update-shape shape-id {:shape-ref shape-ref}))))))
|
||||
|
||||
(defn ^:export set-shape-ref
|
||||
[id shape-ref]
|
||||
(st/emit! (dw/set-shape-ref id shape-ref)))
|
||||
|
||||
(defn ^:export enable-text-v2
|
||||
[]
|
||||
(st/emit! (features/enable-feature "text-editor/v2")))
|
||||
(st/emit! (set-shape-ref* id shape-ref)))
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
nil)
|
||||
|
||||
(defn ^:export get-enabled []
|
||||
(clj->js (features/get-enabled-features @st/state)))
|
||||
(clj->js features/global-enabled-features))
|
||||
|
||||
(defn ^:export get-team-enabled []
|
||||
(clj->js (features/get-team-enabled-features @st/state)))
|
||||
(clj->js (get @st/state :features)))
|
||||
|
||||
(defn ^:export plugins []
|
||||
(st/emit! (features/enable-feature "plugins/runtime"))
|
||||
(plugins/init-plugins-runtime!)
|
||||
nil)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.2.2",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
Reference in New Issue
Block a user