diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1a4824038c..ccb1ec5f4b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -44,7 +44,6 @@ jobs:
- ~/.m2
key: v1-dependencies-{{ checksum "common/deps.edn"}}
-
test-frontend:
docker:
- image: penpotapp/devenv:latest
@@ -93,7 +92,6 @@ jobs:
- ~/.m2
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}
-
test-integration:
docker:
- image: penpotapp/devenv:latest
@@ -180,7 +178,6 @@ jobs:
- ~/.m2
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
-
test-exporter:
docker:
- image: penpotapp/devenv:latest
@@ -210,6 +207,29 @@ jobs:
yarn run fmt:clj:check
yarn run lint:clj
+ test-render-wasm:
+ docker:
+ - image: penpotapp/devenv:latest
+
+ working_directory: ~/repo
+ resource_class: medium+
+ environment:
+
+ steps:
+ - checkout
+
+ - run:
+ name: "fmt check"
+ working_directory: "./render-wasm"
+ command: |
+ cargo fmt --check
+
+ - run:
+ name: "cargo tests"
+ working_directory: "./render-wasm"
+ command: |
+ ./test
+
workflows:
penpot:
jobs:
@@ -218,3 +238,4 @@ workflows:
- test-backend
- test-common
- test-exporter
+ - test-render-wasm
diff --git a/CHANGES.md b/CHANGES.md
index 8a7df1fc18..2f0dbac7d3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,12 +38,14 @@
### :sparkles: New features
-- Viewer role for team members [Taiga #1056 & #6590](https://tree.taiga.io/project/penpot/us/1056 & https://tree.taiga.io/project/penpot/us/6590)
-- File history versions management [Taiga](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
+- Viewer role for team members [Taiga #1056](https://tree.taiga.io/project/penpot/us/1056) & [Taiga #6590](https://tree.taiga.io/project/penpot/us/6590)
+- File history versions management [Taiga #187](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
- Rename selected layer via keyboard shortcut and context menu option [Taiga #8882](https://tree.taiga.io/project/penpot/us/8882)
+- New .penpot file format [Taiga #8657](https://tree.taiga.io/project/penpot/us/8657)
### :bug: Bugs fixed
+- Fix problem with some texts desynchronization [Taiga #9379](https://tree.taiga.io/project/penpot/issue/9379)
## 2.3.3
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index 4824a5d782..6e9f31b313 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -42,7 +42,6 @@
:rpc-rlimit-config "resources/rlimit.edn"
:rpc-climit-config "resources/climit.edn"
- :auto-file-snapshot-total 10
:auto-file-snapshot-every 5
:auto-file-snapshot-timeout "3h"
@@ -101,7 +100,6 @@
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
- [:auto-file-snapshot-total {:optional true} ::sm/int]
[:auto-file-snapshot-every {:optional true} ::sm/int]
[:auto-file-snapshot-timeout {:optional true} ::dt/duration]
diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj
index a8ee40c4d2..5bcf741f1c 100644
--- a/backend/src/app/email.clj
+++ b/backend/src/app/email.clj
@@ -226,8 +226,8 @@
[:priority {:optional true} [:enum :high :low]]
[:extra-data {:optional true} ::sm/text]])
-(def ^:private valid-context?
- (sm/validator schema:context))
+(def ^:private check-context
+ (sm/check-fn schema:context))
(defn template-factory
[& {:keys [id schema]}]
@@ -236,10 +236,8 @@
(sm/check-fn schema)
(constantly nil))]
(fn [context]
- (assert (valid-context? context) "expected a valid context")
- (check-fn context)
-
- (let [email (build-email-template id context)]
+ (let [context (-> context check-context check-fn)
+ email (build-email-template id context)]
(when-not email
(ex/raise :type :internal
:code :email-template-does-not-exists
@@ -271,7 +269,7 @@
"Schedule an already defined email to be sent using asynchronously
using worker task."
[{:keys [::conn ::factory] :as context}]
- (assert (db/connection? conn) "expected a valid database connection")
+ (assert (db/connectable? conn) "expected a valid database connection or pool")
(let [email (if factory
(factory context)
@@ -348,7 +346,7 @@
[:subject ::sm/text]
[:content ::sm/text]])
-(def feedback
+(def user-feedback
"A profile feedback email."
(template-factory
:id ::feedback
diff --git a/backend/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj
index 7e29e7bc71..eec9b322d7 100644
--- a/backend/src/app/features/components_v2.clj
+++ b/backend/src/app/features/components_v2.clj
@@ -884,8 +884,10 @@
:shapes (or (:shapes shape) [])
:hide-in-viewer (if frame? (boolean (:hide-in-viewer shape)) true)
:show-content (if frame? (boolean (:show-content shape)) true)
- :rx (or (:rx shape) 0)
- :ry (or (:ry shape) 0)))
+ :r1 (or (:r1 shape) 0)
+ :r2 (or (:r2 shape) 0)
+ :r3 (or (:r3 shape) 0)
+ :r4 (or (:r4 shape) 0)))
shape))]
(-> file-data
(update :pages-index update-vals fix-container)
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index 1ad2fcc4c8..b971eafddf 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -349,7 +349,6 @@
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-gc-scheduler (ig/ref :app.tasks.file-gc-scheduler/handler)
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
- :file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
@@ -405,10 +404,6 @@
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
- :app.tasks.file-xlog-gc/handler
- {::db/pool (ig/ref ::db/pool)
- ::sto/storage (ig/ref ::sto/storage)}
-
:app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj
index c641a4ff41..e3525ded47 100644
--- a/backend/src/app/rpc/commands/feedback.clj
+++ b/backend/src/app/rpc/commands/feedback.clj
@@ -17,7 +17,7 @@
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
-(declare ^:private send-feedback!)
+(declare ^:private send-user-feedback!)
(def ^:private schema:send-user-feedback
[:map {:title "send-user-feedback"}
@@ -34,14 +34,16 @@
:hint "feedback not enabled"))
(let [profile (profile/get-profile pool profile-id)]
- (send-feedback! pool profile params)
+ (send-user-feedback! pool profile params)
nil))
-(defn- send-feedback!
+(defn- send-user-feedback!
[pool profile params]
- (let [dest (cf/get :feedback-destination)]
+ (let [dest (or (cf/get :user-feedback-destination)
+ ;; LEGACY
+ (cf/get :feedback-destination))]
(eml/send! {::eml/conn pool
- ::eml/factory eml/feedback
+ ::eml/factory eml/user-feedback
:from dest
:to dest
:profile profile
diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj
index 3b746f8abc..a6c74b8100 100644
--- a/backend/src/app/rpc/commands/files.clj
+++ b/backend/src/app/rpc/commands/files.clj
@@ -575,7 +575,7 @@
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
- (assoc :thumbnail-uri (resolve-public-uri media-id)))
+ (assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))
@@ -698,11 +698,7 @@
(defn get-team-recent-files
[conn team-id]
- (->> (db/exec! conn [sql:team-recent-files team-id])
- (mapv (fn [row]
- (if-let [media-id (:thumbnail-id row)]
- (assoc row :thumbnail-uri (resolve-public-uri media-id))
- (dissoc row :media-id))))))
+ (db/exec! conn [sql:team-recent-files team-id]))
(def ^:private schema:get-team-recent-files
[:map {:title "get-team-recent-files"}
diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj
index f470e51350..43e3f1c95e 100644
--- a/backend/src/app/rpc/commands/files_snapshot.clj
+++ b/backend/src/app/rpc/commands/files_snapshot.clj
@@ -28,13 +28,19 @@
[cuerdas.core :as str]))
(def sql:get-file-snapshots
- "SELECT id, label, revn, created_at, created_by, profile_id
- FROM file_change
- WHERE file_id = ?
- AND data IS NOT NULL
- AND (deleted_at IS NULL OR deleted_at > now())
- ORDER BY created_at DESC
- LIMIT 20")
+ "WITH changes AS (
+ SELECT id, label, revn, created_at, created_by, profile_id
+ FROM file_change
+ WHERE file_id = ?
+ AND data IS NOT NULL
+ AND (deleted_at IS NULL OR deleted_at > now())
+ ), versions AS (
+ (SELECT * FROM changes WHERE created_by = 'system' LIMIT 1000)
+ UNION ALL
+ (SELECT * FROM changes WHERE created_by != 'system' LIMIT 1000)
+ )
+ SELECT * FROM versions
+ ORDER BY created_at DESC;")
(defn get-file-snapshots
[conn file-id]
diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj
index 92c8d16b08..eb7bf3c169 100644
--- a/backend/src/app/rpc/commands/files_thumbnails.clj
+++ b/backend/src/app/rpc/commands/files_thumbnails.clj
@@ -50,8 +50,7 @@
" where file_id=? and tag=? and deleted_at is null")
res (db/exec! conn [sql file-id tag])]
(->> res
- (d/index-by :object-id (fn [row]
- (files/resolve-public-uri (:media-id row))))
+ (d/index-by :object-id :media-id)
(d/without-nils))))
(defn- get-object-thumbnails
@@ -62,8 +61,7 @@
" where file_id=? and deleted_at is null")
res (db/exec! conn [sql file-id])]
(->> res
- (d/index-by :object-id (fn [row]
- (files/resolve-public-uri (:media-id row))))
+ (d/index-by :object-id :media-id)
(d/without-nils))))
([conn file-id object-ids]
@@ -75,8 +73,7 @@
res (db/exec! conn [sql file-id ids])]
(->> res
- (d/index-by :object-id (fn [row]
- (files/resolve-public-uri (:media-id row))))
+ (d/index-by :object-id :media-id)
(d/without-nils)))))
(sv/defmethod ::get-file-object-thumbnails
@@ -127,8 +124,11 @@
(if-let [frame (-> frames first)]
(let [frame-id (:id frame)
object-id (thc/fmt-object-id (:id file) page-id frame-id "frame")
- frame (if-let [thumb (get thumbnails object-id)]
- (assoc frame :thumbnail thumb :shapes [])
+
+ frame (if-let [media-id (get thumbnails object-id)]
+ (-> frame
+ (assoc :thumbnail-id media-id)
+ (assoc :shapes []))
(dissoc frame :thumbnail))
children-ids
diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj
index 89f75b5806..a4bdbbe209 100644
--- a/backend/src/app/rpc/commands/files_update.clj
+++ b/backend/src/app/rpc/commands/files_update.clj
@@ -223,15 +223,6 @@
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
(some->> (:data-ref-id file) (sto/touch-object! storage))))
- (-> cfg
- (assoc ::wrk/task :file-xlog-gc)
- (assoc ::wrk/label (str "xlog:" (:id file)))
- (assoc ::wrk/params {:file-id (:id file)})
- (assoc ::wrk/delay (dt/duration "5m"))
- (assoc ::wrk/dedupe true)
- (assoc ::wrk/priority 1)
- (wrk/submit!))
-
(persist-file! cfg file)
(let [params (assoc params :file file)
diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj
index 69265c27fd..f4913edb25 100644
--- a/backend/src/app/rpc/commands/media.clj
+++ b/backend/src/app/rpc/commands/media.clj
@@ -60,15 +60,25 @@
(media/validate-media-type! content)
(media/validate-media-size! content)
- (db/run! cfg (fn [cfg]
- (let [object (create-file-media-object cfg params)
- props {:name (:name params)
- :file-id file-id
- :is-local (:is-local params)
- :size (:size content)
- :mtype (:mtype content)}]
- (with-meta object
- {::audit/replace-props props})))))
+ (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
+ ;; We get the minimal file for proper checking if
+ ;; file is not already deleted
+ (let [_ (files/get-minimal-file conn file-id)
+ mobj (create-file-media-object cfg params)]
+
+ (db/update! conn :file
+ {:modified-at (dt/now)
+ :has-media-trimmed false}
+ {:id file-id}
+ {::db/return-keys false})
+
+ (with-meta mobj
+ {::audit/replace-props
+ {:name (:name params)
+ :file-id file-id
+ :is-local (:is-local params)
+ :size (:size content)
+ :mtype (:mtype content)}})))))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@@ -142,20 +152,14 @@
:always
(assoc ::image (process-main-image info)))))
-(defn create-file-media-object
- [{:keys [::sto/storage ::db/conn ::wrk/executor]}
+(defn- create-file-media-object
+ [{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
{:keys [id file-id is-local name content]}]
-
(let [result (px/invoke! executor (partial process-image content))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
- (db/update! conn :file
- {:modified-at (dt/now)
- :has-media-trimmed false}
- {:id file-id})
-
(db/exec-one! conn [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
@@ -182,7 +186,18 @@
::sm/params schema:create-file-media-object-from-url}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-edition-permissions! pool profile-id file-id)
- (create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))
+ ;; We get the minimal file for proper checking if file is not
+ ;; already deleted
+ (let [_ (files/get-minimal-file cfg file-id)
+ mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))]
+
+ (db/update! pool :file
+ {:modified-at (dt/now)
+ :has-media-trimmed false}
+ {:id file-id}
+ {::db/return-keys false})
+
+ mobj))
(defn download-image
[{:keys [::http/client]} uri]
diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj
index 57034c4613..7c7ca33399 100644
--- a/backend/src/app/rpc/commands/profile.clj
+++ b/backend/src/app/rpc/commands/profile.clj
@@ -422,7 +422,9 @@
:deleted-at deleted-at
:id profile-id}})
- (rph/with-transform {} (session/delete-fn cfg)))))
+
+ (-> (rph/wrap nil)
+ (rph/with-transform (session/delete-fn cfg))))))
;; --- HELPERS
@@ -431,8 +433,11 @@
"WITH owner_teams AS (
SELECT tpr.team_id AS id
FROM team_profile_rel AS tpr
+ JOIN team AS t ON (t.id = tpr.team_id)
WHERE tpr.is_owner IS TRUE
AND tpr.profile_id = ?
+ AND (t.deleted_at IS NULL OR
+ t.deleted_at > now())
)
SELECT tpr.team_id AS id,
count(tpr.profile_id) - 1 AS participants
diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj
index 1a25a6dcfd..801ff555b0 100644
--- a/backend/src/app/rpc/commands/search.clj
+++ b/backend/src/app/rpc/commands/search.clj
@@ -9,7 +9,6 @@
[app.common.schema :as sm]
[app.db :as db]
[app.rpc :as-alias rpc]
- [app.rpc.commands.files :refer [resolve-public-uri]]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
@@ -61,7 +60,7 @@
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
- (assoc :thumbnail-uri (resolve-public-uri media-id)))
+ (assoc :thumbnail-id media-id))
(dissoc row :media-id))))))
(def ^:private schema:search-files
diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj
index 9d15b3e8fa..641d564af6 100644
--- a/backend/src/app/rpc/commands/viewer.clj
+++ b/backend/src/app/rpc/commands/viewer.clj
@@ -77,7 +77,7 @@
:share-links links
:libraries libs
:file file
- :team team
+ :team (assoc team :permissions perms)
:permissions perms}))
(def schema:get-view-only-bundle
diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj
deleted file mode 100644
index f0654916ce..0000000000
--- a/backend/src/app/tasks/file_xlog_gc.clj
+++ /dev/null
@@ -1,64 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) KALEIDOS INC
-
-(ns app.tasks.file-xlog-gc
- (:require
- [app.common.logging :as l]
- [app.config :as cf]
- [app.db :as db]
- [integrant.core :as ig]))
-
-;; Get the latest available snapshots without exceeding the total
-;; snapshot limit
-(def ^:private sql:get-latest-snapshots
- "SELECT fch.id, fch.created_at
- FROM file_change AS fch
- WHERE fch.file_id = ?
- AND fch.created_by = 'system'
- AND fch.data IS NOT NULL
- AND fch.deleted_at > now()
- ORDER BY fch.created_at DESC
- LIMIT ?")
-
-;; Mark all snapshots that are outside the allowed total threshold
-;; available for the GC
-(def ^:private sql:delete-snapshots
- "UPDATE file_change
- SET deleted_at = now()
- WHERE file_id = ?
- AND deleted_at > now()
- AND data IS NOT NULL
- AND created_by = 'system'
- AND created_at < ?")
-
-(defn- get-alive-snapshots
- [conn file-id]
- (let [total (cf/get :auto-file-snapshot-total 10)
- snapshots (db/exec! conn [sql:get-latest-snapshots file-id total])]
- (not-empty snapshots)))
-
-(defn- delete-old-snapshots!
- [{:keys [::db/conn] :as cfg} file-id]
- (when-let [snapshots (get-alive-snapshots conn file-id)]
- (let [last-date (-> snapshots peek :created-at)
- result (db/exec-one! conn [sql:delete-snapshots file-id last-date])]
- (l/inf :hint "delete old file snapshots"
- :file-id (str file-id)
- :current (count snapshots)
- :deleted (db/get-update-count result)))))
-
-(defmethod ig/assert-key ::handler
- [_ params]
- (assert (db/pool? (::db/pool params)) "expected a valid database pool"))
-
-(defmethod ig/init-key ::handler
- [_ cfg]
- (fn [{:keys [props] :as task}]
- (let [file-id (:file-id props)]
- (assert (uuid? file-id) "expected file-id on props")
- (-> cfg
- (assoc ::db/rollback (:rollback props false))
- (db/tx-run! delete-old-snapshots! file-id)))))
diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj
index d6678be29c..679d5221e0 100644
--- a/backend/test/backend_tests/rpc_file_test.clj
+++ b/backend/test/backend_tests/rpc_file_test.clj
@@ -1090,8 +1090,7 @@
(t/is (contains? result :file-id))
(t/is (= (:id file) (:file-id result)))
- (t/is (str/starts-with? (get-in result [:page :objects frame1-id :thumbnail])
- "http://localhost:3449/assets/by-id/"))
+ (t/is (uuid? (get-in result [:page :objects frame1-id :thumbnail-id])))
(t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))
;; Delete thumbnail data
diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj
index 748c72683a..3095a5c050 100644
--- a/backend/test/backend_tests/rpc_media_test.clj
+++ b/backend/test/backend_tests/rpc_media_test.clj
@@ -10,6 +10,7 @@
[app.db :as db]
[app.rpc :as-alias rpc]
[app.storage :as sto]
+ [app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.fs :as fs]))
@@ -245,3 +246,35 @@
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? (:media-id result)))
(t/is (uuid? (:thumbnail-id result))))))
+
+
+(t/deftest media-object-upload-command-when-file-is-deleted
+ (let [prof (th/create-profile* 1)
+ proj (th/create-project* 1 {:profile-id (:id prof)
+ :team-id (:default-team-id prof)})
+ file (th/create-file* 1 {:profile-id (:id prof)
+ :project-id (:default-project-id prof)
+ :is-shared false})
+
+ _ (th/db-update! :file
+ {:deleted-at (dt/now)}
+ {:id (:id file)})
+
+ mfile {:filename "sample.jpg"
+ :path (th/tempfile "backend_tests/test_files/sample.jpg")
+ :mtype "image/jpeg"
+ :size 312043}
+
+ params {::th/type :upload-file-media-object
+ ::rpc/profile-id (:id prof)
+ :file-id (:id file)
+ :is-local true
+ :name "testfile"
+ :content mfile}
+
+ out (th/command! params)]
+
+ (let [error (:error out)
+ error-data (ex-data error)]
+ (t/is (th/ex-info? error))
+ (t/is (= (:type error-data) :not-found)))))
diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj
index 1bd49db485..47e58adba6 100644
--- a/backend/test/backend_tests/rpc_profile_test.clj
+++ b/backend/test/backend_tests/rpc_profile_test.clj
@@ -203,7 +203,24 @@
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
- (t/is (= (:code edata) :owner-teams-with-people))))))
+ (t/is (= (:code edata) :owner-teams-with-people)))
+
+ (let [params {::th/type :delete-team
+ ::rpc/profile-id (:id prof1)
+ :id (:id team1)}
+ out (th/command! params)]
+ ;; (th/print-result! out)
+
+ (let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})]
+ (t/is (dt/instant? (:deleted-at team)))))
+
+ ;; Request profile to be deleted
+ (let [params {::th/type :delete-profile
+ ::rpc/profile-id (:id prof1)}
+ out (th/command! params)]
+ ;; (th/print-result! out)
+ (t/is (nil? (:result out)))
+ (t/is (nil? (:error out)))))))
(t/deftest profile-deletion-3
(let [prof1 (th/create-profile* 1)
@@ -291,7 +308,7 @@
out (th/command! params)]
;; (th/print-result! out)
- (t/is (= {} (:result out)))
+ (t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; query files after profile soft deletion
@@ -336,7 +353,7 @@
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
- (t/is (= {} (:result out)))
+ (t/is (nil? (:result out)))
(t/is (nil? (:error out))))
(th/run-pending-tasks!)
diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc
index 0c25331785..1fdddae3b6 100644
--- a/common/src/app/common/attrs.cljc
+++ b/common/src/app/common/attrs.cljc
@@ -64,7 +64,7 @@
;; (def shapes [{:stroke-color "#ff0000"
;; :stroke-width 3
;; :fill-color "#0000ff"
-;; :x 1000 :y 2000 :rx nil}
+;; :x 1000 :y 2000}
;; {:stroke-width "#ff0000"
;; :stroke-width 5
;; :x 1500 :y 2000}])
@@ -72,13 +72,17 @@
;; (get-attrs-multi shapes [:stroke-color
;; :stroke-width
;; :fill-color
-;; :rx
-;; :ry])
+;; :r1
+;; :r2
+;; :r3
+;; :r4])
;; >>> {:stroke-color "#ff0000"
;; :stroke-width :multiple
;; :fill-color "#0000ff"
-;; :rx nil
-;; :ry nil}
+;; :r1 nil
+;; :r2 nil
+;; :r3 nil
+;; :r4 nil}
;;
(defn get-attrs-multi
([objs attrs]
diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc
index a1580b1248..9ad9d0b28a 100644
--- a/common/src/app/common/files/changes_builder.cljc
+++ b/common/src/app/common/files/changes_builder.cljc
@@ -828,13 +828,13 @@
(apply-changes-local)))
(defn delete-token-set-path
- [changes prefixed-full-set-path]
+ [changes token-set-path]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-sets (some-> (get library-data :tokens-lib)
- (ctob/get-sets-at-prefix-path prefixed-full-set-path))]
+ (ctob/get-path-sets token-set-path))]
(-> changes
- (update :redo-changes conj {:type :del-token-set-path :path prefixed-full-set-path})
+ (update :redo-changes conj {:type :del-token-set-path :path token-set-path})
(update :undo-changes conj {:type :add-token-sets :token-sets prev-token-sets})
(apply-changes-local))))
diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc
index 21a8f304f3..fc246030b3 100644
--- a/common/src/app/common/files/defaults.cljc
+++ b/common/src/app/common/files/defaults.cljc
@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
-(def version 57)
+(def version 58)
diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc
index f6245b13b1..bcc0754bc0 100644
--- a/common/src/app/common/files/migrations.cljc
+++ b/common/src/app/common/files/migrations.cljc
@@ -1130,6 +1130,45 @@
(update :pages-index dissoc nil)
(update :pages-index update-vals update-page))))
+(defn migrate-up-58
+ [data]
+ (letfn [(update-object [object]
+ (if (and (:rx object) (not (:r1 object)))
+ (-> object
+ (assoc :r1 (:rx object))
+ (assoc :r2 (:rx object))
+ (assoc :r3 (:rx object))
+ (assoc :r4 (:rx object)))
+ object))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-object))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+
+(defn migrate-down-58
+ [data]
+ (letfn [(update-object [object]
+ (if (= (:r1 object) (:r2 object) (:r3 object) (:r4 object))
+ (-> object
+ (dissoc :r1 :r2 :r3 :r4)
+ (assoc :rx (:r1 object))
+ (assoc :ry (:r1 object)))
+ object))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-object))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+
+
+
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1178,5 +1217,6 @@
{:id 54 :migrate-up migrate-up-54}
{:id 55 :migrate-up migrate-up-55}
{:id 56 :migrate-up migrate-up-56}
- {:id 57 :migrate-up migrate-up-57}])
+ {:id 57 :migrate-up migrate-up-57}
+ {:id 58 :migrate-up migrate-up-58 :migrate-down migrate-down-58}])
diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc
index 67f90dafeb..381a4ee6b4 100644
--- a/common/src/app/common/files/repair.cljc
+++ b/common/src/app/common/files/repair.cljc
@@ -434,8 +434,10 @@
(assoc shape :type :frame
:fills []
:hide-in-viewer true
- :rx 0
- :ry 0))]
+ :r1 0
+ :r2 0
+ :r3 0
+ :r4 0))]
(log/dbg :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
diff --git a/common/src/app/common/geom/shapes/corners.cljc b/common/src/app/common/geom/shapes/corners.cljc
index 553d66136b..f33fe90370 100644
--- a/common/src/app/common/geom/shapes/corners.cljc
+++ b/common/src/app/common/geom/shapes/corners.cljc
@@ -43,9 +43,9 @@
(defn shape-corners-1
"Retrieve the effective value for the corner given a single value for corner."
- [{:keys [width height rx] :as shape}]
- (if (and (some? rx) (not (mth/almost-zero? rx)))
- (fix-radius width height rx)
+ [{:keys [width height r1] :as shape}]
+ (if (and (some? r1) (not (mth/almost-zero? r1)))
+ (fix-radius width height r1)
0))
(defn shape-corners-4
@@ -55,26 +55,11 @@
(fix-radius width height r1 r2 r3 r4)
[r1 r2 r3 r4]))
-(defn update-corners-scale-1
- "Scales round corners (using a single value)"
- [shape scale]
- (update shape :rx * scale))
-
-(defn update-corners-scale-4
- "Scales round corners (using four values)"
+(defn update-corners-scale
+ "Scales round corners"
[shape scale]
(-> shape
(update :r1 * scale)
(update :r2 * scale)
(update :r3 * scale)
(update :r4 * scale)))
-
-(defn update-corners-scale
- "Scales round corners"
- [shape scale]
- (cond-> shape
- (and (some? (:rx shape)) (> (:rx shape) 0))
- (update-corners-scale-1 scale)
-
- (and (some? (:r1 shape)) (> (:r1 shape) 0))
- (update-corners-scale-4 scale)))
diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc
index 4f544280d7..e6966e2997 100644
--- a/common/src/app/common/logic/shapes.cljc
+++ b/common/src/app/common/logic/shapes.cljc
@@ -438,12 +438,14 @@
;; Resize parent containers that need to
(pcb/resize-parents parents))))
-(defn change-show-in-viewer [shape hide?]
+(defn change-show-in-viewer
+ [shape hide?]
(assoc shape :hide-in-viewer hide?))
-(defn add-new-interaction [shape interaction]
- (-> shape
- (update :interactions ctsi/add-interaction interaction)))
+(defn add-new-interaction
+ [shape interaction]
+ (update shape :interactions ctsi/add-interaction interaction))
-(defn show-in-viewer [shape]
+(defn show-in-viewer
+ [shape]
(dissoc shape :hide-in-viewer))
diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc
index 6c1ab3746c..eaa4fffbdc 100644
--- a/common/src/app/common/schema.cljc
+++ b/common/src/app/common/schema.cljc
@@ -1010,6 +1010,9 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
+(def valid-text?
+ (validator ::text))
+
(def check-safe-int!
(check-fn ::safe-int))
diff --git a/common/src/app/common/svg/path.cljc b/common/src/app/common/svg/path.cljc
index 5951002a18..ac89be9d06 100644
--- a/common/src/app/common/svg/path.cljc
+++ b/common/src/app/common/svg/path.cljc
@@ -40,3 +40,76 @@
(map (fn [segment]
(.toPersistentMap ^js segment)))
(parser/parse path-str)))))
+
+#?(:cljs
+ (defn content->buffer
+ "Converts the path content into binary format."
+ [content]
+ (let [total (count content)
+ ssize 28
+ buffer (new js/ArrayBuffer (* total ssize))
+ dview (new js/DataView buffer)]
+ (loop [index 0]
+ (when (< index total)
+ (let [segment (nth content index)
+ offset (* index ssize)]
+ (case (:command segment)
+ :move-to
+ (let [{:keys [x y]} (:params segment)]
+ (.setInt16 dview (+ offset 0) 1)
+ (.setFloat32 dview (+ offset 20) x)
+ (.setFloat32 dview (+ offset 24) y))
+ :line-to
+ (let [{:keys [x y]} (:params segment)]
+ (.setInt16 dview (+ offset 0) 2)
+ (.setFloat32 dview (+ offset 20) x)
+ (.setFloat32 dview (+ offset 24) y))
+ :curve-to
+ (let [{:keys [c1x c1y c2x c2y x y]} (:params segment)]
+ (.setInt16 dview (+ offset 0) 3)
+ (.setFloat32 dview (+ offset 4) c1x)
+ (.setFloat32 dview (+ offset 8) c1y)
+ (.setFloat32 dview (+ offset 12) c2x)
+ (.setFloat32 dview (+ offset 16) c2y)
+ (.setFloat32 dview (+ offset 20) x)
+ (.setFloat32 dview (+ offset 24) y))
+
+ :close-path
+ (.setInt16 dview (+ offset 0) 4))
+ (recur (inc index)))))
+ buffer)))
+
+#?(:cljs
+ (defn buffer->content
+ "Converts the a buffer to a path content vector"
+ [buffer]
+ (assert (instance? js/ArrayBuffer buffer) "expected ArrayBuffer instance")
+ (let [ssize 28
+ total (/ (.-byteLength buffer) ssize)
+ dview (new js/DataView buffer)]
+ (loop [index 0
+ result []]
+ (if (< index total)
+ (let [offset (* index ssize)
+ type (.getInt16 dview (+ offset 0))
+ command (case type
+ 1 :move-to
+ 2 :line-to
+ 3 :curve-to
+ 4 :close-path)
+ params (case type
+ 1 {:x (.getFloat32 dview (+ offset 20))
+ :y (.getFloat32 dview (+ offset 24))}
+ 2 {:x (.getFloat32 dview (+ offset 20))
+ :y (.getFloat32 dview (+ offset 24))}
+ 3 {:c1x (.getFloat32 dview (+ offset 4))
+ :c1y (.getFloat32 dview (+ offset 8))
+ :c2x (.getFloat32 dview (+ offset 12))
+ :c2y (.getFloat32 dview (+ offset 16))
+ :x (.getFloat32 dview (+ offset 20))
+ :y (.getFloat32 dview (+ offset 24))}
+ 4 {})]
+ (recur (inc index)
+ (conj result {:command command
+ :params params})))
+ result)))))
diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc
index 3a7fdec936..ad86914acc 100644
--- a/common/src/app/common/text.cljc
+++ b/common/src/app/common/text.cljc
@@ -412,7 +412,6 @@
(recur (when continue? (rest styles)) taking? to result))
result))))
-
(defn content->text
"Given a root node of a text content extracts the texts with its associated styles"
[content]
diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc
index fc4ea0093d..b26461f2bb 100644
--- a/common/src/app/common/types/component.cljc
+++ b/common/src/app/common/types/component.cljc
@@ -65,8 +65,6 @@
:fill-color :fill-group
:fill-opacity :fill-group
- :rx :radius-group
- :ry :radius-group
:r1 :radius-group
:r2 :radius-group
:r3 :radius-group
diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc
index 6a03e3a49e..8c57f33094 100644
--- a/common/src/app/common/types/page.cljc
+++ b/common/src/app/common/types/page.cljc
@@ -33,7 +33,7 @@
[:id ::sm/uuid]
[:axis [::sm/one-of #{:x :y}]]
[:position ::sm/safe-number]
- [:frame-id {:optional true} ::sm/uuid]])
+ [:frame-id {:optional true} [:maybe ::sm/uuid]]])
(def schema:guides
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc
index 622404c6e5..dcb1a75dd2 100644
--- a/common/src/app/common/types/shape.cljc
+++ b/common/src/app/common/types/shape.cljc
@@ -192,8 +192,6 @@
[:constraints-v {:optional true}
[::sm/one-of vertical-constraint-types]]
[:fixed-scroll {:optional true} :boolean]
- [:rx {:optional true} ::sm/safe-number]
- [:ry {:optional true} ::sm/safe-number]
[:r1 {:optional true} ::sm/safe-number]
[:r2 {:optional true} ::sm/safe-number]
[:r3 {:optional true} ::sm/safe-number]
@@ -400,13 +398,17 @@
:fills [{:fill-color default-color
:fill-opacity 1}]
:strokes []
- :rx 0
- :ry 0})
+ :r1 0
+ :r2 0
+ :r3 0
+ :r4 0})
(def ^:private minimal-image-attrs
{:type :image
- :rx 0
- :ry 0
+ :r1 0
+ :r2 0
+ :r3 0
+ :r4 0
:fills []
:strokes []})
@@ -417,6 +419,10 @@
:strokes []
:name "Board"
:shapes []
+ :r1 0
+ :r2 0
+ :r3 0
+ :r4 0
:hide-fill-on-export false})
(def ^:private minimal-circle-attrs
diff --git a/common/src/app/common/types/shape/attrs.cljc b/common/src/app/common/types/shape/attrs.cljc
index 75509094e7..49d5a01a26 100644
--- a/common/src/app/common/types/shape/attrs.cljc
+++ b/common/src/app/common/types/shape/attrs.cljc
@@ -15,7 +15,6 @@
{:frame #{:proportion-lock
:width :height
:x :y
- :rx :ry
:r1 :r2 :r3 :r4
:rotation
:selrect
@@ -126,7 +125,6 @@
:width :height
:x :y
:rotation
- :rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
@@ -372,7 +370,6 @@
:width :height
:x :y
:rotation
- :rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
@@ -410,7 +407,6 @@
:width :height
:x :y
:rotation
- :rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
@@ -467,7 +463,6 @@
:width :height
:x :y
:rotation
- :rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
diff --git a/common/src/app/common/types/shape/radius.cljc b/common/src/app/common/types/shape/radius.cljc
index 3edb18cd0b..d125fd3295 100644
--- a/common/src/app/common/types/shape/radius.cljc
+++ b/common/src/app/common/types/shape/radius.cljc
@@ -9,69 +9,42 @@
[app.common.types.shape.attrs :refer [editable-attrs]]))
;; There are some shapes that admit border radius, as rectangles
-;; frames and images. Those shapes may define the radius of the corners in two modes:
-;; - radius-1 all corners have the same radius (although we store two
-;; values :rx and :ry because svg uses it this way).
-;; - radius-4 each corner (top-left, top-right, bottom-right, bottom-left)
-;; has an independent value. SVG does not allow this directly, so we
-;; emulate it with paths.
-
-;; A shape never will have both :rx and :r1 simultaneously
+;; frames components and images.
+;; Those shapes may define the radius of the corners with four values:
+;; One for each corner (top-left, top-right, bottom-right, bottom-left)
+;; has an independent value. SVG does not allow this directly, so we
+;; emulate it with paths.
;; All operations take into account that the shape may not be a one of those
-;; shapes that has border radius, and so it hasn't :rx nor :r1.
+;; shapes that has border radius, and so it hasn't :r1.
;; In this case operations must leave shape untouched.
+(defn can-get-border-radius?
+ [shape]
+ (contains? #{:rect :frame} (:type shape)))
+
(defn has-radius?
[shape]
- (contains? (get editable-attrs (:type shape)) :rx))
-
-(defn radius-mode
- [shape]
- (if (:r1 shape)
- :radius-4
- :radius-1))
-
-(defn radius-1?
- [shape]
- (and (:rx shape) (not= (:rx shape) 0)))
-
-(defn radius-4?
- [shape]
- (and (:r1 shape)
- (or (not= (:r1 shape) 0)
- (not= (:r2 shape) 0)
- (not= (:r3 shape) 0)
- (not= (:r4 shape) 0))))
+ (contains? (get editable-attrs (:type shape)) :r1))
(defn all-equal?
[shape]
(= (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape)))
-(defn switch-to-radius-1
+(defn radius-mode
[shape]
- (let [r (if (all-equal? shape) (:r1 shape) 0)]
- (-> shape
- (assoc :rx r :ry r)
- (dissoc :r1 :r2 :r3 :r4))))
+ (if (all-equal? shape)
+ :radius-1
+ :radius-4))
-(defn switch-to-radius-4
- [shape]
- (let [rx (:rx shape 0)]
- (-> (assoc shape :r1 rx :r2 rx :r3 rx :r4 rx)
- (dissoc :rx :ry))))
-
-(defn set-radius-1
+(defn set-radius-to-all-corners
[shape value]
+ ;; Only Apply changes to shapes that support Border Radius
(cond-> shape
- (:r1 shape)
- (-> (dissoc :r1 :r2 :r3 :r4)
- (assoc :rx 0 :ry 0))
+ (can-get-border-radius? shape)
+ (assoc :r1 value :r2 value :r3 value :r4 value)))
- :always
- (assoc :rx value :ry value)))
-
-(defn set-radius-4
+(defn set-radius-to-single-corner
[shape attr value]
(let [attr (cond->> attr
(:flip-x shape)
@@ -79,11 +52,7 @@
(:flip-y shape)
(get {:r1 :r4 :r2 :r3 :r3 :r2 :r4 :r1}))]
-
+ ;; Only Apply changes to shapes that support border Radius
(cond-> shape
- (:rx shape)
- (-> (dissoc :rx :rx)
- (assoc :r1 0 :r2 0 :r3 0 :r4 0))
-
- :always
+ (can-get-border-radius? shape)
(assoc attr value))))
diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc
index 65ef9ba790..7794b45e1a 100644
--- a/common/src/app/common/types/token.cljc
+++ b/common/src/app/common/types/token.cljc
@@ -86,8 +86,6 @@
(sm/register!
^{::sm/type ::border-radius}
[:map
- [:rx {:optional true} token-name-ref]
- [:ry {:optional true} token-name-ref]
[:r1 {:optional true} token-name-ref]
[:r2 {:optional true} token-name-ref]
[:r3 {:optional true} token-name-ref]
@@ -229,3 +227,4 @@
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
+
diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc
index 683b19f64c..79f8578dbb 100644
--- a/common/src/app/common/types/tokens_lib.cljc
+++ b/common/src/app/common/types/tokens_lib.cljc
@@ -216,8 +216,49 @@
set-name (add-set-path-prefix (last full-path))]
(conj set-path set-name)))
-(defn split-token-set-path [path]
- (split-path path set-separator))
+(defn split-set-prefix [set-path]
+ (some->> set-path
+ (re-matches #"^([SG]-)(.*)")
+ (rest)))
+
+(defn add-set-prefix [set-name]
+ (str set-prefix set-name))
+
+(defn add-set-group-prefix [group-path]
+ (str set-group-prefix group-path))
+
+(defn add-token-set-paths-prefix
+ "Returns token-set paths with prefixes to differentiate between sets and set-groups.
+
+ Sets will be prefixed with `set-prefix` (S-).
+ Set groups will be prefixed with `set-group-prefix` (G-)."
+ [paths]
+ (let [set-path (mapv add-set-group-prefix (butlast paths))
+ set-name (add-set-prefix (last paths))]
+ (conj set-path set-name)))
+
+(defn split-token-set-path [token-set-path]
+ (split-path token-set-path set-separator))
+
+(defn split-token-set-name [token-set-name]
+ (-> (split-token-set-path token-set-name)
+ (add-token-set-paths-prefix)))
+
+(defn get-token-set-path [token-set]
+ (let [path (get-path token-set set-separator)]
+ (add-token-set-paths-prefix path)))
+
+(defn set-name->set-path-string [set-name]
+ (-> (split-token-set-name set-name)
+ (join-set-path)))
+
+(defn set-path->set-name [set-path]
+ (->> (split-token-set-path set-path)
+ (map (fn [path-part]
+ (or (-> (split-set-prefix path-part)
+ (second))
+ path-part)))
+ (join-set-path)))
(defn get-token-set-final-name [path]
(-> (split-token-set-path path)
@@ -413,6 +454,7 @@ When `before-set-name` is nil, move set to bottom")
(get-set-tree [_] "get a nested tree of all sets in the library")
(get-in-set-tree [_ path] "get `path` in nested tree of all sets in the library")
(get-sets [_] "get an ordered sequence of all sets in the library")
+ (get-path-sets [_ path] "get an ordered sequence of sets at `path` in the library")
(get-sets-at-prefix-path [_ prefixed-path] "get an ordered sequence of sets at `prefixed-path` in the library")
(get-sets-at-path [_ path-str] "get an ordered sequence of sets at `path` in the library")
(rename-set-group [_ from-path-str to-path-str] "renames set groups and all child set names from `from-path-str` to `to-path-str`")
@@ -744,6 +786,11 @@ Will return a value that matches this schema:
(->> (tree-seq d/ordered-map? vals sets)
(filter (partial instance? TokenSet))))
+ (get-path-sets [_ path]
+ (some->> (get-in sets (split-token-set-path path))
+ (tree-seq d/ordered-map? vals)
+ (filter (partial instance? TokenSet))))
+
(get-sets-at-prefix-path [_ prefixed-path]
(some->> (get-in sets (split-token-set-path prefixed-path))
(tree-seq d/ordered-map? vals)
diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc
index 3c5bf5d079..b7b49e2c1f 100644
--- a/common/src/app/common/uuid.cljc
+++ b/common/src/app/common/uuid.cljc
@@ -18,11 +18,18 @@
java.nio.ByteBuffer)))
(defn uuid
- "Parse string uuid representation into proper UUID instance."
+ "Creates an UUID instance from string, expectes valid uuid strings,
+ the existense of validation is implementation detail"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/uuid s)))
+(defn parse
+ "Parse string uuid representation into proper UUID instance, validates input"
+ [s]
+ #?(:clj (UUID/fromString s)
+ :cljs (c/parse-uuid s)))
+
(defn next
[]
#?(:clj (UUIDv8/create)
@@ -44,15 +51,15 @@
[v]
(= zero v))
-#?(:clj
- (defn get-word-high
- [id]
- (.getMostSignificantBits ^UUID id)))
+(defn get-word-high
+ [id]
+ #?(:clj (.getMostSignificantBits ^UUID id)
+ :cljs (impl/getHi (.-uuid ^UUID id))))
-#?(:clj
- (defn get-word-low
- [id]
- (.getLeastSignificantBits ^UUID id)))
+(defn get-word-low
+ [id]
+ #?(:clj (.getLeastSignificantBits ^UUID id)
+ :cljs (impl/getLo (.-uuid ^UUID id))))
(defn get-bytes
[^UUID o]
@@ -80,12 +87,21 @@
[id]
(impl/shortV8 (dm/str id))))
+#?(:cljs
+ (defn get-unsigned-parts
+ "Get a Uint32 array of length 4 that represents the UUID, needed
+ for interact with wasm"
+ [this]
+ (impl/getUnsignedParts (.-uuid ^UUID this))))
+
+
#?(:cljs
(defn get-u32
+ "A cached variant of get-unsigned-parts"
[this]
(let [buffer (unchecked-get this "__u32_buffer")]
(if (nil? buffer)
- (let [buffer (impl/getUnsignedInt32Array (.-uuid ^UUID this))]
+ (let [buffer (get-unsigned-parts this)]
(unchecked-set this "__u32_buffer" buffer)
buffer)
buffer))))
@@ -97,3 +113,33 @@
b (.getLeastSignificantBits ^UUID id)]
(+ (clojure.lang.Murmur3/hashLong a)
(clojure.lang.Murmur3/hashLong b)))))
+
+;; Commented code used for debug
+;; #?(:cljs
+;; (defn ^:export test-uuid
+;; []
+;; (let [expected #uuid "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"]
+;;
+;; (js/console.log "===> to-from-bytes-roundtrip")
+;; (js/console.log (uuid.impl/getBytes (str expected)))
+;; (js/console.log (uuid.impl/fromBytes (uuid.impl/getBytes (str expected))))
+;;
+;; (js/console.log "===> HI LO roundtrip")
+;; (let [hi (uuid.impl/getHi (str expected))
+;; lo (uuid.impl/getLo (str expected))
+;; res (uuid.impl/custom hi lo)]
+;;
+;; (js/console.log "HI:" hi)
+;; (js/console.log "LO:" lo)
+;; (js/console.log "RS:" res))
+;;
+;; (js/console.log "===> OTHER")
+;; (let [parts (uuid.impl/getUnsignedParts (str expected))
+;; res (uuid.impl/fromUnsignedParts (aget parts 0)
+;; (aget parts 1)
+;; (aget parts 2)
+;; (aget parts 3))]
+;; (js/console.log "PARTS:" parts)
+;; (js/console.log "RES: " res))
+;;
+;; )))
diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js
index 3267ba3f6e..fec186bb53 100644
--- a/common/src/app/common/uuid_impl.js
+++ b/common/src/app/common/uuid_impl.js
@@ -192,6 +192,76 @@ goog.scope(function() {
}
};
+ const fillBytes = (uuid) => {
+ let rest;
+ int8[0] = (rest = parseInt(uuid.slice(0, 8), 16)) >>> 24;
+ int8[1] = (rest >>> 16) & 0xff;
+ int8[2] = (rest >>> 8) & 0xff;
+ int8[3] = rest & 0xff;
+
+ // Parse ........-####-....-....-............
+ int8[4] = (rest = parseInt(uuid.slice(9, 13), 16)) >>> 8;
+ int8[5] = rest & 0xff;
+
+ // Parse ........-....-####-....-............
+ int8[6] = (rest = parseInt(uuid.slice(14, 18), 16)) >>> 8;
+ int8[7] = rest & 0xff;
+
+ // Parse ........-....-....-####-............
+ int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
+ int8[9] = rest & 0xff,
+
+ // Parse ........-....-....-....-############
+ // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
+ int8[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
+ int8[11] = (rest / 0x100000000) & 0xff;
+ int8[12] = (rest >>> 24) & 0xff;
+ int8[13] = (rest >>> 16) & 0xff;
+ int8[14] = (rest >>> 8) & 0xff;
+ int8[15] = rest & 0xff;
+ }
+
+ const fromPair = (hi, lo) => {
+ view.setBigInt64(0, hi);
+ view.setBigInt64(8, lo);
+ return encoding.bufferToHex(int8, true);
+ }
+
+ const getHi = (uuid) => {
+ fillBytes(uuid);
+ return view.getBigInt64(0);
+ }
+
+ const getLo = (uuid) => {
+ fillBytes(uuid);
+ return view.getBigInt64(8);
+ }
+
+ const getBytes = (uuid) => {
+ fillBytes(uuid);
+ return Int8Array.from(int8);
+ }
+
+ const getUnsignedParts = (uuid) => {
+ fillBytes(uuid);
+ const result = new Uint32Array(4);
+
+ result[0] = view.getUint32(0)
+ result[1] = view.getUint32(4);
+ result[2] = view.getUint32(8);
+ result[3] = view.getUint32(12);
+
+ return result;
+ }
+
+ const fromUnsignedParts = (a, b, c, d) => {
+ view.setUint32(0, a)
+ view.setUint32(4, b)
+ view.setUint32(8, c)
+ view.setUint32(12, d)
+ return encoding.bufferToHex(int8, true);
+ }
+
const fromArray = (u8data) => {
int8.set(u8data);
return encoding.bufferToHex(int8, true);
@@ -209,8 +279,14 @@ goog.scope(function() {
};
factory.create = create;
- factory.setTag = setTag;
factory.fromArray = fromArray;
+ factory.fromPair = fromPair;
+ factory.fromUnsignedParts = fromUnsignedParts;
+ factory.getBytes = getBytes;
+ factory.getHi = getHi;
+ factory.getLo = getLo;
+ factory.getUnsignedParts = getUnsignedParts;
+ factory.setTag = setTag;
return factory;
})();
@@ -220,67 +296,44 @@ goog.scope(function() {
return encoding.bufferToBase62(short);
};
- self.custom = function formatAsUUID(mostSigBits, leastSigBits) {
- const most = mostSigBits.toString("16").padStart(16, "0");
- const least = leastSigBits.toString("16").padStart(16, "0");
- return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring(12)}-${least.substring(0, 4)}-${least.substring(4)}`;
+ self.custom = function formatAsUUID(hi, lo) {
+ if (!(hi instanceof BigInt)) {
+ hi = BigInt(hi);
+ }
+ if (!(hi instanceof BigInt)) {
+ lo = BigInt(lo);
+ }
+
+ return self.v8.fromPair(hi, lo);
};
self.fromBytes = function(data) {
if (data instanceof Uint8Array) {
return self.v8.fromArray(data);
} else if (data instanceof Int8Array) {
- data = Uint8Array.from(data);
return self.v8.fromArray(data);
} else {
- let buffer = data?.buffer;
- if (buffer instanceof ArrayBuffer) {
- data = new Uint8Array(buffer);
- return self.v8.fromArray(data);
- } else {
- throw new Error("invalid array type received");
- }
+ throw new Error("invalid array type received");
}
};
- // Code based from uuidjs/parse.ts
self.getBytes = function parse(uuid) {
- const buffer = new ArrayBuffer(16);
- const view = new Int8Array(buffer);
- let rest;
+ return self.v8.getBytes(uuid);
+ };
- // Parse ########-....-....-....-............
- view[0] = (rest = parseInt(uuid.slice(0, 8), 16)) >>> 24;
- view[1] = (rest >>> 16) & 0xff;
- view[2] = (rest >>> 8) & 0xff;
- view[3] = rest & 0xff;
+ self.getUnsignedParts = function (uuid) {
+ return self.v8.getUnsignedParts(uuid);
+ };
- // Parse ........-####-....-....-............
- view[4] = (rest = parseInt(uuid.slice(9, 13), 16)) >>> 8;
- view[5] = rest & 0xff;
+ self.fromUnsignedParts = function(a,b,c,d) {
+ return self.v8.fromUnsignedParts(a,b,c,d);
+ };
- // Parse ........-....-####-....-............
- view[6] = (rest = parseInt(uuid.slice(14, 18), 16)) >>> 8;
- view[7] = rest & 0xff;
-
- // Parse ........-....-....-####-............
- view[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
- view[9] = rest & 0xff,
-
- // Parse ........-....-....-....-############
- // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
- view[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
- view[11] = (rest / 0x100000000) & 0xff;
- view[12] = (rest >>> 24) & 0xff;
- view[13] = (rest >>> 16) & 0xff;
- view[14] = (rest >>> 8) & 0xff;
- view[15] = rest & 0xff;
-
- return view;
+ self.getHi = function (uuid) {
+ return self.v8.getHi(uuid);
}
- self.getUnsignedInt32Array = function (uuid) {
- const bytes = self.getBytes(uuid);
- return new Uint32Array(bytes.buffer);
+ self.getLo = function (uuid) {
+ return self.v8.getLo(uuid);
}
});
diff --git a/common/test/common_tests/logic/token_apply_test.cljc b/common/test/common_tests/logic/token_apply_test.cljc
index 7e53608c87..be7be1e5f6 100644
--- a/common/test/common_tests/logic/token_apply_test.cljc
+++ b/common/test/common_tests/logic/token_apply_test.cljc
@@ -58,7 +58,7 @@
(defn- apply-all-tokens
[file]
(-> file
- (tht/apply-token-to-shape :frame1 "token-radius" [:rx :ry] [:rx :ry] 10)
+ (tht/apply-token-to-shape :frame1 "token-radius" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 10)
(tht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30)
(tht/apply-token-to-shape :frame1 "token-opacity" [:opacity] [:opacity] 0.7)
(tht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2)
@@ -90,7 +90,7 @@
:attributes []})
(cto/maybe-apply-token-to-shape {:token token-radius
:shape $
- :attributes [:rx :ry]})
+ :attributes [:r1 :r2 :r3 :r4]})
(cto/maybe-apply-token-to-shape {:token token-rotation
:shape $
:attributes [:rotation]})
@@ -119,9 +119,11 @@
applied-tokens' (:applied-tokens frame1')]
;; ==== Check
- (t/is (= (count applied-tokens') 9))
- (t/is (= (:rx applied-tokens') "token-radius"))
- (t/is (= (:ry applied-tokens') "token-radius"))
+ (t/is (= (count applied-tokens') 11))
+ (t/is (= (:r1 applied-tokens') "token-radius"))
+ (t/is (= (:r2 applied-tokens') "token-radius"))
+ (t/is (= (:r3 applied-tokens') "token-radius"))
+ (t/is (= (:r4 applied-tokens') "token-radius"))
(t/is (= (:rotation applied-tokens') "token-rotation"))
(t/is (= (:opacity applied-tokens') "token-opacity"))
(t/is (= (:stroke-width applied-tokens') "token-stroke-width"))
@@ -144,7 +146,7 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
- (cto/unapply-token-id [:rx :ry])
+ (cto/unapply-token-id [:r1 :r2 :r3 :r4])
(cto/unapply-token-id [:rotation])
(cto/unapply-token-id [:opacity])
(cto/unapply-token-id [:stroke-width])
@@ -177,8 +179,10 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
- (ctn/set-shape-attr :rx 0)
- (ctn/set-shape-attr :ry 0)
+ (ctn/set-shape-attr :r1 0)
+ (ctn/set-shape-attr :r2 0)
+ (ctn/set-shape-attr :r3 0)
+ (ctn/set-shape-attr :r4 0)
(ctn/set-shape-attr :rotation 0)
(ctn/set-shape-attr :opacity 0)
(ctn/set-shape-attr :strokes [])
diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc
index 67fd9c105f..5d14c90cbb 100644
--- a/common/test/common_tests/types/tokens_lib_test.cljc
+++ b/common/test/common_tests/types/tokens_lib_test.cljc
@@ -258,10 +258,11 @@
(ctob/delete-set-path "S-not-existing-set"))
token-set' (ctob/get-set tokens-lib' "updated-name")
- token-theme' (ctob/get-theme tokens-lib' "" "test-token-theme")]
+ ;;token-theme' (ctob/get-theme tokens-lib' "" "test-token-theme")
+ ]
(t/is (= (ctob/set-count tokens-lib') 0))
- (t/is (= (:sets token-theme') #{}))
+ ;; (t/is (= (:sets token-theme') #{})) TODO: fix this
(t/is (nil? token-set'))))
(t/deftest active-themes-set-names
diff --git a/common/test/common_tests/uuid_test.cljc b/common/test/common_tests/uuid_test.cljc
index fac59c529f..e0031e1c34 100644
--- a/common/test/common_tests/uuid_test.cljc
+++ b/common/test/common_tests/uuid_test.cljc
@@ -43,5 +43,54 @@
(t/is (= result uuid))))))
+(t/deftest bytes-roundtrip-2
+ (let [uuid (uuid/uuid "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8")
+ result-bytes (uuid/get-bytes uuid)
+ expected-hi #?(:clj -6799692559624781374
+ :cljs (js/BigInt "-6799692559624781374"))
+ expected-lo #?(:clj -3327364263599220776
+ :cljs (js/BigInt "-3327364263599220776"))
+ expected-bytes [-95, -94, -93, -92, -79, -78, -63, -62, -47, -46, -45, -44, -43, -42, -41, -40]]
+ (t/testing "get-bytes"
+ (let [data (uuid/get-bytes uuid)]
+ (t/is (= (nth expected-bytes 0) (aget data 0)))
+ (t/is (= (nth expected-bytes 1) (aget data 1)))
+ (t/is (= (nth expected-bytes 2) (aget data 2)))
+ (t/is (= (nth expected-bytes 3) (aget data 3)))
+ (t/is (= (nth expected-bytes 4) (aget data 4)))
+ (t/is (= (nth expected-bytes 5) (aget data 5)))
+ (t/is (= (nth expected-bytes 6) (aget data 6)))
+ (t/is (= (nth expected-bytes 7) (aget data 7)))
+ (t/is (= (nth expected-bytes 8) (aget data 8)))
+ (t/is (= (nth expected-bytes 9) (aget data 9)))
+ (t/is (= (nth expected-bytes 10) (aget data 10)))
+ (t/is (= (nth expected-bytes 11) (aget data 11)))
+ (t/is (= (nth expected-bytes 12) (aget data 12)))
+ (t/is (= (nth expected-bytes 13) (aget data 13)))
+ (t/is (= (nth expected-bytes 14) (aget data 14)))
+ (t/is (= (nth expected-bytes 15) (aget data 15)))))
+
+ (t/testing "from-bytes"
+ (let [data (create-array expected-bytes)
+ result (uuid/from-bytes data)]
+ (t/is (= result uuid))))
+
+ (t/testing "hi-low"
+ (let [hi (uuid/get-word-high uuid)
+ lo (uuid/get-word-low uuid)]
+
+ (t/is (= hi expected-hi))
+ (t/is (= lo expected-lo))))
+
+ #?(:cljs
+ (t/testing "unsigned-parts"
+ (let [parts (uuid/get-unsigned-parts uuid)
+ expected [2711790500, 2981282242, 3520254932, 3587626968]]
+
+ (t/is (instance? js/Uint32Array parts))
+ (t/is (= (nth expected 0) (aget parts 0)))
+ (t/is (= (nth expected 1) (aget parts 1)))
+ (t/is (= (nth expected 2) (aget parts 2)))
+ (t/is (= (nth expected 3) (aget parts 3))))))))
diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile
index 45e4e08518..79169665cd 100644
--- a/docker/devenv/Dockerfile
+++ b/docker/devenv/Dockerfile
@@ -99,6 +99,7 @@ RUN set -ex; \
libnss3 \
libgbm1 \
xvfb \
+ libfontconfig-dev \
; \
rm -rf /var/lib/apt/lists/*;
diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml
index d7b5da48a5..b0c0fac227 100644
--- a/docker/devenv/docker-compose.yaml
+++ b/docker/devenv/docker-compose.yaml
@@ -125,5 +125,5 @@ services:
- "10636:10636"
ulimits:
nofile:
- soft: "1024"
- hard: "1024"
+ soft: 1024
+ hard: 1024
diff --git a/docs/img/import-export/export-card.webp b/docs/img/import-export/export-card.webp
index 7d07149e6b..b8c86d6a90 100644
Binary files a/docs/img/import-export/export-card.webp and b/docs/img/import-export/export-card.webp differ
diff --git a/docs/img/import-export/export-menu.webp b/docs/img/import-export/export-menu.webp
index 4977929b30..cf29220a55 100644
Binary files a/docs/img/import-export/export-menu.webp and b/docs/img/import-export/export-menu.webp differ
diff --git a/docs/img/styling/blend-opacity.webp b/docs/img/styling/blend-opacity.webp
new file mode 100644
index 0000000000..4a2a3d4eb9
Binary files /dev/null and b/docs/img/styling/blend-opacity.webp differ
diff --git a/docs/img/workspace-basics/history-actions.webp b/docs/img/workspace-basics/history-actions.webp
new file mode 100644
index 0000000000..ebde19d054
Binary files /dev/null and b/docs/img/workspace-basics/history-actions.webp differ
diff --git a/docs/img/workspace-basics/history-autosaved.webp b/docs/img/workspace-basics/history-autosaved.webp
new file mode 100644
index 0000000000..291a2082d1
Binary files /dev/null and b/docs/img/workspace-basics/history-autosaved.webp differ
diff --git a/docs/img/workspace-basics/history-navigate.mp4 b/docs/img/workspace-basics/history-navigate.mp4
deleted file mode 100644
index 393f9a1292..0000000000
Binary files a/docs/img/workspace-basics/history-navigate.mp4 and /dev/null differ
diff --git a/docs/img/workspace-basics/history-navigate.webp b/docs/img/workspace-basics/history-navigate.webp
deleted file mode 100644
index 31faae2ded..0000000000
Binary files a/docs/img/workspace-basics/history-navigate.webp and /dev/null differ
diff --git a/docs/img/workspace-basics/history-pin.webp b/docs/img/workspace-basics/history-pin.webp
new file mode 100644
index 0000000000..48e08e5e00
Binary files /dev/null and b/docs/img/workspace-basics/history-pin.webp differ
diff --git a/docs/img/workspace-basics/history-restore.webp b/docs/img/workspace-basics/history-restore.webp
new file mode 100644
index 0000000000..a5d9eb05c2
Binary files /dev/null and b/docs/img/workspace-basics/history-restore.webp differ
diff --git a/docs/img/workspace-basics/history-save.webp b/docs/img/workspace-basics/history-save.webp
new file mode 100644
index 0000000000..bb9c8b13ac
Binary files /dev/null and b/docs/img/workspace-basics/history-save.webp differ
diff --git a/docs/img/workspace-basics/history-view.webp b/docs/img/workspace-basics/history-view.webp
new file mode 100644
index 0000000000..27d7e75487
Binary files /dev/null and b/docs/img/workspace-basics/history-view.webp differ
diff --git a/docs/img/workspace-basics/history.webp b/docs/img/workspace-basics/history.webp
deleted file mode 100644
index b4498af991..0000000000
Binary files a/docs/img/workspace-basics/history.webp and /dev/null differ
diff --git a/docs/technical-guide/getting-started.md b/docs/technical-guide/getting-started.md
index d8dbedf4a5..7fc6da894e 100644
--- a/docs/technical-guide/getting-started.md
+++ b/docs/technical-guide/getting-started.md
@@ -4,7 +4,8 @@ title: 1. Self-hosting Guide
# Self-hosting Guide
-This guide explains how to get your own Penpot instance, running on a machine you control, to test it, use it by you or your team, or even customize and extend it any way you like.
+This guide explains how to get your own Penpot instance, running on a machine you control,
+to test it, use it by you or your team, or even customize and extend it any way you like.
If you need more context you can look at the post
@@ -14,18 +15,30 @@ about self-hosting in Penpot community.
href="https://design.penpot.app">our SaaS offer for Penpot and your
self-hosted Penpot platform!**
-There are two main options for creating a Penpot instance:
+There are three main options for creating a Penpot instance:
1. Using the platform of our partner Elestio .
2. Using Docker tool.
+3. Using Kubernetes .
-The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible. Use Docker if you already know the tool, if need full control of the process or have extra requirements and do not want to depend on any external provider, or need to do any special customization.
+The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible.
+Use Docker if you already know the tool, if need full control of the process or have extra requirements
+and do not want to depend on any external provider, or need to do any special customization.
Or you can try other options ,
offered by Penpot community.
+## Recommended settings
+To self-host Penpot, you’ll need a server with the following specifications:
+
+* **CPU:** 1-2 CPUs
+* **RAM:** 4 GiB of RAM
+* **Disk Space:** Disk requirements depend on your usage. Disk usage primarily involves the database and any files uploaded by users.
+
+This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).
+
## Install with Elestio
This section explains how to get Penpot up and running using Helm repository with everything
+created a Helm repository with everything
you need.
Therefore, your prerequisite will be to have a Kubernetes cluster on which we can install
@@ -287,7 +300,7 @@ in turn have its own release name.
With these concepts in mind, we can now explain Helm like this:
> Helm installs charts into Kubernetes clusters, creating a new release for each
-> installation. And to find new charts, you can search Helm chart repositories.
+> installation. To find new charts, you can search Helm chart repositories.
### Install Helm
diff --git a/docs/technical-guide/index.md b/docs/technical-guide/index.md
index edba5f8407..5197ea1b35 100644
--- a/docs/technical-guide/index.md
+++ b/docs/technical-guide/index.md
@@ -20,6 +20,8 @@ machine.
* In the [Install with Docker][2] section, you can find the official Docker installation guide.
+* In the [Install with Kubernetes][7] section, you can find the official Kubernetes installation guide.
+
* In the [Configuration][3] section, you can find all the customization options you can set up after installing.
* Or you can try other, not supported by Penpot, [Unofficial options][4].
@@ -28,9 +30,11 @@ machine.
The [Integration Guide][5] explains how to connect Penpot with external apps, so they get notified
when certain events occur and may create your own interconnections and collaboration features.
+
## Developing Penpot
-Also, if you are a developer, you can get into the code, to explore it, learn how it is made, or extend it and contribute with new functionality. For this, we have a different Docker installation.
+Also, if you are a developer, you can get into the code, to explore it, learn how it is made,
+or extend it and contribute with new functionality. For this, we have a different Docker installation.
In the [Developer Guide][6] you can find how to setup a development environment and many other dev-oriented documentation.
[1]: /technical-guide/getting-started/#install-with-elestio
@@ -39,3 +43,4 @@ In the [Developer Guide][6] you can find how to setup a development environment
[4]: /technical-guide/getting-started/#unofficial-self-host-options
[5]: /technical-guide/integration/
[6]: /technical-guide/developer/
+[7]: /technical-guide/getting-started/#install-with-kubernetes
diff --git a/docs/user-guide/import-export/index.njk b/docs/user-guide/import-export/index.njk
index 901df3c5ca..98d66b8f06 100644
--- a/docs/user-guide/import-export/index.njk
+++ b/docs/user-guide/import-export/index.njk
@@ -5,45 +5,25 @@ title: 14· Import/export files
Import and export files
You can export Penpot files to your computer and import them from your computer to your projects.
-
-There are two different formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.
-Penpot file (.penpot).
-The fast one. Binary Penpot specific.
-
- ✅ Highly efficient in terms of memory and transfer time when exporting and importing.
- ❌ It can be opened only in Penpot.
- ❌ Not transparent, code difficult to explore.
-
-Standard file (.zip).
-The open one. A compressed file that includes SVG and JSON.
-
- ✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).
- ✅ Allows some automations and integrations.
- ✅ Is a transparent, existing, open standard format.
- ❌ Highly inefficient in terms of memory and transfer time when exporting and importing (this is because SVG).
-
-
Export Penpot files
Exporting files is useful for many reasons. Sometimes you want to have a backup of your files and sometimes it is useful to share Penpot files with a user that does not belong to one of your teams, or you want to have a backup of your files outside Penpot, both SaaS (design.penpot.app) or at a self-hosted instance.
How to export Penpot files
Export a single file
You can download (export) files from the workspace and from the dashboard.
-
-
- From the workspace : Select the download option at the main menu.
-
-
-
- From the dashboard : Select the download option at the file card menu.
-
-
-
+
+ From the dashboard : Select the download option at the file card menu.
+
+
+
+ From the workspace : Select the download option at the main menu.
+
+
Export multiple files
Select multiple files to export them at the same time. An overlay will show you the progress of the different exports.
-
+
@@ -63,4 +43,27 @@ title: 14· Import/export files
The import option is at the projects menu. Press “Import files” and then select one or more .penpot files to import. You can import a .zip file as well.
Right before importing the files to your project, you’ll still have the opportunity to review the items to be imported, have the information about the ones that can not be imported and also the chance to discard files.
-
+
+
+Penpot export to a unique format that streamline the import and export of files and assets by being more efficient and interoperable.
+Unlike other design tools, Penpot's format is built on standard languages . The exported file is essentially a ZIP archive containing binary assets (such as bitmap and vector images) alongside a readable JSON structure. By avoiding proprietary formats, Penpot empowers users with autonomy from specific tools while enabling seamless third-party integrations.
+
+Deprecated Penpot file formats
+These formats can only be exported from version 2.3 or earlier versions, but can be imported to any Penpot version
+There are two different deprecated Penpot file formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.
+[Deprecated] Penpot file (.penpot).
+The fast one. Binary Penpot specific.
+
+ ✅ Highly efficient in terms of memory and transfer time when exporting and importing.
+ ❌ It can be opened only in Penpot.
+ ❌ Not transparent, code difficult to explore.
+
+[Deprecated] Standard file (.zip).
+The open one. A compressed file that includes SVG and JSON.
+
+ ✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).
+ ✅ Allows some automations and integrations.
+ ✅ Is a transparent, existing, open standard format.
+ ❌ Highly inefficient in terms of memory and transfer time when exporting and importing (this is because SVG).
+
\ No newline at end of file
diff --git a/docs/user-guide/introduction/shortcuts.njk b/docs/user-guide/introduction/shortcuts.njk
index 4e503bf186..cfc2b5acfc 100644
--- a/docs/user-guide/introduction/shortcuts.njk
+++ b/docs/user-guide/introduction/shortcuts.njk
@@ -307,6 +307,11 @@ title: Shortcuts
Shift ↑
⇧ ↑
+
+ Rename selected layer
+ Alt N
+ ⌥ N
+
Send backwards
Ctrl ↓
@@ -424,11 +429,6 @@ title: Shortcuts
Alt P
⌥ P
-
- History
- Alt H
- ⌥ H
-
Layers
Alt L
diff --git a/docs/user-guide/styling/index.njk b/docs/user-guide/styling/index.njk
index 4da6ff2d3c..f59373839e 100644
--- a/docs/user-guide/styling/index.njk
+++ b/docs/user-guide/styling/index.njk
@@ -155,4 +155,30 @@ title: 06· Styling
-
\ No newline at end of file
+
+
+Opacity and blend
+Set the overal opacity for layers and their blend mode.
+Blend allows you to control how a layer interacts with the layers beneath it, determining how pixels from the current layer are combined with pixels in the underlying layers. Use blend to achive various effects, such as shading, highlights, or creative visual styles.
+
+
+
+Blend options available:
+
+ Normal
+ Darken
+ Multiply
+ Color burn
+ Lighten
+ Screen
+ Color dodge
+ Overlay
+ Soft light
+ Hard light
+ Difference
+ Exclusion
+ Hue
+ Saturation
+ Color
+ Luminosity
+
\ No newline at end of file
diff --git a/docs/user-guide/teams/index.njk b/docs/user-guide/teams/index.njk
index 96d7968133..47920629d0 100644
--- a/docs/user-guide/teams/index.njk
+++ b/docs/user-guide/teams/index.njk
@@ -36,9 +36,10 @@ member is allowed to do depends on their permissions.
Team roles
These are the team roles currently available at Penpot:
- Owner: There's only one owner per team, the role is automatically assigned to the team creator. Owners have permissions to change every other member role, including transfering ownership. Owners can update team settings, invite members and delete teams.
- Admin: Permissions to change every other member role except owners. Can invite members and update team settings.
- Editor: Without permissions to change member roles, invite members or update team settings.
+ Viewer: Viewers can view, comment on and inspect files but will not be able to edit them, nor do they have permissions to manage team settings.
+ Editor: Editors can create, import, edit and manage files and libraries, but do not have permissions to manage team settings.
+ Admin: Admins have the same permissions as editors, with the added ability to change every other member's role except owners. They can invite members and update team settings.
+ Owner: There's only one owner per team, the role is automatically assigned to the team creator. Owners have all the permissions of admins, with the additional ability to change any member's role, including transferring ownership. Owners can update team settings, invite members and delete teams.
More team roles will be eventually available, as well as fine grained permissions management to control members access and actions.
diff --git a/docs/user-guide/workspace-basics/index.njk b/docs/user-guide/workspace-basics/index.njk
index 24d7ad360a..dfb5bee2a4 100644
--- a/docs/user-guide/workspace-basics/index.njk
+++ b/docs/user-guide/workspace-basics/index.njk
@@ -199,20 +199,57 @@ geometric structure. In Penpot there are three types of guides:
-History
-The history panel keeps track of the latest changes on an opened file.
+File history versions
+The history panel keeps track of the latest changes on an opened file as well as the different versions of the file, making it easier to track changes, revert to previous states and collaborate.
-View history
-To view the recent history of a file at the workspace press Ctrl/⌘ + H or click at the history icon on the toolbar at the left.
-At the history you can see items with information about the last changes. At first sight you have object type (rectangle, text, image...) and type of change (New, Modified, Deleted...). If you press the item further details are shown.
+View history
+To view the recent history of a file at the workspace click the history icon on the navbar at the left:
+
+ To see the history of file versions go to the History tab.
+ To see the history of item changes go to the Actions tab.
+
-
+
-Note: History panel is still in a very early state and shows only a limited list of changes at a current browser tab session. Refreshing the browser means refreshing the History as well. Eventually, Penpot will have a proper version history capacity.
-Navigate history
-To navigate through the history press Ctrl/⌘ + Z to go backwards and Ctrl/⌘ + Shift/⇧ + Z to go forward.
-You can also press any item of the history list to get to this specific state.
+History panel
+At the History panel, you can save the current version of your file, as well as access previous versions for up to 7 days.
+
+Restore versions
+All saved versions of the file—whether manually saved, autosaved, or pinned—can be restored, reverting the file back to its state at the selected time.
+
+
+
+
+Saved versions
+You can save the current version of your file by clicking the pin icon at the History tab. This will allow the version to be named and it will add it to your list of versions.
+
+
+
+
+Autosaved versions
+When you start working on a file, Penpot will start to automatically save versions of that file across time so that you can later restore them as needed.
+In the History tab, if you click on the autosaved versions, you’ll see a list of the exact date and time when the version was automatically saved.
+
+
+
+
+Pinned versions
+File versions can also be pinned. Pinning a file version will allow you to name it, making it easier to access at the History tab. Pinned file versions will be saved forever and can be renamed, restored or deleted at any time.
+
+
+
+
+Actions panel
+At the Actions panel, you have the object type (rectangle, text, image...) and type of change (New, Modified, Deleted...). If you press the item, it will be reverted to its state before that specific action was performed.
+
+
+
+The Actions panel shows only a limited list of changes at a current browser tab session. Refreshing the browser means refreshing the history of actions as well.
+
+Navigate actions
+To navigate through the actions press Ctrl/⌘ + Z to go backwards and Ctrl/⌘ + Shift/⇧ + Z to go forward.
+You can also press any item of the actions list to get to this specific state.
diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js
index cd48f83bc8..3c975df2e6 100644
--- a/frontend/.storybook/main.js
+++ b/frontend/.storybook/main.js
@@ -2,7 +2,7 @@
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../resources/public"],
- addons: ["@storybook/addon-essentials", "@storybook/addon-themes"],
+ addons: ["@storybook/addon-essentials", "@storybook/addon-themes", "@storybook/addon-interactions"],
core: {
builder: "@storybook/builder-vite",
options: {
diff --git a/frontend/deps.edn b/frontend/deps.edn
index e847b4fbe0..8febd1edd0 100644
--- a/frontend/deps.edn
+++ b/frontend/deps.edn
@@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
- {:git/tag "v2.14"
- :git/sha "0016623"
+ {:git/tag "v2.15"
+ :git/sha "28783a7"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}
@@ -43,7 +43,9 @@
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "2.28.18"}
+ com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
+ criterium/criterium {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.48.0"}}}
:shadow-cljs
diff --git a/frontend/dev/user.clj b/frontend/dev/user.clj
new file mode 100644
index 0000000000..763c2a0094
--- /dev/null
+++ b/frontend/dev/user.clj
@@ -0,0 +1,34 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns user
+ (:require
+ [app.common.data :as d]
+ [app.common.pprint :as pp]
+ [clojure.java.io :as io]
+ [clojure.tools.namespace.repl :as repl]
+ [clojure.pprint :refer [pprint print-table]]
+ [clojure.repl :refer :all]
+ [clojure.walk :refer [macroexpand-all]]
+ [criterium.core :as crit]))
+
+;; --- Benchmarking Tools
+
+(defmacro run-quick-bench
+ [& exprs]
+ `(crit/with-progress-reporting (crit/quick-bench (do ~@exprs) :verbose)))
+
+(defmacro run-quick-bench'
+ [& exprs]
+ `(crit/quick-bench (do ~@exprs)))
+
+(defmacro run-bench
+ [& exprs]
+ `(crit/with-progress-reporting (crit/bench (do ~@exprs) :verbose)))
+
+(defmacro run-bench'
+ [& exprs]
+ `(crit/bench (do ~@exprs)))
diff --git a/frontend/package.json b/frontend/package.json
index 4b5b374915..acf288b23e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -48,11 +48,13 @@
},
"devDependencies": {
"@playwright/test": "1.48.1",
- "@storybook/addon-essentials": "^8.3.6",
- "@storybook/addon-themes": "^8.3.6",
- "@storybook/blocks": "^8.3.6",
- "@storybook/react": "^8.3.6",
- "@storybook/react-vite": "^8.3.6",
+ "@storybook/addon-essentials": "^8.4.6",
+ "@storybook/addon-interactions": "^8.4.6",
+ "@storybook/addon-themes": "^8.4.6",
+ "@storybook/blocks": "^8.4.6",
+ "@storybook/react": "^8.4.6",
+ "@storybook/react-vite": "^8.4.6",
+ "@storybook/test": "^8.4.6",
"@types/node": "^22.7.7",
"autoprefixer": "^10.4.20",
"concurrently": "^9.0.1",
@@ -86,7 +88,7 @@
"sass": "^1.80.3",
"sass-embedded": "^1.80.3",
"shadow-cljs": "2.28.18",
- "storybook": "^8.3.6",
+ "storybook": "^8.4.6",
"svg-sprite": "^2.0.4",
"typescript": "^5.6.3",
"vite": "^5.4.9",
diff --git a/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json
index bae8fd54ef..47e0606bdd 100644
--- a/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json
+++ b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json
@@ -97,7 +97,10 @@
"~ue117f7f6-433c-807e-8004-862a18bba46f": {
"~#shape": {
"~:y": 220,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
@@ -449,7 +452,10 @@
"~ue117f7f6-433c-807e-8004-862a8c166257": {
"~#shape": {
"~:y": 97,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/dashboard/create-project.json b/frontend/playwright/data/dashboard/create-project.json
index 92566a65fb..2d9bb26dd5 100644
--- a/frontend/playwright/data/dashboard/create-project.json
+++ b/frontend/playwright/data/dashboard/create-project.json
@@ -1,6 +1,6 @@
{
"~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7",
- "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6",
+ "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1715266551088",
"~:modified-at": "~m1715266551088",
"~:is-default": false,
diff --git a/frontend/playwright/data/dashboard/get-project-files.json b/frontend/playwright/data/dashboard/get-project-files.json
index b0394aff1f..3eec69caa3 100644
--- a/frontend/playwright/data/dashboard/get-project-files.json
+++ b/frontend/playwright/data/dashboard/get-project-files.json
@@ -6,6 +6,7 @@
"~:modified-at": "~m1714045654874",
"~:name": "New File 2",
"~:revn": 1,
+ "~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false
},
{
@@ -15,6 +16,7 @@
"~:modified-at": "~m1713519762931",
"~:name": "New File 1",
"~:revn": 1,
+ "~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false
}
]
diff --git a/frontend/playwright/data/dashboard/get-shared-files.json b/frontend/playwright/data/dashboard/get-shared-files.json
index 3fffa07f45..672b34e906 100644
--- a/frontend/playwright/data/dashboard/get-shared-files.json
+++ b/frontend/playwright/data/dashboard/get-shared-files.json
@@ -111,7 +111,10 @@
"~ua30724ae-f8d8-8003-8004-69eca9b27c8c": {
"~#shape": {
"~:y": 168,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/dashboard/get-team-members-admin.json b/frontend/playwright/data/dashboard/get-team-members-admin.json
new file mode 100644
index 0000000000..04237e11f8
--- /dev/null
+++ b/frontend/playwright/data/dashboard/get-team-members-admin.json
@@ -0,0 +1,30 @@
+[
+ {
+ "~:is-admin": true,
+ "~:email": "bar@example.com",
+ "~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
+ "~:name": "Han Solo",
+ "~:fullname": "Han Solo",
+ "~:is-owner": true,
+ "~:modified-at": "~m1713533116365",
+ "~:can-edit": true,
+ "~:is-active": true,
+ "~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
+ "~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
+ "~:created-at": "~m1733324626956"
+ },
+ {
+ "~:is-admin": true,
+ "~:email": "foo@example.com",
+ "~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
+ "~:name": "Princesa Leia",
+ "~:fullname": "Princesa Leia",
+ "~:is-owner": false,
+ "~:modified-at": "~m1713533116365",
+ "~:can-edit": true,
+ "~:is-active": true,
+ "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
+ "~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
+ "~:created-at": "~m1713533116365"
+ }
+]
diff --git a/frontend/playwright/data/dashboard/thumbnail.png b/frontend/playwright/data/dashboard/thumbnail.png
new file mode 100644
index 0000000000..d03c7de5b9
Binary files /dev/null and b/frontend/playwright/data/dashboard/thumbnail.png differ
diff --git a/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json
index 0fe5f6a2c5..ffdbd98358 100644
--- a/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json
+++ b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json
@@ -185,7 +185,10 @@
"~ub574c052-1a31-80bb-8004-75636a9b8205": {
"~#shape": {
"~:y": 136,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/design/get-file-multiple-attributes.json b/frontend/playwright/data/design/get-file-multiple-attributes.json
index c0a67da95c..51e574e95a 100644
--- a/frontend/playwright/data/design/get-file-multiple-attributes.json
+++ b/frontend/playwright/data/design/get-file-multiple-attributes.json
@@ -127,7 +127,10 @@
"~u2ace9ce8-8e01-8086-8004-7ba745d4305a":{
"~#shape":{
"~:y":221,
- "~:rx":0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform":{
"~#matrix":{
"~:a":1.0,
diff --git a/frontend/playwright/data/get-teams-role-viewer.json b/frontend/playwright/data/get-teams-role-viewer.json
new file mode 100644
index 0000000000..25ca63ccf4
--- /dev/null
+++ b/frontend/playwright/data/get-teams-role-viewer.json
@@ -0,0 +1,23 @@
+[{
+ "~:features": {
+ "~#set": [
+ "layout/grid",
+ "styles/v2",
+ "fdata/pointer-map",
+ "fdata/objects-map",
+ "components/v2",
+ "fdata/shape-data-type"
+ ]
+ },
+ "~:permissions": {
+ "~:type": "~:membership",
+ "~:is-owner": false,
+ "~:is-admin": false,
+ "~:can-edit": false
+ },
+ "~:name": "Default",
+ "~:modified-at": "~m1713533116375",
+ "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
+ "~:created-at": "~m1713533116375",
+ "~:is-default": true
+}]
diff --git a/frontend/playwright/data/get-teams.json b/frontend/playwright/data/get-teams.json
new file mode 100644
index 0000000000..b572a093ac
--- /dev/null
+++ b/frontend/playwright/data/get-teams.json
@@ -0,0 +1,23 @@
+[{
+ "~:features": {
+ "~#set": [
+ "layout/grid",
+ "styles/v2",
+ "fdata/pointer-map",
+ "fdata/objects-map",
+ "components/v2",
+ "fdata/shape-data-type"
+ ]
+ },
+ "~:permissions": {
+ "~:type": "~:membership",
+ "~:is-owner": true,
+ "~:is-admin": true,
+ "~:can-edit": true
+ },
+ "~:name": "Default",
+ "~:modified-at": "~m1713533116375",
+ "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
+ "~:created-at": "~m1713533116375",
+ "~:is-default": true
+}]
diff --git a/frontend/playwright/data/logged-in-user/get-teams-complete.json b/frontend/playwright/data/logged-in-user/get-teams-complete.json
index 910e1543f1..9ec9e90a03 100644
--- a/frontend/playwright/data/logged-in-user/get-teams-complete.json
+++ b/frontend/playwright/data/logged-in-user/get-teams-complete.json
@@ -35,7 +35,7 @@
},
"~:permissions": {
"~:type": "~:membership",
- "~:is-owner": true,
+ "~:is-owner": false,
"~:is-admin": true,
"~:can-edit": true
},
diff --git a/frontend/playwright/data/workspace/get-file-fragment-7760.json b/frontend/playwright/data/workspace/get-file-fragment-7760.json
index c07d487026..9e13d17da3 100644
--- a/frontend/playwright/data/workspace/get-file-fragment-7760.json
+++ b/frontend/playwright/data/workspace/get-file-fragment-7760.json
@@ -205,7 +205,10 @@
"~u86087f92-9a17-8067-8004-7cdec98dfa7f": {
"~#shape": {
"~:y": 375,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/workspace/get-file-fragment-9056_main-2.json b/frontend/playwright/data/workspace/get-file-fragment-9056_main-2.json
index 0265c2fb7b..00efebccbb 100644
--- a/frontend/playwright/data/workspace/get-file-fragment-9056_main-2.json
+++ b/frontend/playwright/data/workspace/get-file-fragment-9056_main-2.json
@@ -183,7 +183,10 @@
"~u2e0995e6-d90f-80ed-8005-2fd17ece880a": {
"~#shape": {
"~:y": 221,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/workspace/get-file-fragment-9056_shared-2.json b/frontend/playwright/data/workspace/get-file-fragment-9056_shared-2.json
index dccf9101df..dce4680c99 100644
--- a/frontend/playwright/data/workspace/get-file-fragment-9056_shared-2.json
+++ b/frontend/playwright/data/workspace/get-file-fragment-9056_shared-2.json
@@ -97,7 +97,10 @@
"~u2e0995e6-d90f-80ed-8005-2fd0bd35e183": {
"~#shape": {
"~:y": 214,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/workspace/get-file-fragment-9066-2.json b/frontend/playwright/data/workspace/get-file-fragment-9066-2.json
index 0563aad843..8ce93cead6 100644
--- a/frontend/playwright/data/workspace/get-file-fragment-9066-2.json
+++ b/frontend/playwright/data/workspace/get-file-fragment-9066-2.json
@@ -1674,7 +1674,10 @@
"~u6ad3e6b9-c5a0-80cf-8005-283bbe378bd3": {
"~#shape": {
"~:y": 589.9999999999999,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/workspace/get-file-library.json b/frontend/playwright/data/workspace/get-file-library.json
index de47754279..1adee8c4bd 100644
--- a/frontend/playwright/data/workspace/get-file-library.json
+++ b/frontend/playwright/data/workspace/get-file-library.json
@@ -127,7 +127,10 @@
"~uc70224ec-c410-807b-8004-743400e00be8":{
"~#shape":{
"~:y":255,
- "~:rx":0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform":{
"~#matrix":{
"~:a":1.0,
diff --git a/frontend/playwright/data/workspace/get-file-not-empty.json b/frontend/playwright/data/workspace/get-file-not-empty.json
index 27a91a25b7..dc345fe5ee 100644
--- a/frontend/playwright/data/workspace/get-file-not-empty.json
+++ b/frontend/playwright/data/workspace/get-file-not-empty.json
@@ -126,7 +126,10 @@
"~u7c75e310-c3a2-80fd-8004-7cc641479aef":{
"~#shape":{
"~:y":436,
- "~:rx":0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform":{
"~#matrix":{
"~:a":1.0,
diff --git a/frontend/playwright/data/workspace/get-team-shared-files-9056.json b/frontend/playwright/data/workspace/get-team-shared-files-9056.json
index 797b550b34..bfda81bea9 100644
--- a/frontend/playwright/data/workspace/get-team-shared-files-9056.json
+++ b/frontend/playwright/data/workspace/get-team-shared-files-9056.json
@@ -113,7 +113,10 @@
"~u2e0995e6-d90f-80ed-8005-2fd0bd35e183": {
"~#shape": {
"~:y": 214,
- "~:rx": 0,
+ "~:r1": 0,
+ "~:r2": 0,
+ "~:r3": 0,
+ "~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
diff --git a/frontend/playwright/data/workspace/versions-init-fragment.json b/frontend/playwright/data/workspace/versions-init-fragment.json
index b18d2b15e8..a8ef6ebf91 100644
--- a/frontend/playwright/data/workspace/versions-init-fragment.json
+++ b/frontend/playwright/data/workspace/versions-init-fragment.json
@@ -1 +1 @@
-{"~:id":"~u406b7b01-d3e2-80e4-8005-3138b7cc5f0b","~:file-id":"~u406b7b01-d3e2-80e4-8005-3138ac5d449c","~:created-at":"~m1730197748513","~:data":{"~:options":{},"~:objects":{"~u00000000-0000-0000-0000-000000000000":{"~#shape":{"~:y":0,"~:hide-fill-on-export":false,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:name":"Root Frame","~:width":0.01,"~:type":"~:frame","~:points":[{"~#point":{"~:x":0.0,"~:y":0.0}},{"~#point":{"~:x":0.01,"~:y":0.0}},{"~#point":{"~:x":0.01,"~:y":0.01}},{"~#point":{"~:x":0.0,"~:y":0.01}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~u00000000-0000-0000-0000-000000000000","~:parent-id":"~u00000000-0000-0000-0000-000000000000","~:frame-id":"~u00000000-0000-0000-0000-000000000000","~:strokes":[],"~:x":0,"~:proportion":1.0,"~:selrect":{"~#rect":{"~:x":0,"~:y":0,"~:width":0.01,"~:height":0.01,"~:x1":0,"~:y1":0,"~:x2":0.01,"~:y2":0.01}},"~:fills":[{"~:fill-color":"#FFFFFF","~:fill-opacity":1}],"~:flip-x":null,"~:height":0.01,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138aeee944b"]}},"~ua88f39e6-60a5-80c2-8005-3138aeee944b":{"~#shape":{"~:y":427,"~:hide-fill-on-export":false,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:grow-type":"~:fixed","~:hide-in-viewer":false,"~:name":"Board","~:width":551,"~:type":"~:frame","~:points":[{"~#point":{"~:x":637,"~:y":427}},{"~#point":{"~:x":1188,"~:y":427}},{"~#point":{"~:x":1188,"~:y":761}},{"~#point":{"~:x":637,"~:y":761}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:parent-id":"~u00000000-0000-0000-0000-000000000000","~:frame-id":"~u00000000-0000-0000-0000-000000000000","~:strokes":[],"~:x":637,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":637,"~:y":427,"~:width":551,"~:height":334,"~:x1":637,"~:y1":427,"~:x2":1188,"~:y2":761}},"~:fills":[{"~:fill-color":"#FFFFFF","~:fill-opacity":1}],"~:flip-x":null,"~:height":334,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138b4d36f07"]}},"~ua88f39e6-60a5-80c2-8005-3138b196dd95":{"~#shape":{"~:y":489,"~:rx":0,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:grow-type":"~:fixed","~:hide-in-viewer":false,"~:name":"Rectangle","~:width":149,"~:type":"~:rect","~:points":[{"~#point":{"~:x":677,"~:y":489}},{"~#point":{"~:x":826,"~:y":489}},{"~#point":{"~:x":826,"~:y":629}},{"~#point":{"~:x":677,"~:y":629}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138b196dd95","~:parent-id":"~ua88f39e6-60a5-80c2-8005-3138b4d36f07","~:frame-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:strokes":[],"~:x":677,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":677,"~:y":489,"~:width":149,"~:height":140,"~:x1":677,"~:y1":489,"~:x2":826,"~:y2":629}},"~:fills":[{"~:fill-color":"#B1B2B5","~:fill-opacity":1}],"~:flip-x":null,"~:ry":0,"~:height":140,"~:flip-y":null}},"~ua88f39e6-60a5-80c2-8005-3138b4d36f07":{"~#shape":{"~:y":489,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:index":1,"~:name":"Group","~:width":149,"~:type":"~:group","~:points":[{"~#point":{"~:x":677,"~:y":489}},{"~#point":{"~:x":826,"~:y":489}},{"~#point":{"~:x":826,"~:y":629}},{"~#point":{"~:x":677,"~:y":629}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138b4d36f07","~:parent-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:frame-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:strokes":[],"~:x":677,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":677,"~:y":489,"~:width":149,"~:height":140,"~:x1":677,"~:y1":489,"~:x2":826,"~:y2":629}},"~:fills":[],"~:flip-x":null,"~:height":140,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138b196dd95"]}}},"~:id":"~u406b7b01-d3e2-80e4-8005-3138ac5d449d","~:name":"Page 1"}}
+{"~:id":"~u406b7b01-d3e2-80e4-8005-3138b7cc5f0b","~:file-id":"~u406b7b01-d3e2-80e4-8005-3138ac5d449c","~:created-at":"~m1730197748513","~:data":{"~:options":{},"~:objects":{"~u00000000-0000-0000-0000-000000000000":{"~#shape":{"~:y":0,"~:hide-fill-on-export":false,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:name":"Root Frame","~:width":0.01,"~:type":"~:frame","~:points":[{"~#point":{"~:x":0.0,"~:y":0.0}},{"~#point":{"~:x":0.01,"~:y":0.0}},{"~#point":{"~:x":0.01,"~:y":0.01}},{"~#point":{"~:x":0.0,"~:y":0.01}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~u00000000-0000-0000-0000-000000000000","~:parent-id":"~u00000000-0000-0000-0000-000000000000","~:frame-id":"~u00000000-0000-0000-0000-000000000000","~:strokes":[],"~:x":0,"~:proportion":1.0,"~:selrect":{"~#rect":{"~:x":0,"~:y":0,"~:width":0.01,"~:height":0.01,"~:x1":0,"~:y1":0,"~:x2":0.01,"~:y2":0.01}},"~:fills":[{"~:fill-color":"#FFFFFF","~:fill-opacity":1}],"~:flip-x":null,"~:height":0.01,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138aeee944b"]}},"~ua88f39e6-60a5-80c2-8005-3138aeee944b":{"~#shape":{"~:y":427,"~:hide-fill-on-export":false,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:grow-type":"~:fixed","~:hide-in-viewer":false,"~:name":"Board","~:width":551,"~:type":"~:frame","~:points":[{"~#point":{"~:x":637,"~:y":427}},{"~#point":{"~:x":1188,"~:y":427}},{"~#point":{"~:x":1188,"~:y":761}},{"~#point":{"~:x":637,"~:y":761}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:parent-id":"~u00000000-0000-0000-0000-000000000000","~:frame-id":"~u00000000-0000-0000-0000-000000000000","~:strokes":[],"~:x":637,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":637,"~:y":427,"~:width":551,"~:height":334,"~:x1":637,"~:y1":427,"~:x2":1188,"~:y2":761}},"~:fills":[{"~:fill-color":"#FFFFFF","~:fill-opacity":1}],"~:flip-x":null,"~:height":334,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138b4d36f07"]}},"~ua88f39e6-60a5-80c2-8005-3138b196dd95":{"~#shape":{"~:y":489,"~:r1":0, "~:r2":0, "~:r3":0, "~:r4":0,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:grow-type":"~:fixed","~:hide-in-viewer":false,"~:name":"Rectangle","~:width":149,"~:type":"~:rect","~:points":[{"~#point":{"~:x":677,"~:y":489}},{"~#point":{"~:x":826,"~:y":489}},{"~#point":{"~:x":826,"~:y":629}},{"~#point":{"~:x":677,"~:y":629}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138b196dd95","~:parent-id":"~ua88f39e6-60a5-80c2-8005-3138b4d36f07","~:frame-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:strokes":[],"~:x":677,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":677,"~:y":489,"~:width":149,"~:height":140,"~:x1":677,"~:y1":489,"~:x2":826,"~:y2":629}},"~:fills":[{"~:fill-color":"#B1B2B5","~:fill-opacity":1}],"~:flip-x":null,"~:ry":0,"~:height":140,"~:flip-y":null}},"~ua88f39e6-60a5-80c2-8005-3138b4d36f07":{"~#shape":{"~:y":489,"~:transform":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:rotation":0,"~:index":1,"~:name":"Group","~:width":149,"~:type":"~:group","~:points":[{"~#point":{"~:x":677,"~:y":489}},{"~#point":{"~:x":826,"~:y":489}},{"~#point":{"~:x":826,"~:y":629}},{"~#point":{"~:x":677,"~:y":629}}],"~:proportion-lock":false,"~:transform-inverse":{"~#matrix":{"~:a":1.0,"~:b":0.0,"~:c":0.0,"~:d":1.0,"~:e":0.0,"~:f":0.0}},"~:id":"~ua88f39e6-60a5-80c2-8005-3138b4d36f07","~:parent-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:frame-id":"~ua88f39e6-60a5-80c2-8005-3138aeee944b","~:strokes":[],"~:x":677,"~:proportion":1,"~:selrect":{"~#rect":{"~:x":677,"~:y":489,"~:width":149,"~:height":140,"~:x1":677,"~:y1":489,"~:x2":826,"~:y2":629}},"~:fills":[],"~:flip-x":null,"~:height":140,"~:flip-y":null,"~:shapes":["~ua88f39e6-60a5-80c2-8005-3138b196dd95"]}}},"~:id":"~u406b7b01-d3e2-80e4-8005-3138ac5d449d","~:name":"Page 1"}}
diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js
index 75464cd0ba..3f7faa6042 100644
--- a/frontend/playwright/ui/pages/DashboardPage.js
+++ b/frontend/playwright/ui/pages/DashboardPage.js
@@ -110,6 +110,10 @@ export class DashboardPage extends BaseWebSocketPage {
"get-project-files?project-id=*",
"dashboard/get-project-files.json",
);
+
+ await this.mockRPC(/assets\/by-id/gi, "dashboard/thumbnail.png", {
+ contentType: "image/png",
+ });
}
async setupNewProject() {
@@ -207,60 +211,64 @@ export class DashboardPage extends BaseWebSocketPage {
async goToDashboard() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.anyTeamId}/projects`,
+ `#/dashboard/recent?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toBeVisible();
}
async goToSecondTeamDashboard() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/projects`,
+ `#/dashboard/recent?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamMembersSection() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/members`,
+ `#/dashboard/members?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamInvitationsSection() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/invitations`,
+ `#/dashboard/invitations?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamWebhooksSection() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`,
+ `#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamWebhooksSection() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`,
+ `#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamSettingsSection() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.secondTeamId}/settings`,
+ `#/dashboard/settings?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSearch() {
- await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/search`);
+ await this.page.goto(
+ `#/dashboard/search?team-id=${DashboardPage.anyTeamId}`,
+ );
}
async goToDrafts() {
await this.page.goto(
- `#/dashboard/team/${DashboardPage.anyTeamId}/projects/${DashboardPage.draftProjectId}`,
+ `#/dashboard/files?team-id=${DashboardPage.anyTeamId}&project-id=${DashboardPage.draftProjectId}`,
);
await expect(this.mainHeading).toHaveText("Drafts");
}
async goToFonts() {
- await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/fonts`);
+ await this.page.goto(
+ `#/dashboard/fonts?team-id=${DashboardPage.anyTeamId}`,
+ );
await expect(this.mainHeading).toHaveText("Fonts");
}
diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js
index 41fd45a238..311c0c45ff 100644
--- a/frontend/playwright/ui/pages/ViewerPage.js
+++ b/frontend/playwright/ui/pages/ViewerPage.js
@@ -85,7 +85,7 @@ export class ViewerPage extends BaseWebSocketPage {
pageId = ViewerPage.anyPageId,
} = {}) {
await this.page.goto(
- `/#/view/${fileId}?page-id=${pageId}§ion=interactions&index=0`,
+ `/#/view?file-id=${fileId}&page-id=${pageId}§ion=interactions&index=0`,
);
this.#ws = await this.waitForNotificationsWebSocket();
diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js
index 841a34ea1d..3948730c56 100644
--- a/frontend/playwright/ui/pages/WorkspacePage.js
+++ b/frontend/playwright/ui/pages/WorkspacePage.js
@@ -36,6 +36,14 @@ export class WorkspacePage extends BaseWebSocketPage {
"get-team?id=*",
"workspace/get-team-default.json",
);
+ await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
+
+ await BaseWebSocketPage.mockRPC(
+ page,
+ "get-team-members?team-id=*",
+ "logged-in-user/get-team-members-your-penpot.json",
+ );
+
await BaseWebSocketPage.mockRPC(
page,
"get-profiles-for-file-comments?file-id=*",
@@ -43,6 +51,7 @@ export class WorkspacePage extends BaseWebSocketPage {
);
}
+ static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b";
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
@@ -89,7 +98,7 @@ export class WorkspacePage extends BaseWebSocketPage {
pageId = WorkspacePage.anyPageId,
} = {}) {
await this.page.goto(
- `/#/workspace/${WorkspacePage.anyProjectId}/${fileId}?page-id=${pageId}`,
+ `/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
);
this.#ws = await this.waitForNotificationsWebSocket();
diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js
index b4ef9d6e59..26aab69031 100644
--- a/frontend/playwright/ui/specs/dashboard.spec.js
+++ b/frontend/playwright/ui/specs/dashboard.spec.js
@@ -96,3 +96,20 @@ test("User has add font button", async ({ page }) => {
await dashboardPage.goToFonts();
await expect(dashboardPage.page.getByText("add custom font")).toBeVisible();
});
+
+test("Bug 9443, Admin can not demote owner", async ({ page }) => {
+ const dashboardPage = new DashboardPage(page);
+ await dashboardPage.setupDashboardFull();
+ await DashboardPage.mockRPC(
+ page,
+ "get-team-members?team-id=*",
+ "dashboard/get-team-members-admin.json",
+ );
+
+ await dashboardPage.goToSecondTeamMembersSection();
+
+ await expect(page.getByRole("heading", { name: "Members" })).toBeVisible();
+ await expect(page.getByRole("combobox", { name: "Admin" })).toBeVisible();
+ await expect(page.getByText("Owner")).toBeVisible();
+ await expect(page.getByRole("combobox", { name: "Owner" })).toHaveCount(0);
+});
diff --git a/frontend/playwright/ui/specs/versions.spec.js b/frontend/playwright/ui/specs/versions.spec.js
index 40fac7d22f..93b883a18f 100644
--- a/frontend/playwright/ui/specs/versions.spec.js
+++ b/frontend/playwright/ui/specs/versions.spec.js
@@ -30,7 +30,7 @@ test("Save and restore version", async ({ page }) => {
"workspace/versions-snapshot-1.json",
);
- await page.getByLabel("History (Alt+H)").click();
+ await page.getByLabel("History").click();
await workspacePage.mockRPC(
"create-file-snapshot",
@@ -58,10 +58,11 @@ test("Save and restore version", async ({ page }) => {
await page.getByRole("textbox").press("Enter");
await page
- .locator("li")
- .filter({ hasText: "INIT" })
- .getByRole("button")
- .click();
+ .getByLabel("History", { exact: true })
+ .locator("div")
+ .nth(3)
+ .hover();
+ await page.getByRole("button", { name: "Open version menu" }).click();
await page.getByRole("button", { name: "Restore" }).click();
await workspacePage.mockRPC(
@@ -70,4 +71,7 @@ test("Save and restore version", async ({ page }) => {
);
await page.getByRole("button", { name: "Restore" }).click();
+
+ // check that the history panel is closed after restore
+ await expect(page.getByRole("tab", { name: "design" })).toBeVisible();
});
diff --git a/frontend/playwright/ui/specs/workspace-viewer-role.spec.js b/frontend/playwright/ui/specs/workspace-viewer-role.spec.js
index 9830d39b50..d78c3a37d1 100644
--- a/frontend/playwright/ui/specs/workspace-viewer-role.spec.js
+++ b/frontend/playwright/ui/specs/workspace-viewer-role.spec.js
@@ -7,11 +7,7 @@ test.beforeEach(async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
- await WorkspacePage.mockRPC(
- page,
- "get-team?id=*",
- "workspace/get-team-role-viewer.json",
- );
+ await WorkspacePage.mockRPC(page, "get-teams", "get-teams-role-viewer.json");
await workspacePage.goToWorkspace();
});
diff --git a/frontend/resources/images/features/2.4-format.gif b/frontend/resources/images/features/2.4-format.gif
new file mode 100644
index 0000000000..2d8139ad1e
Binary files /dev/null and b/frontend/resources/images/features/2.4-format.gif differ
diff --git a/frontend/resources/images/features/2.4-history.gif b/frontend/resources/images/features/2.4-history.gif
new file mode 100644
index 0000000000..8ce09dd4b6
Binary files /dev/null and b/frontend/resources/images/features/2.4-history.gif differ
diff --git a/frontend/resources/images/features/2.4-slide-0.jpg b/frontend/resources/images/features/2.4-slide-0.jpg
new file mode 100644
index 0000000000..ae439a146c
Binary files /dev/null and b/frontend/resources/images/features/2.4-slide-0.jpg differ
diff --git a/frontend/resources/images/features/2.4-viewer.gif b/frontend/resources/images/features/2.4-viewer.gif
new file mode 100644
index 0000000000..b194f12eae
Binary files /dev/null and b/frontend/resources/images/features/2.4-viewer.gif differ
diff --git a/frontend/resources/images/icons/board-2.svg b/frontend/resources/images/icons/board-2.svg
new file mode 100644
index 0000000000..70a44ea155
--- /dev/null
+++ b/frontend/resources/images/icons/board-2.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss
index 078593de0f..0d61ab7ebb 100644
--- a/frontend/resources/styles/common/base.scss
+++ b/frontend/resources/styles/common/base.scss
@@ -41,13 +41,6 @@ body {
scrollbar-width: thin;
}
-// Firefox-only hack
-@-moz-document url-prefix() {
- * {
- scrollbar-width: auto;
- }
-}
-
img {
height: auto;
width: 100%;
diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js
index 95dce0ac38..12ab0552c9 100644
--- a/frontend/scripts/_helpers.js
+++ b/frontend/scripts/_helpers.js
@@ -237,13 +237,18 @@ async function renderTemplate(path, context = {}, partials = {}) {
return mustache.render(content, context, partials);
}
-const renderer = {
- link(href, title, text) {
- return `${text} `;
+const extension = {
+ useNewRenderer: true,
+ renderer: {
+ link(token) {
+ const href = token.href;
+ const text = token.text;
+ return `${text} `;
+ },
},
};
-marked.use({ renderer });
+marked.use(extension);
async function readTranslations() {
const langs = [
@@ -503,6 +508,7 @@ export async function compileStyles() {
const start = process.hrtime();
log.info("init: compile styles");
+
let result = await compileSassAll(worker);
result = concatSass(result);
diff --git a/frontend/scripts/repl b/frontend/scripts/repl
new file mode 100755
index 0000000000..bf9f4065fd
--- /dev/null
+++ b/frontend/scripts/repl
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
+
+set -ex
+exec clojure $OPTIONS -M -m rebel-readline.main
diff --git a/frontend/scripts/watch.js b/frontend/scripts/watch.js
index 99ced7b708..0e1d68f432 100644
--- a/frontend/scripts/watch.js
+++ b/frontend/scripts/watch.js
@@ -24,15 +24,22 @@ async function compileSassAll() {
async function compileSass(path) {
const start = process.hrtime();
log.info("changed:", path);
- const result = await h.compileSass(worker, path, { modules: true });
- sass.index[result.outputPath] = result.css;
- const output = h.concatSass(sass);
+ try {
+ const result = await h.compileSass(worker, path, { modules: true });
+ sass.index[result.outputPath] = result.css;
- await fs.writeFile("./resources/public/css/main.css", output);
+ const output = h.concatSass(sass);
- const end = process.hrtime(start);
- log.info("done:", `(${ppt(end)})`);
+ await fs.writeFile("./resources/public/css/main.css", output);
+
+ const end = process.hrtime(start);
+ log.info("done:", `(${ppt(end)})`);
+ } catch (cause) {
+ console.error(cause);
+ const end = process.hrtime(start);
+ log.error("error:", `(${ppt(end)})`);
+ }
}
await fs.mkdir("./resources/public/css/", { recursive: true });
diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs
index e9824671f1..9b759ab162 100644
--- a/frontend/src/app/main.cljs
+++ b/frontend/src/app/main.cljs
@@ -10,8 +10,9 @@
[app.common.logging :as log]
[app.common.uuid :as uuid]
[app.config :as cf]
- [app.main.data.events :as ev]
- [app.main.data.users :as du]
+ [app.main.data.auth :as da]
+ [app.main.data.event :as ev]
+ [app.main.data.profile :as dp]
[app.main.data.websocket :as ws]
[app.main.errors]
[app.main.features :as feat]
@@ -22,7 +23,6 @@
[app.main.ui.confirm]
[app.main.ui.css-cursors :as cur]
[app.main.ui.delete-shared]
- [app.main.ui.modal :refer [modal]]
[app.main.ui.routes :as rt]
[app.main.worker :as worker]
[app.plugins :as plugins]
@@ -51,31 +51,9 @@
(let [el (dom/get-element "app")]
(mf/create-root el)))
-(defonce modal-root
- (let [el (dom/get-element "modal")]
- (mf/create-root el)))
-
(defn init-ui
[]
- (mf/render! app-root (mf/element ui/app))
- (mf/render! modal-root (mf/element modal)))
-
-(defn- initialize-profile
- "Event used mainly on application bootstrap; it fetches the profile
- and if and only if the fetched profile corresponds to an
- authenticated user; proceed to fetch teams."
- [stream]
- (rx/merge
- (rx/of (du/fetch-profile))
- (->> stream
- (rx/filter (ptk/type? ::profile-fetched))
- (rx/take 1)
- (rx/map deref)
- (rx/mapcat (fn [profile]
- (if (du/is-authenticated? profile)
- (rx/of (du/fetch-teams))
- (rx/empty))))
- (rx/observe-on :async))))
+ (mf/render! app-root (mf/element ui/app)))
(defn initialize
[]
@@ -88,23 +66,27 @@
(watch [_ _ stream]
(rx/merge
(rx/of (ev/initialize)
- (feat/initialize))
+ (feat/initialize)
+ (dp/refresh-profile))
- (initialize-profile stream)
+ ;; Watch for profile deletion events
+ (->> stream
+ (rx/filter dp/profile-deleted?)
+ (rx/map da/logged-out))
;; Once profile is fetched, initialize all penpot application
;; routes
(->> stream
- (rx/filter du/profile-fetched?)
+ (rx/filter dp/profile-fetched?)
(rx/take 1)
(rx/map #(rt/init-routes)))
;; Once profile fetched and the current user is authenticated,
;; proceed to initialize the websockets connection.
(->> stream
- (rx/filter du/profile-fetched?)
+ (rx/filter dp/profile-fetched?)
(rx/map deref)
- (rx/filter du/is-authenticated?)
+ (rx/filter dp/is-authenticated?)
(rx/take 1)
(rx/map #(ws/initialize)))))))
@@ -126,9 +108,7 @@
;; The hard flag will force to unmount the whole UI and will redraw every component
(when hard?
(mf/unmount! app-root)
- (mf/unmount! modal-root)
- (set! app-root (mf/create-root (dom/get-element "app")))
- (set! modal-root (mf/create-root (dom/get-element "modal"))))
+ (set! app-root (mf/create-root (dom/get-element "app"))))
(st/emit! (ev/initialize))
(init-ui)))
diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs
new file mode 100644
index 0000000000..c1ba640b51
--- /dev/null
+++ b/frontend/src/app/main/data/auth.cljs
@@ -0,0 +1,319 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.data.auth
+ "Auth related data events"
+ (:require
+ [app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.common.exceptions :as ex]
+ [app.common.schema :as sm]
+ [app.common.uuid :as uuid]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
+ [app.main.data.notifications :as ntf]
+ [app.main.data.profile :as dp]
+ [app.main.data.team :as dtm]
+ [app.main.data.websocket :as ws]
+ [app.main.repo :as rp]
+ [app.main.router :as rt]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.storage :as storage]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]))
+
+;; --- HELPERS
+
+(defn is-authenticated?
+ [{:keys [id]}]
+ (and (uuid? id) (not= id uuid/zero)))
+
+;; --- EVENT: login
+
+(defn- logged-in
+ "This is the main event that is executed once we have logged in
+ profile. The profile can proceed from standard login or from
+ accepting invitation, or third party auth signup or singin."
+ [{:keys [props] :as profile}]
+ (letfn [(get-redirect-events [teams]
+ (if-let [redirect-href (:login-redirect storage/session)]
+ (binding [storage/*sync* true]
+ (swap! storage/session dissoc :login-redirect)
+ (if (= redirect-href (rt/get-current-href))
+ (rx/of (rt/reload true))
+ (rx/of (rt/nav-raw :href redirect-href))))
+ (if-let [file-id (get props :welcome-file-id)]
+ (rx/of (dcm/go-to-workspace
+ :file-id file-id
+ :team-id (:default-team-id profile))
+ (dp/update-profile-props {:welcome-file-id nil}))
+
+ (let [teams (into #{} (map :id) teams)
+ team-id (dtm/get-last-team-id)
+ team-id (if (and team-id (contains? teams team-id))
+ team-id
+ (:default-team-id profile))]
+ (rx/of (dcm/go-to-dashboard-recent {:team-id team-id}))))))]
+
+ (ptk/reify ::logged-in
+ ev/Event
+ (-data [_]
+ {::ev/name "signin"
+ ::ev/type "identify"
+ :email (:email profile)
+ :auth-backend (:auth-backend profile)
+ :fullname (:fullname profile)
+ :is-muted (:is-muted profile)
+ :default-team-id (:default-team-id profile)
+ :default-project-id (:default-project-id profile)})
+
+ ptk/WatchEvent
+ (watch [_ _ stream]
+ (->> (rx/merge
+ (rx/of (dp/set-profile profile)
+ (ws/initialize)
+ (dtm/fetch-teams))
+
+ (->> stream
+ (rx/filter (ptk/type? ::dtm/teams-fetched))
+ (rx/take 1)
+ (rx/map deref)
+ (rx/mapcat get-redirect-events)))
+
+ (rx/observe-on :async))))))
+
+(declare login-from-register)
+
+(defn login
+ [{:keys [email password invitation-token] :as data}]
+ (ptk/reify ::login
+ ptk/WatchEvent
+ (watch [_ _ stream]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)
+
+ params {:email email
+ :password password
+ :invitation-token invitation-token}]
+
+ ;; NOTE: We can't take the profile value from login because
+ ;; there are cases when login is successful but the cookie is
+ ;; not set properly (because of possible misconfiguration).
+ ;; So, we proceed to make an additional call to fetch the
+ ;; profile, and ensure that cookie is set correctly. If
+ ;; profile fetch is successful, we mark the user logged in, if
+ ;; the returned profile is an NOT authenticated profile, we
+ ;; proceed to logout and show an error message.
+
+ (->> (rp/cmd! :login-with-password (d/without-nils params))
+ (rx/merge-map (fn [data]
+ (rx/merge
+ (rx/of (dp/fetch-profile))
+ (->> stream
+ (rx/filter dp/profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter (complement is-authenticated?))
+ (rx/tap on-error)
+ (rx/map #(ex/raise :type :authentication))
+ (rx/observe-on :async))
+
+ (->> stream
+ (rx/filter dp/profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter is-authenticated?)
+ (rx/map (fn [profile]
+ (with-meta (merge data profile)
+ {::ev/source "login"})))
+ (rx/tap on-success)
+ (rx/map logged-in)
+ (rx/observe-on :async)))))
+ (rx/catch on-error))))))
+
+(def ^:private schema:login-with-ldap
+ [:map {:title "login-with-ldap"}
+ [:email ::sm/email]
+ [:password :string]])
+
+(defn login-with-ldap
+ [params]
+
+ (dm/assert!
+ "expected valid params"
+ (sm/check schema:login-with-ldap params))
+
+ (ptk/reify ::login-with-ldap
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta params)]
+ (->> (rp/cmd! :login-with-ldap params)
+ (rx/tap on-success)
+ (rx/map (fn [profile]
+ (-> profile
+ (with-meta {::ev/source "login-with-ldap"})
+ (logged-in))))
+ (rx/catch on-error))))))
+
+(defn login-from-token
+ "Used mainly as flow continuation after token validation."
+ [{:keys [profile] :as tdata}]
+ (ptk/reify ::login-from-token
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
+ ;; NOTE: we need this to be asynchronous because the effect
+ ;; should be called before proceed with the login process
+ (rx/observe-on :async)))))
+
+(defn login-from-register
+ "Event used mainly for mark current session as logged-in in after the
+ user successfully registered using third party auth provider (in this
+ case we dont need to verify the email)."
+ []
+ (ptk/reify ::login-from-register
+ ptk/WatchEvent
+ (watch [_ _ stream]
+ (rx/merge
+ (rx/of (dp/fetch-profile))
+ (->> stream
+ (rx/filter dp/profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter is-authenticated?)
+ (rx/map (fn [profile]
+ (with-meta profile
+ {::ev/source "register"})))
+ (rx/map logged-in)
+ (rx/observe-on :async))))))
+
+;; --- EVENT: logout
+
+(defn logged-out
+ []
+ (ptk/reify ::logged-out
+ ptk/UpdateEvent
+ (update [_ state]
+ (select-keys state [:route :router :session-id :history]))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/merge
+ ;; NOTE: We need the `effect` of the current event to be
+ ;; executed before the redirect.
+ (->> (rx/of (rt/nav :auth-login))
+ (rx/observe-on :async))
+ (rx/of (ws/finalize))))
+
+ ptk/EffectEvent
+ (effect [_ _ _]
+ ;; We prefer to keek some stuff in the storage like the current-team-id and the profile
+ (swap! storage/user (constantly {})))))
+
+(defn logout
+ []
+ (ptk/reify ::logout
+ ev/Event
+ (-data [_] {})
+
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [profile-id (:profile-id state)]
+ (->> (rx/interval 500)
+ (rx/take 1)
+ (rx/mapcat (fn [_]
+ (->> (rp/cmd! :logout {:profile-id profile-id})
+ (rx/delay-at-least 300)
+ (rx/catch (constantly (rx/of 1))))))
+ (rx/map logged-out))))))
+
+;; --- Update Profile
+
+(def ^:private
+ schema:request-profile-recovery
+ [:map {:title "request-profile-recovery" :closed true}
+ [:email ::sm/email]])
+
+(defn request-profile-recovery
+ [data]
+
+ (dm/assert!
+ "expected valid parameters"
+ (sm/check schema:request-profile-recovery data))
+
+ (ptk/reify ::request-profile-recovery
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+
+ (->> (rp/cmd! :request-profile-recovery data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: recover-profile (Password)
+
+(def ^:private
+ schema:recover-profile
+ [:map {:title "recover-profile" :closed true}
+ [:password :string]
+ [:token :string]])
+
+(defn recover-profile
+ [data]
+ (dm/assert!
+ "expected valid arguments"
+ (sm/check schema:recover-profile data))
+
+ (ptk/reify ::recover-profile
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+ (->> (rp/cmd! :recover-profile data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: crete-demo-profile
+
+(defn create-demo-profile
+ []
+ (ptk/reify ::create-demo-profile
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :create-demo-profile {})
+ (rx/map login)))))
+
+(defn show-redirect-error
+ "A helper event that interprets the OIDC redirect errors on the URI
+ and shows an appropriate error message using the notification
+ banners."
+ [error]
+ (ptk/reify ::show-redirect-error
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (when-let [hint (case error
+ "registration-disabled"
+ (tr "errors.registration-disabled")
+ "profile-blocked"
+ (tr "errors.profile-blocked")
+ "auth-provider-not-allowed"
+ (tr "errors.auth-provider-not-allowed")
+ "email-domain-not-allowed"
+ (tr "errors.email-domain-not-allowed")
+
+ ;; We explicitly do not show any error here, it a explicit user operation.
+ "unable-to-auth"
+ nil
+
+ (tr "errors.generic"))]
+
+ (rx/of (ntf/warn hint))))))
diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs
index 4da9215a2c..341357e98c 100644
--- a/frontend/src/app/main/data/changes.cljs
+++ b/frontend/src/app/main/data/changes.cljs
@@ -79,7 +79,7 @@
(let [current-file-id (get state :current-file-id)
path (if (= file-id current-file-id)
[:workspace-data]
- [:workspace-libraries file-id :data])
+ [:libraries file-id :data])
undo-changes (if pending
(->> pending
@@ -159,14 +159,14 @@
(let [file (:workspace-file state)]
(if (= (:id file) file-id)
(:revn file)
- (dm/get-in state [:workspace-libraries file-id :revn]))))
+ (dm/get-in state [:libraries file-id :revn]))))
(defn- resolve-file-vern
[state file-id]
(let [file (:workspace-file state)]
(if (= (:id file) file-id)
(:vern file)
- (dm/get-in state [:workspace-libraries file-id :vern]))))
+ (dm/get-in state [:libraries file-id :vern]))))
(defn commit-changes
"Schedules a list of changes to execute now, and add the corresponding undo changes to
diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs
index 3b7dfba2bb..3dc86b5a33 100644
--- a/frontend/src/app/main/data/comments.cljs
+++ b/frontend/src/app/main/data/comments.cljs
@@ -12,7 +12,7 @@
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
@@ -195,13 +195,16 @@
ptk/WatchEvent
(watch [_ state _]
(let [share-id (-> state :viewer-local :share-id)]
- (->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id})
- (rx/catch (fn [{:keys [type code] :as cause}]
- (if (and (= type :restriction)
- (= code :max-quote-reached))
- (rx/throw cause)
- (rx/throw {:type :comment-error}))))
- (rx/ignore))))))
+ (rx/concat
+ (when is-resolved (rx/of
+ (ptk/event ::ev/event {::ev/name "resolve-comment-thread" :thread-id id})))
+ (->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id})
+ (rx/catch (fn [{:keys [type code] :as cause}]
+ (if (and (= type :restriction)
+ (= code :max-quote-reached))
+ (rx/throw cause)
+ (rx/throw {:type :comment-error}))))
+ (rx/ignore)))))))
(defn add-comment
[thread content]
@@ -600,3 +603,18 @@
(filter (fn [comment] (some #(= % (:frame-id comment)) frame-ids?)))
(map update-comment-thread-frame)
(rx/from))))))
+
+(defn fetch-profiles
+ "Fetch or refresh all profile data for comments of the current file"
+ []
+ (ptk/reify ::fetch-comments-profiles
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)
+ share-id (or (-> state :viewer-local :share-id)
+ (:current-share-id state))]
+ (->> (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})
+ (rx/map (fn [profiles]
+ #(update % :profiles merge (d/index-by :id profiles)))))))))
+
+
diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs
index 576e6c6565..925c314677 100644
--- a/frontend/src/app/main/data/common.cljs
+++ b/frontend/src/app/main/data/common.cljs
@@ -12,17 +12,20 @@
[app.common.schema :as sm]
[app.common.types.components-list :as ctkl]
[app.common.types.team :as ctt]
- [app.config :as cf]
[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]
+ [app.util.dom :as-alias dom]
[app.util.i18n :refer [tr]]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
+(declare go-to-dashboard-recent)
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -75,15 +78,13 @@
(watch [_ _ _]
(case code
:upgrade-version
- (when (or (not= (:version params) (:full cf/version))
- (true? (:force params)))
- (rx/of (ntf/dialog
- :content (tr "notifications.by-code.upgrade-version")
- :controls :inline-actions
- :type :inline
- :level level
- :actions [{:label "Refresh" :callback force-reload!}]
- :tag :notification)))
+ (rx/of (ntf/dialog
+ :content (tr "notifications.by-code.upgrade-version")
+ :controls :inline-actions
+ :type :inline
+ :level level
+ :actions [{:label "Refresh" :callback force-reload!}]
+ :tag :notification))
:maintenance
(rx/of (ntf/dialog
@@ -227,16 +228,174 @@
[{:keys [team-id team-name change]}]
(dm/assert! (uuid? team-id))
(ptk/reify ::team-membership-change
+ ptk/UpdateEvent
+ (update [_ state]
+ ;; FIXME: Remove on 2.5
+ (assoc state :current-team-id (dm/get-in state [:profile :default-team-id])))
+
ptk/WatchEvent
(watch [_ state _]
(when (= :removed change)
(let [message (tr "dashboard.removed-from-team" team-name)
- profile (:profile state)]
+ team-id (-> state :profile :default-team-id)]
(rx/concat
- (rx/of (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))
+ (rx/of (go-to-dashboard-recent :team-id team-id))
(->> (rx/of (ntf/info message))
;; Delay so the navigation can finish
(rx/delay 250))))))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; NAVEGATION EVENTS
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn go-to-feedback
+ []
+ (ptk/reify ::go-to-feedback
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (rt/nav :settings-feedback {}
+ ::rt/new-window true
+ ::rt/window-name "penpot-feedback")))))
+
+(defn go-to-dashboard-files
+ [& {:keys [project-id team-id] :as options}]
+ (ptk/reify ::go-to-dashboard-files
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [profile (get state :profile)
+ team-id (or team-id (:current-team-id state))
+ project-id (if (= project-id :default)
+ (:default-project-id profile)
+ project-id)
+
+ params {:team-id team-id
+ :project-id project-id}]
+ (rx/of (rt/nav :dashboard-files params options))))))
+
+(defn go-to-dashboard-search
+ [& {:keys [term] :as options}]
+ (ptk/reify ::go-to-dashboard-search
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/merge
+ (->> (rx/of (rt/nav :dashboard-search
+ {:team-id team-id
+ :search-term term})
+ (modal/hide))
+ (rx/observe-on :async))
+
+ (->> stream
+ (rx/filter (ptk/type? ::rt/navigated))
+ (rx/take 1)
+ (rx/map (fn [_]
+ (ptk/event ::dom/focus-element
+ {:name "search-input"})))))))))
+
+(defn go-to-dashboard-libraries
+ [& {:keys [team-id] :as options}]
+ (ptk/reify ::go-to-dashboard-libraries
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (or team-id (:current-team-id state))]
+ (rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
+(defn go-to-dashboard-fonts
+ [& {:keys [team-id] :as options}]
+ (ptk/reify ::go-to-dashboard-fonts
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (or team-id (:current-team-id state))]
+ (rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
+
+(defn go-to-dashboard-recent
+ [& {:keys [team-id] :as options}]
+ (ptk/reify ::go-to-dashboard-recent
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [profile (get state :profile)
+ team-id (cond
+ (= :default team-id)
+ (:default-team-id profile)
+
+ (uuid? team-id)
+ team-id
+
+ :else
+ (:current-team-id state))
+ params {:team-id team-id}]
+ (rx/of (modal/hide)
+ (rt/nav :dashboard-recent params options))))))
+
+(defn go-to-dashboard-members
+ [& {:as options}]
+ (ptk/reify ::go-to-dashboard-members
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-members {:team-id team-id}))))))
+
+(defn go-to-dashboard-invitations
+ [& {:as options}]
+ (ptk/reify ::go-to-dashboard-invitations
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-invitations {:team-id team-id}))))))
+
+(defn go-to-dashboard-webhooks
+ [& {:as options}]
+ (ptk/reify ::go-to-dashboard-webhooks
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-webhooks {:team-id team-id}))))))
+
+(defn go-to-dashboard-settings
+ [& {:as options}]
+ (ptk/reify ::go-to-dashboard-settings
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-settings {:team-id team-id}))))))
+
+(defn go-to-workspace
+ [& {:keys [team-id file-id page-id layout] :as options}]
+ (ptk/reify ::go-to-workspace
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (or team-id (:current-team-id state))
+ file-id (or file-id (:current-file-id state))
+ ;: FIXME: why not :current-page-id
+ page-id (or page-id
+ (dm/get-in state [:workspace-data :pages 0]))
+ params (-> (rt/get-params state)
+ (assoc :team-id team-id)
+ (assoc :file-id file-id)
+ (assoc :page-id page-id)
+ (assoc :layout layout)
+ (d/without-nils))]
+ (rx/of (rt/nav :workspace params options))))))
+
+(defn go-to-viewer
+ [& {:keys [file-id page-id section frame-id index] :as options}]
+ (ptk/reify ::go-to-viewer
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [page-id (or page-id (:current-page-id state))
+ file-id (or file-id (:current-file-id state))
+ section (or section :interactions)
+ params {:file-id file-id
+ :page-id page-id
+ :section section
+ :frame-id frame-id
+ :index index}
+ params (d/without-nils params)
+ name (dm/str "viewer-" file-id)
+ options (merge {::rt/new-window true
+ ::rt/window-name name}
+ options)]
+ (rx/of ::dps/force-persist
+ (rt/nav :viewer params options))))))
+
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index 795c112e60..49756a5592 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -12,26 +12,17 @@
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
- [app.common.types.team :as ctt]
- [app.common.uri :as u]
[app.common.uuid :as uuid]
- [app.config :as cf]
- [app.main.data.common :as dc]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.fonts :as df]
- [app.main.data.media :as di]
[app.main.data.modal :as modal]
- [app.main.data.users :as du]
[app.main.data.websocket :as dws]
[app.main.features :as features]
[app.main.repo :as rp]
- [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.sse :as sse]
[app.util.time :as dt]
- [app.util.timers :as tm]
- [app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[clojure.set :as set]
[potok.v2.core :as ptk]))
@@ -43,143 +34,38 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects)
-(declare fetch-team-members)
(declare process-message)
(defn initialize
- [{:keys [id]}]
- (dm/assert! (uuid? id))
+ []
(ptk/reify ::initialize
- ptk/UpdateEvent
- (update [_ state]
- (let [prev-team-id (:current-team-id state)]
- (cond-> state
- (not= prev-team-id id)
- (-> (dissoc :current-team-initialized)
- (dissoc :dashboard-files)
- (dissoc :dashboard-projects)
- (dissoc :dashboard-shared-files)
- (dissoc :dashboard-recent-files)
- (dissoc :dashboard-team-members)
- (dissoc :dashboard-team-stats)
- (assoc :current-team-id id)
- (update :workspace-global dissoc :default-font)))))
-
ptk/WatchEvent
(watch [_ state stream]
- (let [stopper (rx/filter (ptk/type? ::finalize) stream)
+ (let [stopper (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(->> (rx/merge
- ;; fetch teams must be first in case the team doesn't exist
- (ptk/watch (du/fetch-teams) state stream)
- (ptk/watch (df/load-team-fonts id) state stream)
- (ptk/watch (fetch-projects) state stream)
- (ptk/watch (fetch-team-members) state stream)
- (ptk/watch (du/fetch-users) state stream)
-
+ (rx/of (fetch-projects)
+ (df/fetch-fonts))
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [topic] :as msg}]
(or (= topic uuid/zero)
(= topic profile-id))))
- (rx/map process-message))
-
- ;; Once the teams are fecthed, initialize features related
- ;; to currently active team
- (->> stream
- (rx/filter (ptk/type? ::du/teams-fetched))
- (rx/observe-on :async)
- (rx/mapcat deref)
- (rx/filter #(= id (:id %)))
- (rx/mapcat (fn [team]
- (rx/of (du/set-current-team team)
- #(assoc % :current-team-initialized true))))))
+ (rx/map process-message)
+ (rx/ignore)))
(rx/take-until stopper))))))
(defn finalize
- [params]
- (ptk/data-event ::finalize params))
+ []
+ (ptk/data-event ::finalize {}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; --- EVENT: fetch-team-members
-
-(defn team-members-fetched
- [members]
- (ptk/reify ::team-members-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :dashboard-team-members (d/index-by :id members)))))
-
-(defn fetch-team-members
- ([] (fetch-team-members nil))
- ([team-id]
- (ptk/reify ::fetch-team-members
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (or team-id (:current-team-id state))]
- (assert (uuid? team-id) "expected team-id to be resolved")
- (->> (rp/cmd! :get-team-members {:team-id team-id})
- (rx/map team-members-fetched)))))))
-
-;; --- EVENT: fetch-team-stats
-
-(defn team-stats-fetched
- [stats]
- (ptk/reify ::team-stats-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :dashboard-team-stats stats))))
-
-(defn fetch-team-stats
- [team-id]
- (ptk/reify ::fetch-team-stats
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-team-stats {:team-id team-id})
- (rx/map team-stats-fetched)))))
-
-;; --- EVENT: fetch-team-invitations
-
-(defn team-invitations-fetched
- [invitations]
- (ptk/reify ::team-invitations-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :dashboard-team-invitations invitations))))
-
-(defn fetch-team-invitations
- []
- (ptk/reify ::fetch-team-invitations
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (->> (rp/cmd! :get-team-invitations {:team-id team-id})
- (rx/map team-invitations-fetched))))))
-
-;; --- EVENT: fetch-team-webhooks
-
-(defn team-webhooks-fetched
- [webhooks]
- (ptk/reify ::team-webhooks-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :dashboard-team-webhooks webhooks))))
-
-(defn fetch-team-webhooks
- []
- (ptk/reify ::fetch-team-webhooks
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (->> (rp/cmd! :get-webhooks {:team-id team-id})
- (rx/map team-webhooks-fetched))))))
-
;; --- EVENT: fetch-projects
(defn projects-fetched
@@ -187,8 +73,10 @@
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
- (let [projects (d/index-by :id projects)]
- (assoc state :dashboard-projects projects)))))
+ (reduce (fn [state {:keys [id] :as project}]
+ (update-in state [:projects id] merge project))
+ state
+ projects))))
(defn fetch-projects
[]
@@ -201,82 +89,28 @@
;; --- EVENT: search
-(defn search-result-fetched
- [result]
- (ptk/reify ::search-result-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :dashboard-search-result result))))
-
-(def schema:search-params
+(def ^:private schema:search-params
[:map {:closed true}
[:search-term [:maybe :string]]])
+(def ^:private check-search-params
+ (sm/check-fn schema:search-params))
+
(defn search
[params]
- (dm/assert! schema:search-params params)
- (ptk/reify ::search
- ptk/UpdateEvent
- (update [_ state]
- (dissoc state :dashboard-search-result))
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (assoc params :team-id team-id)]
- (->> (rp/cmd! :search-files params)
- (rx/map search-result-fetched))))))
-
-;; --- EVENT: files
-
-(defn files-fetched
- [project-id files]
- (letfn [(remove-project-files [files]
- (reduce-kv (fn [result id file]
- (cond-> result
- (= (:project-id file) project-id) (dissoc id)))
- files
- files))]
- (ptk/reify ::files-fetched
+ (let [params (check-search-params params)]
+ (ptk/reify ::search
ptk/UpdateEvent
(update [_ state]
- (-> state
- (update :dashboard-files
- (fn [state]
- (let [state (remove-project-files state)]
- (reduce #(assoc %1 (:id %2) %2) state files))))
- (assoc-in [:dashboard-projects project-id :count] (count files)))))))
+ (dissoc state :search-result))
-(defn fetch-files
- [{:keys [project-id] :as params}]
- (dm/assert! (uuid? project-id))
- (ptk/reify ::fetch-files
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-project-files {:project-id project-id})
- (rx/map #(files-fetched project-id %))))))
-
-;; --- EVENT: shared-files
-
-(defn shared-files-fetched
- [files]
- (ptk/reify ::shared-files-fetched
- ptk/UpdateEvent
- (update [_ state]
- (let [files (d/index-by :id files)]
- (-> state
- (assoc :dashboard-shared-files files)
- (update :dashboard-files d/merge files))))))
-
-(defn fetch-shared-files
- ([] (fetch-shared-files nil))
- ([team-id]
- (ptk/reify ::fetch-shared-files
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (or team-id (:current-team-id state))]
- (->> (rp/cmd! :get-team-shared-files {:team-id team-id})
- (rx/map shared-files-fetched)))))))
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/cmd! :search-files params)
+ (rx/map (fn [result]
+ #(assoc % :search-result result)))))))))
;; --- EVENT: recent-files
@@ -287,8 +121,8 @@
(update [_ state]
(let [files (d/index-by :id files)]
(-> state
- (assoc :dashboard-recent-files files)
- (update :dashboard-files d/merge files))))))
+ (assoc :recent-files files)
+ (update :files d/merge files))))))
(defn fetch-recent-files
[]
@@ -325,27 +159,22 @@
(ptk/reify ::clear-file-select
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-local
- assoc :selected-files #{}
- :selected-project nil
- :menu-open false
- :menu-pos nil))))
+ (-> state
+ (dissoc :selected-files)
+ (dissoc :selected-project)
+ (update :dashboard-local dissoc :menu-open :menu-pos)))))
(defn toggle-file-select
[{:keys [id project-id] :as file}]
(ptk/reify ::toggle-file-select
ptk/UpdateEvent
(update [_ state]
- (let [selected-project-id (get-in state [:dashboard-local :selected-project])]
+ (let [selected-project-id (get state :selected-project)]
(if (or (nil? selected-project-id)
(= selected-project-id project-id))
- (update state :dashboard-local
- (fn [local]
- (-> local
- (update :selected-files #(if (contains? % id)
- (disj % id)
- (conj % id)))
- (assoc :selected-project project-id))))
+ (-> state
+ (update :selected-files #(if (contains? % id) (disj % id) (conj % id)))
+ (assoc :selected-project project-id))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -399,318 +228,6 @@
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; --- EVENT: create-team
-
-(defn team-created
- [team]
- (ptk/reify ::team-created
- IDeref
- (-deref [_] team)))
-
-(defn create-team
- [{:keys [name] :as params}]
- (dm/assert! (string? name))
- (ptk/reify ::create-team
- ptk/WatchEvent
- (watch [it state _]
- (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}]
- (->> (rp/cmd! :create-team (with-meta params (meta it)))
- (rx/tap on-success)
- (rx/map team-created)
- (rx/catch on-error))))))
-
-;; --- EVENT: create-team-with-invitations
-
-(defn create-team-with-invitations
- [{:keys [name emails role] :as params}]
- (ptk/reify ::create-team-with-invitations
- ptk/WatchEvent
- (watch [it state _]
- (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
- :emails emails
- :role role
- :features features}]
- (->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
- (rx/tap on-success)
- (rx/map team-created)
- (rx/catch on-error))))))
-
-;; --- EVENT: update-team
-
-(defn update-team
- [{:keys [id name] :as params}]
- (ptk/reify ::update-team
- ptk/UpdateEvent
- (update [_ state]
- (assoc-in state [:teams id :name] name))
-
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :update-team params)
- (rx/ignore)))))
-
-(defn update-team-photo
- [file]
- (dm/assert!
- "expected a valid blob for `file` param"
- (di/blob? file))
- (ptk/reify ::update-team-photo
- ptk/WatchEvent
- (watch [_ state _]
- (let [on-success di/notify-finished-loading
- on-error #(do (di/notify-finished-loading)
- (di/process-error %))
- team-id (:current-team-id state)
- prepare #(hash-map :file % :team-id team-id)]
-
- (di/notify-start-loading)
- (->> (rx/of file)
- (rx/map di/validate-file)
- (rx/map prepare)
- (rx/mapcat #(rp/cmd! :update-team-photo %))
- (rx/tap on-success)
- (rx/mapcat (fn [_]
- (rx/of (du/fetch-teams)
- (ptk/data-event ::ev/event
- {::ev/name "update-team-photo"
- :team-id team-id}))))
- (rx/catch on-error))))))
-
-(defn update-team-member-role
- [{:keys [role member-id] :as params}]
- (dm/assert! (uuid? member-id))
- (dm/assert! (contains? ctt/valid-roles role))
-
- (ptk/reify ::update-team-member-role
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (assoc params :team-id team-id)]
- (->> (rp/cmd! :update-team-member-role params)
- (rx/mapcat (fn [_]
- (rx/of (fetch-team-members)
- (du/fetch-teams)
- (ptk/data-event ::ev/event
- {::ev/name "update-team-member-role"
- :team-id team-id
- :role role
- :member-id member-id})))))))))
-
-(defn delete-team-member
- [{:keys [member-id] :as params}]
- (dm/assert! (uuid? member-id))
- (ptk/reify ::delete-team-member
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (assoc params :team-id team-id)]
- (->> (rp/cmd! :delete-team-member params)
- (rx/mapcat (fn [_]
- (rx/of (fetch-team-members)
- (du/fetch-teams)
- (ptk/data-event ::ev/event
- {::ev/name "delete-team-member"
- :team-id team-id
- :member-id member-id})))))))))
-
-(defn leave-team
- [{:keys [reassign-to] :as params}]
- (dm/assert! (or (nil? reassign-to)
- (uuid? reassign-to)))
-
- (ptk/reify ::leave-team
- ptk/WatchEvent
- (watch [_ state _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)
- team-id (:current-team-id state)
- params (cond-> {:id team-id}
- (uuid? reassign-to)
- (assoc :reassign-to reassign-to))]
- (->> (rp/cmd! :leave-team params)
- (rx/tap #(tm/schedule on-success))
- (rx/map (fn [_]
- (ptk/data-event ::ev/event
- {::ev/name "leave-team"
- :reassign-to reassign-to
- :team-id team-id})))
- (rx/catch on-error))))))
-
-(defn invite-team-members
- [{:keys [emails role team-id resend?] :as params}]
- (dm/assert! (keyword? role))
- (dm/assert! (uuid? team-id))
-
- (dm/assert!
- "expected a valid set of emails"
- (sm/check-set-of-emails! emails))
-
- (ptk/reify ::invite-team-members
- ev/Event
- (-data [_]
- {:role role
- :team-id team-id
- :resend resend?})
-
- ptk/WatchEvent
- (watch [it _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)
- params (dissoc params :resend?)]
- (->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-
-(defn copy-invitation-link
- [{:keys [email team-id] :as params}]
- (dm/assert!
- "expected a valid email"
- (sm/check-email! email))
-
- (dm/assert! (uuid? team-id))
-
- (ptk/reify ::copy-invitation-link
- IDeref
- (-deref [_] {:email email :team-id team-id})
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)
- router (:router state)]
-
- (->> (rp/cmd! :get-team-invitation-token params)
- (rx/map (fn [params]
- (rt/resolve router :auth-verify-token {} params)))
- (rx/map (fn [fragment]
- (assoc cf/public-uri :fragment fragment)))
- (rx/tap (fn [uri]
- (wapi/write-to-clipboard (str uri))))
- (rx/tap on-success)
- (rx/ignore)
- (rx/catch on-error))))))
-
-
-(defn update-team-invitation-role
- [{:keys [email team-id role] :as params}]
- (dm/assert!
- "expected a valid email"
- (sm/check-email! email))
-
- (dm/assert! (uuid? team-id))
- (dm/assert! (contains? ctt/valid-roles role))
-
- (ptk/reify ::update-team-invitation-role
- IDeref
- (-deref [_] {:role role})
-
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :update-team-invitation-role params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(defn delete-team-invitation
- [{:keys [email team-id] :as params}]
- (dm/assert! (sm/check-email! email))
- (dm/assert! (uuid? team-id))
- (ptk/reify ::delete-team-invitation
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :delete-team-invitation params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(defn delete-team-webhook
- [{:keys [id] :as params}]
- (dm/assert! (uuid? id))
- (ptk/reify ::delete-team-webhook
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (assoc params :team-id team-id)
- {:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :delete-webhook params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(def valid-mtypes
- #{"application/json"
- "application/x-www-form-urlencoded"
- "application/transit+json"})
-
-(defn update-team-webhook
- [{:keys [id uri mtype is-active] :as params}]
- (dm/assert! (uuid? id))
- (dm/assert! (contains? valid-mtypes mtype))
- (dm/assert! (boolean? is-active))
- (dm/assert! (u/uri? uri))
- (ptk/reify ::update-team-webhook
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (assoc params :team-id team-id)
- {:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :update-webhook params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(defn create-team-webhook
- [{:keys [uri mtype is-active] :as params}]
- (dm/assert! (contains? valid-mtypes mtype))
- (dm/assert! (boolean? is-active))
- (dm/assert! (u/uri? uri))
-
- (ptk/reify ::create-team-webhook
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- params (-> params
- (assoc :team-id team-id)
- (update :uri str))
- {:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :create-webhook params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- EVENT: delete-team
-
-(defn delete-team
- [{:keys [id] :as params}]
- (ptk/reify ::delete-team
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :delete-team {:id id})
- (rx/tap on-success)
- (rx/catch on-error))))))
-
;; --- EVENT: create-project
(defn- project-created
@@ -722,7 +239,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (assoc-in [:dashboard-projects id] project)
+ (assoc-in [:projects id] project)
(assoc-in [:dashboard-local :project-for-edit] id)))))
(defn create-project
@@ -730,7 +247,7 @@
(ptk/reify ::create-project
ptk/WatchEvent
(watch [_ state _]
- (let [projects (get state :dashboard-projects)
+ (let [projects (get state :projects)
unames (cfh/get-used-names projects)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
team-id (:current-team-id state)
@@ -751,7 +268,7 @@
(ptk/reify ::project-duplicated
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:dashboard-projects id] project))))
+ (assoc-in state [:projects id] project))))
(defn duplicate-project
[{:keys [id name] :as params}]
@@ -801,11 +318,11 @@
(ptk/reify ::toggle-project-pin
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned)))
+ (assoc-in state [:projects id :is-pinned] (not is-pinned)))
ptk/WatchEvent
(watch [_ state _]
- (let [project (get-in state [:dashboard-projects id])
+ (let [project (get-in state [:projects id])
params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/cmd! :update-project-pin params)
(rx/ignore))))))
@@ -818,7 +335,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (update-in [:dashboard-projects id :name] (constantly name))
+ (update-in [:projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent
@@ -834,7 +351,7 @@
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-projects dissoc id))
+ (update state :projects dissoc id))
ptk/WatchEvent
(watch [_ _ _]
@@ -848,7 +365,7 @@
(ptk/reify ::file-deleted
ptk/UpdateEvent
(update [_ state]
- (update-in state [:dashboard-projects project-id :count] dec))))
+ (update-in state [:projects project-id :count] dec))))
(defn delete-file
[{:keys [id project-id] :as params}]
@@ -856,9 +373,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (d/update-when :dashboard-files dissoc id)
- (d/update-when :dashboard-shared-files dissoc id)
- (d/update-when :dashboard-recent-files dissoc id)))
+ (d/update-when :files dissoc id)
+ (d/update-when :shared-files dissoc id)
+ (d/update-when :recent-files dissoc id)))
ptk/WatchEvent
(watch [_ state _]
@@ -880,9 +397,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (d/update-in-when [:dashboard-files id :name] (constantly name))
- (d/update-in-when [:dashboard-shared-files id :name] (constantly name))
- (d/update-in-when [:dashboard-recent-files id :name] (constantly name))))
+ (d/update-in-when [:files id :name] (constantly name))
+ (d/update-in-when [:shared-files id :name] (constantly name))
+ (d/update-in-when [:recent-files id :name] (constantly name))))
ptk/WatchEvent
(watch [_ _ _]
@@ -904,10 +421,10 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
- (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))
+ (d/update-in-when [:files id :is-shared] (constantly is-shared))
+ (d/update-in-when [:recent-files id :is-shared] (constantly is-shared))
(cond-> (not is-shared)
- (d/update-when :dashboard-shared-files dissoc id))))
+ (d/update-when :shared-files dissoc id))))
ptk/WatchEvent
(watch [_ _ _]
@@ -926,8 +443,8 @@
(= file-id (:id %))
(assoc :thumbnail-id thumbnail-id)))))]
(-> state
- (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id)
- (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id)
+ (d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
+ (d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file
@@ -944,9 +461,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (assoc-in [:dashboard-files id] file)
- (assoc-in [:dashboard-recent-files id] file)
- (update-in [:dashboard-projects project-id :count] inc)))))
+ (assoc-in [:files id] file)
+ (assoc-in [:recent-files id] file)
+ (update-in [:projects project-id :count] inc)))))
(defn create-file
[{:keys [project-id name] :as params}]
@@ -961,7 +478,7 @@
:or {on-success identity
on-error rx/throw}} (meta params)
- files (get state :dashboard-files)
+ files (get state :files)
unames (cfh/get-used-names files)
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
features (-> (features/get-team-enabled-features state)
@@ -1013,14 +530,14 @@
ptk/UpdateEvent
(update [_ state]
- (let [origin-project (get-in state [:dashboard-files (first ids) :project-id])
+ (let [origin-project (get-in state [:files (first ids) :project-id])
update-project (fn [project delta op]
(-> project
(update :count #(op % (count ids)))
(assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))]
(-> state
- (d/update-in-when [:dashboard-projects origin-project] update-project 0 -)
- (d/update-in-when [:dashboard-projects project-id] update-project 10 +))))
+ (d/update-in-when [:projects origin-project] update-project 0 -)
+ (d/update-in-when [:projects project-id] update-project 10 +))))
ptk/WatchEvent
(watch [_ _ _]
@@ -1035,18 +552,17 @@
(defn clone-template
[{:keys [template-id project-id] :as params}]
- (dm/assert! (uuid? project-id))
(ptk/reify ::clone-template
ev/Event
(-data [_]
- {:template-id template-id
- :project-id project-id})
+ {:template-id template-id})
ptk/WatchEvent
- (watch [_ _ _]
+ (watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error rx/throw}} (meta params)]
+ on-error rx/throw}} (meta params)
+ project-id (or project-id (:current-project-id state))]
(->> (rp/cmd! ::sse/clone-template {:project-id project-id
:template-id template-id})
(rx/tap (fn [event]
@@ -1061,119 +577,6 @@
(rx/tap on-success)
(rx/catch on-error))))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Navigation
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(defn go-to-workspace
- [{:keys [id project-id] :as file}]
- (ptk/reify ::go-to-workspace
- ptk/WatchEvent
- (watch [_ _ _]
- (let [pparams {:project-id project-id :file-id id}]
- (rx/of (rt/nav :workspace pparams))))))
-
-
-(defn go-to-files
- ([project-id]
- (ptk/reify ::go-to-files-1
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-files {:team-id team-id
- :project-id project-id}))))))
- ([team-id project-id]
- (ptk/reify ::go-to-files-2
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (rt/nav :dashboard-files {:team-id team-id
- :project-id project-id}))))))
-
-(defn go-to-search
- ([] (go-to-search nil))
- ([term]
- (ptk/reify ::go-to-search
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (if (empty? term)
- (do
- (dom/focus! (dom/get-element "search-input"))
- (rx/of (rt/nav :dashboard-search
- {:team-id team-id})))
- (rx/of (rt/nav :dashboard-search
- {:team-id team-id}
- {:search-term term})))))
-
- ptk/EffectEvent
- (effect [_ _ _]
- (dom/focus! (dom/get-element "search-input"))))))
-
-(defn go-to-projects
- ([]
- (ptk/reify ::go-to-projects-0
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
- ([team-id]
- (ptk/reify ::go-to-projects-1
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
-
-(defn go-to-team-members
- []
- (ptk/reify ::go-to-team-members
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-team-members {:team-id team-id}))))))
-
-(defn go-to-team-invitations
- []
- (ptk/reify ::go-to-team-invitations
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-team-invitations {:team-id team-id}))))))
-
-(defn go-to-team-webhooks
- []
- (ptk/reify ::go-to-team-webhooks
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-team-webhooks {:team-id team-id}))))))
-
-(defn go-to-team-settings
- []
- (ptk/reify ::go-to-team-settings
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-team-settings {:team-id team-id}))))))
-
-(defn go-to-drafts
- []
- (ptk/reify ::go-to-drafts
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)
- projects (:dashboard-projects state)
- default-project (d/seek :is-default (vals projects))]
- (when default-project
- (rx/of (rt/nav :dashboard-files {:team-id team-id
- :project-id (:id default-project)})))))))
-
-(defn go-to-libs
- []
- (ptk/reify ::go-to-libs
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
-
(defn create-element
[]
(ptk/reify ::create-element
@@ -1184,10 +587,10 @@
pparams (:path-params route)
in-project? (contains? pparams :project-id)
name (if in-project?
- (let [files (get state :dashboard-files)
+ (let [files (get state :files)
unames (cfh/get-used-names files)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
- (let [projects (get state :dashboard-projects)
+ (let [projects (get state :projects)
unames (cfh/get-used-names projects)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
params (if in-project?
@@ -1197,7 +600,7 @@
:team-id team-id})
action-name (if in-project? :create-file :create-project)
action (if in-project? file-created project-created)
- can-edit? (dm/get-in state [:permissions :can-edit])]
+ can-edit? (dm/get-in state [:teams team-id :permissions :can-edit])]
(when can-edit?
(->> (rp/cmd! action-name params)
@@ -1208,10 +611,9 @@
(ptk/reify ::open-selected-file
ptk/WatchEvent
(watch [_ state _]
- (let [files (get-in state [:dashboard-local :selected-files])]
+ (let [[file-id :as files] (get state :selected-files)]
(if (= 1 (count files))
- (let [file (get-in state [:dashboard-files (first files)])]
- (rx/of (go-to-workspace file)))
+ (rx/of (dcm/go-to-workspace :file-id file-id))
(rx/empty))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1223,13 +625,13 @@
(ptk/reify ::handle-change-team-role
ptk/WatchEvent
(watch [_ _ _]
- (rx/of (dc/change-team-role params)
+ (rx/of (dcm/change-team-role params)
(modal/hide)))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
- :notification (dc/handle-notification msg)
+ :notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
- :team-membership-change (dc/team-membership-change msg)
+ :team-membership-change (dcm/team-membership-change msg)
nil))
diff --git a/frontend/src/app/main/data/dashboard/shortcuts.cljs b/frontend/src/app/main/data/dashboard/shortcuts.cljs
index 98d987c113..93183ef4b9 100644
--- a/frontend/src/app/main/data/dashboard/shortcuts.cljs
+++ b/frontend/src/app/main/data/dashboard/shortcuts.cljs
@@ -6,27 +6,28 @@
(ns app.main.data.dashboard.shortcuts
(:require
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
+ [app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
- [app.main.data.users :as du]
[app.main.store :as st]))
(def shortcuts
{:go-to-search {:tooltip (ds/meta "F")
:command (ds/c-mod "f")
:subsections [:navigation-dashboard]
- :fn #(st/emit! (dd/go-to-search))}
+ :fn #(st/emit! (dcm/go-to-dashboard-search))}
:go-to-drafts {:tooltip "G D"
:command "g d"
:subsections [:navigation-dashboard]
- :fn #(st/emit! (dd/go-to-drafts))}
+ :fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))}
:go-to-libs {:tooltip "G L"
:command "g l"
:subsections [:navigation-dashboard]
- :fn #(st/emit! (dd/go-to-libs))}
+ :fn #(st/emit! (dcm/go-to-dashboard-libraries))}
:create-new-project {:tooltip "+"
:command "+"
diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/event.cljs
similarity index 98%
rename from frontend/src/app/main/data/events.cljs
rename to frontend/src/app/main/data/event.cljs
index 27001909aa..ff5250be7e 100644
--- a/frontend/src/app/main/data/events.cljs
+++ b/frontend/src/app/main/data/event.cljs
@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
-(ns app.main.data.events
+(ns app.main.data.event
(:require
["ua-parser-js" :as ua]
[app.common.data :as d]
@@ -182,7 +182,7 @@
(rx/filter #(pos? (count %)))
(rx/debounce 2000))
(->> stream
- (rx/filter (ptk/type? :app.main.data.users/logout))
+ (rx/filter (ptk/type? :app.main.data.profile/logout))
(rx/observe-on :async)))
(rx/map (fn [_]
(into [] (take max-buffer-size) @buffer)))
diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs
index 9df77afdaf..ca1d411831 100644
--- a/frontend/src/app/main/data/exports/assets.cljs
+++ b/frontend/src/app/main/data/exports/assets.cljs
@@ -7,6 +7,7 @@
(ns app.main.data.exports.assets
(:require
[app.common.uuid :as uuid]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.persistence :as dwp]
[app.main.data.workspace.state-helpers :as wsh]
@@ -247,6 +248,12 @@
(rx/map #(clear-export-state @resource-id))
(rx/take-until (rx/delay 6000 stopper))))))))
+(defn request-export
+ [{:keys [exports] :as params}]
+ (if (= 1 (count exports))
+ (request-simple-export (assoc params :export (first exports)))
+ (request-multiple-export params)))
+
(defn retry-last-export
[]
(ptk/reify ::retry-last-export
@@ -256,3 +263,16 @@
(when (seq params)
(rx/of (request-multiple-export params)))))))
+(defn export-shapes-event
+ [exports origin]
+ (let [types (reduce (fn [counts {:keys [type]}]
+ (if (#{:png :pdf :svg :jpeg} type)
+ (update counts type inc)
+ counts))
+ {:png 0, :pdf 0, :svg 0, :jpeg 0}
+ exports)]
+ (ptk/event
+ ::ev/event (merge types
+ {::ev/name "export-shapes"
+ ::ev/origin origin
+ :num-shapes (count exports)}))))
diff --git a/frontend/src/app/main/data/exports/files.cljs b/frontend/src/app/main/data/exports/files.cljs
index 439dcaa97c..56ab281a70 100644
--- a/frontend/src/app/main/data/exports/files.cljs
+++ b/frontend/src/app/main/data/exports/files.cljs
@@ -10,7 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.repo :as rp]
diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs
index 87f6709035..e8503ea59e 100644
--- a/frontend/src/app/main/data/fonts.cljs
+++ b/frontend/src/app/main/data/fonts.cljs
@@ -12,7 +12,7 @@
[app.common.logging :as log]
[app.common.media :as cm]
[app.common.uuid :as uuid]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.fonts :as fonts]
[app.main.repo :as rp]
@@ -59,10 +59,10 @@
(adapt-font-id [variant]
(update variant :font-id #(str "custom-" %)))]
- (ptk/reify ::team-fonts-loaded
+ (ptk/reify ::fonts-loaded
ptk/UpdateEvent
(update [_ state]
- (assoc state :dashboard-fonts (d/index-by :id fonts)))
+ (assoc state :fonts (d/index-by :id fonts)))
ptk/EffectEvent
(effect [_ _ _]
@@ -72,13 +72,14 @@
(mapv prepare-font))]
(fonts/register! :custom fonts))))))
-(defn load-team-fonts
- [team-id]
+(defn fetch-fonts
+ []
(ptk/reify ::load-team-fonts
ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-font-variants {:team-id team-id})
- (rx/map fonts-fetched)))))
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-font-variants {:team-id team-id})
+ (rx/map fonts-fetched))))))
(defn process-upload
"Given a seq of blobs and the team id, creates a ready-to-use fonts
@@ -90,12 +91,15 @@
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))
- ;; Vertical metrics determine the baseline in a text and the space between lines of text.
- ;; For historical reasons, there are three pairs of ascender/descender values, known as hhea, OS/2 and uSWin metrics.
- ;; Depending on the font, operating system and application a different set will be used to render text on the screen.
- ;; On Mac, Safari and Chrome use the hhea values to render text. Firefox will respect the useTypoMetrics setting and will use the OS/2 if it is set.
- ;; If the useTypoMetrics is not set, Firefox will also use metrics from the hhea table.
- ;; On Windows, all browsers use the usWin metrics, but respect the useTypoMetrics setting and if set will use the OS/2 values.
+ ;; Vertical metrics determine the baseline in a text and the space between lines of
+ ;; text. For historical reasons, there are three pairs of ascender/descender
+ ;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
+ ;; system and application a different set will be used to render text on the
+ ;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
+ ;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
+ ;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
+ ;; table. On Windows, all browsers use the usWin metrics, but respect the
+ ;; useTypoMetrics setting and if set will use the OS/2 values.
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
@@ -239,7 +243,7 @@
(ptk/reify ::add-font
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-fonts assoc (:id font) font))
+ (update state :fonts assoc (:id font) font))
ptk/WatchEvent
(watch [_ state _]
@@ -260,13 +264,10 @@
(update [_ state]
;; Update all variants that has the same font-id with the new
;; name in the local state.
- (update state :dashboard-fonts
- (fn [fonts]
- (d/mapm (fn [_ font]
- (cond-> font
- (= id (:font-id font))
- (assoc :font-family name)))
- fonts))))
+ (update state :fonts update-vals (fn [font]
+ (cond-> font
+ (= id (:font-id font))
+ (assoc :font-family name)))))
ptk/WatchEvent
(watch [_ state _]
@@ -285,10 +286,11 @@
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-fonts
+ (update state :fonts
(fn [variants]
(d/removem (fn [[_id variant]]
(= (:font-id variant) font-id)) variants))))
+
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
@@ -305,7 +307,7 @@
(ptk/reify ::delete-font-variants
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-fonts
+ (update state :fonts
(fn [variants]
(d/removem (fn [[_ variant]]
(= (:id variant) id))
diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs
index 1055014c28..27500359f8 100644
--- a/frontend/src/app/main/data/modal.cljs
+++ b/frontend/src/app/main/data/modal.cljs
@@ -8,7 +8,7 @@
(:refer-clojure :exclude [update])
(:require
[app.common.uuid :as uuid]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.store :as st]
[cljs.core :as c]
[potok.v2.core :as ptk]))
diff --git a/frontend/src/app/main/data/notifications.cljs b/frontend/src/app/main/data/notifications.cljs
index c58fb4c603..d65ea5486b 100644
--- a/frontend/src/app/main/data/notifications.cljs
+++ b/frontend/src/app/main/data/notifications.cljs
@@ -64,7 +64,7 @@
(rx/merge
(let [stopper (rx/filter (ptk/type? ::hide) stream)]
(->> stream
- (rx/filter (ptk/type? :app.util.router/navigate))
+ (rx/filter (ptk/type? :app.main.router/navigate))
(rx/map (fn [_] (hide)))
(rx/take-until stopper)))
(when (:timeout data)
diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs
index 12112ca91c..759bc0fb97 100644
--- a/frontend/src/app/main/data/persistence.cljs
+++ b/frontend/src/app/main/data/persistence.cljs
@@ -52,7 +52,7 @@
(if-let [current-file-id (:current-file-id state)]
(if (= file-id current-file-id)
(update-in state [:workspace-file :revn] max revn)
- (d/update-in-when state [:workspace-libraries file-id :revn] max revn))
+ (d/update-in-when state [:libraries file-id :revn] max revn))
state))
ptk/EffectEvent
diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs
new file mode 100644
index 0000000000..6ae1641694
--- /dev/null
+++ b/frontend/src/app/main/data/profile.cljs
@@ -0,0 +1,457 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.data.profile
+ (:require
+ [app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.common.schema :as sm]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.config :as cf]
+ [app.main.data.event :as ev]
+ [app.main.data.media :as di]
+ [app.main.data.team :as-alias dtm]
+ [app.main.repo :as rp]
+ [app.main.router :as rt]
+ [app.plugins.register :as plugins.register]
+ [app.util.i18n :as i18n]
+ [app.util.storage :as storage]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]))
+
+(declare update-profile-props)
+
+;; --- SCHEMAS
+
+(def ^:private
+ schema:profile
+ [:map {:title "Profile"}
+ [:id ::sm/uuid]
+ [:created-at {:optional true} :any]
+ [:fullname {:optional true} :string]
+ [:email {:optional true} :string]
+ [:lang {:optional true} :string]
+ [:theme {:optional true} :string]])
+
+(def check-profile
+ (sm/check-fn schema:profile))
+
+;; --- HELPERS
+
+(defn is-authenticated?
+ [{:keys [id]}]
+ (and (uuid? id) (not= id uuid/zero)))
+
+;; --- EVENT: fetch-profile
+
+(defn set-profile
+ "Initialize profile state, only logged-in profile data should be
+ passed to this event"
+ [{:keys [id] :as profile}]
+ (ptk/reify ::set-profile
+ IDeref
+ (-deref [_] profile)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (assoc :profile-id id)
+ (assoc :profile profile)))
+
+ ptk/EffectEvent
+ (effect [_ state _]
+ (let [profile (:profile state)]
+ (swap! storage/user assoc :profile profile)
+ (i18n/set-locale! (:lang profile))
+ (plugins.register/init)))))
+
+(def profile-fetched?
+ (ptk/type? ::profile-fetched))
+
+(defn- on-fetch-profile-exception
+ [cause]
+ (let [data (ex-data cause)]
+ (if (and (= :authorization (:type data))
+ (= :challenge-required (:code data)))
+ (let [path (rt/get-current-path)
+ href (->> path
+ (js/encodeURIComponent)
+ (str "/challenge.html?redirect="))]
+ (rx/of (rt/nav-raw :href href)))
+ (rx/throw cause))))
+
+(defn fetch-profile
+ []
+ (ptk/reify ::fetch-profile
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :get-profile)
+ (rx/map (partial ptk/data-event ::profile-fetched))
+ (rx/catch on-fetch-profile-exception)))))
+
+(defn refresh-profile
+ []
+ (ptk/reify ::refresh-profile
+ ptk/WatchEvent
+ (watch [_ _ stream]
+ (rx/merge
+ (rx/of (fetch-profile))
+ (->> stream
+ (rx/filter profile-fetched?)
+ (rx/map deref)
+ (rx/filter is-authenticated?)
+ (rx/take 1)
+ (rx/map set-profile))))))
+
+;; --- Update Profile
+
+(defn persist-profile
+ [& {:as opts}]
+ (ptk/reify ::persist-profile
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [on-success (:on-success opts identity)
+ on-error (:on-error opts rx/throw)
+ profile (:profile state)
+ params (select-keys profile [:fullname :lang :theme])]
+ (->> (rp/cmd! :update-profile params)
+ (rx/tap on-success)
+ (rx/map set-profile)
+ (rx/catch on-error))))))
+
+(defn update-profile
+ "Optimistic update of the current profile.
+
+ Props are ignored because there is a specific event for updating
+ props"
+ [profile]
+ (dm/assert!
+ "expected valid profile data"
+ (check-profile profile))
+
+ (ptk/reify ::update-profile
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [profile' (get state :profile)
+ profile (d/deep-merge profile' (dissoc profile :props))]
+
+ (rx/merge
+ (rx/of (set-profile profile))
+
+ (when (not= (:theme profile)
+ (:theme profile'))
+ (rx/of (ptk/data-event ::ev/event
+ {::ev/name "activate-theme"
+ ::ev/origin "settings"
+ :theme (:theme profile)}))))))))
+
+;; --- Toggle Theme
+
+(defn toggle-theme
+ []
+ (ptk/reify ::toggle-theme
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:profile :theme]
+ (fn [current]
+ (if (= current "default")
+ "light"
+ "default"))))
+
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [profile (get state :profile)
+ origin (::ev/origin (meta it))]
+ (rx/of (ptk/data-event ::ev/event {:theme (:theme profile)
+ ::ev/name "activate-theme"
+ ::ev/origin origin})
+ (persist-profile))))))
+
+;; --- Request Email Change
+
+(defn request-email-change
+ [{:keys [email] :as data}]
+ (dm/assert! ::us/email email)
+ (ptk/reify ::request-email-change
+ ev/Event
+ (-data [_]
+ {:email email})
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)]
+ (->> (rp/cmd! :request-email-change data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- Cancel Email Change
+
+(def cancel-email-change
+ (ptk/reify ::cancel-email-change
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :cancel-email-change {})
+ (rx/map (constantly (refresh-profile)))))))
+
+;; --- Update Password (Form)
+
+(def schema:update-password
+ [:map {:closed true}
+ [:password-1 :string]
+ [:password-2 :string]
+ ;; Social registered users don't have old-password
+ [:password-old {:optional true} [:maybe :string]]])
+
+
+(defn update-password
+ [data]
+ (dm/assert!
+ "expected valid parameters"
+ (sm/check schema:update-password data))
+
+ (ptk/reify ::update-password
+ ev/Event
+ (-data [_] {})
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)
+ params {:old-password (:password-old data)
+ :password (:password-1 data)}]
+ (->> (rp/cmd! :update-profile-password params)
+ (rx/tap on-success)
+ (rx/catch (fn [err]
+ (on-error err)
+ (rx/empty)))
+ (rx/ignore))))))
+
+(defn update-profile-props
+ [props]
+ (ptk/reify ::update-profile-props
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:profile :props] merge props))
+
+ ;; TODO: for the release 1.13 we should skip fetching profile and just use
+ ;; the response value of update-profile-props RPC call
+ ;; FIXME
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :update-profile-props {:props props})
+ (rx/map (constantly (refresh-profile)))))))
+
+(defn mark-onboarding-as-viewed
+ ([] (mark-onboarding-as-viewed nil))
+ ([{:keys [version]}]
+ (ptk/reify ::mark-onboarding-as-viewed
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [version (or version (:main cf/version))
+ props {:onboarding-viewed true
+ :release-notes-viewed version}]
+ (->> (rp/cmd! :update-profile-props {:props props})
+ (rx/map (constantly (refresh-profile)))))))))
+
+(defn mark-questions-as-answered
+ [onboarding-questions]
+ (ptk/reify ::mark-questions-as-answered
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:profile :props] assoc :onboarding-questions-answered true))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [props {:onboarding-questions-answered true
+ :onboarding-questions onboarding-questions}]
+ (->> (rp/cmd! :update-profile-props {:props props})
+ (rx/map (constantly (refresh-profile))))))))
+
+;; --- Update Photo
+
+(defn update-photo
+ [file]
+ (dm/assert!
+ "expected a valid blob for `file` param"
+ (di/blob? file))
+
+ (ptk/reify ::update-photo
+ ev/Event
+ (-data [_] {})
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [on-success di/notify-finished-loading
+ on-error #(do (di/notify-finished-loading)
+ (di/process-error %))
+
+ prepare
+ (fn [file]
+ {:file file})]
+
+ (di/notify-start-loading)
+ (->> (rx/of file)
+ (rx/map di/validate-file)
+ (rx/map prepare)
+ (rx/mapcat #(rp/cmd! :update-profile-photo %))
+ (rx/tap on-success)
+ (rx/map (constantly (refresh-profile)))
+ (rx/catch on-error))))))
+
+(defn fetch-users
+ []
+ (letfn [(fetched [users state]
+ (->> users
+ (d/index-by :id)
+ (assoc state :users)))]
+ (ptk/reify ::fetch-team-users
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-team-users {:team-id team-id})
+ (rx/map #(partial fetched %))))))))
+
+(defn fetch-file-comments-users
+ [{:keys [team-id]}]
+ (dm/assert! (uuid? team-id))
+ (letfn [(fetched [users state]
+ (->> users
+ (d/index-by :id)
+ (assoc state :file-comments-users)))]
+ (ptk/reify ::fetch-file-comments-users
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [share-id (-> state :viewer-local :share-id)]
+ (->> (rp/cmd! :get-profiles-for-file-comments {:team-id team-id :share-id share-id})
+ (rx/map #(partial fetched %))))))))
+
+;; --- EVENT: request-account-deletion
+
+(def profile-deleted?
+ (ptk/type? ::profile-deleted))
+
+(defn request-account-deletion
+ [params]
+ (ptk/reify ::request-account-deletion
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta params)]
+ (->> (rp/cmd! :delete-profile {})
+ (rx/tap on-success)
+ (rx/map (fn [_]
+ (ptk/data-event ::profile-deleted params)))
+ (rx/catch on-error)
+ (rx/delay-at-least 300))))))
+
+;; --- EVENT: request-profile-recovery
+
+(def ^:private
+ schema:request-profile-recovery
+ [:map {:title "request-profile-recovery" :closed true}
+ [:email ::sm/email]])
+
+(defn request-profile-recovery
+ [data]
+
+ (dm/assert!
+ "expected valid parameters"
+ (sm/check schema:request-profile-recovery data))
+
+ (ptk/reify ::request-profile-recovery
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+
+ (->> (rp/cmd! :request-profile-recovery data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: recover-profile (Password)
+
+(def ^:private
+ schema:recover-profile
+ [:map {:title "recover-profile" :closed true}
+ [:password :string]
+ [:token :string]])
+
+(defn recover-profile
+ [data]
+ (dm/assert!
+ "expected valid arguments"
+ (sm/check schema:recover-profile data))
+
+ (ptk/reify ::recover-profile
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+ (->> (rp/cmd! :recover-profile data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: fetch-team-webhooks
+
+(defn access-tokens-fetched
+ [access-tokens]
+ (ptk/reify ::access-tokens-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :access-tokens access-tokens))))
+
+(defn fetch-access-tokens
+ []
+ (ptk/reify ::fetch-access-tokens
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :get-access-tokens)
+ (rx/map access-tokens-fetched)))))
+
+;; --- EVENT: create-access-token
+
+(defn access-token-created
+ [access-token]
+ (ptk/reify ::access-token-created
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :access-token-created access-token))))
+
+(defn create-access-token
+ [{:keys [] :as params}]
+ (ptk/reify ::create-access-token
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :create-access-token params)
+ (rx/map access-token-created)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: delete-access-token
+
+(defn delete-access-token
+ [{:keys [id] :as params}]
+ (us/assert! ::us/uuid id)
+ (ptk/reify ::delete-access-token
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :delete-access-token params)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
diff --git a/frontend/src/app/main/data/project.cljs b/frontend/src/app/main/data/project.cljs
new file mode 100644
index 0000000000..74a927d8d0
--- /dev/null
+++ b/frontend/src/app/main/data/project.cljs
@@ -0,0 +1,80 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.data.project
+ (:require
+ [app.common.data :as d]
+ [app.common.logging :as log]
+ [app.main.repo :as rp]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]))
+
+(log/set-level! :warn)
+
+(defn- project-fetched
+ [{:keys [id] :as project}]
+ (ptk/reify ::project-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:projects id] merge project))))
+
+(defn fetch-project
+ "Fetch or refresh a single project"
+ ([] (fetch-project))
+ ([project-id]
+ (assert (uuid? project-id) "expected a valid uuid for `project-id`")
+
+ (ptk/reify ::fetch-project
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [project-id (or project-id (:current-project-id state))]
+ (->> (rp/cmd! :get-project {:id project-id})
+ (rx/map project-fetched)))))))
+
+(defn initialize-project
+ [project-id]
+ (ptk/reify ::initialize-project
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :current-project-id project-id))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (fetch-project project-id)))))
+
+(defn finalize-project
+ [project-id]
+ (ptk/reify ::finalize-project
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [project-id' (get state :current-project-id)]
+ (if (= project-id' project-id)
+ (dissoc state :current-project-id)
+ state)))))
+
+
+(defn- files-fetched
+ [project-id files]
+ (ptk/reify ::files-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (update :files merge (d/index-by :id files))
+ (d/update-in-when [:projects project-id] (fn [project]
+ (assoc project :count (count files))))))))
+
+(defn fetch-files
+ [project-id]
+ (assert (uuid? project-id) "expected valid uuid for `project-id`")
+ (ptk/reify ::fetch-files
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :get-project-files {:project-id project-id})
+ (rx/map (partial files-fetched project-id))))))
+
+
+
+
diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs
new file mode 100644
index 0000000000..b145b07cc8
--- /dev/null
+++ b/frontend/src/app/main/data/team.cljs
@@ -0,0 +1,559 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.data.team
+ (:require
+ [app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.common.logging :as log]
+ [app.common.schema :as sm]
+ [app.common.types.team :as ctt]
+ [app.common.uri :as u]
+ [app.config :as cf]
+ [app.main.data.event :as ev]
+ [app.main.data.media :as di]
+ [app.main.features :as features]
+ [app.main.repo :as rp]
+ [app.main.router :as rt]
+ [app.util.storage :as storage]
+ [app.util.webapi :as wapi]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]))
+
+(log/set-level! :warn)
+
+(defn get-last-team-id
+ "Get last accessed team id"
+ []
+ (::current-team-id storage/global))
+
+(defn teams-fetched
+ [teams]
+ (ptk/reify ::teams-fetched
+ IDeref
+ (-deref [_] teams)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (reduce (fn [state {:keys [id] :as team}]
+ (update-in state [:teams id] merge team))
+ state
+ teams))))
+
+(defn fetch-teams
+ []
+ (ptk/reify ::fetch-teams
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :get-teams)
+ (rx/map teams-fetched)))))
+
+;; --- EVENT: fetch-members
+
+(defn- members-fetched
+ [team-id members]
+ (ptk/reify ::members-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (update-in [:teams team-id] assoc :members members)
+ (update :profiles merge (d/index-by :id members))))))
+
+(defn fetch-members
+ []
+ (ptk/reify ::fetch-members
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-team-members {:team-id team-id})
+ (rx/map (partial members-fetched team-id)))))))
+
+(defn- invitations-fetched
+ [team-id invitations]
+ (ptk/reify ::invitations-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:teams team-id] assoc :invitations invitations))))
+
+(defn fetch-invitations
+ []
+ (ptk/reify ::fetch-invitations
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-team-invitations {:team-id team-id})
+ (rx/map (partial invitations-fetched team-id)))))))
+
+(defn set-current-team
+ [{:keys [id permissions features] :as team}]
+ (ptk/reify ::set-current-team
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ ;; FIXME: redundant operation, only necessary on workspace
+ ;; until workspace initialization is refactored
+ (update-in [:teams id] merge team)
+ (assoc :permissions permissions)
+ ;; FIXME: this is a redundant operation that only needed by
+ ;; workspace; ti will not be needed after workspace
+ ;; bootstrap & urls refactor
+ (assoc :current-team-id id)))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (features/initialize (or features #{}))))
+
+ ptk/EffectEvent
+ (effect [_ _ _]
+ (swap! storage/global assoc ::current-team-id id))))
+
+(defn- team-initialized
+ []
+ (ptk/reify ::team-initialized
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ teams (get state :teams)
+ team (get teams team-id)]
+ (rx/of (set-current-team team)
+ (fetch-members))))))
+
+(defn initialize-team
+ [team-id]
+ (ptk/reify ::initialize-team
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :current-team-id team-id))
+
+ ptk/WatchEvent
+ (watch [_ _ stream]
+ (let [stopper (rx/filter (ptk/type? ::finalize-team) stream)]
+ (->> (rx/merge
+ (rx/of (fetch-teams))
+ (->> stream
+ (rx/filter (ptk/type? ::teams-fetched))
+ (rx/observe-on :async)
+ (rx/map team-initialized)))
+ (rx/take-until stopper))))))
+
+(defn finalize-team
+ [team-id]
+ (ptk/reify ::finalize-team
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [team-id' (get state :current-team-id)]
+ (if (= team-id' team-id)
+ (-> state
+ (dissoc :current-team-id)
+ (dissoc :shared-files)
+ (dissoc :fonts))
+ state)))))
+
+;; --- ROLES
+
+(defn update-member-role
+ [{:keys [role member-id] :as params}]
+ (dm/assert! (uuid? member-id))
+ (dm/assert! (contains? ctt/valid-roles role))
+
+ (ptk/reify ::update-member-role
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/cmd! :update-team-member-role params)
+ (rx/mapcat (fn [_]
+ (rx/of (fetch-members)
+ (fetch-teams)
+ (ptk/data-event ::ev/event
+ {::ev/name "update-team-member-role"
+ :team-id team-id
+ :role role
+ :member-id member-id})))))))))
+
+(defn delete-member
+ [{:keys [member-id] :as params}]
+ (dm/assert! (uuid? member-id))
+ (ptk/reify ::delete-member
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/cmd! :delete-team-member params)
+ (rx/mapcat (fn [_]
+ (rx/of (fetch-members)
+ (fetch-teams)
+ (ptk/data-event ::ev/event
+ {::ev/name "delete-team-member"
+ :team-id team-id
+ :member-id member-id})))))))))
+
+
+(defn- stats-fetched
+ [team-id stats]
+ (ptk/reify ::stats-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:teams team-id] assoc :stats stats))))
+
+(defn fetch-stats
+ []
+ (ptk/reify ::fetch-stats
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-team-stats {:team-id team-id})
+ (rx/map (partial stats-fetched team-id)))))))
+
+(defn- webhooks-fetched
+ [team-id webhooks]
+ (ptk/reify ::webhooks-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:team-id team-id] assoc :webhooks webhooks))))
+
+(defn fetch-webhooks
+ []
+ (ptk/reify ::fetch-webhooks
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-webhooks {:team-id team-id})
+ (rx/map (partial webhooks-fetched team-id)))))))
+
+(defn- shared-files-fetched
+ [files]
+ (ptk/reify ::shared-files-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [files (d/index-by :id files)]
+ (assoc state :shared-files files)))))
+
+(defn fetch-shared-files
+ "Event mainly used for fetch a list of shared libraries for a team,
+ this list does not includes the content of the library per se. It
+ is used mainly for show available libraries and a summary of it."
+ []
+ (ptk/reify ::fetch-shared-files
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/cmd! :get-team-shared-files {:team-id team-id})
+ (rx/map shared-files-fetched))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Data Modification
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn update-team-photo
+ [file]
+ (dm/assert!
+ "expected a valid blob for `file` param"
+ (di/blob? file))
+ (ptk/reify ::update-team-photo
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [on-success di/notify-finished-loading
+ on-error #(do (di/notify-finished-loading)
+ (di/process-error %))
+ team-id (:current-team-id state)
+ prepare #(hash-map :file % :team-id team-id)]
+
+ (di/notify-start-loading)
+ (->> (rx/of file)
+ (rx/map di/validate-file)
+ (rx/map prepare)
+ (rx/mapcat #(rp/cmd! :update-team-photo %))
+ (rx/tap on-success)
+ (rx/mapcat (fn [_]
+ (rx/of (fetch-teams)
+ (ptk/data-event ::ev/event
+ {::ev/name "update-team-photo"
+ :team-id team-id}))))
+ (rx/catch on-error))))))
+
+
+;; --- EVENT: create-team
+
+(defn- team-created
+ [team]
+ (ptk/reify ::team-created
+ IDeref
+ (-deref [_] team)))
+
+(defn create-team
+ [{:keys [name] :as params}]
+ (dm/assert! (string? name))
+ (ptk/reify ::create-team
+ ptk/WatchEvent
+ (watch [it state _]
+ (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}]
+ (->> (rp/cmd! :create-team (with-meta params (meta it)))
+ (rx/tap on-success)
+ (rx/map team-created)
+ (rx/catch on-error))))))
+
+;; --- EVENT: create-team-with-invitations
+
+(defn create-team-with-invitations
+ [{:keys [name emails role] :as params}]
+ (ptk/reify ::create-team-with-invitations
+ ptk/WatchEvent
+ (watch [it state _]
+ (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
+ :emails emails
+ :role role
+ :features features}]
+ (->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
+ (rx/tap on-success)
+ (rx/map team-created)
+ (rx/catch on-error))))))
+
+(defn update-team
+ [{:keys [id name] :as params}]
+ (ptk/reify ::update-team
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc-in state [:teams id :name] name))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :update-team params)
+ (rx/ignore)))))
+
+(defn- team-leaved
+ [{:keys [id] :as params}]
+ (ptk/reify ::team-leaved
+ IDeref
+ (-deref [_] params)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :teams dissoc id))
+
+ ptk/EffectEvent
+ (effect [_ state _]
+ (let [teams (get state :teams)]
+ (when-let [ctid (::current-team-id storage/user)]
+ (when-not (contains? teams ctid)
+ (swap! storage/user dissoc ::current-team-id)))))))
+
+(defn leave-current-team
+ "High-level event for leave team, mainly executed from the
+ dashboard. It automatically redirects user to the default team, once
+ the team-leave operation succeed"
+ [{:keys [reassign-to] :as params}]
+
+ (when reassign-to
+ (assert (uuid? reassign-to) "expect a valid uuid for `reassign-to`"))
+
+ (ptk/reify ::leave-current-team
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (get state :current-team-id)
+ params (assoc params :id team-id)
+
+ {:keys [on-error on-success]
+ :or {on-success rx/empty
+ on-error rx/throw}}
+ (meta params)]
+
+ (->> (rp/cmd! :leave-team params)
+ (rx/mapcat
+ (fn [_]
+ (rx/merge
+ (rx/of (team-leaved params)
+ (fetch-teams)
+ (ptk/data-event ::ev/event
+ {::ev/name "leave-team"
+ :reassign-to reassign-to
+ :team-id team-id}))
+ (on-success))))
+ (rx/catch on-error))))))
+
+(defn create-invitations
+ [{:keys [emails role team-id resend?] :as params}]
+ (dm/assert! (keyword? role))
+ (dm/assert! (uuid? team-id))
+
+ (dm/assert!
+ "expected a valid set of emails"
+ (sm/check-set-of-emails! emails))
+
+ (ptk/reify ::create-invitations
+ ev/Event
+ (-data [_]
+ {:role role
+ :team-id team-id
+ :resend resend?})
+
+ ptk/WatchEvent
+ (watch [it _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)
+ params (dissoc params :resend?)]
+ (->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+(defn copy-invitation-link
+ [{:keys [email team-id] :as params}]
+ (dm/assert!
+ "expected a valid email"
+ (sm/check-email! email))
+
+ (dm/assert! (uuid? team-id))
+
+ (ptk/reify ::copy-invitation-link
+ IDeref
+ (-deref [_] {:email email :team-id team-id})
+
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)
+ router (:router state)]
+
+ (->> (rp/cmd! :get-team-invitation-token params)
+ (rx/map (fn [params]
+ (rt/resolve router :auth-verify-token params)))
+ (rx/map (fn [fragment]
+ (assoc cf/public-uri :fragment fragment)))
+ (rx/tap (fn [uri]
+ (wapi/write-to-clipboard (str uri))))
+ (rx/tap on-success)
+ (rx/ignore)
+ (rx/catch on-error))))))
+
+(defn update-invitation-role
+ [{:keys [email team-id role] :as params}]
+ (dm/assert!
+ "expected a valid email"
+ (sm/check-email! email))
+
+ (dm/assert! (uuid? team-id))
+ (dm/assert! (contains? ctt/valid-roles role))
+
+ (ptk/reify ::update-invitation-role
+ IDeref
+ (-deref [_] {:role role})
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :update-team-invitation-role params)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+(defn delete-invitation
+ [{:keys [email team-id] :as params}]
+ (dm/assert! (sm/check-email! email))
+ (dm/assert! (uuid? team-id))
+ (ptk/reify ::delete-invitation
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :delete-team-invitation params)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+(defn delete-team
+ [{:keys [id] :as params}]
+ (ptk/reify ::delete-team
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success rx/empty
+ on-error rx/throw}}
+ (meta params)]
+
+ (->> (rp/cmd! :delete-team {:id id})
+ (rx/mapcat on-success)
+ (rx/catch on-error))))))
+
+(defn delete-webhook
+ [{:keys [id] :as params}]
+ (dm/assert! (uuid? id))
+
+ (ptk/reify ::delete-webhook
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)
+ {:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :delete-webhook params)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+(def valid-mtypes
+ #{"application/json"
+ "application/x-www-form-urlencoded"
+ "application/transit+json"})
+
+(defn update-webhook
+ [{:keys [id uri mtype is-active] :as params}]
+ (dm/assert! (uuid? id))
+ (dm/assert! (contains? valid-mtypes mtype))
+ (dm/assert! (boolean? is-active))
+ (dm/assert! (u/uri? uri))
+
+ (ptk/reify ::update-webhook
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)
+ {:keys [on-success on-error]
+ :or {on-success rx/empty
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :update-webhook params)
+ (rx/mapcat (fn [_]
+ (rx/concat
+ (on-success)
+ (rx/of (fetch-webhooks)))))
+ (rx/catch on-error))))))
+
+(defn create-webhook
+ [{:keys [uri mtype is-active] :as params}]
+ (dm/assert! (contains? valid-mtypes mtype))
+ (dm/assert! (boolean? is-active))
+ (dm/assert! (u/uri? uri))
+
+ (ptk/reify ::create-webhook
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (:current-team-id state)
+ params (-> params
+ (assoc :team-id team-id)
+ (update :uri str))
+ {:keys [on-success on-error]
+ :or {on-success rx/empty
+ on-error rx/throw}} (meta params)]
+ (->> (rp/cmd! :create-webhook params)
+ (rx/mapcat (fn [_]
+ (rx/concat
+ (on-success)
+ (rx/of (fetch-webhooks)))))
+ (rx/catch on-error))))))
+
+
+
+
diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs
index d0bbc890b5..1700335b51 100644
--- a/frontend/src/app/main/data/tokens.cljs
+++ b/frontend/src/app/main/data/tokens.cljs
@@ -68,6 +68,14 @@
(->> (ctob/set-name-string->prefixed-set-path-string token-set-name)
(wtts/assoc-selected-token-set-path state)))))
+(defn set-selected-token-set-id-from-name
+ [token-set-name]
+ (ptk/reify ::set-selected-token-set-id-from-name
+ ptk/UpdateEvent
+ (update [_ state]
+ (->> (ctob/set-name->set-path-string token-set-name)
+ (wtts/assoc-selected-token-set-path state)))))
+
(defn create-token-theme [token-theme]
(let [new-token-theme token-theme]
(ptk/reify ::create-token-theme
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
deleted file mode 100644
index 237cdde463..0000000000
--- a/frontend/src/app/main/data/users.cljs
+++ /dev/null
@@ -1,743 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) KALEIDOS INC
-
-(ns app.main.data.users
- (:require
- [app.common.data :as d]
- [app.common.data.macros :as dm]
- [app.common.exceptions :as ex]
- [app.common.schema :as sm]
- [app.common.spec :as us]
- [app.common.uuid :as uuid]
- [app.config :as cf]
- [app.main.data.events :as ev]
- [app.main.data.media :as di]
- [app.main.data.notifications :as ntf]
- [app.main.data.websocket :as ws]
- [app.main.features :as features]
- [app.main.repo :as rp]
- [app.plugins.register :as register]
- [app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
- [app.util.storage :as storage]
- [beicon.v2.core :as rx]
- [potok.v2.core :as ptk]))
-
-(declare update-profile-props)
-
-;; --- SCHEMAS
-
-(def ^:private
- schema:profile
- [:map {:title "Profile"}
- [:id ::sm/uuid]
- [:created-at {:optional true} :any]
- [:fullname {:optional true} :string]
- [:email {:optional true} :string]
- [:lang {:optional true} :string]
- [:theme {:optional true} :string]])
-
-(def check-profile!
- (sm/check-fn schema:profile))
-
-;; --- HELPERS
-
-(defn is-authenticated?
- [{:keys [id]}]
- (and (uuid? id) (not= id uuid/zero)))
-
-(defn get-current-team-id
- [profile]
- (let [team-id (::current-team-id storage/user)]
- (or team-id (:default-team-id profile))))
-
-(defn set-current-team!
- [team-id]
- (if (nil? team-id)
- (swap! storage/user dissoc ::current-team-id)
- (swap! storage/user assoc ::current-team-id team-id)))
-
-;; --- EVENT: fetch-teams
-
-(defn teams-fetched
- [teams]
- (ptk/reify ::teams-fetched
- IDeref
- (-deref [_] teams)
-
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :teams (d/index-by :id teams)))
-
- ptk/EffectEvent
- (effect [_ _ _]
- ;; Check if current team-id is part of available teams
- ;; if not, dissoc it from storage.
-
- (let [ids (into #{} (map :id) teams)]
- (when-let [ctid (::current-team-id storage/user)]
- (when-not (contains? ids ctid)
- (swap! storage/user dissoc ::current-team-id)))))))
-
-(defn fetch-teams
- []
- (ptk/reify ::fetch-teams
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-teams)
- (rx/map teams-fetched)))))
-
-(defn set-current-team
- [team]
- (ptk/reify ::set-current-team
- ptk/UpdateEvent
- (update [_ state]
- (-> state
- (assoc :team team)
- (assoc :permissions (:permissions team))
- (assoc :current-team-id (:id team))))
-
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (features/initialize (:features team #{}))))
-
- ptk/EffectEvent
- (effect [_ _ _]
- (set-current-team! (:id team)))))
-
-;; --- EVENT: fetch-profile
-
-(declare logout)
-
-(def profile-fetched?
- (ptk/type? ::profile-fetched))
-
-(defn profile-fetched
- [{:keys [id] :as profile}]
- (ptk/reify ::profile-fetched
- IDeref
- (-deref [_] profile)
-
- ptk/UpdateEvent
- (update [_ state]
- (cond-> state
- (is-authenticated? profile)
- (-> (assoc :profile-id id)
- (assoc :profile profile))))
-
- ptk/EffectEvent
- (effect [_ state _]
- (let [profile (:profile state)
- email (:email profile)
- previous-profile (:profile storage/user)
- previous-email (:email previous-profile)]
- (when profile
- (swap! storage/user assoc :profile profile)
- (i18n/set-locale! (:lang profile))
- (when (not= previous-email email)
- (set-current-team! nil))
-
- (register/init))))))
-
-(defn- on-fetch-profile-exception
- [cause]
- (let [data (ex-data cause)]
- (if (and (= :authorization (:type data))
- (= :challenge-required (:code data)))
- (let [path (rt/get-current-path)
- href (->> path
- (js/encodeURIComponent)
- (str "/challenge.html?redirect="))]
- (rx/of (rt/nav-raw :href href)))
- (rx/throw cause))))
-
-(defn fetch-profile
- []
- (ptk/reify ::fetch-profile
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-profile)
- (rx/map profile-fetched)
- (rx/catch on-fetch-profile-exception)))))
-
-;; --- EVENT: login
-
-(defn- logged-in
- "This is the main event that is executed once we have logged in
- profile. The profile can proceed from standard login or from
- accepting invitation, or third party auth signup or singin."
- [profile]
- (letfn [(get-redirect-events []
- (let [team-id (get-current-team-id profile)
- welcome-file-id (dm/get-in profile [:props :welcome-file-id])
- redirect-href (:login-redirect @storage/session)
- current-href (rt/get-current-href)]
-
- (cond
- (some? redirect-href)
- (binding [storage/*sync* true]
- (swap! storage/session dissoc :login-redirect)
- (if (= current-href redirect-href)
- (rx/of (rt/reload true))
- (rx/of (rt/nav-raw :href redirect-href))))
-
- (some? welcome-file-id)
- (rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
- :file-id welcome-file-id})
- (update-profile-props {:welcome-file-id nil}))
-
- :else
- (rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
-
- (ptk/reify ::logged-in
- ev/Event
- (-data [_]
- {::ev/name "signin"
- ::ev/type "identify"
- :email (:email profile)
- :auth-backend (:auth-backend profile)
- :fullname (:fullname profile)
- :is-muted (:is-muted profile)
- :default-team-id (:default-team-id profile)
- :default-project-id (:default-project-id profile)})
-
- ptk/WatchEvent
- (watch [_ _ _]
- (when (is-authenticated? profile)
- (->> (rx/concat
- (rx/of (profile-fetched profile)
- (fetch-teams)
- (ws/initialize))
- (get-redirect-events))
- (rx/observe-on :async)))))))
-
-(declare login-from-register)
-
-(defn login
- [{:keys [email password invitation-token] :as data}]
- (ptk/reify ::login
- ptk/WatchEvent
- (watch [_ _ stream]
- (let [{:keys [on-error on-success]
- :or {on-error rx/throw
- on-success identity}} (meta data)
-
- params {:email email
- :password password
- :invitation-token invitation-token}]
-
- ;; NOTE: We can't take the profile value from login because
- ;; there are cases when login is successful but the cookie is
- ;; not set properly (because of possible misconfiguration).
- ;; So, we proceed to make an additional call to fetch the
- ;; profile, and ensure that cookie is set correctly. If
- ;; profile fetch is successful, we mark the user logged in, if
- ;; the returned profile is an NOT authenticated profile, we
- ;; proceed to logout and show an error message.
-
- (->> (rp/cmd! :login-with-password (d/without-nils params))
- (rx/merge-map (fn [data]
- (rx/merge
- (rx/of (fetch-profile))
- (->> stream
- (rx/filter profile-fetched?)
- (rx/take 1)
- (rx/map deref)
- (rx/filter (complement is-authenticated?))
- (rx/tap on-error)
- (rx/map #(ex/raise :type :authentication))
- (rx/observe-on :async))
-
- (->> stream
- (rx/filter profile-fetched?)
- (rx/take 1)
- (rx/map deref)
- (rx/filter is-authenticated?)
- (rx/map (fn [profile]
- (with-meta (merge data profile)
- {::ev/source "login"})))
- (rx/tap on-success)
- (rx/map logged-in)
- (rx/observe-on :async)))))
- (rx/catch on-error))))))
-
-(def ^:private schema:login-with-ldap
- [:map {:title "login-with-ldap"}
- [:email ::sm/email]
- [:password :string]])
-
-(defn login-with-ldap
- [params]
-
- (dm/assert!
- "expected valid params"
- (sm/check schema:login-with-ldap params))
-
- (ptk/reify ::login-with-ldap
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error rx/throw
- on-success identity}} (meta params)]
- (->> (rp/cmd! :login-with-ldap params)
- (rx/tap on-success)
- (rx/map (fn [profile]
- (-> profile
- (with-meta {::ev/source "login-with-ldap"})
- (logged-in))))
- (rx/catch on-error))))))
-
-(defn login-from-token
- "Used mainly as flow continuation after token validation."
- [{:keys [profile] :as tdata}]
- (ptk/reify ::login-from-token
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
- ;; NOTE: we need this to be asynchronous because the effect
- ;; should be called before proceed with the login process
- (rx/observe-on :async)))
-
- ptk/EffectEvent
- (effect [_ _ _]
- (set-current-team! nil))))
-
-(defn login-from-register
- "Event used mainly for mark current session as logged-in in after the
- user successfully registered using third party auth provider (in this
- case we dont need to verify the email)."
- []
- (ptk/reify ::login-from-register
- ptk/WatchEvent
- (watch [_ _ stream]
- (rx/merge
- (rx/of (fetch-profile))
- (->> stream
- (rx/filter (ptk/type? ::profile-fetched))
- (rx/take 1)
- (rx/map deref)
- (rx/map (fn [profile]
- (with-meta profile
- {::ev/source "register"})))
- (rx/map logged-in)
- (rx/observe-on :async))))))
-
-;; --- EVENT: logout
-
-(defn logged-out
- ([] (logged-out {}))
- ([_params]
- (ptk/reify ::logged-out
- ptk/UpdateEvent
- (update [_ state]
- (select-keys state [:route :router :session-id :history]))
-
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/merge
- ;; NOTE: We need the `effect` of the current event to be
- ;; executed before the redirect.
- (->> (rx/of (rt/nav :auth-login))
- (rx/observe-on :async))
- (rx/of (ws/finalize))))
-
- ptk/EffectEvent
- (effect [_ _ _]
- ;; We prefer to keek some stuff in the storage like the current-team-id and the profile
- (swap! storage/user (constantly {}))))))
-
-(defn logout
- ([] (logout {}))
- ([params]
- (ptk/reify ::logout
- ev/Event
- (-data [_] {})
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [profile-id (:profile-id state)]
- (->> (rx/interval 500)
- (rx/take 1)
- (rx/mapcat (fn [_]
- (->> (rp/cmd! :logout {:profile-id profile-id})
- (rx/delay-at-least 300)
- (rx/catch (constantly (rx/of 1))))))
- (rx/map #(logged-out params))))))))
-
-;; --- Update Profile
-
-(defn persist-profile
- [& {:as opts}]
- (ptk/reify ::persist-profile
- ptk/WatchEvent
- (watch [_ state _]
- (let [on-success (:on-success opts identity)
- on-error (:on-error opts rx/throw)
- profile (:profile state)
- params (select-keys profile [:fullname :lang :theme])]
- (->> (rp/cmd! :update-profile params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(defn update-profile
- [data]
- (dm/assert!
- "expected valid profile data"
- (check-profile! data))
-
- (ptk/reify ::update-profile
- ptk/WatchEvent
- (watch [_ state _]
- (let [data (dissoc data :props)
- profile (:profile state)
- profile' (d/deep-merge profile data)]
-
- (rx/concat
- (rx/of #(assoc % :profile profile'))
-
- (when (not= (:theme profile) (:theme profile'))
- (rx/of (ptk/data-event ::ev/event
- {::ev/name "activate-theme"
- ::ev/origin "settings"
- :theme (:theme profile')}))))))))
-
-;; --- Toggle Theme
-
-(defn toggle-theme
- []
- (ptk/reify ::toggle-theme
- ptk/UpdateEvent
- (update [_ state]
- (update-in state [:profile :theme]
- (fn [current]
- (if (= current "default")
- "light"
- "default"))))
-
- ptk/WatchEvent
- (watch [it state _]
- (let [profile (get state :profile)
- origin (::ev/origin (meta it))]
- (rx/of (ptk/data-event ::ev/event {:theme (:theme profile)
- ::ev/name "activate-theme"
- ::ev/origin origin})
- (persist-profile))))))
-
-;; --- Request Email Change
-
-(defn request-email-change
- [{:keys [email] :as data}]
- (dm/assert! ::us/email email)
- (ptk/reify ::request-email-change
- ev/Event
- (-data [_]
- {:email email})
-
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)]
- (->> (rp/cmd! :request-email-change data)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- Cancel Email Change
-
-(def cancel-email-change
- (ptk/reify ::cancel-email-change
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :cancel-email-change {})
- (rx/map (constantly (fetch-profile)))))))
-
-;; --- Update Password (Form)
-
-(def schema:update-password
- [:map {:closed true}
- [:password-1 :string]
- [:password-2 :string]
- ;; Social registered users don't have old-password
- [:password-old {:optional true} [:maybe :string]]])
-
-
-(defn update-password
- [data]
- (dm/assert!
- "expected valid parameters"
- (sm/check schema:update-password data))
-
- (ptk/reify ::update-password
- ev/Event
- (-data [_] {})
-
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)
- params {:old-password (:password-old data)
- :password (:password-1 data)}]
- (->> (rp/cmd! :update-profile-password params)
- (rx/tap on-success)
- (rx/catch (fn [err]
- (on-error err)
- (rx/empty)))
- (rx/ignore))))))
-
-(defn update-profile-props
- [props]
- (ptk/reify ::update-profile-props
- ptk/UpdateEvent
- (update [_ state]
- (update-in state [:profile :props] merge props))
-
- ;; TODO: for the release 1.13 we should skip fetching profile and just use
- ;; the response value of update-profile-props RPC call
- ;; FIXME
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :update-profile-props {:props props})
- (rx/map (constantly (fetch-profile)))))))
-
-(defn mark-onboarding-as-viewed
- ([] (mark-onboarding-as-viewed nil))
- ([{:keys [version]}]
- (ptk/reify ::mark-onboarding-as-viewed
- ptk/WatchEvent
- (watch [_ _ _]
- (let [version (or version (:main cf/version))
- props {:onboarding-viewed true
- :release-notes-viewed version}]
- (->> (rp/cmd! :update-profile-props {:props props})
- (rx/map (constantly (fetch-profile)))))))))
-
-(defn mark-questions-as-answered
- [onboarding-questions]
- (ptk/reify ::mark-questions-as-answered
- ptk/UpdateEvent
- (update [_ state]
- (update-in state [:profile :props] assoc :onboarding-questions-answered true))
-
- ptk/WatchEvent
- (watch [_ _ _]
- (let [props {:onboarding-questions-answered true
- :onboarding-questions onboarding-questions}]
- (->> (rp/cmd! :update-profile-props {:props props})
- (rx/map (constantly (fetch-profile))))))))
-
-;; --- Update Photo
-
-(defn update-photo
- [file]
- (dm/assert!
- "expected a valid blob for `file` param"
- (di/blob? file))
-
- (ptk/reify ::update-photo
- ev/Event
- (-data [_] {})
-
- ptk/WatchEvent
- (watch [_ _ _]
- (let [on-success di/notify-finished-loading
- on-error #(do (di/notify-finished-loading)
- (di/process-error %))
-
- prepare
- (fn [file]
- {:file file})]
-
- (di/notify-start-loading)
- (->> (rx/of file)
- (rx/map di/validate-file)
- (rx/map prepare)
- (rx/mapcat #(rp/cmd! :update-profile-photo %))
- (rx/tap on-success)
- (rx/map (constantly (fetch-profile)))
- (rx/catch on-error))))))
-
-(defn fetch-users
- []
- (letfn [(fetched [users state]
- (->> users
- (d/index-by :id)
- (assoc state :users)))]
- (ptk/reify ::fetch-team-users
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (->> (rp/cmd! :get-team-users {:team-id team-id})
- (rx/map #(partial fetched %))))))))
-
-(defn fetch-file-comments-users
- [{:keys [team-id]}]
- (dm/assert! (uuid? team-id))
- (letfn [(fetched [users state]
- (->> users
- (d/index-by :id)
- (assoc state :file-comments-users)))]
- (ptk/reify ::fetch-file-comments-users
- ptk/WatchEvent
- (watch [_ state _]
- (let [share-id (-> state :viewer-local :share-id)]
- (->> (rp/cmd! :get-profiles-for-file-comments {:team-id team-id :share-id share-id})
- (rx/map #(partial fetched %))))))))
-
-;; --- EVENT: request-account-deletion
-
-(defn request-account-deletion
- [params]
- (ptk/reify ::request-account-deletion
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error rx/throw
- on-success identity}} (meta params)]
- (->> (rp/cmd! :delete-profile {})
- (rx/tap on-success)
- (rx/map logged-out)
- (rx/catch on-error)
- (rx/delay-at-least 300))))))
-
-;; --- EVENT: request-profile-recovery
-
-(def ^:private
- schema:request-profile-recovery
- [:map {:title "request-profile-recovery" :closed true}
- [:email ::sm/email]])
-
-(defn request-profile-recovery
- [data]
-
- (dm/assert!
- "expected valid parameters"
- (sm/check schema:request-profile-recovery data))
-
- (ptk/reify ::request-profile-recovery
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error rx/throw
- on-success identity}} (meta data)]
-
- (->> (rp/cmd! :request-profile-recovery data)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- EVENT: recover-profile (Password)
-
-(def ^:private
- schema:recover-profile
- [:map {:title "recover-profile" :closed true}
- [:password :string]
- [:token :string]])
-
-(defn recover-profile
- [data]
- (dm/assert!
- "expected valid arguments"
- (sm/check schema:recover-profile data))
-
- (ptk/reify ::recover-profile
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-error on-success]
- :or {on-error rx/throw
- on-success identity}} (meta data)]
- (->> (rp/cmd! :recover-profile data)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- EVENT: crete-demo-profile
-
-(defn create-demo-profile
- []
- (ptk/reify ::create-demo-profile
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :create-demo-profile {})
- (rx/map login)))))
-
-;; --- EVENT: fetch-team-webhooks
-
-(defn access-tokens-fetched
- [access-tokens]
- (ptk/reify ::access-tokens-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :access-tokens access-tokens))))
-
-(defn fetch-access-tokens
- []
- (ptk/reify ::fetch-access-tokens
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-access-tokens)
- (rx/map access-tokens-fetched)))))
-
-;; --- EVENT: create-access-token
-
-(defn access-token-created
- [access-token]
- (ptk/reify ::access-token-created
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :access-token-created access-token))))
-
-(defn create-access-token
- [{:keys [] :as params}]
- (ptk/reify ::create-access-token
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :create-access-token params)
- (rx/map access-token-created)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- EVENT: delete-access-token
-
-(defn delete-access-token
- [{:keys [id] :as params}]
- (us/assert! ::us/uuid id)
- (ptk/reify ::delete-access-token
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error rx/throw}} (meta params)]
- (->> (rp/cmd! :delete-access-token params)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-(defn show-redirect-error
- "A helper event that interprets the OIDC redirect errors on the URI
- and shows an appropriate error message using the notification
- banners."
- [error]
- (ptk/reify ::show-redirect-error
- ptk/WatchEvent
- (watch [_ _ _]
- (when-let [hint (case error
- "registration-disabled"
- (tr "errors.registration-disabled")
- "profile-blocked"
- (tr "errors.profile-blocked")
- "auth-provider-not-allowed"
- (tr "errors.auth-provider-not-allowed")
- "email-domain-not-allowed"
- (tr "errors.email-domain-not-allowed")
-
- ;; We explicitly do not show any error here, it a explicit user operation.
- "unable-to-auth"
- nil
-
- (tr "errors.generic"))]
-
- (rx/of (ntf/warn hint))))))
diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs
index f247e70f50..409c1819d0 100644
--- a/frontend/src/app/main/data/viewer.cljs
+++ b/frontend/src/app/main/data/viewer.cljs
@@ -15,13 +15,15 @@
[app.common.transit :as t]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.interactions :as ctsi]
- [app.main.data.comments :as dcm]
- [app.main.data.events :as ev]
+ [app.common.uuid :as uuid]
+ [app.main.data.comments :as dcmt]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.features :as features]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.util.globals :as ug]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -32,7 +34,7 @@
{:zoom 1
:fullscreen? false
:interactions-mode :show-on-click
- :interactions-show? false
+ :show-interactions false
:comments-mode :all
:comments-show :unresolved
:selected #{}
@@ -55,7 +57,7 @@
[:page-id {:optional true} ::sm/uuid]])
(defn initialize
- [{:keys [file-id share-id interactions-show?] :as params}]
+ [{:keys [file-id share-id] :as params}]
(dm/assert!
"expected valid params"
(sm/check schema:initialize params))
@@ -65,13 +67,13 @@
(update [_ state]
(-> state
(assoc :current-file-id file-id)
+ (assoc :current-share-id share-id)
(update :viewer-local
(fn [lstate]
(if (nil? lstate)
default-local-state
lstate)))
- (assoc-in [:viewer-local :share-id] share-id)
- (assoc-in [:viewer-local :interactions-show?] interactions-show?)))
+ (assoc-in [:viewer-local :share-id] share-id)))
ptk/WatchEvent
(watch [_ state _]
@@ -256,12 +258,9 @@
ptk/WatchEvent
(watch [_ state _]
(let [zoom-type (get-in state [:viewer-local :zoom-type])
- route (:route state)
- screen (-> route :data :name keyword)
- qparams (:query-params route)
- pparams (:path-params route)]
+ params (rt/get-params state)]
- (rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type)))))))
+ (rx/of (rt/nav :viewer (assoc params :zoom zoom-type)))))))
(def increase-zoom
(ptk/reify ::increase-zoom
@@ -293,14 +292,17 @@
(ptk/reify ::zoom-to-fit
ptk/UpdateEvent
(update [_ state]
- (let [srect (as-> (get-in state [:route :query-params :page-id]) %
- (get-in state [:viewer :pages % :frames])
- (nth % (get-in state [:route :query-params :index]))
- (get % :selrect))
- orig-size (get-in state [:viewer-local :viewport-size])
- wdiff (/ (:width orig-size) (:width srect))
- hdiff (/ (:height orig-size) (:height srect))
- minzoom (min wdiff hdiff)]
+ (let [params (rt/get-params state)
+ page-id (some-> (:page-id params) uuid/parse)
+ index (some-> (:index params) parse-long)
+
+ frames (dm/get-in state [:viewer :pages page-id :frames])
+ srect (-> (nth frames index)
+ (get :selrect))
+ osize (dm/get-in state [:viewer-local :viewport-size])
+ wdiff (/ (:width osize) (:width srect))
+ hdiff (/ (:height osize) (:height srect))
+ minzoom (min wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] minzoom)
(assoc-in [:viewer-local :zoom-type] :fit))))
@@ -312,17 +314,25 @@
(ptk/reify ::zoom-to-fill
ptk/UpdateEvent
(update [_ state]
- (let [srect (as-> (get-in state [:route :query-params :page-id]) %
- (get-in state [:viewer :pages % :frames])
- (nth % (get-in state [:route :query-params :index]))
- (get % :selrect))
- orig-size (get-in state [:viewer-local :viewport-size])
- wdiff (/ (:width orig-size) (:width srect))
- hdiff (/ (:height orig-size) (:height srect))
- maxzoom (max wdiff hdiff)]
+
+ (let [params (rt/get-params state)
+ page-id (some-> (:page-id params) uuid/parse)
+ index (some-> (:index params) parse-long)
+
+ frames (dm/get-in state [:viewer :pages page-id :frames])
+ srect (-> (nth frames index)
+ (get :selrect))
+
+ osize (dm/get-in state [:viewer-local :viewport-size])
+
+ wdiff (/ (:width osize) (:width srect))
+ hdiff (/ (:height osize) (:height srect))
+
+ maxzoom (max wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] maxzoom)
(assoc-in [:viewer-local :zoom-type] :fill))))
+
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
@@ -376,16 +386,15 @@
(-> state
(dissoc :viewer-animations)
(assoc :viewer-overlays [])))
+
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- qparams (:query-params route)
- pparams (:path-params route)
- index (:index qparams)]
+ (let [params (rt/get-params state)
+ index (some-> params :index parse-long)]
(when (pos? index)
(rx/of
- (dcm/close-thread)
- (rt/nav :viewer pparams (assoc qparams :index (dec index)))))))))
+ (dcmt/close-thread)
+ (rt/nav :viewer (assoc params :index (dec index)))))))))
(def select-next-frame
(ptk/reify ::select-next-frame
@@ -396,30 +405,25 @@
(assoc :viewer-overlays [])))
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- pparams (:path-params route)
- qparams (:query-params route)
-
- page-id (:page-id qparams)
- index (:index qparams)
+ (let [params (rt/get-params state)
+ index (some-> params :index parse-long)
+ page-id (some-> params :page-id parse-uuid)
total (count (get-in state [:viewer :pages page-id :frames]))]
(when (< index (dec total))
(rx/of
- (dcm/close-thread)
- (rt/nav :viewer pparams (assoc qparams :index (inc index)))))))))
+ (dcmt/close-thread)
+ (rt/nav :viewer params (assoc params :index (inc index)))))))))
(def select-first-frame
(ptk/reify ::select-first-frame
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- qparams (:query-params route)
- pparams (:path-params route)]
+ (let [params (rt/get-params state)]
(rx/of
- (dcm/close-thread)
- (rt/nav :viewer pparams (assoc qparams :index 0)))))))
+ (dcmt/close-thread)
+ (rt/nav :viewer (assoc params :index 0)))))))
(def valid-interaction-modes
#{:hide :show :show-on-click})
@@ -434,17 +438,14 @@
(update [_ state]
(-> state
(assoc-in [:viewer-local :interactions-mode] mode)
- (assoc-in [:viewer-local :interactions-show?] (case mode
- :hide false
- :show true
- :show-on-click false))))
+ (assoc-in [:viewer-local :show-interactions] (case mode
+ :hide false
+ :show true
+ :show-on-click false))))
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- screen (-> route :data :name keyword)
- qparams (:query-params route)
- pparams (:path-params route)]
- (rx/of (rt/nav screen pparams (assoc qparams :interactions-mode mode)))))))
+ (let [params (rt/get-params state)]
+ (rx/of (rt/nav :viewer (assoc params :interactions-mode mode)))))))
(declare flash-done)
@@ -453,7 +454,7 @@
(ptk/reify ::flash-interactions
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:viewer-local :interactions-show?] true))
+ (assoc-in state [:viewer-local :show-interactions] true))
ptk/WatchEvent
(watch [_ _ stream]
@@ -466,7 +467,7 @@
(ptk/reify ::flash-done
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:viewer-local :interactions-show?] false))))
+ (assoc-in state [:viewer-local :show-interactions] false))))
(defn set-nav-scroll
[scroll]
@@ -500,11 +501,8 @@
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- screen (-> route :data :name keyword)
- qparams (:query-params route)
- pparams (:path-params route)]
- (rx/of (rt/nav screen pparams (assoc qparams :index index)))))))
+ (let [params (rt/get-params state)]
+ (rx/of (rt/nav :viewer (assoc params :index index)))))))
(defn go-to-frame
([frame-id]
@@ -573,10 +571,8 @@
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- pparams (:path-params route)
- qparams (:query-params route)]
- (rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
+ (let [params (rt/get-params state)]
+ (rx/of (rt/nav :viewer (assoc params :section section)))))))
;; --- Overlays
@@ -771,9 +767,8 @@
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state _]
- (let [team-id (get-in state [:viewer :project :team-id])
- params {:team-id team-id}]
- (rx/of (rt/nav :dashboard-projects params))))))
+ (let [team-id (get-in state [:viewer :project :team-id])]
+ (rx/of (dcm/go-to-dashboard-recent :team-id team-id))))))
(defn go-to-page
[page-id]
@@ -784,13 +779,10 @@
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- pparams (:path-params route)
- qparams (-> (:query-params route)
- (assoc :index 0)
- (assoc :page-id page-id))
- rname (get-in route [:data :name])]
- (rx/of (rt/nav rname pparams qparams))))))
+ (let [params (-> (rt/get-params state)
+ (assoc :index 0)
+ (assoc :page-id page-id))]
+ (rx/of (rt/nav :viewer params))))))
(defn go-to-workspace
([] (go-to-workspace nil))
@@ -798,14 +790,16 @@
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state _]
- (let [route (:route state)
- project-id (get-in state [:viewer :project :id])
- file-id (get-in state [:viewer :file :id])
- saved-page-id (get-in route [:query-params :page-id])
- pparams {:project-id project-id :file-id file-id}
- qparams {:page-id (or page-id saved-page-id)}]
- (rx/of (rt/nav-new-window*
- {:rname :workspace
- :path-params pparams
- :query-params qparams
- :name (str "workspace-" file-id)})))))))
+ (let [params (rt/get-params state)
+ file-id (get-in state [:viewer :file :id])
+ team-id (get-in state [:viewer :project :team-id])
+ page-id (or page-id
+ (some-> (:page-id params) uuid/parse))
+ params {:page-id page-id
+ :file-id file-id
+ :team-id team-id}
+ name (dm/str "workspace-" file-id)]
+
+ (rx/of (rt/nav :workspace params
+ ::rt/new-window true
+ ::rt/window-name name)))))))
diff --git a/frontend/src/app/main/data/viewer/shortcuts.cljs b/frontend/src/app/main/data/viewer/shortcuts.cljs
index f6ac67296c..3fa3165eae 100644
--- a/frontend/src/app/main/data/viewer/shortcuts.cljs
+++ b/frontend/src/app/main/data/viewer/shortcuts.cljs
@@ -6,6 +6,7 @@
(ns app.main.data.viewer.shortcuts
(:require
+ [app.main.data.common :as dcm]
[app.main.data.shortcuts :as ds]
[app.main.data.viewer :as dv]
[app.main.store :as st]))
@@ -69,7 +70,7 @@
:open-workspace {:tooltip "G W"
:command "g w"
:subsections [:navigation-viewer]
- :fn #(st/emit! (dv/go-to-workspace))}})
+ :fn #(st/emit! (dcm/go-to-workspace))}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index f309e7ea58..c289ec5ca9 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -36,16 +36,18 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
- [app.main.data.comments :as dcm]
- [app.main.data.events :as ev]
+ [app.main.data.comments :as dcmt]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.persistence :as dps]
[app.main.data.plugins :as dp]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
+ [app.main.data.project :as dpj]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.collapse :as dwco]
+ [app.main.data.workspace.colors :as dwcl]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.fix-broken-shapes :as fbs]
@@ -65,6 +67,7 @@
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
+ [app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.thumbnails :as dwth]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
@@ -74,6 +77,7 @@
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.render-wasm :as wasm]
@@ -81,8 +85,8 @@
[app.util.globals :as ug]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.storage :as storage]
+ [app.util.text.content :as tc]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
@@ -100,12 +104,15 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized)
+(declare ^:private fetch-libraries)
(declare ^:private libraries-fetched)
-(declare go-to-layout)
+(declare ^:private preload-data-uris)
+
+;; (declare go-to-layout)
;; --- Initialize Workspace
-(defn initialize-layout
+(defn initialize-workspace-layout
[lname]
(ptk/reify ::initialize-layout
ptk/UpdateEvent
@@ -120,121 +127,6 @@
(rx/of (layout/ensure-layout lname))
(rx/of (layout/ensure-layout :layers))))))
-(defn- workspace-initialized
- []
- (ptk/reify ::workspace-initialized
- ptk/UpdateEvent
- (update [_ state]
- (-> state
- (assoc :workspace-undo {})
- (assoc :workspace-ready? true)))
-
- ptk/WatchEvent
- (watch [_ state _]
- (rx/of
- (when (and (not (boolean (-> state :profile :props :v2-info-shown)))
- (features/active-feature? state "components/v2"))
- (modal/show :v2-info {}))
- (dp/check-open-plugin)
- (fdf/fix-deleted-fonts)
- (fbs/fix-broken-shapes)))))
-
-(defn- workspace-data-loaded
- [data]
- (ptk/reify ::workspace-data-loaded
- ptk/UpdateEvent
- (update [_ state]
- (let [data (d/removem (comp t/pointer? val) data)]
- (assoc state :workspace-data data)))))
-
-(defn- bundle-fetched
- [{:keys [features file thumbnails project team team-users comments-users]}]
- (ptk/reify ::bundle-fetched
- ptk/UpdateEvent
- (update [_ state]
- (-> state
- (assoc :users (d/index-by :id team-users))
- (assoc :workspace-thumbnails thumbnails)
- (assoc :workspace-file (dissoc file :data))
- (assoc :workspace-project project)
- (assoc :current-file-comments-users (d/index-by :id comments-users))))
-
- ptk/WatchEvent
- (watch [_ _ stream]
- (let [team-id (:id team)
- file-id (:id file)
- stopper (rx/filter (ptk/type? ::bundle-fetched) stream)]
-
- (->> (rx/concat
- ;; Initialize notifications
- (rx/of (dwn/initialize team-id file-id)
- (dwsl/initialize))
-
- ;; Load team fonts. We must ensure custom fonts are
- ;; fully loadad before mark workspace as initialized
- (rx/merge
- (->> stream
- (rx/filter (ptk/type? ::df/team-fonts-loaded))
- (rx/take 1)
- (rx/ignore))
-
- (rx/of (df/load-team-fonts team-id))
-
- ;; FIXME: move to bundle fetch stages
-
- ;; Load main file
- (->> (fpmap/resolve-file file)
- (rx/map :data)
- (rx/mapcat (fn [{:keys [pages-index] :as data}]
- (->> (rx/from (seq pages-index))
- (rx/mapcat
- (fn [[id page]]
- (let [page (update page :objects ctst/start-page-index)]
- (->> (uw/ask! {:cmd :initialize-page-index :page page})
- (rx/map (fn [_] [id page]))))))
- (rx/reduce conj {})
- (rx/map (fn [pages-index]
- (assoc data :pages-index pages-index))))))
- (rx/map workspace-data-loaded))
-
- ;; Load libraries
- (->> (rp/cmd! :get-file-libraries {:file-id file-id})
- (rx/mapcat identity)
- (rx/merge-map
- (fn [{:keys [id synced-at]}]
- (->> (rp/cmd! :get-file {:id id :features features})
- (rx/map #(assoc % :synced-at synced-at)))))
- (rx/merge-map fpmap/resolve-file)
- (rx/merge-map
- (fn [{:keys [id] :as file}]
- (->> (rp/cmd! :get-file-object-thumbnails {:file-id id :tag "component"})
- (rx/map #(assoc file :thumbnails %)))))
- (rx/reduce conj [])
- (rx/map libraries-fetched)))
-
- (rx/of (with-meta (workspace-initialized)
- {:file-id file-id})))
- (rx/take-until stopper))))))
-
-(defn- libraries-fetched
- [libraries]
- (ptk/reify ::libraries-fetched
- ptk/UpdateEvent
- (update [_ state]
- (assoc state :workspace-libraries (d/index-by :id libraries)))
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [file-id (dm/get-in state [:workspace-file :id])
- ignore-until (dm/get-in state [:workspace-file :ignore-sync-until])
- needs-check? (some #(and (> (:modified-at %) (:synced-at %))
- (or (not ignore-until)
- (> (:modified-at %) ignore-until)))
- libraries)]
- (when needs-check?
- (rx/concat (rx/timer 1000)
- (rx/of (dwl/notify-sync-file file-id))))))))
-
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
@@ -243,7 +135,7 @@
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
-(defn- fetch-file-object-thumbnails
+(defn- get-file-object-thumbnails
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
@@ -258,181 +150,254 @@
(rx/of [k v])))))))
(rx/reduce conj {})))
-(defn- fetch-bundle-stage-1
- [project-id file-id]
- (ptk/reify ::fetch-bundle-stage-1
+(defn- resolve-file
+ [file]
+ (->> (fpmap/resolve-file file)
+ (rx/map :data)
+ (rx/mapcat
+ (fn [{:keys [pages-index] :as data}]
+ (->> (rx/from (seq pages-index))
+ (rx/mapcat
+ (fn [[id page]]
+ (let [page (update page :objects ctst/start-page-index)]
+ (->> (uw/ask! {:cmd :initialize-page-index :page page})
+ (rx/map (fn [_] [id page]))))))
+ (rx/reduce conj {})
+ (rx/map (fn [pages-index]
+ (let [data (assoc data :pages-index pages-index)]
+ (assoc file :data (d/removem (comp t/pointer? val) data))))))))))
+
+(defn- libraries-fetched
+ [libraries]
+ (ptk/reify ::libraries-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :libraries (d/index-by :id libraries)))
+
ptk/WatchEvent
- (watch [_ _ stream]
- (->> (rp/cmd! :get-project {:id project-id})
- (rx/mapcat (fn [project]
- (rx/concat
- ;; Wait the wasm module to be loaded or failed to
- ;; load. We need to wait the promise to be resolved
- ;; before continue with the next workspace loading
- ;; steps
- (->> (rx/from wasm/module)
- (rx/ignore))
- (->> (rp/cmd! :get-team {:id (:team-id project)})
- (rx/mapcat (fn [team]
- (let [bundle {:team team
- :project project
- :file-id file-id
- :project-id project-id}]
- (rx/of (du/set-current-team team)
- (ptk/data-event ::bundle-stage-1 bundle)))))))))
- (rx/take-until
- (rx/filter (ptk/type? ::fetch-bundle) stream))))))
+ (watch [_ state _]
+ (let [file-id (dm/get-in state [:workspace-file :id])
+ ignore-until (dm/get-in state [:workspace-file :ignore-sync-until])
+ needs-check? (some #(and (> (:modified-at %) (:synced-at %))
+ (or (not ignore-until)
+ (> (:modified-at %) ignore-until)))
+ libraries)]
+ (when needs-check?
+ (rx/concat (rx/timer 1000)
+ (rx/of (dwl/notify-sync-file file-id))))))))
-(defn- fetch-bundle-stage-2
- [{:keys [file-id project-id] :as bundle}]
- (ptk/reify ::fetch-bundle-stage-2
+(defn- fetch-libraries
+ [file-id]
+ (ptk/reify ::fetch-libries
ptk/WatchEvent
- (watch [_ state stream]
- (let [features (features/get-team-enabled-features state)
+ (watch [_ state _]
+ (let [features (features/get-team-enabled-features state)]
+ (->> (rp/cmd! :get-file-libraries {:file-id file-id})
+ (rx/mapcat
+ (fn [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)))))
+ (rx/merge-map resolve-file)
+ (rx/reduce conj [])
+ (rx/map libraries-fetched))
+ (->> (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))))))))))
- ;; WTF is this?
- share-id (-> state :viewer-local :share-id)]
- (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id})
- (fetch-file-object-thumbnails file-id)
- (rp/cmd! :get-team-users {:file-id file-id})
- (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
- (rx/take 1)
- (rx/map (fn [[file thumbnails team-users comments-users]]
- (let [bundle (-> bundle
- (assoc :file file)
- (assoc :features features)
- (assoc :thumbnails thumbnails)
- (assoc :team-users team-users)
- (assoc :comments-users comments-users))]
- (ptk/data-event ::bundle-stage-2 bundle))))
- (rx/take-until
- (rx/filter (ptk/type? ::fetch-bundle) stream)))))))
+(defn- workspace-initialized
+ []
+ (ptk/reify ::workspace-initialized
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (assoc :workspace-undo {})
+ (assoc :workspace-ready true)))
-(declare go-to-component)
+ ptk/WatchEvent
+ (watch [_ state _]
+ (rx/of
+ (when (and (not (boolean (-> state :profile :props :v2-info-shown)))
+ (features/active-feature? state "components/v2"))
+ (modal/show :v2-info {}))
+ (dp/check-open-plugin)
+ (fdf/fix-deleted-fonts)
+ (fbs/fix-broken-shapes)))))
+
+(defn- bundle-fetched
+ [{:keys [features file thumbnails]}]
+ (ptk/reify ::bundle-fetched
+ IDeref
+ (-deref [_]
+ {:features features
+ :file file
+ :thumbnails thumbnails})
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (assoc :thumbnails thumbnails)
+ (assoc :workspace-file (dissoc file :data))
+ (assoc :workspace-data (:data 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))))))
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
- [project-id file-id]
+ [file-id]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state stream]
- (->> (rx/merge
- (rx/of (fetch-bundle-stage-1 project-id file-id))
+ (let [features (features/get-team-enabled-features state)
+ render-wasm? (contains? features "render-wasm/v1")
+ stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
- (->> stream
- (rx/filter (ptk/type? ::bundle-stage-1))
- (rx/observe-on :async)
- (rx/map deref)
- (rx/map fetch-bundle-stage-2))
+ (->> (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? ::bundle-stage-2))
- (rx/observe-on :async)
- (rx/map deref)
- (rx/map bundle-fetched))
+ (->> stream
+ (rx/filter (ptk/type? ::df/fonts-loaded))
+ (rx/take 1)
+ (rx/ignore))
+ (rx/of (df/fetch-fonts)))
- (when-let [component-id (get-in state [:route :query-params :component-id])]
- (->> stream
- (rx/filter (ptk/type? ::workspace-initialized))
- (rx/observe-on :async)
+ ;; 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/map #(go-to-component (uuid/uuid component-id))))))
+ (rx/mapcat
+ (fn [[file thumbnails]]
+ (->> (resolve-file file)
+ (rx/map (fn [file]
+ {:file file
+ :features features
+ :thumbnails thumbnails})))))
+ (rx/map bundle-fetched)))
- (rx/take-until
- (rx/filter (ptk/type? ::fetch-bundle) stream))))))
+ (rx/take-until stopper-s))))))
-(defn initialize-file
- [project-id file-id]
- (dm/assert! (uuid? project-id))
- (dm/assert! (uuid? file-id))
+(defn initialize-workspace
+ [file-id]
+ (assert (uuid? file-id) "expected valud uuid for `file-id`")
- (ptk/reify ::initialize-file
+ (ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
(assoc state
:recent-colors (:recent-colors storage/user)
- :workspace-ready? false
+ :workspace-ready false
:current-file-id file-id
- :current-project-id project-id
:workspace-presence {}))
ptk/WatchEvent
- (watch [_ _ stream]
- (log/debug :hint "initialize-file" :file-id file-id)
- (let [stoper-s (rx/filter (ptk/type? ::finalize-file) stream)]
- (rx/merge
- (rx/of (ntf/hide)
- ;; We initialize the features without knowning the
- ;; team specific features in this step.
- (features/initialize)
- (dcm/retrieve-comment-threads file-id)
- (fetch-bundle project-id file-id))
+ (watch [_ state stream]
+ (log/debug :hint "initialize-workspace" :file-id file-id)
+ (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
+ rparams (rt/get-params state)]
- (->> stream
- (rx/filter dch/commit?)
- (rx/map deref)
- (rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
- (if (and save-undo? (seq undo-changes))
- (let [entry {:undo-changes undo-changes
- :redo-changes redo-changes
- :undo-group undo-group
- :tags tags}]
- (rx/of (dwu/append-undo entry stack-undo?)))
- (rx/empty))))
+ (->> (rx/merge
+ (rx/of (ntf/hide)
+ (dcmt/retrieve-comment-threads file-id)
+ (dcmt/fetch-profiles)
+ (fetch-bundle file-id))
- (rx/take-until stoper-s)))))
+ (->> 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)
+ (with-meta {:file-id file-id}))))))
+
+ (when-let [component-id (some-> rparams :component-id parse-uuid)]
+ (->> stream
+ (rx/filter (ptk/type? ::workspace-initialized))
+ (rx/observe-on :async)
+ (rx/take 1)
+ (rx/map #(dwl/go-to-local-component :id component-id))))
+
+ (->> stream
+ (rx/filter dch/commit?)
+ (rx/map deref)
+ (rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
+ (if (and save-undo? (seq undo-changes))
+ (let [entry {:undo-changes undo-changes
+ :redo-changes redo-changes
+ :undo-group undo-group
+ :tags tags}]
+ (rx/of (dwu/append-undo entry stack-undo?)))
+ (rx/empty))))))
+ (rx/take-until stoper-s))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
-(defn reload-file
- []
- (ptk/reify ::reload-file
- ptk/WatchEvent
- (watch [_ state _]
- (let [file-id (:current-file-id state)
- project-id (:current-project-id state)]
- (rx/of (initialize-file project-id file-id))))))
-
-;; We need to inject this so there are no cycles
-(set! app.main.data.workspace.notifications/reload-file reload-file)
-(set! app.main.errors/reload-file reload-file)
-
-(defn finalize-file
- [_project-id file-id]
+(defn finalize-workspace
+ [file-id]
(ptk/reify ::finalize-file
ptk/UpdateEvent
(update [_ state]
(-> state
(dissoc
:current-file-id
- :current-project-id
:workspace-data
:workspace-editor-state
:workspace-file
- :workspace-libraries
+ :libraries
:workspace-media-objects
:workspace-persistence
:workspace-presence
- :workspace-project
- :workspace-ready?
+ :workspace-ready
:workspace-undo)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)))
ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (dwn/finalize file-id)
- (dwsl/finalize)))))
+ (watch [_ state _]
+ (let [project-id (:current-project-id state)]
-(declare go-to-page)
-(declare ^:private preload-data-uris)
+ (rx/of (dwn/finalize file-id)
+ (dpj/finalize-project project-id)
+ (dwsl/finalize-shape-layout)
+ (dwcl/stop-picker)
+ (modal/hide)
+ (ntf/hide))))))
+
+(defn- reload-current-file
+ []
+ (ptk/reify ::reload-current-file
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)]
+ (rx/of (initialize-workspace file-id))))))
+
+;; Make this event callable through dynamic resolution
+(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
(defn initialize-page
[page-id]
- (dm/assert! (uuid? page-id))
+ (assert (uuid? page-id) "expected valid uuid for `page-id`")
+
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
@@ -447,30 +412,20 @@
;; FIXME: this should be done on `initialize-layout` (?)
(update :workspace-layout layout/load-layout-flags)
- (update :workspace-global layout/load-layout-state)
-
- (update :workspace-global assoc :background-color (-> page :options :background))
- (update-in [:route :params :query] assoc :page-id (dm/str id))))
+ (update :workspace-global layout/load-layout-state)))
state))
ptk/WatchEvent
(watch [_ state _]
- ;; NOTE: there are cases between files navigation when this
- ;; event is emmited but the page-index is still not loaded, so
- ;; we only need to proceed when page-index is properly loaded
- (when-let [pindex (-> state :workspace-data :pages-index)]
- (if (contains? pindex page-id)
- (let [file-id (:current-file-id state)]
- (rx/of (preload-data-uris page-id)
- (dwth/watch-state-changes file-id page-id)
- (dwl/watch-component-changes)))
- (let [page-id (dm/get-in state [:workspace-data :pages 0])]
- (rx/of (go-to-page page-id))))))))
+ (let [file-id (:current-file-id state)]
+ (rx/of (preload-data-uris page-id)
+ (dwth/watch-state-changes file-id page-id)
+ (dwl/watch-component-changes))))))
(defn finalize-page
[page-id]
- (dm/assert! (uuid? page-id))
+ (assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::finalize-page
ptk/UpdateEvent
(update [_ state]
@@ -617,7 +572,7 @@
(let [file-data
(if (= file-id (:current-file-id state))
(:workspace-data state)
- (get-in state [:workspace-libraries file-id :data]))
+ (get-in state [:libraries file-id :data]))
changes
(-> (pcb/empty-changes it)
@@ -666,7 +621,7 @@
(rx/of (dch/commit-changes changes)
(when (= id (:current-page-id state))
- go-to-file))))))
+ (go-to-file)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WORKSPACE File Actions
@@ -830,7 +785,7 @@
(let [selected (wsh/lookup-selected state)
id (first selected)]
(when (= (count selected) 1)
- (rx/of (go-to-layout :layers)
+ (rx/of (dcm/go-to-workspace :layout :layers)
(start-rename-shape id)))))))
;; --- Shape Vertical Ordering
@@ -1094,76 +1049,17 @@
(rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true)))
(rx/of (dwsh/update-shapes selected #(update % :proportion-lock not))))))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Navigation
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
(defn workspace-focus-lost
[]
(ptk/reify ::workspace-focus-lost
ptk/UpdateEvent
(update [_ state]
+ ;; FIXME: remove the `?` from show-distances?
(assoc-in state [:workspace-global :show-distances?] false))))
-(defn navigate-to-project
- [project-id]
- (ptk/reify ::navigate-to-project
- ptk/WatchEvent
- (watch [_ state _]
- (let [page-ids (get-in state [:projects project-id :pages])
- params {:project project-id :page (first page-ids)}]
- (rx/of (rt/nav :workspace/page params))))))
-
-(defn go-to-page
- ([]
- (ptk/reify ::go-to-page
- ptk/WatchEvent
- (watch [_ state _]
- (let [project-id (:current-project-id state)
- file-id (:current-file-id state)
- page-id (get-in state [:workspace-data :pages 0])
-
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id}]
- (rx/of (rt/nav' :workspace pparams qparams))))))
- ([page-id]
- (dm/assert! (uuid? page-id))
- (ptk/reify ::go-to-page-2
- ptk/WatchEvent
- (watch [_ state _]
- (let [project-id (:current-project-id state)
- file-id (:current-file-id state)
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id}]
- (rx/of (rt/nav :workspace pparams qparams)))))))
-
-(defn go-to-layout
- [layout]
- (ptk/reify ::go-to-layout
- IDeref
- (-deref [_] {:layout layout})
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [project-id (get-in state [:workspace-project :id])
- file-id (get-in state [:workspace-file :id])
- page-id (get state :current-page-id)
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id :layout (name layout)}]
- (rx/of (rt/nav :workspace pparams qparams))))))
-
-(defn navigate-to-library
- "Open a new tab, and navigate to the workspace with the provided file"
- [library-id]
- (ptk/reify ::navigate-to-file
- ptk/WatchEvent
- (watch [_ state _]
- (when-let [file (dm/get-in state [:workspace-libraries library-id])]
- (let [params {:rname :workspace
- :path-params {:project-id (:project-id file)
- :file-id (:id file)}
- :query-params {:page-id (dm/get-in file [:data :pages 0])}}]
- (rx/of (rt/nav-new-window* params)))))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Navigation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-assets-section-open
[file-id section open?]
@@ -1225,110 +1121,18 @@
(update-in state [:workspace-assets :selected] dissoc file-id)
(update state :workspace-assets dissoc :selected))))))
-(defn go-to-main-instance
- [file-id component-id]
- (dm/assert!
- "expected uuid type for `file-id` parameter (nilable)"
- (or (nil? file-id)
- (uuid? file-id)))
-
- (dm/assert!
- "expected uuid type for `component-id` parameter"
- (uuid? component-id))
-
- (ptk/reify ::go-to-main-instance
- ptk/WatchEvent
- (watch [_ state stream]
- (let [current-file-id (:current-file-id state)
- current-page-id (:current-page-id state)
- current-project-id (:current-project-id state)
- file-id (or file-id current-file-id)
-
- select-and-zoom
- (fn [shape-id]
- (rx/of (dws/select-shapes (d/ordered-set shape-id))
- dwz/zoom-to-selected-shape))
-
- redirect-to-page
- (fn [page-id shape-id]
- (rx/concat
- (rx/of (go-to-page page-id))
- (->> stream
- (rx/filter (ptk/type? ::initialize-page))
- (rx/take 1)
- (rx/observe-on :async))
- (select-and-zoom shape-id)))
-
- redirect-to-file
- (fn [file-id page-id]
- (let [pparams {:file-id file-id :project-id current-project-id}
- qparams {:page-id page-id}]
- (rx/merge
- (rx/of (rt/nav :workspace pparams qparams))
- (->> stream
- (rx/filter (ptk/type? ::workspace-initialized))
- (rx/map meta)
- (rx/filter #(= file-id (:file-id %)))
- (rx/take 1)
- (rx/observe-on :async)
- (rx/map #(go-to-main-instance file-id component-id))))))]
-
- (if (= file-id current-file-id)
- (let [component (dm/get-in state [:workspace-data :components component-id])
- page-id (:main-instance-page component)
- shape-id (:main-instance-id component)]
- (when (some? page-id)
- (if (= page-id current-page-id)
- (select-and-zoom shape-id)
- (redirect-to-page page-id shape-id))))
-
- (let [component (dm/get-in state [:workspace-libraries file-id :data :components component-id])]
- (some->> (:main-instance-page component)
- (redirect-to-file file-id))))))))
-
-(defn go-to-component
- [component-id]
- (ptk/reify ::go-to-component
- IDeref
- (-deref [_] {:layout :assets})
-
- ptk/WatchEvent
- (watch [_ state _]
- (let [components-v2 (features/active-feature? state "components/v2")]
- (if components-v2
- (rx/of (go-to-main-instance nil component-id))
- (let [project-id (get-in state [:workspace-project :id])
- file-id (get-in state [:workspace-file :id])
- page-id (get state :current-page-id)
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id :layout :assets}]
- (rx/of (rt/nav :workspace pparams qparams)
- (set-assets-section-open file-id :library true)
- (set-assets-section-open file-id :components true)
- (select-single-asset file-id component-id :components))))))
-
- ptk/EffectEvent
- (effect [_ state _]
- (let [components-v2 (features/active-feature? state "components/v2")
- wrapper-id (str "component-shape-id-" component-id)]
- (when-not components-v2
- (tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id))))))))
-
(defn show-component-in-assets
[component-id]
+
(ptk/reify ::show-component-in-assets
ptk/WatchEvent
(watch [_ state _]
- (let [project-id (get-in state [:workspace-project :id])
- file-id (get-in state [:workspace-file :id])
- page-id (get state :current-page-id)
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id :layout :assets}
- component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path]))
- paths (map (fn [i] (cfh/join-path (take (inc i) component-path))) (range (count component-path)))]
+ (let [component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path]))
+ paths (map (fn [i] (cfh/join-path (take (inc i) component-path))) (range (count component-path)))
+ file-id (:current-file-id state)]
(rx/concat
(rx/from (map #(set-assets-group-open file-id :components % true) paths))
- (rx/of (rt/nav :workspace pparams qparams)
+ (rx/of (dcm/go-to-workspace :layout :assets)
(set-assets-section-open file-id :library true)
(set-assets-section-open file-id :components true)
(select-single-asset file-id component-id :components)))))
@@ -1338,55 +1142,6 @@
(let [wrapper-id (str "component-shape-id-" component-id)]
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))
-(def go-to-file
- (ptk/reify ::go-to-file
- ptk/WatchEvent
- (watch [_ state _]
- (let [{:keys [id project-id data] :as file} (:workspace-file state)
- page-id (get-in data [:pages 0])
- pparams {:project-id project-id :file-id id}
- qparams {:page-id page-id}]
- (rx/of (rt/nav :workspace pparams qparams))))))
-
-(defn go-to-viewer
- ([] (go-to-viewer {}))
- ([{:keys [file-id page-id section frame-id]}]
- (ptk/reify ::go-to-viewer
- ptk/WatchEvent
- (watch [_ state _]
- (let [{:keys [current-file-id current-page-id]} state
- pparams {:file-id (or file-id current-file-id)}
- qparams (cond-> {:page-id (or page-id current-page-id)}
- (some? section)
- (assoc :section section)
- (some? frame-id)
- (assoc :frame-id frame-id))]
- (rx/of ::dps/force-persist
- (rt/nav-new-window* {:rname :viewer
- :path-params pparams
- :query-params qparams
- :name (str "viewer-" (:file-id pparams))})))))))
-
-(defn go-to-dashboard
- ([] (go-to-dashboard nil))
- ([{:keys [team-id]}]
- (ptk/reify ::go-to-dashboard
- ptk/WatchEvent
- (watch [_ state _]
- (when-let [team-id (or team-id (:current-team-id state))]
- (rx/of ::dps/force-persist
- (rt/nav :dashboard-projects {:team-id team-id})))))))
-
-(defn go-to-dashboard-fonts
- []
- (ptk/reify ::go-to-dashboard-fonts
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/of ::dps/force-persist
- (rt/nav :dashboard-fonts {:team-id team-id}))))))
-
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1622,8 +1377,8 @@
(rx/catch on-copy-error)
(rx/ignore)))
- ;; FIXME: this is to support Firefox versions below 116 that don't support `ClipboardItem`
- ;; after the version 116 is less common we could remove this.
+ ;; FIXME: this is to support Firefox versions below 116 that don't support
+ ;; `ClipboardItem` after the version 116 is less common we could remove this.
;; https://caniuse.com/?search=ClipboardItem
(->> (rx/from shapes)
(rx/merge-map (partial prepare-object objects frame-id))
@@ -1636,6 +1391,7 @@
(rx/ignore))))))))))
(declare ^:private paste-transit)
+(declare ^:private paste-html-text)
(declare ^:private paste-text)
(declare ^:private paste-image)
(declare ^:private paste-svg-text)
@@ -1703,6 +1459,7 @@
(let [pdata (wapi/read-from-paste-event event)
image-data (some-> pdata wapi/extract-images)
text-data (some-> pdata wapi/extract-text)
+ html-data (some-> pdata wapi/extract-html-text)
transit-data (ex/ignoring (some-> text-data t/decode-str))]
(cond
(and (string? text-data) (re-find #"> (filter ctk/instance-head? orig-shapes)
+ (map (fn [{:keys [component-file]}]
+ (ptk/event ::ev/event
+ {::ev/name "use-library-component"
+ ::ev/origin "paste"
+ :external-library (not= file-id component-file)})))
+ (rx/from))
+ (rx/of (dwu/start-undo-transaction undo-id)
+ (dch/commit-changes changes)
+ (dws/select-shapes selected)
+ (ptk/data-event :layout/update {:ids [frame-id]})
+ (dwu/commit-undo-transaction undo-id))))))))
(defn as-content [text]
(let [paragraphs (->> (str/lines text)
@@ -2056,6 +1828,34 @@
:else
(deref ms/mouse-position)))
+(defn- paste-html-text
+ [html text]
+ (dm/assert! (string? html))
+ (ptk/reify ::paste-html-text
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [root (dwtxt/create-root-from-html html)
+ content (tc/dom->cljs root)
+
+ id (uuid/next)
+ width (max 8 (min (* 7 (count text)) 700))
+ height 16
+ {:keys [x y]} (calculate-paste-position state)
+
+ shape {:id id
+ :type :text
+ :name (txt/generate-shape-name text)
+ :x x
+ :y y
+ :width width
+ :height height
+ :grow-type (if (> (count text) 100) :auto-height :auto-width)
+ :content content}
+ undo-id (js/Symbol)]
+ (rx/of (dwu/start-undo-transaction undo-id)
+ (dwsh/create-and-add-shape :text x y shape)
+ (dwu/commit-undo-transaction undo-id))))))
+
(defn- paste-text
[text]
(dm/assert! (string? text))
diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs
index dafd3de4ef..8b3ccbc319 100644
--- a/frontend/src/app/main/data/workspace/colors.cljs
+++ b/frontend/src/app/main/data/workspace/colors.cljs
@@ -16,7 +16,7 @@
[app.common.types.shape :refer [check-stroke!]]
[app.common.types.shape.shadow :refer [check-shadow!]]
[app.main.broadcast :as mbc]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index 1e023740e0..d1b250cc1d 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -13,8 +13,9 @@
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
- [app.main.data.comments :as dcm]
- [app.main.data.events :as ev]
+ [app.main.data.comments :as dcmt]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwco]
[app.main.data.workspace.drawing :as dwd]
@@ -23,7 +24,6 @@
[app.main.repo :as rp]
[app.main.streams :as ms]
[app.util.mouse :as mse]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -38,7 +38,7 @@
(watch [_ _ stream]
(let [stopper (rx/filter #(= ::finalize %) stream)]
(rx/merge
- (rx/of (dcm/retrieve-comment-threads file-id))
+ (rx/of (dcmt/retrieve-comment-threads file-id))
(->> stream
(rx/filter mse/mouse-event?)
(rx/filter mse/mouse-click-event?)
@@ -60,8 +60,8 @@
(watch [_ state _]
(let [local (:comments-local state)]
(cond
- (:draft local) (rx/of (dcm/close-thread))
- (:open local) (rx/of (dcm/close-thread))
+ (:draft local) (rx/of (dcmt/close-thread))
+ (:open local) (rx/of (dcmt/close-thread))
:else
(rx/of (dw/clear-edition-mode)
@@ -78,19 +78,19 @@
(watch [_ state _]
(let [local (:comments-local state)]
(if (some? (:open local))
- (rx/of (dcm/close-thread))
+ (rx/of (dcmt/close-thread))
(let [page-id (:current-page-id state)
file-id (:current-file-id state)
params {:position position
:page-id page-id
:file-id file-id}]
- (rx/of (dcm/create-draft params))))))))
+ (rx/of (dcmt/create-draft params))))))))
(defn center-to-comment-thread
[{:keys [position] :as thread}]
(dm/assert!
"expected valid comment thread"
- (dcm/check-comment-thread! thread))
+ (dcmt/check-comment-thread! thread))
(ptk/reify ::center-to-comment-thread
ptk/UpdateEvent
@@ -109,22 +109,21 @@
[thread]
(dm/assert!
"expected valid comment thread"
- (dcm/check-comment-thread! thread))
+ (dcmt/check-comment-thread! thread))
(ptk/reify ::open-comment-thread
ptk/WatchEvent
(watch [_ _ stream]
- (let [pparams {:project-id (:project-id thread)
- :file-id (:file-id thread)}
- qparams {:page-id (:page-id thread)}]
- (rx/merge
- (rx/of (rt/nav :workspace pparams qparams))
- (->> stream
- (rx/filter (ptk/type? ::dwv/initialize-viewport))
- (rx/take 1)
- (rx/mapcat #(rx/of (center-to-comment-thread thread)
- (dwd/select-for-drawing :comments)
- (with-meta (dcm/open-thread thread)
- {::ev/origin "workspace"})))))))))
+ (rx/merge
+ (rx/of (dcm/go-to-workspace :file-id (:file-id thread)
+ :page-id (:page-id thread)))
+
+ (->> stream
+ (rx/filter (ptk/type? ::dwv/initialize-viewport))
+ (rx/take 1)
+ (rx/mapcat #(rx/of (center-to-comment-thread thread)
+ (dwd/select-for-drawing :comments)
+ (with-meta (dcmt/open-thread thread)
+ {::ev/origin "workspace"}))))))))
(defn update-comment-thread-position
([thread [new-x new-y]]
@@ -133,7 +132,7 @@
([thread [new-x new-y] frame-id]
(dm/assert!
"expected valid comment thread"
- (dcm/check-comment-thread! thread))
+ (dcmt/check-comment-thread! thread))
(ptk/reify ::update-comment-thread-position
ptk/WatchEvent
(watch [it state _]
diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs
index 6547d57723..4ecfaba63f 100644
--- a/frontend/src/app/main/data/workspace/guides.cljs
+++ b/frontend/src/app/main/data/workspace/guides.cljs
@@ -12,7 +12,7 @@
[app.common.geom.shapes :as gsh]
[app.common.types.page :as ctp]
[app.main.data.changes :as dwc]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs
index ef62306921..978413941d 100644
--- a/frontend/src/app/main/data/workspace/interactions.cljs
+++ b/frontend/src/app/main/data/workspace/interactions.cljs
@@ -17,6 +17,7 @@
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
+ [app.main.data.event :as ev]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@@ -168,7 +169,8 @@
objects (get page :objects)
frame (cfh/get-root-frame objects (:id shape))
- flows (get page :objects)
+ first? (not-any? #(seq (:interactions %)) (vals objects))
+ flows (get page :flows)
flow (ctp/get-frame-flow flows (:id frame))]
(rx/concat
(rx/of (dwsh/update-shapes
@@ -177,14 +179,17 @@
(let [new-interaction (-> ctsi/default-interaction
(ctsi/set-destination destination)
(assoc :position-relative-to (:id shape)))]
- (cls/add-new-interaction shape new-interaction))))
+ (cls/add-new-interaction shape new-interaction)))))
- (when destination
- (dwsh/update-shapes [destination] cls/show-in-viewer))
+ (when destination
+ (rx/of (dwsh/update-shapes [destination] cls/show-in-viewer)))
- (when (and (not (connected-frame? objects (:id frame)))
- (nil? flow))
- (add-flow (:id frame))))))))))
+ (when (and (not (connected-frame? objects (:id frame)))
+ (nil? flow))
+ (rx/of (add-flow (:id frame))))
+ (when first?
+ ;; When the first interaction of the page is created we emit the event "create-prototype"
+ (rx/of (ptk/event ::ev/event {::ev/name "create-prototype"})))))))))
(defn remove-interaction
([shape index]
diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs
index 1fb219863a..c99deb8714 100644
--- a/frontend/src/app/main/data/workspace/layout.cljs
+++ b/frontend/src/app/main/data/workspace/layout.cljs
@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.util.storage :as storage]
[clojure.set :as set]
[potok.v2.core :as ptk]))
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index f4187ab2ba..3461de8886 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -27,7 +27,8 @@
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.workspace :as-alias dw]
@@ -40,14 +41,15 @@
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
+ [app.main.data.workspace.zoom :as dwz]
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.util.color :as uc]
[app.util.i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.storage :as storage]
[app.util.time :as dt]
[beicon.v2.core :as rx]
@@ -57,11 +59,11 @@
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
-(defn- pretty-file
+(defn- debug-pretty-file
[file-id state]
(if (= file-id (:current-file-id state))
""
- (str "<" (get-in state [:workspace-libraries file-id :name]) ">")))
+ (str "<" (get-in state [:libraries file-id :name]) ">")))
(defn- log-changes
[changes file]
@@ -586,7 +588,7 @@
in the given file library. Then selects the newly created instance."
([file-id component-id position]
(instantiate-component file-id component-id position nil))
- ([file-id component-id position {:keys [start-move? initial-point id-ref]}]
+ ([file-id component-id position {:keys [start-move? initial-point id-ref origin]}]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? component-id))
(dm/assert! (gpt/point? position))
@@ -600,6 +602,8 @@
changes (-> (pcb/empty-changes it (:id page))
(pcb/with-objects objects))
+ current-file-id (:current-file-id state)
+
[new-shape changes]
(cll/generate-instantiate-component changes
objects
@@ -608,12 +612,18 @@
position
page
libraries)
+
undo-id (js/Symbol)]
(when id-ref
(reset! id-ref (:id new-shape)))
- (rx/of (dwu/start-undo-transaction undo-id)
+ (rx/of (ptk/event
+ ::ev/event
+ {::ev/name "use-library-component"
+ ::ev/origin origin
+ :external-library (not= file-id current-file-id)})
+ (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [(:id new-shape)]})
(dws/select-shapes (d/ordered-set (:id new-shape)))
@@ -676,21 +686,49 @@
(rx/of (when can-detach?
(dch/commit-changes changes)))))))
-(defn nav-to-component-file
+(defn go-to-component-file
[file-id component]
(dm/assert! (uuid? file-id))
(dm/assert! (some? component))
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state _]
- (let [project-id (get-in state [:workspace-libraries file-id :project-id])
- path-params {:project-id project-id
- :file-id file-id}
- query-params {:page-id (:main-instance-page component)
- :component-id (:id component)}]
- (rx/of (rt/nav-new-window* {:rname :workspace
- :path-params path-params
- :query-params query-params}))))))
+ (let [params (-> (rt/get-params state)
+ (assoc :file-id file-id)
+ (assoc :page-id (:main-instance-page component))
+ (assoc :component-id (:id component)))]
+ (rx/of (rt/nav :workspace params :new-window? true))))))
+
+
+(defn go-to-local-component
+ [& {:keys [id] :as options}]
+ (ptk/reify ::go-to-local-component
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [current-page-id (:current-page-id state)
+
+ select-and-zoom
+ (fn [shape-id]
+ (rx/of (dws/select-shapes (d/ordered-set shape-id))
+ dwz/zoom-to-selected-shape))
+
+ redirect-to-page
+ (fn [page-id shape-id]
+ (rx/merge
+ (rx/of (dcm/go-to-workspace :page-id page-id))
+ (->> stream
+ (rx/filter (ptk/type? ::initialize-page))
+ (rx/take 1)
+ (rx/observe-on :async)
+ (rx/mapcat (fn [_] (select-and-zoom shape-id))))))]
+
+ (when-let [component (dm/get-in state [:workspace-data :components id])]
+ (let [page-id (:main-instance-page component)
+ shape-id (:main-instance-id component)]
+ (when (some? page-id)
+ (if (= page-id current-page-id)
+ (select-and-zoom shape-id)
+ (redirect-to-page page-id shape-id)))))))))
(defn library-thumbnails-fetched
[thumbnails]
@@ -722,9 +760,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (update-in [:workspace-libraries library-id]
+ (update-in [:libraries library-id]
assoc :modified-at modified-at :revn revn)
- (d/update-in-when [:workspace-libraries library-id :data]
+ (d/update-in-when [:libraries library-id :data]
ch/process-changes changes)))
ptk/WatchEvent
@@ -859,7 +897,7 @@
current-file? (= current-file-id file-id)
data (if current-file?
(get state :workspace-data)
- (get-in state [:workspace-libraries file-id :data]))
+ (get-in state [:libraries file-id :data]))
component (ctkl/get-component data component-id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
@@ -880,11 +918,15 @@
(rx/of
(dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group)
- (sync-file current-file-id file-id :components (:component-id shape) undo-group)
+
+ ;; These two calls are necessary for properly sync thumbnails
+ ;; when a main component does not live in the same page
(update-component-thumbnail-sync state component-id file-id "frame")
(update-component-thumbnail-sync state component-id file-id "component")
+
+ (sync-file current-file-id file-id :components component-id undo-group)
(when (not current-file?)
- (sync-file file-id file-id :components (:component-id shape) undo-group))
+ (sync-file file-id file-id :components component-id undo-group))
(dwu/commit-undo-transaction undo-id)))))))
(defn launch-component-sync
@@ -936,9 +978,9 @@
;; in the grid creating new rows/columns to make space
(let [file (wsh/get-file state file-id)
libraries (wsh/get-libraries state)
- page (wsh/lookup-page state)
- objects (wsh/lookup-page-objects state)
- parent (get objects (:parent-id shape))
+ page (wsh/lookup-page state)
+ objects (wsh/lookup-page-objects state)
+ parent (get objects (:parent-id shape))
;; If the target parent is a grid layout we need to pass the target cell
target-cell (when (ctl/grid-layout? parent)
@@ -979,7 +1021,7 @@
(watch [_ state _]
(let [undo-id (js/Symbol)]
(log/info :msg "COMPONENT-SWAP"
- :file (pretty-file file-id state)
+ :file (debug-pretty-file file-id state)
:id-new-component id-new-component
:undo-id undo-id)
(rx/concat
@@ -1025,15 +1067,15 @@
(update [_ state]
(if (and (not= library-id (:current-file-id state))
(nil? asset-id))
- (d/assoc-in-when state [:workspace-libraries library-id :synced-at] (dt/now))
+ (d/assoc-in-when state [:libraries library-id :synced-at] (dt/now))
state))
ptk/WatchEvent
(watch [it state _]
(when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file
(log/info :msg "SYNC-FILE"
- :file (pretty-file file-id state)
- :library (pretty-file library-id state)
+ :file (debug-pretty-file file-id state)
+ :library (debug-pretty-file library-id state)
:asset-type asset-type
:asset-id asset-id
:undo-group undo-group)
@@ -1104,9 +1146,11 @@
ptk/WatchEvent
(watch [_ state _]
- (rp/cmd! :ignore-file-library-sync-status
- {:file-id (get-in state [:workspace-file :id])
- :date (dt/now)}))))
+ (let [file-id (:current-file-id state)]
+ (->> (rp/cmd! :ignore-file-library-sync-status
+ {:file-id file-id
+ :date (dt/now)})
+ (rx/ignore))))))
(defn assets-need-sync
"Get a lazy sequence of all the assets of each type in the library that have
@@ -1127,7 +1171,7 @@
(let [file-data (:workspace-data state)
ignore-until (dm/get-in state [:workspace-file :ignore-sync-until])
libraries-need-sync (filter #(seq (assets-need-sync % file-data ignore-until))
- (vals (get state :workspace-libraries)))
+ (vals (get state :libraries)))
do-more-info #(modal/show! :libraries-dialog {:starting-tab "updates"})
do-update #(do (apply st/emit! (map (fn [library]
(sync-file (:current-file-id state)
@@ -1296,23 +1340,6 @@
(->> (rp/cmd! :set-file-shared params)
(rx/ignore))))))
-(defn- shared-files-fetched
- [files]
- (ptk/reify ::shared-files-fetched
- ptk/UpdateEvent
- (update [_ state]
- (let [state (dissoc state :files)]
- (assoc state :workspace-shared-files files)))))
-
-(defn fetch-shared-files
- [{:keys [team-id] :as params}]
- (dm/assert! (uuid? team-id))
- (ptk/reify ::fetch-shared-files
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-team-shared-files {:team-id team-id})
- (rx/map shared-files-fetched)))))
-
;; --- Link and unlink Files
(defn link-file-to-library
@@ -1331,7 +1358,7 @@
(let [libraries (:workspace-shared-files state)
library (d/seek #(= (:id %) library-id) libraries)]
(if library
- (update state :workspace-libraries assoc library-id (dissoc library :library-summary))
+ (update state :libraries assoc library-id (dissoc library :library-summary))
state)))
ptk/WatchEvent
@@ -1345,7 +1372,7 @@
(rx/merge-map fpmap/resolve-file)
(rx/map (fn [file]
(fn [state]
- (assoc-in state [:workspace-libraries library-id] file)))))
+ (assoc-in state [:libraries library-id] file)))))
(->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"})
(rx/map (fn [thumbnails]
(fn [state]
@@ -1363,7 +1390,7 @@
ptk/UpdateEvent
(update [_ state]
- (d/dissoc-in state [:workspace-libraries library-id]))
+ (d/dissoc-in state [:libraries library-id]))
ptk/WatchEvent
(watch [_ _ _]
diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs
index ed745892aa..c2d75abf6a 100644
--- a/frontend/src/app/main/data/workspace/modifiers.cljs
+++ b/frontend/src/app/main/data/workspace/modifiers.cljs
@@ -525,8 +525,6 @@
:points
:x
:y
- :rx
- :ry
:r1
:r2
:r3
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index d4b6b7ffe5..e59e7b92b3 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -16,6 +16,7 @@
[app.main.data.modal :as modal]
[app.main.data.plugins :as dpl]
[app.main.data.websocket :as dws]
+ [app.main.data.workspace :as-alias dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.layout :as dwly]
@@ -30,9 +31,6 @@
[clojure.set :as set]
[potok.v2.core :as ptk]))
-;; From app.main.data.workspace we can use directly because it causes a circular dependency
-(def reload-file nil)
-
;; FIXME: this ns should be renamed to something different
(declare process-message)
@@ -292,7 +290,7 @@
curr-vern (dm/get-in state [:workspace-file :vern])
reload? (and (= file-id curr-file-id) (not= vern curr-vern))]
(when reload?
- (rx/of (reload-file)))))))
+ (rx/of (ptk/event ::dw/reload-current-file)))))))
(def ^:private schema:handle-library-change
[:map {:title "handle-library-change"}
@@ -316,6 +314,6 @@
(ptk/reify ::handle-library-change
ptk/WatchEvent
(watch [_ state _]
- (when (contains? (:workspace-libraries state) file-id)
+ (when (contains? (:libraries state) file-id)
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
(dwl/notify-sync-file file-id))))))
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index 8150ea4659..7125453b58 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -18,7 +18,7 @@
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.specialized-panel :as-alias dwsp]
@@ -456,22 +456,30 @@
id-duplicated (first new-ids)
- frames (into #{}
- (map #(get-in objects [% :frame-id]))
- ids)
- undo-id (js/Symbol)]
-
+ frames (into #{}
+ (map #(get-in objects [% :frame-id]))
+ ids)
+ undo-id (js/Symbol)]
+ (rx/concat
+ (->> (map (d/getf objects) ids)
+ (filter ctk/instance-head?)
+ (map (fn [{:keys [component-file]}]
+ (ptk/event ::ev/event
+ {::ev/name "use-library-component"
+ ::ev/origin "duplicate"
+ :external-library (not= file-id component-file)})))
+ (rx/from))
;; Warning: This order is important for the focus mode.
- (->> (rx/of
- (dwu/start-undo-transaction undo-id)
- (dch/commit-changes changes)
- (when change-selection?
- (select-shapes new-ids))
- (ptk/data-event :layout/update {:ids frames})
- (memorize-duplicated id-original id-duplicated)
- (dwu/commit-undo-transaction undo-id))
- (rx/tap #(when (some? return-ref)
- (reset! return-ref id-duplicated))))))))))
+ (->> (rx/of
+ (dwu/start-undo-transaction undo-id)
+ (dch/commit-changes changes)
+ (when change-selection?
+ (select-shapes new-ids))
+ (ptk/data-event :layout/update {:ids frames})
+ (memorize-duplicated id-original id-duplicated)
+ (dwu/commit-undo-transaction undo-id))
+ (rx/tap #(when (some? return-ref)
+ (reset! return-ref id-duplicated)))))))))))
(defn duplicate-selected
([move-delta?]
diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs
index a8e614093a..16d6a1a811 100644
--- a/frontend/src/app/main/data/workspace/shape_layout.cljs
+++ b/frontend/src/app/main/data/workspace/shape_layout.cljs
@@ -21,7 +21,7 @@
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.colors :as cl]
[app.main.data.workspace.grid-layout.editor :as dwge]
[app.main.data.workspace.modifiers :as dwm]
@@ -110,13 +110,15 @@
:undo-group undo-group})))
(rx/empty))))))
-(defn initialize
+(defn initialize-shape-layout
[]
- (ptk/reify ::initialize
+ (ptk/reify ::initialize-shape-layout
ptk/WatchEvent
(watch [_ _ stream]
- (let [stopper (rx/filter (ptk/type? ::finalize) stream)]
+ (let [stopper (rx/filter (ptk/type? ::finalize-shape-layout) stream)]
(->> stream
+ ;; FIXME: we don't need use types for simple signaling,
+ ;; we can just use a keyword for it
(rx/filter (ptk/type? :layout/update))
(rx/map deref)
;; We buffer the updates to the layout so if there are many changes at the same time
@@ -129,9 +131,9 @@
(update-layout-positions {:ids ids}))))
(rx/take-until stopper))))))
-(defn finalize
+(defn finalize-shape-layout
[]
- (ptk/reify ::finalize))
+ (ptk/data-event ::finalize-shape-layout))
(defn create-layout-from-id
[id type & {:keys [from-frame? calculate-params?] :or {from-frame? false calculate-params? true}}]
diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs
index c489b07ddf..3433c6a1cd 100644
--- a/frontend/src/app/main/data/workspace/shapes.cljs
+++ b/frontend/src/app/main/data/workspace/shapes.cljs
@@ -18,7 +18,7 @@
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 3e87114af3..40879477ec 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -7,13 +7,14 @@
(ns app.main.data.workspace.shortcuts
(:require
[app.common.data.macros :as dm]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.exports.assets :as de]
[app.main.data.modal :as modal]
[app.main.data.plugins :as dpl]
[app.main.data.preview :as dp]
+ [app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
- [app.main.data.users :as du]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
[app.main.data.workspace.drawing :as dwd]
@@ -440,17 +441,18 @@
:toggle-layers {:tooltip (ds/alt "L")
:command (ds/a-mod "l")
:subsections [:panels]
- :fn #(st/emit! (dw/go-to-layout :layers))}
+ :fn #(st/emit! (dcm/go-to-workspace :layout :layers))}
:toggle-assets {:tooltip (ds/alt "I")
:command (ds/a-mod "i")
:subsections [:panels]
- :fn #(st/emit! (dw/go-to-layout :assets))}
+ :fn #(st/emit! (dcm/go-to-workspace :layout :assets))}
:toggle-history {:tooltip (ds/alt "H")
:command (ds/a-mod "h")
:subsections [:panels]
- :fn #(emit-when-no-readonly (dw/go-to-layout :document-history))}
+ :fn #(emit-when-no-readonly
+ (dcm/go-to-workspace :layout :document-history))}
:toggle-colorpalette {:tooltip (ds/alt "P")
:command (ds/a-mod "p")
@@ -516,22 +518,22 @@
:open-viewer {:tooltip "G V"
:command "g v"
:subsections [:navigation-workspace]
- :fn #(st/emit! (dw/go-to-viewer))}
+ :fn #(st/emit! (dcm/go-to-viewer))}
:open-inspect {:tooltip "G I"
:command "g i"
:subsections [:navigation-workspace]
- :fn #(st/emit! (dw/go-to-viewer {:section :inspect}))}
+ :fn #(st/emit! (dcm/go-to-viewer :section :inspect))}
:open-comments {:tooltip "G C"
:command "g c"
:subsections [:navigation-workspace]
- :fn #(st/emit! (dw/go-to-viewer {:section :comments}))}
+ :fn #(st/emit! (dcm/go-to-viewer :section :comments))}
:open-dashboard {:tooltip "G D"
:command "g d"
:subsections [:navigation-workspace]
- :fn #(st/emit! (dw/go-to-dashboard))}
+ :fn #(st/emit! (dcm/go-to-dashboard-recent))}
:select-prev {:tooltip (ds/shift "tab")
:command "shift+tab"
diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs
index 6c55e9da8c..eefcb74282 100644
--- a/frontend/src/app/main/data/workspace/state_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/state_helpers.cljs
@@ -27,13 +27,9 @@
(-> (lookup-page state page-id)
(get :objects))))
-(defn lookup-viewer-objects
- ([state page-id]
- (dm/get-in state [:viewer :pages page-id :objects])))
-
(defn lookup-library-objects
[state file-id page-id]
- (dm/get-in state [:workspace-libraries file-id :data :pages-index page-id :objects]))
+ (dm/get-in state [:libraries file-id :data :pages-index page-id :objects]))
(defn lookup-objects
[state file-id page-id]
@@ -112,7 +108,7 @@
[state file-id]
(if (= file-id (:current-file-id state))
(get state :workspace-data)
- (dm/get-in state [:workspace-libraries file-id :data])))
+ (dm/get-in state [:libraries file-id :data])))
(defn get-file-full
"Get the data content of the given file (it may be the current file
@@ -121,13 +117,13 @@
(if (= file-id (:current-file-id state))
(-> (get state :workspace-file)
(assoc :data (get state :workspace-data)))
- (dm/get-in state [:workspace-libraries file-id :data])))
+ (dm/get-in state [:libraries file-id :data])))
(defn get-libraries
"Retrieve all libraries, including the local file."
[state]
(let [{:keys [id] :as local} (:workspace-data state)]
- (-> (:workspace-libraries state)
+ (-> (:libraries state)
(assoc id {:id id
:data local}))))
diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index 042fce3a55..982df883b8 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -17,7 +17,7 @@
[app.common.text :as txt]
[app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.modifiers :as dwm]
@@ -27,7 +27,7 @@
[app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.fonts :as fonts]
- [app.util.router :as rt]
+ [app.main.router :as rt]
[app.util.text-editor :as ted]
[app.util.text.content.styles :as styles]
[app.util.timers :as ts]
@@ -37,6 +37,8 @@
;; -- V2 Editor Helpers
+(def ^function create-root-from-string editor.v2/createRootFromString)
+(def ^function create-root-from-html editor.v2/createRootFromHTML)
(def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)
diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs
index a043c38110..0d2ef2239f 100644
--- a/frontend/src/app/main/data/workspace/thumbnails.cljs
+++ b/frontend/src/app/main/data/workspace/thumbnails.cljs
@@ -168,7 +168,7 @@
(.error js/console cause)
(rx/empty)))
- (rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
+ (rx/tap #(l/dbg :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
;; We cancel all the stream if user starts editing while
;; thumbnail is generating
diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs
index 529965fbb0..2df61c5e55 100644
--- a/frontend/src/app/main/data/workspace/undo.cljs
+++ b/frontend/src/app/main/data/workspace/undo.cljs
@@ -13,8 +13,8 @@
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.changes :as dch]
+ [app.main.data.common :as dcm]
[app.main.data.workspace.state-helpers :as wsh]
- [app.util.router :as rt]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -290,14 +290,8 @@
(ptk/reify ::assure-valid-current-page
ptk/WatchEvent
(watch [_ state _]
- (let [current_page (:current-page-id state)
- pages (get-in state [:workspace-data :pages])
- exists? (some #(= current_page %) pages)
-
- project-id (:current-project-id state)
- file-id (:current-file-id state)
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id (first pages)}]
- (if exists?
+ (let [page-id (:current-page-id state)
+ pages (dm/get-in state [:workspace-data :pages])]
+ (if (contains? pages page-id)
(rx/empty)
- (rx/of (rt/nav :workspace pparams qparams)))))))
+ (rx/of (dcm/go-to-workspace :page-id (first pages))))))))
diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs
index a9de40a137..3a8243c63e 100644
--- a/frontend/src/app/main/data/workspace/versions.cljs
+++ b/frontend/src/app/main/data/workspace/versions.cljs
@@ -8,7 +8,8 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
- [app.main.data.events :as ev]
+ [app.common.schema :as sm]
+ [app.main.data.event :as ev]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
@@ -25,7 +26,7 @@
(declare fetch-versions)
(defn init-version-state
- [file-id]
+ []
(ptk/reify ::init-version-state
ptk/UpdateEvent
(update [_ state]
@@ -33,7 +34,7 @@
ptk/WatchEvent
(watch [_ _ _]
- (rx/of (fetch-versions file-id)))))
+ (rx/of (fetch-versions)))))
(defn update-version-state
[version-state]
@@ -43,123 +44,89 @@
(update state :workspace-versions merge version-state))))
(defn fetch-versions
- [file-id]
- (dm/assert! (uuid? file-id))
+ []
(ptk/reify ::fetch-versions
ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! :get-file-snapshots {:file-id file-id})
- (rx/map #(update-version-state {:status :loaded :data %}))))))
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)]
+ (->> (rp/cmd! :get-file-snapshots {:file-id file-id})
+ (rx/map #(update-version-state {:status :loaded :data %})))))))
(defn create-version
- [file-id]
- (dm/assert! (uuid? file-id))
+ []
(ptk/reify ::create-version
ptk/WatchEvent
- (watch [_ _ _]
- (let [label (dt/format (dt/now) :date-full)]
+ (watch [_ state _]
+ (let [label (dt/format (dt/now) :date-full)
+ file-id (:current-file-id state)]
+
;; Force persist before creating snapshot, otherwise we could loss changes
(rx/concat
- (rx/of ::dwp/force-persist)
+ (rx/of ::dwp/force-persist
+ (ptk/event ::ev/event {::ev/name "create-version"}))
+
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
(rx/mapcat
(fn [{:keys [id]}]
- (rx/of
- (update-version-state {:editing id})
- (fetch-versions file-id)))))
- (rx/of (ptk/event ::ev/event {::ev/name "create-version"})))))))
-
-(defn create-version-from-plugins
- [file-id label resolve reject]
- (dm/assert! (uuid? file-id))
- (ptk/reify ::create-version-plugins
- ptk/WatchEvent
- (watch [_ _ _]
- ;; Force persist before creating snapshot, otherwise we could loss changes
- (->> (rx/concat
- (rx/of ::dwp/force-persist)
- (->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
- (rx/filter #(or (nil? %) (= :saved %)))
- (rx/take 1)
- (rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
-
- (rx/mapcat
- (fn [{:keys [id]}]
- (->> (rp/cmd! :get-file-snapshots {:file-id file-id})
- (rx/take 1)
- (rx/map (fn [versions] (d/seek #(= id (:id %)) versions))))))
- (rx/tap resolve)
- (rx/ignore))
- (rx/of (ptk/event ::ev/event {::ev/origin "plugins"
- ::ev/name "create-version"})))
-
- ;; On error reject the promise and empty the stream
- (rx/catch (fn [error]
- (reject error)
- (rx/empty)))))))
+ (rx/of (update-version-state {:editing id})
+ (fetch-versions))))))))))
(defn rename-version
- [file-id id label]
- (dm/assert! (uuid? file-id))
- (dm/assert! (uuid? id))
- (dm/assert! (and (string? label) (d/not-empty? label)))
+ [id label]
+ (assert (uuid? id) "expected valid uuid for `id`")
+ (assert (sm/valid-text? label) "expected not empty string for `label`")
(ptk/reify ::rename-version
ptk/WatchEvent
- (watch [_ _ _]
- (rx/merge
- (rx/of (update-version-state {:editing false}))
- (->> (rp/cmd! :update-file-snapshot {:id id :label label})
- (rx/map #(fetch-versions file-id)))
- (rx/of (ptk/event ::ev/event {::ev/name "rename-version"}))))))
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)]
+ (rx/merge
+ (rx/of (update-version-state {:editing false})
+ (ptk/event ::ev/event {::ev/name "rename-version"
+ :file-id file-id}))
+ (->> (rp/cmd! :update-file-snapshot {:id id :label label})
+ (rx/map fetch-versions)))))))
(defn restore-version
- [project-id file-id id origin]
- (dm/assert! (uuid? project-id))
- (dm/assert! (uuid? file-id))
- (dm/assert! (uuid? id))
-
+ [id origin]
+ (assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::restore-version
ptk/WatchEvent
- (watch [_ _ _]
- (rx/concat
- (rx/of ::dwp/force-persist)
- (->> (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-file project-id file-id)))
- (case origin
- :version
- (rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)]
+ (rx/concat
+ (rx/of ::dwp/force-persist
+ (dw/remove-layout-flag :document-history))
- :snapshot
- (rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
+ ;; 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)))
- :plugin
- (rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
-
- (rx/empty))))))
+ (when-let [name (case origin
+ :version "restore-pin-version"
+ :snapshot "restore-autosave"
+ nil)]
+ (rx/of (ptk/event ::ev/event {::ev/name name}))))))))
(defn delete-version
- [file-id id]
- (dm/assert! (uuid? file-id))
- (dm/assert! (uuid? id))
+ [id]
+ (assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::delete-version
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :delete-file-snapshot {:id id})
- (rx/map #(fetch-versions file-id))))))
+ (rx/map fetch-versions)))))
(defn pin-version
- [file-id id]
- (dm/assert! (uuid? file-id))
- (dm/assert! (uuid? id))
-
+ [id]
+ (assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::pin-version
ptk/WatchEvent
(watch [_ state _]
@@ -168,8 +135,82 @@
params {:id id
:label (dt/format (:created-at version) :date-full)}]
- (rx/concat
- (->> (rp/cmd! :update-file-snapshot params)
- (rx/mapcat #(rx/of (update-version-state {:editing id})
- (fetch-versions file-id))))
- (rx/of (ptk/event ::ev/event {::ev/name "pin-version"})))))))
+ (->> (rp/cmd! :update-file-snapshot params)
+ (rx/mapcat (fn [_]
+ (rx/of (update-version-state {:editing id})
+ (fetch-versions)
+ (ptk/event ::ev/event {::ev/name "pin-version"})))))))))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; PLUGINS SPECIFIC EVENTS
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- wait-persisted-status
+ []
+ (->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
+ (rx/filter #(or (nil? %) (= :saved %)))
+ (rx/take 1)))
+
+(defn create-version-from-plugins
+ [file-id label resolve reject]
+
+ (assert (uuid? file-id) "expected valid uuid for `file-id`")
+ (assert (sm/valid-text? label) "expected not empty string for `label`")
+
+ (ptk/reify ::create-version-from-plugins
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [current-file-id (:current-file-id state)]
+ ;; Force persist before creating snapshot, otherwise we could loss changes
+ (->> (rx/concat
+ (rx/of (ptk/event ::ev/event {::ev/origin "plugins"
+ ::ev/name "create-version"}))
+
+ (when (= file-id current-file-id)
+ (rx/of ::dwp/force-persist))
+
+ (->> (if (= file-id current-file-id)
+ (wait-persisted-status)
+ (rx/of :nothing))
+ (rx/mapcat
+ (fn [_]
+ (rp/cmd! :create-file-snapshot {:file-id file-id :label label})))
+ (rx/mapcat
+ (fn [{:keys [id]}]
+ (->> (rp/cmd! :get-file-snapshots {:file-id file-id})
+ (rx/take 1)
+ (rx/map (fn [versions] (d/seek #(= id (:id %)) versions))))))
+ (rx/tap resolve)
+ (rx/ignore)))
+
+ ;; On error reject the promise and empty the stream
+ (rx/catch (fn [error]
+ (reject error)
+ (rx/empty))))))))
+
+(defn restore-version-from-plugin
+ [file-id id resolve _reject]
+ (assert (uuid? id) "expected valid uuid for `id`")
+
+ (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)
+
+ ;; 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)))
+
+ (->> (rx/of 1)
+ (rx/tap resolve)
+ (rx/ignore))))))
+
+
+
+
diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs
index 7a4f2e2e4f..5ad3c84a22 100644
--- a/frontend/src/app/main/errors.cljs
+++ b/frontend/src/app/main/errors.cljs
@@ -10,13 +10,14 @@
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
+ [app.main.data.auth :as da]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.workspace :as-alias dw]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.util.globals :as glob]
[app.util.i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.timers :as ts]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
@@ -116,7 +117,7 @@
(if show-oops?
(st/async-emit! (rt/assign-exception e))
(do
- (st/emit! (du/logout {:capture-redirect true}))
+ (st/emit! (da/logout))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
;; Error that happens on an active business model validation does not
@@ -141,7 +142,7 @@
:timeout 3000})))
(= code :vern-conflict)
- (st/emit! (reload-file))
+ (st/emit! (ptk/event ::dw/reload-current-file))
:else
(st/async-emit! (rt/assign-exception error))))
@@ -212,7 +213,6 @@
(ts/schedule
#(st/emit! (rt/assign-exception error))))
-
(defn- redirect-to-dashboard
[]
(let [team-id (:current-team-id @st/state)
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index dce5e74632..75882c1e6d 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -13,6 +13,7 @@
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
+ [app.config :as cf]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st]
[app.main.ui.workspace.tokens.token-set :as wtts]
@@ -21,22 +22,33 @@
;; ---- Global refs
(def route
- (l/derived :route st/state))
+ (l/derived (l/key :route) st/state))
(def router
- (l/derived :router st/state))
+ (l/derived (l/key :router) st/state))
(def profile
- (l/derived :profile st/state))
+ (l/derived (l/key :profile) st/state))
(def team
- (l/derived :team st/state))
+ (l/derived (fn [state]
+ (let [team-id (:current-team-id state)
+ teams (:teams state)]
+ (get teams team-id)))
+ st/state))
+
+(def project
+ (l/derived (fn [state]
+ (let [project-id (:current-project-id state)
+ projects (:projects state)]
+ (get projects project-id)))
+ st/state))
(def permissions
- (l/derived :permissions st/state))
+ (l/derived (l/key :permissions) team))
(def teams
- (l/derived :teams st/state))
+ (l/derived (l/key :teams) st/state))
(def exception
(l/derived :exception st/state))
@@ -53,68 +65,41 @@
(def persistence
(l/derived :persistence st/state))
-;; ---- Dashboard refs
+(def projects
+ (l/derived :projects st/state))
-(def dashboard-local
- (l/derived :dashboard-local st/state))
+(def files
+ (l/derived :files st/state))
-(def dashboard-fonts
- (l/derived :dashboard-fonts st/state))
+(def shared-files
+ "A derived state that points to the current list of shared
+ files (without the content, only summary)"
+ (l/derived :shared-files st/state))
-(def dashboard-projects
- (l/derived :dashboard-projects st/state))
+(def libraries
+ (l/derived :libraries st/state))
-(def dashboard-files
- (l/derived :dashboard-files st/state))
-
-(def dashboard-shared-files
- (l/derived :dashboard-shared-files st/state))
-
-(def dashboard-search-result
- (l/derived :dashboard-search-result st/state))
-
-(def dashboard-team-stats
- (l/derived :dashboard-team-stats st/state))
-
-(def dashboard-team-members
- (l/derived :dashboard-team-members st/state))
-
-(def dashboard-team-invitations
- (l/derived :dashboard-team-invitations st/state))
-
-(def dashboard-team-webhooks
- (l/derived :dashboard-team-webhooks st/state))
-
-(def dashboard-selected-project
- (l/derived (fn [state]
- (dm/get-in state [:dashboard-local :selected-project]))
- st/state))
-
-(defn- dashboard-extract-selected
+(defn extract-selected-files
[files selected]
(let [get-file #(get files %)
sim-file #(select-keys % [:id :name :project-id :is-shared])
xform (comp (keep get-file)
(map sim-file))]
- (->> (into #{} xform selected)
+ (->> (sequence xform selected)
(d/index-by :id))))
-(def dashboard-selected-search
+(def selected-files
(l/derived (fn [state]
- ;; we need to this because :dashboard-search-result is a list
- ;; of maps and we need a map of maps (using :id as key).
- (let [files (d/index-by :id (:dashboard-search-result state))]
- (->> (dm/get-in state [:dashboard-local :selected-files])
- (dashboard-extract-selected files))))
+ (let [selected (get state :selected-files)
+ files (get state :files)]
+ (extract-selected-files files selected)))
st/state))
-(def dashboard-selected-files
- (l/derived (fn [state]
- (->> (dm/get-in state [:dashboard-local :selected-files])
- (dashboard-extract-selected (:dashboard-files state))))
- st/state))
+(def selected-project
+ (l/derived :selected-project st/state))
-;; ---- Workspace refs
+(def dashboard-local
+ (l/derived :dashboard-local st/state))
(def render-state
(l/derived :render-state st/state))
@@ -248,10 +233,8 @@
(l/derived :workspace-data st/state))
(def workspace-file-colors
- (l/derived (fn [data]
- (when data
- (->> (:colors data)
- (d/mapm #(assoc %2 :file-id (:id data))))))
+ (l/derived (fn [{:keys [id] :as data}]
+ (some-> (:colors data) (update-vals #(assoc % :file-id id))))
workspace-data
=))
@@ -261,6 +244,8 @@
(dm/get-in state [:recent-colors file-id])))
st/state))
+;; FIXME: fonts are not prefixed, so the recent font list is shared
+;; across all teams. This may not be expected behavior
(def workspace-recent-fonts
(l/derived (fn [data]
(get data :recent-fonts []))
@@ -269,31 +254,9 @@
(def workspace-file-typography
(l/derived :typographies workspace-data))
-(def workspace-project
- (l/derived :workspace-project st/state))
-
-(def workspace-shared-files
- (l/derived :workspace-shared-files st/state))
-
-(def workspace-local-library
- (l/derived (fn [state]
- (select-keys (:workspace-data state)
- [:id
- :colors
- :media
- :typographies
- :components]))
- st/state =))
-
-(def workspace-libraries
- (l/derived :workspace-libraries st/state))
-
(def workspace-presence
(l/derived :workspace-presence st/state))
-(def workspace-snap-data
- (l/derived :workspace-snap-data st/state))
-
(def workspace-page
(l/derived (fn [state]
(let [page-id (:current-page-id state)
@@ -493,6 +456,9 @@
(def workspace-selected-token-set-path
(l/derived wtts/get-selected-token-set-path st/state))
+(def workspace-token-set-group-selected?
+ (l/derived wtts/token-group-selected? st/state))
+
(def workspace-ordered-token-sets
(l/derived #(or (some-> % ctob/get-sets) []) tokens-lib))
@@ -536,12 +502,16 @@
;; ---- Viewer refs
+(defn get-viewer-objects
+ [state page-id]
+ (dm/get-in state [:viewer :pages page-id :objects]))
+
(defn lookup-viewer-objects-by-id
[page-id]
- (l/derived #(wsh/lookup-viewer-objects % page-id) st/state =))
+ (l/derived #(get-viewer-objects % page-id) st/state =))
(def viewer-data
- (l/derived :viewer st/state))
+ (l/derived (l/key :viewer) st/state))
(def viewer-file
(l/derived :file viewer-data))
@@ -567,14 +537,8 @@
(def comments-local
(l/derived :comments-local st/state))
-(def users
- (l/derived :users st/state))
-
-(def current-file-comments-users
- (l/derived :current-file-comments-users st/state))
-
-(def current-team-comments-users
- (l/derived :current-team-comments-users st/state))
+(def profiles
+ (l/derived :profiles st/state))
(def viewer-fullscreen?
(l/derived (fn [state]
@@ -586,14 +550,12 @@
(dm/get-in state [:viewer-local :zoom-type]))
st/state))
-(def workspace-thumbnails
- (l/derived :workspace-thumbnails st/state))
-
(defn workspace-thumbnail-by-id
[object-id]
(l/derived
(fn [state]
- (dm/get-in state [:workspace-thumbnails object-id]))
+ (some-> (dm/get-in state [:thumbnails object-id])
+ (cf/resolve-media)))
st/state))
(def workspace-text-modifier
@@ -638,35 +600,9 @@
(every? (partial ctl/grid-layout-immediate-child? objects))))
workspace-page-objects =))
-;; FIXME: move to viewer.inspect.code
-(defn get-flex-child-viewer
- [ids page-id]
- (l/derived
- (fn [state]
- (let [objects (wsh/lookup-viewer-objects state page-id)]
- (into []
- (comp (map (d/getf objects))
- (filter (partial ctl/flex-layout-immediate-child? objects)))
- ids)))
- st/state =))
-
-;; FIXME: move to viewer.inspect.code
-(defn get-viewer-objects
- ([]
- (let [route (deref route)
- page-id (:page-id (:query-params route))]
- (get-viewer-objects page-id)))
- ([page-id]
- (l/derived
- (fn [state]
- (let [objects (wsh/lookup-viewer-objects state page-id)]
- objects))
- st/state =)))
-
(def colorpicker
(l/derived :colorpicker st/state))
-
(def workspace-grid-edition
(l/derived :workspace-grid-edition st/state))
@@ -674,12 +610,6 @@
[id]
(l/derived #(get % id) workspace-grid-edition))
-(def current-file-id
- (l/derived :current-file-id st/state))
-
-(def current-project-id
- (l/derived :current-project-id st/state))
-
(def workspace-preview-blend
(l/derived :workspace-preview-blend st/state))
diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs
index d58ce0fe93..f0a9d14a08 100644
--- a/frontend/src/app/main/render.cljs
+++ b/frontend/src/app/main/render.cljs
@@ -99,7 +99,7 @@
[{:keys [shape]}]
(let [thumbnails? (mf/use-ctx muc/render-thumbnails)
childs (mapv (d/getf objects) (:shapes shape))]
- (if (and thumbnails? (some? (:thumbnail shape)))
+ (if (and thumbnails? (some? (:thumbnail-id shape)))
[:& frame/frame-thumbnail {:shape shape :bounds (:children-bounds shape)}]
[:& frame-shape {:shape shape :childs childs}])))))
@@ -338,7 +338,7 @@
;; used to render thumbnails on assets panel.
(mf/defc component-svg
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
- [{:keys [objects root-shape show-grids? zoom class] :or {zoom 1} :as props}]
+ [{:keys [objects root-shape show-grids? is-hidden zoom class] :or {zoom 1} :as props}]
(when root-shape
(let [root-shape-id (:id root-shape)
include-metadata (mf/use-ctx export/include-metadata-ctx)
@@ -381,13 +381,14 @@
:xmlns:penpot (when include-metadata "https://penpot.app/xmlns")
:fill "none"}
- [:*
- [:> shape-container {:shape root-shape'}
- [:& (mf/provider muc/is-component?) {:value true}
- [:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
+ (when-not is-hidden
+ [:*
+ [:> shape-container {:shape root-shape'}
+ [:& (mf/provider muc/is-component?) {:value true}
+ [:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
- (when show-grids?
- [:& empty-grids {:root-shape-id root-shape-id :objects objects}])]])))
+ (when show-grids?
+ [:& empty-grids {:root-shape-id root-shape-id :objects objects}])])])))
(mf/defc component-svg-thumbnail
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs
index 4854957010..2c5247c28f 100644
--- a/frontend/src/app/main/repo.cljs
+++ b/frontend/src/app/main/repo.cljs
@@ -11,7 +11,7 @@
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
- [app.main.data.events :as-alias ev]
+ [app.main.data.event :as-alias ev]
[app.util.http :as http]
[app.util.sse :as sse]
[beicon.v2.core :as rx]
diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/main/router.cljs
similarity index 75%
rename from frontend/src/app/util/router.cljs
rename to frontend/src/app/main/router.cljs
index 1a92d4ec7a..2639834ee6 100644
--- a/frontend/src/app/util/router.cljs
+++ b/frontend/src/app/main/router.cljs
@@ -4,13 +4,13 @@
;;
;; Copyright (c) KALEIDOS INC
-(ns app.util.router
+(ns app.main.router
(:refer-clojure :exclude [resolve])
(:require
[app.common.data.macros :as dm]
[app.common.uri :as u]
[app.config :as cf]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.util.browser-history :as bhistory]
[app.util.dom :as dom]
[app.util.globals :as globals]
@@ -28,11 +28,10 @@
(r/map->Match data))
(defn resolve
- ([router id] (resolve router id {} {}))
- ([router id path-params] (resolve router id path-params {}))
- ([router id path-params query-params]
- (when-let [match (r/match-by-name router id path-params)]
- (r/match->path match query-params))))
+ ([router id] (resolve router id {}))
+ ([router id params]
+ (when-let [match (r/match-by-name router id)]
+ (r/match->path match params))))
(defn create
[routes]
@@ -63,6 +62,9 @@
(defn navigated
[match]
(ptk/reify ::navigated
+ IDeref
+ (-deref [_] match)
+
ev/Event
(-data [_]
(let [route (dm/get-in match [:data :name])
@@ -77,25 +79,29 @@
(assoc :route match)
(dissoc :exception)))))
-(defn navigate*
- [id path-params query-params replace]
+(defn navigate
+ [id params & {:keys [::replace ::new-window] :as options}]
(ptk/reify ::navigate
IDeref
(-deref [_]
{:id id
- :path-params path-params
- :query-params query-params
- :replace replace})
+ :params params
+ :options options})
ptk/EffectEvent
(effect [_ state _]
(let [router (:router state)
history (:history state)
- path (resolve router id path-params query-params)]
- (ts/asap
- #(if ^boolean replace
- (bhistory/replace-token! history path)
- (bhistory/set-token! history path)))))))
+ path (resolve router id params)]
+
+ (if ^boolean new-window
+ (let [name (or (::window-name options) "_blank")
+ uri (assoc cf/public-uri :fragment path)]
+ (dom/open-new-window uri name nil))
+ (ts/asap
+ #(if ^boolean replace
+ (bhistory/replace-token! history path)
+ (bhistory/set-token! history path))))))))
(defn assign-exception
[error]
@@ -107,27 +113,14 @@
(assoc state :exception error)))))
(defn nav
- ([id] (nav id nil nil))
- ([id path-params] (nav id path-params nil))
- ([id path-params query-params] (navigate* id path-params query-params false)))
+ ([id] (navigate id nil))
+ ([id params] (navigate id params))
+ ([id params & {:as options}]
+ (navigate id params options)))
-(defn nav'
- ([id] (nav id nil nil))
- ([id path-params] (nav id path-params nil))
- ([id path-params query-params] (navigate* id path-params query-params true)))
-
-(def navigate nav)
-
-(defn nav-new-window*
- [{:keys [rname path-params query-params name]}]
- (ptk/reify ::nav-new-window
- ptk/EffectEvent
- (effect [_ state _]
- (let [router (:router state)
- path (resolve router rname path-params query-params)
- name (or name "_blank")
- uri (assoc cf/public-uri :fragment path)]
- (dom/open-new-window uri name nil)))))
+(defn get-params
+ [state]
+ (dm/get-in state [:route :params :query]))
(defn nav-back
[]
diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs
index 703b3952de..949e3b77e9 100644
--- a/frontend/src/app/main/store.cljs
+++ b/frontend/src/app/main/store.cljs
@@ -60,7 +60,7 @@
:app.main.data.workspace.persistence/update-persistence-status
:app.main.data.websocket/send-message
:app.main.data.workspace.notifications/handle-pointer-send
- :app.util.router/assign-exception}]
+ :app.main.router/assign-exception}]
(->> (rx/merge
(->> stream
(rx/filter (ptk/type? :app.main.data.changes/commit))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 17ce25d757..7fef9a6f47 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -6,14 +6,20 @@
(ns app.main.ui
(:require
+ [app.common.data :as d]
[app.config :as cf]
+ [app.main.data.common :as dcm]
+ [app.main.data.team :as dtm]
[app.main.refs :as refs]
+ [app.main.repo :as rp]
+ [app.main.router :as rt]
+ [app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.debug.icons-preview :refer [icons-preview]]
+ [app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.error-boundary :refer [error-boundary*]]
[app.main.ui.exports.files]
[app.main.ui.frame-preview :as frame-preview]
- [app.main.ui.icons :as i]
[app.main.ui.notifications :as notifications]
[app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]]
[app.main.ui.onboarding.questions :refer [questions-modal]]
@@ -22,6 +28,8 @@
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(def auth-page
@@ -31,23 +39,117 @@
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def viewer-page
- (mf/lazy-component app.main.ui.viewer/viewer))
+ (mf/lazy-component app.main.ui.viewer/viewer*))
(def dashboard-page
- (mf/lazy-component app.main.ui.dashboard/dashboard))
+ (mf/lazy-component app.main.ui.dashboard/dashboard*))
(def settings-page
(mf/lazy-component app.main.ui.settings/settings))
(def workspace-page
- (mf/lazy-component app.main.ui.workspace/workspace))
+ (mf/lazy-component app.main.ui.workspace/workspace*))
-(mf/defc main-page
+(mf/defc workspace-legacy-redirect*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [project-id file-id page-id layout]}]
+ (mf/with-effect []
+ (->> (rp/cmd! :get-project {:id project-id})
+ (rx/subs! (fn [{:keys [team-id]}]
+ (st/emit! (dcm/go-to-workspace :team-id team-id
+ :file-id file-id
+ :page-id page-id
+ :layout layout)))
+ ptk/handle-error)))
+
+ [:> loader*
+ {:title (tr "labels.loading")
+ :overlay true}])
+
+(mf/defc dashboard-legacy-redirect*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [section team-id project-id search-term plugin-url]}]
+ (let [section (case section
+ :dashboard-legacy-search
+ :dashboard-search
+ :dashboard-legacy-projects
+ :dashboard-recent
+ :dashboard-legacy-files
+ :dashboard-files
+ :dashboard-legacy-libraries
+ :dashboard-libraries
+ :dashboard-legacy-fonts
+ :dashboard-fonts
+ :dashboard-legacy-font-providers
+ :dashboard-font-providers
+ :dashboard-legacy-team-members
+ :dashboard-members
+ :dashboard-legacy-team-invitations
+ :dashboard-invitations
+ :dashboard-legacy-team-webhooks
+ :dashboard-webhooks
+ :dashboard-legacy-team-settings
+ :dashboard-settings)]
+
+ (mf/with-effect []
+ (let [params {:team-id team-id
+ :project-id project-id
+ :search-term search-term
+ :plugin plugin-url}]
+ (st/emit! (rt/nav section (d/without-nils params)))))
+
+ [:> loader*
+ {:title (tr "labels.loading")
+ :overlay true}]))
+
+(mf/defc viewer-legacy-redirect*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [page-id file-id section index share-id interactions-mode frame-id share]}]
+ (mf/with-effect []
+ (let [params {:page-id page-id
+ :file-id file-id
+ :section section
+ :index index
+ :share-id share-id
+ :interactions-mode interactions-mode
+ :frame-id frame-id
+ :share share}]
+ (st/emit! (rt/nav :viewer (d/without-nils params)))))
+
+ [:> loader*
+ {:title (tr "labels.loading")
+ :overlay true}])
+
+(mf/defc team-container*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [team-id children]}]
+
+ (mf/with-effect [team-id]
+ (st/emit! (dtm/initialize-team team-id))
+ (fn []
+ (st/emit! (dtm/finalize-team team-id))))
+
+ (let [team (mf/deref refs/team)]
+ (when (= team-id (:id team))
+ [:& (mf/provider ctx/current-team-id) {:value team-id}
+ [:& (mf/provider ctx/permissions) {:value (:permissions team)}
+ ;; The `:key` is mandatory here because we want to reinitialize
+ ;; all dom tree instead of simple rerender.
+ [:* {:key (str team-id)} children]]])))
+
+(mf/defc page*
{::mf/props :obj
::mf/private true}
[{:keys [route profile]}]
(let [{:keys [data params]} route
- props (get profile :props)
+ props (get profile :props)
+ section (get data :name)
+
+
show-question-modal?
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
@@ -72,7 +174,7 @@
(not= "0.0" (:main cf/version)))]
[:& (mf/provider ctx/current-route) {:value route}
- (case (:name data)
+ (case section
(:auth-login
:auth-register
:auth-register-validate
@@ -96,66 +198,53 @@
[:& icons-preview])
(:dashboard-search
- :dashboard-projects
+ :dashboard-recent
:dashboard-files
:dashboard-libraries
:dashboard-fonts
:dashboard-font-providers
- :dashboard-team-members
- :dashboard-team-invitations
- :dashboard-team-webhooks
- :dashboard-team-settings)
- [:?
- #_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
- #_[:& app.main.ui.onboarding/onboarding-templates-modal]
- #_[:& app.main.ui.onboarding/onboarding-modal]
- #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
+ :dashboard-members
+ :dashboard-invitations
+ :dashboard-webhooks
+ :dashboard-settings)
+ (let [params (get params :query)
+ team-id (some-> params :team-id uuid)
+ project-id (some-> params :project-id uuid)
+ search-term (some-> params :search-term)
+ plugin-url (some-> params :plugin)]
+ [:?
+ #_[:& app.main.ui.releases/release-notes-modal {:version "2.4"}]
+ #_[:& app.main.ui.onboarding/onboarding-templates-modal]
+ #_[:& app.main.ui.onboarding/onboarding-modal]
+ #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
- (cond
- show-question-modal?
- [:& questions-modal]
+ (cond
+ show-question-modal?
+ [:& questions-modal]
- show-newsletter-modal?
- [:& onboarding-newsletter]
+ show-newsletter-modal?
+ [:& onboarding-newsletter]
- show-team-modal?
- [:& onboarding-team-modal {:go-to-team? true}]
+ show-team-modal?
+ [:& onboarding-team-modal {:go-to-team? true}]
- show-release-modal?
- [:& release-notes-modal {:version (:main cf/version)}])
+ show-release-modal?
+ [:& release-notes-modal {:version (:main cf/version)}])
- [:& dashboard-page {:route route :profile profile}]]
- :viewer
- (let [{:keys [query-params path-params]} route
- {:keys [index share-id section page-id interactions-mode frame-id share]
- :or {section :interactions interactions-mode :show-on-click}} query-params
- {:keys [file-id]} path-params]
- [:? {}
- (if (:token query-params)
- [:> static/error-container* {}
- [:div.image i/detach]
- [:div.main-message (tr "viewer.breaking-change.message")]
- [:div.desc-message (tr "viewer.breaking-change.description")]]
-
- [:& viewer-page
- {:page-id page-id
- :file-id file-id
- :section section
- :index index
- :share-id share-id
- :interactions-mode (keyword interactions-mode)
- :interactions-show? (case (keyword interactions-mode)
- :hide false
- :show true
- :show-on-click false)
- :frame-id frame-id
- :share share}])])
+ [:> team-container* {:team-id team-id}
+ [:> dashboard-page {:profile profile
+ :section section
+ :team-id team-id
+ :search-term search-term
+ :plugin-url plugin-url
+ :project-id project-id}]]])
:workspace
- (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)
- layout (some-> params :query :layout keyword)]
+ (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)
+ layout (some-> params :layout keyword)]
[:? {}
(when (cf/external-feature-flag "onboarding-03" "test")
(cond
@@ -171,11 +260,87 @@
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}]))
- [:& workspace-page {:project-id project-id
- :file-id file-id
- :page-id page-id
- :layout-name layout
- :key file-id}]])
+ [:> team-container* {:team-id team-id}
+ [:> workspace-page {:team-id team-id
+ :file-id file-id
+ :page-id page-id
+ :layout-name layout
+ :key file-id}]]])
+
+ :viewer
+ (let [params (get params :query)
+ index (some-> (:index params) parse-long)
+ share-id (some-> (:share-id params) parse-uuid)
+ section (or (some-> (:section params) keyword)
+ :interactions)
+
+ file-id (some-> (:file-id params) parse-uuid)
+ page-id (some-> (:page-id params) parse-uuid)
+ imode (or (some-> (:interactions-mode params) keyword)
+ :show-on-click)
+ frame-id (some-> (:frame-id params) parse-uuid)
+ share (:share params)]
+
+ [:? {}
+ [:> viewer-page
+ {:page-id page-id
+ :file-id file-id
+ :frame-id frame-id
+ :section section
+ :index index
+ :share-id share-id
+ :interactions-mode imode
+ :share share}]])
+
+
+ :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)
+ layout (some-> params :query :layout keyword)]
+
+ [:> workspace-legacy-redirect*
+ {:project-id project-id
+ :file-id file-id
+ :page-id page-id
+ :layout layout}])
+
+ (:dashboard-legacy-search
+ :dashboard-legacy-projects
+ :dashboard-legacy-files
+ :dashboard-legacy-libraries
+ :dashboard-legacy-fonts
+ :dashboard-legacy-font-providers
+ :dashboard-legacy-team-members
+ :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)
+ search-term (some-> params :query :search-term)
+ plugin-url (some-> params :query :plugin)]
+ [:> dashboard-legacy-redirect*
+ {:team-id team-id
+ :section section
+ :project-id project-id
+ :search-term search-term
+ :plugin-url plugin-url}])
+
+ :viewer-legacy
+ (let [{:keys [query-params path-params]} route
+ {:keys [index share-id section page-id interactions-mode frame-id share]
+ :or {section :interactions interactions-mode :show-on-click}} query-params
+ {:keys [file-id]} path-params]
+
+ [:> viewer-legacy-redirect*
+ {:page-id page-id
+ :file-id file-id
+ :section section
+ :index index
+ :share-id share-id
+ :interactions-mode (keyword interactions-mode)
+ :frame-id frame-id
+ :share share}])
:frame-preview
[:& frame-preview/frame-preview]
@@ -199,4 +364,4 @@
[:> error-boundary* {:fallback static/internal-error*}
[:& notifications/current-notification]
(when route
- [:& main-page {:route route :profile profile}])])]]))
+ [:> page* {:route route :profile profile}])])]]))
diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs
index 1b5fb62b4a..7a5acbd448 100644
--- a/frontend/src/app/main/ui/auth.cljs
+++ b/frontend/src/app/main/ui/auth.cljs
@@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
- [app.main.data.users :as du]
+ [app.main.data.auth :as da]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]]
@@ -19,7 +19,6 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
-
(mf/defc auth
{::mf/props :obj}
[{:keys [route]}]
@@ -35,7 +34,7 @@
(mf/with-effect [error]
(when error
- (st/emit! (du/show-redirect-error error))))
+ (st/emit! (da/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
(when show-login-icon
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 901f0dd58e..e30b259577 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -10,9 +10,10 @@
[app.common.logging :as log]
[app.common.schema :as sm]
[app.config :as cf]
+ [app.main.data.auth :as da]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.button-link :as bl]
[app.main.ui.components.forms :as fm]
@@ -22,7 +23,6 @@
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
- [app.util.router :as rt]
[app.util.storage :as s]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -43,7 +43,7 @@
(defn create-demo-profile
[]
- (st/emit! (du/create-demo-profile)))
+ (st/emit! (da/create-demo-profile)))
(defn- store-login-redirect
[save-login-redirect]
@@ -124,7 +124,7 @@
(mf/use-fn
(fn [data]
(when-let [token (:invitation-token data)]
- (st/emit! (rt/nav :auth-verify-token {} {:token token})))))
+ (st/emit! (rt/nav :auth-verify-token {:token token})))))
on-success
(fn [data]
@@ -140,7 +140,7 @@
(let [params (with-meta (:clean-data @form)
{:on-error on-error
:on-success on-success})]
- (st/emit! (du/login params)))))
+ (st/emit! (da/login params)))))
on-submit-ldap
(mf/use-callback
@@ -154,7 +154,7 @@
params (with-meta params
{:on-error on-error
:on-success on-success})]
- (st/emit! (du/login-with-ldap params)))))
+ (st/emit! (da/login-with-ldap params)))))
default-recovery-req
(mf/use-fn
@@ -283,7 +283,7 @@
[{:keys [params] :as props}]
(let [go-register
(mf/use-fn
- #(st/emit! (rt/nav :auth-register {} params)))]
+ #(st/emit! (rt/nav :auth-register params)))]
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :auth-title)
diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs
index cc567d310c..009dcd1f14 100644
--- a/frontend/src/app/main/ui/auth/recovery.cljs
+++ b/frontend/src/app/main/ui/auth/recovery.cljs
@@ -9,11 +9,11 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[rumext.v2 :as mf]))
(def ^:private schema:recovery-form
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index afb2406477..74f19116b4 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -9,12 +9,12 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index 98cee17f1c..7b6bc2f996 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -10,16 +10,16 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.config :as cf]
+ [app.main.data.auth :as da]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.auth.login :as login]
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -74,7 +74,7 @@
on-success (fn [data]
(if (fn? on-success-callback)
(on-success-callback data)
- (st/emit! (rt/nav :auth-register-validate {} data))))]
+ (st/emit! (rt/nav :auth-register-validate data))))]
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
@@ -131,7 +131,7 @@
[:div {:class (stl/css :links)}
[:div {:class (stl/css :account)}
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
- [:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params))
+ [:& lk/link {:action #(st/emit! (rt/nav :auth-login params))
:class (stl/css :account-link)
:data-testid "login-here-link"}
(tr "auth.login-here")]]
@@ -191,10 +191,10 @@
(cond
(some? (:invitation-token params))
(let [token (:invitation-token params)]
- (st/emit! (rt/nav :auth-verify-token {} {:token token})))
+ (st/emit! (rt/nav :auth-verify-token {:token token})))
(:is-active params)
- (st/emit! (du/login-from-register))
+ (st/emit! (da/login-from-register))
:else
(do
@@ -257,7 +257,7 @@
[:div {:class (stl/css :links)}
[:div {:class (stl/css :go-back)}
- [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))
+ [:& lk/link {:action #(st/emit! (rt/nav :auth-register {}))
:class (stl/css :go-back-link)}
(tr "labels.go-back")]]]])
diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs
index 9e8bdbbd5e..ec80e4e9a5 100644
--- a/frontend/src/app/main/ui/auth/verify_token.cljs
+++ b/frontend/src/app/main/ui/auth/verify_token.cljs
@@ -6,15 +6,17 @@
(ns app.main.ui.auth.verify-token
(:require
+ [app.main.data.auth :as da]
+ [app.main.data.common :as dcm]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -25,32 +27,33 @@
[data]
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
(ts/schedule 1000 #(st/emit! (ntf/success msg)))
- (st/emit! (du/login-from-token data))))
+ (st/emit! (da/login-from-token data))))
(defmethod handle-token :change-email
[_data]
(let [msg (tr "dashboard.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (ntf/success msg)))
(st/emit! (rt/nav :settings-profile)
- (du/fetch-profile))))
+ (du/refresh-profile))))
(defmethod handle-token :auth
[tdata]
- (st/emit! (du/login-from-token tdata)))
+ (st/emit! (da/login-from-token tdata)))
(defmethod handle-token :team-invitation
[tdata]
(case (:state tdata)
:created
- (st/emit!
- (ntf/success (tr "auth.notifications.team-invitation-accepted"))
- (du/fetch-profile)
- (rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
+ (let [team-id (:team-id tdata)]
+ (st/emit!
+ (ntf/success (tr "auth.notifications.team-invitation-accepted"))
+ (du/refresh-profile)
+ (dcm/go-to-dashboard-recent :team-id team-id)))
:pending
(let [token (:invitation-token tdata)
route-id (:redirect-to tdata :auth-register)]
- (st/emit! (rt/nav route-id {} {:invitation-token token})))))
+ (st/emit! (rt/nav route-id {:invitation-token token})))))
(defmethod handle-token :default
[_tdata]
diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs
index 61a3480587..643c35d23c 100644
--- a/frontend/src/app/main/ui/comments.cljs
+++ b/frontend/src/app/main/ui/comments.cljs
@@ -253,8 +253,8 @@
:disabled disabled?}]]]))
(mf/defc comment-item
- [{:keys [comment thread users origin] :as props}]
- (let [owner (get users (:owner-id comment))
+ [{:keys [comment thread profiles origin] :as props}]
+ (let [owner (get profiles (:owner-id comment))
profile (mf/deref refs/profile)
options (mf/deref comments-local-options)
edition? (mf/use-state false)
@@ -384,7 +384,7 @@
(mf/defc thread-comments
{::mf/wrap [mf/memo]}
- [{:keys [thread zoom users origin position-modifier viewport]}]
+ [{:keys [thread zoom profiles origin position-modifier viewport]}]
(let [ref (mf/use-ref)
thread-id (:id thread)
thread-pos (:position thread)
@@ -435,13 +435,13 @@
[:div {:class (stl/css :comments)}
[:& comment-item {:comment comment
- :users users
+ :profiles profiles
:thread thread
:origin origin}]
(for [item (rest comments)]
[:* {:key (dm/str (:id item))}
[:& comment-item {:comment item
- :users users
+ :profiles profiles
:origin origin}]])]
[:& reply-form {:thread thread}]
[:div {:ref ref}]])))
@@ -573,8 +573,8 @@
[:span (:seqn thread)]]))
(mf/defc comment-thread
- [{:keys [item users on-click]}]
- (let [owner (get users (:owner-id item))
+ [{:keys [item profiles on-click]}]
+ (let [owner (get profiles (:owner-id item))
on-click*
(mf/use-fn
(mf/deps item)
@@ -613,7 +613,7 @@
[:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]))
(mf/defc comment-thread-group
- [{:keys [group users on-thread-click]}]
+ [{:keys [group profiles on-thread-click]}]
[:div {:class (stl/css :thread-group)}
(if (:file-name group)
[:div {:class (stl/css :section-title)
@@ -631,5 +631,5 @@
[:& comment-thread
{:item item
:on-click on-thread-click
- :users users
+ :profiles profiles
:key (:id item)}])]])
diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs
index edcf7af521..eb1795427e 100644
--- a/frontend/src/app/main/ui/components/color_bullet.cljs
+++ b/frontend/src/app/main/ui/components/color_bullet.cljs
@@ -16,6 +16,8 @@
(defn- color-title
[color-item]
(let [name (:name color-item)
+ path (:path color-item)
+ path-and-name (if path (str path " / " name) name)
gradient (:gradient color-item)
image (:image color-item)
color (:color color-item)]
@@ -23,16 +25,16 @@
(if (some? name)
(cond
(some? color)
- (str/ffmt "% (%)" name color)
+ (str/ffmt "% (%)" path-and-name color)
(some? gradient)
- (str/ffmt "% (%)" name (uc/gradient-type->string (:type gradient)))
+ (str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient)))
(some? image)
- (str/ffmt "% (%)" name (tr "media.image"))
+ (str/ffmt "% (%)" path-and-name (tr "media.image"))
:else
- name)
+ path-and-name)
(cond
(some? color)
diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss
index ea8a89cc5b..a2ced28ced 100644
--- a/frontend/src/app/main/ui/components/context_menu_a11y.scss
+++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss
@@ -18,6 +18,7 @@
opacity: $op-10;
visibility: visible;
}
+
&.fixed {
position: fixed;
}
@@ -35,8 +36,9 @@
border: $s-2 solid var(--panel-border-color);
background-color: var(--menu-background-color);
overflow: auto;
+
& .separator {
- height: $s-12;
+ border-top: solid $s-1 var(--color-background-quaternary);
}
&.min-width {
@@ -45,6 +47,7 @@
.context-menu-item {
display: flex;
+
.context-menu-action {
@include bodySmallTypography;
display: flex;
@@ -56,12 +59,15 @@
border-radius: $br-8;
white-space: nowrap;
color: var(--menu-foreground-color);
+
&.submenu {
display: flex;
align-items: center;
justify-content: space-between;
+
.submenu-icon {
margin-left: 0.5rem;
+
svg {
@extend .button-icon-small;
stroke: var(--menu-foreground-color);
@@ -76,6 +82,7 @@
background: none;
border: none;
cursor: pointer;
+
.submenu-icon-back svg {
@extend .button-icon-small;
stroke: var(--menu-foreground-color);
@@ -83,27 +90,34 @@
}
}
}
+
&:hover .context-menu-action {
background-color: var(--menu-background-color-hover);
text-decoration: none;
color: var(--menu-foreground-color);
+
&.submenu .submenu-icon svg {
stroke: var(--menu-foreground-color);
}
+
&.submenu-back .submenu-icon-back svg {
stroke: var(--menu-foreground-color);
}
}
+
&:focus {
outline: none;
}
+
&:focus-visible {
outline: none;
+
.context-menu-action {
border: 1px solid var(--menu-border-color-focus);
background-color: var(--menu-background-color-focus);
text-decoration: none;
color: var(--menu-foreground-color-focus);
+
&.submenu .submenu-icon svg {
stroke: var(--menu-foreground-color-focus);
}
@@ -113,15 +127,18 @@
}
}
}
+
&.selected {
.context-menu-action {
justify-content: space-between;
color: var(--menu-foreground-color-focus);
}
+
.selected-icon {
@extend .button-tag;
border-radius: $br-8;
height: 100%;
+
svg {
@extend .button-icon-small;
stroke: var(--menu-foreground-color-focus);
@@ -129,6 +146,7 @@
}
}
}
+
.is-selected .context-menu-action {
padding-left: $s-28;
background-image: url(/images/icons/tick.svg);
@@ -138,6 +156,7 @@
font-weight: $fw700;
}
}
+
&.is-selectable {
.context-menu-action {
padding-left: 1.5rem;
diff --git a/frontend/src/app/main/ui/components/copy_button.cljs b/frontend/src/app/main/ui/components/copy_button.cljs
index 912fcb9f9a..a767d931bd 100644
--- a/frontend/src/app/main/ui/components/copy_button.cljs
+++ b/frontend/src/app/main/ui/components/copy_button.cljs
@@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
- [app.main.data.events :as-alias ev]
+ [app.main.data.event :as-alias ev]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.timers :as tm]
diff --git a/frontend/src/app/main/ui/components/reorder_handler.cljs b/frontend/src/app/main/ui/components/reorder_handler.cljs
new file mode 100644
index 0000000000..8f4acfe320
--- /dev/null
+++ b/frontend/src/app/main/ui/components/reorder_handler.cljs
@@ -0,0 +1,23 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.components.reorder-handler
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
+ [rumext.v2 :as mf]))
+
+(mf/defc reorder-handler
+ {::mf/forward-ref true}
+ [_ ref]
+ [:*
+ [:div {:ref ref :class (stl/css :reorder)}
+ [:> icon*
+ {:id ic/reorder
+ :class (stl/css :reorder-icon)
+ :aria-hidden true}]]
+ [:hr {:class (stl/css :reorder-separator-top)}]
+ [:hr {:class (stl/css :reorder-separator-bottom)}]])
diff --git a/frontend/src/app/main/ui/components/reorder_handler.scss b/frontend/src/app/main/ui/components/reorder_handler.scss
new file mode 100644
index 0000000000..692a24c156
--- /dev/null
+++ b/frontend/src/app/main/ui/components/reorder_handler.scss
@@ -0,0 +1,42 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+.reorder {
+ cursor: grab;
+ display: flex;
+ flex-direction: column;
+ height: calc(100% + var(--sp-m));
+ justify-content: center;
+ left: var(--reorder-left-position, calc(-1 * var(--sp-l)));
+ position: absolute;
+ top: calc(-1 * (var(--sp-m) / 2));
+ z-index: var(--z-index-panels);
+}
+
+.reorder-icon {
+ height: var(--sp-l);
+ pointer-events: none;
+ visibility: var(--reorder-icon-visibility, hidden);
+ --icon-stroke-color: var(--color-foreground-secondary);
+}
+
+.reorder-separator-top,
+.reorder-separator-bottom {
+ border-color: var(--color-accent-primary);
+ margin: 0;
+ position: absolute;
+ width: 100%;
+}
+
+.reorder-separator-top {
+ display: var(--reorder-top-display, none);
+ top: calc(-1 * var(--sp-xxs));
+}
+
+.reorder-separator-bottom {
+ display: var(--reorder-bottom-display, none);
+ bottom: calc(-1 * var(--sp-xxs));
+}
diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs
index 060213681f..86ce8f3bfe 100644
--- a/frontend/src/app/main/ui/components/shape_icon.cljs
+++ b/frontend/src/app/main/ui/components/shape_icon.cljs
@@ -9,6 +9,7 @@
[app.common.types.component :as ctk]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
+ [app.config :as cf]
[app.main.ui.icons :as i]
[rumext.v2 :as mf]))
@@ -31,7 +32,7 @@
i/flex-grid
:else
- i/board)
+ (if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board))
;; TODO -> THUMBNAIL ICON
:image i/img
:line (if (cts/has-images? shape) i/img i/path)
@@ -56,7 +57,7 @@
(if main-instance?
i/component
(case type
- :frame i/board
+ :frame (if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board)
:image i/img
:shape i/path
:text i/text
diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs
index d192558a37..09f12f9956 100644
--- a/frontend/src/app/main/ui/context.cljs
+++ b/frontend/src/app/main/ui/context.cljs
@@ -33,4 +33,4 @@
(def is-component? (mf/create-context false))
(def sidebar (mf/create-context nil))
-(def team-permissions (mf/create-context nil))
+(def permissions (mf/create-context nil))
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 9efd0635aa..f5f495ac2e 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -7,62 +7,47 @@
(ns app.main.ui.dashboard
(:require-macros [app.main.style :as stl])
(:require
- [app.common.data :as d]
[app.common.data.macros :as dm]
- [app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.dashboard.shortcuts :as sc]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as notif]
[app.main.data.plugins :as dp]
[app.main.refs :as refs]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.context :as ctx]
- [app.main.ui.dashboard.files :refer [files-section]]
- [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
+ [app.main.ui.dashboard.files :refer [files-section*]]
+ [app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
[app.main.ui.dashboard.import]
- [app.main.ui.dashboard.libraries :refer [libraries-page]]
- [app.main.ui.dashboard.projects :refer [projects-section]]
- [app.main.ui.dashboard.search :refer [search-page]]
- [app.main.ui.dashboard.sidebar :refer [sidebar]]
- [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]]
- [app.main.ui.dashboard.templates :refer [templates-section]]
+ [app.main.ui.dashboard.libraries :refer [libraries-page*]]
+ [app.main.ui.dashboard.projects :refer [projects-section*]]
+ [app.main.ui.dashboard.search :refer [search-page*]]
+ [app.main.ui.dashboard.sidebar :refer [sidebar*]]
+ [app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]]
+ [app.main.ui.dashboard.templates :refer [templates-section*]]
[app.main.ui.hooks :as hooks]
+ [app.main.ui.modal :refer [modal-container*]]
[app.main.ui.workspace.plugins]
[app.plugins.register :as preg]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[goog.events :as events]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
-(defn ^boolean uuid-str?
- [s]
- (and (string? s)
- (boolean (re-seq us/uuid-rx s))))
-
-(defn- parse-params
- [route]
- (let [search-term (get-in route [:params :query :search-term])
- team-id (get-in route [:params :path :team-id])
- project-id (get-in route [:params :path :project-id])]
- (cond-> {:search-term search-term}
- (uuid-str? team-id)
- (assoc :team-id (uuid team-id))
-
- (uuid-str? project-id)
- (assoc :project-id (uuid project-id)))))
-
-(mf/defc dashboard-content
- [{:keys [team projects project section search-term profile] :as props}]
+(mf/defc dashboard-content*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [team projects project section search-term profile default-project]}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)
+
project-id (:id project)
team-id (:id team)
@@ -72,10 +57,7 @@
file-menu-open? (:menu-open dashboard-local)
default-project-id
- (mf/with-memo [projects]
- (->> (vals projects)
- (d/seek :is-default)
- (:id)))
+ (get default-project :id)
on-resize
(mf/use-fn
@@ -88,7 +70,7 @@
(mf/use-fn
#(st/emit! (dd/clear-selected-files)))
- show-templates
+ show-templates?
(and (contains? cf/flags :dashboard-templates-section)
(:can-edit permissions))]
@@ -103,61 +85,65 @@
:on-click clear-selected-fn
:ref container}
(case section
- :dashboard-projects
- [:*
- [:& projects-section
- {:team team
- :projects projects
- :profile profile
- :default-project-id default-project-id}]
+ :dashboard-recent
+ (when (seq projects)
+ [:*
+ [:> projects-section*
+ {:team team
+ :projects projects
+ :profile profile}]
- (when show-templates
- [:& templates-section {:profile profile
- :project-id project-id
- :team-id team-id
- :default-project-id default-project-id
- :content-width @content-width}])]
+ (when ^boolean show-templates?
+ [:> templates-section*
+ {:profile profile
+ :project-id project-id
+ :team-id team-id
+ :default-project-id default-project-id
+ :content-width @content-width}])])
:dashboard-fonts
- [:& fonts-page {:team team}]
+ [:> fonts-page* {:team team}]
:dashboard-font-providers
- [:& font-providers-page {:team team}]
+ [:> font-providers-page* {:team team}]
:dashboard-files
(when project
[:*
- [:& files-section {:team team :project project}]
- (when show-templates
- [:& templates-section {:profile profile
- :team-id team-id
- :project-id project-id
- :default-project-id default-project-id
- :content-width @content-width}])])
+ [:> files-section* {:team team
+ :project project}]
+ (when ^boolean show-templates?
+ [:> templates-section*
+ {:profile profile
+ :team-id team-id
+ :project-id project-id
+ :default-project-id default-project-id
+ :content-width @content-width}])])
:dashboard-search
- [:& search-page {:team team
- :search-term search-term}]
+ [:> search-page* {:team team
+ :search-term search-term}]
:dashboard-libraries
- [:& libraries-page {:team team}]
+ [:> libraries-page* {:team team
+ :default-project default-project}]
- :dashboard-team-members
- [:& team-members-page {:team team :profile profile}]
+ :dashboard-members
+ [:> team-members-page* {:team team :profile profile}]
- :dashboard-team-invitations
- [:& team-invitations-page {:team team}]
+ :dashboard-invitations
+ [:> team-invitations-page* {:team team}]
- :dashboard-team-webhooks
- [:& team-webhooks-page {:team team}]
+ :dashboard-webhooks
+ [:> webhooks-page* {:team team}]
- :dashboard-team-settings
- [:& team-settings-page {:team team :profile profile}]
+ :dashboard-settings
+ [:> team-settings-page* {:team team :profile profile}]
nil)]))
(def ref:dashboard-initialized
- (l/derived :current-team-initialized st/state))
+ (l/derived :team-initialized st/state))
(defn use-plugin-register
[plugin-url team-id project-id]
@@ -167,8 +153,9 @@
(st/emit!
(dp/delay-open-plugin plugin)
(rt/nav :workspace
- {:project-id project-id :file-id id}
- {:page-id (dm/get-in data [:pages 0])})))
+ {:page-id (dm/get-in data [:pages 0])
+ :project-id project-id
+ :file-id id})))
create-file!
(fn [plugin]
@@ -198,11 +185,11 @@
:on-accept
#(do (preg/install-plugin! plugin)
(st/emit! (modal/hide)
- (rt/nav :dashboard-projects {:team-id team-id})
+ (rt/nav :dashboard-recent {:team-id team-id})
(open-try-out-dialog plugin)))
:on-close
#(st/emit! (modal/hide)
- (rt/nav :dashboard-projects {:team-id team-id}))}))]
+ (rt/nav :dashboard-recent {:team-id team-id}))}))]
(mf/with-layout-effect
[plugin-url team-id project-id]
@@ -218,33 +205,29 @@
(fn [_]
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
-(mf/defc dashboard
+(mf/defc dashboard*
{::mf/props :obj}
- [{:keys [route profile]}]
- (let [section (get-in route [:data :name])
- params (parse-params route)
+ [{:keys [profile project-id team-id search-term plugin-url section]}]
+ (let [team (mf/deref refs/team)
+ projects (mf/deref refs/projects)
- project-id (:project-id params)
+ project (get projects project-id)
+ projects (mf/with-memo [projects team-id]
+ (->> (vals projects)
+ (filterv #(= team-id (:team-id %)))))
- team-id (:team-id params)
- search-term (:search-term params)
-
- plugin-url (-> route :query-params :plugin)
-
- team (mf/deref refs/team)
- projects (mf/deref refs/dashboard-projects)
- project (get projects project-id)
-
- default-project (->> projects vals (d/seek :is-default))
-
- initialized? (mf/deref ref:dashboard-initialized)]
+ default-project
+ (mf/with-memo [projects]
+ (->> projects
+ (filter :is-default)
+ (first)))]
(hooks/use-shortcuts ::dashboard sc/shortcuts)
- (mf/with-effect [team-id]
- (st/emit! (dd/initialize {:id team-id}))
+ (mf/with-effect []
+ (st/emit! (dd/initialize))
(fn []
- (st/emit! (dd/finalize {:id team-id}))))
+ (st/emit! (dd/finalize))))
(mf/with-effect []
(let [key (events/listen goog/global "keydown"
@@ -257,31 +240,30 @@
(use-plugin-register plugin-url team-id (:id default-project))
- [:& (mf/provider ctx/current-team-id) {:value team-id}
- [:& (mf/provider ctx/current-project-id) {:value project-id}
- [:& (mf/provider ctx/team-permissions) {:value (:permissions team)}
- ;; NOTE: dashboard events and other related functions assumes
- ;; that the team is a implicit context variable that is
- ;; available using react context or accessing
- ;; the :current-team-id on the state. We set the key to the
- ;; team-id because we want to completely refresh all the
- ;; components on team change. Many components assumes that the
- ;; team is already set so don't put the team into mf/deps.
- (when (and team initialized?)
- [:main {:class (stl/css :dashboard)
- :key (:id team)}
- [:& sidebar
- {:team team
- :projects projects
- :project project
- :profile profile
- :section section
- :search-term search-term}]
- (when (and team profile (seq projects))
- [:& dashboard-content
- {:projects projects
- :profile profile
- :project project
- :section section
- :search-term search-term
- :team team}])])]]]))
+ [:& (mf/provider ctx/current-project-id) {:value project-id}
+ [:> modal-container*]
+ ;; NOTE: dashboard events and other related functions assumes
+ ;; that the team is a implicit context variable that is
+ ;; available using react context or accessing
+ ;; the :current-team-id on the state. We set the key to the
+ ;; team-id because we want to completely refresh all the
+ ;; components on team change. Many components assumes that the
+ ;; team is already set so don't put the team into mf/deps.
+ [:main {:class (stl/css :dashboard)
+ :key (dm/str (:id team))}
+ [:> sidebar*
+ {:team team
+ :projects projects
+ :project project
+ :default-project default-project
+ :profile profile
+ :section section
+ :search-term search-term}]
+ [:> dashboard-content*
+ {:projects projects
+ :profile profile
+ :project project
+ :default-project default-project
+ :section section
+ :search-term search-term
+ :team team}]]]))
diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs
index d87056e00d..2118b96ddb 100644
--- a/frontend/src/app/main/ui/dashboard/change_owner.cljs
+++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs
@@ -9,7 +9,6 @@
(:require
[app.common.schema :as sm]
[app.main.data.modal :as modal]
- [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
@@ -25,8 +24,7 @@
::mf/register-as :leave-and-reassign}
[{:keys [profile team accept]}]
(let [form (fm/use-form :schema schema:leave-modal-form :initial {})
- members-map (mf/deref refs/dashboard-team-members)
- members (vals members-map)
+ members (get team :members)
options
(into [{:value ""
diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs
index 7a34437cc3..c2b77134ae 100644
--- a/frontend/src/app/main/ui/dashboard/comments.cljs
+++ b/frontend/src/app/main/ui/dashboard/comments.cljs
@@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.comments :as dcm]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.workspace.comments :as dwcm]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -63,7 +63,7 @@
(mf/defc comments-section
[{:keys [profile team show? on-hide-comments]}]
(let [threads-map (mf/deref refs/comment-threads)
- users (mf/deref refs/current-team-comments-users)
+ profiles (mf/deref refs/profiles)
team-id (:id team)
tgroups (->> (vals threads-map)
@@ -114,13 +114,13 @@
{:group (first tgroups)
:on-thread-click on-navigate
:show-file-name true
- :users users}]
+ :profiles profiles}]
(for [tgroup (rest tgroups)]
[:& cmt/comment-thread-group
{:group tgroup
:on-thread-click on-navigate
:show-file-name true
- :users users
+ :profiles profiles
:key (:page-id tgroup)}])]
[:div {:class (stl/css :thread-groups-placeholder)}
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index 9a0926110c..f3ef69ca40 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -9,18 +9,17 @@
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as-alias ev]
+ [app.main.data.event :as-alias ev]
[app.main.data.exports.files :as fexp]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.refs :as refs]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.context :as ctx]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -57,9 +56,8 @@
(mf/defc file-menu*
{::mf/props :obj}
- [{:keys [files show on-edit on-menu-close top left navigate origin parent-id can-edit]}]
+ [{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}]
(assert (seq files) "missing `files` prop")
- (assert (boolean? show) "missing `show` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
(assert (boolean? navigate) "missing `navigate` prop")
@@ -74,21 +72,19 @@
multi? (> file-count 1)
current-team-id (mf/use-ctx ctx/current-team-id)
- teams (mf/use-state nil)
- default-team (-> (mf/deref refs/teams)
- (get current-team-id))
+ teams* (mf/use-state nil)
+ teams (deref teams*)
- current-team (or (get @teams current-team-id) default-team)
- other-teams (remove #(= (:id %) current-team-id) (vals @teams))
+ current-team (get teams current-team-id)
+ other-teams (remove #(= (:id %) current-team-id) (vals teams))
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
on-new-tab
(fn [_]
- (let [path-params {:project-id (:project-id file)
- :file-id (:id file)}]
- (st/emit! (rt/nav-new-window* {:rname :workspace
- :path-params path-params}))))
+ (st/emit! (dcm/go-to-workspace
+ {:file-id (:id file)
+ ::rt/new-window true})))
on-duplicate
(fn [_]
@@ -134,7 +130,9 @@
(st/emit! (ntf/success (tr "dashboard.success-move-files")))
(st/emit! (ntf/success (tr "dashboard.success-move-file"))))
(if (or navigate (not= team-id current-team-id))
- (st/emit! (dd/go-to-files team-id project-id))
+ (st/emit! (dcm/go-to-dashboard-files
+ {:project-id project-id
+ :team-id team-id}))
(st/emit! (dd/fetch-recent-files)
(dd/clear-selected-files))))
@@ -207,142 +205,134 @@
on-export-standard-files
(mf/use-fn
(mf/deps on-export-files)
- (partial on-export-files :legacy-zip))
+ (partial on-export-files :legacy-zip))]
- ;; NOTE: this is used for detect if component is still mounted
- mounted-ref (mf/use-ref true)]
+ (mf/with-effect []
+ (->> (rp/cmd! :get-all-projects)
+ (rx/map group-by-team)
+ (rx/subs! #(reset! teams* %))))
- (mf/use-effect
- (mf/deps show)
- (fn []
- (when show
- (->> (rp/cmd! :get-all-projects)
- (rx/map group-by-team)
- (rx/subs! #(when (mf/ref-val mounted-ref)
- (reset! teams %)))))))
+ (let [sub-options
+ (concat
+ (for [project current-projects]
+ {:name (get-project-name project)
+ :id (get-project-id project)
+ :handler (on-move (:id current-team)
+ (:id project))})
+ (when (seq other-teams)
+ [{:name (tr "dashboard.move-to-other-team")
+ :id "move-to-other-team"
+ :options
+ (for [team other-teams]
+ {:name (get-team-name team)
+ :id (get-project-id team)
+ :options
+ (for [sub-project (:projects team)]
+ {:name (get-project-name sub-project)
+ :id (get-project-id sub-project)
+ :handler (on-move (:id team)
+ (:id sub-project))})})}]))
- (when current-team
- (let [sub-options
- (concat
- (for [project current-projects]
- {:name (get-project-name project)
- :id (get-project-id project)
- :handler (on-move (:id current-team)
- (:id project))})
- (when (seq other-teams)
- [{:name (tr "dashboard.move-to-other-team")
- :id "move-to-other-team"
- :options
- (for [team other-teams]
- {:name (get-team-name team)
- :id (get-project-id team)
- :options
- (for [sub-project (:projects team)]
- {:name (get-project-name sub-project)
- :id (get-project-id sub-project)
- :handler (on-move (:id team)
- (:id sub-project))})})}]))
+ options
+ (if multi?
+ [(when can-edit
+ {:name (tr "dashboard.duplicate-multi" file-count)
+ :id "duplicate-multi"
+ :handler on-duplicate})
- options
- (if multi?
- [(when can-edit
- {:name (tr "dashboard.duplicate-multi" file-count)
- :id "duplicate-multi"
- :handler on-duplicate})
+ (when (and (or (seq current-projects) (seq other-teams)) can-edit)
+ {:name (tr "dashboard.move-to-multi" file-count)
+ :id "file-move-multi"
+ :options sub-options})
- (when (and (or (seq current-projects) (seq other-teams)) can-edit)
- {:name (tr "dashboard.move-to-multi" file-count)
- :id "file-move-multi"
- :options sub-options})
+ (when-not (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.export-binary-multi" file-count)
+ :id "file-binary-export-multi"
+ :handler on-export-binary-files})
- (when-not (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.export-binary-multi" file-count)
- :id "file-binary-export-multi"
- :handler on-export-binary-files})
+ (when (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.export-binary-multi" file-count)
+ :id "file-binary-export-multi"
+ :handler on-export-binary-files-v3})
- (when (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.export-binary-multi" file-count)
- :id "file-binary-export-multi"
- :handler on-export-binary-files-v3})
+ (when-not (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.export-standard-multi" file-count)
+ :id "file-standard-export-multi"
+ :handler on-export-standard-files})
- (when-not (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.export-standard-multi" file-count)
- :id "file-standard-export-multi"
- :handler on-export-standard-files})
+ (when (and (:is-shared file) can-edit)
+ {:name (tr "labels.unpublish-multi-files" file-count)
+ :id "file-unpublish-multi"
+ :handler on-del-shared})
- (when (and (:is-shared file) can-edit)
- {:name (tr "labels.unpublish-multi-files" file-count)
- :id "file-unpublish-multi"
- :handler on-del-shared})
+ (when (and (not is-lib-page?) can-edit)
+ {:name :separator}
+ {:name (tr "labels.delete-multi-files" file-count)
+ :id "file-delete-multi"
+ :handler on-delete})]
- (when (and (not is-lib-page?) can-edit)
- {:name :separator}
- {:name (tr "labels.delete-multi-files" file-count)
- :id "file-delete-multi"
- :handler on-delete})]
+ [{:name (tr "dashboard.open-in-new-tab")
+ :id "file-open-new-tab"
+ :handler on-new-tab}
+ (when (and (not is-search-page?) can-edit)
+ {:name (tr "labels.rename")
+ :id "file-rename"
+ :handler on-edit})
- [{:name (tr "dashboard.open-in-new-tab")
- :id "file-open-new-tab"
- :handler on-new-tab}
- (when (and (not is-search-page?) can-edit)
- {:name (tr "labels.rename")
- :id "file-rename"
- :handler on-edit})
+ (when (and (not is-search-page?) can-edit)
+ {:name (tr "dashboard.duplicate")
+ :id "file-duplicate"
+ :handler on-duplicate})
- (when (and (not is-search-page?) can-edit)
- {:name (tr "dashboard.duplicate")
- :id "file-duplicate"
- :handler on-duplicate})
+ (when (and (not is-lib-page?)
+ (not is-search-page?)
+ (or (seq current-projects) (seq other-teams))
+ can-edit)
+ {:name (tr "dashboard.move-to")
+ :id "file-move-to"
+ :options sub-options})
- (when (and (not is-lib-page?)
- (not is-search-page?)
- (or (seq current-projects) (seq other-teams))
- can-edit)
- {:name (tr "dashboard.move-to")
- :id "file-move-to"
- :options sub-options})
+ (when (and (not is-search-page?)
+ can-edit)
+ (if (:is-shared file)
+ {:name (tr "dashboard.unpublish-shared")
+ :id "file-del-shared"
+ :handler on-del-shared}
+ {:name (tr "dashboard.add-shared")
+ :id "file-add-shared"
+ :handler on-add-shared}))
- (when (and (not is-search-page?)
- can-edit)
- (if (:is-shared file)
- {:name (tr "dashboard.unpublish-shared")
- :id "file-del-shared"
- :handler on-del-shared}
- {:name (tr "dashboard.add-shared")
- :id "file-add-shared"
- :handler on-add-shared}))
+ {:name :separator}
- {:name :separator}
+ (when-not (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.download-binary-file")
+ :id "download-binary-file"
+ :handler on-export-binary-files})
- (when-not (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.download-binary-file")
- :id "download-binary-file"
- :handler on-export-binary-files})
+ (when (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.download-binary-file")
+ :id "download-binary-file"
+ :handler on-export-binary-files-v3})
- (when (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.download-binary-file")
- :id "download-binary-file"
- :handler on-export-binary-files-v3})
+ (when-not (contains? cf/flags :export-file-v3)
+ {:name (tr "dashboard.download-standard-file")
+ :id "download-standard-file"
+ :handler on-export-standard-files})
- (when-not (contains? cf/flags :export-file-v3)
- {:name (tr "dashboard.download-standard-file")
- :id "download-standard-file"
- :handler on-export-standard-files})
+ (when (and (not is-lib-page?) (not is-search-page?) can-edit)
+ {:name :separator})
- (when (and (not is-lib-page?) (not is-search-page?) can-edit)
- {:name :separator})
+ (when (and (not is-lib-page?) (not is-search-page?) can-edit)
+ {:name (tr "labels.delete")
+ :id "file-delete"
+ :handler on-delete})])]
- (when (and (not is-lib-page?) (not is-search-page?) can-edit)
- {:name (tr "labels.delete")
- :id "file-delete"
- :handler on-delete})])]
-
- [:> context-menu*
- {:on-close on-menu-close
- :show show
- :fixed (or (not= top 0) (not= left 0))
- :min-width true
- :top top
- :left left
- :options options
- :origin parent-id}]))))
+ [:> context-menu*
+ {:on-close on-menu-close
+ :fixed (or (not= top 0) (not= left 0))
+ :show true
+ :min-width true
+ :top top
+ :left left
+ :options options
+ :origin parent-id}])))
diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs
index 019e7b5025..8572cf103f 100644
--- a/frontend/src/app/main/ui/dashboard/files.cljs
+++ b/frontend/src/app/main/ui/dashboard/files.cljs
@@ -7,8 +7,10 @@
(ns app.main.ui.dashboard.files
(:require-macros [app.main.style :as stl])
(:require
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
+ [app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
@@ -21,7 +23,6 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
- [app.util.router :as rt]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -32,9 +33,12 @@
{::mf/props :obj
::mf/private true}
[{:keys [project create-fn can-edit]}]
- (let [local (mf/use-state
- {:menu-open false
- :edition false})
+ (let [project-id (:id project)
+
+ local
+ (mf/use-state
+ {:menu-open false
+ :edition false})
on-create-click
(mf/use-fn
@@ -63,9 +67,9 @@
on-import
(mf/use-fn
- (mf/deps (:id project))
+ (mf/deps project-id)
(fn []
- (st/emit! (dd/fetch-files {:project-id (:id project)})
+ (st/emit! (dpj/fetch-files project-id)
(dd/clear-selected-files))))]
@@ -126,32 +130,37 @@
:on-menu-close on-menu-close
:on-import on-import}])]]))
-(mf/defc files-section
+(mf/defc files-section*
{::mf/props :obj}
[{:keys [project team]}]
- (let [files-map (mf/deref refs/dashboard-files)
- can-edit? (-> team :permissions :can-edit)
- project-id (:id project)
- is-draft-proyect (:is-default project)
+ (let [files (mf/deref refs/files)
+ project-id (get project :id)
- [rowref limit] (hooks/use-dynamic-grid-item-width)
+ files (mf/with-memo [project-id files]
+ (->> (vals files)
+ (filter #(= project-id (:project-id %)))
+ (sort-by :modified-at)
+ (reverse)))
- files (mf/with-memo [project-id files-map]
- (->> (vals files-map)
- (filter #(= project-id (:project-id %)))
- (sort-by :modified-at)
- (reverse)))
- file-count (or (count files) 0)
+
+ can-edit? (-> team :permissions :can-edit)
+ project-id (:id project)
+ is-draft-proyect (:is-default project)
+
+ [rowref limit] (hooks/use-dynamic-grid-item-width)
+
+ file-count (or (count files) 0)
empty-state-viewer (and (not can-edit?)
(= 0 file-count))
+ selected-files (mf/deref refs/selected-files)
+
on-file-created
(mf/use-fn
- (fn [data]
- (let [pparams {:project-id (:project-id data)
- :file-id (:id data)}
- qparams {:page-id (get-in data [:data :pages 0])}]
- (st/emit! (rt/nav :workspace pparams qparams)))))
+ (fn [file-data]
+ (let [file-id (:id file-data)
+ page-id (get-in file-data [:pages 0])]
+ (st/emit! (dcm/go-to-workspace :file-id file-id :page-id page-id)))))
create-file
(mf/use-fn
@@ -170,7 +179,7 @@
(dom/set-html-title (tr "title.dashboard.files" pname)))))
(mf/with-effect [project-id]
- (st/emit! (dd/fetch-files {:project-id project-id})
+ (st/emit! (dpj/fetch-files project-id)
(dd/clear-selected-files)))
[:*
@@ -191,6 +200,7 @@
(tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& grid {:project project
:files files
+ :selected-files selected-files
:can-edit can-edit?
:origin :files
:create-fn create-file
diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs
index fecf68eb0e..7ae210b5ff 100644
--- a/frontend/src/app/main/ui/dashboard/fonts.cljs
+++ b/frontend/src/app/main/ui/dashboard/fonts.cljs
@@ -11,7 +11,6 @@
[app.common.media :as cm]
[app.main.data.fonts :as df]
[app.main.data.modal :as modal]
- [app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
@@ -24,6 +23,7 @@
[app.util.keyboard :as kbd]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
+ [okulary.core :as l]
[rumext.v2 :as mf]))
(defn- use-page-title
@@ -42,7 +42,7 @@
(and (contains? font :font-family-tmp)
(str/blank? (:font-family-tmp font))))
-(mf/defc header
+(mf/defc header*
{::mf/props :obj
::mf/memo true
::mf/private true}
@@ -52,7 +52,7 @@
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.fonts")]]])
-(mf/defc font-variant-display-name
+(mf/defc font-variant-display-name*
{::mf/props :obj
::mf/private true}
[{:keys [variant]}]
@@ -61,10 +61,10 @@
(when (not= "normal" (:font-style variant))
[:span " " (str/capital (:font-style variant))])])
-(mf/defc uploaded-fonts
+(mf/defc uploaded-fonts*
{::mf/props :obj
::mf/private true}
- [{:keys [team installed-fonts] :as props}]
+ [{:keys [team installed-fonts]}]
(let [fonts* (mf/use-state {})
fonts (deref fonts*)
font-vals (mf/with-memo [fonts]
@@ -219,7 +219,7 @@
:default-value (:font-family item)}]]
[:div {:class (stl/css :table-field :variants)}
[:span {:class (stl/css :label)}
- [:& font-variant-display-name {:variant item}]]]
+ [:> font-variant-display-name* {:variant item}]]]
[:div {:class (stl/css :table-field :filenames)}
(for [item (:names item)]
@@ -364,7 +364,7 @@
:inhert-variant (not can-edit))
:key (dm/str id)}
[:span {:class (stl/css :label)}
- [:& font-variant-display-name {:variant item}]]
+ [:> font-variant-display-name* {:variant item}]]
(when can-edit
[:span
{:class (stl/css :icon :close)
@@ -396,8 +396,9 @@
:on-delete on-delete-font
:on-edit on-edit}]]))]))
-(mf/defc installed-fonts
- [{:keys [fonts can-edit] :as props}]
+(mf/defc installed-fonts*
+ {::mf/props :obj}
+ [{:keys [fonts can-edit]}]
(let [sterm (mf/use-state "")
matches?
@@ -445,26 +446,27 @@
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
:type 2}]))]))
+(def ^:private ref:fonts
+ (l/derived :fonts st/state))
-(mf/defc fonts-page
+(mf/defc fonts-page*
{::mf/props :obj}
[{:keys [team]}]
- (let [fonts (mf/deref refs/dashboard-fonts)
+ (let [fonts (mf/deref ref:fonts)
permissions (:permissions team)
can-edit (:can-edit permissions)]
[:*
- [:& header {:team team :section :fonts}]
+ [:> header* {:team team :section :fonts}]
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
(when ^boolean can-edit
- [:& uploaded-fonts {:team team :installed-fonts fonts}])
- [:& installed-fonts {:team team :fonts fonts :can-edit can-edit}]]]))
+ [:> uploaded-fonts* {:team team :installed-fonts fonts}])
+ [:> installed-fonts*
+ {:team team :fonts fonts :can-edit can-edit}]]]))
-(mf/defc font-providers-page
+(mf/defc font-providers-page*
{::mf/props :obj}
[{:keys [team]}]
[:*
- [:& header {:team team :section :providers}]
+ [:> header* {:team team :section :providers}]
[:section {:class (stl/css :dashboard-container)}
[:span "font providers"]]])
-
-
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index ccd9de1970..8bc7fb6573 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -12,8 +12,11 @@
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.config :as cf]
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[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]
@@ -71,11 +74,18 @@
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn))))
-(mf/defc grid-item-thumbnail
- {::mf/wrap-props false}
- [{:keys [file-id revn thumbnail-id background-color can-edit]}]
- (let [container (mf/use-ref)
- visible? (h/use-visible container :once? true)]
+(mf/defc grid-item-thumbnail*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [can-edit file]}]
+ (let [file-id (get file :id)
+ revn (get file :revn)
+ thumbnail-id (get file :thumbnail-id)
+
+ bg-color (dm/get-in file [:data :background])
+
+ container (mf/use-ref)
+ visible? (h/use-visible container :once? true)]
(mf/with-effect [file-id revn visible? thumbnail-id]
(when (and visible? (not thumbnail-id))
@@ -89,7 +99,7 @@
:message (ex-message cause)))))))
[:div {:class (stl/css :grid-item-th)
- :style {:background-color background-color}
+ :style {:background-color bg-color}
:ref container}
(when visible?
(if thumbnail-id
@@ -108,10 +118,9 @@
(def ^:private menu-icon
(i/icon-xref :menu (stl/css :menu-icon)))
-(mf/defc grid-item-library
- {::mf/wrap [mf/memo]}
- [{:keys [file] :as props}]
-
+(mf/defc grid-item-library*
+ {::mf/props :obj}
+ [{:keys [file]}]
(mf/with-effect [file]
(when file
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
@@ -231,16 +240,12 @@
(dom/set-text! counter-el (str file-count))
counter-el))
-(mf/defc grid-item
- {:wrap [mf/memo]}
- [{:keys [file origin library-view? can-edit] :as props}]
+(mf/defc grid-item*
+ {::mf/props :obj}
+ [{:keys [file origin can-edit selected-files]}]
(let [file-id (:id file)
- ;; FIXME: this breaks react hooks rule, hooks should never to
- ;; be in a conditional code
- selected-files (if (= origin :search)
- (mf/deref refs/dashboard-selected-search)
- (mf/deref refs/dashboard-selected-files))
+ is-library-view (= origin :libraries)
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
@@ -267,12 +272,12 @@
on-navigate
(mf/use-fn
- (mf/deps file)
+ (mf/deps file-id)
(fn [event]
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
- (st/emit! (dd/go-to-workspace file))))))
+ (st/emit! (dcm/go-to-workspace :file-id file-id))))))
on-drag-start
(mf/use-fn
@@ -354,9 +359,12 @@
(on-select event)) ;; TODO Fix this
)))]
- [:li {:class (stl/css-case :grid-item true :project-th true :library library-view?)}
+ [:li {:class (stl/css-case :grid-item true
+ :project-th true
+ :library is-library-view)}
[:div
- {:class (stl/css-case :selected selected? :library library-view?)
+ {:class (stl/css-case :selected selected?
+ :library is-library-view)
:ref node-ref
:role "button"
:title (:name file)
@@ -369,16 +377,11 @@
[:div {:class (stl/css :overlay)}]
- (if library-view?
- [:& grid-item-library {:file file}]
- [:& grid-item-thumbnail
- {:file-id (:id file)
- :can-edit can-edit
- :revn (:revn file)
- :thumbnail-id (:thumbnail-id file)
- :background-color (dm/get-in file [:data :options :background])}])
+ (if ^boolean is-library-view
+ [:> grid-item-library* {:file file}]
+ [:> grid-item-thumbnail* {:file file :can-edit can-edit}])
- (when (and (:is-shared file) (not library-view?))
+ (when (and (:is-shared file) (not is-library-view))
[:div {:class (stl/css :item-badge)} i/library])
[:div {:class (stl/css :info-wrapper)}
@@ -406,7 +409,6 @@
;; so the menu can be handled
[:div {:style {:pointer-events "all"}}
[:> file-menu* {:files (vals selected-files)
- :show (:menu-open dashboard-local)
:left (+ 24 (:x (:menu-pos dashboard-local)))
:top (:y (:menu-pos dashboard-local))
:can-edit can-edit
@@ -417,7 +419,8 @@
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
(mf/defc grid
- [{:keys [files project origin limit library-view? create-fn can-edit] :as props}]
+ {::mf/props :obj}
+ [{:keys [files project origin limit create-fn can-edit selected-files]}]
(let [dragging? (mf/use-state false)
project-id (:id project)
node-ref (mf/use-var nil)
@@ -425,10 +428,12 @@
on-finish-import
(mf/use-fn
(fn []
- (st/emit! (dd/fetch-files {:project-id project-id})
- (dd/fetch-shared-files)
+ (st/emit! (dpj/fetch-files project-id)
+ (dtm/fetch-shared-files)
(dd/clear-selected-files))))
+
+
import-files (use-import-file project-id on-finish-import)
on-drag-enter
@@ -484,13 +489,12 @@
(when @dragging?
[:li {:class (stl/css :grid-item)}])
(for [item slice]
- [:& grid-item
+ [:> grid-item*
{:file item
- :key (:id item)
- :navigate? true
+ :key (dm/str (:id item))
:origin origin
- :can-edit can-edit
- :library-view? library-view?}])])
+ :selected-files selected-files
+ :can-edit can-edit}])])
:else
[:& empty-placeholder
@@ -510,13 +514,12 @@
[:li {:class (stl/css :grid-item :dragged)}])
(for [item (take limit files)]
- [:& grid-item
+ [:> grid-item*
{:id (:id item)
:file item
:selected-files selected-files
:can-edit can-edit
- :key (:id item)
- :navigate? false}])]))
+ :key (dm/str (:id item))}])]))
(mf/defc line-grid
[{:keys [project team files limit create-fn can-edit] :as props}]
@@ -524,8 +527,8 @@
project-id (:id project)
team-id (:id team)
- selected-files (mf/deref refs/dashboard-selected-files)
- selected-project (mf/deref refs/dashboard-selected-project)
+ selected-files (mf/deref refs/selected-files)
+ selected-project (mf/deref refs/selected-project)
on-finish-import
(mf/use-fn
diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs
index 21f793c7e2..c9fb7df2b0 100644
--- a/frontend/src/app/main/ui/dashboard/import.cljs
+++ b/frontend/src/app/main/ui/dashboard/import.cljs
@@ -11,7 +11,7 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.errors :as errors]
diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss
index fbcad58fed..257134dc4b 100644
--- a/frontend/src/app/main/ui/dashboard/import.scss
+++ b/frontend/src/app/main/ui/dashboard/import.scss
@@ -64,6 +64,7 @@
}
.file-entry {
+ display: flex;
.file-name {
@include flexRow;
.file-icon {
@@ -114,6 +115,8 @@
}
.error-message,
.progress-message {
+ display: flex;
+ align-items: center;
height: $s-32;
color: var(--modal-text-foreground-color);
}
diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs
index e883c5f0c5..1ba3e856f1 100644
--- a/frontend/src/app/main/ui/dashboard/libraries.cljs
+++ b/frontend/src/app/main/ui/dashboard/libraries.cljs
@@ -7,9 +7,8 @@
(ns app.main.ui.dashboard.libraries
(:require-macros [app.main.style :as stl])
(:require
- [app.common.data :as d]
[app.main.data.dashboard :as dd]
- [app.main.features :as features]
+ [app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
@@ -18,35 +17,32 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
-(mf/defc libraries-page
+(mf/defc libraries-page*
{::mf/props :obj}
- [{:keys [team] :as props}]
- (let [files-map (mf/deref refs/dashboard-shared-files)
- projects (mf/deref refs/dashboard-projects)
- can-edit (-> team :permissions :can-edit)
+ [{:keys [team default-project]}]
+ (let [files
+ (mf/deref refs/shared-files)
- default-project (->> projects vals (d/seek :is-default))
+ files
+ (mf/with-memo [files]
+ (->> (vals files)
+ (sort-by :modified-at)
+ (reverse)))
- files (mf/with-memo [files-map]
- (if (nil? files-map)
- nil
- (->> (vals files-map)
- (sort-by :modified-at)
- (reverse))))
+ can-edit
+ (-> team :permissions :can-edit)
- components-v2 (features/use-feature "components/v2")
-
- [rowref limit] (hooks/use-dynamic-grid-item-width 350)]
+ [rowref limit]
+ (hooks/use-dynamic-grid-item-width 350)]
(mf/with-effect [team]
- (when team
- (let [tname (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team))]
- (dom/set-html-title (tr "title.dashboard.shared-libraries" tname)))))
+ (let [tname (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))]
+ (dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
- (mf/with-effect []
- (st/emit! (dd/fetch-shared-files (:id team))
+ (mf/with-effect [team]
+ (st/emit! (dtm/fetch-shared-files)
(dd/clear-selected-files)))
[:*
@@ -58,6 +54,5 @@
:project default-project
:origin :libraries
:limit limit
- :can-edit can-edit
- :library-view? components-v2}]]]))
+ :can-edit can-edit}]]]))
diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs
index f99da5d3da..367fca95ef 100644
--- a/frontend/src/app/main/ui/dashboard/project_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui.dashboard.project-menu
(:require
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
@@ -16,7 +17,6 @@
[app.main.ui.dashboard.import :as udi]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[rumext.v2 :as mf]))
(mf/defc project-menu*
@@ -32,9 +32,9 @@
on-duplicate-success
(fn [new-project]
(st/emit! (ntf/success (tr "dashboard.success-duplicate-project"))
- (rt/nav :dashboard-files
- {:team-id (:team-id new-project)
- :project-id (:id new-project)})))
+ (dcm/go-to-dashboard-files
+ :team-id (:team-id new-project)
+ :project-id (:id new-project))))
on-duplicate
(fn []
@@ -46,7 +46,7 @@
on-move-success
(fn [team-id]
- (st/emit! (dd/go-to-projects team-id)))
+ (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))
on-move
(fn [team-id]
@@ -57,9 +57,10 @@
delete-fn
(fn [_]
- (st/emit! (ntf/success (tr "dashboard.success-delete-project"))
- (dd/delete-project project)
- (dd/go-to-projects (:team-id project))))
+ (let [team-id (:team-id project)]
+ (st/emit! (ntf/success (tr "dashboard.success-delete-project"))
+ (dd/delete-project project)
+ (dcm/go-to-dashboard-recent :team-id team-id))))
on-delete
#(st/emit!
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index 611f205cf3..4f76b121f0 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -8,9 +8,11 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.geom.point :as gpt]
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
+ [app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [line-grid]]
@@ -23,7 +25,6 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
- [app.util.router :as rt]
[app.util.storage :as storage]
[app.util.time :as dt]
[cuerdas.core :as str]
@@ -43,8 +44,10 @@
(def ^:private menu-icon
(i/icon-xref :menu (stl/css :menu-icon)))
-(mf/defc header
- {::mf/wrap [mf/memo]}
+(mf/defc header*
+ {::mf/wrap [mf/memo]
+ ::mf/props :obj
+ ::mf/private true}
[{:keys [can-edit]}]
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
@@ -60,7 +63,7 @@
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [team on-close]}]
- (let [on-nav-members-click (mf/use-fn #(st/emit! (dd/go-to-team-members)))
+ (let [on-nav-members-click (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
on-invite
(mf/use-fn
@@ -96,34 +99,35 @@
:aria-label (tr "labels.close")}
close-icon]]))
-(def builtin-templates
- (l/derived :builtin-templates st/state))
-
-(mf/defc project-item
- [{:keys [project first? team files can-edit] :as props}]
+(mf/defc project-item*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [project is-first team files can-edit]}]
(let [locale (mf/deref i18n/locale)
- file-count (or (:count project) 0)
+
project-id (:id project)
- is-draft-proyect (:is-default project)
- team-id (:id team)
- empty-state-viewer (and (not can-edit)
- (= 0 file-count))
+
+ file-count (or (:count project) 0)
+ is-draft? (:is-default project)
+ empty? (and (not can-edit)
+ (= 0 file-count))
dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate)
local (mf/use-state {:menu-open false
:menu-pos nil
- :edition? (= (:id project) edit-id)})
+ :edition (= (:id project) edit-id)})
+
+ [rowref limit]
+ (hooks/use-dynamic-grid-item-width)
- [rowref limit] (hooks/use-dynamic-grid-item-width)
on-nav
(mf/use-fn
- (mf/deps project-id team-id)
+ (mf/deps project-id)
(fn []
- (st/emit! (rt/nav :dashboard-files
- {:team-id team-id
- :project-id project-id}))))
+ (st/emit! (dcm/go-to-dashboard-files :project-id project-id))))
+
toggle-pin
(mf/use-fn
(mf/deps project)
@@ -152,7 +156,7 @@
(mf/use-fn #(swap! local assoc :menu-open false))
on-edit-open
- (mf/use-fn #(swap! local assoc :edition? true))
+ (mf/use-fn #(swap! local assoc :edition true))
on-edit
(mf/use-fn
@@ -162,15 +166,13 @@
(when-not (str/empty? name)
(st/emit! (-> (dd/rename-project (assoc project :name name))
(with-meta {::ev/origin "dashboard"}))))
- (swap! local assoc :edition? false))))
+ (swap! local assoc :edition false))))
on-file-created
(mf/use-fn
- (fn [data]
- (let [pparams {:project-id (:project-id data)
- :file-id (:id data)}
- qparams {:page-id (get-in data [:data :pages 0])}]
- (st/emit! (rt/nav :workspace pparams qparams)))))
+ (fn [{:keys [id data]}]
+ (let [page-id (get-in data [:pages 0])]
+ (st/emit! (dcm/go-to-workspace :file-id id :page-id page-id)))))
create-file
(mf/use-fn
@@ -189,9 +191,9 @@
on-import
(mf/use-fn
- (mf/deps project-id (:id team))
+ (mf/deps project-id)
(fn []
- (st/emit! (dd/fetch-files {:project-id project-id})
+ (st/emit! (dpj/fetch-files project-id)
(dd/fetch-recent-files)
(dd/fetch-projects)
(dd/clear-selected-files))))
@@ -212,10 +214,10 @@
(on-menu-click event))))
title-width (/ 100 limit)]
- [:article {:class (stl/css-case :dashboard-project-row true :first first?)}
+ [:article {:class (stl/css-case :dashboard-project-row true :first is-first)}
[:header {:class (stl/css :project)}
[:div {:class (stl/css :project-name-wrapper)}
- (if (:edition? @local)
+ (if (:edition @local)
[:& inline-edition {:content (:name project)
:on-end on-edit}]
[:h2 {:on-click on-nav
@@ -231,7 +233,6 @@
[:div {:class (stl/css :info-wrapper)}
-
;; We group these two spans under a div to avoid having extra space between them.
[:div
[:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))]
@@ -274,13 +275,13 @@
:on-import on-import}])]]]
[:div {:class (stl/css :grid-container) :ref rowref}
- (if empty-state-viewer
- [:> empty-placeholder* {:title (if is-draft-proyect
+ (if ^boolean empty?
+ [:> empty-placeholder* {:title (if ^boolean is-draft?
(tr "dashboard.empty-placeholder-drafts-title")
(tr "dashboard.empty-placeholder-files-title"))
:class (stl/css :placeholder-placement)
:type 1
- :subtitle (if is-draft-proyect
+ :subtitle (if ^boolean is-draft?
(tr "dashboard.empty-placeholder-drafts-subtitle")
(tr "dashboard.empty-placeholder-files-subtitle"))}]
@@ -303,16 +304,19 @@
[:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")]
show-more-icon])]))
-(def ref:recent-files
- (l/derived :dashboard-recent-files st/state))
+(def ^:private ref:recent-files
+ (l/derived :recent-files st/state))
-(mf/defc projects-section
+(mf/defc projects-section*
{::mf/props :obj}
[{:keys [team projects profile]}]
- (let [projects (->> (vals projects)
- (sort-by :modified-at)
- (reverse))
+ (let [projects
+ (mf/with-memo [projects]
+ (->> projects
+ (sort-by :modified-at)
+ (reverse)))
+
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
@@ -334,7 +338,7 @@
::ev/origin "dashboard"}))))]
(mf/with-effect [show-team-hero?]
- (swap! storage/global assoc ::show-team-hero show-team-hero?))
+ (swap! storage/global assoc ::show-eam-hero show-team-hero?))
(mf/with-effect [team]
(let [tname (if (:is-default team)
@@ -348,7 +352,7 @@
(when (seq projects)
[:*
- [:& header {:can-edit can-edit}]
+ [:> header* {:can-edit can-edit}]
[:div {:class (stl/css :projects-container)}
[:*
(when (and show-team-hero?
@@ -368,9 +372,9 @@
(->> (vals recent-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
- [:& project-item {:project project
- :team team
- :files files
- :can-edit can-edit
- :first? (= project (first projects))
- :key id}]))]]]])))
+ [:> project-item* {:project project
+ :team team
+ :files files
+ :can-edit can-edit
+ :is-first (= project (first projects))
+ :key id}]))]]]])))
diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs
index 862fc700af..0ad67d06a7 100644
--- a/frontend/src/app/main/ui/dashboard/search.cljs
+++ b/frontend/src/app/main/ui/dashboard/search.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.dashboard.search
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.main.data.dashboard :as dd]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -15,28 +16,43 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
+ [okulary.core :as l]
[rumext.v2 :as mf]))
-(mf/defc search-page
- [{:keys [team search-term] :as props}]
- (let [search-term (or search-term "")
- result (mf/deref refs/dashboard-search-result)
- [rowref limit] (hooks/use-dynamic-grid-item-width)]
+(def ^:private ref:search-result
+ (l/derived :search-result st/state))
- (mf/use-effect
- (mf/deps team)
- (fn []
- (when team
- (let [tname (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team))]
- (dom/set-html-title (tr "title.dashboard.search" tname))))))
+(def ^:private ref:selected
+ (l/derived (fn [state]
+ ;; we need to this because :dashboard-search-result is a list
+ ;; of maps and we need a map of maps (using :id as key).
+ (let [files (d/index-by :id (:search-result state))]
+ (->> (get state :selected-files)
+ (refs/extract-selected-files files))))
+ st/state))
+
+(mf/defc search-page*
+ {::mf/props :obj}
+ [{:keys [team search-term]}]
+ (let [search-term (d/nilv search-term "")
+
+ result (mf/deref ref:search-result)
+ selected (mf/deref ref:selected)
+
+ [rowref limit]
+ (hooks/use-dynamic-grid-item-width)]
+
+ (mf/with-effect [team]
+ (when team
+ (let [tname (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))]
+ (dom/set-html-title (tr "title.dashboard.search" tname)))))
+
+ (mf/with-effect [search-term]
+ (st/emit! (dd/search {:search-term search-term})
+ (dd/clear-selected-files)))
- (mf/use-effect
- (mf/deps search-term)
- (fn []
- (st/emit! (dd/search {:search-term search-term})
- (dd/clear-selected-files))))
[:*
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-search-title {:class (stl/css :dashboard-title)}
@@ -62,6 +78,6 @@
:else
[:& grid {:files result
- :hide-new? true
+ :selected-files selected
:origin :search
:limit limit}])]]))
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index 2e53b3bf76..dfb3691d78 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -7,16 +7,19 @@
(ns app.main.ui.dashboard.sidebar
(:require-macros [app.main.style :as stl])
(:require
- [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.spec :as us]
+ [app.common.uuid :as uuid]
[app.config :as cf]
+ [app.main.data.auth :as da]
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.team :as dtm]
[app.main.refs :as refs]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]]
@@ -30,7 +33,6 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
- [app.util.router :as rt]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
@@ -74,31 +76,34 @@
edit-id (:project-for-edit dstate)
local* (mf/use-state
- {:menu-open false
- :menu-pos nil
- :edition? (= (:id item) edit-id)
- :dragging? false})
+ #(do {:menu-open false
+ :menu-pos nil
+ :edition? (= (:id item) edit-id)
+ :dragging? false}))
+
+ local (deref local*)
+
+ project-id (get item :id)
- local @local*
on-click
(mf/use-fn
- (mf/deps item)
+ (mf/deps project-id)
(fn []
- (st/emit! (dd/go-to-files (:id item)))))
+ (st/emit! (dcm/go-to-dashboard-files :project-id project-id))))
on-key-down
(mf/use-fn
- (mf/deps item)
+ (mf/deps project-id)
(fn [event]
(when (kbd/enter? event)
- (st/emit! (dd/go-to-files (:id item))
- (ts/schedule-on-idle
- (fn []
- (let [project-title (dom/get-element (str (:id item)))]
- (when project-title
- (dom/set-attribute! project-title "tabindex" "0")
- (dom/focus! project-title)
- (dom/set-attribute! project-title "tabindex" "-1")))))))))
+ (st/emit!
+ (dcm/go-to-dashboard-files :project-id project-id)
+ (ts/schedule-on-idle
+ (fn []
+ (when-let [title (dom/get-element (str project-id))]
+ (dom/set-attribute! title "tabindex" "0")
+ (dom/focus! title)
+ (dom/set-attribute! title "tabindex" "-1"))))))))
on-menu-click
(mf/use-fn
@@ -148,9 +153,10 @@
on-drop-success
(mf/use-fn
- (mf/deps (:id item))
- #(st/emit! (ntf/success (tr "dashboard.success-move-file"))
- (dd/go-to-files (:id item))))
+ (mf/deps project-id)
+ (fn [_]
+ (st/emit! (dcm/go-to-dashboard-files :project-id project-id)
+ (ntf/success (tr "dashboard.success-move-file")))))
on-drop
(mf/use-fn
@@ -201,19 +207,18 @@
on-search-change
(mf/use-fn
- (mf/deps team-id)
(fn [event]
(let [value (dom/get-target-val event)]
- (emit! (dd/go-to-search value)))))
+ (emit! (dcm/go-to-dashboard-search :term value)))))
on-clear-click
(mf/use-fn
(mf/deps team-id)
(fn [e]
+ (emit! (dcm/go-to-dashboard-search))
(let [search-input (dom/get-element "search-input")]
(dom/clean-value! search-input)
(dom/focus! search-input)
- (emit! (dd/go-to-search))
(dom/prevent-default e)
(dom/stop-propagation e))))
@@ -277,8 +282,10 @@
(mf/use-fn
(fn [event]
(let [team-id (-> (dom/get-current-target event)
- (dom/get-data "value"))]
- (st/emit! (dd/go-to-projects team-id)))))
+ (dom/get-data "value")
+ (uuid/parse))]
+
+ (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
handle-select-default
(mf/use-fn
@@ -343,20 +350,22 @@
(mf/defc team-options-dropdown
[{:keys [team profile] :as props}]
- (let [go-members #(st/emit! (dd/go-to-team-members))
- go-invitations #(st/emit! (dd/go-to-team-invitations))
- go-webhooks #(st/emit! (dd/go-to-team-webhooks))
- go-settings #(st/emit! (dd/go-to-team-settings))
+ (let [go-members #(st/emit! (dcm/go-to-dashboard-members))
+ go-invitations #(st/emit! (dcm/go-to-dashboard-invitations))
+ go-webhooks #(st/emit! (dcm/go-to-dashboard-webhooks))
+ go-settings #(st/emit! (dcm/go-to-dashboard-settings))
- members-map (mf/deref refs/dashboard-team-members)
- members (vals members-map)
- can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
+ members (get team :members)
+ permissions (get team :permissions)
+ can-rename? (or (:is-owner permissions)
+ (:is-admin permissions))
on-success
(fn []
- (st/emit! (dd/go-to-projects (:default-team-id profile))
- (modal/hide)
- (du/fetch-teams)))
+ ;; FIXME: this should be handled in the event, not here
+ (let [team-id (:default-team-id profile)]
+ (rx/of (dcm/go-to-dashboard-recent :team-id team-id)
+ (modal/hide))))
on-error
(fn [{:keys [code] :as error}]
@@ -377,15 +386,15 @@
(mf/deps on-success on-error)
(fn [member-id]
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
- (st/emit! (dd/leave-team (with-meta params
- {:on-success on-success
- :on-error on-error}))))))
+ (st/emit! (dtm/leave-current-team (with-meta params
+ {:on-success on-success
+ :on-error on-error}))))))
delete-fn
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
- (st/emit! (dd/delete-team (with-meta team {:on-success on-success
- :on-error on-error})))))
+ (st/emit! (dtm/delete-team (with-meta team {:on-success on-success
+ :on-error on-error})))))
on-rename-clicked
(mf/use-fn
(mf/deps team)
@@ -406,7 +415,7 @@
(mf/use-fn
(mf/deps team profile leave-fn)
(fn []
- (st/emit! (dd/fetch-team-members (:id team))
+ (st/emit! (dtm/fetch-members)
(modal/show
{:type :leave-and-reassign
:profile profile
@@ -590,6 +599,10 @@
(when (get-in team [:permissions :is-owner])
"teams-options-delete-team")]
+
+ ;; _ (prn "--------------- sidebar-team-switch")
+ ;; _ (app.common.pprint/pprint teams)
+
handle-show-team-click
(fn [event]
(dom/stop-propagation event)
@@ -679,45 +692,48 @@
[:& team-options-dropdown {:team team
:profile profile}]]]))
-(mf/defc sidebar-content
- [{:keys [projects profile section team project search-term] :as props}]
+(mf/defc sidebar-content*
+ {::mf/private true
+ ::mf/props :obj}
+ [{:keys [projects profile section team project search-term default-project] :as props}]
(let [default-project-id
- (->> (vals projects)
- (d/seek :is-default)
- (:id))
+ (get default-project :id)
- projects? (= section :dashboard-projects)
+ team-id (get team :id)
+
+ projects? (= section :dashboard-recent)
fonts? (= section :dashboard-fonts)
libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
+ container (mf/use-ref nil)
+ overflow* (mf/use-state false)
+ overflow? (deref overflow*)
go-projects
- (mf/use-fn
- (mf/deps team)
- #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})))
+ (mf/use-fn #(st/emit! (dcm/go-to-dashboard-recent)))
go-projects-with-key
(mf/use-fn
- (mf/deps team)
- #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})
- (ts/schedule-on-idle
- (fn []
- (let [projects-title (dom/get-element "dashboard-projects-title")]
- (when projects-title
- (dom/set-attribute! projects-title "tabindex" "0")
- (dom/focus! projects-title)
- (dom/set-attribute! projects-title "tabindex" "-1")))))))
+ (mf/deps team-id)
+ (fn []
+ (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)
+ (ts/schedule-on-idle
+ (fn []
+ (when-let [projects-title (dom/get-element "dashboard-projects-title")]
+ (dom/set-attribute! projects-title "tabindex" "0")
+ (dom/focus! projects-title)
+ (dom/set-attribute! projects-title "tabindex" "-1")))))))
go-fonts
(mf/use-fn
- (mf/deps team)
- #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)})))
+ (mf/deps team-id)
+ #(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)))
go-fonts-with-key
(mf/use-fn
(mf/deps team)
- #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)})
+ #(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
@@ -727,34 +743,31 @@
(dom/set-attribute! font-title "tabindex" "-1")))))))
go-drafts
(mf/use-fn
- (mf/deps team default-project-id)
+ (mf/deps team-id default-project-id)
(fn []
- (st/emit! (rt/nav :dashboard-files
- {:team-id (:id team)
- :project-id default-project-id}))))
+ (st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))))
go-drafts-with-key
(mf/use-fn
- (mf/deps team default-project-id)
- #(st/emit! (rt/nav :dashboard-files {:team-id (:id team)
- :project-id default-project-id})
- (ts/schedule-on-idle
- (fn []
- (let [drafts-title (dom/get-element "dashboard-drafts-title")]
- (when drafts-title
- (dom/set-attribute! drafts-title "tabindex" "0")
- (dom/focus! drafts-title)
- (dom/set-attribute! drafts-title "tabindex" "-1")))))))
+ (mf/deps team-id default-project-id)
+ (fn []
+ (st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))
+ (ts/schedule-on-idle
+ (fn []
+ (when-let [title (dom/get-element "dashboard-drafts-title")]
+ (dom/set-attribute! title "tabindex" "0")
+ (dom/focus! title)
+ (dom/set-attribute! title "tabindex" "-1"))))))
go-libs
(mf/use-fn
- (mf/deps team)
- #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})))
+ (mf/deps team-id)
+ #(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)))
go-libs-with-key
(mf/use-fn
- (mf/deps team)
- #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})
+ (mf/deps team-id)
+ #(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
@@ -763,74 +776,86 @@
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1")))))))
pinned-projects
- (->> (vals projects)
+ (->> projects
(remove :is-default)
(filter :is-pinned))]
- [:div {:class (stl/css :sidebar-content)}
- [:& sidebar-team-switch {:team team :profile profile}]
+ (mf/use-layout-effect
+ (mf/deps pinned-projects)
+ (fn []
+ (let [dom (mf/ref-val container)
+ client-height (obj/get dom "clientHeight")
+ scroll-height (obj/get dom "scrollHeight")]
+ (reset! overflow* (> scroll-height client-height)))))
- [:& sidebar-search {:search-term search-term
- :team-id (:id team)}]
+ [:*
+ [:div {:class (stl/css-case :sidebar-content true)
+ :ref container}
+ [:& sidebar-team-switch {:team team :profile profile}]
- [:div {:class (stl/css :sidebar-content-section)}
- [:ul {:class (stl/css :sidebar-nav)}
- [:li {:class (stl/css-case :recent-projects true
- :sidebar-nav-item true
- :current projects?)}
- [:& link {:action go-projects
- :class (stl/css :sidebar-link)
- :keyboard-action go-projects-with-key}
- [:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
+ [:& sidebar-search {:search-term search-term
+ :team-id (:id team)}]
- [:li {:class (stl/css-case :current drafts?
- :sidebar-nav-item true)}
- [:& link {:action go-drafts
- :class (stl/css :sidebar-link)
- :keyboard-action go-drafts-with-key}
- [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]
+ [:div {:class (stl/css :sidebar-content-section)}
+ [:ul {:class (stl/css :sidebar-nav)}
+ [:li {:class (stl/css-case :recent-projects true
+ :sidebar-nav-item true
+ :current projects?)}
+ [:& link {:action go-projects
+ :class (stl/css :sidebar-link)
+ :keyboard-action go-projects-with-key}
+ [:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
+
+ [:li {:class (stl/css-case :current drafts?
+ :sidebar-nav-item true)}
+ [:& link {:action go-drafts
+ :class (stl/css :sidebar-link)
+ :keyboard-action go-drafts-with-key}
+ [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]
- [:li {:class (stl/css-case :current libs?
- :sidebar-nav-item true)}
- [:& link {:action go-libs
- :data-testid "libs-link-sidebar"
- :class (stl/css :sidebar-link)
- :keyboard-action go-libs-with-key}
- [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]]
+ [:li {:class (stl/css-case :current libs?
+ :sidebar-nav-item true)}
+ [:& link {:action go-libs
+ :data-testid "libs-link-sidebar"
+ :class (stl/css :sidebar-link)
+ :keyboard-action go-libs-with-key}
+ [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]]
- [:div {:class (stl/css :sidebar-content-section)}
- [:ul {:class (stl/css :sidebar-nav)}
- [:li {:class (stl/css-case :sidebar-nav-item true
- :current fonts?)}
- [:& link {:action go-fonts
- :class (stl/css :sidebar-link)
- :keyboard-action go-fonts-with-key
- :data-testid "fonts"}
- [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]]
+ [:div {:class (stl/css :sidebar-content-section)}
+ [:ul {:class (stl/css :sidebar-nav)}
+ [:li {:class (stl/css-case :sidebar-nav-item true
+ :current fonts?)}
+ [:& link {:action go-fonts
+ :class (stl/css :sidebar-link)
+ :keyboard-action go-fonts-with-key
+ :data-testid "fonts"}
+ [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]]
- [:div {:class (stl/css :sidebar-content-section)
- :data-testid "pinned-projects"}
- (if (seq pinned-projects)
- [:ul {:class (stl/css :sidebar-nav :pinned-projects)}
- (for [item pinned-projects]
- [:& sidebar-project
- {:item item
- :key (dm/str (:id item))
- :id (:id item)
- :team-id (:id team)
- :selected? (= (:id item) (:id project))}])]
- [:div {:class (stl/css :sidebar-empty-placeholder)}
- pin-icon
- [:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]))
+ [:div {:class (stl/css :sidebar-content-section)
+ :data-testid "pinned-projects"}
+ (if (seq pinned-projects)
+ [:ul {:class (stl/css :sidebar-nav :pinned-projects)}
+ (for [item pinned-projects]
+ [:& sidebar-project
+ {:item item
+ :key (dm/str (:id item))
+ :id (:id item)
+ :team-id (:id team)
+ :selected? (= (:id item) (:id project))}])]
+ [:div {:class (stl/css :sidebar-empty-placeholder)}
+ pin-icon
+ [:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]
+ [:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]))
-(mf/defc profile-section
- [{:keys [profile team] :as props}]
+(mf/defc profile-section*
+ {::mf/props :obj}
+ [{:keys [profile team]}]
(let [show* (mf/use-state false)
show (deref show*)
- photo (cf/resolve-profile-photo-url profile)
+ photo (cf/resolve-profile-photo-url profile)
on-click
(mf/use-fn
@@ -875,14 +900,13 @@
(when (kbd/enter? event)
(reset! show* true))))
- handle-close
+ on-close
(fn [event]
(dom/stop-propagation event)
(reset! show* false))
handle-key-down-profile
(mf/use-fn
- (mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
(on-click :settings-profile event))))
@@ -910,36 +934,41 @@
(show-release-notes))))
handle-feedback-click
- (mf/use-fn
- (mf/deps on-click)
- #(on-click :settings-feedback %))
+ (mf/use-fn #(on-click :settings-feedback %))
handle-feedback-keydown
(mf/use-fn
- (mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
(on-click :settings-feedback event))))
handle-logout-click
(mf/use-fn
- (mf/deps on-click)
- #(on-click (du/logout) %))
+ #(on-click (da/logout) %))
handle-logout-keydown
(mf/use-fn
- (mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
- (on-click (du/logout) event))))
+ (on-click (da/logout) event))))
handle-set-profile
(mf/use-fn
- (mf/deps on-click)
- (fn [event]
- (on-click :settings-profile event)))]
+ #(on-click :settings-profile %))
+
+ on-power-up-click
+ (mf/use-fn
+ (fn []
+ (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 (and team profile)
[:& comments-section
{:profile profile
@@ -959,7 +988,9 @@
:alt (:fullname profile)}]
[:span {:class (stl/css :profile-fullname)} (:fullname profile)]]
- [:& dropdown-menu {:on-close handle-close :show show :list-class (stl/css :profile-dropdown)}
+ [:& dropdown-menu {:on-close on-close
+ :show show
+ :list-class (stl/css :profile-dropdown)}
[:li {:tab-index (if show "0" "-1")
:class (stl/css :profile-dropdown-item)
:on-click handle-set-profile
@@ -1045,15 +1076,13 @@
:show? show-comments?
:on-show-comments handle-show-comments}])]]))
-(mf/defc sidebar
- {::mf/wrap-props false
+(mf/defc sidebar*
+ {::mf/props :obj
::mf/wrap [mf/memo]}
- [props]
- (let [team (obj/get props "team")
- profile (obj/get props "profile")]
- [:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
- [:> sidebar-content props]
- [:& profile-section
- {:profile profile
- :team team}]]))
+ [{:keys [team profile] :as props}]
+ [:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
+ [:> sidebar-content* props]
+ [:> profile-section*
+ {:profile profile
+ :team team}]])
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss
index 22a0a3c9c6..65afa269d2 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.scss
+++ b/frontend/src/app/main/ui/dashboard/sidebar.scss
@@ -6,6 +6,9 @@
@use "common/refactor/common-refactor.scss" as *;
@use "common/refactor/common-dashboard";
+@use "../ds/typography.scss" as t;
+
+@import "../ds/_borders.scss";
// SIDEBAR COMPONENT
.dashboard-sidebar {
@@ -32,6 +35,16 @@
overflow-y: auto;
}
+.separator {
+ height: var(--sp-xxs);
+ width: 94%;
+ margin-left: 3%;
+}
+
+.overflow-separator {
+ border-bottom: $b-1 solid var(--color-background-quaternary);
+}
+
// SIDEBAR TEAM SWITCH
.sidebar-team-switch {
position: relative;
@@ -78,6 +91,7 @@
// This icon still use the old svg
.penpot-icon {
@include flexCenter;
+
svg {
fill: var(--icon-foreground);
width: $s-24;
@@ -138,6 +152,7 @@
.action {
--sidebar-action-icon-color: var(--icon-foreground);
--sidebar-icon-backgroun-color: var(--color-background-secondary);
+
&:hover {
--sidebar-action-icon-color: var(--color-background-secondary);
--sidebar-icon-backgroun-color: var(--color-accent-primary);
@@ -175,6 +190,7 @@
right: $s-2;
top: $s-52;
max-height: $s-480;
+
&:not(.teams-dropdown) {
min-width: $s-160;
}
@@ -204,8 +220,10 @@
.sidebar-nav-item {
cursor: pointer;
+
&:hover {
background-color: var(--sidebar-element-background-color-hover);
+
span {
color: var(--sidebar-element-foreground-color-hover);
}
@@ -213,6 +231,7 @@
&.current {
background-color: var(--sidebar-element-background-color-selected);
+
.element-title {
color: var(--sidebar-element-foreground-color-selected);
}
@@ -228,6 +247,7 @@
padding: $s-8 $s-8 $s-8 $s-24;
font-weight: $fw400;
width: 100%;
+
&:hover {
text-decoration: none;
}
@@ -293,6 +313,7 @@
outline: none;
border: $s-1 solid var(--search-bar-input-border-color-focus);
}
+
::placeholder {
color: var(--search-bar-placeholder-foreground-color);
}
@@ -370,6 +391,7 @@
.profile-separator {
height: $s-6;
+ border-top: solid $s-1 var(--color-background-quaternary);
}
.item-with-icon {
@@ -382,3 +404,31 @@
@extend .button-icon;
stroke: var(--icon-foreground);
}
+
+.upgrade-plan-section {
+ @include buttonStyle;
+ display: flex;
+ justify-content: space-between;
+ border: $b-1 solid var(--color-background-quaternary);
+ border-radius: var(--sp-s);
+ padding: var(--sp-m);
+ margin: var(--sp-m) var(--sp-s) var(--sp-m) var(--sp-m);
+ color: var(--color-foreground-secondary);
+ cursor: pointer;
+}
+
+.penpot-free {
+ @include t.use-typography("body-medium");
+ display: flex;
+ flex-direction: column;
+ text-align: left;
+}
+
+.no-limits {
+ @include t.use-typography("body-small");
+}
+
+.power-up {
+ @include t.use-typography("body-small");
+ color: var(--color-accent-tertiary);
+}
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index 431dd2711a..ce2b9b7911 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -11,11 +11,11 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.config :as cfg]
- [app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
@@ -31,6 +31,7 @@
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
+ [okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private arrow-icon
@@ -61,10 +62,10 @@
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [section team]}]
- (let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
- on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
- on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
- on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
+ (let [on-nav-members (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
+ on-nav-settings (mf/use-fn #(st/emit! (dcm/go-to-dashboard-settings)))
+ on-nav-invitations (mf/use-fn #(st/emit! (dcm/go-to-dashboard-invitations)))
+ on-nav-webhooks (mf/use-fn #(st/emit! (dcm/go-to-dashboard-webhooks)))
route (mf/deref refs/route)
invite-email (-> route :query-params :invite-email)
@@ -139,12 +140,12 @@
::mf/register-as :invite-members
::mf/props :obj}
[{:keys [team origin invite-email]}]
- (let [members-map (mf/deref refs/dashboard-team-members)
- perms (:permissions team)
+ (let [members (get team :members)
+ perms (get team :permissions)
+ team-id (get team :id)
roles (mf/with-memo [perms]
(get-available-roles perms))
- team-id (:id team)
initial (mf/with-memo [team-id invite-email]
(if invite-email
@@ -156,7 +157,7 @@
error-text (mf/use-state "")
current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
- current-members-emails (into #{} (map (comp :email second)) members-map)
+ current-members-emails (into #{} (map :email) members)
on-success
(fn [_form {:keys [total]}]
@@ -164,8 +165,8 @@
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))
(st/emit! (modal/hide)
- (dd/fetch-team-members)
- (dd/fetch-team-invitations)))
+ (dtm/fetch-members)
+ (dtm/fetch-invitations)))
on-error
(fn [_form cause]
@@ -198,11 +199,11 @@
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
- (st/emit! (-> (dd/invite-team-members (with-meta params mdata))
+ (st/emit! (-> (dtm/create-invitations (with-meta params mdata))
(with-meta {::ev/origin origin}))
- (dd/fetch-team-invitations)
- (dd/fetch-team-members (:id team)))))]
-
+ ;; FIXME: looks duplicate
+ (dtm/fetch-invitations)
+ (dtm/fetch-members))))]
[:div {:class (stl/css-case :modal-team-container true
:modal-team-container-workspace (= origin :workspace)
@@ -278,27 +279,30 @@
is-you (= (:id profile) (:id member))
can-change-rol (or is-owner is-admin)
- not-superior (or is-admin (and can-change-rol (or member-is-admin member-is-editor member-is-viewer)))
+ not-superior (or (and (not member-is-owner) is-admin) (and can-change-rol (or member-is-admin member-is-editor member-is-viewer)))
role (cond
member-is-owner "labels.owner"
member-is-admin "labels.admin"
member-is-editor "labels.editor"
- :else "labels.viewer")
-
+ :else "labels.viewer")
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
[:*
(if (and can-change-rol not-superior (not (and is-you is-owner)))
[:div {:class (stl/css :rol-selector :has-priv)
+ :role "combobox"
+ :aria-labelledby "role-label-id"
:on-click on-show}
- [:span {:class (stl/css :rol-label)} (tr role)]
+ [:span {:class (stl/css :rol-label)
+ :id "role-label-id"} (tr role)]
arrow-icon]
[:div {:class (stl/css :rol-selector)}
[:span {:class (stl/css :rol-label)} (tr role)]])
[:& dropdown {:show @show? :on-close on-hide}
- [:ul {:class (stl/css :roles-dropdown)}
+ [:ul {:class (stl/css :roles-dropdown)
+ :role "listbox"}
[:li {:on-click on-set-viewer
:class (stl/css :rol-dropdown-item)}
(tr "labels.viewer")]
@@ -338,7 +342,8 @@
(when is-you?
[:li {:on-click on-leave
:class (stl/css :action-dropdown-item)
- :key "is-you-option"} (tr "dashboard.leave-team")])
+ :key "is-you-option"}
+ (tr "dashboard.leave-team")])
(when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
[:li {:on-click on-delete
:class (stl/css :action-dropdown-item)
@@ -346,18 +351,18 @@
(defn- set-role! [member-id role]
(let [params {:member-id member-id :role role}]
- (st/emit! (dd/update-team-member-role params))))
+ (st/emit! (dtm/update-member-role params))))
-(mf/defc team-member
+(mf/defc team-member*
{::mf/wrap [mf/memo]
::mf/props :obj}
- [{:keys [team member members profile]}]
+ [{:keys [team member total-members profile]}]
- (let [member-id (:id member)
+ (let [member-id (:id member)
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
on-set-viewer (mf/use-fn (mf/deps member-id) (partial set-role! member-id :viewer))
- owner? (dm/get-in team [:permissions :is-owner])
+ owner? (dm/get-in team [:permissions :is-owner])
on-set-owner
(mf/use-fn
@@ -373,18 +378,12 @@
(st/emit! (modal/show params)))))
on-success
- (mf/use-fn
- (mf/deps profile)
- (fn []
- (st/emit! (dd/go-to-projects (:default-team-id profile))
- (modal/hide)
- (du/fetch-teams))))
+ (mf/use-fn #(rx/of (dcm/go-to-dashboard-recent :team-id :default)))
on-error
(mf/use-fn
(fn [{:keys [code] :as error}]
(condp = code
-
:no-enough-members-for-leave
(rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))
@@ -400,17 +399,17 @@
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
- (st/emit! (dd/delete-team (with-meta team {:on-success on-success
- :on-error on-error})))))
+ (st/emit! (dtm/delete-team (with-meta team {:on-success on-success
+ :on-error on-error})))))
on-leave-accepted
(mf/use-fn
(mf/deps on-success on-error)
(fn [member-id]
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
- (st/emit! (dd/leave-team (with-meta params
- {:on-success on-success
- :on-error on-error}))))))
+ (st/emit! (dtm/leave-current-team (with-meta params
+ {:on-success on-success
+ :on-error on-error}))))))
on-leave-and-close
(mf/use-fn
@@ -428,7 +427,7 @@
(mf/use-fn
(mf/deps profile team on-leave-accepted)
(fn []
- (st/emit! (dd/fetch-team-members (:id team))
+ (st/emit! (dtm/fetch-members)
(modal/show
{:type :leave-and-reassign
:profile profile
@@ -450,7 +449,7 @@
(mf/use-fn
(mf/deps member-id)
(fn []
- (let [on-accept #(st/emit! (dd/delete-team-member {:member-id member-id}))
+ (let [on-accept #(st/emit! (dtm/delete-member {:member-id member-id}))
params {:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
@@ -459,7 +458,7 @@
(st/emit! (modal/show params)))))
on-leave'
- (cond (= 1 (count members)) on-leave-and-close
+ (cond (= 1 total-members) on-leave-and-close
(= true owner?) on-change-owner-and-leave
:else on-leave)]
@@ -483,16 +482,25 @@
:on-delete on-delete
:on-leave on-leave'}]]]))
-(mf/defc team-members
- {::mf/props :obj}
- [{:keys [members-map team profile]}]
- (let [members (mf/with-memo [members-map]
- (->> (vals members-map)
- (sort-by :created-at)
- (remove :is-owner)))
- owner (mf/with-memo [members-map]
- (->> (vals members-map)
- (d/seek :is-owner)))]
+(mf/defc team-members*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [team profile]}]
+ (let [members (get team :members)
+
+ total-members
+ (count members)
+
+ owner
+ (mf/with-memo [members]
+ (d/seek :is-owner members))
+
+ members
+ (mf/with-memo [team]
+ (->> (:members team)
+ (sort-by :created-at)
+ (remove :is-owner)
+ (vec)))]
[:div {:class (stl/css :dashboard-table :team-members)}
[:div {:class (stl/css :table-header)}
@@ -500,42 +508,39 @@
[:div {:class (stl/css :table-field :title-field-role)} (tr "labels.role")]]
[:div {:class (stl/css :table-rows)}
- [:& team-member
+ [:> team-member*
{:member owner
:team team
:profile profile
- :members members-map}]
+ :total-members total-members}]
(for [item members]
- [:& team-member
+ [:> team-member*
{:member item
:team team
:profile profile
- :key (:id item)
- :members members-map}])]]))
+ :key (dm/str (:id item))
+ :total-members total-members}])]]))
-(mf/defc team-members-page
+(mf/defc team-members-page*
{::mf/props :obj}
[{:keys [team profile]}]
- (let [members-map (mf/deref refs/dashboard-team-members)]
+ (mf/with-effect [team]
+ (dom/set-html-title
+ (tr "title.team-members"
+ (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team)))))
- (mf/with-effect [team]
- (dom/set-html-title
- (tr "title.team-members"
- (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team)))))
+ (mf/with-effect []
+ (st/emit! (dtm/fetch-members)))
- (mf/with-effect [team]
- (st/emit! (dd/fetch-team-members (:id team))))
-
- [:*
- [:& header {:section :dashboard-team-members :team team}]
- [:section {:class (stl/css :dashboard-container :dashboard-team-members)}
- [:& team-members
- {:profile profile
- :team team
- :members-map members-map}]]]))
+ [:*
+ [:& header {:section :dashboard-team-members :team team}]
+ [:section {:class (stl/css :dashboard-container :dashboard-team-members)}
+ [:> team-members*
+ {:profile profile
+ :team team}]]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
@@ -587,8 +592,9 @@
:on-click on-change'}
(tr "labels.viewer")]]]]))
-(mf/defc invitation-actions
- {::mf/props :obj}
+(mf/defc invitation-actions*
+ {::mf/props :obj
+ ::mf/private true}
[{:keys [invitation team-id]}]
(let [show? (mf/use-state false)
@@ -622,15 +628,15 @@
(mf/deps email team-id)
(fn []
(let [params {:email email :team-id team-id}
- mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
- (st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
+ mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
+ (st/emit! (dtm/delete-invitation (with-meta params mdata))))))
on-resend-success
(mf/use-fn
(fn []
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))
(modal/hide)
- (dd/fetch-team-invitations))))
+ (dtm/fetch-invitations))))
on-resend
(mf/use-fn
@@ -643,7 +649,7 @@
{:on-success on-resend-success
:on-error on-error})]
(st/emit!
- (-> (dd/invite-team-members params)
+ (-> (dtm/create-invitations params)
(with-meta {::ev/origin :team}))))))
on-copy-success
@@ -660,7 +666,7 @@
{:on-success on-copy-success
:on-error on-error})]
(st/emit!
- (-> (dd/copy-invitation-link params)
+ (-> (dtm/copy-invitation-link params)
(with-meta {::ev/origin :team}))))))
on-hide (mf/use-fn #(reset! show? false))
@@ -694,17 +700,19 @@
role (:role invitation)
status (if expired? :expired :pending)
type (if expired? :warning :default)
- badge-content (if (= status :expired)
- (tr "labels.expired-invitation")
- (tr "labels.pending-invitation"))
+
+ badge-content
+ (if (= status :expired)
+ (tr "labels.expired-invitation")
+ (tr "labels.pending-invitation"))
on-change-role
(mf/use-fn
(mf/deps email team-id)
(fn [role _event]
(let [params {:email email :team-id team-id :role role}
- mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
- (st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
+ mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
+ (st/emit! (dtm/update-invitation-role (with-meta params mdata))))))]
[:div {:class (stl/css :table-row :table-row-invitations)}
[:div {:class (stl/css :table-field :field-email)} email]
@@ -720,25 +728,36 @@
[:& badge-notification {:type type :content badge-content}]]
[:div {:class (stl/css :table-field :field-actions)}
- (when can-invite
- [:& invitation-actions
+ (when ^boolean can-invite
+ [:> invitation-actions*
{:invitation invitation
:team-id team-id}])]]))
-(mf/defc empty-invitation-table
- [{:keys [can-invite] :as props}]
+(mf/defc empty-invitation-table*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [can-invite]}]
[:div {:class (stl/css :empty-invitations)}
[:span (tr "labels.no-invitations")]
- (when can-invite
+ (when ^boolean can-invite
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
:tag-name "span"}])])
-(mf/defc invitation-section
- [{:keys [team invitations] :as props}]
- (let [owner? (dm/get-in team [:permissions :is-owner])
- admin? (dm/get-in team [:permissions :is-admin])
- can-invite (or owner? admin?)
- team-id (:id team)]
+(def ^:private ref:invitations
+ (l/derived :invitations st/state))
+
+(mf/defc invitation-section*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [team]}]
+ (let [permissions (get team :permissions)
+ invitations (mf/deref ref:invitations)
+
+ team-id (get team :id)
+
+ owner? (get permissions :is-owner)
+ admin? (get permissions :is-admin)
+ can-invite? (or owner? admin?)]
[:div {:class (stl/css :invitations)}
[:div {:class (stl/css :table-header)}
@@ -746,38 +765,34 @@
[:div {:class (stl/css :title-field-role)} (tr "labels.role")]
[:div {:class (stl/css :title-field-status)} (tr "labels.status")]]
(if (empty? invitations)
- [:& empty-invitation-table {:can-invite can-invite}]
+ [:> empty-invitation-table* {:can-invite can-invite?}]
[:div {:class (stl/css :table-rows)}
(for [invitation invitations]
[:> invitation-row*
{:key (:email invitation)
:invitation invitation
- :can-invite can-invite
+ :can-invite can-invite?
:team-id team-id}])])]))
-(mf/defc team-invitations-page
+(mf/defc team-invitations-page*
+ {::mf/props :obj}
[{:keys [team]}]
- (let [invitations (mf/deref refs/dashboard-team-invitations)]
- (mf/with-effect [team]
- (dom/set-html-title
- (tr "title.team-invitations"
- (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team)))))
+ (mf/with-effect [team]
+ (dom/set-html-title
+ (tr "title.team-invitations"
+ (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team)))))
- (mf/with-effect []
- (st/emit! (dd/fetch-team-invitations)))
+ (mf/with-effect []
+ (st/emit! (dtm/fetch-invitations)))
- [:*
- [:& header {:section :dashboard-team-invitations
- :team team}]
- [:section {:class (stl/css :dashboard-team-invitations)}
- ;; TODO: We should consider adding a "loading state" here
- ;; with an (if (nil? invitations) [:& loading-state] [:& invitations])
- (when-not (nil? invitations)
- [:& invitation-section {:team team
- :invitations invitations}])]]))
+ [:*
+ [:& header {:section :dashboard-team-invitations
+ :team team}]
+ [:section {:class (stl/css :dashboard-team-invitations)}
+ [:> invitation-section* {:team team}]]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBHOOKS SECTION
@@ -811,9 +826,8 @@
(mf/use-fn
(fn [_]
(let [message (tr "dashboard.webhooks.create.success")]
- (st/emit! (dd/fetch-team-webhooks)
- (ntf/success message)
- (modal/hide)))))
+ (rx/of (ntf/success message)
+ (modal/hide)))))
on-error
(mf/use-fn
@@ -846,7 +860,7 @@
params {:uri (:uri cdata)
:mtype (:mtype cdata)
:is-active (:is-active cdata)}]
- (st/emit! (dd/create-team-webhook
+ (st/emit! (dtm/create-webhook
(with-meta params mdata))))))
on-update-submit
@@ -855,7 +869,7 @@
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
- (st/emit! (dd/update-team-webhook
+ (st/emit! (dtm/update-webhook
(with-meta params mdata))))))
on-submit
@@ -910,7 +924,7 @@
(tr "modals.edit-webhook.submit-label")
(tr "modals.create-webhook.submit-label"))}]]]]]]))
-(mf/defc webhooks-hero
+(mf/defc webhooks-hero*
{::mf/props :obj}
[]
[:div {:class (stl/css :webhooks-hero-container)}
@@ -922,7 +936,7 @@
:on-click #(st/emit! (modal/show :webhook {}))}
(tr "dashboard.webhooks.create")]])
-(mf/defc webhook-actions
+(mf/defc webhook-actions*
{::mf/props :obj
::mf/private true}
[{:keys [on-edit on-delete can-edit]}]
@@ -945,8 +959,10 @@
:class (stl/css :menu-disabled)}
[:> icon* {:id "menu"}]])))
-(mf/defc webhook-item
- {::mf/wrap [mf/memo]}
+(mf/defc webhook-item*
+ {::mf/wrap [mf/memo]
+ ::mf/props :obj
+ ::mf/private true}
[{:keys [webhook permissions]}]
(let [error-code (:error-code webhook)
id (:id webhook)
@@ -966,8 +982,8 @@
(mf/deps id)
(fn []
(let [params {:id id}
- mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
- (st/emit! (dd/delete-team-webhook (with-meta params mdata))))))
+ mdata {:on-success #(st/emit! (dtm/fetch-webhooks))}]
+ (st/emit! (dtm/delete-webhook (with-meta params mdata))))))
on-delete
(mf/use-fn
@@ -1005,22 +1021,29 @@
(tr "labels.active")
(tr "labels.inactive"))]]
[:div {:class (stl/css :table-field :actions)}
- [:& webhook-actions
+ [:> webhook-actions*
{:on-edit on-edit
:on-delete on-delete
:can-edit can-edit}]]]))
-(mf/defc webhooks-list
- {::mf/props :obj}
+(mf/defc webhooks-list*
+ {::mf/props :obj
+ ::mf/private true}
[{:keys [webhooks permissions]}]
[:div {:class (stl/css :table-rows :webhook-table)}
(for [webhook webhooks]
- [:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])])
+ [:> webhook-item*
+ {:webhook webhook
+ :key (dm/str (:id webhook))
+ :permissions permissions}])])
-(mf/defc team-webhooks-page
+(def ^:private ref:webhooks
+ (l/derived :webhooks st/state))
+
+(mf/defc webhooks-page*
{::mf/props :obj}
[{:keys [team]}]
- (let [webhooks (mf/deref refs/dashboard-team-webhooks)]
+ (let [webhooks (mf/deref ref:webhooks)]
(mf/with-effect [team]
(dom/set-html-title
@@ -1030,33 +1053,34 @@
(:name team)))))
(mf/with-effect [team]
- (st/emit! (dd/fetch-team-webhooks)))
+ (st/emit! (dtm/fetch-webhooks)))
[:*
[:& header {:team team :section :dashboard-team-webhooks}]
[:section {:class (stl/css :dashboard-container :dashboard-team-webhooks)}
[:*
- [:& webhooks-hero]
+ [:> webhooks-hero* {}]
(if (empty? webhooks)
[:div {:class (stl/css :webhooks-empty)}
[:div (tr "dashboard.webhooks.empty.no-webhooks")]
[:div (tr "dashboard.webhooks.empty.add-one")]]
- [:& webhooks-list {:webhooks webhooks :permissions (:permissions team)}])]]]))
+ [:> webhooks-list*
+ {:webhooks webhooks
+ :permissions (:permissions team)}])]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETTINGS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(mf/defc team-settings-page
+(mf/defc team-settings-page*
{::mf/props :obj}
[{:keys [team]}]
(let [finput (mf/use-ref)
- members-map (mf/deref refs/dashboard-team-members)
- owner (->> (vals members-map)
- (d/seek :is-owner))
+ members (get team :members)
+ stats (get team :stats)
- stats (mf/deref refs/dashboard-team-stats)
+ owner (d/seek :is-owner members)
permissions (:permissions team)
can-edit (or (:is-owner permissions)
@@ -1067,8 +1091,7 @@
on-file-selected
(fn [file]
- (st/emit! (dd/update-team-photo file)))]
-
+ (st/emit! (dtm/update-team-photo file)))]
(mf/with-effect [team]
(dom/set-html-title (tr "title.team-settings"
@@ -1076,11 +1099,9 @@
(tr "dashboard.your-penpot")
(:name team)))))
-
- (mf/with-effect [team]
- (let [team-id (:id team)]
- (st/emit! (dd/fetch-team-members team-id)
- (dd/fetch-team-stats team-id))))
+ (mf/with-effect []
+ (st/emit! (dtm/fetch-members)
+ (dtm/fetch-stats)))
[:*
[:& header {:section :dashboard-team-settings :team team}]
@@ -1116,7 +1137,7 @@
[:div {:class (stl/css :block-content)}
user-icon
[:span {:class (stl/css :block-text)}
- (tr "dashboard.num-of-members" (count members-map))]]]
+ (tr "dashboard.num-of-members" (count members))]]]
[:div {:class (stl/css :block)}
[:div {:class (stl/css :block-label)}
diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs
index cf8796b75b..a83ce0d87b 100644
--- a/frontend/src/app/main/ui/dashboard/team_form.cljs
+++ b/frontend/src/app/main/ui/dashboard/team_form.cljs
@@ -8,17 +8,17 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
- [app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
+ [app.main.data.team :as dtm]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -28,15 +28,15 @@
(defn- on-create-success
[_form response]
- (let [msg "Team created successfully"]
- (st/emit! (ntf/success msg)
- (modal/hide)
- (rt/nav :dashboard-projects {:team-id (:id response)}))))
+ (let [message "Team created successfully"
+ team-id (:id response)]
+ (st/emit! (ntf/success message)
+ (dcm/go-to-dashboard-recent :team-id team-id))))
(defn- on-update-success
[_form _response]
- (let [msg "Team created successfully"]
- (st/emit! (ntf/success msg)
+ (let [message "Team created successfully"]
+ (st/emit! (ntf/success message)
(modal/hide))))
(defn- on-error
@@ -51,7 +51,7 @@
(let [mdata {:on-success (partial on-create-success form)
:on-error (partial on-error form)}
params {:name (get-in @form [:clean-data :name])}]
- (st/emit! (-> (dd/create-team (with-meta params mdata))
+ (st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :dashboard})))))
(defn- on-update-submit
@@ -59,7 +59,7 @@
(let [mdata {:on-success (partial on-update-success form)
:on-error (partial on-error form)}
team (get @form :clean-data)]
- (st/emit! (dd/update-team (with-meta team mdata))
+ (st/emit! (dtm/update-team (with-meta team mdata))
(modal/hide))))
(defn- on-submit
@@ -73,7 +73,9 @@
{::mf/register modal/components
::mf/register-as :team-form}
[{:keys [team] :as props}]
- (let [initial (mf/use-memo (fn [] (or team {})))
+ (let [initial (mf/use-memo (fn []
+ (or (some-> team (select-keys [:name :id]))
+ {})))
form (fm/use-form :schema schema:team-form
:initial initial)
handle-keydown
diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs
index f410c332df..b336942364 100644
--- a/frontend/src/app/main/ui/dashboard/templates.cljs
+++ b/frontend/src/app/main/ui/dashboard/templates.cljs
@@ -9,8 +9,9 @@
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
+ [app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -18,7 +19,6 @@
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
- [app.util.router :as rt]
[app.util.storage :as storage]
[okulary.core :as l]
[potok.v2.core :as ptk]
@@ -43,9 +43,9 @@
:section section})
(when-not (some? project-id)
- (rt/nav :dashboard-files
- {:team-id team-id
- :project-id default-project-id}))))]
+ (dcm/go-to-dashboard-recent
+ :team-id team-id
+ :project-id default-project-id))))]
(st/emit!
(ptk/event ::ev/event {::ev/name "import-template-launch"
@@ -157,8 +157,8 @@
[:div {:class (stl/css :template-link-title)} (tr "dashboard.libraries-and-templates")]
[:div {:class (stl/css :template-link-text)} (tr "dashboard.libraries-and-templates.explore")]]]]]]))
-(mf/defc templates-section
- {::mf/wrap-props false}
+(mf/defc templates-section*
+ {::mf/props :obj}
[{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]
diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index dff48b329b..b17332ab6c 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -9,6 +9,7 @@
[app.config :as cf]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
@@ -22,6 +23,7 @@
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.storybook :as sb]
+ [app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.util.i18n :as i18n]))
@@ -38,10 +40,12 @@
:Loader loader*
:RawSvg raw-svg*
:Select select*
+ :Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*
:Toast toast*
:TokenStatusIcon token-status-icon*
+ :Swatch swatch*
;; meta / misc
:meta #js {:icons (clj->js (sort icon-list))
:tokenStatus (clj->js (sort token-status-list))
diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss
index 1011d12851..83f5732183 100644
--- a/frontend/src/app/main/ui/ds/_sizes.scss
+++ b/frontend/src/app/main/ui/ds/_sizes.scss
@@ -8,10 +8,13 @@
// TODO: create actual tokens once we have them from design
$sz-16: px2rem(16);
+$sz-24: px2rem(24);
$sz-32: px2rem(32);
$sz-36: px2rem(36);
$sz-160: px2rem(160);
$sz-200: px2rem(200);
$sz-224: px2rem(224);
+$sz-252: px2rem(252);
+$sz-284: px2rem(284);
$sz-400: px2rem(400);
$sz-964: px2rem(964);
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs
new file mode 100644
index 0000000000..e39efedb1c
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs
@@ -0,0 +1,252 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.controls.combobox
+ (:require-macros
+ [app.common.data.macros :as dm]
+ [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
+ [app.util.array :as array]
+ [app.util.dom :as dom]
+ [app.util.keyboard :as kbd]
+ [app.util.object :as obj]
+ [rumext.v2 :as mf]))
+
+(def listbox-id-index (atom 0))
+
+(defn- get-option
+ [options id]
+ (array/find #(= id (obj/get % "id")) options))
+
+(defn- handle-focus-change
+ [options focused* new-index options-nodes-refs]
+ (let [option (aget options new-index)
+ id (obj/get option "id")
+ nodes (mf/ref-val options-nodes-refs)
+ node (obj/get nodes id)]
+ (reset! focused* id)
+ (dom/scroll-into-view-if-needed! node)))
+
+(defn- handle-selection
+ [focused* selected* open*]
+ (when-let [focused (deref focused*)]
+ (reset! selected* focused))
+ (reset! open* false)
+ (reset! focused* nil))
+
+(def ^:private schema:combobox-option
+ [:and
+ [:map {:title "option"}
+ [:id :string]
+ [:icon {:optional true}
+ [:and :string [:fn #(contains? icon-list %)]]]
+ [:label {:optional true} :string]
+ [:aria-label {:optional true} :string]]
+ [:fn {:error/message "invalid data: missing required props"}
+ (fn [option]
+ (or (and (contains? option :icon)
+ (or (contains? option :label)
+ (contains? option :aria-label)))
+ (contains? option :label)))]])
+
+(def ^:private schema:combobox
+ [:map
+ [:options [:vector {:min 1} schema:combobox-option]]
+ [:class {:optional true} :string]
+ [:disabled {:optional true} :boolean]
+ [:default-selected {:optional true} :string]
+ [:on-change {:optional true} fn?]])
+
+(mf/defc combobox*
+ {::mf/props :obj
+ ::mf/schema schema:combobox}
+ [{:keys [options class disabled default-selected on-change] :rest props}]
+ (let [open* (mf/use-state false)
+ open (deref open*)
+
+ selected* (mf/use-state default-selected)
+ selected (deref selected*)
+
+ focused* (mf/use-state nil)
+ focused (deref focused*)
+
+ has-focus* (mf/use-state false)
+ has-focus (deref has-focus*)
+
+ dropdown-options
+ (mf/use-memo
+ (mf/deps options selected)
+ (fn []
+ (->> options
+ (array/filter (fn [option]
+ (let [lower-option (.toLowerCase (obj/get option "id"))
+ lower-filter (.toLowerCase selected)]
+ (.includes lower-option lower-filter)))))))
+
+ on-click
+ (mf/use-fn
+ (mf/deps disabled)
+ (fn [event]
+ (dom/stop-propagation event)
+ (when-not disabled
+ (reset! has-focus* true)
+ (if (= "INPUT" (.-tagName (.-target event)))
+ (reset! open* true)
+ (swap! open* not)))))
+
+ on-option-click
+ (mf/use-fn
+ (mf/deps on-change)
+ (fn [event]
+ (let [node (dom/get-current-target event)
+ id (dom/get-data node "id")]
+ (reset! selected* id)
+ (reset! focused* nil)
+ (reset! open* false)
+ (when (fn? on-change)
+ (on-change id)))))
+
+ options-nodes-refs (mf/use-ref nil)
+ options-ref (mf/use-ref nil)
+ listbox-id-ref (mf/use-ref (dm/str "listbox-" (swap! listbox-id-index inc)))
+ listbox-id (mf/ref-val listbox-id-ref)
+ combobox-ref (mf/use-ref nil)
+
+ set-ref
+ (mf/use-fn
+ (fn [node id]
+ (let [refs (or (mf/ref-val options-nodes-refs) #js {})
+ refs (if node
+ (obj/set! refs id node)
+ (obj/unset! refs id))]
+ (mf/set-ref-val! options-nodes-refs refs))))
+
+ on-blur
+ (mf/use-fn
+ (fn [event]
+ (let [target (.-relatedTarget event)
+ outside? (not (.contains (mf/ref-val combobox-ref) target))]
+ (when outside?
+ (reset! focused* nil)
+ (reset! open* false)
+ (reset! has-focus* false)))))
+
+ on-key-down
+ (mf/use-fn
+ (mf/deps open focused disabled dropdown-options)
+ (fn [event]
+ (when-not disabled
+ (let [options dropdown-options
+ focused (deref focused*)
+ len (alength options)
+ index (array/find-index #(= (deref focused*) (obj/get % "id")) options)]
+ (dom/stop-propagation event)
+
+ (when (< len 0)
+ (reset! index len))
+
+ (cond
+ (and (not open) (kbd/down-arrow? event))
+ (reset! open* true)
+
+ open
+ (cond
+ (kbd/home? event)
+ (handle-focus-change options focused* 0 options-nodes-refs)
+
+ (kbd/up-arrow? event)
+ (let [new-index (if (= index -1)
+ (dec len)
+ (mod (- index 1) len))]
+ (handle-focus-change options focused* new-index options-nodes-refs))
+
+
+ (kbd/down-arrow? event)
+ (let [new-index (if (= index -1)
+ 0
+ (mod (+ index 1) len))]
+ (handle-focus-change options focused* new-index options-nodes-refs))
+
+ (or (kbd/space? event) (kbd/enter? event))
+ (when (deref open*)
+ (dom/prevent-default event)
+ (handle-selection focused* selected* open*)
+ (when (fn? on-change)
+ (on-change focused)))
+
+ (kbd/esc? event)
+ (do (reset! open* false)
+ (reset! focused* nil))))))))
+
+ on-input-change
+ (mf/use-fn
+ (fn [event]
+ (let [value (.-value (.-currentTarget event))]
+ (reset! selected* value)
+ (reset! focused* nil)
+ (when (fn? on-change)
+ (on-change value)))))
+ on-focus
+ (mf/use-fn
+ (fn [_] (reset! has-focus* true)))
+
+ class (dm/str class " " (stl/css :combobox))
+
+ selected-option (get-option options selected)
+ icon (obj/get selected-option "icon")]
+
+ (mf/with-effect [options]
+ (mf/set-ref-val! options-ref options))
+
+ [:div {:ref combobox-ref
+ :class (stl/css-case
+ :combobox-wrapper true
+ :focused has-focus)}
+
+ [:div {:class class
+ :on-click on-click
+ :on-focus on-focus
+ :on-blur on-blur}
+ [:span {:class (stl/css-case :combobox-header true
+ :header-icon (some? icon))}
+ (when icon
+ [:> icon* {:id icon
+ :size "s"
+ :aria-hidden true}])
+ [:input {:type "text"
+ :role "combobox"
+ :aria-autocomplete "both"
+ :aria-expanded open
+ :aria-controls listbox-id
+ :aria-activedescendant focused
+ :class (stl/css :input)
+ :data-testid "combobox-input"
+ :disabled disabled
+ :value selected
+ :on-change on-input-change
+ :on-key-down on-key-down}]]
+
+ [:> :button {:tab-index "-1"
+ :aria-expanded open
+ :aria-controls listbox-id
+ :class (stl/css :button-toggle-list)
+ :on-click on-click}
+ [:> icon* {:id i/arrow
+ :class (stl/css :arrow)
+ :size "s"
+ :aria-hidden true
+ :data-testid "combobox-open-button"}]]]
+
+ (when (and open (seq dropdown-options))
+ [:> options-dropdown* {:on-click on-option-click
+ :options dropdown-options
+ :selected selected
+ :focused focused
+ :set-ref set-ref
+ :id listbox-id
+ :data-testid "combobox-options"}])]))
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.mdx b/frontend/src/app/main/ui/ds/controls/combobox.mdx
new file mode 100644
index 0000000000..3d2016a88c
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/combobox.mdx
@@ -0,0 +1,62 @@
+import { Canvas, Meta } from "@storybook/blocks";
+import * as ComboboxStories from "./combobox.stories";
+
+
+
+# Combobox
+
+Combobox lets users choose one option from an options menu or enter a custom value that is not listed in the menu. It combines the functionality of a dropdown menu and an input field, allowing for both selection and free-form input.
+
+## Variants
+
+**Text**: We will use this variant when there are enough space and icons don't add any useful context.
+
+
+
+**Icon and text**: We will use this variant when there are enough space and icons add any useful context.
+
+
+
+## Technical notes
+
+### Icons
+
+Each option of `combobox*` may accept an `icon`, which must contain an [icon ID](../foundations/assets/icon.mdx).
+These are available in the `app.main.ds.foundations.assets.icon` namespace.
+
+```clj
+(ns app.main.ui.foo
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :as i]))
+```
+
+```clj
+[:> combobox*
+ {:options [{ :label "Code"
+ :id "option-code"
+ :icon i/fill-content }
+ { :label "Design"
+ :id "option-design"
+ :icon i/pentool }
+ { :label "Menu"
+ :id "option-menu" }
+ ]}]
+```
+
+
+
+## Usage guidelines (design)
+
+### Where to Use
+
+Combobox is used in applications where users need to select from a range of text-based options or enter custom input.
+
+### When to Use
+
+Consider using a combobox when you have five or more options to present, and users may benefit from the ability to search or input a custom value that is not in the predefined list.
+
+### Interaction / Behavior
+
+- **Opening Options**: When the user clicks on the input area, a dropdown menu of options appears. Users can either scroll through the options, type to filter them, or input a new value directly.
+- **Selecting an Option**: Once an option is selected or a custom value is entered, the dropdown closes, and the input field displays the chosen value.
+- **Keyboard Support**: Combobox supports navigation using keyboard input, including arrow keys to navigate the list and Enter to make a selection.
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.scss b/frontend/src/app/main/ui/ds/controls/combobox.scss
new file mode 100644
index 0000000000..9fad67590f
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/combobox.scss
@@ -0,0 +1,87 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../_borders.scss" as *;
+@use "../_sizes.scss" as *;
+@use "../typography.scss" as *;
+
+.combobox-wrapper {
+ --combobox-icon-fg-color: var(--color-foreground-secondary);
+ --combobox-fg-color: var(--color-foreground-primary);
+ --combobox-bg-color: var(--color-background-tertiary);
+ --combobox-outline-color: none;
+ --combobox-border-color: none;
+
+ @include use-typography("body-small");
+ position: relative;
+ display: grid;
+ grid-template-rows: auto;
+ gap: var(--sp-xxs);
+ width: 100%;
+
+ &:hover {
+ --combobox-bg-color: var(--color-background-quaternary);
+ }
+}
+
+.combobox {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: var(--sp-xs);
+ height: $sz-32;
+ width: 100%;
+ padding: var(--sp-s);
+ border: none;
+ border-radius: $br-8;
+ outline: $b-1 solid var(--combobox-outline-color);
+ border: $b-1 solid var(--combobox-border-color);
+ background: var(--combobox-bg-color);
+ color: var(--combobox-fg-color);
+ appearance: none;
+
+ &:disabled {
+ --combobox-bg-color: var(--color-background-primary);
+ --combobox-border-color: var(--color-background-quaternary);
+ --combobox-fg-color: var(--color-foreground-secondary);
+ }
+}
+
+.focused {
+ --combobox-outline-color: var(--color-accent-primary);
+}
+
+.arrow {
+ color: var(--combobox-icon-fg-color);
+ transform: rotate(90deg);
+}
+
+.combobox-header {
+ display: grid;
+ justify-items: start;
+ gap: var(--sp-xs);
+}
+
+.input {
+ all: unset;
+
+ @include use-typography("body-small");
+ background-color: transparent;
+ overflow: hidden;
+ text-align: left;
+ inline-size: 100%;
+ padding-inline-start: var(--sp-xxs);
+ color: var(--combobox-fg-color);
+}
+
+.header-icon {
+ grid-template-columns: auto 1fr;
+ color: var(--combobox-icon-fg-color);
+}
+
+.button-toggle-list {
+ all: unset;
+ display: flex;
+}
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.stories.jsx b/frontend/src/app/main/ui/ds/controls/combobox.stories.jsx
new file mode 100644
index 0000000000..3318639374
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/combobox.stories.jsx
@@ -0,0 +1,216 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+import * as React from "react";
+import Components from "@target/components";
+
+import { userEvent, within, expect } from "@storybook/test";
+
+const { Combobox } = Components;
+
+let lastValue = null;
+
+export default {
+ title: "Controls/Combobox",
+ component: Combobox,
+ argTypes: {
+ disabled: { control: "boolean" },
+ },
+ args: {
+ disabled: false,
+ options: [
+ { id: "January", label: "January" },
+ { id: "February", label: "February" },
+ { id: "March", label: "March" },
+ { id: "April", label: "April" },
+ { id: "May", label: "May" },
+ { id: "June", label: "June" },
+ { id: "July", label: "July" },
+ { id: "August", label: "August" },
+ { id: "September", label: "September" },
+ { id: "October", label: "October" },
+ { id: "November", label: "November" },
+ { id: "December", label: "December" },
+ ],
+ defaultSelected: "February",
+ onChange: (value) => (lastValue = value),
+ },
+ parameters: {
+ controls: {
+ exclude: ["options", "defaultSelected"],
+ },
+ },
+ render: ({ ...args }) => (
+
+
+
+ ),
+};
+
+export const Default = {
+ parameters: {
+ docs: {
+ story: {
+ height: "450px",
+ },
+ },
+ },
+};
+
+export const WithIcons = {
+ args: {
+ options: [
+ { id: "January", label: "January", icon: "fill-content" },
+ { id: "February", label: "February", icon: "pentool" },
+ { id: "March", label: "March" },
+ { id: "April", label: "April" },
+ { id: "May", label: "May" },
+ { id: "June", label: "June" },
+ { id: "July", label: "July" },
+ { id: "August", label: "August" },
+ { id: "September", label: "September" },
+ { id: "October", label: "October" },
+ { id: "November", label: "November" },
+ { id: "December", label: "December" },
+ ],
+ },
+ parameters: {
+ docs: {
+ story: {
+ height: "450px",
+ },
+ },
+ },
+};
+
+export const TestInteractions = {
+ ...WithIcons,
+ play: async ({ canvasElement, step }) => {
+ const canvas = within(canvasElement);
+
+ const combobox = await canvas.getByRole("combobox");
+ const button = await canvas.getByTestId("combobox-open-button");
+ const input = await canvas.getByTestId("combobox-input");
+
+ const waitOptionNotPresent = async () => {
+ expect(canvas.queryByTestId("combobox-options")).not.toBeInTheDocument();
+ };
+
+ const waitOptionsPresent = async () => {
+ const options = await canvas.findByTestId("combobox-options");
+ expect(options).toBeVisible();
+
+ return options;
+ };
+
+ await userEvent.clear(input);
+
+ await step("Toggle dropdown on click arrow button", async () => {
+ await userEvent.click(button);
+
+ await waitOptionsPresent();
+ expect(combobox).toHaveAttribute("aria-expanded", "true");
+
+ await userEvent.click(button);
+ await waitOptionNotPresent();
+ expect(combobox).toHaveAttribute("aria-expanded", "false");
+ });
+
+ await step("Aria controls is set correctly", async () => {
+ await userEvent.click(button);
+
+ const ariaControls = combobox.getAttribute("aria-controls");
+
+ const options = await canvas.findByTestId("combobox-options");
+
+ expect(options).toHaveAttribute("id", ariaControls);
+ });
+
+ await step("Navigation keys", async () => {
+ // Arrow down
+ await userEvent.click(input);
+ await waitOptionsPresent();
+
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{Enter}");
+
+ expect(input).toHaveValue("February");
+ expect(lastValue).toBe("February");
+ await userEvent.clear(input);
+
+ // Arrow up
+ await userEvent.keyboard("{ArrowDown}");
+ await waitOptionsPresent();
+
+ await userEvent.keyboard("{ArrowUp}");
+ await userEvent.keyboard("{ArrowUp}");
+ expect(combobox).toHaveAttribute("aria-activedescendant", "November");
+ await userEvent.keyboard("{Enter}");
+
+ expect(input).toHaveValue("November");
+ expect(lastValue).toBe("November");
+ await userEvent.clear(input);
+
+ // Home
+ await userEvent.keyboard("{ArrowDown}");
+ await waitOptionsPresent();
+
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{Home}");
+ expect(combobox).toHaveAttribute("aria-activedescendant", "January");
+ await userEvent.keyboard("{Enter}");
+
+ expect(input).toHaveValue("January");
+ expect(lastValue).toBe("January");
+ await userEvent.clear(input);
+ });
+
+ await step("Toggle dropdown with arrow down and ESC", async () => {
+ userEvent.click(input);
+
+ await waitOptionsPresent();
+
+ await userEvent.keyboard("{Escape}");
+ expect(combobox).toHaveAttribute("aria-expanded", "false");
+ await waitOptionNotPresent();
+
+ await userEvent.keyboard("{ArrowDown}");
+ await waitOptionsPresent();
+ expect(combobox).toHaveAttribute("aria-expanded", "true");
+
+ await userEvent.keyboard("{Escape}");
+ await waitOptionNotPresent();
+ expect(combobox).toHaveAttribute("aria-expanded", "false");
+ });
+
+ await step("Filter with 'Ju' and select July", async () => {
+ await userEvent.type(input, "Ju");
+
+ const options = await canvas.findAllByTestId("dropdown-option");
+ expect(options).toHaveLength(2);
+
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{ArrowDown}");
+
+ await userEvent.keyboard("{Enter}");
+
+ expect(input).toHaveValue("July");
+ expect(lastValue).toBe("July");
+ });
+
+ await step("Close dropdown when focus out", async () => {
+ await userEvent.click(button);
+
+ await waitOptionsPresent();
+
+ await userEvent.tab();
+
+ await waitOptionNotPresent();
+ });
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs
index ae74123c34..0df0c0c6da 100644
--- a/frontend/src/app/main/ui/ds/controls/input.cljs
+++ b/frontend/src/app/main/ui/ds/controls/input.cljs
@@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@@ -25,17 +26,25 @@
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema:input}
- [{:keys [icon class type external-ref] :rest props}]
- (let [ref (or external-ref (mf/use-ref))
- type (or type "text")
- icon-class (stl/css-case :input true
- :input-with-icon (some? icon))
- props (mf/spread-props props {:class icon-class :ref ref :type type})
- handle-icon-click (mf/use-fn (mf/deps ref)
- (fn [_]
- (let [input-node (mf/ref-val ref)]
- (dom/select-node input-node)
- (dom/focus! input-node))))]
- [:> "span" {:class (dm/str class " " (stl/css :container))}
- (when icon [:> icon* {:id icon :class (stl/css :icon) :on-click handle-icon-click}])
- [:> "input" props]]))
+ [{:keys [icon class type] :rest props} ref]
+ (let [ref (or ref (mf/use-ref))
+ type (d/nilv type "text")
+ props (mf/spread-props props
+ :class (stl/css-case
+ :input true
+ :input-with-icon (some? icon))
+ :ref ref
+ :type type)
+
+ on-icon-click
+ (mf/use-fn
+ (mf/deps ref)
+ (fn [_event]
+ (let [input-node (mf/ref-val ref)]
+ (dom/select-node input-node)
+ (dom/focus! input-node))))]
+
+ [:> :span {:class (dm/str class " " (stl/css :container))}
+ (when (some? icon)
+ [:> icon* {:id icon :class (stl/css :icon) :on-click on-icon-click}])
+ [:> :input props]]))
diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs
index e50e6a45fb..0d2730ebb6 100644
--- a/frontend/src/app/main/ui/ds/controls/select.cljs
+++ b/frontend/src/app/main/ui/ds/controls/select.cljs
@@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
+ [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.util.array :as array]
[app.util.dom :as dom]
@@ -16,63 +17,6 @@
[app.util.object :as obj]
[rumext.v2 :as mf]))
-(mf/defc option*
- {::mf/props :obj
- ::mf/private true}
- [{:keys [id label icon aria-label on-click selected set-ref focused] :rest props}]
- [:> :li {:value id
- :class (stl/css-case :option true
- :option-with-icon (some? icon)
- :option-current focused)
- :aria-selected selected
-
- :ref (fn [node]
- (set-ref node id))
- :role "option"
- :id id
- :on-click on-click
- :data-id id}
-
- (when (some? icon)
- [:> icon*
- {:id icon
- :size "s"
- :class (stl/css :option-icon)
- :aria-hidden (when label true)
- :aria-label (when (not label) aria-label)}])
-
- [:span {:class (stl/css :option-text)} label]
- (when selected
- [:> icon*
- {:id i/tick
- :size "s"
- :class (stl/css :option-check)
- :aria-hidden (when label true)}])])
-
-(mf/defc options-dropdown*
- {::mf/props :obj
- ::mf/private true}
- [{:keys [set-ref on-click options selected focused] :rest props}]
- (let [props (mf/spread-props props
- {:class (stl/css :option-list)
- :tab-index "-1"
- :role "listbox"})]
- [:> "ul" props
- (for [option ^js options]
- (let [id (obj/get option "id")
- label (obj/get option "label")
- aria-label (obj/get option "aria-label")
- icon (obj/get option "icon")]
- [:> option* {:selected (= id selected)
- :key id
- :id id
- :label label
- :icon icon
- :aria-label aria-label
- :set-ref set-ref
- :focused (= id focused)
- :on-click on-click}]))]))
-
(def ^:private schema:select-option
[:and
[:map {:title "option"}
diff --git a/frontend/src/app/main/ui/ds/controls/select.scss b/frontend/src/app/main/ui/ds/controls/select.scss
index ff2cbe5077..072870cd4d 100644
--- a/frontend/src/app/main/ui/ds/controls/select.scss
+++ b/frontend/src/app/main/ui/ds/controls/select.scss
@@ -14,7 +14,6 @@
--select-bg-color: var(--color-background-tertiary);
--select-outline-color: none;
--select-border-color: none;
- --select-dropdown-border-color: var(--color-background-quaternary);
&:hover {
--select-bg-color: var(--color-background-quaternary);
@@ -81,67 +80,3 @@
grid-template-columns: auto 1fr;
color: var(--select-icon-fg-color);
}
-
-.option-list {
- --options-dropdown-bg-color: var(--color-background-tertiary);
- position: absolute;
- right: 0;
- top: $sz-36;
- width: 100%;
- background-color: var(--options-dropdown-bg-color);
- border-radius: $br-8;
- border: $b-1 solid var(--select-dropdown-border-color);
- padding-block: var(--sp-xs);
- margin-block-end: 0;
- max-height: $sz-400;
- overflow-y: auto;
- overflow-x: hidden;
-}
-
-.option {
- --select-option-fg-color: var(--color-foreground-primary);
- --select-option-bg-color: unset;
-
- &:hover {
- --select-option-bg-color: var(--color-background-quaternary);
- }
-
- &[aria-selected="true"] {
- --select-option-bg-color: var(--color-background-quaternary);
- }
-
- display: grid;
- align-items: center;
- justify-items: start;
- grid-template-columns: 1fr auto;
- gap: var(--sp-xs);
- width: 100%;
- height: $sz-32;
- padding: var(--sp-s);
- border-radius: $br-8;
- outline: $b-1 solid var(--select-outline-color);
- outline-offset: -1px;
- background-color: var(--select-option-bg-color);
-}
-
-.option-with-icon {
- grid-template-columns: auto 1fr auto;
-}
-
-.option-text {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- width: 100%;
- min-width: 0;
- padding-inline-start: var(--sp-xxs);
-}
-
-.option-icon {
- color: var(--select-icon-fg-color);
-}
-
-.option-current {
- --select-option-outline-color: var(--color-accent-primary);
- outline: $b-1 solid var(--select-option-outline-color);
-}
diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs
new file mode 100644
index 0000000000..16ce0240ac
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs
@@ -0,0 +1,69 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.controls.shared.options-dropdown
+ (:require-macros
+ [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
+ [app.util.object :as obj]
+ [rumext.v2 :as mf]))
+
+(mf/defc option*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [id label icon aria-label on-click selected set-ref focused] :rest props}]
+ [:> :li {:value id
+ :class (stl/css-case :option true
+ :option-with-icon (some? icon)
+ :option-current focused)
+ :aria-selected selected
+ :ref (fn [node]
+ (set-ref node id))
+ :role "option"
+ :id id
+ :on-click on-click
+ :data-id id
+ :data-testid "dropdown-option"}
+
+ (when (some? icon)
+ [:> icon*
+ {:id icon
+ :size "s"
+ :class (stl/css :option-icon)
+ :aria-hidden (when label true)
+ :aria-label (when (not label) aria-label)}])
+
+ [:span {:class (stl/css :option-text)} label]
+ (when selected
+ [:> icon*
+ {:id i/tick
+ :size "s"
+ :class (stl/css :option-check)
+ :aria-hidden (when label true)}])])
+
+(mf/defc options-dropdown*
+ {::mf/props :obj}
+ [{:keys [set-ref on-click options selected focused] :rest props}]
+ (let [props (mf/spread-props props
+ {:class (stl/css :option-list)
+ :tab-index "-1"
+ :role "listbox"})]
+ [:> "ul" props
+ (for [option ^js options]
+ (let [id (obj/get option "id")
+ label (obj/get option "label")
+ aria-label (obj/get option "aria-label")
+ icon (obj/get option "icon")]
+ [:> option* {:selected (= id selected)
+ :key id
+ :id id
+ :label label
+ :icon icon
+ :aria-label aria-label
+ :set-ref set-ref
+ :focused (= id focused)
+ :on-click on-click}]))]))
diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
new file mode 100644
index 0000000000..8b330e30b8
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
@@ -0,0 +1,74 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../../_borders.scss" as *;
+@use "../../_sizes.scss" as *;
+@use "../../typography.scss" as *;
+
+.option-list {
+ --options-dropdown-icon-fg-color: var(--color-foreground-secondary);
+ --options-dropdown-bg-color: var(--color-background-tertiary);
+ --options-dropdown-outline-color: none;
+ --options-dropdown-border-color: var(--color-background-quaternary);
+
+ position: absolute;
+ right: 0;
+ top: $sz-36;
+ width: 100%;
+ background-color: var(--options-dropdown-bg-color);
+ border-radius: $br-8;
+ border: $b-1 solid var(--options-dropdown-dropdown-border-color);
+ padding-block: var(--sp-xs);
+ margin-block-end: 0;
+ max-height: $sz-400;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.option {
+ --options-dropdown-fg-color: var(--color-foreground-primary);
+ --options-dropdown-bg-color: unset;
+
+ display: grid;
+ align-items: center;
+ justify-items: start;
+ grid-template-columns: 1fr auto;
+ gap: var(--sp-xs);
+ width: 100%;
+ height: $sz-32;
+ padding: var(--sp-s);
+ border-radius: $br-8;
+ outline: $b-1 solid var(--options-dropdown-outline-color);
+ outline-offset: -1px;
+ background-color: var(--options-dropdown-bg-color);
+
+ &:hover,
+ &[aria-selected="true"] {
+ --options-dropdown-bg-color: var(--color-background-quaternary);
+ }
+}
+
+.option-with-icon {
+ grid-template-columns: auto 1fr auto;
+}
+
+.option-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ min-width: 0;
+ padding-inline-start: var(--sp-xxs);
+}
+
+.option-icon {
+ color: var(--options-dropdown-icon-fg-color);
+}
+
+.option-current {
+ --options-dropdown-outline-color: var(--color-accent-primary);
+ outline: $b-1 solid var(--options-dropdown-outline-color);
+}
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.scss b/frontend/src/app/main/ui/ds/foundations/assets/icon.scss
index 7430a0c31c..afc37923d1 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/icon.scss
+++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.scss
@@ -5,6 +5,6 @@
// Copyright (c) KALEIDOS INC
.icon {
- fill: none;
- stroke: currentColor;
+ fill: var(--icon-fill-color, none);
+ stroke: var(--icon-stroke-color, currentColor);
}
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.stories.jsx b/frontend/src/app/main/ui/ds/foundations/assets/icon.stories.jsx
index 49219f46bc..ad375f8d06 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/icon.stories.jsx
+++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.stories.jsx
@@ -26,16 +26,26 @@ export const All = {
<>
All Icons
- Hover on an icon to see its ID.
-
+
{icons.map((iconId) => (
+
+ {iconId}
+
))}
diff --git a/frontend/src/app/main/ui/ds/storybook.scss b/frontend/src/app/main/ui/ds/storybook.scss
index 167fe085c2..0bd5c45e9b 100644
--- a/frontend/src/app/main/ui/ds/storybook.scss
+++ b/frontend/src/app/main/ui/ds/storybook.scss
@@ -16,6 +16,7 @@
.story-header {
color: var(--color-foreground-primary);
+ margin-bottom: 1rem;
}
.story-grid-row {
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs
new file mode 100644
index 0000000000..bfce203c86
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs
@@ -0,0 +1,103 @@
+
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.utilities.swatch
+ (:require-macros
+ [app.main.style :as stl])
+ (:require
+ [app.common.data.macros :as dm]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(def ^:private schema:swatch
+ [:map
+ [:background :string]
+ [:class {:optional true} :string]
+ [:format {:optional true} [:enum "square" "rounded"]]
+ [:size {:optional true} [:enum "small" "medium"]]
+ [:active {:optional true} :boolean]
+ [:on-click {:optional true} fn?]])
+
+(def hex-regex #"^#(?:[0-9a-fA-F]{3}){1,2}$")
+(def rgb-regex #"^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$")
+(def hsl-regex #"^hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$")
+(def hsla-regex #"^hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0|1|0?\.\d+)\)$")
+(def rgba-regex #"^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0|1|0?\.\d+)\)$")
+
+(defn- gradient? [background]
+ (or
+ (str/starts-with? background "linear-gradient")
+ (str/starts-with? background "radial-gradient")))
+
+(defn- color-solid? [background]
+ (boolean
+ (or (re-matches hex-regex background)
+ (or (re-matches hsl-regex background)
+ (re-matches rgb-regex background)))))
+
+(defn- color-opacity? [background]
+ (boolean
+ (or (re-matches hsla-regex background)
+ (re-matches rgba-regex background))))
+
+(defn- extract-color-and-opacity [background]
+ (cond
+ (re-matches rgba-regex background)
+ (let [[_ r g b a] (re-matches rgba-regex background)]
+ {:color (dm/str "rgb(" r ", " g ", " b ")")
+ :opacity (js/parseFloat a)})
+
+ (re-matches hsla-regex background)
+ (let [[_ h s l a] (re-matches hsla-regex background)]
+ {:color (dm/str "hsl(" h ", " s "%, " l "%)")
+ :opacity (js/parseFloat a)})
+
+ :else
+ {:color background
+ :opacity 1.0}))
+
+(mf/defc swatch*
+ {::mf/props :obj
+ ::mf/schema schema:swatch}
+ [{:keys [background on-click format size active class]
+ :rest props}]
+ (let [element-type (if on-click "button" "div")
+ button-type (if on-click "button" nil)
+ format (or format "square")
+ size (or size "small")
+ active (or active false)
+ {:keys [color opacity]} (extract-color-and-opacity background)
+ class (dm/str class " " (stl/css-case
+ :swatch true
+ :small (= size "small")
+ :medium (= size "medium")
+ :square (= format "square")
+ :active (= active true)
+ :interactive (= element-type "button")
+ :rounded (= format "rounded")))
+ props (mf/spread-props props {:class class :on-click on-click :type button-type})]
+
+ [:> element-type props
+ (cond
+ (color-solid? background)
+ [:span {:class (stl/css :swatch-solid)
+ :style {:background background}}]
+
+ (color-opacity? background)
+ [:span {:class (stl/css :swatch-opacity)}
+ [:span {:class (stl/css :swatch-solid-side)
+ :style {:background color}}]
+ [:span {:class (stl/css :swatch-opacity-side)
+ :style {:background color :opacity opacity}}]]
+
+ (gradient? background)
+ [:span {:class (stl/css :swatch-gradient)
+ :style {:background-image (str background ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
+
+ :else
+ [:span {:class (stl/css :swatch-image)
+ :style {:background-image (str "url('" background "'), repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}])]))
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.mdx b/frontend/src/app/main/ui/ds/utilities/swatch.mdx
new file mode 100644
index 0000000000..a091a4e325
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.mdx
@@ -0,0 +1,67 @@
+import { Canvas, Meta } from "@storybook/blocks";
+import * as SwatchStories from "./swatch.stories";
+
+
+
+# Swatch
+
+Swatches are elements that display a color, gradient or image. They can sometimes trigger an action.
+
+## Variants
+
+**Color** (`"color"`), displays a solid color. It can take a hexadecimal, an rgb or an rgba.
+
+
+
+**WithOpacity** (`"color"`), displays a solid color on one side and the same color with its opacity applied on the other side. It can take a hexadecimal, an rgb or an rgba.
+
+
+
+**Gradient** (`"gradient"`), displays a gradient. A gradient should be a `linear-gradient` or a `conic-gradient`.
+
+
+
+**Image** (`"image"`) the swatch could display any image.
+
+
+
+**Active** (`"active"`) displays the swatch as active while an interface related action is happening.
+
+
+
+**Size** (`"size"`) shows a bigger or smaller swatch. Accepts `small` and `medium` (_default_) sizes.
+
+
+
+**Format** (`"format"`) displays a square or rounded swatch. Accepts `square` (_default_) and `rounded` sizes.
+
+
+
+## Technical Notes
+
+### Background
+
+The `swatch*` component accepts a `background` prop, which must be:
+
+- An hexadecimal (e.g. `#996633`)
+- An RGB (e.g. `rgb(125, 125, 0)`)
+- An RGBA (e.g. `rgba(125, 125, 0, 0.3)`)
+- A linear gradient (e.g. `linear-gradient(to right, blue, pink)`)
+- A conic gradient (e.g. `conic-gradient(red, orange, yellow, green, blue)`)
+- An image (e.g. `url(https://placecats.com/100/100)`)
+
+### onClick
+
+> Note: If the swatch is interactive, an `aria-label` is required. More on the `Accessibility` section.
+
+The swatch button accepts an onClick prop that expect a function on the parent context.
+It should be useful for launching other tools as a color picker.
+It runs when the user clics on the swatch, or presses enter or space while focusing it.
+
+### Accessibility
+
+If the swatch is interactive, an `aria-label` is required.
+
+```clj
+[:> swatch* {:on-click launch-colorpicker :aria-label "Lorem ipsum"}]
+```
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss
new file mode 100644
index 0000000000..938bfc7322
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss
@@ -0,0 +1,104 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../_borders.scss" as *;
+@use "../_sizes.scss" as *;
+@use "../colors.scss" as *;
+
+.swatch {
+ --border-color: var(--color-accent-primary-muted);
+ --border-radius: #{$br-4};
+ --border-color-active: var(--color-foreground-primary);
+ --border-color-active-inset: var(--color-background-primary);
+
+ --checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%);
+ --checkerboard-size: 0.5rem 0.5rem;
+
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ overflow: hidden;
+
+ &:focus {
+ --border-color: var(--color-accent-primary);
+ }
+}
+
+.small {
+ inline-size: $sz-16;
+ block-size: $sz-16;
+}
+
+.medium {
+ --checkerboard-size: 1rem 1rem;
+
+ inline-size: $sz-24;
+ block-size: $sz-24;
+}
+
+.rounded {
+ --border-radius: #{$br-circle};
+}
+
+.active {
+ --border-color: var(--border-color-active);
+
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ inline-size: 100%;
+ block-size: 100%;
+ border-radius: 3px;
+ box-shadow: 0 0 0 1px var(--border-color-active-inset) inset;
+ }
+}
+
+.interactive {
+ cursor: pointer;
+ appearance: none;
+ margin: 0;
+ padding: 0;
+ background: none;
+
+ &:hover {
+ border: 2px solid var(--border-color);
+ }
+}
+
+.swatch-image,
+.swatch-gradient,
+.swatch-opacity,
+.swatch-solid {
+ block-size: 100%;
+ display: block;
+}
+
+.swatch-gradient {
+ background-size: cover, var(--checkerboard-size);
+ background-position: center, center;
+ background-repeat: no-repeat, repeat;
+}
+
+.swatch-image {
+ background-size: cover, var(--checkerboard-size);
+ background-position: center, center;
+ background-repeat: no-repeat, repeat;
+}
+
+.swatch-opacity {
+ background: var(--checkerboard-background);
+ background-size: var(--checkerboard-size);
+ display: flex;
+}
+
+.swatch-solid-side,
+.swatch-opacity-side {
+ flex: 1;
+ display: block;
+}
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
new file mode 100644
index 0000000000..165b7c5993
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
@@ -0,0 +1,86 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+import * as React from "react";
+import Components from "@target/components";
+import { action } from "@storybook/addon-actions";
+
+const { Swatch } = Components;
+
+export default {
+ title: "Foundations/Utilities/Swatch",
+ component: Swatch,
+ argTypes: {
+ background: {
+ control: { type: "text" },
+ },
+ format: {
+ control: "select",
+ options: ["square", "rounded"],
+ },
+ size: {
+ control: "select",
+ options: ["small", "medium"],
+ },
+ active: {
+ control: { type: "boolean" },
+ },
+ },
+ args: {
+ background: "#663399",
+ format: "square",
+ size: "medium",
+ active: false,
+ },
+ render: ({ ...args }) => ,
+};
+
+export const Default = {};
+
+export const WithOpacity = {
+ args: {
+ background: "rgba(255, 0, 0, 0.5)",
+ },
+};
+
+export const LinearGradient = {
+ args: {
+ background: "linear-gradient(to right, transparent, mistyrose)",
+ },
+};
+
+export const Image = {
+ args: {
+ background: "images/form/never-used.png",
+ size: "medium",
+ },
+};
+
+export const Rounded = {
+ args: {
+ format: "rounded",
+ },
+};
+
+export const Small = {
+ args: {
+ size: "small",
+ },
+};
+
+export const Active = {
+ args: {
+ active: true,
+ background: "#CC00CC",
+ },
+};
+
+export const Clickable = {
+ args: {
+ onClick: action("on-click"),
+ "aria-label": "Click swatch",
+ },
+};
diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs
index a2d343a498..633bceed7d 100644
--- a/frontend/src/app/main/ui/exports/assets.cljs
+++ b/frontend/src/app/main/ui/exports/assets.cljs
@@ -12,7 +12,6 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
- [app.main.data.events :as ev]
[app.main.data.exports.assets :as de]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
@@ -23,7 +22,6 @@
[app.util.i18n :as i18n :refer [tr c]]
[app.util.strings :as ust]
[cuerdas.core :as str]
- [potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(def ^:private neutral-icon
@@ -59,13 +57,8 @@
(fn [event]
(dom/prevent-default event)
(st/emit! (modal/hide)
- (de/request-multiple-export
- {:exports enabled-exports
- :cmd cmd})
- (ptk/event
- ::ev/event {::ev/name "export-shapes"
- ::ev/origin origin
- :num-shapes (count enabled-exports)})))
+ (de/request-multiple-export {:exports enabled-exports :cmd cmd})
+ (de/export-shapes-event enabled-exports origin)))
on-toggle-enabled
(mf/use-fn
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index c73c7d1bd9..29b0575572 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -63,6 +63,7 @@
(def ^:icon arrow (icon-xref :arrow))
(def ^:icon asc-sort (icon-xref :asc-sort))
(def ^:icon board (icon-xref :board))
+(def ^:icon board-2 (icon-xref :board-2))
(def ^:icon boards-thumbnail (icon-xref :boards-thumbnail))
(def ^:icon boolean-difference (icon-xref :boolean-difference))
(def ^:icon boolean-exclude (icon-xref :boolean-exclude))
diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs
index 4cae675931..3aa83a9ac6 100644
--- a/frontend/src/app/main/ui/modal.cljs
+++ b/frontend/src/app/main/ui/modal.cljs
@@ -7,7 +7,8 @@
(ns app.main.ui.modal
(:require-macros [app.main.style :as stl])
(:require
- [app.main.data.modal :as dm]
+ [app.common.data.macros :as dm]
+ [app.main.data.modal :as modal]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.keyboard :as k]
@@ -20,13 +21,13 @@
[event allow-click-outside]
(when (and (k/esc? event) (not allow-click-outside))
(dom/stop-propagation event)
- (st/emit! (dm/hide))))
+ (st/emit! (modal/hide))))
(defn- on-pop-state
[event]
(dom/prevent-default event)
(dom/stop-propagation event)
- (st/emit! (dm/hide))
+ (st/emit! (modal/hide))
(.forward js/history))
(defn- on-click-outside
@@ -41,15 +42,14 @@
(= (.-button event) 0))
(dom/stop-propagation event)
(dom/prevent-default event)
- (st/emit! (dm/hide)))))
+ (st/emit! (modal/hide)))))
-(mf/defc modal-wrapper
- {::mf/wrap-props false
+(mf/defc modal-wrapper*
+ {::mf/props :obj
::mf/wrap [mf/memo]}
- [props]
- (let [data (unchecked-get props "data")
- wrapper-ref (mf/use-ref nil)
- components (mf/deref dm/components)
+ [{:keys [data]}]
+ (let [wrapper-ref (mf/use-ref nil)
+ components (mf/deref modal/components)
allow-click-outside (:allow-click-outside data)
@@ -61,31 +61,29 @@
(fn [event]
(on-esc-clicked event allow-click-outside))]
- (mf/use-layout-effect
- (mf/deps allow-click-outside)
- (fn []
- (let [keys [(events/listen js/window EventType.POPSTATE on-pop-state)
- (events/listen js/document EventType.KEYDOWN handle-keydown)
+ (mf/with-effect [allow-click-outside]
+ (let [keys [(events/listen js/window EventType.POPSTATE on-pop-state)
+ (events/listen js/document EventType.KEYDOWN handle-keydown)
- ;; Changing to js/document breaks the color picker
- (events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside)
+ ;; Changing to js/document breaks the color picker
+ (events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside)
- (events/listen js/document EventType.CONTEXTMENU handle-click-outside)]]
- #(doseq [key keys]
- (events/unlistenByKey key)))))
+ (events/listen js/document EventType.CONTEXTMENU handle-click-outside)]]
+ (fn []
+ (run! events/unlistenByKey keys))))
(when-let [component (get components (:type data))]
[:div {:ref wrapper-ref
:class (stl/css :modal-wrapper)}
(mf/element component (:props data))])))
-(def modal-ref
- (l/derived ::dm/modal st/state))
+(def ^:private ref:modal
+ (l/derived ::modal/modal st/state))
-(mf/defc modal
- {::mf/wrap-props false}
+(mf/defc modal-container*
+ {::mf/props :obj}
[]
- (let [modal (mf/deref modal-ref)]
- (when modal
- [:& modal-wrapper {:data modal
- :key (:id modal)}])))
+ (when-let [modal (mf/deref ref:modal)]
+ (mf/portal
+ (mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}])
+ (.-body js/document))))
diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs
index 48c3db10fe..a501659f29 100644
--- a/frontend/src/app/main/ui/onboarding/newsletter.cljs
+++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs
@@ -7,9 +7,9 @@
(ns app.main.ui.onboarding.newsletter
(:require-macros [app.main.style :as stl])
(:require
- [app.main.data.events :as-alias ev]
+ [app.main.data.event :as-alias ev]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs
index 5e223f69f6..6f5592db52 100644
--- a/frontend/src/app/main/ui/onboarding/questions.cljs
+++ b/frontend/src/app/main/ui/onboarding/questions.cljs
@@ -11,8 +11,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
- [app.main.data.events :as-alias ev]
- [app.main.data.users :as du]
+ [app.main.data.event :as-alias ev]
+ [app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs
index 0fe0f961b3..1bec75b409 100644
--- a/frontend/src/app/main/ui/onboarding/team_choice.cljs
+++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs
@@ -9,15 +9,15 @@
(:require
[app.common.data.macros :as dm]
[app.common.schema :as sm]
- [app.main.data.dashboard :as dd]
- [app.main.data.events :as ev]
- [app.main.data.users :as du]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
+ [app.main.data.profile :as du]
+ [app.main.data.team :as dtm]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -84,7 +84,7 @@
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
:onboarding-viewed true})
(when go-to-team?
- (rt/nav :dashboard-projects {:team-id team-id}))))))
+ (dcm/go-to-dashboard-recent :team-id team-id))))))
on-error
(mf/use-fn
@@ -117,7 +117,7 @@
(let [mdata {:on-success on-success
:on-error on-error}
params {:name name}]
- (st/emit! (-> (dd/create-team (with-meta params mdata))
+ (st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :onboarding-without-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
@@ -133,7 +133,7 @@
(let [mdata {:on-success on-success
:on-error on-error}]
- (st/emit! (-> (dd/create-team-with-invitations (with-meta params mdata))
+ (st/emit! (-> (dtm/create-team-with-invitations (with-meta params mdata))
(with-meta {::ev/origin :onboarding-with-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs
index d07f2d95f1..da769523d1 100644
--- a/frontend/src/app/main/ui/releases.cljs
+++ b/frontend/src/app/main/ui/releases.cljs
@@ -7,7 +7,7 @@
(ns app.main.ui.releases
(:require
[app.main.data.modal :as modal]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10]
@@ -30,6 +30,7 @@
[app.main.ui.releases.v2-1]
[app.main.ui.releases.v2-2]
[app.main.ui.releases.v2-3]
+ [app.main.ui.releases.v2-4]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.v2 :as mf]))
@@ -94,4 +95,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
- (rc/render-release-notes (assoc params :version "2.2")))
+ (rc/render-release-notes (assoc params :version "2.4")))
diff --git a/frontend/src/app/main/ui/releases/v2_3.cljs b/frontend/src/app/main/ui/releases/v2_3.cljs
index 4d3789a10b..8b3040b8f4 100644
--- a/frontend/src/app/main/ui/releases/v2_3.cljs
+++ b/frontend/src/app/main/ui/releases/v2_3.cljs
@@ -41,7 +41,7 @@
"The introduction of our brand new Plugin system allows you to access even richer ecosystem of capabilities."]
[:p {:class (stl/css :feature-content)}
- "We are beyond excitement about how this will further involve the Penpot community in building the best design and prototyping platform."]
+ "We are beyond excited about how this will further involve the Penpot community in building the best design and prototyping platform."]
[:p {:class (stl/css :feature-content)}
"Let’s dive in!"]]
@@ -69,7 +69,7 @@
"Penpot Plugins encourage developers to easily customize and expand the platform using standard web technologies like JavaScript, CSS, and HTML."]
[:p {:class (stl/css :feature-content)}
- "Find everything you need in ouor full comprehensive documentation to start building your plugins now!"]]
+ "Find everything you need in our full comprehensive documentation to start building your plugins now!"]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
@@ -101,7 +101,7 @@
"Be sure to keep an eye on our evolving " [:a {:href "https://penpot.app/penpothub" :target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
[:p {:class (stl/css :feature-content)}
- "This is just the beginning of a myriad of possibilities. Let’s build this community together <3."]]
+ "This is just the beginning of a myriad of possibilities. Let’s build this community together ❤️."]]
[:div {:class (stl/css :navigation)}
diff --git a/frontend/src/app/main/ui/releases/v2_4.cljs b/frontend/src/app/main/ui/releases/v2_4.cljs
new file mode 100644
index 0000000000..1559911a4d
--- /dev/null
+++ b/frontend/src/app/main/ui/releases/v2_4.cljs
@@ -0,0 +1,141 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.releases.v2-4
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.data.macros :as dm]
+ [app.main.ui.releases.common :as c]
+ [rumext.v2 :as mf]))
+
+;; TODO: Review all copies and alt text
+(defmethod c/render-release-notes "2.4"
+ [{:keys [slide klass next finish navigate version]}]
+ (mf/html
+ (case slide
+ :start
+ [:div {:class (stl/css-case :modal-overlay true)}
+ [:div.animated {:class klass}
+ [:div {:class (stl/css :modal-container)}
+ [:img {:src "images/features/2.4-slide-0.jpg"
+ :class (stl/css :start-image)
+ :border "0"
+ :alt "A graphic illustration with Penpot style"}]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-header)}
+ [:h1 {:class (stl/css :modal-title)}
+ "What’s new in Penpot?"]
+
+ [:div {:class (stl/css :version-tag)}
+ (dm/str "Version " version)]]
+
+ [:div {:class (stl/css :features-block)}
+ [:span {:class (stl/css :feature-title)}
+ "At Penpot we are at full speed!"]
+
+ [:p {:class (stl/css :feature-content)}
+ "With the release of the long-awaited Plugins System still fresh, this 2.4 brings improvements in a wide range of areas that will serve a variety of use cases."]
+
+ [:p {:class (stl/css :feature-content)}
+ "This release combines some of the most requested features—such as versioning and the viewer-only role—with performance improvements and a new .penpot format that will streamline the export of files and assets."]
+
+ [:p {:class (stl/css :feature-content)}
+ "Let’s dive in!"]]
+
+ [:div {:class (stl/css :navigation)}
+ [:button {:class (stl/css :next-btn)
+ :on-click next} "Continue"]]]]]]
+
+ 0
+ [:div {:class (stl/css-case :modal-overlay true)}
+ [:div.animated {:class klass}
+ [:div {:class (stl/css :modal-container)}
+ [:img {:src "images/features/2.4-viewer.gif"
+ :class (stl/css :start-image)
+ :border "0"
+ :alt "Viewer role, designed to enhance collaboration"}]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-header)}
+ [:h1 {:class (stl/css :modal-title)}
+ "Viewer role, designed to enhance collaboration"]]
+
+ [:div {:class (stl/css :feature)}
+ [:p {:class (stl/css :feature-content)}
+ "Collaboration takes many forms, and sometimes the risk of making unwanted or accidental adjustments can be a barrier to engaging with a design file."]
+
+ [:p {:class (stl/css :feature-content)}
+ "Now, you can invite members to your teams who only need to view and comment on files. Team members, stakeholders, developers… pick your case. Anyone who doesn't need to edit can participate confidently."]]
+
+ [:div {:class (stl/css :navigation)}
+ [:& c/navigation-bullets
+ {:slide slide
+ :navigate navigate
+ :total 3}]
+
+ [:button {:on-click next
+ :class (stl/css :next-btn)} "Continue"]]]]]]
+
+ 1
+ [:div {:class (stl/css-case :modal-overlay true)}
+ [:div.animated {:class klass}
+ [:div {:class (stl/css :modal-container)}
+ [:img {:src "images/features/2.4-history.gif"
+ :class (stl/css :start-image)
+ :border "0"
+ :alt "A timeline for your design process"}]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-header)}
+ [:h1 {:class (stl/css :modal-title)}
+ "A timeline for your design process"]]
+
+ [:div {:class (stl/css :feature)}
+ [:p {:class (stl/css :feature-content)}
+ "Version History allows you to save different stages of your design process, so you can revisit them whenever needed."]
+
+ [:p {:class (stl/css :feature-content)}
+ "Some versions are saved automatically, serving as an invaluable emergency backup. Additionally, you can manually save versions, giving you full control over the timeline associated with a file. This way, you can always restore specific versions that you've intentionally saved."]]
+
+ [:div {:class (stl/css :navigation)}
+ [:& c/navigation-bullets
+ {:slide slide
+ :navigate navigate
+ :total 3}]
+
+ [:button {:on-click next
+ :class (stl/css :next-btn)} "Continue"]]]]]]
+
+ 2
+ [:div {:class (stl/css-case :modal-overlay true)}
+ [:div.animated {:class klass}
+ [:div {:class (stl/css :modal-container)}
+ [:img {:src "images/features/2.4-format.gif"
+ :class (stl/css :start-image)
+ :border "0"
+ :alt "New export format: fast and open"}]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-header)}
+ [:h1 {:class (stl/css :modal-title)}
+ "New export format: fast and open"]]
+ [:div {:class (stl/css :feature)}
+ [:p {:class (stl/css :feature-content)}
+ "The new .penpot format will streamline the import and export of files and assets by being more efficient and interoperable."]
+ [:p {:class (stl/css :feature-content)}
+ "This format replaces the previous two—so no more choosing between them or accidentally picking the wrong one! It's better for both scenarios: if you just need to import or export files quickly, it’ll be a bit faster. And if you want to extract data (like a list of color assets), this new format is much easier to read."]]
+
+ [:div {:class (stl/css :navigation)}
+
+ [:& c/navigation-bullets
+ {:slide slide
+ :navigate navigate
+ :total 3}]
+
+ [:button {:on-click finish
+ :class (stl/css :next-btn)} "Let's go"]]]]]])))
+
diff --git a/frontend/src/app/main/ui/releases/v2_4.scss b/frontend/src/app/main/ui/releases/v2_4.scss
new file mode 100644
index 0000000000..dd1b81c82b
--- /dev/null
+++ b/frontend/src/app/main/ui/releases/v2_4.scss
@@ -0,0 +1,102 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@import "refactor/common-refactor.scss";
+
+.modal-overlay {
+ @extend .modal-overlay-base;
+}
+
+.modal-container {
+ display: grid;
+ grid-template-columns: $s-324 1fr;
+ height: $s-500;
+ width: $s-888;
+ border-radius: $br-8;
+ background-color: var(--modal-background-color);
+ border: $s-2 solid var(--modal-border-color);
+}
+
+.start-image {
+ width: $s-324;
+ border-radius: $br-8 0 0 $br-8;
+}
+
+.modal-content {
+ padding: $s-40;
+ display: grid;
+ grid-template-rows: auto 1fr $s-32;
+ gap: $s-24;
+
+ a {
+ color: var(--button-primary-background-color-rest);
+ }
+}
+
+.modal-header {
+ display: grid;
+ gap: $s-8;
+}
+
+.version-tag {
+ @include flexCenter;
+ @include headlineSmallTypography;
+ height: $s-32;
+ width: $s-96;
+ background-color: var(--communication-tag-background-color);
+ color: var(--communication-tag-foreground-color);
+ border-radius: $br-8;
+}
+
+.modal-title {
+ @include headlineLargeTypography;
+ color: var(--modal-title-foreground-color);
+}
+
+.features-block {
+ display: flex;
+ flex-direction: column;
+ gap: $s-16;
+ width: $s-440;
+}
+
+.feature {
+ display: flex;
+ flex-direction: column;
+ gap: $s-8;
+}
+
+.feature-title {
+ @include bodyLargeTypography;
+ color: var(--modal-title-foreground-color);
+}
+
+.feature-content {
+ @include bodyMediumTypography;
+ margin: 0;
+ color: var(--modal-text-foreground-color);
+}
+
+.feature-list {
+ @include bodyMediumTypography;
+ color: var(--modal-text-foreground-color);
+ list-style: disc;
+ display: grid;
+ gap: $s-8;
+}
+
+.navigation {
+ width: 100%;
+ display: grid;
+ grid-template-areas: "bullets button";
+}
+
+.next-btn {
+ @extend .button-primary;
+ width: $s-100;
+ justify-self: flex-end;
+ grid-area: button;
+}
diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs
index a43038b730..a199056976 100644
--- a/frontend/src/app/main/ui/routes.cljs
+++ b/frontend/src/app/main/ui/routes.cljs
@@ -7,34 +7,17 @@
(ns app.main.ui.routes
(:require
[app.common.data.macros :as dm]
- [app.common.spec :as us]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
- [app.main.data.users :as du]
+ [app.main.data.team :as dtm]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
- [app.util.router :as rt]
[beicon.v2.core :as rx]
- [cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
-(s/def ::page-id ::us/uuid)
-(s/def ::file-id ::us/uuid)
-(s/def ::section ::us/keyword)
-(s/def ::index ::us/integer)
-(s/def ::token (s/nilable ::us/not-empty-string))
-(s/def ::share-id ::us/uuid)
-
-(s/def ::viewer-path-params
- (s/keys :req-un [::file-id]))
-
-(s/def ::viewer-query-params
- (s/keys :opt-un [::index ::share-id ::section ::page-id]))
-
-(s/def ::any any?)
-
(def routes
[["/auth"
["/login" :auth-login]
@@ -53,11 +36,10 @@
["/access-tokens" :settings-access-tokens]]
["/frame-preview" :frame-preview]
- ["/view/:file-id"
- {:name :viewer
- :conform
- {:path-params ::viewer-path-params
- :query-params ::viewer-query-params}}]
+
+ ["/view" :viewer]
+
+ ["/view/:file-id" :viewer-legacy]
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
@@ -65,33 +47,32 @@
;; Used for export
["/render-sprite/:file-id" :render-sprite]
- ["/dashboard/team/:team-id"
- ["/members" :dashboard-team-members]
- ["/invitations" :dashboard-team-invitations]
- ["/webhooks" :dashboard-team-webhooks]
- ["/settings" :dashboard-team-settings]
- ["/projects" :dashboard-projects]
+ ["/dashboard"
+ ["/members" :dashboard-members]
+ ["/invitations" :dashboard-invitations]
+ ["/webhooks" :dashboard-webhooks]
+ ["/settings" :dashboard-settings]
+ ["/recent" :dashboard-recent]
["/search" :dashboard-search]
["/fonts" :dashboard-fonts]
["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries]
- ["/projects/:project-id" :dashboard-files]]
+ ["/files" :dashboard-files]]
- ["/workspace/:project-id/:file-id" :workspace]])
+ ["/dashboard/team/:team-id"
+ ["/members" :dashboard-legacy-team-members]
+ ["/invitations" :dashboard-legacy-team-invitations]
+ ["/webhooks" :dashboard-legacy-team-webhooks]
+ ["/settings" :dashboard-legacy-team-settings]
+ ["/projects" :dashboard-legacy-projects]
+ ["/search" :dashboard-legacy-search]
+ ["/fonts" :dashboard-legacy-fonts]
+ ["/fonts/providers" :dashboard-legacy-font-providers]
+ ["/libraries" :dashboard-legacy-libraries]
+ ["/projects/:project-id" :dashboard-legacy-files]]
-(defn- match-path
- [router path]
- (when-let [match (rt/match router path)]
- (if-let [conform (get-in match [:data :conform])]
- (let [spath (get conform :path-params ::any)
- squery (get conform :query-params ::any)]
- (try
- (-> (dissoc match :params)
- (assoc :path-params (us/conform spath (get match :path-params))
- :query-params (us/conform squery (get match :query-params))))
- (catch :default _
- nil)))
- match)))
+ ["/workspace" :workspace]
+ ["/workspace/:project-id/:file-id" :workspace-legacy]])
(defn on-navigate
[router path]
@@ -99,8 +80,9 @@
[base-path qs] (str/split path "?")
location-path (dm/str (.-origin location) (.-pathname location))
valid-location? (= location-path (dm/str cf/public-uri))
- match (match-path router path)
+ match (rt/match router path)
empty-path? (or (= base-path "") (= base-path "/"))]
+
(cond
(not valid-location?)
(st/emit! (rt/assign-exception {:type :not-found}))
@@ -119,7 +101,11 @@
(st/emit! (rt/nav :auth-login))
empty-path?
- (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)} (u/query-string->map qs)))
+ (let [team-id (or (dtm/get-last-team-id)
+ (:default-team-id profile))]
+ (st/emit! (rt/nav :dashboard-recent
+ (-> (u/query-string->map qs)
+ (assoc :team-id team-id)))))
:else
(st/emit! (rt/assign-exception {:type :not-found})))))))))
diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs
index ce6099c884..d5192320d8 100644
--- a/frontend/src/app/main/ui/settings.cljs
+++ b/frontend/src/app/main/ui/settings.cljs
@@ -9,8 +9,10 @@
(:require
[app.main.data.dashboard.shortcuts :as sc]
[app.main.refs :as refs]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
+ [app.main.ui.modal :refer [modal-container*]]
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
[app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account]
@@ -20,7 +22,6 @@
[app.main.ui.settings.profile :refer [profile-page]]
[app.main.ui.settings.sidebar :refer [sidebar]]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
[rumext.v2 :as mf]))
(mf/defc header
@@ -41,25 +42,29 @@
(when (nil? profile)
(st/emit! (rt/nav :auth-login))))
- [:section {:class (stl/css :dashboard-layout-refactor :dashboard)}
- [:& sidebar {:profile profile
- :section section}]
+ [:*
+ [:> modal-container*]
+ [:section {:class (stl/css :dashboard-layout-refactor :dashboard)}
- [:div {:class (stl/css :dashboard-content)}
- [:& header]
- [:section {:class (stl/css :dashboard-container)}
- (case section
- :settings-profile
- [:& profile-page]
- :settings-feedback
- [:& feedback-page]
+ [:& sidebar {:profile profile
+ :section section}]
- :settings-password
- [:& password-page]
+ [:div {:class (stl/css :dashboard-content)}
+ [:& header]
+ [:section {:class (stl/css :dashboard-container)}
+ (case section
+ :settings-profile
+ [:& profile-page]
- :settings-options
- [:& options-page]
+ :settings-feedback
+ [:& feedback-page]
- :settings-access-tokens
- [:& access-tokens-page])]]]))
+ :settings-password
+ [:& password-page]
+
+ :settings-options
+ [:& options-page]
+
+ :settings-access-tokens
+ [:& access-tokens-page])]]]]))
diff --git a/frontend/src/app/main/ui/settings/access_tokens.cljs b/frontend/src/app/main/ui/settings/access_tokens.cljs
index deae25f542..b555e47e58 100644
--- a/frontend/src/app/main/ui/settings/access_tokens.cljs
+++ b/frontend/src/app/main/ui/settings/access_tokens.cljs
@@ -10,7 +10,7 @@
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.forms :as fm]
diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs
index da5e13779a..5535c08c32 100644
--- a/frontend/src/app/main/ui/settings/change_email.cljs
+++ b/frontend/src/app/main/ui/settings/change_email.cljs
@@ -10,7 +10,7 @@
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
@@ -41,7 +41,7 @@
(defn- on-success
[profile data]
(if (:changed data)
- (st/emit! (du/fetch-profile)
+ (st/emit! (du/refresh-profile)
(modal/hide))
(let [message (tr "notifications.validation-email-sent" (:email profile))]
(st/emit! (ntf/info message)
diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs
index 4836465bbf..3e87531fd7 100644
--- a/frontend/src/app/main/ui/settings/delete_account.cljs
+++ b/frontend/src/app/main/ui/settings/delete_account.cljs
@@ -9,7 +9,7 @@
(:require
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs
index e1d49acfd4..f3ab76e288 100644
--- a/frontend/src/app/main/ui/settings/options.cljs
+++ b/frontend/src/app/main/ui/settings/options.cljs
@@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
@@ -22,9 +22,8 @@
[:theme {:optional true} [:string {:max 250}]]])
(defn- on-success
- [profile]
- (st/emit! (ntf/success (tr "notifications.profile-saved"))
- (du/profile-fetched profile)))
+ [_]
+ (st/emit! (ntf/success (tr "notifications.profile-saved"))))
(defn- on-submit
[form _event]
diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs
index ac93d65991..5de6d7b657 100644
--- a/frontend/src/app/main/ui/settings/password.cljs
+++ b/frontend/src/app/main/ui/settings/password.cljs
@@ -9,7 +9,7 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as udu]
+ [app.main.data.profile :as udu]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.dom :as dom]
diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs
index e591162031..763ee3c836 100644
--- a/frontend/src/app/main/ui/settings/profile.cljs
+++ b/frontend/src/app/main/ui/settings/profile.cljs
@@ -11,7 +11,7 @@
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
- [app.main.data.users :as du]
+ [app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
@@ -25,12 +25,15 @@
[:fullname [::sm/text {:max 250}]]
[:email ::sm/email]])
+(defn- on-success
+ [_]
+ (st/emit! (ntf/success (tr "notifications.profile-saved"))))
+
(defn- on-submit
[form _event]
(let [data (:clean-data @form)]
(st/emit! (du/update-profile data)
- (du/persist-profile)
- (ntf/success (tr "notifications.profile-saved")))))
+ (du/persist-profile {:on-success on-success}))))
;; --- Profile Form
diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs
index 651129a363..8da8bb6a3c 100644
--- a/frontend/src/app/main/ui/settings/sidebar.cljs
+++ b/frontend/src/app/main/ui/settings/sidebar.cljs
@@ -8,15 +8,16 @@
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cf]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.data.modal :as modal]
- [app.main.data.users :as du]
+ [app.main.data.team :as dtm]
+ [app.main.router :as rt]
[app.main.store :as st]
- [app.main.ui.dashboard.sidebar :refer [profile-section]]
+ [app.main.ui.dashboard.sidebar :refer [profile-section*]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
- [app.util.router :as rt]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -26,6 +27,7 @@
(def ^:private feedback-icon
(i/icon-xref :feedback (stl/css :feedback-icon)))
+;; FIXME: move to common
(def ^:private go-settings-profile
#(st/emit! (rt/nav :settings-profile)))
@@ -58,12 +60,13 @@
options? (= section :settings-options)
feedback? (= section :settings-feedback)
access-tokens? (= section :settings-access-tokens)
- team-id (du/get-current-team-id profile)
+ team-id (or (dtm/get-last-team-id)
+ (:default-team-id profile))
go-dashboard
(mf/use-fn
(mf/deps team-id)
- #(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))]
+ #(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))]
[:div {:class (stl/css :sidebar-content)}
[:div {:class (stl/css :sidebar-content-section)}
@@ -119,5 +122,5 @@
[:div {:class (stl/css :dashboard-sidebar :settings)}
[:& sidebar-content {:profile profile
:section section}]
- [:& profile-section {:profile profile}]])
+ [:> profile-section* {:profile profile}]])
diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs
index 3d2ae90750..1ff0f8e6bf 100644
--- a/frontend/src/app/main/ui/shapes/export.cljs
+++ b/frontend/src/app/main/ui/shapes/export.cljs
@@ -110,10 +110,6 @@
(add! :r3)
(add! :r4)))
- (cond-> (and image? (some? (:rx shape)))
- (-> (add! :rx)
- (add! :ry)))
-
(cond-> path?
(-> (add! :stroke-cap-start)
(add! :stroke-cap-end)))
diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs
index 3befcda740..090091d675 100644
--- a/frontend/src/app/main/ui/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/shapes/frame.cljs
@@ -60,7 +60,6 @@
(mf/defc frame-container
{::mf/wrap-props false}
[props]
-
(let [shape (unchecked-get props "shape")
children (unchecked-get props "children")
@@ -135,7 +134,7 @@
bounds (mf/with-memo [bounds points]
(or bounds (gsb/get-frame-bounds shape)))
- thumb (:thumbnail shape)
+ thumb (cf/resolve-media (:thumbnail-id shape))
debug? (dbg/enabled? :thumbnails)
safari? (cf/check-browser? :safari)
@@ -171,7 +170,7 @@
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")]
- (when ^boolean (:thumbnail shape)
+ (when ^boolean (:thumbnail-id shape)
[:> frame-container props
[:> frame-thumbnail-image props]])))
diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs
index a65daafc64..67c3a03562 100644
--- a/frontend/src/app/main/ui/shapes/text/styles.cljs
+++ b/frontend/src/app/main/ui/shapes/text/styles.cljs
@@ -49,11 +49,16 @@
(defn generate-paragraph-styles
[_shape data]
- (let [line-height (:line-height data 1.2)
+ (let [line-height (:line-height data)
+ line-height
+ (if (and (some? line-height) (not= "" line-height))
+ line-height
+ (:line-height txt/default-text-attrs))
+
text-align (:text-align data "start")
base #js {;; Fix a problem when exporting HTML
:fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px")
- :lineHeight (:line-height data (:line-height txt/default-text-attrs))
+ :lineHeight line-height
:margin 0}]
(cond-> base
diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs
index 50a3141e81..ccf8926222 100644
--- a/frontend/src/app/main/ui/static.cljs
+++ b/frontend/src/app/main/ui/static.cljs
@@ -11,22 +11,22 @@
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
- [app.main.data.common :as dc]
- [app.main.data.events :as ev]
+ [app.main.data.common :as dcm]
+ [app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.repo :as rp]
+ [app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-methods]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
[app.main.ui.auth.register :as register]
- [app.main.ui.dashboard.sidebar :refer [sidebar]]
+ [app.main.ui.dashboard.sidebar :refer [sidebar*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.header :as viewer.header]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
- [app.util.router :as rt]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@@ -213,7 +213,8 @@
(mf/use-fn
(mf/deps profile)
(fn []
- (st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))))
+ (let [team-id (:default-team-id profile)]
+ (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
on-success
(mf/use-fn
@@ -233,7 +234,7 @@
{:team-id team-id})
mdata {:on-success on-success
:on-error on-error}]
- (st/emit! (dc/create-team-access-request
+ (st/emit! (dcm/create-team-access-request
(with-meta params mdata))))))]
[:*
@@ -267,7 +268,7 @@
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
- [:& sidebar
+ [:> sidebar*
{:team nil
:projects []
:project (:default-project-id profile)
diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss
index 0a509f0164..aff796cee3 100644
--- a/frontend/src/app/main/ui/static.scss
+++ b/frontend/src/app/main/ui/static.scss
@@ -318,7 +318,7 @@
hr {
margin: $s-20 0;
- border-top: solid 1px var(--modal-separator-backogrund-color);
+ border-top: solid $s-1 var(--modal-separator-backogrund-color);
}
.separator {
diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs
index 0410bd1df4..6e2562b774 100644
--- a/frontend/src/app/main/ui/viewer.cljs
+++ b/frontend/src/app/main/ui/viewer.cljs
@@ -25,7 +25,8 @@
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
- [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]]
+ [app.main.ui.modal :refer [modal-container*]]
+ [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar*]]
[app.main.ui.viewer.header :as header]
[app.main.ui.viewer.inspect :as inspect]
[app.main.ui.viewer.interactions :as interactions]
@@ -129,8 +130,8 @@
:comment-sidebar show-sidebar?}]
(when show-sidebar?
- [:& comments-sidebar
- {:users users
+ [:> comments-sidebar*
+ {:profiles users
:frame frame
:page page}])]))
@@ -274,9 +275,9 @@
:page page
:zoom zoom}])]])
-(mf/defc viewer-content
- {::mf/wrap-props false}
- [{:keys [data page-id share-id section index interactions-mode share] :as props}]
+(mf/defc viewer-content*
+ {::mf/props :obj}
+ [{:keys [data page-id share-id section index interactions-mode share]}]
(let [{:keys [file users project permissions]} data
allowed (or
(= section :interactions)
@@ -620,8 +621,8 @@
;; --- Component: Viewer
-(mf/defc viewer
- {::mf/wrap-props false}
+(mf/defc viewer*
+ {::mf/props :obj}
[{:keys [file-id share-id page-id] :as props}]
(mf/with-effect [file-id page-id share-id]
(let [params {:file-id file-id
@@ -630,9 +631,12 @@
(st/emit! (dv/initialize params))
(fn []
(st/emit! (dv/finalize params)))))
+
(if-let [data (mf/deref refs/viewer-data)]
(let [props (obj/merge props #js {:data data :key (dm/str file-id)})]
- [:> viewer-content props])
+ [:*
+ [:> modal-container*]
+ [:> viewer-content* props]])
[:> loader* {:title (tr "labels.loading")
:overlay true}]))
diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs
index a84f44b7d5..2ab261bb30 100644
--- a/frontend/src/app/main/ui/viewer/comments.cljs
+++ b/frontend/src/app/main/ui/viewer/comments.cljs
@@ -14,7 +14,7 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.main.data.comments :as dcm]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.comments :as cmt]
@@ -220,7 +220,7 @@
{:thread thread
:position-modifier modifier1
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
- :users users
+ :profiles users
:zoom zoom}])
(when-let [draft (:draft local)]
@@ -231,10 +231,11 @@
:on-submit on-draft-submit
:zoom zoom}])]]]))
-(mf/defc comments-sidebar
- [{:keys [users frame page]}]
+(mf/defc comments-sidebar*
+ {::mf/props :obj}
+ [{:keys [profiles frame page]}]
(let [profile (mf/deref refs/profile)
- local (mf/deref refs/comments-local)
+ local (mf/deref refs/comments-local)
threads-map (mf/deref refs/comment-threads)
threads (->> (vals threads-map)
(dcm/apply-filters local profile)
@@ -242,4 +243,8 @@
(gsh/has-point? frame position))))]
[:aside {:class (stl/css :comments-sidebar)}
[:div {:class (stl/css :settings-bar-inside)}
- [:& wc/comments-sidebar {:from-viewer true :users users :threads threads :page-id (:id page)}]]]))
+ [:> wc/comments-sidebar*
+ {:from-viewer true
+ :profiles profiles
+ :threads threads
+ :page-id (:id page)}]]]))
diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs
index 6db8a69b91..86eb9eb049 100644
--- a/frontend/src/app/main/ui/viewer/header.cljs
+++ b/frontend/src/app/main/ui/viewer/header.cljs
@@ -18,7 +18,7 @@
[app.main.ui.formats :as fmt]
[app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-menu]]
- [app.main.ui.viewer.interactions :refer [flows-menu* interactions-menu]]
+ [app.main.ui.viewer.interactions :refer [flows-menu* interactions-menu*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
@@ -173,7 +173,8 @@
:interactions [:*
(when index
[:> flows-menu* {:page page :index index}])
- [:& interactions-menu {:interactions-mode interactions-mode}]]
+ [:> interactions-menu*
+ {:interactions-mode interactions-mode}]]
:comments [:& comments-menu]
[:div {:class (stl/css :view-options)}])
diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs
index 99b32b9ee3..e7d48424a2 100644
--- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs
+++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs
@@ -39,7 +39,7 @@
mf/deref)
colors-library-ws (-> (mf/use-memo
(mf/deps (:file-id color))
- #(make-colors-library-ref :workspace-libraries (:file-id color)))
+ #(make-colors-library-ref :libraries (:file-id color)))
mf/deref)]
(or colors-library-v colors-library-ws)))
diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs
index 66cf55eb39..03c9de511d 100644
--- a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs
+++ b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs
@@ -59,7 +59,7 @@
file-typographies-viewer (mf/deref file-typographies-ref)
file-typographies-workspace (mf/deref refs/workspace-file-typography)
- file-library-workspace (get (mf/deref refs/workspace-libraries) (:typography-ref-file style))
+ file-library-workspace (get (mf/deref refs/libraries) (:typography-ref-file style))
typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)])
color-format (mf/use-state :hex)
diff --git a/frontend/src/app/main/ui/viewer/inspect/code.cljs b/frontend/src/app/main/ui/viewer/inspect/code.cljs
index abfc29b90b..ee15b210af 100644
--- a/frontend/src/app/main/ui/viewer/inspect/code.cljs
+++ b/frontend/src/app/main/ui/viewer/inspect/code.cljs
@@ -13,7 +13,7 @@
[app.common.geom.shapes :as gsh]
[app.common.types.shape-tree :as ctst]
[app.config :as cfg]
- [app.main.data.events :as ev]
+ [app.main.data.event :as ev]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -30,6 +30,7 @@
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
+ [okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -49,13 +50,26 @@