From a455fc015ba05e2ae37412c9501617e3c388913c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 09:22:38 +0200 Subject: [PATCH 01/22] :bug: Fix several issues related to pointer-map and components-v2 --- backend/src/app/rpc/commands/files.clj | 137 +++++++++++------- backend/src/app/rpc/commands/viewer.clj | 3 +- common/src/app/common/pages/migrations.cljc | 14 +- .../src/app/common/types/components_list.cljc | 30 ++-- common/src/app/common/types/container.cljc | 12 +- common/src/app/common/types/file.cljc | 3 +- frontend/src/app/main/data/workspace.cljs | 9 +- 7 files changed, 120 insertions(+), 88 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index b6a47c1f5e..7e5a0ca85d 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -201,15 +201,15 @@ ::db/check-deleted? false})] (blob/decode (:content row)))) -(defn load-all-pointers! - [data] +(defn- load-all-pointers! + [{:keys [data] :as file}] (doseq [[_id page] (:pages-index data)] (when (pmap/pointer-map? page) (pmap/load! page))) (doseq [[_id component] (:components data)] (when (pmap/pointer-map? component) (pmap/load! component))) - data) + file) (defn persist-pointers! [conn file-id] @@ -247,22 +247,44 @@ ;; QUERY COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn handle-file-features - [{:keys [features] :as file} client-features] +(defn handle-file-features! + [conn {:keys [id features data] :as file} client-features] + (when (and (contains? features "components/v2") (not (contains? client-features "components/v2"))) (ex/raise :type :restriction :code :feature-mismatch :feature "components/v2" :hint "file has 'components/v2' feature enabled but frontend didn't specifies it")) - (cond-> file - (and (contains? client-features "components/v2") - (not (contains? features "components/v2"))) - (update :data ctf/migrate-to-components-v2) - (and (contains? features "storage/pointer-map") - (not (contains? client-features "storage/pointer-map"))) - (process-pointers deref))) + ;; NOTE: this operation is needed because the components migration + ;; generates a new page with random id which is returned to the + ;; client; without persisting the migration this can cause that two + ;; simultaneous clients can have a different view of the file data + ;; and end persisting two pages with main components and breaking + ;; the whole file + (let [file (if (and (contains? client-features "components/v2") + (not (contains? features "components/v2"))) + (binding [pmap/*tracked* (atom {})] + (let [data (ctf/migrate-to-components-v2 data) + features (conj features "components/v2") + modified-at (dt/now)] + (db/update! conn :file + {:data (blob/encode data) + :modified-at modified-at + :features features} + {:id id}) + (persist-pointers! conn id) + (-> file + (assoc :modified-at modified-at) + (assoc :features features) + (assoc :data data)))) + file)] + + (cond-> file + (and (contains? features "storage/pointer-map") + (not (contains? client-features "storage/pointer-map"))) + (process-pointers deref)))) ;; --- COMMAND QUERY: get-file (by id) @@ -306,10 +328,18 @@ ;; here we check if client requested features are supported (check-features-compatibility! client-features) (binding [pmap/*load-fn* (partial load-pointer conn id)] - (-> (db/get-by-id conn :file id) - (decode-row) - (pmg/migrate-file) - (handle-file-features client-features)))) + (let [file (-> (db/get-by-id conn :file id) + (decode-row) + (pmg/migrate-file)) + + file (handle-file-features! conn file client-features)] + + ;; NOTE: if migrations are applied, probably new pointers generated so + ;; instead of persiting them on each get-file, we just resolve them until + ;; user updates the file and permanently persists the new pointers + (cond-> file + (pmg/migrated? file) + (process-pointers deref))))) (defn get-minimal-file [{:keys [::db/pool] :as cfg} id] @@ -543,7 +573,7 @@ ;; --- COMMAND QUERY: get-file-libraries -(def ^:private sql:file-libraries +(def ^:private sql:get-file-libraries "WITH RECURSIVE libs AS ( SELECT fl.*, flr.synced_at FROM file AS fl @@ -556,7 +586,6 @@ JOIN libs AS l ON (flr.file_id = l.id) ) SELECT l.id, - l.data, l.features, l.project_id, l.created_at, @@ -569,33 +598,24 @@ WHERE l.deleted_at IS NULL OR l.deleted_at > now();") (defn get-file-libraries - [conn file-id client-features] - (check-features-compatibility! client-features) - (->> (db/exec! conn [sql:file-libraries file-id]) - (map decode-row) - (map #(assoc % :is-indirect false)) - (map (fn [{:keys [id] :as row}] - (binding [pmap/*load-fn* (partial load-pointer conn id)] - (-> row - ;; TODO: re-enable this dissoc and replace call - ;; with other that gets files individually - ;; See task https://tree.taiga.io/project/penpot/task/4904 - ;; (update :data dissoc :pages-index) - (handle-file-features client-features))))) - (vec))) + [conn file-id] + (into [] + (comp + (map #(assoc % :is-indirect false)) + (map decode-row)) + (db/exec! conn [sql:get-file-libraries file-id]))) (s/def ::get-file-libraries (s/keys :req [::rpc/profile-id] - :req-un [::file-id] - :opt-un [::features])) + :req-un [::file-id])) (sv/defmethod ::get-file-libraries "Get libraries used by the specified file." {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) - (get-file-libraries conn file-id features))) + (get-file-libraries conn file-id))) ;; --- COMMAND QUERY: Files that use this File library @@ -719,31 +739,38 @@ {:is-shared is-shared} {:id id})) +(def sql:get-referenced-files + "SELECT f.id + FROM file_library_rel AS flr + INNER JOIN file AS f ON (f.id = flr.file_id) + WHERE flr.library_file_id = ? + ORDER BY f.created_at ASC;") + (defn absorb-library "Find all files using a shared library, and absorb all library assets into the file local libraries" [conn {:keys [id] :as params}] (let [library (db/get-by-id conn :file id)] (when (:is-shared library) - (let [ldata (-> library decode-row pmg/migrate-file :data)] - (binding [pmap/*load-fn* (partial load-pointer conn id)] - (load-all-pointers! ldata)) - - (->> (db/query conn :file-library-rel {:library-file-id id}) - (map :file-id) - (keep #(db/get-by-id conn :file % ::db/check-deleted? false)) - (map decode-row) - (map pmg/migrate-file) - (run! (fn [{:keys [id data revn] :as file}] - (binding [pmap/*tracked* (atom {}) - pmap/*load-fn* (partial load-pointer conn id)] - (let [data (ctf/absorb-assets data ldata)] - (db/update! conn :file - {:revn (inc revn) - :data (blob/encode data) - :modified-at (dt/now)} - {:id id})) - (persist-pointers! conn id))))))))) + (let [ldata (binding [pmap/*load-fn* (partial load-pointer conn id)] + (-> library decode-row load-all-pointers! pmg/migrate-file :data)) + rows (db/exec! conn [sql:get-referenced-files id])] + (doseq [file-id (map :id rows)] + (binding [pmap/*load-fn* (partial load-pointer conn file-id) + pmap/*tracked* (atom {})] + (let [file (-> (db/get-by-id conn :file file-id + ::db/check-deleted? false + ::db/remove-deleted? false) + (decode-row) + (load-all-pointers!) + (pmg/migrate-file)) + data (ctf/absorb-assets (:data file) ldata)] + (db/update! conn :file + {:revn (inc (:revn file)) + :data (blob/encode data) + :modified-at (dt/now)} + {:id file-id}) + (persist-pointers! conn file-id)))))))) (s/def ::set-file-shared (s/keys :req [::rpc/profile-id] diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index 495b5a9623..878637adf5 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -27,9 +27,8 @@ [conn file-id profile-id features] (let [file (files/get-file conn file-id features) project (get-project conn (:project-id file)) - libs (files/get-file-libraries conn file-id features) + libs (files/get-file-libraries conn file-id) users (comments/get-file-comments-users conn file-id profile-id) - links (->> (db/query conn :share-link {:file-id file-id}) (mapv (fn [row] (-> row diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index a8c05724ac..5104237102 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -37,10 +37,16 @@ (reduce migrate-fn data (range (:version data 0) to-version)))))) (defn migrate-file - [file] - (-> file - (update :data assoc :id (:id file)) - (update :data migrate-data))) + [{:keys [id data] :as file}] + (let [data (assoc data :id id)] + (-> file + (assoc ::orig-version (:version data)) + (assoc :data (migrate-data data))))) + +(defn migrated? + [{:keys [data] :as file}] + (> (:version data) + (::orig-version file))) ;; Default handler, noop (defmethod migrate :default [data] data) diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 70af302305..858a506ef8 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -30,25 +30,19 @@ (assoc component :modified-at (dt/now))) (defn add-component - [file-data {:keys [id name path main-instance-id main-instance-page shapes]}] - (let [components-v2 (dm/get-in file-data [:options :components-v2]) - wrap-object-fn feat/*wrap-with-objects-map-fn*] - (cond-> file-data - :always - (assoc-in [:components id] - (touch {:id id - :name name - :path path})) - - (not components-v2) - (assoc-in [:components id :objects] - (->> shapes - (d/index-by :id) - (wrap-object-fn))) - components-v2 - (update-in [:components id] assoc + [fdata {:keys [id name path main-instance-id main-instance-page shapes]}] + (let [components-v2 (dm/get-in fdata [:options :components-v2]) + fdata (update fdata :components assoc id (touch {:id id :name name :path path}))] + (if components-v2 + (update-in fdata [:components id] assoc :main-instance-id main-instance-id - :main-instance-page main-instance-page)))) + :main-instance-page main-instance-page) + + (let [wrap-object-fn feat/*wrap-with-objects-map-fn*] + (assoc-in fdata [:components id :objects] + (->> shapes + (d/index-by :id) + (wrap-object-fn))))))) (defn mod-component [file-data {:keys [id name path objects annotation]}] diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 7d66711e3d..56b67a500b 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -173,7 +173,8 @@ (make-component-instance container component library-data position components-v2 {})) ([container component library-data position components-v2 - {:keys [main-instance? force-id] :or {main-instance? false force-id nil}}] + {:keys [main-instance? force-id force-frame-id] + :or {main-instance? false force-id nil force-frame-id nil}}] (let [component-page (when components-v2 (ctpl/get-page library-data (:main-instance-page component))) component-shape (if components-v2 @@ -188,10 +189,11 @@ objects (:objects container) unames (volatile! (common/retrieve-used-names objects)) - frame-id (ctst/frame-id-by-position objects - (gpt/add orig-pos delta) - {:skip-components? true}) ; It'd be weird to make an instance - frame-ids-map (volatile! {}) ; inside other component + frame-id (or force-frame-id + (ctst/frame-id-by-position objects + (gpt/add orig-pos delta) + {:skip-components? true})) + frame-ids-map (volatile! {}) update-new-shape (fn [new-shape original-shape] diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index c72d628e06..ffffa3d794 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -345,7 +345,8 @@ file-data position false - {:main-instance? true}) + {:main-instance? true + :force-frame-id uuid/zero}) add-shapes (fn [page] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 3eb63b18cc..700f6962cf 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -225,14 +225,17 @@ (resolve-pointers id) (rx/map workspace-data-pointers-loaded)) - (->> (rp/cmd! :get-file-libraries {:file-id id :features features}) + (->> (rp/cmd! :get-file-libraries {:file-id id}) (rx/mapcat identity) - (rx/mapcat + (rx/merge-map + (fn [{:keys [id]}] + (rp/cmd! :get-file {:id id :features features}))) + (rx/merge-map (fn [{:keys [id data] :as file}] (->> (filter (comp t/pointer? val) data) (resolve-pointers id) (rx/map #(update file :data merge %))))) - (rx/mapcat + (rx/merge-map (fn [{:keys [id data] :as file}] ;; Resolve all pages of each library, if needed (->> (rx/from (seq (:pages-index data))) From a1819e78e49c4d205ac53a9d530679364101f5e9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 09:23:03 +0200 Subject: [PATCH 02/22] :arrow_up: Update rumext dependency --- frontend/deps.edn | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/deps.edn b/frontend/deps.edn index c158bea0ea..e304106200 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -13,8 +13,8 @@ funcool/tubax {:mvn/version "2021.05.20-0"} funcool/rumext - {:git/tag "v2.1" - :git/sha "6343102" + {:git/tag "v2.3" + :git/sha "09942e7" :git/url "https://github.com/funcool/rumext.git" } From d1e74b0da947cfb0f5dc0443c8e1eabb9c70bed4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 09:23:39 +0200 Subject: [PATCH 03/22] :sparkles: Increase default stacktrace size on cljs --- frontend/src/app/main.cljs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 9c1454323f..08ef4fdc1f 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -98,3 +98,6 @@ (fn [_ _ old-value current-value] (when (not= old-value current-value) (reinit)))) + +(set! (.-stackTraceLimit js/Error) 50) + From ef273012389d0c7327876bbb16a3da3e07cece30 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 09:20:32 +0200 Subject: [PATCH 04/22] :sparkles: Add arity-1 to d/nilv that returns a transducer --- common/src/app/common/data.cljc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index f7316a274d..2a3320f6b1 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -580,8 +580,10 @@ (defn nilv "Returns a default value if the given value is nil" - [v default] - (if (some? v) v default)) + ([default] + (map #(nilv % default))) + ([v default] + (if (some? v) v default))) (defn num? "Checks if a value `val` is a number but not an Infinite or NaN" From fcc4f4eed8ee7cd501dd030d2d1acbc6eda79d7c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 09:35:59 +0200 Subject: [PATCH 05/22] :zap: Refactor state management of workspace assets sidebar --- frontend/src/app/main/data/common.cljs | 1 - frontend/src/app/main/data/dashboard.cljs | 28 - frontend/src/app/main/data/workspace.cljs | 67 +- .../app/main/data/workspace/libraries.cljs | 24 +- .../src/app/main/data/workspace/texts.cljs | 111 +- frontend/src/app/main/refs.cljs | 20 +- .../src/app/main/ui/dashboard/file_menu.cljs | 16 +- frontend/src/app/main/ui/delete_shared.cljs | 139 +- frontend/src/app/main/ui/workspace.cljs | 8 +- .../src/app/main/ui/workspace/header.cljs | 9 +- .../src/app/main/ui/workspace/libraries.cljs | 349 +-- .../app/main/ui/workspace/sidebar/assets.cljs | 2099 +++++++++-------- .../workspace/sidebar/collapsable_button.cljs | 11 +- .../workspace/sidebar/options/menus/text.cljs | 9 +- .../sidebar/options/menus/typography.cljs | 140 +- .../main/ui/workspace/viewport/actions.cljs | 2 +- 16 files changed, 1665 insertions(+), 1368 deletions(-) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 2b10703a48..684f2747c0 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -43,4 +43,3 @@ (watch [_ _ _] (->> (rp/cmd! :delete-share-link {:id id}) (rx/ignore))))) - diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 0ad8ba02e7..fdf4224cfe 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -237,34 +237,6 @@ (->> (rp/cmd! :get-team-shared-files {:team-id team-id}) (rx/map shared-files-fetched)))))) -;; --- EVENT: Get files that use this shared-file - -(defn clean-temp-shared - [] - (ptk/reify ::clean-temp-shared - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:dashboard-local :files-with-shared] nil)))) - -(defn library-using-files-fetched - [files] - (ptk/reify ::library-using-files-fetched - ptk/UpdateEvent - (update [_ state] - (let [files (d/index-by :id files)] - (assoc-in state [:dashboard-local :files-with-shared] files))))) - -(defn fetch-libraries-using-files - [files] - (ptk/reify ::fetch-library-using-files - ptk/WatchEvent - (watch [_ _ _] - (->> (rx/from files) - (rx/map :id) - (rx/mapcat #(rp/cmd! :get-library-file-references {:file-id %})) - (rx/reduce into []) - (rx/map library-using-files-fetched))))) - ;; --- EVENT: recent-files (defn recent-files-fetched diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 700f6962cf..166fb86de6 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -181,13 +181,14 @@ (rx/of (dwn/initialize team-id id) (dwsl/initialize)) - ;; Load team fonts. We must ensure custom fonts are fully loadad before starting the workspace load + ;; Load team fonts. We must ensure custom fonts are fully loadad + ;; before starting the workspace load. (rx/merge + (rx/of (df/load-team-fonts team-id)) (->> stream (rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded)) (rx/take 1) - (rx/ignore)) - (rx/of (df/load-team-fonts team-id))) + (rx/ignore))) (rx/merge ;; Load all pages, independently if they are pointers or already @@ -218,8 +219,6 @@ (rx/merge-map (fn [_] (rx/merge - (rx/of (workspace-initialized)) - (->> data (filter (comp t/pointer? val)) (resolve-pointers id) @@ -249,8 +248,9 @@ (fn [pages-index] (assoc-in file [:data :pages-index] pages-index)))))) (rx/reduce conj []) - (rx/map libraries-fetched)))))))) + (rx/map libraries-fetched))))))) + (rx/of (workspace-initialized))) (rx/take-until stoper))))))) (defn- libraries-fetched @@ -1135,6 +1135,19 @@ 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))))))) + (defn check-in-asset [items element] (let [items (or items #{})] @@ -1147,31 +1160,31 @@ (ptk/reify ::toggle-selected-assets ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-global :selected-assets type] #(check-in-asset % asset))))) + (update-in state [:workspace-assets-selected type] #(check-in-asset % asset))))) (defn select-single-asset [asset type] (ptk/reify ::select-single-asset ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-global :selected-assets type] #{asset})))) + (assoc-in state [:workspace-assets-selected type] #{asset})))) (defn select-assets [assets type] (ptk/reify ::select-assets ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-global :selected-assets type] (into #{} assets))))) + (assoc-in state [:workspace-assets-selected type] (into #{} assets))))) (defn unselect-all-assets [] (ptk/reify ::unselect-all-assets ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-global :selected-assets] {:components #{} - :graphics #{} - :colors #{} - :typographies #{}})))) + (assoc state :workspace-assets-selected {:components #{} + :graphics #{} + :colors #{} + :typographies #{}})))) (defn go-to-main-instance [page-id shape-id] (dm/assert! (uuid? page-id)) @@ -1216,8 +1229,8 @@ pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id :layout :assets}] (rx/of (rt/nav :workspace pparams qparams) - (dwl/set-assets-box-open file-id :library true) - (dwl/set-assets-box-open file-id :components true) + (dwl/set-assets-section-open file-id :library true) + (dwl/set-assets-section-open file-id :components true) (select-single-asset component-id :components)))))) ptk/EffectEvent @@ -1238,8 +1251,8 @@ pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id :layout :assets}] (rx/of (rt/nav :workspace pparams qparams) - (dwl/set-assets-box-open file-id :library true) - (dwl/set-assets-box-open file-id :components true) + (dwl/set-assets-section-open file-id :library true) + (dwl/set-assets-section-open file-id :components true) (select-single-asset component-id :components)))) ptk/EffectEvent @@ -2091,26 +2104,6 @@ (dissoc :page-item))] (assoc state :workspace-local local))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; File Library persistent settings -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - - -(defn set-file-library-listing-thumbs - [listing-thumbs?] - (ptk/reify ::set-file-library-listing-thumbs - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :file-library-listing-thumbs] listing-thumbs?)))) - -(defn set-file-library-reverse-sort - [reverse-sort?] - (ptk/reify ::set-file-library-reverse-sort - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :file-library-reverse-sort] reverse-sort?)))) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Components annotations ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index ebbb49a73b..468b3a874b 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -76,19 +76,19 @@ (declare sync-file) -(defn set-assets-box-open - [file-id box open?] - (ptk/reify ::set-assets-box-open +(defn set-assets-section-open + [file-id section open?] + (ptk/reify ::set-assets-section-open ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-global :assets-files-open file-id box] open?)))) + (assoc-in state [:workspace-assets-open-status file-id section] open?)))) (defn set-assets-group-open - [file-id box path open?] + [file-id section path open?] (ptk/reify ::set-assets-group-open ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-global :assets-files-open file-id :groups box path] open?)))) + (assoc-in state [:workspace-assets-open-status file-id :groups section path] open?)))) (defn extract-path-if-missing [item] @@ -951,12 +951,22 @@ (defn link-file-to-library [file-id library-id] (ptk/reify ::attach-library + ;; NOTE: this event implements UpdateEvent protocol for perform an + ;; optimistic update state for make the UI feel more responsive. + ptk/UpdateEvent + (update [_ state] + (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)) + state))) + ptk/WatchEvent (watch [_ state _] (let [features (cond-> ffeat/enabled (features/active-feature? state :components-v2) (conj "components/v2"))] - (rx/concat + (rx/merge (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) (rx/ignore)) (->> (rp/cmd! :get-file {:id library-id :features features}) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 6af8929acb..2328386975 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -15,17 +15,22 @@ [app.common.pages.helpers :as cph] [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.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] + [app.main.fonts :as fonts] [app.util.router :as rt] [app.util.text-editor :as ted] [app.util.timers :as ts] [beicon.core :as rx] + [cuerdas.core :as str] [potok.core :as ptk])) ;; -- Attrs @@ -321,6 +326,7 @@ (cph/group-shape? shape) (cph/get-children-ids objects id))] (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs)))))))) + (defn migrate-node [node] (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] @@ -595,22 +601,97 @@ (rx/empty)))))) (defn update-attrs -[id attrs] + [id attrs] (ptk/reify ::update-attrs - ptk/WatchEvent - (watch [_ _ _] + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (let [attrs (select-keys attrs root-attrs)] + (if-not (empty? attrs) + (rx/of (update-root-attrs {:id id :attrs attrs})) + (rx/empty))) + + (let [attrs (select-keys attrs paragraph-attrs)] + (if-not (empty? attrs) + (rx/of (update-paragraph-attrs {:id id :attrs attrs})) + (rx/empty))) + + (let [attrs (select-keys attrs text-attrs)] + (if-not (empty? attrs) + (rx/of (update-text-attrs {:id id :attrs attrs})) + (rx/empty))))))) + + +(defn apply-typography + "A higher level event that has the resposability of to apply the + specified typography to the selected shapes." + [typography file-id] + (ptk/reify ::apply-typography + ptk/WatchEvent + (watch [_ state _] + (let [editor-state (:workspace-editor-state state) + selected (wsh/lookup-selected state) + attrs (-> typography + (assoc :typography-ref-file file-id) + (assoc :typography-ref-id (:id typography)) + (dissoc :id :name))] + + (->> (rx/from (seq selected)) + (rx/map (fn [id] + (let [editor (get editor-state id)] + (update-text-attrs {:id id :editor editor :attrs attrs}))))))))) + +(defn generate-typography-name + [{:keys [font-id font-variant-id] :as typography}] + (let [{:keys [name]} (fonts/get-font-data font-id)] + (assoc typography :name (str name " " (str/title font-variant-id))))) + +(defn add-typography + "A higher level version of dwl/add-typography, and has mainly two + responsabilities: add the typography to the library and apply it to + the currently selected text shapes (being aware of the open text + editors." + [file-id] + (ptk/reify ::add-typography + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state) + objects (wsh/lookup-page-objects state) + + xform (comp (keep (d/getf objects)) + (filter cph/text-shape?)) + shapes (into [] xform selected) + shape (first shapes) + + values (current-text-values + {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) + :shape shape + :attrs text-attrs}) + + multiple? (or (> 1 (count shapes)) + (d/seek (partial = :multiple) + (vals values))) + + values (-> (d/without-nils values) + (select-keys + (d/concat-vec text-font-attrs + text-spacing-attrs + text-transform-attrs))) + + typ-id (uuid/next) + typ (-> (if multiple? + txt/default-typography + (merge txt/default-typography values)) + (generate-typography-name) + (assoc :id typ-id))] + (rx/concat - (let [attrs (select-keys attrs root-attrs)] - (if-not (empty? attrs) - (rx/of (update-root-attrs {:id id :attrs attrs})) - (rx/empty))) + (rx/of (dwl/add-typography typ) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "typography"})) - (let [attrs (select-keys attrs paragraph-attrs)] - (if-not (empty? attrs) - (rx/of (update-paragraph-attrs {:id id :attrs attrs})) - (rx/empty))) + (when (not multiple?) + (rx/of (update-attrs (:id shape) + {:typography-ref-id typ-id + :typography-ref-file file-id})))))))) - (let [attrs (select-keys attrs text-attrs)] - (if-not (empty? attrs) - (rx/of (update-text-attrs {:id id :attrs attrs})) - (rx/empty))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index feb0d601a0..cb4c4f5037 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -187,18 +187,9 @@ (def editing-page-item (l/derived :page-item workspace-local)) -(def file-library-listing-thumbs? - (l/derived :file-library-listing-thumbs workspace-global)) - -(def file-library-reverse-sort? - (l/derived :file-library-reverse-sort workspace-global)) - (def current-hover-ids (l/derived :hover-ids context-menu)) -(def selected-assets - (l/derived :selected-assets workspace-global)) - (def workspace-layout (l/derived :workspace-layout st/state)) @@ -212,10 +203,9 @@ data (:workspace-data state)] (-> file (dissoc :data) - (assoc :options (:options data) - :components (:components data) - :pages (:pages data) - :pages-index (:pages-index data))))) + ;; FIXME: still used in sitemaps but sitemaps + ;; should declare its own lense for it + (assoc :pages (:pages data))))) st/state =)) (def workspace-data @@ -395,9 +385,7 @@ (def selected-objects (letfn [(selector [{:keys [selected objects]}] - (->> selected - (map #(get objects %)) - (filterv (comp not nil?))))] + (into [] (keep (d/getf objects)) selected))] (l/derived selector selected-data =))) (def selected-shapes-with-children diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 6911004692..cea4baf067 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -98,12 +98,12 @@ (let [num-shared (filter #(:is-shared %) files)] (if (< 0 (count num-shared)) - (do (st/emit! (dd/fetch-libraries-using-files files)) - (st/emit! (modal/show - {:type :delete-shared - :origin :delete - :on-accept delete-fn - :count-libraries (count num-shared)}))) + (st/emit! (modal/show + {:type :delete-shared-libraries + :origin :delete + :ids (into #{} (map :id) files) + :on-accept delete-fn + :count-libraries (count num-shared)})) (if multi? (st/emit! (modal/show @@ -161,10 +161,10 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (dd/fetch-libraries-using-files files)) (st/emit! (modal/show - {:type :delete-shared + {:type :delete-shared-libraries :origin :unpublish + :ids (into #{} (map :id) files) :on-accept del-shared :count-libraries file-count}))) diff --git a/frontend/src/app/main/ui/delete_shared.cljs b/frontend/src/app/main/ui/delete_shared.cljs index 5aac382069..7d47a89548 100644 --- a/frontend/src/app/main/ui/delete_shared.cljs +++ b/frontend/src/app/main/ui/delete_shared.cljs @@ -6,88 +6,101 @@ (ns app.main.ui.delete-shared (:require - [app.main.data.dashboard :as dd] + [app.common.data.macros :as dm] [app.main.data.modal :as modal] - [app.main.refs :as refs] + [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as k] + [beicon.core :as rx] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) + +(def ^:private noop (constantly nil)) (mf/defc delete-shared-dialog {::mf/register modal/components - ::mf/register-as :delete-shared} - [{:keys [on-accept - on-cancel - accept-style - origin - count-libraries] :as props}] - (let [on-accept (or on-accept identity) - on-cancel (or on-cancel identity) - cancel-label (tr "labels.cancel") - accept-style (or accept-style :danger) - dashboard-local (mf/deref refs/dashboard-local) - files->shared (:files-with-shared dashboard-local) + ::mf/register-as :delete-shared-libraries + ::mf/wrap-props false} + [{:keys [ids on-accept on-cancel accept-style origin count-libraries]}] + (let [references* (mf/use-state {}) + references (deref references*) - is-delete? (= origin :delete) - count-files (count (keys files->shared)) + on-accept (or on-accept noop) + on-cancel (or on-cancel noop) - title (if is-delete? - (tr "modals.delete-shared-confirm.title" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.title" (i18n/c count-libraries))) - message (if is-delete? - (tr "modals.delete-shared-confirm.message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.message" (i18n/c count-libraries))) - accept-label (if is-delete? - (tr "modals.delete-shared-confirm.accept" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.accept" (i18n/c count-libraries))) - no-files-message (if is-delete? - (tr "modals.delete-shared-confirm.no-files-message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.no-files-message" (i18n/c count-libraries))) - scd-message (if is-delete? - (if (= count-files 1) - (tr "modals.delete-shared-confirm.scd-message" (i18n/c count-libraries)) - (tr "modals.delete-shared-confirm.scd-message-many" (i18n/c count-libraries))) - (if (= count-files 1) - (tr "modals.unpublish-shared-confirm.scd-message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.scd-message-many" (i18n/c count-libraries)))) - hint (if is-delete? - (if (= count-files 1) - (tr "modals.delete-shared-confirm.hint" (i18n/c count-libraries)) + cancel-label (tr "labels.cancel") + accept-style (or accept-style :danger) + + is-delete? (= origin :delete) + count-files (count (keys references)) + + title (if ^boolean is-delete? + (tr "modals.delete-shared-confirm.title" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.title" (i18n/c count-libraries))) + + subtitle (if ^boolean is-delete? + (tr "modals.delete-shared-confirm.message" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.message" (i18n/c count-libraries))) + + accept-label (if ^boolean is-delete? + (tr "modals.delete-shared-confirm.accept" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.accept" (i18n/c count-libraries))) + + no-files-msg (if ^boolean is-delete? + (tr "modals.delete-shared-confirm.no-files-message" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.no-files-message" (i18n/c count-libraries))) + + scd-msg (if ^boolean is-delete? + (if (= count-files 1) + (tr "modals.delete-shared-confirm.scd-message" (i18n/c count-libraries)) + (tr "modals.delete-shared-confirm.scd-message-many" (i18n/c count-libraries))) + (if (= count-files 1) + (tr "modals.unpublish-shared-confirm.scd-message" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.scd-message-many" (i18n/c count-libraries)))) + hint (if ^boolean is-delete? + (if (= count-files 1) + (tr "modals.delete-shared-confirm.hint" (i18n/c count-libraries)) (tr "modals.delete-shared-confirm.hint-many" (i18n/c count-libraries))) - (if (= count-files 1) - (tr "modals.unpublish-shared-confirm.hint" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.hint-many" (i18n/c count-libraries)))) + (if (= count-files 1) + (tr "modals.unpublish-shared-confirm.hint" (i18n/c count-libraries)) + (tr "modals.unpublish-shared-confirm.hint-many" (i18n/c count-libraries)))) accept-fn - (mf/use-callback + (mf/use-fn + (mf/deps on-accept) (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) - (on-accept props))) + (on-accept))) cancel-fn - (mf/use-callback + (mf/use-fn + (mf/deps on-cancel) (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) - (on-cancel props)))] + (on-cancel)))] - (mf/with-effect + (mf/with-effect [ids] + (->> (rx/from ids) + (rx/map #(array-map :file-id %)) + (rx/mapcat #(rp/cmd! :get-library-file-references %)) + (rx/mapcat identity) + (rx/map (juxt :id :name)) + (rx/reduce conj []) + (rx/subs #(reset! references* %)))) + + (mf/with-effect [accept-fn] (letfn [(on-keydown [event] (when (k/enter? event) (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (modal/hide)) - (on-accept props)))] - (let [key (events/listen js/document EventType.KEYDOWN on-keydown)] - (fn [] - (events/unlistenByKey key) - (st/emit! (dd/clean-temp-shared)))))) + (accept-fn)))] + (let [key (events/listen js/document "keydown" on-keydown)] + (partial events/unlistenByKey key)))) [:div.modal-overlay [:div.modal-container.confirm-dialog @@ -98,23 +111,23 @@ {:on-click cancel-fn} i/close]] [:div.modal-content.delete-shared - (when (and (string? message) (not= message "")) - [:h3 message]) + (when (and (string? subtitle) (not= subtitle "")) + [:h3 subtitle]) (when (not= 0 count-libraries) - (if (> (count files->shared) 0) + (if (pos? (count references)) [:* [:div - (when (and (string? scd-message) (not= scd-message "")) - [:h3 scd-message]) + (when (and (string? scd-msg) (not= scd-msg "")) + [:h3 scd-msg]) [:ul.file-list - (for [[id file] files->shared] + (for [[file-id file-name] references] [:li.modal-item-element - {:key id} - [:span "- " (:name file)]])]] + {:key (dm/str file-id)} + [:span "- " file-name]])]] (when (and (string? hint) (not= hint "")) [:h3 hint])] [:* - [:h3 no-files-message]]))] + [:h3 no-files-msg]]))] [:div.modal-footer [:div.action-buttons diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 88d741dcc8..07bb322f5f 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -36,7 +36,6 @@ [app.util.dom :as dom] [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] [app.util.router :as rt] [debug :refer [debug?]] [goog.events :as events] @@ -47,14 +46,11 @@ (mf/defc workspace-content {::mf/wrap-props false} - [props] + [{:keys [file layout page-id wglobal]}] (let [selected (mf/deref refs/selected-shapes) - file (obj/get props "file") - layout (obj/get props "layout") - page-id (obj/get props "page-id") {:keys [vport] :as wlocal} (mf/deref refs/workspace-local) - {:keys [options-mode] :as wglobal} (obj/get props "wglobal") + {:keys [options-mode]} wglobal colorpalette? (:colorpalette layout) textpalette? (:textpalette layout) diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 34f14cad56..8d6113c443 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -9,7 +9,6 @@ [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.config :as cf] - [app.main.data.dashboard :as dd] [app.main.data.events :as ev] [app.main.data.exports :as de] [app.main.data.modal :as modal] @@ -108,6 +107,7 @@ ;; --- Header Users +;; FIXME: refactor & optimizations (mf/defc menu [{:keys [layout project file team-id] :as props}] (let [show-menu? (mf/use-state false) @@ -122,9 +122,6 @@ add-shared-fn #(st/emit! (dwl/set-file-shared (:id file) true)) - - - on-add-shared (mf/use-fn (mf/deps file) @@ -144,10 +141,10 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (dd/fetch-libraries-using-files [file])) (st/emit! (modal/show - {:type :delete-shared + {:type :delete-shared-libraries :origin :unpublish + :ids #{(:id file)} :on-accept #(st/emit! (dwl/set-file-shared (:id file) false)) :count-libraries 1})))) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index fc18741a36..ad25ce8311 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -7,13 +7,13 @@ (ns app.main.ui.workspace.libraries (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.types.components-list :as ctkl] - [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] [app.main.data.workspace.libraries :as dwl] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets :as a] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -22,243 +22,302 @@ [okulary.core :as l] [rumext.v2 :as mf])) -(def workspace-file +(def ref:workspace-file (l/derived :workspace-file st/state)) -(defn library-str +(defn create-file-library-ref + [library-id] + (letfn [(getter-fn [state] + (let [fdata (let [{:keys [id] :as wfile} (:workspace-data state)] + (if (= id library-id) + wfile + (dm/get-in state [:workspace-libraries library-id :data])))] + {:colors (-> fdata :colors vals) + :media (-> fdata :media vals) + :components (ctkl/components-seq fdata) + :typographies (-> fdata :typographies vals)}))] + (l/derived getter-fn st/state =))) + +(defn- describe-library [components-count graphics-count colors-count typography-count] (str (str/join " ยท " (cond-> [] - (< 0 components-count) + (pos? components-count) (conj (tr "workspace.libraries.components" components-count)) - (< 0 graphics-count) + (pos? graphics-count) (conj (tr "workspace.libraries.graphics" graphics-count)) - (< 0 colors-count) + (pos? colors-count) (conj (tr "workspace.libraries.colors" colors-count)) - (< 0 typography-count) + (pos? typography-count) (conj (tr "workspace.libraries.typography" typography-count)))) "\u00A0")) -(defn local-library-str +(defn- describe-linked-library [library] (let [components-count (count (or (ctkl/components-seq (:data library)) [])) - graphics-count (count (get-in library [:data :media] [])) - colors-count (count (get-in library [:data :colors] [])) - typography-count (count (get-in library [:data :typographies] []))] - (library-str components-count graphics-count colors-count typography-count))) + graphics-count (count (dm/get-in library [:data :media] [])) + colors-count (count (dm/get-in library [:data :colors] [])) + typography-count (count (dm/get-in library [:data :typographies] []))] + (describe-library components-count graphics-count colors-count typography-count))) -(defn external-library-str +(defn- describe-external-library [library] - (let [components-count (get-in library [:library-summary :components :count] 0) - graphics-count (get-in library [:library-summary :media :count] 0) - colors-count (get-in library [:library-summary :colors :count] 0) - typography-count (get-in library [:library-summary :typographies :count] 0)] - (library-str components-count graphics-count colors-count typography-count))) + (let [components-count (dm/get-in library [:library-summary :components :count] 0) + graphics-count (dm/get-in library [:library-summary :media :count] 0) + colors-count (dm/get-in library [:library-summary :colors :count] 0) + typography-count (dm/get-in library [:library-summary :typographies :count] 0)] + (describe-library components-count graphics-count colors-count typography-count))) (mf/defc libraries-tab - [{:keys [file colors typographies media components libraries shared-files] :as props}] - (let [search-term (mf/use-state "") + {::mf/wrap-props false} + [{:keys [file-id shared? linked-libraries shared-libraries]}] + (let [search-term* (mf/use-state "") + search-term (deref search-term*) - sorted-libraries (->> (vals libraries) - (sort-by #(str/lower (:name %)))) + library-ref (mf/with-memo [file-id] + (create-file-library-ref file-id)) + library (deref library-ref) + colors (:colors library) + components (:components library) + media (:media library) + typographies (:typographies library) - filtered-files (->> shared-files - (filter #(not= (:id %) (:id file))) - (filter #(nil? (get libraries (:id %)))) - (filter #(matches-search (:name %) @search-term)) - (sort-by #(str/lower (:name %)))) + shared-libraries + (mf/with-memo [shared-libraries linked-libraries file-id search-term] + (->> shared-libraries + (remove #(= (:id %) file-id)) + (remove #(contains? linked-libraries (:id %))) + (filter #(matches-search (:name %) search-term)) + (sort-by (comp str/lower :name)))) - on-search-term-change - (mf/use-callback + linked-libraries + (mf/with-memo [linked-libraries] + (->> (vals linked-libraries) + (sort-by (comp str/lower :name)))) + + change-search-term + (mf/use-fn (fn [event] (let [value (-> (dom/get-target event) (dom/get-value))] - (reset! search-term value)))) + (reset! search-term* value)))) - on-search-clear - (mf/use-callback - (fn [_] - (reset! search-term ""))) + clear-search-term + (mf/use-fn #(reset! search-term* "")) link-library - (mf/use-callback (mf/deps file) #(st/emit! (dwl/link-file-to-library (:id file) %))) + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-target event) + (dom/get-data "library-id") + (parse-uuid))] + (st/emit! (dwl/link-file-to-library file-id library-id))))) unlink-library - (mf/use-callback - (mf/deps file) - (fn [library-id] - (st/emit! (dwl/unlink-file-from-library (:id file) library-id) - (dwl/sync-file (:id file) library-id)))) - add-shared - (mf/use-callback - (mf/deps file) - #(st/emit! (dwl/set-file-shared (:id file) true))) - - del-shared - (mf/use-callback - (mf/deps file) - (fn [_] - (st/emit! (dd/fetch-libraries-using-files [file])) - (st/emit! (modal/show - {:type :delete-shared - :origin :unpublish - :on-accept (fn [] - (st/emit! (dwl/set-file-shared (:id file) false)) - (modal/show! :libraries-dialog {})) - :on-cancel #(modal/show! :libraries-dialog {}) - :count-libraries 1})))) - handle-key-down - (mf/use-callback + (mf/use-fn + (mf/deps file-id) (fn [event] - (let [enter? (kbd/enter? event) - esc? (kbd/esc? event) - input-node (dom/event->target event)] + (let [library-id (some-> (dom/get-target event) + (dom/get-data "library-id") + (parse-uuid))] + (st/emit! (dwl/unlink-file-from-library file-id library-id) + (dwl/sync-file file-id library-id))))) - (when enter? + on-delete-accept + (mf/use-fn + (mf/deps file-id) + #(st/emit! (dwl/set-file-shared file-id false) + (modal/show :libraries-dialog {}))) + + on-delete-cancel + (mf/use-fn #(st/emit! (modal/show :libraries-dialog {}))) + + publish + (mf/use-fn + (mf/deps file-id) + #(st/emit! (dwl/set-file-shared file-id true))) + + unpublish + (mf/use-fn + (mf/deps file-id) + (fn [_] + (st/emit! (modal/show + {:type :delete-shared-libraries + :ids #{file-id} + :origin :unpublish + :on-accept on-delete-accept + :on-cancel on-delete-cancel + :count-libraries 1})))) + + handle-key-down + (mf/use-fn + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event) + input-node (dom/event->target event)] + (when ^boolean enter? (dom/blur! input-node)) - (when esc? + (when ^boolean esc? (dom/blur! input-node)))))] + [:* [:div.section [:div.section-title (tr "workspace.libraries.in-this-file")] [:div.section-list + [:div.section-list-item - [:div + [:div {:data-kaka "1"} [:div.item-name (tr "workspace.libraries.file-library")] - [:div.item-contents (library-str (count components) (count media) (count colors) (count typographies) )]] + [:div.item-contents (describe-library + (count components) + (count media) + (count colors) + (count typographies))]] [:div - (if (:is-shared file) + (if ^boolean shared? [:input.item-button {:type "button" :value (tr "common.unpublish") - :on-click del-shared}] + :on-click unpublish}] [:input.item-button {:type "button" :value (tr "common.publish") - :on-click add-shared}])]] + :on-click publish}])]] - (for [library sorted-libraries] - [:div.section-list-item {:key (:id library)} - [:div.item-name (:name library)] - [:div.item-contents (local-library-str library)] + (for [{:keys [id name] :as library} linked-libraries] + [:div.section-list-item {:key (dm/str id)} + [:div.item-name name] + [:div.item-contents (describe-linked-library library)] [:input.item-button {:type "button" :value (tr "labels.remove") - :on-click #(unlink-library (:id library))}]]) - ]] + :data-library-id (dm/str id) + :on-click unlink-library}]])]] + [:div.section [:div.section-title (tr "workspace.libraries.shared-libraries")] [:div.libraries-search [:input.search-input {:placeholder (tr "workspace.libraries.search-shared-libraries") :type "text" - :value @search-term - :on-change on-search-term-change + :value search-term + :on-change change-search-term :on-key-down handle-key-down}] - (if (str/empty? @search-term) + (if (str/empty? search-term) [:div.search-icon i/search] [:div.search-icon.search-close - {:on-click on-search-clear} + {:on-click clear-search-term} i/close])] - (if (> (count filtered-files) 0) + + (if (seq shared-libraries) [:div.section-list - (for [file filtered-files] - [:div.section-list-item {:key (:id file)} - [:div.item-name (:name file)] - [:div.item-contents (external-library-str file)] + (for [{:keys [id name] :as library} shared-libraries] + [:div.section-list-item {:key (dm/str id)} + [:div.item-name name] + [:div.item-contents (describe-external-library library)] [:input.item-button {:type "button" :value (tr "workspace.libraries.add") - :on-click #(link-library (:id file))}]])] + :data-library-id (dm/str id) + :on-click link-library}]])] [:div.section-list-empty - (if (nil? shared-files) + (if (nil? shared-libraries) i/loader-pencil [:* i/library - (if (str/empty? @search-term) + (if (str/empty? search-term) (tr "workspace.libraries.no-shared-libraries-available") - (tr "workspace.libraries.no-matches-for" @search-term))])])]])) + (tr "workspace.libraries.no-matches-for" search-term))])])]])) (mf/defc updates-tab - [{:keys [file libraries] :as props}] - (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) - (vals libraries)) - update-library #(st/emit! (dwl/sync-file (:id file) %))] - [:div.section - (if (empty? libraries-need-sync) - [:div.section-list-empty - i/library - (tr "workspace.libraries.no-libraries-need-sync")] - [:* - [:div.section-title (tr "workspace.libraries.library")] - [:div.section-list - (for [library libraries-need-sync] - [:div.section-list-item {:key (:id library)} - [:div.item-name (:name library)] - [:div.item-contents (external-library-str library)] - [:input.item-button {:type "button" - :value (tr "workspace.libraries.update") - :on-click #(update-library (:id library))}]])]])])) + {::mf/wrap-props false} + [{:keys [file-id libraries]}] + (let [libraries (mf/with-memo [libraries] + (filter #(> (:modified-at %) (:synced-at %)) (vals libraries))) + + update (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-target event) + (dom/get-data "library-id") + (parse-uuid))] + (st/emit! (dwl/sync-file file-id library-id)))))] + [:div.section + (if (empty? libraries) + [:div.section-list-empty + i/library + (tr "workspace.libraries.no-libraries-need-sync")] + [:* + [:div.section-title (tr "workspace.libraries.library")] + [:div.section-list + (for [{:keys [id name] :as library} libraries] + [:div.section-list-item {:key (dm/str id)} + [:div.item-name name] + [:div.item-contents (describe-external-library library)] + [:input.item-button {:type "button" + :value (tr "workspace.libraries.update") + :data-library-id (dm/str id) + :on-click update}]])]])])) (mf/defc libraries-dialog {::mf/register modal/components ::mf/register-as :libraries-dialog} - [{:keys [] :as ctx}] - (let [selected-tab (mf/use-state :libraries) - project (mf/deref refs/workspace-project) - file (mf/deref workspace-file) + [] + (let [project (mf/deref refs/workspace-project) + file (mf/deref ref:workspace-file) - libraries (->> (mf/deref refs/workspace-libraries) - (d/removem (fn [[_ val]] (:is-indirect val)))) - shared-files (mf/deref refs/workspace-shared-files) + team-id (:team-id project) + file-id (:id file) + shared? (:is-shared file) - colors-ref (mf/use-memo (mf/deps (:id file)) #(a/file-colors-ref (:id file))) - colors (mf/deref colors-ref) + selected-tab* (mf/use-state :libraries) + selected-tab (deref selected-tab*) - typography-ref (mf/use-memo (mf/deps (:id file)) #(a/file-typography-ref (:id file))) - typographies (mf/deref typography-ref) + libraries (mf/deref refs/workspace-libraries) + libraries (mf/with-memo [libraries] + (d/removem (fn [[_ val]] (:is-indirect val)) libraries)) - media-ref (mf/use-memo (mf/deps (:id file)) #(a/file-media-ref (:id file))) - media (mf/deref media-ref) + ;; NOTE: we really don't need react on shared files + shared-libraries + (mf/deref refs/workspace-shared-files) - components-ref (mf/use-memo (mf/deps (:id file)) #(a/file-components-ref (:id file))) - components (mf/deref components-ref) + select-libraries-tab + (mf/use-fn #(reset! selected-tab* :libraries)) - change-tab #(reset! selected-tab %) - close #(modal/hide!)] + select-updates-tab + (mf/use-fn #(reset! selected-tab* :updates)) - (mf/use-effect - (mf/deps project) - (fn [] - (when (:team-id project) - (st/emit! (dwl/fetch-shared-files {:team-id (:team-id project)}))))) + close-dialog + (mf/use-fn #(modal/hide!))] + + (mf/with-effect [team-id] + (when team-id + (st/emit! (dwl/fetch-shared-files {:team-id team-id})))) [:div.modal-overlay [:div.modal.libraries-dialog - [:a.close {:on-click close} i/close] + [:a.close {:on-click close-dialog} i/close] [:div.modal-content [:div.libraries-header [:div.header-item - {:class (dom/classnames :active (= @selected-tab :libraries)) - :on-click #(change-tab :libraries)} + {:class (dom/classnames :active (= selected-tab :libraries)) + :on-click select-libraries-tab} (tr "workspace.libraries.libraries")] [:div.header-item - {:class (dom/classnames :active (= @selected-tab :updates)) - :on-click #(change-tab :updates)} + {:class (dom/classnames :active (= selected-tab :updates)) + :on-click select-updates-tab} (tr "workspace.libraries.updates")]] [:div.libraries-content - (case @selected-tab + (case selected-tab :libraries - [:& libraries-tab {:file file - :colors colors - :typographies typographies - :media media - :components components - :libraries libraries - :shared-files shared-files}] + [:& libraries-tab {:file-id file-id + :shared? shared? + :linked-libraries libraries + :shared-libraries shared-libraries}] :updates - [:& updates-tab {:file file + [:& updates-tab {:file-id file-id :libraries libraries}])]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 7502d59be1..0626dd0ac0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -11,10 +11,7 @@ [app.common.media :as cm] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.text :as txt] - [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.events :as ev] [app.main.data.modal :as modal] @@ -22,7 +19,6 @@ [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.media :as dwm] - [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] @@ -36,13 +32,14 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.menus.text :refer [generate-typography-name]] + [app.main.ui.workspace.libraries :refer [create-file-library-ref]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] [app.util.color :as uc] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] + [app.util.perf :as perf] [app.util.router :as rt] [app.util.strings :refer [matches-search]] [app.util.timers :as ts] @@ -52,18 +49,22 @@ [potok.core :as ptk] [rumext.v2 :as mf])) -;; NOTE: TODO: for avoid too many arguments, I think we can use react -;; context variables for pass to the down tree all the common -;; variables that are defined on the MAIN container/box component. +(def ctx:filters (mf/create-context nil)) +(def ctx:toggle-ordering (mf/create-context nil)) +(def ctx:toggle-list-style (mf/create-context nil)) -;; TODO: change update operations to admit multiple ids, thus avoiding the need of -;; emitting many events and opening an undo transaction. Also move the logic -;; of grouping, deleting, etc. to events in the data module, since now the -;; selection info is in the global state. +(def ref:selected-assets + (l/derived :workspace-assets-selected st/state =)) -(def typography-data - (l/derived #(dm/select-keys % [:rename-typography :edit-typography]) - refs/workspace-global =)) +(def ref:open-status + (l/derived :workspace-assets-open-status st/state)) + +(def ref:typography-section-state + (l/derived (fn [gstate] + {:rename-typography (:rename-typography gstate) + :edit-typography (:edit-typography gstate)}) + refs/workspace-global + =)) ;; ---- Group assets management ---- @@ -77,20 +78,20 @@ 'group2': {'subgroup21': {'': [{asset21A}}}} " [assets reverse-sort?] - (letfn [(sort-key [key1 key2] - (if reverse-sort? - (compare (d/name key2) (d/name key1)) - (compare (d/name key1) (d/name key2))))] - (when-not (empty? assets) - (reduce (fn [groups asset] - (let [path-vector (cph/split-path (or (:path asset) ""))] - (update-in groups (conj path-vector "") - (fn [group] - (if-not group - [asset] - (conj group asset)))))) - (sorted-map-by sort-key) - assets)))) + (when-not (empty? assets) + (reduce (fn [groups {:keys [path] :as asset}] + (let [path (cph/split-path (or path ""))] + (update-in groups + (conj path "") + (fn [group] + (if group + (conj group asset) + [asset]))))) + (sorted-map-by (fn [key1 key2] + (if reverse-sort? + (compare key2 key1) + (compare key1 key2)))) + assets))) (defn add-group [asset group-name] @@ -137,7 +138,7 @@ create? (empty? path) - on-close #(modal/hide!) + on-close (mf/use-fn #(modal/hide!)) on-accept (mf/use-fn @@ -186,38 +187,38 @@ (defn- create-assets-group [rename components-to-group group-name] (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> components-to-group - (map #(rename - (:id %) - (add-group % group-name))))) - (st/emit! (dwu/commit-undo-transaction undo-id)))) + (st/emit! (dwu/start-undo-transaction undo-id)) + (apply st/emit! + (->> components-to-group + (map #(rename + (:id %) + (add-group % group-name))))) + (st/emit! (dwu/commit-undo-transaction undo-id)))) (defn- on-drop-asset - [event asset dragging? selected-assets selected-assets-full selected-assets-paths rename] + [event asset dragging* selected-assets selected-assets-full selected-assets-paths rename] (let [create-typed-assets-group (partial create-assets-group rename)] (when (not (dnd/from-child? event)) - (reset! dragging? false) + (reset! dragging* false) (when - (and (not (contains? selected-assets (:id asset))) - (every? #(= % (:path asset)) selected-assets-paths)) + (and (not (contains? selected-assets (:id asset))) + (every? #(= % (:path asset)) selected-assets-paths)) (let [components-to-group (conj selected-assets-full asset) create-typed-assets-group (partial create-typed-assets-group components-to-group)] (modal/show! :name-group-dialog {:accept create-typed-assets-group})))))) (defn- on-drag-enter-asset - [event asset dragging? selected-assets selected-assets-paths] + [event asset dragging* selected-assets selected-assets-paths] (when (and (not (dnd/from-child? event)) (every? #(= % (:path asset)) selected-assets-paths) (not (contains? selected-assets (:id asset)))) - (reset! dragging? true))) + (reset! dragging* true))) (defn- on-drag-leave-asset - [event dragging?] + [event dragging*] (when (not (dnd/from-child? event)) - (reset! dragging? false))) + (reset! dragging* false))) (defn- create-counter-element [asset-count] @@ -232,10 +233,10 @@ item-el (mf/ref-val item-ref) counter-el (create-counter-element num-selected)] - ;; set-drag-image requires that the element is rendered and - ;; visible to the user at the moment of creating the ghost - ;; image (to make a snapshot), but you may remove it right - ;; afterwards, in the next render cycle. + ;; set-drag-image requires that the element is rendered and + ;; visible to the user at the moment of creating the ghost + ;; image (to make a snapshot), but you may remove it right + ;; afterwards, in the next render cycle. (dom/append-child! item-el counter-el) (dnd/set-drag-image! event item-el (:x offset) (:y offset)) (ts/raf #(.removeChild ^js item-el counter-el)))) @@ -254,17 +255,17 @@ (set-drag-image event item-ref num-selected)))) (defn- on-drag-enter-asset-group - [event dragging? prefix selected-assets-paths] + [event dragging* prefix selected-assets-paths] (dom/stop-propagation event) (when (and (not (dnd/from-child? event)) (not (every? #(= % prefix) selected-assets-paths))) - (reset! dragging? true))) + (reset! dragging* true))) (defn- on-drop-asset-group - [event dragging? prefix selected-assets-paths selected-assets-full rename] + [event dragging* prefix selected-assets-paths selected-assets-full rename] (dom/stop-propagation event) (when (not (dnd/from-child? event)) - (reset! dragging? false) + (reset! dragging* false) (when (not (every? #(= % prefix) selected-assets-paths)) (doseq [target-asset selected-assets-full] (st/emit! @@ -274,27 +275,24 @@ ;; ---- Common blocks ---- -(def auto-pos-menu-state - {:open? false - :top nil - :left nil}) +(def ^:private initial-context-menu-state + {:open? false :top nil :left nil}) -(defn- open-auto-pos-menu - [state event] - (let [pos (dom/get-client-position event) - top (:y pos) +(defn- open-context-menu + [state pos] + (let [top (:y pos) left (+ (:x pos) 10)] - (dom/prevent-default event) (assoc state :open? true :top top :left left))) -(defn- close-auto-pos-menu +(defn- close-context-menu [state] (assoc state :open? false)) -(mf/defc auto-pos-menu +(mf/defc assets-context-menu + {::mf/wrap-props false} [{:keys [options state on-close]}] [:& context-menu {:selectable false @@ -305,7 +303,7 @@ :options options}]) (mf/defc asset-section - [{:keys [children file-id title box assets-count open?]}] + [{:keys [children file-id title section assets-count open?]}] (let [children (->> (if (array? children) children [children]) (filter some?)) get-role #(.. % -props -role) @@ -313,7 +311,7 @@ content (filter #(= (get-role %) :content) children)] [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} - [:span {:on-click #(st/emit! (dwl/set-assets-box-open file-id box (not open?)))} + [:span {:on-click #(st/emit! (dwl/set-assets-section-open file-id section (not open?)))} i/arrow-slide title] [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space title-buttons] @@ -325,29 +323,29 @@ [:* children]) (mf/defc asset-group-title - [{:keys [file-id box path group-open? on-rename on-ungroup]}] + [{:keys [file-id section path group-open? on-rename on-ungroup]}] (when-not (empty? path) (let [[other-path last-path truncated] (cph/compact-path path 35) - menu-state (mf/use-state auto-pos-menu-state) + menu-state (mf/use-state initial-context-menu-state) on-fold-group (mf/use-fn - (mf/deps file-id box path group-open?) + (mf/deps file-id section path group-open?) (fn [event] (dom/stop-propagation event) (st/emit! (dwl/set-assets-group-open file-id - box + section path (not group-open?))))) on-context-menu (mf/use-fn (fn [event] - (swap! menu-state #(open-auto-pos-menu % event)))) + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (swap! menu-state open-context-menu pos)))) on-close-menu - (mf/use-fn - (fn [] - (swap! menu-state close-auto-pos-menu)))] + (mf/use-fn #(swap! menu-state close-context-menu))] [:div.group-title {:class (when-not group-open? "closed") :on-click on-fold-group @@ -358,30 +356,54 @@ other-path "\u00A0/\u00A0"]) [:span {:title (when truncated path)} last-path] - [:& auto-pos-menu + [:& assets-context-menu {:on-close on-close-menu :state @menu-state :options [[(tr "workspace.assets.rename") #(on-rename % path last-path)] [(tr "workspace.assets.ungroup") #(on-ungroup path)]]}]]))) -;;---- Components box ---- +;;---- Components section ---- + + +(defn- get-component-root-and-container + [file-id component components-v2] + (if (= file-id (:id @refs/workspace-file)) + (let [data @refs/workspace-data] + [(ctf/get-component-root data component) + (if components-v2 + (ctf/get-component-page data component) + component)]) + (let [data (dm/get-in @refs/workspace-libraries [file-id :data])] + [(ctf/get-component-root data component) + (if components-v2 + (ctf/get-component-page data component) + component)]))) (mf/defc components-item - [{:keys [component renaming listing-thumbs? selected-components file - on-asset-click on-context-menu on-drag-start do-rename cancel-rename - selected-components-full selected-components-paths]}] - (let [item-ref (mf/use-ref) - dragging? (mf/use-state false) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + {::mf/wrap-props false} + [{:keys [component renaming listing-thumbs? selected + file-id on-asset-click on-context-menu on-drag-start do-rename + cancel-rename selected-full selected-paths]}] - components-v2 (mf/use-ctx ctx/components-v2) + ;; (prn "components-item" (:name component)) + (let [item-ref (mf/use-ref) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + components-v2 (mf/use-ctx ctx/components-v2) + component-id (:id component) + + ;; _ (app.common.pprint/pprint component) + + ;; NOTE: we don't use reactive deref for it because we don't + ;; really need rerender on any change on the file change. If + ;; the component changes, it will trigger rerender anyway. + [root-shape container] + (get-component-root-and-container file-id component components-v2) - file (or (:data file) file) - root-shape (ctf/get-component-root file component) - component-container (if components-v2 - (ctf/get-component-page file component) - component) unselect-all (mf/use-fn (fn [] @@ -389,71 +411,74 @@ on-component-click (mf/use-fn - (mf/deps component selected-components) + (mf/deps component selected) (fn [event] (dom/stop-propagation event) - (on-asset-click event (:id component) unselect-all))) + (on-asset-click component-id unselect-all event))) on-component-double-click (mf/use-fn - (mf/deps component selected-components) - (fn [event] - (dom/stop-propagation event) - (let [main-instance-id (:main-instance-id component) - main-instance-page (:main-instance-page component)] - (when (and main-instance-id main-instance-page) ;; Only when :components-v2 is enabled - (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) + (mf/deps component selected) + (fn [event] + (dom/stop-propagation event) + (let [main-instance-id (:main-instance-id component) + main-instance-page (:main-instance-page component)] + (when (and main-instance-id main-instance-page) ;; Only when :components-v2 is enabled + (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) on-drop (mf/use-fn - (mf/deps component dragging? selected-components selected-components-full selected-components-paths) + (mf/deps component dragging* selected selected-full selected-paths) (fn [event] - (on-drop-asset event component dragging? selected-components selected-components-full - selected-components-paths dwl/rename-component))) - - on-drag-over - (mf/use-fn #(dom/prevent-default %)) + (on-drop-asset event component dragging* selected selected-full + selected-paths dwl/rename-component))) on-drag-enter (mf/use-fn - (mf/deps component dragging? selected-components selected-components-paths) + (mf/deps component dragging* selected selected-paths) (fn [event] - (on-drag-enter-asset event component dragging? selected-components selected-components-paths))) + (on-drag-enter-asset event component dragging* selected selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) + (on-drag-leave-asset event dragging*))) on-component-drag-start (mf/use-fn - (mf/deps component selected-components item-ref on-drag-start workspace-read-only?) + (mf/deps component selected item-ref on-drag-start read-only?) (fn [event] - (if workspace-read-only? + (if read-only? (dom/prevent-default event) - (on-asset-drag-start event component selected-components item-ref :components on-drag-start))))] + (on-asset-drag-start event component selected item-ref :components on-drag-start)))) + + on-context-menu + (mf/use-fn + (mf/deps component-id) + (partial on-context-menu component-id))] [:div {:ref item-ref :class (dom/classnames - :selected (contains? selected-components (:id component)) + :selected (contains? selected (:id component)) :grid-cell listing-thumbs? :enum-item (not listing-thumbs?)) - :id (str "component-shape-id-" (:id component)) - :draggable (not workspace-read-only?) + :id (dm/str "component-shape-id-" (:id component)) + :draggable (not read-only?) :on-click on-component-click :on-double-click on-component-double-click - :on-context-menu (on-context-menu (:id component)) + :on-context-menu on-context-menu :on-drag-start on-component-drag-start :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - (when (and (some? root-shape) (some? component-container)) + (when (and (some? root-shape) + (some? container)) [:* [:& component-svg {:root-shape root-shape - :objects (:objects component-container)}] + :objects (:objects container)}] (let [renaming? (= renaming (:id component))] [:* [:& editable-label @@ -468,51 +493,55 @@ :disable-dbl-click? true :on-change do-rename :on-cancel cancel-rename}] - (when @dragging? + + (when ^boolean dragging? [:div.dragging])])])])) (mf/defc components-group - [{:keys [file prefix groups open-groups renaming listing-thumbs? selected-components on-asset-click + {::mf/wrap-props false} + [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected on-asset-click on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu - selected-components-full]}] - (let [group-open? (get open-groups prefix true) + selected-full]}] - dragging? (mf/use-state false) + (let [group-open? (get open-groups prefix true) - selected-components-paths (->> selected-components-full - (map #(:path %)) - (map #(if (nil? %) "" %))) + dragging* (mf/use-state false) + dragging? (deref dragging*) + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) on-drag-enter (mf/use-fn - (mf/deps dragging? prefix selected-components-paths) + (mf/deps dragging* prefix selected-paths) (fn [event] - (on-drag-enter-asset-group event dragging? prefix selected-components-paths))) + (on-drag-enter-asset-group event dragging* prefix selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drag-leave-asset event dragging*))) on-drop (mf/use-fn - (mf/deps dragging? prefix selected-components-paths selected-components-full) + (mf/deps dragging* prefix selected-paths selected-full) (fn [event] - (on-drop-asset-group event dragging? prefix selected-components-paths selected-components-full dwl/rename-component)))] + (on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-component)))] [:div {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - [:& asset-group-title {:file-id (:id file) - :box :components - :path prefix - :group-open? group-open? - :on-rename on-rename-group - :on-ungroup on-ungroup}] + + [:& asset-group-title + {:file-id file-id + :section :components + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] (when group-open? [:* @@ -524,41 +553,46 @@ :drop-space (and (empty? components) (some? groups) - (not @dragging?))) + (not dragging?))) :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - (when @dragging? + + (when ^boolean dragging? [:div.grid-placeholder "\u00A0"]) - (when (and - (empty? components) - (some? groups)) + + (when (and (empty? components) + (some? groups)) [:div.drop-space]) + (for [component components] - [:& components-item {:component component - :key (:id component) - :renaming renaming - :listing-thumbs? listing-thumbs? - :file file - :selected-components selected-components - :on-asset-click on-asset-click - :on-context-menu on-context-menu - :on-drag-start on-drag-start - :on-group on-group - :do-rename do-rename - :cancel-rename cancel-rename - :selected-components-full selected-components-full - :selected-components-paths selected-components-paths}])]) + [:& components-item + {:component component + :key (dm/str "component-" (:id component)) + :renaming renaming + :listing-thumbs? listing-thumbs? + :file-id file-id + :selected selected + :selected-full selected-full + :selected-paths selected-paths + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-drag-start + :on-group on-group + :do-rename do-rename + :cancel-rename cancel-rename}])]) + (for [[path-item content] groups] (when-not (empty? path-item) - [:& components-group {:file file + [:& components-group {:file-id file-id + :key path-item :prefix (cph/merge-path-item prefix path-item) :groups content :open-groups open-groups :renaming renaming :listing-thumbs? listing-thumbs? - :selected-components selected-components + :selected selected :on-asset-click on-asset-click :on-drag-start on-drag-start :do-rename do-rename @@ -566,40 +600,49 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-components-full selected-components-full}]))])])) + :selected-full selected-full}]))])])) + +(mf/defc components-section + {::mf/wrap-props false} + [{:keys [file-id local? components listing-thumbs? open? reverse-sort? selected-assets + on-asset-click on-assets-delete on-clear-selection open-status-ref]}] -(mf/defc components-box - [{:keys [file local? components listing-thumbs? open? reverse-sort? open-groups selected-assets - on-asset-click on-assets-delete on-clear-selection] :as props}] (let [input-ref (mf/use-ref nil) state (mf/use-state {:renaming nil :component-id nil}) - menu-state (mf/use-state auto-pos-menu-state) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :components]) + (l/derived open-status-ref))) - selected-components (:components selected-assets) - selected-components-full (filter #(contains? selected-components (:id %)) components) - multi-components? (> (count selected-components) 1) + open-groups (mf/deref open-groups-ref) + + menu-state (mf/use-state initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + selected (:components selected-assets) + selected-full (into #{} (filter #(contains? selected (:id %))) components) + multi-components? (> (count selected) 1) multi-assets? (or (seq (:graphics selected-assets)) (seq (:colors selected-assets)) (seq (:typographies selected-assets))) - groups (group-assets components reverse-sort?) + groups (mf/with-memo [components reverse-sort?] + (group-assets components reverse-sort?)) components-v2 (mf/use-ctx ctx/components-v2) add-component (mf/use-fn (fn [] - #(st/emit! (dwl/set-assets-box-open (:id file) :components true)) + (st/emit! (dwl/set-assets-section-open file-id :components true)) (dom/click (mf/ref-val input-ref)))) on-file-selected (mf/use-fn - (mf/deps file) + (mf/deps file-id) (fn [blobs] - (let [params {:file-id (:id file) + (let [params {:file-id file-id :blobs (seq blobs)}] (st/emit! (dwm/upload-media-components params) (ptk/event ::ev/event {::ev/name "add-asset-to-library" @@ -610,30 +653,30 @@ (mf/deps @state) (fn [] (let [undo-id (js/Symbol)] - (if (empty? selected-components) - (st/emit! (dwl/duplicate-component (:id file) (:component-id @state))) + (if (empty? selected) + (st/emit! (dwl/duplicate-component file-id (:component-id @state))) (do (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! (map (partial dwl/duplicate-component (:id file)) selected-components)) + (apply st/emit! (map (partial dwl/duplicate-component file-id) selected)) (st/emit! (dwu/commit-undo-transaction undo-id))))))) on-delete (mf/use-fn - (mf/deps @state file multi-components? multi-assets?) + (mf/deps @state file-id multi-components? multi-assets?) (fn [] (let [undo-id (js/Symbol)] (if (or multi-components? multi-assets?) - (on-assets-delete) - (st/emit! (dwu/start-undo-transaction undo-id) - (dwl/delete-component {:id (:component-id @state)}) - (dwl/sync-file (:id file) (:id file) :components (:component-id @state)) - (dwu/commit-undo-transaction undo-id)))))) + (on-assets-delete) + (st/emit! (dwu/start-undo-transaction undo-id) + (dwl/delete-component {:id (:component-id @state)}) + (dwl/sync-file file-id file-id :components (:component-id @state)) + (dwu/commit-undo-transaction undo-id)))))) on-rename (mf/use-fn - (mf/deps @state) (fn [] - (swap! state assoc :renaming (:component-id @state)))) + (swap! state (fn [state] + (assoc state :renaming (:component-id state)))))) do-rename (mf/use-fn @@ -654,35 +697,36 @@ on-context-menu (mf/use-fn - (mf/deps selected-components on-clear-selection workspace-read-only?) - (fn [component-id] - (fn [event] - (when (and local? (not workspace-read-only?)) - (when-not (contains? selected-components component-id) + (mf/deps selected on-clear-selection read-only?) + (fn [component-id event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected component-id) (on-clear-selection)) (swap! state assoc :component-id component-id) - (swap! menu-state #(open-auto-pos-menu % event)))))) + (swap! menu-state open-context-menu pos))))) on-close-menu (mf/use-fn (fn [] - (swap! menu-state close-auto-pos-menu))) + (swap! menu-state close-context-menu))) create-group (mf/use-fn - (mf/deps components selected-components on-clear-selection) + (mf/deps components selected on-clear-selection) (fn [group-name] (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> components - (filter #(if multi-components? - (contains? selected-components (:id %)) + (run! st/emit! + (->> components + (filter #(if multi-components? + (contains? selected (:id %)) (= (:component-id @state) (:id %)))) - (map #(dwl/rename-component - (:id %) - (add-group % group-name))))) + (map #(dwl/rename-component + (:id %) + (add-group % group-name))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) rename-group @@ -692,17 +736,17 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> components - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-component - (:id %) - (rename-group % path last-path))))) + (run! st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component + (:id %) + (rename-group % path last-path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-group (mf/use-fn - (mf/deps components selected-components) + (mf/deps components selected) (fn [event] (dom/stop-propagation event) (modal/show! :name-group-dialog {:accept create-group}))) @@ -723,18 +767,17 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> components - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-component - (:id %) - (ungroup % path))))) + (run! st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component (:id %) (ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-drag-start (mf/use-fn + (mf/deps file-id) (fn [component event] - (dnd/set-data! event "penpot/component" {:file-id (:id file) + (dnd/set-data! event "penpot/component" {:file-id file-id :component component}) (dnd/set-allowed-effect! event "move"))) @@ -750,16 +793,19 @@ main-instance-id (:main-instance-id component) main-instance-page (:main-instance-page component)] (when (and main-instance-id main-instance-page) ;; Only when :components-v2 is enabled - (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id))))))] + (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) - [:& asset-section {:file-id (:id file) + on-asset-click + (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] + + [:& asset-section {:file-id file-id :title (tr "workspace.assets.components") - :box :components + :section :components :assets-count (count components) :open? open?} (when local? [:& asset-section-block {:role :title-button} - (when (and components-v2 (not workspace-read-only?)) + (when (and components-v2 (not read-only?)) [:div.assets-button {:on-click add-component} i/plus [:& file-uploader {:accept cm/str-image-types @@ -768,14 +814,14 @@ :on-selected on-file-selected}]])]) [:& asset-section-block {:role :content} - [:& components-group {:file file + [:& components-group {:file-id file-id :prefix "" :groups groups :open-groups open-groups :renaming (:renaming @state) :listing-thumbs? listing-thumbs? - :selected-components selected-components - :on-asset-click (partial on-asset-click groups) + :selected selected + :on-asset-click on-asset-click :on-drag-start on-drag-start :do-rename do-rename :cancel-rename cancel-rename @@ -783,9 +829,9 @@ :on-group on-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-components-full selected-components-full}] + :selected-full selected-full}] (when local? - [:& auto-pos-menu + [:& assets-context-menu {:on-close on-close-menu :state @menu-state :options [(when-not (or multi-components? multi-assets?) @@ -801,58 +847,72 @@ [(tr "workspace.shape.menu.show-main") on-show-main])]}])]])) -;; ---- Graphics box ---- +;; ---- Graphics section ---- (mf/defc graphics-item [{:keys [object renaming listing-thumbs? selected-objects on-asset-click on-context-menu on-drag-start do-rename cancel-rename - selected-graphics-full selected-graphics-paths]}] - (let [item-ref (mf/use-ref) - visible? (h/use-visible item-ref :once? true) - dragging? (mf/use-state false) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + selected-full selected-graphics-paths]}] + (let [item-ref (mf/use-ref) + visible? (h/use-visible item-ref :once? true) + object-id (:id object) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) on-drop (mf/use-fn - (mf/deps object dragging? selected-objects selected-graphics-full selected-graphics-paths) + (mf/deps object dragging* selected-objects selected-full selected-graphics-paths) (fn [event] - (on-drop-asset event object dragging? selected-objects selected-graphics-full + (on-drop-asset event object dragging* selected-objects selected-full selected-graphics-paths dwl/rename-media))) - on-drag-over (mf/use-fn #(dom/prevent-default %)) - on-drag-enter (mf/use-fn - (mf/deps object dragging? selected-objects selected-graphics-paths) + (mf/deps object dragging* selected-objects selected-graphics-paths) (fn [event] - (on-drag-enter-asset event object dragging? selected-objects selected-graphics-paths))) + (on-drag-enter-asset event object dragging* selected-objects selected-graphics-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) + (on-drag-leave-asset event dragging*))) on-grahic-drag-start (mf/use-fn - (mf/deps object selected-objects item-ref on-drag-start workspace-read-only?) + (mf/deps object selected-objects item-ref on-drag-start read-only?) (fn [event] - (if workspace-read-only? + (if read-only? (dom/prevent-default event) - (on-asset-drag-start event object selected-objects item-ref :graphics on-drag-start))))] + (on-asset-drag-start event object selected-objects item-ref :graphics on-drag-start)))) + + on-context-menu + (mf/use-fn + (mf/deps object-id) + (partial on-context-menu object-id)) + + on-asset-click + (mf/use-fn + (mf/deps object-id) + (partial on-asset-click object-id nil)) + + ] [:div {:ref item-ref :class-name (dom/classnames - :selected (contains? selected-objects (:id object)) + :selected (contains? selected-objects object-id) :grid-cell listing-thumbs? :enum-item (not listing-thumbs?)) - :draggable (not workspace-read-only?) - :on-click #(on-asset-click % (:id object) nil) - :on-context-menu (on-context-menu (:id object)) + :draggable (not read-only?) + :on-click on-asset-click + :on-context-menu on-context-menu :on-drag-start on-grahic-drag-start :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} (when visible? @@ -874,49 +934,49 @@ :disable-dbl-click? true :on-change do-rename :on-cancel cancel-rename}] - (when @dragging? + + (when ^boolean dragging? [:div.dragging])])])])) (mf/defc graphics-group [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected-objects on-asset-click on-drag-start do-rename cancel-rename on-rename-group on-ungroup - on-context-menu selected-graphics-full]}] + on-context-menu selected-full]}] (let [group-open? (get open-groups prefix true) - dragging? (mf/use-state false) - - selected-graphics-paths (->> selected-graphics-full - (map #(:path %)) - (map #(if (nil? %) "" %))) + dragging* (mf/use-state false) + dragging? (deref dragging*) + selected-paths + (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) on-drag-enter (mf/use-fn - (mf/deps dragging? prefix selected-graphics-paths) + (mf/deps dragging* prefix selected-paths) (fn [event] - (on-drag-enter-asset-group event dragging? prefix selected-graphics-paths))) + (on-drag-enter-asset-group event dragging* prefix selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drag-leave-asset event dragging*))) on-drop (mf/use-fn - (mf/deps dragging? prefix selected-graphics-paths selected-graphics-full) + (mf/deps dragging* prefix selected-paths selected-full) (fn [event] - (on-drop-asset-group event dragging? prefix selected-graphics-paths selected-graphics-full dwl/rename-media)))] - + (on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-media)))] [:div {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} [:& asset-group-title {:file-id file-id - :box :graphics + :section :graphics :path prefix :group-open? group-open? :on-rename on-rename-group @@ -930,19 +990,21 @@ :drop-space (and (empty? objects) (some? groups) - (not @dragging?))) + (not dragging?))) :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - (when @dragging? + + (when ^boolean dragging? [:div.grid-placeholder "\u00A0"]) - (when (and - (empty? objects) - (some? groups)) + + (when (and (empty? objects) + (some? groups)) [:div.drop-space]) + (for [object objects] - [:& graphics-item {:key (:id object) + [:& graphics-item {:key (dm/str "object-" (:id object)) :object object :renaming renaming :listing-thumbs? listing-thumbs? @@ -952,11 +1014,12 @@ :on-drag-start on-drag-start :do-rename do-rename :cancel-rename cancel-rename - :selected-graphics-full selected-graphics-full - :selected-graphics-paths selected-graphics-paths}])]) + :selected-full selected-full + :selected-paths selected-paths}])]) (for [[path-item content] groups] (when-not (empty? path-item) [:& graphics-group {:file-id file-id + :key path-item :prefix (cph/merge-path-item prefix path-item) :groups content :open-groups open-groups @@ -970,38 +1033,44 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-graphics-full selected-graphics-full - :selected-graphics-paths selected-graphics-paths}]))])])) + :selected-full selected-full + :selected-paths selected-paths}]))])])) -(mf/defc graphics-box - [{:keys [file-id project-id local? objects listing-thumbs? open? open-groups selected-assets reverse-sort? - on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [input-ref (mf/use-ref nil) - state (mf/use-state {:renaming nil - :object-id nil}) +(mf/defc graphics-section + {::mf/wrap-props false} + [{:keys [file-id project-id local? objects listing-thumbs? open? open-status-ref selected-assets reverse-sort? + on-asset-click on-assets-delete on-clear-selection]}] + (let [input-ref (mf/use-ref nil) + state (mf/use-state {:renaming nil :object-id nil}) - menu-state (mf/use-state auto-pos-menu-state) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + menu-state (mf/use-state initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) - selected-objects (:graphics selected-assets) - selected-graphics-full (filter #(contains? selected-objects (:id %)) objects) - multi-objects? (> (count selected-objects) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:colors selected-assets)) - (seq (:typographies selected-assets))) - objects (->> objects - (map dwl/extract-path-if-missing)) + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :graphics]) + (l/derived open-status-ref))) + open-groups (mf/deref open-groups-ref) + selected (:graphics selected-assets) + selected-full (into #{} (filter #(contains? selected (:id %))) objects) + multi-objects? (> (count selected) 1) + multi-assets? (or (seq (:components selected-assets)) + (seq (:colors selected-assets)) + (seq (:typographies selected-assets))) - groups (group-assets objects reverse-sort?) + objects (mf/with-memo [objects] + (mapv dwl/extract-path-if-missing objects)) - components-v2 (mf/use-ctx ctx/components-v2) - team-id (mf/use-ctx ctx/current-team-id) + groups (mf/with-memo [objects reverse-sort?] + (group-assets objects reverse-sort?)) + + components-v2 (mf/use-ctx ctx/components-v2) + team-id (mf/use-ctx ctx/current-team-id) add-graphic (mf/use-fn (fn [] - #(st/emit! (dwl/set-assets-box-open file-id :graphics true)) + (st/emit! (dwl/set-assets-section-open file-id :graphics true)) (dom/click (mf/ref-val input-ref)))) on-file-selected @@ -1016,7 +1085,6 @@ :file-id file-id :project-id project-id :team-id team-id}))))) - on-delete (mf/use-fn (mf/deps @state multi-objects? multi-assets?) @@ -1027,10 +1095,9 @@ on-rename (mf/use-fn - (mf/deps @state) (fn [] - (swap! state assoc :renaming (:object-id @state)))) - + (swap! state (fn [state] + (assoc state :renaming (:component-id state)))))) cancel-rename (mf/use-fn (fn [] @@ -1045,35 +1112,34 @@ on-context-menu (mf/use-fn - (mf/deps selected-objects on-clear-selection workspace-read-only?) - (fn [object-id] - (fn [event] - (when (and local? (not workspace-read-only?)) - (when-not (contains? selected-objects object-id) + (mf/deps selected on-clear-selection read-only?) + (fn [object-id event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected object-id) (on-clear-selection)) (swap! state assoc :object-id object-id) - (swap! menu-state #(open-auto-pos-menu % event)))))) + (swap! menu-state open-context-menu pos))))) on-close-menu (mf/use-fn (fn [] - (swap! menu-state close-auto-pos-menu))) + (swap! menu-state close-context-menu))) create-group (mf/use-fn - (mf/deps objects selected-objects on-clear-selection) + (mf/deps objects selected on-clear-selection) (fn [group-name] (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> objects - (filter #(if multi-objects? - (contains? selected-objects (:id %)) - (= (:object-id @state) (:id %)))) - (map #(dwl/rename-media - (:id %) - (add-group % group-name))))) + (run! st/emit! + (->> objects + (filter #(if multi-objects? + (contains? selected (:id %)) + (= (:object-id @state) (:id %)))) + (map #(dwl/rename-media (:id %) (add-group % group-name))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) rename-group @@ -1083,17 +1149,15 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> objects - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-media - (:id %) - (rename-group % path last-path))))) + (run! st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media (:id %) (rename-group % path last-path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-group (mf/use-fn - (mf/deps objects selected-objects) + (mf/deps objects selected) (fn [event] (dom/stop-propagation event) (modal/show! :name-group-dialog {:accept create-group}))) @@ -1113,12 +1177,10 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> objects - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-media - (:id %) - (ungroup % path))))) + (run! st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media (:id %) (ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-drag-start @@ -1127,16 +1189,19 @@ (dnd/set-data! event "text/asset-id" (str id)) (dnd/set-data! event "text/asset-name" name) (dnd/set-data! event "text/asset-type" mtype) - (dnd/set-allowed-effect! event "move")))] + (dnd/set-allowed-effect! event "move"))) + + on-asset-click + (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] [:& asset-section {:file-id file-id :title (tr "workspace.assets.graphics") - :box :graphics + :section :graphics :assets-count (count objects) :open? open?} (when local? [:& asset-section-block {:role :title-button} - (when (and (not components-v2) (not workspace-read-only?)) + (when (and (not components-v2) (not read-only?)) [:div.assets-button {:on-click add-graphic} i/plus [:& file-uploader {:accept cm/str-image-types @@ -1151,17 +1216,17 @@ :open-groups open-groups :renaming (:renaming @state) :listing-thumbs? listing-thumbs? - :selected-objects selected-objects - :on-asset-click (partial on-asset-click groups) + :selected selected + :on-asset-click on-asset-click :on-drag-start on-drag-start :do-rename do-rename :cancel-rename cancel-rename :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-graphics-full selected-graphics-full}] + :selected-full selected-full}] (when local? - [:& auto-pos-menu + [:& assets-context-menu {:on-close on-close-menu :state @menu-state :options [(when-not (or multi-objects? multi-assets?) @@ -1171,148 +1236,185 @@ [(tr "workspace.assets.group") on-group])]}])]])) -;; ---- Colors box ---- +;; ---- Colors section ---- (mf/defc color-item - [{:keys [color local? file-id selected-colors multi-colors? multi-assets? + {::mf/wrap-props false} + [{:keys [color local? file-id selected multi-colors? multi-assets? on-asset-click on-assets-delete on-clear-selection on-group - selected-colors-full selected-colors-paths move-color] :as props}] - (let [item-ref (mf/use-ref) - dragging? (mf/use-state false) - rename? (= (:color-for-rename @refs/workspace-local) (:id color)) - input-ref (mf/use-ref) - state (mf/use-state {:editing rename?}) + selected-full selected-paths move-color]}] - menu-state (mf/use-state auto-pos-menu-state) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + (let [color (mf/with-memo [color file-id] + (cond-> color + (:value color) (assoc :color (:value color) :opacity 1) + (:value color) (dissoc :value) + true (assoc :file-id file-id))) + + + color-id (:id color) + + item-ref (mf/use-ref) + dragging* (mf/use-state false) + dragging? (deref dragging*) + + rename? (= (:color-for-rename @refs/workspace-local) color-id) + input-ref (mf/use-ref) + + editing* (mf/use-state rename?) + editing? (deref editing*) + + menu-state (mf/use-state initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) default-name (cond - (:gradient color) (uc/gradient-type->string (get-in color [:gradient :type])) - (:color color) (:color color) - :else (:value color)) + (:gradient color) (uc/gradient-type->string (dm/get-in color [:gradient :type])) + (:color color) (:color color) + :else (:value color)) apply-color - (fn [event] - (st/emit! (dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event)))) + (mf/use-fn + (mf/deps color) + (fn [event] + (st/emit! (dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event))))) rename-color - (fn [name] - (st/emit! (dwl/rename-color file-id (:id color) name))) + (mf/use-fn + (mf/deps file-id color-id) + (fn [name] + (st/emit! (dwl/rename-color file-id color-id name)))) edit-color - (fn [new-color] - (let [old-data (-> (select-keys color [:id :file-id]) - (assoc :name (cph/merge-path-item (:path color) (:name color)))) - updated-color (merge new-color old-data)] - (st/emit! (dwl/update-color updated-color file-id)))) + (mf/use-fn + (mf/deps color file-id) + (fn [attrs] + (let [name (cph/merge-path-item (:path color) (:name color)) + color (-> attrs + (assoc :id (:id color)) + (assoc :file-id file-id) + (assoc :name name))] + (st/emit! (dwl/update-color color file-id))))) delete-color (mf/use-fn - (mf/deps @state multi-colors? multi-assets? file-id) + (mf/deps multi-colors? multi-assets? file-id color-id) (fn [] (if (or multi-colors? multi-assets?) (on-assets-delete) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id) (dwl/delete-color color) - (dwl/sync-file file-id file-id :colors (:id color)) + (dwl/sync-file file-id file-id :colors color-id) (dwu/commit-undo-transaction undo-id)))))) rename-color-clicked - (fn [event] - (when (and local? (not workspace-read-only?)) - (dom/prevent-default event) - (swap! state assoc :editing true))) + (mf/use-fn + (mf/deps read-only? local?) + (fn [event] + (when (and local? (not read-only?)) + (dom/prevent-default event) + (reset! editing* true)))) input-blur - (fn [event] - (let [target (dom/event->target event) - name (dom/get-value target)] - (rename-color name) - (st/emit! dwl/clear-color-for-rename) - (swap! state assoc :editing false))) + (mf/use-fn + (mf/deps rename-color) + (fn [event] + (let [target (dom/event->target event) + name (dom/get-value target)] + (rename-color name) + (st/emit! dwl/clear-color-for-rename) + (reset! editing* false)))) input-key-down - (fn [event] - (when (kbd/esc? event) - (st/emit! dwl/clear-color-for-rename) - (swap! state assoc :editing false)) - (when (kbd/enter? event) - (input-blur event))) + (mf/use-fn + (mf/deps input-blur) + (fn [event] + (when (kbd/esc? event) + (st/emit! dwl/clear-color-for-rename) + (reset! editing* false)) + (when (kbd/enter? event) + (input-blur event)))) edit-color-clicked - (fn [event] - (modal/show! :colorpicker - {:x (.-clientX event) - :y (.-clientY event) - :on-accept edit-color - :data color - :position :right})) + (mf/use-fn + (mf/deps edit-color color) + (fn [event] + (modal/show! :colorpicker + {:x (.-clientX ^js event) + :y (.-clientY ^js event) + :on-accept edit-color + :data color + :position :right}))) on-context-menu (mf/use-fn - (mf/deps color selected-colors on-clear-selection workspace-read-only?) + (mf/deps color-id selected on-clear-selection read-only?) (fn [event] - (when (and local? (not workspace-read-only?)) - (when-not (contains? selected-colors (:id color)) - (on-clear-selection)) - (swap! menu-state #(open-auto-pos-menu % event))))) + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected color-id) + (on-clear-selection)) + (swap! menu-state open-context-menu pos))))) on-close-menu (mf/use-fn (fn [] - (swap! menu-state close-auto-pos-menu))) + (swap! menu-state close-context-menu))) + on-drop (mf/use-fn - (mf/deps color dragging? selected-colors selected-colors-full selected-colors-paths move-color) + (mf/deps color dragging* selected selected-full selected-paths move-color) (fn [event] - (on-drop-asset event color dragging? selected-colors selected-colors-full - selected-colors-paths move-color))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drop-asset event color dragging* selected selected-full + selected-paths move-color))) on-drag-enter (mf/use-fn - (mf/deps color dragging? selected-colors selected-colors-paths) + (mf/deps color dragging* selected selected-paths) (fn [event] - (on-drag-enter-asset event color dragging? selected-colors selected-colors-paths))) + (on-drag-enter-asset event color dragging* selected selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) + (on-drag-leave-asset event dragging*))) on-color-drag-start (mf/use-fn - (mf/deps color selected-colors item-ref workspace-read-only?) + (mf/deps color selected item-ref read-only?) (fn [event] - (if workspace-read-only? + (if read-only? (dom/prevent-default event) - (on-asset-drag-start event color selected-colors item-ref :colors identity))))] + (on-asset-drag-start event color selected item-ref :colors identity)))) - (mf/use-effect - (mf/deps (:editing @state)) - #(when (:editing @state) + on-click + (mf/use-fn + (mf/deps color-id apply-color) + (partial on-asset-click color-id apply-color))] + + (mf/with-effect [editing?] + (when editing? (let [input (mf/ref-val input-ref)] - (dom/select-text! input)) - nil)) + (dom/select-text! input) + nil))) + + [:div.asset-list-item + {:class-name (dom/classnames + :selected (contains? selected (:id color))) + :on-context-menu on-context-menu + :on-click (when-not editing? on-click) + :ref item-ref + :draggable (and (not read-only?) (not editing?)) + :on-drag-start on-color-drag-start + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} - [:div.asset-list-item {:class-name (dom/classnames - :selected (contains? selected-colors (:id color))) - :on-context-menu on-context-menu - :on-click (when-not (:editing @state) - #(on-asset-click % (:id color) apply-color)) - :ref item-ref - :draggable (and (not workspace-read-only?) (not (:editing @state))) - :on-drag-start on-color-drag-start - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over on-drag-over - :on-drop on-drop} [:& bc/color-bullet {:color color}] - (if (:editing @state) + (if ^boolean editing? [:input.element-name {:type "text" :ref input-ref @@ -1326,8 +1428,9 @@ (:name color) (when-not (= (:name color) default-name) [:span default-name])]) + (when local? - [:& auto-pos-menu + [:& assets-context-menu {:on-close on-close-menu :state @menu-state :options [(when-not (or multi-colors? multi-assets?) @@ -1337,49 +1440,52 @@ [(tr "workspace.assets.delete") delete-color] (when-not multi-assets? [(tr "workspace.assets.group") (on-group (:id color))])]}]) - (when @dragging? + + (when ^boolean dragging? [:div.dragging])])) (mf/defc colors-group - [{:keys [file-id prefix groups open-groups local? selected-colors + [{:keys [file-id prefix groups open-groups local? selected multi-colors? multi-assets? on-asset-click on-assets-delete on-clear-selection on-group on-rename-group on-ungroup colors - selected-colors-full]}] - (let [group-open? (get open-groups prefix true) - dragging? (mf/use-state false) + selected-full]}] + (let [group-open? (get open-groups prefix true) - selected-colors-paths (->> selected-colors-full - (map #(:path %)) - (map #(if (nil? %) "" %))) + dragging* (mf/use-state false) + dragging? (deref dragging*) + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) - move-color (partial dwl/rename-color file-id) + move-color + (mf/use-fn (mf/deps file-id) (partial dwl/rename-color file-id)) on-drag-enter (mf/use-fn - (mf/deps dragging? prefix selected-colors-paths) + (mf/deps dragging* prefix selected-paths) (fn [event] - (on-drag-enter-asset-group event dragging? prefix selected-colors-paths))) + (on-drag-enter-asset-group event dragging* prefix selected-paths))) - on-drag-leave (mf/use-fn - (mf/deps dragging?) + on-drag-leave + (mf/use-fn + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drag-leave-asset event dragging*))) on-drop (mf/use-fn - (mf/deps dragging? prefix selected-colors-paths selected-colors-full move-color) + (mf/deps dragging* prefix selected-paths selected-full move-color) (fn [event] - (on-drop-asset-group event dragging? prefix selected-colors-paths selected-colors-full move-color)))] + (on-drop-asset-group event dragging* prefix selected-paths selected-full move-color)))] [:div {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} [:& asset-group-title {:file-id file-id - :box :colors + :section :colors :path prefix :group-open? group-open? :on-rename on-rename-group @@ -1389,43 +1495,42 @@ (let [colors (get groups "" [])] [:div.asset-list {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - (when @dragging? + + (when ^boolean dragging? [:div.grid-placeholder "\u00A0"]) - (when (and - (empty? colors) - (some? groups)) + + (when (and (empty? colors) + (some? groups)) [:div.drop-space]) + (for [color colors] - (let [color (cond-> color - (:value color) (assoc :color (:value color) :opacity 1) - (:value color) (dissoc :value) - true (assoc :file-id file-id))] - [:& color-item {:key (:id color) - :color color - :file-id file-id - :local? local? - :selected-colors selected-colors - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :on-group on-group - :colors colors - :selected-colors-full selected-colors-full - :selected-colors-paths selected-colors-paths - :move-color move-color}]))]) + [:& color-item {:key (dm/str (:id color)) + :color color + :file-id file-id + :local? local? + :selected selected + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :colors colors + :selected-full selected-full + :selected-paths selected-paths + :move-color move-color}])]) + (for [[path-item content] groups] (when-not (empty? path-item) [:& colors-group {:file-id file-id :prefix (cph/merge-path-item prefix path-item) - :key (str "group-" path-item) + :key (dm/str "group-" path-item) :groups content :open-groups open-groups :local? local? - :selected-colors selected-colors + :selected selected :multi-colors? multi-colors? :multi-assets? multi-assets? :on-asset-click on-asset-click @@ -1435,32 +1540,41 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :colors colors - :selected-colors-full selected-colors-full}]))])])) + :selected-full selected-full}]))])])) -(mf/defc colors-box - [{:keys [file-id local? colors open? open-groups selected-assets reverse-sort? +(mf/defc colors-section + [{:keys [file-id local? colors open? open-status-ref selected-assets reverse-sort? on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [selected-colors (:colors selected-assets) - selected-colors-full (filter #(contains? selected-colors (:id %)) colors) - multi-colors? (> (count selected-colors) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:graphics selected-assets)) - (seq (:typographies selected-assets))) - groups (group-assets colors reverse-sort?) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + (let [selected (:colors selected-assets) + selected-full (mf/with-memo [selected colors] + (into #{} (filter #(contains? selected (:id %))) colors)) + + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :colors]) + (l/derived open-status-ref))) + open-groups (mf/deref open-groups-ref) + + multi-colors? (> (count selected) 1) + multi-assets? (or (seq (:components selected-assets)) + (seq (:graphics selected-assets)) + (seq (:typographies selected-assets))) + + groups (mf/with-memo [colors reverse-sort?] + (group-assets colors reverse-sort?)) + + read-only? (mf/use-ctx ctx/workspace-read-only?) add-color (mf/use-fn - (mf/deps file-id) - (fn [value _opacity] + (fn [value _] (st/emit! (dwl/add-color value)))) add-color-clicked (mf/use-fn (mf/deps file-id) (fn [event] - (st/emit! (dwl/set-assets-box-open file-id :colors true) + (st/emit! (dwl/set-assets-section-open file-id :colors true) (ptk/event ::ev/event {::ev/name "add-asset-to-library" :asset-type "color"})) (modal/show! :colorpicker @@ -1473,21 +1587,21 @@ create-group (mf/use-fn - (mf/deps colors selected-colors on-clear-selection file-id) + (mf/deps colors selected on-clear-selection file-id) (fn [color-id] (fn [group-name] (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> colors - (filter #(if multi-colors? - (contains? selected-colors (:id %)) - (= color-id (:id %)))) - (map #(dwl/update-color - (assoc % :name - (add-group % group-name)) - file-id)))) + (run! st/emit! + (->> colors + (filter #(if multi-colors? + (contains? selected (:id %)) + (= color-id (:id %)))) + (map #(dwl/update-color + (assoc % :name + (add-group % group-name)) + file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id)))))) rename-group @@ -1497,18 +1611,18 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> colors - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/update-color - (assoc % :name - (rename-group % path last-path)) - file-id)))) + (run! st/emit! + (->> colors + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-color + (assoc % :name + (rename-group % path last-path)) + file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-group (mf/use-fn - (mf/deps colors selected-colors) + (mf/deps colors selected) (fn [color-id] (fn [event] (dom/stop-propagation event) @@ -1536,16 +1650,19 @@ (assoc % :name (ungroup % path)) file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id)))))] + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-asset-click + (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] [:& asset-section {:file-id file-id :title (tr "workspace.assets.colors") - :box :colors + :section :colors :assets-count (count colors) :open? open?} (when local? [:& asset-section-block {:role :title-button} - (when-not workspace-read-only? + (when-not read-only? [:div.assets-button {:on-click add-color-clicked} i/plus])]) @@ -1555,120 +1672,150 @@ :groups groups :open-groups open-groups :local? local? - :selected-colors selected-colors + :selected selected :multi-colors? multi-colors? :multi-assets? multi-assets? - :on-asset-click (partial on-asset-click groups) + :on-asset-click on-asset-click :on-assets-delete on-assets-delete :on-clear-selection on-clear-selection :on-group on-group :on-rename-group on-rename-group :on-ungroup on-ungroup :colors colors - :selected-colors-full selected-colors-full}]]])) + :selected-full selected-full}]]])) -;; ---- Typography box ---- +;; ---- Typography section ---- (mf/defc typography-item - [{:keys [typography file local? handle-change selected-typographies apply-typography - editing-id local-data on-asset-click on-context-menu selected-typographies-full - selected-typographies-paths move-typography] :as props}] - (let [item-ref (mf/use-ref) - dragging? (mf/use-state false) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - editing? (= editing-id (:id typography)) - open? (mf/use-state editing?) + {::mf/wrap-props false} + [{:keys [typography file-id local? handle-change selected apply-typography editing-id on-asset-click + on-context-menu selected-full selected-paths move-typography rename?]}] + (let [item-ref (mf/use-ref) + typography-id (:id typography) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + editing? (= editing-id (:id typography)) + + open* (mf/use-state editing?) + open? (deref open*) + on-drop (mf/use-fn - (mf/deps typography dragging? selected-typographies selected-typographies-full selected-typographies-paths move-typography) + (mf/deps typography dragging* selected selected-full selected-paths move-typography) (fn [event] - (on-drop-asset event typography dragging? selected-typographies selected-typographies-full - selected-typographies-paths move-typography))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drop-asset event typography dragging* selected selected-full + selected-paths move-typography))) on-drag-enter (mf/use-fn - (mf/deps typography dragging? selected-typographies selected-typographies-paths) + (mf/deps typography dragging* selected selected-paths) (fn [event] - (on-drag-enter-asset event typography dragging? selected-typographies selected-typographies-paths))) + (on-drag-enter-asset event typography dragging* selected selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) + (on-drag-leave-asset event dragging*))) on-typography-drag-start (mf/use-fn - (mf/deps typography selected-typographies item-ref workspace-read-only?) + (mf/deps typography selected item-ref read-only?) (fn [event] - (if workspace-read-only? + (if read-only? (dom/prevent-default event) - (on-asset-drag-start event typography selected-typographies item-ref :typographies identity))))] + (on-asset-drag-start event typography selected item-ref :typographies identity)))) + + on-context-menu + (mf/use-fn + (mf/deps on-context-menu typography-id) + (partial on-context-menu typography-id)) + + handle-change + (mf/use-fn + (mf/deps typography) + (partial handle-change typography)) + + apply-typography + (mf/use-fn + (mf/deps typography) + (partial apply-typography typography)) + + on-asset-click + (mf/use-fn + (mf/deps typography apply-typography) + (partial on-asset-click typography-id apply-typography)) + + ] [:div.typography-container {:ref item-ref - :draggable (and (not workspace-read-only?) (not @open?)) + :draggable (and (not read-only?) (not open?)) :on-drag-start on-typography-drag-start :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} [:& typography-entry - {:key (:id typography) - :typography typography - :file file + {:typography typography :local? local? - :on-context-menu #(on-context-menu (:id typography) %) - :on-change #(handle-change typography %) - :selected? (contains? selected-typographies (:id typography)) - :on-click #(on-asset-click % (:id typography) - (partial apply-typography typography)) + :on-context-menu on-context-menu + :on-change handle-change + :selected? (contains? selected typography-id) + :on-click on-asset-click :editing? editing? - :focus-name? (= (:rename-typography local-data) (:id typography)) - :open? open?}] - (when @dragging? + :focus-name? rename? + :external-open* open* + :file-id file-id + }] + + (when ^boolean dragging? [:div.dragging])])) (mf/defc typographies-group - [{:keys [file-id prefix groups open-groups file local? selected-typographies local-data + {::mf/wrap-props false} + [{:keys [file-id prefix groups open-groups file local? selected local-data editing-id on-asset-click handle-change apply-typography on-rename-group - on-ungroup on-context-menu selected-typographies-full]}] - (let [group-open? (get open-groups prefix true) - dragging? (mf/use-state false) + on-ungroup on-context-menu selected-full]}] + (let [group-open? (get open-groups prefix true) + dragging* (mf/use-state false) + dragging? (deref dragging*) - selected-typographies-paths (->> selected-typographies-full - (map #(:path %)) - (map #(if (nil? %) "" %))) - - move-typography (partial dwl/rename-typography file-id) + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) + move-typography + (mf/use-fn + (mf/deps file-id) + (partial dwl/rename-typography file-id)) on-drag-enter (mf/use-fn - (mf/deps dragging? prefix selected-typographies-paths) + (mf/deps dragging* prefix selected-paths) (fn [event] - (on-drag-enter-asset-group event dragging? prefix selected-typographies-paths))) + (on-drag-enter-asset-group event dragging* prefix selected-paths))) on-drag-leave (mf/use-fn - (mf/deps dragging?) + (mf/deps dragging*) (fn [event] - (on-drag-leave-asset event dragging?))) - - on-drag-over (mf/use-fn #(dom/prevent-default %)) + (on-drag-leave-asset event dragging*))) on-drop (mf/use-fn - (mf/deps dragging? prefix selected-typographies-paths selected-typographies-full move-typography) + (mf/deps dragging* prefix selected-paths selected-full move-typography) (fn [event] - (on-drop-asset-group event dragging? prefix selected-typographies-paths selected-typographies-full move-typography)))] + (on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))] [:div {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} [:& asset-group-title {:file-id file-id - :box :typographies + :section :typographies :path prefix :group-open? group-open? :on-rename on-rename-group @@ -1678,40 +1825,42 @@ (let [typographies (get groups "" [])] [:div.asset-list {:on-drag-enter on-drag-enter :on-drag-leave on-drag-leave - :on-drag-over on-drag-over + :on-drag-over dom/prevent-default :on-drop on-drop} - (when @dragging? + + (when ^boolean dragging? [:div.grid-placeholder "\u00A0"]) + (when (and (empty? typographies) (some? groups)) [:div.drop-space]) - (for [typography typographies] + (for [{:keys [id] :as typography} typographies] [:& typography-item {:typography typography - :key (dm/str (:id typography)) - :file file + :key (dm/str "typography-" id) + :file-id file-id :local? local? :handle-change handle-change - :selected-typographies selected-typographies + :selected selected :apply-typography apply-typography :editing-id editing-id - :local-data local-data + :rename? (= (:rename-typography local-data) id) :on-asset-click on-asset-click :on-context-menu on-context-menu - :selected-typographies-full selected-typographies-full - :selected-typographies-paths selected-typographies-paths + :selected-full selected-full + :selected-paths selected-paths :move-typography move-typography}])]) (for [[path-item content] groups] (when-not (empty? path-item) [:& typographies-group {:file-id file-id :prefix (cph/merge-path-item prefix path-item) - :key (dm/str path-item) + :key (dm/str "group-" path-item) :groups content :open-groups open-groups :file file :local? local? - :selected-typographies selected-typographies + :selected selected :editing-id editing-id :local-data local-data :on-asset-click on-asset-click @@ -1720,69 +1869,43 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-typographies-full selected-typographies-full}]))])])) + :selected-full selected-full}]))])])) -(mf/defc typographies-box - [{:keys [file file-id local? typographies open? open-groups selected-assets reverse-sort? - on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [state (mf/use-state {:detail-open? false - :id nil}) +(mf/defc typographies-section + {::mf/wrap-props false} + [{:keys [file file-id local? typographies open? open-status-ref selected-assets reverse-sort? + on-asset-click on-assets-delete on-clear-selection]}] + (let [state (mf/use-state {:detail-open? false :id nil}) + local-data (mf/deref ref:typography-section-state) - local-data (mf/deref typography-data) - menu-state (mf/use-state auto-pos-menu-state) - typographies (->> typographies - (map dwl/extract-path-if-missing)) + read-only? (mf/use-ctx ctx/workspace-read-only?) + menu-state (mf/use-state initial-context-menu-state) + typographies (mf/with-memo [typographies] + (mapv dwl/extract-path-if-missing typographies)) - groups (group-assets typographies reverse-sort?) + groups (mf/with-memo [typographies reverse-sort?] + (group-assets typographies reverse-sort?)) - selected-typographies (:typographies selected-assets) - selected-typographies-full (filter #(contains? selected-typographies (:id %)) typographies) - multi-typographies? (> (count selected-typographies) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:graphics selected-assets)) - (seq (:colors selected-assets))) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + selected (:typographies selected-assets) + selected-full (mf/with-memo [selected typographies] + (into #{} (filter #(contains? selected (:id %))) typographies)) - text-shapes (->> - (mf/deref refs/selected-objects) - (filter #(= (:type %) :text))) + multi-typographies? (> (count selected) 1) + multi-assets? (or (seq (:components selected-assets)) + (seq (:graphics selected-assets)) + (seq (:colors selected-assets))) - state-map (mf/deref refs/workspace-editor-state) - text-shape (first text-shapes) - editor-state (get state-map (:id text-shape)) + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :components]) + (l/derived open-status-ref))) - text-values (dwt/current-text-values - {:editor-state editor-state - :shape text-shape - :attrs dwt/text-attrs}) - - multiple? (or (> 1 (count text-shape)) - (->> text-values vals (d/seek #(= % :multiple)))) - - values (-> (d/without-nils text-values) - (select-keys - (d/concat-vec dwt/text-font-attrs - dwt/text-spacing-attrs - dwt/text-transform-attrs))) - - typography-id (uuid/next) - typography (-> (if multiple? - txt/default-typography - (merge txt/default-typography values)) - (generate-typography-name) - (assoc :id typography-id)) + open-groups (mf/deref open-groups-ref) add-typography (mf/use-fn - (mf/deps file-id typography) + (mf/deps file-id) (fn [_] - (when (not multiple?) - (st/emit! (dwt/update-attrs (:id text-shape) {:typography-ref-id typography-id - :typography-ref-file file-id}))) - - (st/emit! (dwl/add-typography typography) - (ptk/event ::ev/event {::ev/name "add-asset-to-library" - :asset-type "typography"})))) + (st/emit! (dwt/add-typography file-id)))) handle-change (mf/use-fn @@ -1791,35 +1914,27 @@ (st/emit! (dwl/update-typography (merge typography changes) file-id)))) apply-typography - (fn [typography _event] - (let [ids (wsh/lookup-selected @st/state) - attrs (merge - {:typography-ref-file file-id - :typography-ref-id (:id typography)} - (dissoc typography :id :name))] - (run! #(st/emit! - (dwt/update-text-attrs - {:id % - :editor (get @refs/workspace-editor-state %) - :attrs attrs})) - ids))) + (mf/use-fn + (mf/deps file-id) + (fn [typography _event] + (st/emit! (dwt/apply-typography typography file-id)))) create-group (mf/use-fn - (mf/deps typographies selected-typographies on-clear-selection file-id) + (mf/deps typographies selected on-clear-selection file-id) (fn [group-name] (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> typographies - (filter #(if multi-typographies? - (contains? selected-typographies (:id %)) - (= (:id @state) (:id %)))) - (map #(dwl/update-typography - (assoc % :name - (add-group % group-name)) - file-id)))) + (run! st/emit! + (->> typographies + (filter #(if multi-typographies? + (contains? selected (:id %)) + (= (:id @state) (:id %)))) + (map #(dwl/update-typography + (assoc % :name + (add-group % group-name)) + file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id))))) rename-group @@ -1829,18 +1944,18 @@ (on-clear-selection) (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> typographies - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/update-typography - (assoc % :name - (rename-group % path last-path)) - file-id)))) + (run! st/emit! + (->> typographies + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-typography + (assoc % :name + (rename-group % path last-path)) + file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id))))) on-group (mf/use-fn - (mf/deps typographies selected-typographies) + (mf/deps typographies selected) (fn [event] (dom/stop-propagation event) (modal/show! :name-group-dialog {:accept create-group}))) @@ -1871,18 +1986,20 @@ on-context-menu (mf/use-fn - (mf/deps selected-typographies on-clear-selection workspace-read-only?) + (mf/deps selected on-clear-selection read-only?) (fn [id event] - (when (and local? (not workspace-read-only?)) - (when-not (contains? selected-typographies id) - (on-clear-selection)) - (swap! state assoc :id id) - (swap! menu-state #(open-auto-pos-menu % event))))) + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected id) + (on-clear-selection)) + (swap! state assoc :id id) + (swap! menu-state open-context-menu pos))))) on-close-menu (mf/use-fn (fn [] - (swap! menu-state close-auto-pos-menu))) + (swap! menu-state close-context-menu))) handle-rename-typography-clicked (fn [] @@ -1905,7 +2022,12 @@ (dwu/commit-undo-transaction undo-id)))))) editing-id (or (:rename-typography local-data) - (:edit-typography local-data))] + (:edit-typography local-data)) + + on-asset-click + (mf/use-fn + (mf/deps groups) + (partial on-asset-click groups))] (mf/use-effect (mf/deps local-data) @@ -1917,12 +2039,12 @@ [:& asset-section {:file-id file-id :title (tr "workspace.assets.typography") - :box :typographies + :section :typographies :assets-count (count typographies) :open? open?} (when local? [:& asset-section-block {:role :title-button} - (when-not workspace-read-only? + (when-not read-only? [:div.assets-button {:on-click add-typography} i/plus])]) @@ -1934,19 +2056,19 @@ :state state :file file :local? local? - :selected-typographies selected-typographies + :selected selected :editing-id editing-id :local-data local-data - :on-asset-click (partial on-asset-click groups) + :on-asset-click on-asset-click :handle-change handle-change :apply-typography apply-typography :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-typographies-full selected-typographies-full}] + :selected-full selected-full}] (when local? - [:& auto-pos-menu + [:& assets-context-menu {:on-close on-close-menu :state @menu-state :options [(when-not (or multi-typographies? multi-assets?) @@ -1958,135 +2080,132 @@ [(tr "workspace.assets.group") on-group])]}])]])) -;; --- Assets toolbox ---- +;; --- Assets toolsection ---- -(defn file-colors-ref - [id] - (l/derived (fn [state] - (let [wfile (:workspace-data state)] - (if (= (:id wfile) id) - (vals (get wfile :colors)) - (vals (get-in state [:workspace-libraries id :data :colors]))))) - st/state =)) - -(defn file-media-ref - [id] - (l/derived (fn [state] - (let [wfile (:workspace-data state)] - (if (= (:id wfile) id) - (vals (get wfile :media)) - (vals (get-in state [:workspace-libraries id :data :media]))))) - st/state =)) - -(defn file-components-ref - [id] - (l/derived (fn [state] - (let [wfile (:workspace-data state)] - (if (= (:id wfile) id) - (ctkl/components-seq wfile) - (ctkl/components-seq (get-in state [:workspace-libraries id :data]))))) - st/state =)) - -(defn file-typography-ref - [id] - (l/derived (fn [state] - (let [wfile (:workspace-data state)] - (if (= (:id wfile) id) - (vals (get wfile :typographies)) - (vals (get-in state [:workspace-libraries id :data :typographies]))))) - st/state =)) - -(defn make-open-file-ref - [id] - (mf/with-memo [id] - (-> (l/in [:assets-files-open id]) - (l/derived refs/workspace-global)))) - -(defn apply-filters - [coll filters reverse-sort?] - (let [comp-fn (if reverse-sort? > <)] +(defn- apply-filters + [coll {:keys [ordering term] :as filters}] + (let [reverse? (= :desc ordering) + comp-fn (if ^boolean reverse? > <)] (->> coll (filter (fn [item] - (or (matches-search (:name item "!$!") (:term filters)) - (matches-search (:value item "!$!") (:term filters))))) - ; Sort by folder order, but putting all "root" items always first, - ; independently of sort order. + (or (matches-search (:name item "!$!") term) + (matches-search (:value item "!$!") term)))) + ; Sort by folder order, but + ; putting all "root" items + ; always first, independently + ; of sort order. (sort-by #(str/lower (cph/merge-path-item (if (empty? (:path %)) - (if reverse-sort? "z" "a") + (if reverse? "z" "a") (:path %)) (:name %))) comp-fn)))) -(mf/defc file-library - [{:keys [file local? default-open? filters] :as props}] - (let [open-file (mf/deref (make-open-file-ref (:id file))) - open? (-> open-file - :library - (d/nilv default-open?)) - open-box? (fn [box] - (-> open-file - box - (d/nilv true))) - open-groups (fn [box] - (-> open-file - :groups - box - (d/nilv {}))) - shared? (:is-shared file) - router (mf/deref refs/router) - reverse-sort? (mf/deref refs/file-library-reverse-sort?) - reverse-sort? (if (nil? reverse-sort?) false reverse-sort?) +(mf/defc file-library-title + {::mf/wrap-props false} + [{:keys [open? local? shared? project-id file-id page-id file-name]}] + (let [router (mf/deref refs/router) + url (rt/resolve router :workspace + {:project-id project-id + :file-id file-id} + {:page-id page-id}) - listing-thumbs? (mf/deref refs/file-library-listing-thumbs?) - listing-thumbs? (if (nil? listing-thumbs?) true listing-thumbs?) - - selected-assets (mf/deref refs/selected-assets) - - selected-count (+ (count (:components selected-assets)) - (count (:graphics selected-assets)) - (count (:colors selected-assets)) - (count (:typographies selected-assets))) - - components-v2 (mf/use-ctx ctx/components-v2) - - toggle-open #(st/emit! (dwl/set-assets-box-open (:id file) :library (not open?))) - - url (rt/resolve router :workspace - {:project-id (:project-id file) - :file-id (:id file)} - {:page-id (get-in file [:data :pages 0])}) - - colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file))) - colors (apply-filters (mf/deref colors-ref) filters reverse-sort?) - - typography-ref (mf/use-memo (mf/deps (:id file)) #(file-typography-ref (:id file))) - typographies (apply-filters (mf/deref typography-ref) filters reverse-sort?) - - media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file))) - media (apply-filters (mf/deref media-ref) filters reverse-sort?) - - components-ref (mf/use-memo (mf/deps (:id file)) #(file-components-ref (:id file))) - components (apply-filters (mf/deref components-ref) filters reverse-sort?) - - toggle-sort + toggle-open (mf/use-fn - (mf/deps reverse-sort?) - (fn [_] - (st/emit! (dw/set-file-library-reverse-sort (not reverse-sort?))))) + (mf/deps file-id open?) + (fn [] + (st/emit! (dwl/set-assets-section-open file-id :library (not open?))))) + ] - toggle-listing - (mf/use-fn - (mf/deps listing-thumbs?) - (fn [_] - (st/emit! (dw/set-file-library-listing-thumbs (not listing-thumbs?))))) + [:div.tool-window-bar.library-bar + {:on-click toggle-open} + [:div.collapse-library + {:class (dom/classnames :open open?)} + i/arrow-slide] + + (if local? + [:* + [:span file-name " (" (tr "workspace.assets.local-library") ")"] + (when shared? + [:span.tool-badge (tr "workspace.assets.shared")])] + [:* + [:span file-name] + [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"} + [:a {:href (str "#" url) + :target "_blank" + :on-click dom/stop-propagation} + i/chain]]])])) + +(mf/defc file-library-content + {::mf/wrap-props false} + [{:keys [file local? open-status-ref on-clear-selection]}] + (let [components-v2 (mf/use-ctx ctx/components-v2) + open-status (mf/deref open-status-ref) + + file-id (:id file) + project-id (:project-id file) + + filters (mf/use-ctx ctx:filters) + filters-section (:section filters) + filters-term (:term filters) + filters-ordering (:ordering filters) + filters-list-style (:list-style filters) + + reverse-sort? (= :desc filters-ordering) + listing-thumbs? (= :thumbs filters-list-style) + + toggle-ordering (mf/use-ctx ctx:toggle-ordering) + toggle-list-style (mf/use-ctx ctx:toggle-list-style) + + library-ref (mf/with-memo [file-id] + (create-file-library-ref file-id)) + + library (mf/deref library-ref) + colors (:colors library) + components (:components library) + media (:media library) + typographies (:typographies library) + + colors (mf/with-memo [filters colors] + (apply-filters colors filters)) + components (mf/with-memo [filters components] + (apply-filters components filters)) + media (mf/with-memo [filters media] + (apply-filters media filters)) + typographies (mf/with-memo [filters typographies] + (apply-filters typographies filters)) + + show-components? (and (or (= filters-section :all) + (= filters-section :components)) + (or (pos? (count components)) + (str/empty? filters-term))) + show-graphics? (and (or (= filters-section :all) + (= filters-section :graphics)) + (or (pos? (count media)) + (and (str/empty? filters-term) + (not components-v2)))) + show-colors? (and (or (= filters-section :all) + (= filters-section :colors)) + (or (> (count colors) 0) + (str/empty? filters-term))) + show-typography? (and (or (= filters-section :all) + (= filters-section :typographies)) + (or (pos? (count typographies)) + (str/empty? filters-term))) + + selected-assets (mf/deref ref:selected-assets) + selected-count (+ (count (:components selected-assets)) + (count (:graphics selected-assets)) + (count (:colors selected-assets)) + (count (:typographies selected-assets))) extend-selected-assets (mf/use-fn (mf/deps selected-assets) (fn [asset-type asset-groups asset-id] - (letfn [(flatten-groups - [groups] + + ;; FIXME: revisit performance of this function + (letfn [(flatten-groups [groups] (reduce concat [(get groups "" []) (into [] (->> (filter #(seq (first %)) groups) @@ -2104,23 +2223,17 @@ selected-idx (vector first-idx clicked-idx) min-idx (apply min (conj selected-idx clicked-idx)) max-idx (apply max (conj selected-idx clicked-idx)) - values (->> all-assets - d/enumerate - (filter #(<= min-idx (first %) max-idx)) - (map #(-> % second :id)) - set)] + values (->> all-assets + (d/enumerate) + (filter #(<= min-idx (first %) max-idx)) + (map #(-> % second :id)) + (set))] (st/emit! (dw/select-assets values asset-type)))))))) - unselect-all - (mf/use-fn - (fn [] - (st/emit! (dw/unselect-all-assets)))) - on-asset-click (mf/use-fn - (mf/deps selected-assets) - (fn [asset-type asset-groups event asset-id default-click] + (fn [asset-type asset-groups asset-id default-click event] (cond (kbd/mod? event) (do @@ -2136,224 +2249,284 @@ (when default-click (default-click event))))) + on-component-click + (mf/use-fn (partial on-asset-click :components)) + + on-graphics-click + (mf/use-fn (partial on-asset-click :graphics)) + + on-colors-click + (mf/use-fn (partial on-asset-click :colors)) + + on-typography-click + (mf/use-fn (partial on-asset-click :typographies)) + on-assets-delete (mf/use-fn (mf/deps selected-assets) (fn [] (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! (map #(dwl/delete-component {:id %}) - (:components selected-assets))) - (apply st/emit! (map #(dwl/delete-media {:id %}) - (:graphics selected-assets))) - (apply st/emit! (map #(dwl/delete-color {:id %}) - (:colors selected-assets))) - (apply st/emit! (map #(dwl/delete-typography %) - (:typographies selected-assets))) + (run! st/emit! (map #(dwl/delete-component {:id %}) + (:components selected-assets))) + (run! st/emit! (map #(dwl/delete-media {:id %}) + (:graphics selected-assets))) + (run! st/emit! (map #(dwl/delete-color {:id %}) + (:colors selected-assets))) + (run! st/emit! (map #(dwl/delete-typography %) + (:typographies selected-assets))) (when (or (d/not-empty? (:components selected-assets)) (d/not-empty? (:colors selected-assets)) (d/not-empty? (:typographies selected-assets))) (st/emit! (dwl/sync-file (:id file) (:id file)))) (st/emit! (dwu/commit-undo-transaction undo-id)))))] - [:div.tool-window {:on-context-menu #(dom/prevent-default %) + [:div.tool-window-content + [:div.listing-options + (when (> selected-count 0) + [:span.selected-count + (tr "workspace.assets.selected-count" (i18n/c selected-count))]) + [:div.listing-option-btn.first {:on-click toggle-ordering} + (if reverse-sort? + i/sort-ascending + i/sort-descending)] + [:div.listing-option-btn {:on-click toggle-list-style} + (if listing-thumbs? + i/listing-enum + i/listing-thumbs)]] + + (when ^boolean show-components? + [:& components-section + {:file-id file-id + :local? local? + :components components + :listing-thumbs? listing-thumbs? + :open? (get open-status :components true) + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected-assets selected-assets + :on-asset-click on-component-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-graphics? + [:& graphics-section + {:file-id file-id + :project-id project-id + :local? local? + :objects media + :listing-thumbs? listing-thumbs? + :open? (get open-status :graphics true) + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected-assets selected-assets + :on-asset-click on-graphics-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-colors? + [:& colors-section + {:file-id file-id + :local? local? + :colors colors + :open? (get open-status :colors true) + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected-assets selected-assets + :on-asset-click on-colors-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-typography? + [:& typographies-section + {:file file + :file-id (:id file) + :local? local? + :typographies typographies + :open? (get open-status :typographies true) + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected-assets selected-assets + :on-asset-click on-typography-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when (and (not ^boolean show-components?) + (not ^boolean show-graphics?) + (not ^boolean show-colors?) + (not ^boolean show-typography?)) + [:div.asset-section + [:div.asset-title (tr "workspace.assets.not-found")]])])) + +(mf/defc file-library + {::mf/wrap-props false} + [{:keys [file local? default-open? filters]}] + (let [file-id (:id file) + file-name (:name file) + shared? (:is-shared file) + project-id (:project-id file) + page-id (dm/get-in file [:data :pages 0]) + + open-status-ref (mf/with-memo [file-id] + (-> (l/key file-id) + (l/derived ref:open-status))) + open-status (mf/deref open-status-ref) + open? (d/nilv (:library open-status) default-open?) + + unselect-all + (mf/use-fn + (fn [] + (st/emit! (dw/unselect-all-assets)))) + + ] + + [:div.tool-window {:on-context-menu dom/prevent-default :on-click unselect-all} - [:div.tool-window-bar.library-bar - {:on-click toggle-open} - [:div.collapse-library - {:class (dom/classnames :open open?)} - i/arrow-slide] + [:& file-library-title + {:project-id project-id + :file-id file-id + :page-id page-id + :file-name file-name + :open? open? + :local? local? + :shared? shared?}] + (when ^boolean open? + [:& file-library-content + {:file file + :local? local? + :filters filters + :on-clear-selection unselect-all + :open-status-ref open-status-ref}])])) - (if local? - [:* - [:span (:name file) " (" (tr "workspace.assets.local-library") ")"] - (when shared? - [:span.tool-badge (tr "workspace.assets.shared")])] - [:* - [:span (:name file)] - [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"} - [:a {:href (str "#" url) - :target "_blank" - :on-click dom/stop-propagation} - i/chain]]])] +(mf/defc assets-libraries + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [filters]}] + (let [libraries (mf/deref refs/workspace-libraries) + libraries (mf/with-memo [libraries] + (->> (vals libraries) + (remove :is-indirect) + (map (fn [file] + (update file :data dissoc :pages-index))) + (sort-by #(str/lower (:name %)))))] + (for [file libraries] + [:& file-library + {:key (dm/str (:id file)) + :file file + :local? false + :default-open? false + :filters filters}]))) - (when open? - (let [show-components? (and (or (= (:box filters) :all) - (= (:box filters) :components)) - (or (> (count components) 0) - (str/empty? (:term filters)))) - show-graphics? (and (or (= (:box filters) :all) - (= (:box filters) :graphics)) - (or (> (count media) 0) - (and (str/empty? (:term filters)) - (not components-v2)))) - show-colors? (and (or (= (:box filters) :all) - (= (:box filters) :colors)) - (or (> (count colors) 0) - (str/empty? (:term filters)))) - show-typography? (and (or (= (:box filters) :all) - (= (:box filters) :typographies)) - (or (> (count typographies) 0) - (str/empty? (:term filters))))] - [:div.tool-window-content - [:div.listing-options - (when (> selected-count 0) - [:span.selected-count - (tr "workspace.assets.selected-count" (i18n/c selected-count))]) - [:div.listing-option-btn.first {:on-click toggle-sort} - (if reverse-sort? - i/sort-ascending - i/sort-descending)] - [:div.listing-option-btn {:on-click toggle-listing} - (if listing-thumbs? - i/listing-enum - i/listing-thumbs)]] +(mf/defc assets-local-library + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [filters]}] + ;; NOTE: as workspace-file is an incomplete view of file (it do not + ;; contain :data), we need to reconstruct it using workspace-data + (let [file (mf/deref refs/workspace-file) + data (mf/deref refs/workspace-data) + data (mf/with-memo [data] + (dissoc data :pages-index)) + file (mf/with-memo [file data] + (assoc file :data data))] - (when show-components? - [:& components-box {:file file - :local? local? - :components components - :listing-thumbs? listing-thumbs? - :open? (open-box? :components) - :open-groups (open-groups :components) - :reverse-sort? reverse-sort? - :selected-assets selected-assets - :on-asset-click (partial on-asset-click :components) - :on-assets-delete on-assets-delete - :on-clear-selection unselect-all}]) - - (when show-graphics? - [:& graphics-box {:file-id (:id file) - :project-id (:project-id file) - :local? local? - :objects media - :listing-thumbs? listing-thumbs? - :open? (open-box? :graphics) - :open-groups (open-groups :graphics) - :reverse-sort? reverse-sort? - :selected-assets selected-assets - :on-asset-click (partial on-asset-click :graphics) - :on-assets-delete on-assets-delete - :on-clear-selection unselect-all}]) - (when show-colors? - [:& colors-box {:file-id (:id file) - :local? local? - :colors colors - :open? (open-box? :colors) - :open-groups (open-groups :colors) - :reverse-sort? reverse-sort? - :selected-assets selected-assets - :on-asset-click (partial on-asset-click :colors) - :on-assets-delete on-assets-delete - :on-clear-selection unselect-all}]) - - (when show-typography? - [:& typographies-box {:file file - :file-id (:id file) - :local? local? - :typographies typographies - :open? (open-box? :typographies) - :open-groups (open-groups :typographies) - :reverse-sort? reverse-sort? - :selected-assets selected-assets - :on-asset-click (partial on-asset-click :typographies) - :on-assets-delete on-assets-delete - :on-clear-selection unselect-all}]) - - (when (and (not show-components?) (not show-graphics?) (not show-colors?) (not show-typography?)) - [:div.asset-section - [:div.asset-title (tr "workspace.assets.not-found")]])]))])) + [:& file-library + {:file file + :local? true + :default-open? true + :filters filters}])) +(defn- toggle-values + [v [a b]] + (if (= v a) b a)) (mf/defc assets-toolbox + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [] - (let [libraries (->> (mf/deref refs/workspace-libraries) - (vals) - (remove :is-indirect)) - file (mf/deref refs/workspace-file) - team-id (mf/use-ctx ctx/current-team-id) - filters (mf/use-state {:term "" :box :all}) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + (let [read-only? (mf/use-ctx ctx/workspace-read-only?) + filters* (mf/use-state + {:term "" + :section :all + :ordering :asc + :list-style :thumbs}) + filters (deref filters*) + + + toggle-ordering + (mf/use-fn #(swap! filters* update :ordering toggle-values [:asc :desc])) + + toggle-list-style + (mf/use-fn #(swap! filters* update :list-style toggle-values [:thumbs :list])) on-search-term-change (mf/use-fn - (mf/deps team-id) (fn [event] (let [value (dom/get-target-val event)] - (swap! filters assoc :term value)))) + (swap! filters* assoc :term value)))) on-search-clear-click - (mf/use-fn - (mf/deps team-id) - (fn [_] - (swap! filters assoc :term ""))) + (mf/use-fn #(swap! filters* assoc :term "")) - on-box-filter-change + on-section-filter-change (mf/use-fn - (mf/deps team-id) (fn [event] (let [value (-> (dom/get-target event) (dom/get-value) (d/read-string))] - (swap! filters assoc :box value)))) + (swap! filters* assoc :section value)))) handle-key-down - (mf/use-callback + (mf/use-fn (fn [event] (let [enter? (kbd/enter? event) esc? (kbd/esc? event) - input-node (dom/event->target event)] + node (dom/event->target event)] - (when enter? - (dom/blur! input-node)) - (when esc? - (dom/blur! input-node)))))] + (when ^boolean enter? (dom/blur! node)) + (when ^boolean esc? (dom/blur! node))))) - [:div.assets-bar - [:div.tool-window - [:div.tool-window-content - [:div.assets-bar-title - (tr "workspace.assets.assets") - (when-not workspace-read-only? - [:div.libraries-button {:on-click #(modal/show! :libraries-dialog {})} - i/text-align-justify - (tr "workspace.assets.libraries")])] + show-libraries-dialog + (mf/use-fn #(modal/show! :libraries-dialog {}))] - [:div.search-block - [:input.search-input - {:placeholder (tr "workspace.assets.search") - :type "text" - :value (:term @filters) - :on-change on-search-term-change - :on-key-down handle-key-down}] - (if (str/empty? (:term @filters)) - [:div.search-icon - i/search] - [:div.search-icon.close - {:on-click on-search-clear-click} - i/close])] + [:& perf/profiler {:enabled true :label "sidebar/assets"} + [:div.assets-bar + [:div.tool-window + [:div.tool-window-content + [:div.assets-bar-title + (tr "workspace.assets.assets") - [:select.input-select {:value (:box @filters) - :on-change on-box-filter-change} - [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] - [:option {:value ":components"} (tr "workspace.assets.components")] - [:option {:value ":graphics"} (tr "workspace.assets.graphics")] - [:option {:value ":colors"} (tr "workspace.assets.colors")] - [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] + (when-not ^boolean read-only? + [:div.libraries-button {:on-click show-libraries-dialog} + i/text-align-justify + (tr "workspace.assets.libraries")])] - [:div.libraries-wrapper - [:& file-library - {:file file - :local? true - :default-open? true - :filters @filters}] + [:div.search-block + [:input.search-input + {:placeholder (tr "workspace.assets.search") + :type "text" + :value (:term filters) + :on-change on-search-term-change + :on-key-down handle-key-down}] - (for [file (->> libraries - (sort-by #(str/lower (:name %))))] - [:& file-library - {:key (:id file) - :file file - :local? false - :default-open? false - :filters @filters}])]])) + (if ^boolean (str/empty? (:term filters)) + [:div.search-icon + i/search] + [:div.search-icon.close + {:on-click on-search-clear-click} + i/close])] + [:select.input-select {:value (:section filters) + :on-change on-section-filter-change} + [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] + [:option {:value ":components"} (tr "workspace.assets.components")] + [:option {:value ":graphics"} (tr "workspace.assets.graphics")] + [:option {:value ":colors"} (tr "workspace.assets.colors")] + [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] + + [:& (mf/provider ctx:filters) {:value filters} + [:& (mf/provider ctx:toggle-ordering) {:value toggle-ordering} + [:& (mf/provider ctx:toggle-list-style) {:value toggle-list-style} + [:div.libraries-wrapper + [:& assets-local-library {:filters filters}] + [:& assets-libraries {:filters filters}]]]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs index f132e9e953..86ef4cf188 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs @@ -18,15 +18,16 @@ (mf/defc collapsed-button {::mf/wrap-props false} [] - (let [new-css-system (mf/use-ctx ctx/new-css-system)] - (if new-css-system + (let [new-css? (mf/use-ctx ctx/new-css-system) + on-click (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))] + (if ^boolean new-css? [:div {:class (dom/classnames (css :collapsed-sidebar) true)} [:div {:class (dom/classnames (css :collapsed-title) true)} [:button {:class (dom/classnames (css :collapsed-button) true) - :on-click #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)) + :on-click on-click :aria-label (tr "workspace.sidebar.expand")} i/arrow-refactor]]] [:button.collapse-sidebar.collapsed - {:on-click #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)) + {:on-click on-click :aria-label (tr "workspace.sidebar.expand")} - i/arrow-slide]))) \ No newline at end of file + i/arrow-slide]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 9cbfc528c4..6e2f4394fb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -14,7 +14,6 @@ [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] - [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -23,7 +22,6 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as ts] - [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc text-align-options @@ -159,11 +157,6 @@ :on-click #(handle-change % "line-through")} i/strikethrough]])) -(defn generate-typography-name - [{:keys [font-id font-variant-id] :as typography}] - (let [{:keys [name]} (fonts/get-font-data font-id)] - (assoc typography :name (str name " " (str/title font-variant-id))))) - (mf/defc text-menu {::mf/wrap [mf/memo]} [{:keys [ids type values] :as props}] @@ -215,7 +208,7 @@ dwt/text-spacing-attrs dwt/text-transform-attrs))) typography (merge txt/default-typography set-values) - typography (generate-typography-name typography) + typography (dwt/generate-typography-name typography) id (uuid/next)] (st/emit! (dwl/add-typography (assoc typography :id id) false)) (run! #(emit-update! % {:typography-ref-id id diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 7f5e210b11..81fbcbe223 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -13,6 +13,7 @@ [app.common.text :as txt] [app.main.data.fonts :as fts] [app.main.data.shortcuts :as dsc] + [app.main.data.workspace :as dw] [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] @@ -24,8 +25,6 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.object :as obj] - [app.util.router :as rt] [app.util.strings :as ust] [app.util.timers :as tm] [cuerdas.core :as str] @@ -38,7 +37,7 @@ (ust/format-precision value 2))) (defn select-all [event] - (dom/select-text! (dom/get-target event))) + (some-> event dom/get-target dom/select-text!)) (defn- get-next-font [{:keys [id] :as current} fonts] @@ -229,8 +228,8 @@ [:div.fonts-list [:> rvt/AutoSizer {} (fn [props] - (let [width (obj/get props "width") - height (obj/get props "height") + (let [width (unchecked-get props "width") + height (unchecked-get props "height") render #(row-renderer fonts @selected on-select-and-close %)] (mf/html [:> rvt/List #js {:height height @@ -241,9 +240,9 @@ :rowRenderer render}])))]]]])) (defn row-renderer [fonts selected on-select props] - (let [index (obj/get props "index") - key (obj/get props "key") - style (obj/get props "style") + (let [index (unchecked-get props "index") + key (unchecked-get props "key") + style (unchecked-get props "style") font (nth fonts index)] (mf/html [:& font-item {:key key @@ -253,7 +252,8 @@ :current? (= (:id font) (:id selected))}]))) (mf/defc font-options - [{:keys [values on-change on-blur show-recent] :as props}] + {::mf/wrap-props false} + [{:keys [values on-change on-blur show-recent]}] (let [{:keys [font-id font-size font-variant-id]} values font-id (or font-id (:font-id txt/default-text-attrs)) @@ -371,7 +371,8 @@ (mf/defc spacing-options - [{:keys [values on-change on-blur] :as props}] + {::mf/wrap-props false} + [{:keys [values on-change on-blur]}] (let [{:keys [line-height letter-spacing]} values @@ -416,7 +417,8 @@ :on-blur on-blur}]]])) (mf/defc text-transform-options - [{:keys [values on-change on-blur] :as props}] + {::mf/wrap-props false} + [{:keys [values on-change on-blur]}] (let [text-transform (or (:text-transform values) "none") handle-change (fn [_ type] @@ -446,6 +448,7 @@ i/titlecase]])) (mf/defc typography-options + {::mf/wrap-props false} [{:keys [ids editor values on-change on-blur show-recent]}] (let [opts #js {:editor editor :ids ids @@ -460,19 +463,18 @@ [:div.row-flex [:> text-transform-options opts]]])) - -;; TODO: this need to be refactored, right now combines too much logic -;; and has a dropdown that behaves like a modal but is not a modal. -;; In summary, this need to a good UX/UI/IMPL rework. - (mf/defc typography-entry - [{:keys [typography local? selected? on-click on-change on-detach on-context-menu editing? focus-name? file open?]}] - (let [hover-detach (mf/use-state false) + {::mf/wrap-props false} + [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? focus-name? external-open*]}] + (let [hover-detach* (mf/use-state false) + hover-detach? (deref hover-detach*) + name-input-ref (mf/use-ref) - on-change-ref (mf/use-ref nil) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - editable? (and local? (not workspace-read-only?)) - open? (if (nil? open?) (mf/use-state editing?) open?) + read-only? (mf/use-ctx ctx/workspace-read-only?) + editable? (and local? (not read-only?)) + + open* (mf/use-state editing?) + open? (deref open*) on-name-blur (mf/use-callback @@ -480,11 +482,36 @@ (fn [event] (let [name (dom/get-target-val event)] (when-not (str/blank? name) - (on-change {:name name})))))] + (on-change {:name name}))))) + + on-pointer-enter + (mf/use-fn #(reset! hover-detach* true)) + + on-pointer-leave + (mf/use-fn #(reset! hover-detach* false)) + + on-open + (mf/use-fn #(reset! open* true)) + + on-close + (mf/use-fn #(reset! open* false)) + + navigate-to-library + (mf/use-fn + (mf/deps file-id) + (fn [] + (when file-id + (st/emit! (dw/navigate-to-library file-id))))) + + ] (mf/with-effect [editing?] (when editing? - (reset! open? editing?))) + (reset! open* editing?))) + + (mf/with-effect [open?] + (when (some? external-open*) + (reset! external-open* open?))) (mf/with-effect [focus-name?] (when focus-name? @@ -493,15 +520,12 @@ (dom/focus! node) (dom/select-text! node))))) - (mf/with-effect [on-change] - (mf/set-ref-val! on-change-ref {:on-change on-change})) - [:* [:div.element-set-options-group.typography-entry - {:class (when selected? "selected") - :style {:display (when @open? "none")}} + {:class (when ^boolean selected? "selected") + :style {:display (when ^boolean open? "none")}} [:div.typography-selection-wrapper - {:class (when on-click "is-selectable") + {:class (when ^boolean on-click "is-selectable") :on-click on-click :on-context-menu on-context-menu} [:div.typography-sample @@ -511,20 +535,36 @@ (tr "workspace.assets.typography.sample")] [:div.typography-name {:title (:name typography)}(:name typography)]] [:div.element-set-actions - (when on-detach + (when ^boolean on-detach [:div.element-set-actions-button - {:on-pointer-enter #(reset! hover-detach true) - :on-pointer-leave #(reset! hover-detach false) + {:on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave :on-click on-detach} - (if @hover-detach i/unchain i/chain)]) + (if ^boolean hover-detach? i/unchain i/chain)]) [:div.element-set-actions-button - {:on-click #(reset! open? true)} + {:on-click on-open} i/actions]]] - [:& advanced-options {:visible? @open? - :on-close #(reset! open? false)} - (if (not editable?) + [:& advanced-options {:visible? open? :on-close on-close} + (if ^boolean editable? + [:* + [:div.element-set-content + [:div.row-flex + [:input.element-name.adv-typography-name + {:type "text" + :ref name-input-ref + :default-value (:name typography) + :on-blur on-name-blur}] + + [:div.element-set-actions-button + {:on-click on-close} + i/actions]]] + + [:& typography-options {:values typography + :on-change on-change + :show-recent false}]] + [:div.element-set-content.typography-read-only-data [:div.row-flex.typography-name [:span {:title (:name typography)} (:name typography)]] @@ -534,7 +574,7 @@ [:span (:font-id typography)]] [:div.element-set-actions-button.actions-inside - {:on-click #(reset! open? false)} + {:on-click on-close} i/actions] [:div.row-flex @@ -560,25 +600,7 @@ (when-not local? [:div.row-flex [:a.go-to-lib-button - {:on-click #(st/emit! (rt/nav-new-window* {:rname :workspace - :path-params {:project-id (:project-id file) - :file-id (:id file)} - :query-params {:page-id (get-in file [:data :pages 0])}}))} + {:on-click navigate-to-library} (tr "workspace.assets.typography.go-to-edit")]])] - [:* - [:div.element-set-content - [:div.row-flex - [:input.element-name.adv-typography-name - {:type "text" - :ref name-input-ref - :default-value (:name typography) - :on-blur on-name-blur}] - - [:div.element-set-actions-button - {:on-click #(reset! open? false)} - i/actions]]] - - [:& typography-options {:values typography - :on-change on-change - :show-recent false}]])]])) + )]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 4b0c2ef127..628c6c8ff1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -224,7 +224,7 @@ (defn on-context-menu [hover hover-ids workspace-read-only?] - (mf/use-callback + (mf/use-fn (mf/deps @hover @hover-ids workspace-read-only?) (fn [event] (if workspace-read-only? From bdb0e24c400de999e3af6d5c5916fee1de044a2a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 13:34:18 +0200 Subject: [PATCH 06/22] :zap: Refactor state management of workspace header --- frontend/src/app/main/data/modal.cljs | 4 +- .../src/app/main/ui/workspace/header.cljs | 870 +++++++++++------- .../app/main/ui/workspace/sidebar/assets.cljs | 5 +- 3 files changed, 524 insertions(+), 355 deletions(-) diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index 2156acbfde..fe7055297e 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -60,8 +60,8 @@ (c/update ::modal merge options))))) (defn show! - [type props] - (st/emit! (show type props))) + ([props] (st/emit! (show props))) + ([type props] (st/emit! (show type props)))) (defn update-props! [type props] diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 8d6113c443..644e2840b5 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -37,7 +37,7 @@ [potok.core :as ptk] [rumext.v2 :as mf])) -(def workspace-persistence-ref +(def ref:workspace-persistence (l/derived :workspace-persistence st/state)) ;; --- Persistence state Widget @@ -45,57 +45,71 @@ (mf/defc persistence-state-widget {::mf/wrap [mf/memo]} [] - (let [data (mf/deref workspace-persistence-ref)] + (let [{:keys [status]} (mf/deref ref:workspace-persistence)] [:div.persistence-status-widget - (cond - (= :pending (:status data)) + (case status + :pending [:div.pending [:span.label (tr "workspace.header.unsaved")]] - (= :saving (:status data)) + :saving [:div.saving [:span.icon i/toggle] [:span.label (tr "workspace.header.saving")]] - (= :saved (:status data)) + :saved [:div.saved [:span.icon i/tick] [:span.label (tr "workspace.header.saved")]] - (= :error (:status data)) + :error [:div.error {:title "There was an error saving the data. Please refresh if this persists."} [:span.icon i/msg-warning] - [:span.label (tr "workspace.header.save-error")]])])) + [:span.label (tr "workspace.header.save-error")]] + + nil)])) ;; --- Zoom Widget (mf/defc zoom-widget-workspace - {::mf/wrap [mf/memo]} - [{:keys [zoom - on-increase - on-decrease - on-zoom-reset - on-zoom-fit - on-zoom-selected] - :as props}] - (let [show-dropdown? (mf/use-state false)] - [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} - [:span.label (fmt/format-percent zoom {:precision 0})] + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}] + (let [open* (mf/use-state false) + open? (deref open*) + + open-dropdown + (mf/use-fn #(reset! open* true)) + + close-dropdown + (mf/use-fn #(reset! open* false)) + + on-increase + (mf/use-fn + (mf/deps on-increase) + (fn [event] + (dom/stop-propagation event) + (on-increase))) + + on-decrease + (mf/use-fn + (mf/deps on-decrease) + (fn [event] + (dom/stop-propagation event) + (on-decrease))) + + zoom (fmt/format-percent zoom {:precision 0})] + + [:div.zoom-widget {:on-click open-dropdown} + [:span.label zoom] [:span.icon i/arrow-down] - [:& dropdown {:show @show-dropdown? - :on-close #(reset! show-dropdown? false)} + [:& dropdown {:show open? :on-close close-dropdown} [:ul.dropdown [:li.basic-zoom-bar [:span.zoom-btns - [:button {:on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (on-decrease))} "-"] - [:p.zoom-size {} (fmt/format-percent zoom {:precision 0})] - [:button {:on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (on-increase))} "+"]] + [:button {:on-click on-decrease} "-"] + [:p.zoom-size zoom] + [:button {:on-click on-increase} "+"]] [:button.reset-btn {:on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]] [:li.separator] [:li {:on-click on-zoom-fit} @@ -103,86 +117,290 @@ [:li {:on-click on-zoom-selected} (tr "workspace.header.zoom-selected") [:span (sc/get-tooltip :zoom-selected)]]]]])) - - ;; --- Header Users -;; FIXME: refactor & optimizations -(mf/defc menu - [{:keys [layout project file team-id] :as props}] - (let [show-menu? (mf/use-state false) - show-sub-menu? (mf/use-state false) - editing? (mf/use-state false) - edit-input-ref (mf/use-ref nil) - objects (mf/deref refs/workspace-page-objects) - frames (->> (cph/get-immediate-children objects uuid/zero) - (filterv cph/frame-shape?)) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) +(mf/defc help-info-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout on-close]}] + (let [nav-to-helpc-center + (mf/use-fn #(dom/open-new-window "https://help.penpot.app")) + + nav-to-community + (mf/use-fn #(dom/open-new-window "https://community.penpot.app")) + + nav-to-youtube + (mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot")) + + nav-to-templates + (mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates")) + + nav-to-github + (mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot")) + + nav-to-terms + (mf/use-fn #(dom/open-new-window "https://penpot.app/terms")) + + nav-to-feedback + (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) + + show-shortcuts + (mf/use-fn + (mf/deps layout) + (fn [] + (when (contains? layout :collapse-left-sidebar) + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + + (st/emit! + (-> (dw/toggle-layout-flag :shortcuts) + (vary-meta assoc ::ev/origin "workspace-header"))))) + + show-release-notes + (mf/use-fn + (fn [event] + (let [version (:main @cf/version)] + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + (if (and (kbd/alt? event) (kbd/mod? event)) + (st/emit! (modal/show {:type :onboarding})) + (st/emit! (modal/show {:type :release-notes :version version})))))) + + ] + + [:& dropdown {:show true :on-close on-close} + [:ul.sub-menu.help-info + [:li {:on-click nav-to-helpc-center} + [:span (tr "labels.help-center")]] + [:li {:on-click nav-to-community} + [:span (tr "labels.community")]] + [:li {:on-click nav-to-youtube} + [:span (tr "labels.tutorials")]] + [:li {:on-click show-release-notes} + [:span (tr "labels.release-notes")]] + [:li.separator {:on-click nav-to-templates} + [:span (tr "labels.libraries-and-templates")]] + [:li {:on-click nav-to-github} + [:span (tr "labels.github-repo")]] + [:li {:on-click nav-to-terms} + [:span (tr "auth.terms-of-service")]] + [:li.separator {:on-click show-shortcuts} + [:span (tr "label.shortcuts")] + [:span.shortcut (sc/get-tooltip :show-shortcuts)]] + + (when (contains? @cf/flags :user-feedback) + [:* + [:li.feedback {:on-click nav-to-feedback} + [:span (tr "labels.give-feedback")]]])]])) + +(mf/defc preferences-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout toggle-flag on-close]}] + (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] + + [:& dropdown {:show true :on-close on-close} + [:ul.sub-menu.preferences + [:li {:on-click toggle-flag + :data-flag "scale-text"} + [:span + (if (contains? layout :scale-text) + (tr "workspace.header.menu.disable-scale-content") + (tr "workspace.header.menu.enable-scale-content"))] + [:span.shortcut (sc/get-tooltip :toggle-scale-text)]] + + [:li {:on-click toggle-flag + :data-flag "snap-guides"} + [:span + (if (contains? layout :snap-guides) + (tr "workspace.header.menu.disable-snap-guides") + (tr "workspace.header.menu.enable-snap-guides"))] + [:span.shortcut (sc/get-tooltip :toggle-snap-guide)]] + + [:li {:on-click toggle-flag + :data-flag "snap-grid"} + [:span + (if (contains? layout :snap-grid) + (tr "workspace.header.menu.disable-snap-grid") + (tr "workspace.header.menu.enable-snap-grid"))] + [:span.shortcut (sc/get-tooltip :toggle-snap-grid)]] + + [:li {:on-click toggle-flag + :data-flag "dynamic-alignment"} + [:span + (if (contains? layout :dynamic-alignment) + (tr "workspace.header.menu.disable-dynamic-alignment") + (tr "workspace.header.menu.enable-dynamic-alignment"))] + [:span.shortcut (sc/get-tooltip :toggle-alignment)]] + + [:li {:on-click toggle-flag + :data-flag "snap-pixel-grid"} + [:span + (if (contains? layout :snap-pixel-grid) + (tr "workspace.header.menu.disable-snap-pixel-grid") + (tr "workspace.header.menu.enable-snap-pixel-grid"))] + [:span.shortcut (sc/get-tooltip :snap-pixel-grid)]] + + [:li {:on-click show-nudge-options} + [:span (tr "modals.nudge-title")]]]])) + +(mf/defc view-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout toggle-flag on-close]}] + (let [read-only? (mf/use-ctx ctx/workspace-read-only?) + + toggle-color-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :textpalette) + (-> (dw/toggle-layout-flag :colorpalette) + (vary-meta assoc ::ev/origin "workspace-menu"))))) + + toggle-text-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :colorpalette) + (-> (dw/toggle-layout-flag :textpalette) + (vary-meta assoc ::ev/origin "workspace-menu")))))] + + [:& dropdown {:show true :on-close on-close} + [:ul.sub-menu.view + [:li {:on-click toggle-flag + :data-flag "rules"} + [:span + (if (contains? layout :rules) + (tr "workspace.header.menu.hide-rules") + (tr "workspace.header.menu.show-rules"))] + [:span.shortcut (sc/get-tooltip :toggle-rules)]] + + [:li {:on-click toggle-flag + :data-flag "display-grid"} + [:span + (if (contains? layout :display-grid) + (tr "workspace.header.menu.hide-grid") + (tr "workspace.header.menu.show-grid"))] + [:span.shortcut (sc/get-tooltip :toggle-grid)]] + + (when-not ^boolean read-only? + [:* + [:li {:on-click toggle-color-palette} + [:span + (if (contains? layout :colorpalette) + (tr "workspace.header.menu.hide-palette") + (tr "workspace.header.menu.show-palette"))] + [:span.shortcut (sc/get-tooltip :toggle-colorpalette)]] + + [:li {:on-click toggle-text-palette} + [:span + (if (contains? layout :textpalette) + (tr "workspace.header.menu.hide-textpalette") + (tr "workspace.header.menu.show-textpalette"))] + [:span.shortcut (sc/get-tooltip :toggle-textpalette)]]]) + + [:li {:on-click toggle-flag + :data-flag "display-artboard-names"} + [:span + (if (contains? layout :display-artboard-names) + (tr "workspace.header.menu.hide-artboard-names") + (tr "workspace.header.menu.show-artboard-names"))]] + + [:li {:on-click toggle-flag + :data-flag "show-pixel-grid"} + [:span + (if (contains? layout :show-pixel-grid) + (tr "workspace.header.menu.hide-pixel-grid") + (tr "workspace.header.menu.show-pixel-grid"))] + [:span.shortcut (sc/get-tooltip :show-pixel-grid)]] + + [:li {:on-click toggle-flag + :data-flag "hide-ui"} + [:span + (tr "workspace.shape.menu.hide-ui")] + [:span.shortcut (sc/get-tooltip :hide-ui)]]]])) + +(mf/defc edit-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [on-close]}] + (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) + undo (mf/use-fn #(st/emit! dwc/undo)) + redo (mf/use-fn #(st/emit! dwc/redo))] + [:& dropdown {:show true :on-close on-close} + [:ul.sub-menu.edit + [:li {:on-click select-all} + [:span (tr "workspace.header.menu.select-all")] + [:span.shortcut (sc/get-tooltip :select-all)]] + + [:li {:on-click undo} + [:span (tr "workspace.header.menu.undo")] + [:span.shortcut (sc/get-tooltip :undo)]] + + [:li {:on-click redo} + [:span (tr "workspace.header.menu.redo")] + [:span.shortcut (sc/get-tooltip :redo)]]]])) + +(mf/defc file-menu + {::mf/wrap-props false} + [{:keys [on-close file team-id]}] + (let [file-id (:id file) + file-name (:name file) + shared? (:is-shared file) + + objects (mf/deref refs/workspace-page-objects) + frames (->> (cph/get-immediate-children objects uuid/zero) + (filterv cph/frame-shape?)) add-shared-fn - #(st/emit! (dwl/set-file-shared (:id file) true)) + (mf/use-fn + (mf/deps file-id) + #(st/emit! (dwl/set-file-shared file-id true))) on-add-shared (mf/use-fn - (mf/deps file) - #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.add-shared-confirm.message" (:name file)) - :hint (tr "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared-fn}))) + (mf/deps file-name add-shared-fn) + #(modal/show! {:type :confirm + :message "" + :title (tr "modals.add-shared-confirm.message" file-name) + :hint (tr "modals.add-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.add-shared-confirm.accept") + :accept-style :primary + :on-accept add-shared-fn})) on-remove-shared (mf/use-fn - (mf/deps file) + (mf/deps file-id) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (modal/show - {:type :delete-shared-libraries - :origin :unpublish - :ids #{(:id file)} - :on-accept #(st/emit! (dwl/set-file-shared (:id file) false)) - :count-libraries 1})))) - - handle-blur - (fn [_] - (let [value (str/trim (-> edit-input-ref mf/ref-val dom/get-value))] - (when (not= value "") - (st/emit! (dw/rename-file (:id file) value)))) - (reset! editing? false)) - - handle-name-keydown (fn [event] - (when (kbd/enter? event) - (handle-blur event))) - start-editing-name (fn [event] - (dom/prevent-default event) - (reset! editing? true)) + (modal/show! + {:type :delete-shared-libraries + :origin :unpublish + :ids #{file-id} + :on-accept #(st/emit! (dwl/set-file-shared file-id false)) + :count-libraries 1}))) on-export-shapes - (mf/use-callback - (fn [_] - (st/emit! (de/show-workspace-export-dialog)))) + (mf/use-fn #(st/emit! (de/show-workspace-export-dialog))) on-export-file - (fn [event-name binary?] - (st/emit! (ptk/event ::ev/event {::ev/name event-name - ::ev/origin "workspace" - :num-files 1})) + (mf/use-fn + (mf/deps file) + (fn [event-name binary?] + (st/emit! (ptk/event ::ev/event {::ev/name event-name + ::ev/origin "workspace" + :num-files 1})) - (->> (rx/of file) - (rx/flat-map - (fn [file] - (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) - (rx/map #(assoc file :has-libraries? %))))) - (rx/reduce conj []) - (rx/subs - (fn [files] - (st/emit! - (modal/show + (->> (rx/of file) + (rx/flat-map + (fn [file] + (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) + (rx/map #(assoc file :has-libraries? %))))) + (rx/reduce conj []) + (rx/subs + (fn [files] + (modal/show! {:type :export :team-id team-id :has-libraries? (->> files (some :has-libraries?)) @@ -190,297 +408,248 @@ :binary? binary?})))))) on-export-binary-file - (mf/use-callback - (mf/deps file team-id) - (fn [_] - (on-export-file "export-binary-files" true))) + (mf/use-fn + (mf/deps on-export-file) + (partial on-export-file "export-binary-files" true)) on-export-standard-file - (mf/use-callback - (mf/deps file team-id) - (fn [_] - (on-export-file "export-standard-files" false))) + (mf/use-fn + (mf/deps on-export-file) + (partial on-export-file "export-standard-files" false)) on-export-frames - (mf/use-callback - (mf/deps file frames) + (mf/use-fn + (mf/deps frames) (fn [_] - (st/emit! (de/show-workspace-export-frames-dialog (reverse frames))))) + (st/emit! (de/show-workspace-export-frames-dialog (reverse frames)))))] - on-item-hover - (mf/use-callback - (fn [item] - (fn [event] - (dom/stop-propagation event) - (reset! show-sub-menu? item)))) + [:& dropdown {:show true :on-close on-close} + [:ul.sub-menu.file + (if ^boolean shared? + [:li {:on-click on-remove-shared} + [:span (tr "dashboard.unpublish-shared")]] + [:li {:on-click on-add-shared} + [:span (tr "dashboard.add-shared")]]) + [:li.export-file {:on-click on-export-shapes} + [:span (tr "dashboard.export-shapes")] + [:span.shortcut (sc/get-tooltip :export-shapes)]] + [:li.separator.export-file {:on-click on-export-binary-file} + [:span (tr "dashboard.download-binary-file")]] + [:li.export-file {:on-click on-export-standard-file} + [:span (tr "dashboard.download-standard-file")]] + (when (seq frames) + [:li.separator.export-file {:on-click on-export-frames} + [:span (tr "dashboard.export-frames")]])]])) - on-item-click - (mf/use-callback - (fn [item] - (fn [event] - (dom/stop-propagation event) - (reset! show-sub-menu? item)))) +(mf/defc menu + {::mf/wrap-props false} + [{:keys [layout file team-id]}] + (let [show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + sub-menu* (mf/use-state false) + sub-menu (deref sub-menu*) + + open-menu (mf/use-fn #(reset! show-menu* true)) + close-menu (mf/use-fn #(reset! show-menu* false)) + close-sub-menu (mf/use-fn #(reset! sub-menu* nil)) + + on-menu-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [menu (-> (dom/get-target event) + (dom/get-data "menu") + (keyword))] + (reset! sub-menu* menu)))) toggle-flag - (mf/use-callback - (fn [flag] - (-> (dw/toggle-layout-flag flag) - (vary-meta assoc ::ev/origin "workspace-menu")))) - - show-release-notes - (mf/use-callback + (mf/use-fn (fn [event] - (let [version (:main @cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) - (if (and (kbd/alt? event) (kbd/mod? event)) - (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] + (let [flag (-> (dom/get-target event) + (dom/get-data :flag) + (keyword))] + (st/emit! + (-> (dw/toggle-layout-flag flag) + (vary-meta assoc ::ev/origin "workspace-menu"))))))] - (mf/use-effect - (mf/deps @editing?) - #(when @editing? - (dom/select-text! (mf/ref-val edit-input-ref)))) - [:div.menu-section - [:div.btn-icon-dark.btn-small {:on-click #(reset! show-menu? true)} i/actions] - [:div.project-tree {:alt (tr "workspace.sitemap")} - [:span.project-name - {:on-click #(st/emit! (rt/nav-new-window* {:rname :dashboard-files - :path-params {:team-id team-id - :project-id (:project-id file)}}))} - (:name project) " /"] - (if @editing? - [:input.file-name - {:type "text" - :ref edit-input-ref - :on-blur handle-blur - :on-key-down handle-name-keydown - :auto-focus true - :default-value (:name file "")}] - [:span - {:on-double-click start-editing-name} - (:name file)])] - (when (:is-shared file) - [:div.shared-badge i/library]) + [:* + [:div.btn-icon-dark.btn-small {:on-click open-menu} i/actions] - [:& dropdown {:show @show-menu? - :on-close #(reset! show-menu? false)} + [:& dropdown {:show show-menu? :on-close close-menu} [:ul.menu - [:li {:on-click (on-item-click :file) - :on-pointer-enter (on-item-hover :file)} + [:li {:on-click on-menu-click + :on-pointer-enter on-menu-click + :data-menu "file"} [:span (tr "workspace.header.menu.option.file")] [:span i/arrow-slide]] - [:li {:on-click (on-item-click :edit) - :on-pointer-enter (on-item-hover :edit)} - [:span (tr "workspace.header.menu.option.edit")] [:span i/arrow-slide]] - [:li {:on-click (on-item-click :view) - :on-pointer-enter (on-item-hover :view)} - [:span (tr "workspace.header.menu.option.view")] [:span i/arrow-slide]] - [:li {:on-click (on-item-click :preferences) - :on-pointer-enter (on-item-hover :preferences)} - [:span (tr "workspace.header.menu.option.preferences")] [:span i/arrow-slide]] - [:li.info {:on-click (on-item-click :help-info) - :on-pointer-enter (on-item-hover :help-info)} - [:span (tr "workspace.header.menu.option.help-info")] [:span i/arrow-slide]]]] + [:li {:on-click on-menu-click + :on-pointer-enter on-menu-click + :data-menu "edit"} + [:span (tr "workspace.header.menu.option.edit")] + [:span i/arrow-slide]] + [:li {:on-click on-menu-click + :on-pointer-enter on-menu-click + :data-menu :view} + [:span (tr "workspace.header.menu.option.view")] + [:span i/arrow-slide]] + [:li {:on-click on-menu-click + :on-pointer-enter on-menu-click + :data-menu "preferences"} + [:span (tr "workspace.header.menu.option.preferences")] + [:span i/arrow-slide]] + [:li.info {:on-click on-menu-click + :on-pointer-enter on-menu-click + :data-menu "help-info"} + [:span (tr "workspace.header.menu.option.help-info")] + [:span i/arrow-slide]]]] - [:& dropdown {:show (= @show-sub-menu? :file) - :on-close #(reset! show-sub-menu? false)} - [:ul.sub-menu.file - (if (:is-shared file) - [:li {:on-click on-remove-shared} - [:span (tr "dashboard.unpublish-shared")]] - [:li {:on-click on-add-shared} - [:span (tr "dashboard.add-shared")]]) - [:li.export-file {:on-click on-export-shapes} - [:span (tr "dashboard.export-shapes")] - [:span.shortcut (sc/get-tooltip :export-shapes)]] - [:li.separator.export-file {:on-click on-export-binary-file} - [:span (tr "dashboard.download-binary-file")]] - [:li.export-file {:on-click on-export-standard-file} - [:span (tr "dashboard.download-standard-file")]] - (when (seq frames) - [:li.separator.export-file {:on-click on-export-frames} - [:span (tr "dashboard.export-frames")]])]] + (case sub-menu + :file + [:& file-menu + {:file file + :team-id team-id + :on-close close-sub-menu}] - [:& dropdown {:show (= @show-sub-menu? :edit) - :on-close #(reset! show-sub-menu? false)} - [:ul.sub-menu.edit - [:li {:on-click #(st/emit! (dw/select-all))} - [:span (tr "workspace.header.menu.select-all")] - [:span.shortcut (sc/get-tooltip :select-all)]] + :edit + [:& edit-menu + {:on-close close-sub-menu}] - [:li {:on-click #(st/emit! dwc/undo)} - [:span (tr "workspace.header.menu.undo")] - [:span.shortcut (sc/get-tooltip :undo)]] + :view + [:& view-menu + {:layout layout + :toggle-flag toggle-flag + :on-close close-sub-menu}] - [:li {:on-click #(st/emit! dwc/redo)} - [:span (tr "workspace.header.menu.redo")] - [:span.shortcut (sc/get-tooltip :redo)]]]] + :preferences + [:& preferences-menu + {:layout layout + :toggle-flag toggle-flag + :on-close close-sub-menu}] - [:& dropdown {:show (= @show-sub-menu? :view) - :on-close #(reset! show-sub-menu? false)} - [:ul.sub-menu.view - [:li {:on-click #(st/emit! (toggle-flag :rules))} - [:span - (if (contains? layout :rules) - (tr "workspace.header.menu.hide-rules") - (tr "workspace.header.menu.show-rules"))] - [:span.shortcut (sc/get-tooltip :toggle-rules)]] + :help-info + [:& help-info-menu + {:layout layout + :on-close close-sub-menu}] - [:li {:on-click #(st/emit! (toggle-flag :display-grid))} - [:span - (if (contains? layout :display-grid) - (tr "workspace.header.menu.hide-grid") - (tr "workspace.header.menu.show-grid"))] - [:span.shortcut (sc/get-tooltip :toggle-grid)]] - - (when-not workspace-read-only? - [:* - [:li {:on-click (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :textpalette) - (toggle-flag :colorpalette)))} - [:span - (if (contains? layout :colorpalette) - (tr "workspace.header.menu.hide-palette") - (tr "workspace.header.menu.show-palette"))] - [:span.shortcut (sc/get-tooltip :toggle-colorpalette)]] - - [:li {:on-click (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :colorpalette) - (toggle-flag :textpalette)))} - [:span - (if (contains? layout :textpalette) - (tr "workspace.header.menu.hide-textpalette") - (tr "workspace.header.menu.show-textpalette"))] - [:span.shortcut (sc/get-tooltip :toggle-textpalette)]]]) - - [:li {:on-click #(st/emit! (toggle-flag :display-artboard-names))} - [:span - (if (contains? layout :display-artboard-names) - (tr "workspace.header.menu.hide-artboard-names") - (tr "workspace.header.menu.show-artboard-names"))]] - - [:li {:on-click #(st/emit! (toggle-flag :show-pixel-grid))} - [:span - (if (contains? layout :show-pixel-grid) - (tr "workspace.header.menu.hide-pixel-grid") - (tr "workspace.header.menu.show-pixel-grid"))] - [:span.shortcut (sc/get-tooltip :show-pixel-grid)]] - - [:li {:on-click #(st/emit! (-> (toggle-flag :hide-ui) - (vary-meta assoc ::ev/origin "workspace-menu")))} - [:span - (tr "workspace.shape.menu.hide-ui")] - [:span.shortcut (sc/get-tooltip :hide-ui)]]]] - - [:& dropdown {:show (= @show-sub-menu? :preferences) - :on-close #(reset! show-sub-menu? false)} - [:ul.sub-menu.preferences - [:li {:on-click #(st/emit! (toggle-flag :scale-text))} - [:span - (if (contains? layout :scale-text) - (tr "workspace.header.menu.disable-scale-content") - (tr "workspace.header.menu.enable-scale-content"))] - [:span.shortcut (sc/get-tooltip :toggle-scale-text)]] - - [:li {:on-click #(st/emit! (toggle-flag :snap-guides))} - [:span - (if (contains? layout :snap-guides) - (tr "workspace.header.menu.disable-snap-guides") - (tr "workspace.header.menu.enable-snap-guides"))] - [:span.shortcut (sc/get-tooltip :toggle-snap-guide)]] - - [:li {:on-click #(st/emit! (toggle-flag :snap-grid))} - [:span - (if (contains? layout :snap-grid) - (tr "workspace.header.menu.disable-snap-grid") - (tr "workspace.header.menu.enable-snap-grid"))] - [:span.shortcut (sc/get-tooltip :toggle-snap-grid)]] - - [:li {:on-click #(st/emit! (toggle-flag :dynamic-alignment))} - [:span - (if (contains? layout :dynamic-alignment) - (tr "workspace.header.menu.disable-dynamic-alignment") - (tr "workspace.header.menu.enable-dynamic-alignment"))] - [:span.shortcut (sc/get-tooltip :toggle-alignment)]] - - [:li {:on-click #(st/emit! (toggle-flag :snap-pixel-grid))} - [:span - (if (contains? layout :snap-pixel-grid) - (tr "workspace.header.menu.disable-snap-pixel-grid") - (tr "workspace.header.menu.enable-snap-pixel-grid"))] - [:span.shortcut (sc/get-tooltip :snap-pixel-grid)]] - - [:li {:on-click #(st/emit! (modal/show {:type :nudge-option}))} - [:span (tr "modals.nudge-title")]]]] - - [:& dropdown {:show (= @show-sub-menu? :help-info) - :on-close #(reset! show-sub-menu? false)} - [:ul.sub-menu.help-info - [:li {:on-click #(dom/open-new-window "https://help.penpot.app")} - [:span (tr "labels.help-center")]] - [:li {:on-click #(dom/open-new-window "https://community.penpot.app")} - [:span (tr "labels.community")]] - [:li {:on-click #(dom/open-new-window "https://www.youtube.com/c/Penpot")} - [:span (tr "labels.tutorials")]] - [:li {:on-click show-release-notes} - [:span (tr "labels.release-notes")]] - [:li.separator {:on-click #(dom/open-new-window "https://penpot.app/libraries-templates")} - [:span (tr "labels.libraries-and-templates")]] - [:li {:on-click #(dom/open-new-window "https://github.com/penpot/penpot")} - [:span (tr "labels.github-repo")]] - [:li {:on-click #(dom/open-new-window "https://penpot.app/terms")} - [:span (tr "auth.terms-of-service")]] - [:li.separator {:on-click #(st/emit! (when (contains? layout :collapse-left-sidebar) (dw/toggle-layout-flag :collapse-left-sidebar)) - (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "workspace-header")))} - [:span (tr "label.shortcuts")] - [:span.shortcut (sc/get-tooltip :show-shortcuts)]] - - (when (contains? @cf/flags :user-feedback) - [:* - [:li.feedback {:on-click #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))} - [:span (tr "labels.give-feedback")]]])]]])) + nil)])) ;; --- Header Component (mf/defc header - [{:keys [file layout project page-id] :as props}] - (let [team-id (:team-id project) - zoom (mf/deref refs/selected-zoom) - params {:page-id page-id :file-id (:id file) :section "interactions"} - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + {::mf/wrap-props false} + [{:keys [file layout project page-id]}] + (let [file-id (:id file) + file-name (:name file) + project-id (:id project) + team-id (:team-id project) + shared? (:is-shared file) + + zoom (mf/deref refs/selected-zoom) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil))) + on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil))) + on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom)) + on-zoom-fit (mf/use-fn #(st/emit! dw/zoom-to-fit-all)) + on-zoom-selected (mf/use-fn #(st/emit! dw/zoom-to-selected-shape)) + + + editing* (mf/use-state false) + editing? (deref editing*) + + input-ref (mf/use-ref nil) + + handle-blur + (mf/use-fn + (mf/deps file-id) + (fn [_] + (let [value (str/trim (-> input-ref mf/ref-val dom/get-value))] + (when (not= value "") + (st/emit! (dw/rename-file file-id value))) + (reset! editing* false)))) + + handle-name-keydown + (mf/use-fn + (mf/deps handle-blur) + (fn [event] + (when (kbd/enter? event) + (handle-blur event)))) + + start-editing-name + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (reset! editing* true))) close-modals - (mf/use-callback - (fn [] - (st/emit! (dc/stop-picker)) - (st/emit! (modal/hide!)))) + (mf/use-fn + #(st/emit! (dc/stop-picker) + (modal/hide))) go-back - (mf/use-callback + (mf/use-fn (mf/deps project) (fn [] (close-modals) (st/emit! (dw/go-to-dashboard project)))) - go-viewer - (mf/use-callback - (mf/deps file page-id) - #(st/emit! (dw/go-to-viewer params)))] + nav-to-viewer + (mf/use-fn + (mf/deps file-id page-id) + (fn [] + (let [params {:page-id page-id + :file-id file-id + :section "interactions"}] + (st/emit! (dw/go-to-viewer params))))) + + nav-to-project + (mf/use-fn + (mf/deps team-id project-id) + #(st/emit! (rt/nav-new-window* {:rname :dashboard-files + :path-params {:team-id team-id + :project-id (:project-id project-id)}}))) + + toggle-history + (mf/use-fn + #(st/emit! (-> (dw/toggle-layout-flag :document-history) + (vary-meta assoc ::ev/origin "workspace-header"))))] + + (mf/with-effect [editing?] + (when ^boolean editing? + (dom/select-text! (mf/ref-val input-ref)))) [:header.workspace-header [:div.left-area [:div.main-icon [:a {:on-click go-back} i/logo-icon]] - [:& menu {:layout layout - :project project - :file file - :team-id team-id - :page-id page-id}]] + [:div.menu-section + [:& menu {:layout layout + :file file + :read-only? read-only? + :team-id team-id + :page-id page-id}] + + [:div.project-tree {:alt (tr "workspace.sitemap")} + [:span.project-name + {:on-click nav-to-project} + (:name project) " /"] + + (if ^boolean editing? + [:input.file-name + {:type "text" + :ref input-ref + :on-blur handle-blur + :on-key-down handle-name-keydown + :auto-focus true + :default-value (:name file "")}] + [:span + {:on-double-click start-editing-name} + file-name])] + + (when ^boolean shared? + [:div.shared-badge i/library])]] [:div.center-area [:div.users-section @@ -490,26 +659,25 @@ [:div.options-section [:& persistence-state-widget] [:& export-progress-widget] - (when-not workspace-read-only? + (when-not ^boolean read-only? [:button.document-history {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) :aria-label (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) :class (when (contains? layout :document-history) "selected") - :on-click #(st/emit! (-> (dw/toggle-layout-flag :document-history) - (vary-meta assoc ::ev/origin "workspace-header")))} + :on-click toggle-history} i/recent])] [:div.options-section [:& zoom-widget-workspace {:zoom zoom - :on-increase #(st/emit! (dw/increase-zoom nil)) - :on-decrease #(st/emit! (dw/decrease-zoom nil)) - :on-zoom-reset #(st/emit! dw/reset-zoom) - :on-zoom-fit #(st/emit! dw/zoom-to-fit-all) - :on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}] + :on-increase on-increase + :on-decrease on-decrease + :on-zoom-reset on-zoom-reset + :on-zoom-fit on-zoom-fit + :on-zoom-selected on-zoom-selected}] [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) - :on-click go-viewer} + :on-click nav-to-viewer} i/play]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 0626dd0ac0..f48010aea9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -303,6 +303,7 @@ :options options}]) (mf/defc asset-section + {::mf/wrap-props false} [{:keys [children file-id title section assets-count open?]}] (let [children (->> (if (array? children) children [children]) (filter some?)) @@ -310,12 +311,12 @@ title-buttons (filter #(= (get-role %) :title-button) children) content (filter #(= (get-role %) :content) children)] [:div.asset-section - [:div.asset-title {:class (when (not open?) "closed")} + [:div.asset-title {:class (when (not ^boolean open?) "closed")} [:span {:on-click #(st/emit! (dwl/set-assets-section-open file-id section (not open?)))} i/arrow-slide title] [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space title-buttons] - (when open? + (when ^boolean open? content)])) (mf/defc asset-section-block From ca439cf6044d254d36985bda3f374d5a0cc97556 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 14:11:23 +0200 Subject: [PATCH 07/22] :zap: Add minor performance improvements to workspace main components --- frontend/src/app/main/ui/workspace.cljs | 77 ++++++++++++------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 07bb322f5f..7312b1679c 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -5,7 +5,6 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace - (:import goog.events.EventType) (:require [app.common.data.macros :as dm] [app.main.data.messages :as msg] @@ -101,22 +100,26 @@ :selected selected :layout layout}]])])) -(def trimmed-page-ref (l/derived :workspace-trimmed-page st/state =)) +(def ^:private ref:page-loaded + (l/derived + (fn [state] + (some? (:workspace-trimmed-page state))) + st/state)) (mf/defc workspace-page - [{:keys [file layout page-id wglobal] :as props}] + {::mf/wrap-props false} + [{:keys [file layout page-id wglobal]}] + (let [prev-page-id (hooks/use-previous page-id) + page-loaded? (mf/deref ref:page-loaded)] - (let [prev-page-id (hooks/use-previous page-id)] - (mf/with-effect - [page-id] + (mf/with-effect [page-id prev-page-id] (when (and prev-page-id (not= prev-page-id page-id)) (st/emit! (dw/finalize-page prev-page-id))) - (if (nil? page-id) (st/emit! (dw/go-to-page)) (st/emit! (dw/initialize-page page-id)))) - (when (mf/deref trimmed-page-ref) + (when ^boolean page-loaded? [:& workspace-content {:page-id page-id :file file :wglobal wglobal @@ -128,31 +131,28 @@ i/loader-pencil]) (mf/defc workspace - {::mf/wrap [mf/memo]} - [{:keys [project-id file-id page-id layout-name] :as props}] - (let [file (mf/deref refs/workspace-file) - project (mf/deref refs/workspace-project) - layout (mf/deref refs/workspace-layout) - wglobal (mf/deref refs/workspace-global) - ready? (mf/deref refs/workspace-ready?) - workspace-read-only? (mf/deref refs/workspace-read-only?) + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [project-id file-id page-id layout-name]}] + (let [file (mf/deref refs/workspace-file) + project (mf/deref refs/workspace-project) + layout (mf/deref refs/workspace-layout) + wglobal (mf/deref refs/workspace-global) + ready? (mf/deref refs/workspace-ready?) + read-only? (mf/deref refs/workspace-read-only?) - components-v2 (features/use-feature :components-v2) - new-css-system (features/use-feature :new-css-system) + team-id (:team-id project) + file-name (:name file) - background-color (:background-color wglobal) + components-v2 (features/use-feature :components-v2) + new-css-system (features/use-feature :new-css-system) - focus-out - (mf/use-callback - (fn [] - (st/emit! (dw/workspace-focus-lost))))] + background-color (:background-color wglobal)] - (mf/use-effect - (mf/deps focus-out) - (fn [] - (let [keys [(events/listen globals/window EventType.BLUR focus-out)]] - #(doseq [key keys] - (events/unlistenByKey key))))) + (mf/with-effect [] + (let [focus-out #(st/emit! (dw/workspace-focus-lost)) + key (events/listen globals/document "blur" focus-out)] + (partial events/unlistenByKey key))) ;; Setting the layout preset by its name (mf/with-effect [layout-name] @@ -164,22 +164,21 @@ (st/emit! ::dwp/force-persist (dw/finalize-file project-id file-id)))) - ;; Set html theme color and close any non-modal dialog that may be still open - (mf/with-effect + (mf/with-effect [] (st/emit! msg/hide)) ;; Set properly the page title - (mf/with-effect [(:name file)] - (when (:name file) - (dom/set-html-title (tr "title.workspace" (:name file))))) + (mf/with-effect [file-name] + (when file-name + (dom/set-html-title (tr "title.workspace" file-name)))) - [:& (mf/provider ctx/current-file-id) {:value (:id file)} - [:& (mf/provider ctx/current-team-id) {:value (:team-id project)} - [:& (mf/provider ctx/current-project-id) {:value (:id project)} + [:& (mf/provider ctx/current-file-id) {:value file-id} + [:& (mf/provider ctx/current-team-id) {:value team-id} + [:& (mf/provider ctx/current-project-id) {:value project-id} [:& (mf/provider ctx/current-page-id) {:value page-id} [:& (mf/provider ctx/components-v2) {:value components-v2} [:& (mf/provider ctx/new-css-system) {:value new-css-system} - [:& (mf/provider ctx/workspace-read-only?) {:value workspace-read-only?} + [:& (mf/provider ctx/workspace-read-only?) {:value read-only?} [:section#workspace {:style {:background-color background-color :touch-action "none"}} (when (not (:hide-ui layout)) @@ -190,7 +189,7 @@ [:& context-menu] - (if ready? + (if ^boolean ready? [:& workspace-page {:page-id page-id :file file :wglobal wglobal From 5d8562e07204a76999eb30b3b7a6f8a5f8d5e4b7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 15:44:20 +0200 Subject: [PATCH 08/22] :sparkles: Fix react warnings on workspace shortcuts panel --- .../src/app/main/ui/workspace/sidebar/shortcuts.cljs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index 1cbec51175..7e17c0f7dd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.shortcuts (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.config :as cf] [app.main.data.dashboard.shortcuts] [app.main.data.events :as ev] @@ -213,7 +214,7 @@ [{:keys [content command] :as props}] (let [managed-list (if (coll? content) content - (conj () content)) + (conj () content)) chars-list (map ds/split-sc managed-list) last-element (last chars-list) short-char-list (if (= 1 (count chars-list)) @@ -224,13 +225,16 @@ (for [chars short-char-list] [:* (for [char chars] - [:& converted-chars {:char char :command command}]) + [:& converted-chars {:key (dm/str char "-" (name command)) + :char char + :command command}]) (when (not= chars penultimate) [:span.space ","])]) (when (not= last-element penultimate) [:* [:span.space (tr "shortcuts.or")] (for [char last-element] - [:& converted-chars {:char char + [:& converted-chars {:key (dm/str char "-" (name command)) + :char char :command command}])])])) (mf/defc shortcut-row @@ -463,7 +467,7 @@ (when (kbd/enter? event) (on-search-clear-click) (dom/focus! (dom/get-element "shortcut-search")))))] - + (mf/with-effect [] (dom/focus! (dom/get-element "shortcut-search"))) From 02fbce13f02764cc68d67d31ca424417571e07d6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 May 2023 15:54:41 +0200 Subject: [PATCH 09/22] :zap: Add minor performance improvements to workspace left toolbar --- .../app/main/ui/workspace/left_toolbar.cljs | 157 +++++++++++------- 1 file changed, 100 insertions(+), 57 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index cb8c18a59c..6728f863ba 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -20,30 +20,28 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] [app.util.timers :as ts] [rumext.v2 :as mf])) - (mf/defc image-upload {::mf/wrap [mf/memo]} [] - (let [ref (mf/use-ref nil) - file (mf/deref refs/workspace-file) + (let [ref (mf/use-ref nil) + file-id (mf/use-ctx ctx/current-file-id) on-click - (mf/use-callback #(dom/click (mf/ref-val ref))) + (mf/use-fn #(dom/click (mf/ref-val ref))) - on-files-selected - (mf/use-callback - (mf/deps file) + on-selected + (mf/use-fn + (mf/deps file-id) (fn [blobs] ;; We don't want to add a ref because that redraws the component ;; for everychange. Better direct access on the callback. (let [vbox (deref refs/vbox) x (+ (:x vbox) (/ (:width vbox) 2)) y (+ (:y vbox) (/ (:height vbox) 2)) - params {:file-id (:id file) + params {:file-id file-id :blobs (seq blobs) :position (gpt/point x y)}] (st/emit! (dwm/upload-media-workspace params)))))] @@ -53,26 +51,84 @@ {:title (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) :aria-label (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) :on-click on-click} - [:* - i/image - [:& file-uploader {:input-id "image-upload" - :accept cm/str-image-types - :multi true - :ref ref - :on-selected on-files-selected}]]]])) + i/image + [:& file-uploader + {:input-id "image-upload" + :accept cm/str-image-types + :multi true + :ref ref + :on-selected on-selected}]]])) (mf/defc left-toolbar {::mf/wrap [mf/memo] ::mf/wrap-props false} - [props] - (let [layout (obj/get props "layout") - selected-drawtool (mf/deref refs/selected-drawing-tool) - select-drawtool #(st/emit! :interrupt (dw/select-for-drawing %)) + [{:keys [layout]}] + (let [selected-drawtool (mf/deref refs/selected-drawing-tool) edition (mf/deref refs/selected-edition) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - new-css-system (mf/use-ctx ctx/new-css-system) - show-palette-btn? (and (not workspace-read-only?) (not new-css-system)) + + new-css? (mf/use-ctx ctx/new-css-system) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + show-palette-btn? (and (not ^boolean read-only?) (not ^boolean new-css?)) + + + interrupt + (mf/use-fn #(st/emit! :interrupt)) + + select-drawtool + (mf/use-fn + (fn [event] + (let [tool (-> (dom/get-current-target event) + (dom/get-data "tool") + (keyword))] + (st/emit! :interrupt (dw/select-for-drawing tool))))) + + toggle-text-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (-> (dom/get-element-by-class "color-palette") + (dom/add-class! "fade-out-down")) + (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :colorpalette) + (-> (dw/toggle-layout-flag :textpalette) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) + + toggle-color-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (-> (dom/get-element-by-class "color-palette") + (dom/add-class! "fade-out-down")) + (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) + (-> (dw/toggle-layout-flag :colorpalette) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) + + toggle-shortcuts + (mf/use-fn + (mf/deps layout) + (fn [] + (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] + (when is-sidebar-closed? + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + (st/emit! + (dw/remove-layout-flag :debug-panel) + (-> (dw/toggle-layout-flag :shortcuts) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) + + toggle-debug-panel + (mf/use-fn + (mf/deps layout) + (fn [] + (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] + (when is-sidebar-closed? + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + (st/emit! + (dw/remove-layout-flag :shortcuts) + (-> (dw/toggle-layout-flag :debug-panel) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) + ] + [:aside.left-toolbar [:ul.left-toolbar-options [:li @@ -81,16 +137,17 @@ :aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move)) :class (when (and (nil? selected-drawtool) (not edition)) "selected") - :on-click #(st/emit! :interrupt)} + :on-click interrupt} i/pointer-inner]] - (when-not workspace-read-only? + (when-not ^boolean read-only? [:* [:li [:button {:title (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) :aria-label (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) :class (when (= selected-drawtool :frame) "selected") - :on-click (partial select-drawtool :frame) + :on-click select-drawtool + :data-tool "frame" :data-test "artboard-btn"} i/artboard]] [:li @@ -98,7 +155,8 @@ {:title (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) :aria-label (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) :class (when (= selected-drawtool :rect) "selected") - :on-click (partial select-drawtool :rect) + :on-click select-drawtool + :data-tool "rect" :data-test "rect-btn"} i/box]] [:li @@ -106,7 +164,8 @@ {:title (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) :aria-label (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) :class (when (= selected-drawtool :circle) "selected") - :on-click (partial select-drawtool :circle) + :on-click select-drawtool + :data-tool "circle" :data-test "ellipse-btn"} i/circle]] [:li @@ -114,7 +173,8 @@ {:title (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) :aria-label (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) :class (when (= selected-drawtool :text) "selected") - :on-click (partial select-drawtool :text)} + :on-click select-drawtool + :data-tool "text"} i/text]] [:& image-upload] @@ -124,7 +184,8 @@ {:title (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) :aria-label (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) :class (when (= selected-drawtool :curve) "selected") - :on-click (partial select-drawtool :curve) + :on-click select-drawtool + :data-tool "curve" :data-test "curve-btn"} i/pencil]] [:li @@ -132,7 +193,8 @@ {:title (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) :aria-label (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) :class (when (= selected-drawtool :path) "selected") - :on-click (partial select-drawtool :path) + :on-click select-drawtool + :data-tool "path" :data-test "path-btn"} i/pen]]]) @@ -141,23 +203,19 @@ {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) :aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) :class (when (= selected-drawtool :comments) "selected") - :on-click (partial select-drawtool :comments)} + :on-click select-drawtool + :data-tool "comments"} i/chat]]] [:ul.left-toolbar-options.panels - (when show-palette-btn? + (when ^boolean show-palette-btn? [:* [:li [:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) :aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) :class (when (contains? layout :textpalette) "selected") - :on-click (fn [] - (r/set-resize-type! :bottom) - (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :colorpalette) - (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))} + :on-click toggle-text-palette} "Ag"]] [:li @@ -165,34 +223,19 @@ {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) :aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) :class (when (contains? layout :colorpalette) "selected") - :on-click (fn [] - (r/set-resize-type! :bottom) - (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))} + :on-click toggle-color-palette} i/palette]]]) [:li [:button {:title (tr "workspace.toolbar.shortcuts" (sc/get-tooltip :show-shortcuts)) :aria-label (tr "workspace.toolbar.shortcuts" (sc/get-tooltip :show-shortcuts)) :class (when (contains? layout :shortcuts) "selected") - :on-click (fn [] - (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] - (ts/schedule 300 #(st/emit! (when is-sidebar-closed? (dw/toggle-layout-flag :collapse-left-sidebar)) - (dw/remove-layout-flag :debug-panel) - (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "workspace-left-toolbar"))))))} + :on-click toggle-shortcuts} i/shortcut] (when *assert* [:button {:title "Debugging tool" :class (when (contains? layout :debug-panel) "selected") - :on-click (fn [] - (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] - (ts/schedule 300 #(st/emit! (when is-sidebar-closed? (dw/toggle-layout-flag :collapse-left-sidebar)) - (dw/remove-layout-flag :shortcuts) - (-> (dw/toggle-layout-flag :debug-panel) - (vary-meta assoc ::ev/origin "workspace-left-toolbar"))))))} + :on-click toggle-debug-panel} i/bug])]]])) From 390f2b35fcd7fc59b05225b302358aac016b6755 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 18 May 2023 17:03:08 +0200 Subject: [PATCH 10/22] :bug: Ensure verify! works as expected on production builds --- common/src/app/common/data/macros.cljc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 5ee0151106..836656d938 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -153,6 +153,22 @@ (throw (ex-info hint# params#))))))))) (defmacro verify! - [& params] - (binding [*assert* true] - `(assert! ~@params))) + ([expr] + `(assert! nil ~expr)) + ([hint expr] + (let [hint (cond + (vector? hint) + `(str/ffmt ~@hint) + + (some? hint) + hint + + :else + (str "expr assert: " (pr-str expr)))] + `(binding [*assert-context* true] + (when-not ~expr + (let [hint# ~hint + params# {:type :assertion + :code :expr-validation + :hint hint#}] + (throw (ex-info hint# params#)))))))) From 8ca6055935e68e99c78f381e1ccc17b36772a58d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 18 May 2023 17:03:44 +0200 Subject: [PATCH 11/22] :bug: Fix backend shape validation after changes apply --- common/src/app/common/pages/changes.cljc | 39 +++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index c42c3a0a23..85a118acf9 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -240,25 +240,28 @@ ;; Changes Processing Impl (defn validate-shapes! - [data objects items] - (letfn [(validate-shape! [[page-id {:keys [id] :as shape}]] - (when-not (= shape (dm/get-in data [:pages-index page-id :objects id])) - ;; If object has changed verify is correct - (dm/verify! (cts/shape? shape))))] + [data-old data-new items] + (letfn [(validate-shape! [[page-id id]] + (let [shape-old (dm/get-in data-old [:pages-index page-id :objects id]) + shape-new (dm/get-in data-new [:pages-index page-id :objects id])] - (let [lookup (d/getf objects)] - (->> (into #{} (map :page-id) items) - (mapcat (fn [page-id] - (filter #(= page-id (:page-id %)) items))) - (mapcat (fn [{:keys [type id page-id] :as item}] - (sequence - (comp (keep lookup) - (map (partial vector page-id))) - (case type - (:add-obj :mod-obj :del-obj) (cons id nil) - (:mov-objects :reg-objects) (:shapes item) - nil)))) - (run! validate-shape!))))) + ;; If object has changed verify is correct + (when (and (some? shape-old) + (some? shape-new) + (not= shape-old shape-new)) + (dm/verify! (cts/shape? shape-new)))))] + + (->> (into #{} (map :page-id) items) + (mapcat (fn [page-id] + (filter #(= page-id (:page-id %)) items))) + (mapcat (fn [{:keys [type id page-id] :as item}] + (sequence + (map (partial vector page-id)) + (case type + (:add-obj :mod-obj :del-obj) (cons id nil) + (:mov-objects :reg-objects) (:shapes item) + nil)))) + (run! validate-shape!)))) (defmulti process-change (fn [_ change] (:type change))) (defmulti process-operation (fn [_ _ op] (:type op))) From ff00043811043114cce29ccfcc8f94468a7d3f1b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 18 May 2023 17:04:21 +0200 Subject: [PATCH 12/22] :sparkles: Improve workspace initialization flow --- frontend/src/app/main/data/workspace.cljs | 175 ++++++++---------- frontend/src/app/main/refs.cljs | 6 +- frontend/src/app/main/ui/workspace.cljs | 3 +- .../app/main/ui/workspace/sidebar/assets.cljs | 72 ++++--- 4 files changed, 114 insertions(+), 142 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 166fb86de6..64435d5b66 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -142,116 +142,86 @@ (let [data (d/removem (comp t/pointer? val) data)] (assoc state :workspace-data data))))) -(defn- workspace-data-pointers-loaded - [pdata] - (ptk/reify ::workspace-data-pointers-loaded - ptk/UpdateEvent - (update [_ state] - (update state :workspace-data merge pdata)))) +(defn- resolve-file-data + [file-id {:keys [pages-index] :as data}] + (letfn [(resolve-pointer [[key val :as kv]] + (if (t/pointer? val) + (->> (rp/cmd! :get-file-fragment {:file-id file-id :fragment-id @val}) + (rx/map #(get % :content)) + (rx/map #(vector key %))) + (rx/of kv))) + + (resolve-pointers [coll] + (->> (rx/from (seq coll)) + (rx/merge-map resolve-pointer) + (rx/reduce conj {})))] + + (->> (rx/zip (resolve-pointers data) + (resolve-pointers pages-index)) + (rx/take 1) + (rx/map (fn [[data pages-index]] + (assoc data :pages-index pages-index)))))) (defn- bundle-fetched [features [{:keys [id data] :as file} thumbnails project users comments-users]] - (letfn [(resolve-pointer [file-id [key pointer]] - (->> (rp/cmd! :get-file-fragment {:file-id file-id :fragment-id @pointer}) - (rx/map :content) - (rx/map #(vector key %)))) + (ptk/reify ::bundle-fetched + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc :workspace-thumbnails thumbnails) + (assoc :workspace-file (dissoc file :data)) + (assoc :workspace-project project) + (assoc :current-team-id (:team-id project)) + (assoc :users (d/index-by :id users)) + (assoc :current-file-comments-users (d/index-by :id comments-users)))) - (resolve-pointers [file-id coll] - (->> (rx/from (seq coll)) - (rx/merge-map (partial resolve-pointer file-id)) - (rx/reduce conj {})))] + ptk/WatchEvent + (watch [_ _ stream] + (let [team-id (:team-id project) + stoper (rx/filter (ptk/type? ::bundle-fetched) stream)] + (->> (rx/concat + ;; Initialize notifications + (rx/of (dwn/initialize team-id id) + (dwsl/initialize)) - (ptk/reify ::bundle-fetched - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc :workspace-thumbnails thumbnails) - (assoc :workspace-file (dissoc file :data)) - (assoc :workspace-project project) - (assoc :current-team-id (:team-id project)) - (assoc :users (d/index-by :id users)) - (assoc :current-file-comments-users (d/index-by :id comments-users)))) + ;; Load team fonts. We must ensure custom fonts are + ;; fully loadad before mark workspace as initialized + (rx/merge + (->> stream + (rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded)) + (rx/take 1) + (rx/ignore)) - ptk/WatchEvent - (watch [_ _ stream] - (let [team-id (:team-id project) - stoper (rx/filter (ptk/type? ::bundle-fetched) stream)] - (->> (rx/concat - ;; Initialize notifications - (rx/of (dwn/initialize team-id id) - (dwsl/initialize)) + (rx/of (df/load-team-fonts team-id)) - ;; Load team fonts. We must ensure custom fonts are fully loadad - ;; before starting the workspace load. - (rx/merge - (rx/of (df/load-team-fonts team-id)) - (->> stream - (rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded)) - (rx/take 1) - (rx/ignore))) + ;; Load main file + (->> (resolve-file-data id 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)) - (rx/merge - ;; Load all pages, independently if they are pointers or already - ;; resolved values. - (->> (rx/from (seq (:pages-index data))) - (rx/merge-map - (fn [[_ page :as kp]] - (if (t/pointer? page) - (resolve-pointer id kp) - (rx/of kp)))) - (rx/merge-map - (fn [[id page]] - (let [page (update page :objects ctst/start-page-index)] - (->> (uw/ask! {:cmd :initialize-page-index :page page}) - (rx/map (constantly [id page])))))) - (rx/reduce conj {}) - (rx/map (fn [pages-index] - (-> data - (assoc :pages-index pages-index) - (workspace-data-loaded))))) - - ;; Once workspace data is loaded, proceed asynchronously load - ;; the local library and all referenced libraries, without - ;; blocking the main workspace load process. - (->> stream - (rx/filter (ptk/type? ::workspace-data-loaded)) - (rx/take 1) - (rx/merge-map - (fn [_] - (rx/merge - (->> data - (filter (comp t/pointer? val)) - (resolve-pointers id) - (rx/map workspace-data-pointers-loaded)) - - (->> (rp/cmd! :get-file-libraries {:file-id id}) - (rx/mapcat identity) - (rx/merge-map - (fn [{:keys [id]}] - (rp/cmd! :get-file {:id id :features features}))) - (rx/merge-map - (fn [{:keys [id data] :as file}] - (->> (filter (comp t/pointer? val) data) - (resolve-pointers id) - (rx/map #(update file :data merge %))))) - (rx/merge-map - (fn [{:keys [id data] :as file}] - ;; Resolve all pages of each library, if needed - (->> (rx/from (seq (:pages-index data))) - (rx/merge-map - (fn [[_ page :as kp]] - (if (t/pointer? page) - (resolve-pointer id kp) - (rx/of kp)))) - (rx/reduce conj {}) - (rx/map - (fn [pages-index] - (assoc-in file [:data :pages-index] pages-index)))))) - (rx/reduce conj []) - (rx/map libraries-fetched))))))) - - (rx/of (workspace-initialized))) - (rx/take-until stoper))))))) + ;; Load libraries + (->> (rp/cmd! :get-file-libraries {:file-id id}) + (rx/mapcat identity) + (rx/merge-map + (fn [{:keys [id]}] + (rp/cmd! :get-file {:id id :features features}))) + (rx/merge-map + (fn [{:keys [id data] :as file}] + (->> (resolve-file-data id data) + (rx/map (fn [data] (assoc file :data data)))))) + (rx/reduce conj []) + (rx/map libraries-fetched))) + (rx/of (workspace-initialized))) + (rx/take-until stoper)))))) (defn- libraries-fetched [libraries] @@ -329,6 +299,7 @@ ptk/UpdateEvent (update [_ state] (assoc state + :workspace-ready? false :current-file-id file-id :current-project-id project-id :workspace-presence {})) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index cb4c4f5037..6d2b12cf48 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -105,7 +105,11 @@ (l/derived :workspace-drawing st/state)) (def workspace-ready? - (l/derived :workspace-ready? st/state)) + (l/derived (fn [state] + (and (:workspace-ready? state) + (:current-file-id state) + (:current-project-id state))) + st/state)) ;; TODO: rename to workspace-selected (?) ;; Don't use directly from components, this is a proxy to improve performance of selected-shapes diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 7312b1679c..dcb117441c 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -131,8 +131,7 @@ i/loader-pencil]) (mf/defc workspace - {::mf/wrap [mf/memo] - ::mf/wrap-props false} + {::mf/wrap-props false} [{:keys [project-id file-id page-id layout-name]}] (let [file (mf/deref refs/workspace-file) project (mf/deref refs/workspace-project) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index f48010aea9..b38078721b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -39,7 +39,6 @@ [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.perf :as perf] [app.util.router :as rt] [app.util.strings :refer [matches-search]] [app.util.timers :as ts] @@ -2490,44 +2489,43 @@ show-libraries-dialog (mf/use-fn #(modal/show! :libraries-dialog {}))] - [:& perf/profiler {:enabled true :label "sidebar/assets"} - [:div.assets-bar - [:div.tool-window - [:div.tool-window-content - [:div.assets-bar-title - (tr "workspace.assets.assets") + [:div.assets-bar + [:div.tool-window + [:div.tool-window-content + [:div.assets-bar-title + (tr "workspace.assets.assets") - (when-not ^boolean read-only? - [:div.libraries-button {:on-click show-libraries-dialog} - i/text-align-justify - (tr "workspace.assets.libraries")])] + (when-not ^boolean read-only? + [:div.libraries-button {:on-click show-libraries-dialog} + i/text-align-justify + (tr "workspace.assets.libraries")])] - [:div.search-block - [:input.search-input - {:placeholder (tr "workspace.assets.search") - :type "text" - :value (:term filters) - :on-change on-search-term-change - :on-key-down handle-key-down}] + [:div.search-block + [:input.search-input + {:placeholder (tr "workspace.assets.search") + :type "text" + :value (:term filters) + :on-change on-search-term-change + :on-key-down handle-key-down}] - (if ^boolean (str/empty? (:term filters)) - [:div.search-icon - i/search] - [:div.search-icon.close - {:on-click on-search-clear-click} - i/close])] + (if ^boolean (str/empty? (:term filters)) + [:div.search-icon + i/search] + [:div.search-icon.close + {:on-click on-search-clear-click} + i/close])] - [:select.input-select {:value (:section filters) - :on-change on-section-filter-change} - [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] - [:option {:value ":components"} (tr "workspace.assets.components")] - [:option {:value ":graphics"} (tr "workspace.assets.graphics")] - [:option {:value ":colors"} (tr "workspace.assets.colors")] - [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] + [:select.input-select {:value (:section filters) + :on-change on-section-filter-change} + [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] + [:option {:value ":components"} (tr "workspace.assets.components")] + [:option {:value ":graphics"} (tr "workspace.assets.graphics")] + [:option {:value ":colors"} (tr "workspace.assets.colors")] + [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] - [:& (mf/provider ctx:filters) {:value filters} - [:& (mf/provider ctx:toggle-ordering) {:value toggle-ordering} - [:& (mf/provider ctx:toggle-list-style) {:value toggle-list-style} - [:div.libraries-wrapper - [:& assets-local-library {:filters filters}] - [:& assets-libraries {:filters filters}]]]]]]])) + [:& (mf/provider ctx:filters) {:value filters} + [:& (mf/provider ctx:toggle-ordering) {:value toggle-ordering} + [:& (mf/provider ctx:toggle-list-style) {:value toggle-list-style} + [:div.libraries-wrapper + [:& assets-local-library {:filters filters}] + [:& assets-libraries {:filters filters}]]]]]])) From 1d4bd34dfc43b956d373929fa3ef8024baf061af Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 May 2023 19:37:53 +0200 Subject: [PATCH 13/22] :sparkles: Move fressian to common module --- backend/deps.edn | 1 - backend/dev/user.clj | 26 +++++++++---------- backend/src/app/rpc/commands/binfile.clj | 22 ++++++++-------- backend/src/app/util/blob.clj | 4 +-- backend/src/app/util/objects_map.clj | 3 +-- backend/src/app/util/pointer_map.clj | 2 +- .../backend_tests/util_objects_map_test.clj | 2 +- .../backend_tests/util_pointer_map_test.clj | 4 +-- common/deps.edn | 7 ++--- .../src/app/common}/fressian.clj | 2 +- 10 files changed, 36 insertions(+), 37 deletions(-) rename {backend/src/app/util => common/src/app/common}/fressian.clj (99%) diff --git a/backend/deps.edn b/backend/deps.edn index c93092fd4b..cd5d6eb2e6 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -7,7 +7,6 @@ org.clojure/core.async {:mvn/version "1.6.673"} com.github.luben/zstd-jni {:mvn/version "1.5.2-5"} - org.clojure/data.fressian {:mvn/version "1.0.0"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} diff --git a/backend/dev/user.clj b/backend/dev/user.clj index d86d4fc073..1b02c6ea00 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -8,30 +8,23 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.fressian :as fres] [app.common.geom.matrix :as gmt] [app.common.logging :as l] [app.common.perf :as perf] [app.common.pprint :as pp] + [app.common.schema :as sm] + [app.common.schema.desc-js-like :as smdj] + [app.common.schema.desc-native :as smdn] + [app.common.schema.generators :as sg] [app.common.spec :as us] [app.common.transit :as t] [app.common.uuid :as uuid] - [app.common.schema :as sm] - [app.common.schema.generators :as sg] - [app.common.schema.desc-native :as smdn] - [app.common.schema.desc-js-like :as smdj] [app.config :as cfg] [app.main :as main] - [malli.core :as m] - [malli.error :as me] - [malli.dev.pretty :as mdp] - [malli.transform :as mt] - [malli.util :as mu] - [malli.registry :as mr] - [malli.generator :as mg] [app.srepl.helpers] [app.srepl.main :as srepl] [app.util.blob :as blob] - [app.util.fressian :as fres] [app.util.json :as json] [app.util.time :as dt] [clj-async-profiler.core :as prof] @@ -48,7 +41,14 @@ [criterium.core :as crit] [cuerdas.core :as str] [datoteka.core] - [integrant.core :as ig])) + [integrant.core :as ig] + [malli.core :as m] + [malli.dev.pretty :as mdp] + [malli.error :as me] + [malli.generator :as mg] + [malli.registry :as mr] + [malli.transform :as mt] + [malli.util :as mu])) (repl/disable-reload! (find-ns 'integrant.core)) (set! *warn-on-reflection* true) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 8b90957569..c962bc2f53 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -8,8 +8,10 @@ (:refer-clojure :exclude [assert]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.features :as ffeat] + [app.common.fressian :as fres] [app.common.logging :as l] [app.common.pages.migrations :as pmg] [app.common.spec :as us] @@ -28,7 +30,6 @@ [app.storage.tmp :as tmp] [app.tasks.file-gc] [app.util.blob :as blob] - [app.util.fressian :as fres] [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -45,8 +46,7 @@ java.io.DataInputStream java.io.DataOutputStream java.io.InputStream - java.io.OutputStream - java.lang.AutoCloseable)) + java.io.OutputStream)) (set! *warn-on-reflection* true) @@ -296,7 +296,7 @@ (defn- retrieve-file [pool file-id] - (with-open [^AutoCloseable conn (db/open pool)] + (dm/with-open [conn (db/open pool)] (binding [pmap/*load-fn* (partial files/load-pointer conn file-id)] (some-> (db/get* conn :file {:id file-id}) (files/decode-row) @@ -307,7 +307,7 @@ (defn- retrieve-file-media [pool {:keys [data id] :as file}] - (with-open [^AutoCloseable conn (db/open pool)] + (dm/with-open [conn (db/open pool)] (let [ids (app.tasks.file-gc/collect-used-media data) ids (db/create-array conn "uuid" ids)] @@ -341,7 +341,7 @@ (defn- retrieve-libraries [pool ids] - (with-open [^AutoCloseable conn (db/open pool)] + (dm/with-open [conn (db/open pool)] (let [ids (db/create-array conn "uuid" ids)] (map :id (db/exec! pool [sql:file-libraries ids]))))) @@ -351,7 +351,7 @@ (defn- retrieve-library-relations [pool ids] - (with-open [^AutoCloseable conn (db/open pool)] + (dm/with-open [conn (db/open pool)] (db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)]))) (defn- create-or-update-file @@ -616,7 +616,7 @@ (-> data (update :pages-index update-vals #(update % :objects omap-wrap)) (update :pages-index update-vals pmap-wrap) - (update :components update-vals #(update % :objects omap-wrap)) + (update :components update-vals #(d/update-when % :objects omap-wrap)) (update :components pmap-wrap)))) (defmethod read-section :v1/files @@ -834,7 +834,7 @@ cs (volatile! nil)] (try (l/info :hint "start exportation" :export-id id) - (with-open [^AutoCloseable output (io/output-stream output)] + (dm/with-open [output (io/output-stream output)] (binding [*position* (atom 0)] (write-export! (assoc cfg ::output output)))) @@ -857,7 +857,7 @@ (defn export-to-tmpfile! [cfg] (let [path (tmp/tempfile :prefix "penpot.export.")] - (with-open [^AutoCloseable output (io/output-stream path)] + (dm/with-open [output (io/output-stream path)] (export! cfg output) path))) @@ -869,7 +869,7 @@ (l/info :hint "import: started" :import-id id) (try (binding [*position* (atom 0)] - (with-open [^AutoCloseable input (io/input-stream input)] + (dm/with-open [input (io/input-stream input)] (read-import! (assoc cfg ::input input)))) (catch Throwable cause diff --git a/backend/src/app/util/blob.clj b/backend/src/app/util/blob.clj index 9fe030e748..6263e8e878 100644 --- a/backend/src/app/util/blob.clj +++ b/backend/src/app/util/blob.clj @@ -8,9 +8,9 @@ "A generic blob storage encoding. Mainly used for page data, page options and txlog payload storage." (:require + [app.common.fressian :as fres] [app.common.transit :as t] - [app.config :as cf] - [app.util.fressian :as fres]) + [app.config :as cf]) (:import com.github.luben.zstd.Zstd java.io.ByteArrayInputStream diff --git a/backend/src/app/util/objects_map.clj b/backend/src/app/util/objects_map.clj index 8d053a6938..eecd395e30 100644 --- a/backend/src/app/util/objects_map.clj +++ b/backend/src/app/util/objects_map.clj @@ -16,10 +16,9 @@ properly from each value." (:require - ;; [app.common.logging :as l] + [app.common.fressian :as fres] [app.common.transit :as t] [app.common.uuid :as uuid] - [app.util.fressian :as fres] [clojure.core :as c]) (:import clojure.lang.Counted diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index 70da3dc8a2..cfe79cd3a5 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -36,10 +36,10 @@ " (:require + [app.common.fressian :as fres] [app.common.logging :as l] [app.common.transit :as t] [app.common.uuid :as uuid] - [app.util.fressian :as fres] [app.util.time :as dt] [clojure.core :as c]) (:import diff --git a/backend/test/backend_tests/util_objects_map_test.clj b/backend/test/backend_tests/util_objects_map_test.clj index 7d75191e7f..b85c841c60 100644 --- a/backend/test/backend_tests/util_objects_map_test.clj +++ b/backend/test/backend_tests/util_objects_map_test.clj @@ -6,11 +6,11 @@ (ns backend-tests.util-objects-map-test (:require + [app.common.fressian :as fres] [app.common.schema.generators :as sg] [app.common.transit :as transit] [app.common.types.shape :as cts] [app.common.uuid :as uuid] - [app.util.fressian :as fres] [app.util.objects-map :as omap] [backend-tests.helpers :as th] [clojure.pprint :refer [pprint]] diff --git a/backend/test/backend_tests/util_pointer_map_test.clj b/backend/test/backend_tests/util_pointer_map_test.clj index f496a57386..85715dc957 100644 --- a/backend/test/backend_tests/util_pointer_map_test.clj +++ b/backend/test/backend_tests/util_pointer_map_test.clj @@ -6,13 +6,13 @@ (ns backend-tests.util-pointer-map-test (:require - [backend-tests.helpers :as th] + [app.common.fressian :as fres] [app.common.spec :as us] [app.common.transit :as transit] [app.common.types.shape :as cts] [app.common.uuid :as uuid] - [app.util.fressian :as fres] [app.util.pointer-map :as pmap] + [backend-tests.helpers :as th] [clojure.pprint :refer [pprint]] [clojure.spec.alpha :as s] [clojure.test :as t] diff --git a/common/deps.edn b/common/deps.edn index e70f6c4546..5ff183c57a 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -2,9 +2,9 @@ {org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/data.json {:mvn/version "2.4.0"} org.clojure/tools.cli {:mvn/version "1.0.214"} - metosin/jsonista {:mvn/version "0.3.7"} org.clojure/clojurescript {:mvn/version "1.11.60"} org.clojure/test.check {:mvn/version "1.1.1"} + org.clojure/data.fressian {:mvn/version "1.0.0"} ;; Logging org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"} @@ -18,6 +18,7 @@ selmer/selmer {:mvn/version "1.12.55"} criterium/criterium {:mvn/version "0.4.6"} + metosin/jsonista {:mvn/version "0.3.7"} metosin/malli {:mvn/version "0.11.0"} expound/expound {:mvn/version "0.9.0"} @@ -30,14 +31,14 @@ {:git/tag "11.0-alpha13" :git/sha "f6cab38" :git/url "https://github.com/funcool/promesa.git"} + funcool/datoteka {:mvn/version "3.0.66" + :exclusions [funcool/promesa]} lambdaisland/uri {:mvn/version "1.13.95" :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} - funcool/datoteka {:mvn/version "3.0.66" - :exclusions [funcool/promesa]} com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} org.la4j/la4j {:mvn/version "0.6.0"} diff --git a/backend/src/app/util/fressian.clj b/common/src/app/common/fressian.clj similarity index 99% rename from backend/src/app/util/fressian.clj rename to common/src/app/common/fressian.clj index 82e364ccb0..1b78fbcb81 100644 --- a/backend/src/app/util/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.util.fressian +(ns app.common.fressian (:require [app.common.data :as d] [app.common.geom.matrix :as gmt] From 0078c0e6011055f46e455f662e6be82cdcb43ae0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 May 2023 19:39:15 +0200 Subject: [PATCH 14/22] :bug: Fix missing pointer persistence on file gc task --- backend/src/app/tasks/file_gc.clj | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 34a394f453..9eceac13d2 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -275,7 +275,8 @@ [{:keys [::db/conn] :as cfg} {:keys [id data revn modified-at features] :as file}] (l/debug :hint "processing file" :id id :modified-at modified-at) - (binding [pmap/*load-fn* (partial files/load-pointer conn id)] + (binding [pmap/*load-fn* (partial files/load-pointer conn id) + pmap/*tracked* (atom {})] (let [data (-> (blob/decode data) (assoc :id id) (pmg/migrate-data))] @@ -291,4 +292,6 @@ ;; Mark file as trimmed (db/update! conn :file {:has-media-trimmed true} - {:id id})))) + {:id id}) + + (files/persist-pointers! conn id)))) From 68c0b0e8a70af99c5a65de6b934fa5db49c4f4ac Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 May 2023 19:50:45 +0200 Subject: [PATCH 15/22] :zap: Add minor perf improvement on components-v2 migration --- common/src/app/common/types/file.cljc | 146 +++++++++++++------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index ffffa3d794..4ca0da8243 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -326,88 +326,88 @@ main instances for all components there and remove shapes from library components. Mark the file with the :components-v2 option." [file-data] - (let [components (ctkl/components-seq file-data)] - (if (or (empty? components) - (dm/get-in file-data [:options :components-v2])) - (assoc-in file-data [:options :components-v2] true) - (let [grid-gap 50 + (let [migrated? (dm/get-in file-data [:options :components-v2])] + (if migrated? + file-data + (let [components (ctkl/components-seq file-data)] + (if (empty? components) + (assoc-in file-data [:options :components-v2] true) + (let [grid-gap 50 + [file-data page-id start-pos] + (get-or-add-library-page file-data grid-gap) - [file-data page-id start-pos] - (get-or-add-library-page file-data grid-gap) + add-main-instance + (fn [file-data component position] + (let [page (ctpl/get-page file-data page-id) - add-main-instance - (fn [file-data component position] - (let [page (ctpl/get-page file-data page-id) + [new-shape new-shapes] + (ctn/make-component-instance page + component + file-data + position + false + {:main-instance? true + :force-frame-id uuid/zero}) - [new-shape new-shapes] - (ctn/make-component-instance page - component - file-data - position - false - {:main-instance? true - :force-frame-id uuid/zero}) + add-shapes + (fn [page] + (reduce (fn [page shape] + (ctst/add-shape (:id shape) + shape + page + (:frame-id shape) + (:parent-id shape) + nil ; <- As shapes are ordered, we can safely add each + true)) ; one at the end of the parent's children list. + page + new-shapes)) - add-shapes - (fn [page] - (reduce (fn [page shape] - (ctst/add-shape (:id shape) - shape - page - (:frame-id shape) - (:parent-id shape) - nil ; <- As shapes are ordered, we can safely add each - true)) ; one at the end of the parent's children list. - page - new-shapes)) + update-component + (fn [component] + (-> component + (assoc :main-instance-id (:id new-shape) + :main-instance-page page-id) + (dissoc :objects)))] - update-component - (fn [component] - (-> component - (assoc :main-instance-id (:id new-shape) - :main-instance-page page-id) - (dissoc :objects)))] + (-> file-data + (ctpl/update-page page-id add-shapes) + (ctkl/update-component (:id component) update-component)))) - (-> file-data - (ctpl/update-page page-id add-shapes) - (ctkl/update-component (:id component) update-component)))) + add-instance-grid + (fn [file-data components] + (let [position-seq (ctst/generate-shape-grid + (map (partial get-component-root file-data) components) + start-pos + grid-gap)] + (loop [file-data file-data + components-seq (seq components) + position-seq position-seq] + (let [component (first components-seq) + position (first position-seq)] + (if (nil? component) + file-data + (recur (add-main-instance file-data component position) + (rest components-seq) + (rest position-seq))))))) - add-instance-grid - (fn [file-data components] - (let [position-seq (ctst/generate-shape-grid - (map (partial get-component-root file-data) components) - start-pos - grid-gap)] - (loop [file-data file-data - components-seq (seq components) - position-seq position-seq] - (let [component (first components-seq) - position (first position-seq)] - (if (nil? component) - file-data - (recur (add-main-instance file-data component position) - (rest components-seq) - (rest position-seq))))))) + root-to-board + (fn [shape] + (cond-> shape + (and (ctk/instance-root? shape) + (cph/frame-shape? shape)) + (assoc :fills [] + :hide-in-viewer true + :rx 0 + :ry 0))) - root-to-board - (fn [shape] - (cond-> shape - (and (ctk/instance-head? shape) - (not= (:type shape) :frame)) - (assoc :type :frame - :fills [] - :hide-in-viewer true - :rx 0 - :ry 0))) + roots-to-board + (fn [page] + (update page :objects update-vals root-to-board))] - roots-to-board - (fn [page] - (update page :objects update-vals root-to-board))] - - (-> file-data - (add-instance-grid (sort-by :name components)) - (update :pages-index update-vals roots-to-board) - (assoc-in [:options :components-v2] true)))))) + (-> file-data + (add-instance-grid (sort-by :name components)) + (update :pages-index update-vals roots-to-board) + (assoc-in [:options :components-v2] true)))))))) (defn- absorb-components [file-data used-components library-data] From 8f72faf27d9c6f9bafb0bd30cc5edcec3563d6fc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 23 May 2023 16:04:47 +0200 Subject: [PATCH 16/22] :bug: Fix issues on penpot file import and components-v2 --- backend/src/app/rpc/commands/files.clj | 3 ++- common/src/app/common/types/container.cljc | 4 ++-- common/src/app/common/types/shape/text.cljc | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 7e5a0ca85d..d80f9b2502 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -268,7 +268,8 @@ (binding [pmap/*tracked* (atom {})] (let [data (ctf/migrate-to-components-v2 data) features (conj features "components/v2") - modified-at (dt/now)] + modified-at (dt/now) + features (db/create-array conn "text" features)] (db/update! conn :file {:data (blob/encode data) :modified-at modified-at diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 56b67a500b..f61756da56 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -14,7 +14,6 @@ [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.pages-list :as ctpl] - [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid])) @@ -34,7 +33,7 @@ [:path {:optional true} [:maybe :string]] [:modified-at {:optional true} ::sm/inst] [:objects {:optional true} - [:map-of {:gen/max 10} ::sm/uuid ::cts/shape]]]) + [:map-of {:gen/max 10} ::sm/uuid :map]]]) (def container? (sm/pred-fn ::container)) @@ -68,6 +67,7 @@ (defn get-shape [container shape-id] + (dm/assert! "expected valid container" (container? container)) diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index 837b67ac9e..3eb0918f84 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -31,7 +31,8 @@ [:type [:= "paragraph"]] [:key {:optional true} :string] [:fills {:optional true} - [:vector {:gen/max 2} ::shape/fill]] + [:maybe + [:vector {:gen/max 2} ::shape/fill]]] [:font-family {:optional true} :string] [:font-size {:optional true} :string] [:font-style {:optional true} :string] From 0ea07fbe01d1e2e87db1895cf7ce9428557a4f75 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 May 2023 14:43:44 +0200 Subject: [PATCH 17/22] :recycle: Refactor selection management on workspace assets component --- frontend/src/app/main/data/workspace.cljs | 58 ++-- .../app/main/data/workspace/libraries.cljs | 14 - .../app/main/ui/workspace/sidebar/assets.cljs | 257 +++++++++--------- 3 files changed, 166 insertions(+), 163 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 64435d5b66..5c5bd34015 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1119,7 +1119,21 @@ :query-params {:page-id (dm/get-in file [:data :pages 0])}}] (rx/of (rt/nav-new-window* params))))))) -(defn check-in-asset +(defn set-assets-section-open + [file-id section open?] + (ptk/reify ::set-assets-section-open + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-assets :open-status file-id section] open?)))) + +(defn set-assets-group-open + [file-id section path open?] + (ptk/reify ::set-assets-group-open + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-assets :open-status file-id :groups section path] open?)))) + +(defn- check-in-asset [items element] (let [items (or items #{})] (if (contains? items element) @@ -1127,35 +1141,37 @@ (conj items element)))) (defn toggle-selected-assets - [asset type] + [file-id asset-id type] (ptk/reify ::toggle-selected-assets ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-assets-selected type] #(check-in-asset % asset))))) + (update-in state [:workspace-assets :selected file-id type] check-in-asset asset-id)))) (defn select-single-asset - [asset type] + [file-id asset-id type] (ptk/reify ::select-single-asset ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-assets-selected type] #{asset})))) + (prn "select-single-asset" file-id asset-id type) + (assoc-in state [:workspace-assets :selected file-id type] #{asset-id})))) (defn select-assets - [assets type] + [file-id assets-ids type] (ptk/reify ::select-assets ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-assets-selected type] (into #{} assets))))) + (assoc-in state [:workspace-assets :selected file-id type] (into #{} assets-ids))))) (defn unselect-all-assets - [] - (ptk/reify ::unselect-all-assets - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-assets-selected {:components #{} - :graphics #{} - :colors #{} - :typographies #{}})))) + ([] (unselect-all-assets nil)) + ([file-id] + (ptk/reify ::unselect-all-assets + ptk/UpdateEvent + (update [_ state] + (if file-id + (update-in state [:workspace-assets :selected] dissoc file-id) + (update state :workspace-assets dissoc :selected)))))) + (defn go-to-main-instance [page-id shape-id] (dm/assert! (uuid? page-id)) @@ -1200,9 +1216,9 @@ pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id :layout :assets}] (rx/of (rt/nav :workspace pparams qparams) - (dwl/set-assets-section-open file-id :library true) - (dwl/set-assets-section-open file-id :components true) - (select-single-asset component-id :components)))))) + (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 _] @@ -1222,9 +1238,9 @@ pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id :layout :assets}] (rx/of (rt/nav :workspace pparams qparams) - (dwl/set-assets-section-open file-id :library true) - (dwl/set-assets-section-open file-id :components true) - (select-single-asset component-id :components)))) + (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 [_ _ _] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 468b3a874b..5620783f3b 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -76,20 +76,6 @@ (declare sync-file) -(defn set-assets-section-open - [file-id section open?] - (ptk/reify ::set-assets-section-open - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-assets-open-status file-id section] open?)))) - -(defn set-assets-group-open - [file-id section path open?] - (ptk/reify ::set-assets-group-open - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-assets-open-status file-id :groups section path] open?)))) - (defn extract-path-if-missing [item] (let [[path name] (cph/parse-path-name (:name item))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index b38078721b..1a7a7e4cf9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -52,13 +52,14 @@ (def ctx:toggle-ordering (mf/create-context nil)) (def ctx:toggle-list-style (mf/create-context nil)) -(def ref:selected-assets - (l/derived :workspace-assets-selected st/state =)) +(def lens:selected + (-> (l/in [:workspace-assets :selected]) + (l/derived st/state))) -(def ref:open-status - (l/derived :workspace-assets-open-status st/state)) +(def lens:open-status + (l/derived (l/in [:workspace-assets :open-status]) st/state)) -(def ref:typography-section-state +(def lens:typography-section-state (l/derived (fn [gstate] {:rename-typography (:rename-typography gstate) :edit-typography (:edit-typography gstate)}) @@ -195,23 +196,23 @@ (st/emit! (dwu/commit-undo-transaction undo-id)))) (defn- on-drop-asset - [event asset dragging* selected-assets selected-assets-full selected-assets-paths rename] + [event asset dragging* selected selected-full selected-paths rename] (let [create-typed-assets-group (partial create-assets-group rename)] (when (not (dnd/from-child? event)) (reset! dragging* false) (when - (and (not (contains? selected-assets (:id asset))) - (every? #(= % (:path asset)) selected-assets-paths)) - (let [components-to-group (conj selected-assets-full asset) + (and (not (contains? selected (:id asset))) + (every? #(= % (:path asset)) selected-paths)) + (let [components-to-group (conj selected-full asset) create-typed-assets-group (partial create-typed-assets-group components-to-group)] (modal/show! :name-group-dialog {:accept create-typed-assets-group})))))) (defn- on-drag-enter-asset - [event asset dragging* selected-assets selected-assets-paths] + [event asset dragging* selected selected-paths] (when (and (not (dnd/from-child? event)) - (every? #(= % (:path asset)) selected-assets-paths) - (not (contains? selected-assets (:id asset)))) + (every? #(= % (:path asset)) selected-paths) + (not (contains? selected (:id asset)))) (reset! dragging* true))) (defn- on-drag-leave-asset @@ -241,32 +242,32 @@ (ts/raf #(.removeChild ^js item-el counter-el)))) (defn- on-asset-drag-start - [event asset selected-assets item-ref asset-type on-drag-start] - (let [id-asset (:id asset) - num-selected (if (contains? selected-assets id-asset) - (count selected-assets) + [event file-id asset selected item-ref asset-type on-drag-start] + (let [id-asset (:id asset) + num-selected (if (contains? selected id-asset) + (count selected) 1)] - (when (not (contains? selected-assets id-asset)) - (st/emit! (dw/unselect-all-assets) - (dw/toggle-selected-assets id-asset asset-type))) + (when (not (contains? selected id-asset)) + (st/emit! (dw/unselect-all-assets file-id) + (dw/toggle-selected-assets file-id id-asset asset-type))) (on-drag-start asset event) (when (> num-selected 1) (set-drag-image event item-ref num-selected)))) (defn- on-drag-enter-asset-group - [event dragging* prefix selected-assets-paths] + [event dragging* prefix selected-paths] (dom/stop-propagation event) (when (and (not (dnd/from-child? event)) - (not (every? #(= % prefix) selected-assets-paths))) + (not (every? #(= % prefix) selected-paths))) (reset! dragging* true))) (defn- on-drop-asset-group - [event dragging* prefix selected-assets-paths selected-assets-full rename] + [event dragging* prefix selected-paths selected-full rename] (dom/stop-propagation event) (when (not (dnd/from-child? event)) (reset! dragging* false) - (when (not (every? #(= % prefix) selected-assets-paths)) - (doseq [target-asset selected-assets-full] + (when (not (every? #(= % prefix) selected-paths)) + (doseq [target-asset selected-full] (st/emit! (rename (:id target-asset) @@ -311,7 +312,7 @@ content (filter #(= (get-role %) :content) children)] [:div.asset-section [:div.asset-title {:class (when (not ^boolean open?) "closed")} - [:span {:on-click #(st/emit! (dwl/set-assets-section-open file-id section (not open?)))} + [:span {:on-click #(st/emit! (dw/set-assets-section-open file-id section (not open?)))} i/arrow-slide title] [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space title-buttons] @@ -333,10 +334,10 @@ (mf/deps file-id section path group-open?) (fn [event] (dom/stop-propagation event) - (st/emit! (dwl/set-assets-group-open file-id - section - path - (not group-open?))))) + (st/emit! (dw/set-assets-group-open file-id + section + path + (not group-open?))))) on-context-menu (mf/use-fn (fn [event] @@ -385,8 +386,6 @@ [{:keys [component renaming listing-thumbs? selected file-id on-asset-click on-context-menu on-drag-start do-rename cancel-rename selected-full selected-paths]}] - - ;; (prn "components-item" (:name component)) (let [item-ref (mf/use-ref) dragging* (mf/use-state false) @@ -396,8 +395,6 @@ components-v2 (mf/use-ctx ctx/components-v2) component-id (:id component) - ;; _ (app.common.pprint/pprint component) - ;; NOTE: we don't use reactive deref for it because we don't ;; really need rerender on any change on the file change. If ;; the component changes, it will trigger rerender anyway. @@ -447,11 +444,11 @@ on-component-drag-start (mf/use-fn - (mf/deps component selected item-ref on-drag-start read-only?) + (mf/deps file-id component selected item-ref on-drag-start read-only?) (fn [event] (if read-only? (dom/prevent-default event) - (on-asset-drag-start event component selected item-ref :components on-drag-start)))) + (on-asset-drag-start event file-id component selected item-ref :components on-drag-start)))) on-context-menu (mf/use-fn @@ -604,7 +601,7 @@ (mf/defc components-section {::mf/wrap-props false} - [{:keys [file-id local? components listing-thumbs? open? reverse-sort? selected-assets + [{:keys [file-id local? components listing-thumbs? open? reverse-sort? selected on-asset-click on-assets-delete on-clear-selection open-status-ref]}] (let [input-ref (mf/use-ref nil) @@ -620,12 +617,12 @@ menu-state (mf/use-state initial-context-menu-state) read-only? (mf/use-ctx ctx/workspace-read-only?) - selected (:components selected-assets) + selected (:components selected) selected-full (into #{} (filter #(contains? selected (:id %))) components) multi-components? (> (count selected) 1) - multi-assets? (or (seq (:graphics selected-assets)) - (seq (:colors selected-assets)) - (seq (:typographies selected-assets))) + multi-assets? (or (seq (:graphics selected)) + (seq (:colors selected)) + (seq (:typographies selected))) groups (mf/with-memo [components reverse-sort?] (group-assets components reverse-sort?)) @@ -635,7 +632,7 @@ add-component (mf/use-fn (fn [] - (st/emit! (dwl/set-assets-section-open file-id :components true)) + (st/emit! (dw/set-assets-section-open file-id :components true)) (dom/click (mf/ref-val input-ref)))) on-file-selected @@ -796,7 +793,7 @@ (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) on-asset-click - (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] [:& asset-section {:file-id file-id :title (tr "workspace.assets.components") @@ -850,7 +847,7 @@ ;; ---- Graphics section ---- (mf/defc graphics-item - [{:keys [object renaming listing-thumbs? selected-objects + [{:keys [object renaming listing-thumbs? selected-objects file-id on-asset-click on-context-menu on-drag-start do-rename cancel-rename selected-full selected-graphics-paths]}] (let [item-ref (mf/use-ref) @@ -883,11 +880,11 @@ on-grahic-drag-start (mf/use-fn - (mf/deps object selected-objects item-ref on-drag-start read-only?) + (mf/deps object file-id selected-objects item-ref on-drag-start read-only?) (fn [event] (if read-only? (dom/prevent-default event) - (on-asset-drag-start event object selected-objects item-ref :graphics on-drag-start)))) + (on-asset-drag-start event file-id object selected-objects item-ref :graphics on-drag-start)))) on-context-menu (mf/use-fn @@ -896,7 +893,7 @@ on-asset-click (mf/use-fn - (mf/deps object-id) + (mf/deps object-id on-asset-click) (partial on-asset-click object-id nil)) ] @@ -1005,6 +1002,7 @@ (for [object objects] [:& graphics-item {:key (dm/str "object-" (:id object)) + :file-id file-id :object object :renaming renaming :listing-thumbs? listing-thumbs? @@ -1038,7 +1036,7 @@ (mf/defc graphics-section {::mf/wrap-props false} - [{:keys [file-id project-id local? objects listing-thumbs? open? open-status-ref selected-assets reverse-sort? + [{:keys [file-id project-id local? objects listing-thumbs? open? open-status-ref selected reverse-sort? on-asset-click on-assets-delete on-clear-selection]}] (let [input-ref (mf/use-ref nil) state (mf/use-state {:renaming nil :object-id nil}) @@ -1051,12 +1049,12 @@ (l/derived open-status-ref))) open-groups (mf/deref open-groups-ref) - selected (:graphics selected-assets) + selected (:graphics selected) selected-full (into #{} (filter #(contains? selected (:id %))) objects) multi-objects? (> (count selected) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:colors selected-assets)) - (seq (:typographies selected-assets))) + multi-assets? (or (seq (:components selected)) + (seq (:colors selected)) + (seq (:typographies selected))) objects (mf/with-memo [objects] (mapv dwl/extract-path-if-missing objects)) @@ -1070,7 +1068,7 @@ add-graphic (mf/use-fn (fn [] - (st/emit! (dwl/set-assets-section-open file-id :graphics true)) + (st/emit! (dw/set-assets-section-open file-id :graphics true)) (dom/click (mf/ref-val input-ref)))) on-file-selected @@ -1192,7 +1190,7 @@ (dnd/set-allowed-effect! event "move"))) on-asset-click - (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] [:& asset-section {:file-id file-id :title (tr "workspace.assets.graphics") @@ -1382,15 +1380,15 @@ on-color-drag-start (mf/use-fn - (mf/deps color selected item-ref read-only?) + (mf/deps color file-id selected item-ref read-only?) (fn [event] (if read-only? (dom/prevent-default event) - (on-asset-drag-start event color selected item-ref :colors identity)))) + (on-asset-drag-start event file-id color selected item-ref :colors identity)))) on-click (mf/use-fn - (mf/deps color-id apply-color) + (mf/deps color-id apply-color on-asset-click) (partial on-asset-click color-id apply-color))] (mf/with-effect [editing?] @@ -1543,10 +1541,10 @@ :selected-full selected-full}]))])])) (mf/defc colors-section - [{:keys [file-id local? colors open? open-status-ref selected-assets reverse-sort? + [{:keys [file-id local? colors open? open-status-ref selected reverse-sort? on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [selected (:colors selected-assets) + (let [selected (:colors selected) selected-full (mf/with-memo [selected colors] (into #{} (filter #(contains? selected (:id %))) colors)) @@ -1556,9 +1554,9 @@ open-groups (mf/deref open-groups-ref) multi-colors? (> (count selected) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:graphics selected-assets)) - (seq (:typographies selected-assets))) + multi-assets? (or (seq (:components selected)) + (seq (:graphics selected)) + (seq (:typographies selected))) groups (mf/with-memo [colors reverse-sort?] (group-assets colors reverse-sort?)) @@ -1574,7 +1572,7 @@ (mf/use-fn (mf/deps file-id) (fn [event] - (st/emit! (dwl/set-assets-section-open file-id :colors true) + (st/emit! (dw/set-assets-section-open file-id :colors true) (ptk/event ::ev/event {::ev/name "add-asset-to-library" :asset-type "color"})) (modal/show! :colorpicker @@ -1653,7 +1651,7 @@ (st/emit! (dwu/commit-undo-transaction undo-id))))) on-asset-click - (mf/use-fn (mf/deps groups) (partial on-asset-click groups))] + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] [:& asset-section {:file-id file-id :title (tr "workspace.assets.colors") @@ -1723,11 +1721,11 @@ on-typography-drag-start (mf/use-fn - (mf/deps typography selected item-ref read-only?) + (mf/deps typography file-id selected item-ref read-only?) (fn [event] (if read-only? (dom/prevent-default event) - (on-asset-drag-start event typography selected item-ref :typographies identity)))) + (on-asset-drag-start event file-id typography selected item-ref :typographies identity)))) on-context-menu (mf/use-fn @@ -1746,7 +1744,7 @@ on-asset-click (mf/use-fn - (mf/deps typography apply-typography) + (mf/deps typography apply-typography on-asset-click) (partial on-asset-click typography-id apply-typography)) ] @@ -1873,10 +1871,10 @@ (mf/defc typographies-section {::mf/wrap-props false} - [{:keys [file file-id local? typographies open? open-status-ref selected-assets reverse-sort? + [{:keys [file file-id local? typographies open? open-status-ref selected reverse-sort? on-asset-click on-assets-delete on-clear-selection]}] (let [state (mf/use-state {:detail-open? false :id nil}) - local-data (mf/deref ref:typography-section-state) + local-data (mf/deref lens:typography-section-state) read-only? (mf/use-ctx ctx/workspace-read-only?) menu-state (mf/use-state initial-context-menu-state) @@ -1886,14 +1884,14 @@ groups (mf/with-memo [typographies reverse-sort?] (group-assets typographies reverse-sort?)) - selected (:typographies selected-assets) + selected (:typographies selected) selected-full (mf/with-memo [selected typographies] (into #{} (filter #(contains? selected (:id %))) typographies)) multi-typographies? (> (count selected) 1) - multi-assets? (or (seq (:components selected-assets)) - (seq (:graphics selected-assets)) - (seq (:colors selected-assets))) + multi-assets? (or (seq (:components selected)) + (seq (:graphics selected)) + (seq (:colors selected))) open-groups-ref (mf/with-memo [open-status-ref] (-> (l/in [:groups :components]) @@ -2026,7 +2024,7 @@ on-asset-click (mf/use-fn - (mf/deps groups) + (mf/deps groups on-asset-click) (partial on-asset-click groups))] (mf/use-effect @@ -2114,7 +2112,7 @@ (mf/use-fn (mf/deps file-id open?) (fn [] - (st/emit! (dwl/set-assets-section-open file-id :library (not open?))))) + (st/emit! (dw/set-assets-section-open file-id :library (not open?))))) ] [:div.tool-window-bar.library-bar @@ -2193,92 +2191,94 @@ (or (pos? (count typographies)) (str/empty? filters-term))) - selected-assets (mf/deref ref:selected-assets) - selected-count (+ (count (:components selected-assets)) - (count (:graphics selected-assets)) - (count (:colors selected-assets)) - (count (:typographies selected-assets))) - extend-selected-assets - (mf/use-fn - (mf/deps selected-assets) - (fn [asset-type asset-groups asset-id] + selected-lens (mf/with-memo [file-id] + (-> (l/key file-id) + (l/derived lens:selected))) + selected (mf/deref selected-lens) + selected-count (+ (count (get selected :components)) + (count (get selected :graphics)) + (count (get selected :colors)) + (count (get selected :typographies))) - ;; FIXME: revisit performance of this function - (letfn [(flatten-groups [groups] - (reduce concat [(get groups "" []) - (into [] - (->> (filter #(seq (first %)) groups) - (map second) - (mapcat flatten-groups)))]))] - (let [selected-assets-type (get selected-assets asset-type) - count-assets (count selected-assets-type)] - (if (<= count-assets 0) - (st/emit! (dw/select-single-asset asset-id asset-type)) - (let [all-assets (flatten-groups asset-groups) - clicked-idx (d/index-of-pred all-assets #(= (:id %) asset-id)) - components (get selected-assets asset-type) + extend-selected + (fn [type asset-groups asset-id] + (letfn [(flatten-groups [groups] + (reduce concat [(get groups "" []) + (into [] + (->> (filter #(seq (first %)) groups) + (map second) + (mapcat flatten-groups)))]))] - first-idx (first (sort (map (fn [asset] (d/index-of-pred all-assets #(= (:id %) asset))) components))) - selected-idx (vector first-idx clicked-idx) - min-idx (apply min (conj selected-idx clicked-idx)) - max-idx (apply max (conj selected-idx clicked-idx)) - values (->> all-assets - (d/enumerate) - (filter #(<= min-idx (first %) max-idx)) - (map #(-> % second :id)) - (set))] + (let [selected' (get selected type)] + (if (zero? (count selected')) + (st/emit! (dw/select-single-asset file-id asset-id type)) + (let [all-assets (flatten-groups asset-groups) + click-index (d/index-of-pred all-assets #(= (:id %) asset-id)) + first-index (->> (get selected type) + (map (fn [asset] (d/index-of-pred all-assets #(= (:id %) asset)))) + (sort) + (first)) - (st/emit! (dw/select-assets values asset-type)))))))) + min-index (min first-index click-index) + max-index (max first-index click-index) + ids (->> (d/enumerate all-assets) + (into #{} (comp (filter #(<= min-index (first %) max-index)) + (map (comp :id second)))))] + + (st/emit! (dw/select-assets file-id ids type))))))) on-asset-click (mf/use-fn + (mf/deps file-id extend-selected) (fn [asset-type asset-groups asset-id default-click event] (cond (kbd/mod? event) (do (dom/stop-propagation event) - (st/emit! (dw/toggle-selected-assets asset-id asset-type))) + (st/emit! (dw/toggle-selected-assets file-id asset-id asset-type))) (kbd/shift? event) (do (dom/stop-propagation event) - (extend-selected-assets asset-type asset-groups asset-id)) + (extend-selected asset-type asset-groups asset-id)) :else (when default-click (default-click event))))) on-component-click - (mf/use-fn (partial on-asset-click :components)) + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :components)) on-graphics-click - (mf/use-fn (partial on-asset-click :graphics)) + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :graphics)) on-colors-click - (mf/use-fn (partial on-asset-click :colors)) + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :colors)) on-typography-click - (mf/use-fn (partial on-asset-click :typographies)) + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :typographies)) on-assets-delete (mf/use-fn - (mf/deps selected-assets) + (mf/deps selected file-id) (fn [] (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) (run! st/emit! (map #(dwl/delete-component {:id %}) - (:components selected-assets))) + (:components selected))) (run! st/emit! (map #(dwl/delete-media {:id %}) - (:graphics selected-assets))) + (:graphics selected))) (run! st/emit! (map #(dwl/delete-color {:id %}) - (:colors selected-assets))) + (:colors selected))) (run! st/emit! (map #(dwl/delete-typography %) - (:typographies selected-assets))) - (when (or (d/not-empty? (:components selected-assets)) - (d/not-empty? (:colors selected-assets)) - (d/not-empty? (:typographies selected-assets))) - (st/emit! (dwl/sync-file (:id file) (:id file)))) + (:typographies selected))) + + (when (or (seq (:components selected)) + (seq (:colors selected)) + (seq (:typographies selected))) + (st/emit! (dwl/sync-file file-id file-id))) + (st/emit! (dwu/commit-undo-transaction undo-id)))))] [:div.tool-window-content @@ -2304,7 +2304,7 @@ :open? (get open-status :components true) :open-status-ref open-status-ref :reverse-sort? reverse-sort? - :selected-assets selected-assets + :selected selected :on-asset-click on-component-click :on-assets-delete on-assets-delete :on-clear-selection on-clear-selection}]) @@ -2319,7 +2319,7 @@ :open? (get open-status :graphics true) :open-status-ref open-status-ref :reverse-sort? reverse-sort? - :selected-assets selected-assets + :selected selected :on-asset-click on-graphics-click :on-assets-delete on-assets-delete :on-clear-selection on-clear-selection}]) @@ -2332,7 +2332,7 @@ :open? (get open-status :colors true) :open-status-ref open-status-ref :reverse-sort? reverse-sort? - :selected-assets selected-assets + :selected selected :on-asset-click on-colors-click :on-assets-delete on-assets-delete :on-clear-selection on-clear-selection}]) @@ -2346,7 +2346,7 @@ :open? (get open-status :typographies true) :open-status-ref open-status-ref :reverse-sort? reverse-sort? - :selected-assets selected-assets + :selected selected :on-asset-click on-typography-click :on-assets-delete on-assets-delete :on-clear-selection on-clear-selection}]) @@ -2369,14 +2369,15 @@ open-status-ref (mf/with-memo [file-id] (-> (l/key file-id) - (l/derived ref:open-status))) + (l/derived lens:open-status))) open-status (mf/deref open-status-ref) open? (d/nilv (:library open-status) default-open?) unselect-all (mf/use-fn + (mf/deps file-id) (fn [] - (st/emit! (dw/unselect-all-assets)))) + (st/emit! (dw/unselect-all-assets file-id)))) ] From bd834ba84076276171af3cade7f1cc31c0d75a5e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 May 2023 19:08:53 +0200 Subject: [PATCH 18/22] :sparkles: Improve component renaming process on workspace --- frontend/src/app/main/data/workspace.cljs | 100 +++++++++++++----- .../app/main/data/workspace/libraries.cljs | 20 +++- frontend/src/app/main/refs.cljs | 3 + .../app/main/ui/workspace/sidebar/assets.cljs | 89 +++++++--------- .../main/ui/workspace/sidebar/layer_name.cljs | 20 ++-- .../ui/workspace/viewport/viewport_ref.cljs | 2 +- 6 files changed, 139 insertions(+), 95 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5c5bd34015..e957d16089 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -78,7 +78,6 @@ [beicon.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [linked.core :as lks] [potok.core :as ptk])) (def default-workspace-local {:zoom 1}) @@ -220,7 +219,7 @@ (rx/map (fn [data] (assoc file :data data)))))) (rx/reduce conj []) (rx/map libraries-fetched))) - (rx/of (workspace-initialized))) + (rx/of (with-meta (workspace-initialized) {:file-id id}))) (rx/take-until stoper)))))) (defn- libraries-fetched @@ -369,7 +368,10 @@ ptk/WatchEvent (watch [_ state _] - (let [pindex (-> state :workspace-data :pages-index)] + ;; 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) (rx/of (preload-data-uris page-id) (dwth/watch-state-changes) @@ -593,6 +595,7 @@ (defn start-rename-shape + "Start shape renaming process" [id] (dm/assert! (uuid? id)) (ptk/reify ::start-rename-shape @@ -601,14 +604,33 @@ (assoc-in state [:workspace-local :shape-for-rename] id)))) (defn end-rename-shape - [] - (ptk/reify ::end-rename-shape - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local dissoc :shape-for-rename)))) + "End the ongoing shape rename process" + ([] (end-rename-shape nil)) + ([name] + (ptk/reify ::end-rename-shape + ptk/WatchEvent + (watch [_ state _] + (prn "end-rename-shape" name (string? name) (not (str/blank? name))) + (when-let [shape-id (dm/get-in state [:workspace-local :shape-for-rename])] + (prn "end-rename-shape" shape-id) + (let [shape (wsh/lookup-shape state shape-id)] + (rx/concat + ;; Remove rename state from workspace local state + (rx/of #(update % :workspace-local dissoc :shape-for-rename)) + + ;; Rename the shape if string is not empty/blank + (when (and (string? name) (not (str/blank? name))) + (rx/of (update-shape shape-id {:name name}))) + + ;; Update the component in case if shape is a main instance + (when (:main-instance? shape) + (when-let [component-id (:component-id shape)] + (rx/of (dwl/rename-component component-id name))))))))))) + ;; --- Update Selected Shapes attrs + (defn update-selected-shapes [attrs] (dm/assert! (cts/shape-attrs? attrs)) @@ -1173,27 +1195,53 @@ (update state :workspace-assets dissoc :selected)))))) (defn go-to-main-instance - [page-id shape-id] - (dm/assert! (uuid? page-id)) - (dm/assert! (uuid? shape-id)) + [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-page-id (:current-page-id state)] - (if (= page-id current-page-id) - (rx/of (dws/select-shapes (lks/set shape-id)) - dwz/zoom-to-selected-shape) - (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/merge - (rx/of (rt/nav :workspace pparams qparams)) - (->> stream - (rx/filter (ptk/type? ::dwv/page-loaded)) - (rx/take 1) - (rx/mapcat #(rx/of (dws/select-shapes (lks/set shape-id)) - dwz/zoom-to-selected-shape)))))))))) + (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) + + redirect-to + (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)] + + (when (some? page-id) + (if (= page-id current-page-id) + (let [shape-id (:main-instance-id component)] + (rx/of (dws/select-shapes (d/ordered-set shape-id)) + dwz/zoom-to-selected-shape)) + (redirect-to current-page-id page-id)))) + + (let [component (dm/get-in state [:workspace-libraries file-id :data :components component-id])] + (some->> (:main-instance-page component) + (redirect-to file-id)))))))) + (defn go-to-component [component-id] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 5620783f3b..29f40d1c23 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -335,6 +335,8 @@ ;; NOTE: we need to ensure the component exists, ;; because there are small possibilities of race ;; conditions with component deletion. + ;; + ;; FIXME: this race conditon should be handled in pcb/update-component (when component (cond-> component :always @@ -343,7 +345,7 @@ (not components-v2) (update :objects - ;; Give the same name to the root shape + ;; Give the same name to the root shape #(assoc-in % [id :name] name))))) changes (-> (pcb/empty-changes it) @@ -353,9 +355,19 @@ (rx/of (dch/commit-changes changes))))))) (defn rename-component-and-main-instance - [component-id shape-id name page-id] - (st/emit! (rename-component component-id name) - (dch/update-shapes [shape-id] #(merge % {:name name}) {:page-id page-id :stack-undo? true}))) + [component-id name] + (ptk/reify ::rename-component-and-main-instance + ptk/WatchEvent + (watch [_ state _] + (when-let [component (dm/get-in state [:workspace-data :components component-id])] + (let [shape-id (:main-instance-id component) + page-id (:main-instance-page component)] + (rx/concat + (rx/of (rename-component component-id name)) + + ;; NOTE: only when components-v2 is enabled + (when (and shape-id page-id) + (rx/of (dch/update-shapes [shape-id] #(assoc % :name name) {:page-id page-id :stack-undo? true}))))))))) (defn duplicate-component "Create a new component copied from the one with the given id." diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 6d2b12cf48..70b9e935e4 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -104,9 +104,12 @@ (def workspace-drawing (l/derived :workspace-drawing st/state)) +;; FIXME: define it as function, because in some situations this +;; current check is not enought for true readiness (def workspace-ready? (l/derived (fn [state] (and (:workspace-ready? state) + (:workspace-data state) (:current-file-id state) (:current-project-id state))) st/state)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 1a7a7e4cf9..7c0872ed96 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -415,13 +415,10 @@ on-component-double-click (mf/use-fn - (mf/deps component selected) + (mf/deps file-id component-id) (fn [event] (dom/stop-propagation event) - (let [main-instance-id (:main-instance-id component) - main-instance-page (:main-instance-page component)] - (when (and main-instance-id main-instance-page) ;; Only when :components-v2 is enabled - (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) + (st/emit! (dw/go-to-main-instance file-id component-id)))) on-drop (mf/use-fn @@ -605,8 +602,12 @@ on-asset-click on-assets-delete on-clear-selection open-status-ref]}] (let [input-ref (mf/use-ref nil) - state (mf/use-state {:renaming nil - :component-id nil}) + + state* (mf/use-state {}) + state (deref state*) + + current-component-id (:component-id state) + renaming? (:renaming state) open-groups-ref (mf/with-memo [open-status-ref] (-> (l/in [:groups :components]) @@ -647,50 +648,43 @@ on-duplicate (mf/use-fn - (mf/deps @state) + (mf/deps current-component-id selected) (fn [] - (let [undo-id (js/Symbol)] - (if (empty? selected) - (st/emit! (dwl/duplicate-component file-id (:component-id @state))) - (do - (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! (map (partial dwl/duplicate-component file-id) selected)) - (st/emit! (dwu/commit-undo-transaction undo-id))))))) + (if (empty? selected) + (st/emit! (dwl/duplicate-component file-id current-component-id)) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! (map (partial dwl/duplicate-component file-id) selected)) + (st/emit! (dwu/commit-undo-transaction undo-id)))))) on-delete (mf/use-fn - (mf/deps @state file-id multi-components? multi-assets?) + (mf/deps current-component-id file-id multi-components? multi-assets? on-assets-delete) (fn [] (let [undo-id (js/Symbol)] (if (or multi-components? multi-assets?) (on-assets-delete) (st/emit! (dwu/start-undo-transaction undo-id) - (dwl/delete-component {:id (:component-id @state)}) - (dwl/sync-file file-id file-id :components (:component-id @state)) + (dwl/delete-component {:id current-component-id}) + (dwl/sync-file file-id file-id :components current-component-id) (dwu/commit-undo-transaction undo-id)))))) + on-close-menu + (mf/use-fn #(swap! menu-state close-context-menu)) + on-rename - (mf/use-fn - (fn [] - (swap! state (fn [state] - (assoc state :renaming (:component-id state)))))) + (mf/use-fn #(swap! state* assoc :renaming true)) + + cancel-rename + (mf/use-fn #(swap! state* dissoc :renaming)) do-rename (mf/use-fn - (mf/deps @state) + (mf/deps current-component-id) (fn [new-name] - (let [component-id (:renaming @state) - component (dm/get-in file [:components component-id]) - main-instance-id (:main-instance-id component) - main-instance-page (:main-instance-page component)] - - (dwl/rename-component-and-main-instance component-id main-instance-id new-name main-instance-page) - (swap! state assoc :renaming nil)))) - - cancel-rename - (mf/use-fn - (fn [] - (swap! state assoc :renaming nil))) + (swap! state* dissoc :renaming) + (st/emit! + (dwl/rename-component-and-main-instance current-component-id new-name)))) on-context-menu (mf/use-fn @@ -701,17 +695,13 @@ (when (and local? (not read-only?)) (when-not (contains? selected component-id) (on-clear-selection)) - (swap! state assoc :component-id component-id) - (swap! menu-state open-context-menu pos))))) - on-close-menu - (mf/use-fn - (fn [] - (swap! menu-state close-context-menu))) + (swap! state* assoc :component-id component-id) + (swap! menu-state open-context-menu pos))))) create-group (mf/use-fn - (mf/deps components selected on-clear-selection) + (mf/deps current-component-id components selected on-clear-selection) (fn [group-name] (on-clear-selection) (let [undo-id (js/Symbol)] @@ -720,7 +710,7 @@ (->> components (filter #(if multi-components? (contains? selected (:id %)) - (= (:component-id @state) (:id %)))) + (= current-component-id (:id %)))) (map #(dwl/rename-component (:id %) (add-group % group-name))))) @@ -780,17 +770,10 @@ on-show-main (mf/use-fn - (mf/deps @state components) + (mf/deps current-component-id file-id) (fn [event] (dom/stop-propagation event) - (let [component-id (:component-id @state) - component (->> components - (filter #(= (:id %) component-id)) - first) - main-instance-id (:main-instance-id component) - main-instance-page (:main-instance-page component)] - (when (and main-instance-id main-instance-page) ;; Only when :components-v2 is enabled - (st/emit! (dw/go-to-main-instance main-instance-page main-instance-id)))))) + (st/emit! (dw/go-to-main-instance file-id current-component-id)))) on-asset-click (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] @@ -815,7 +798,7 @@ :prefix "" :groups groups :open-groups open-groups - :renaming (:renaming @state) + :renaming (when ^boolean renaming? current-component-id) :listing-thumbs? listing-thumbs? :selected selected :on-asset-click on-asset-click diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 26b03c5b85..b7d47067d4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -8,7 +8,6 @@ (:require-macros [app.main.style :refer [css]]) (:require [app.main.data.workspace :as dw] - [app.main.data.workspace.libraries :as dwl] [app.main.store :as st] [app.main.ui.context :as ctx] [app.util.dom :as dom] @@ -19,6 +18,7 @@ (def shape-for-rename-ref (l/derived (l/in [:workspace-local :shape-for-rename]) st/state)) + (mf/defc layer-name [{:keys [shape on-start-edit disabled-double-click on-stop-edit name-ref depth parent-size selected? type-comp type-frame hidden] :as props}] (let [local (mf/use-state {}) @@ -28,27 +28,25 @@ start-edit (fn [] (when (not disabled-double-click) (on-start-edit) - (swap! local assoc :edition true))) + (swap! local assoc :edition true) + (st/emit! (dw/start-rename-shape (:id shape))))) accept-edit (fn [] (let [name-input (mf/ref-val name-ref) - name (str/trim (dom/get-value name-input)) - main-instance? (:main-instance? shape)] + name (str/trim (dom/get-value name-input))] (on-stop-edit) (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape)) - (when-not (str/empty? name) - (if main-instance? - (dwl/rename-component-and-main-instance (:component-id shape) (:id shape) name nil) - (st/emit! (dw/update-shape (:id shape) {:name name})))))) + (st/emit! (dw/end-rename-shape name)))) + cancel-edit (fn [] (on-stop-edit) (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape))) + (st/emit! (dw/end-rename-shape nil))) on-key-down (fn [event] (when (kbd/enter? event) (accept-edit)) (when (kbd/esc? event) (cancel-edit))) + space-for-icons 110 parent-size (str (- parent-size space-for-icons) "px")] @@ -87,4 +85,4 @@ :ref name-ref :on-double-click start-edit} (:name shape "") - (when (seq (:touched shape)) " *")]))) \ No newline at end of file + (when (seq (:touched shape)) " *")]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs index 32bd7fccf1..41c379d024 100644 --- a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs @@ -49,7 +49,7 @@ (defn point->viewport [pt] - (let [zoom (dm/get-in @st/state [:workspace-local :zoom])] + (let [zoom (dm/get-in @st/state [:workspace-local :zoom] 1)] (when (and (some? @viewport-ref) (some? @viewport-brect)) (let [vbox (.. ^js @viewport-ref -viewBox -baseVal) From a6659601f4fed51961adb63db25c9fe3377e51c3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 May 2023 23:28:28 +0200 Subject: [PATCH 19/22] :sparkles: Make workspace readiness state more robust --- frontend/src/app/main/refs.cljs | 13 ++++++------- frontend/src/app/main/ui/workspace.cljs | 5 ++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 70b9e935e4..ebc7b162ab 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -104,14 +104,13 @@ (def workspace-drawing (l/derived :workspace-drawing st/state)) -;; FIXME: define it as function, because in some situations this -;; current check is not enought for true readiness -(def workspace-ready? +(defn make-workspace-ready-ref + [file-id] (l/derived (fn [state] - (and (:workspace-ready? state) - (:workspace-data state) - (:current-file-id state) - (:current-project-id state))) + (let [data (:workspace-data state)] + (and (:workspace-ready? state) + (= file-id (:current-file-id state)) + (= file-id (:id data))))) st/state)) ;; TODO: rename to workspace-selected (?) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index dcb117441c..ee75b6f552 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -137,9 +137,12 @@ project (mf/deref refs/workspace-project) layout (mf/deref refs/workspace-layout) wglobal (mf/deref refs/workspace-global) - ready? (mf/deref refs/workspace-ready?) read-only? (mf/deref refs/workspace-read-only?) + ready-lens (mf/with-memo [file-id] + (refs/make-workspace-ready-ref file-id)) + ready? (mf/deref ready-lens) + team-id (:team-id project) file-name (:name file) From da5209001b04ad827267e5a32c9a022b5a91b1dc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 May 2023 23:30:15 +0200 Subject: [PATCH 20/22] :sparkles: Hide all messages on enter workspace move the logic from component to event --- frontend/src/app/main/data/workspace.cljs | 3 ++- frontend/src/app/main/ui/workspace.cljs | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e957d16089..995327cf55 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -305,7 +305,8 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (dcm/retrieve-comment-threads file-id) + (rx/of msg/hide + (dcm/retrieve-comment-threads file-id) (dwp/initialize-file-persistence file-id) (fetch-bundle project-id file-id))) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index ee75b6f552..4de952879e 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.workspace (:require [app.common.data.macros :as dm] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.persistence :as dwp] @@ -166,9 +165,6 @@ (st/emit! ::dwp/force-persist (dw/finalize-file project-id file-id)))) - (mf/with-effect [] - (st/emit! msg/hide)) - ;; Set properly the page title (mf/with-effect [file-name] (when file-name From 5d892d14d5ff5a0692f00b275a54d947a8ff376a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 May 2023 23:52:31 +0200 Subject: [PATCH 21/22] :sparkles: Move sidebar ns to correct location --- frontend/src/app/main/ui/workspace.cljs | 2 +- .../ui/workspace/{sidebar => }/sidebar.cljs | 139 +++++++++--------- .../workspace/{sidebar => }/sidebar.css.json | 0 .../ui/workspace/{sidebar => }/sidebar.scss | 0 4 files changed, 72 insertions(+), 69 deletions(-) rename frontend/src/app/main/ui/workspace/{sidebar => }/sidebar.cljs (50%) rename frontend/src/app/main/ui/workspace/{sidebar => }/sidebar.css.json (100%) rename frontend/src/app/main/ui/workspace/{sidebar => }/sidebar.scss (100%) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 4de952879e..2e7c240ff5 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -28,7 +28,7 @@ [app.main.ui.workspace.palette :refer [palette]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] - [app.main.ui.workspace.sidebar.sidebar :refer [left-sidebar right-sidebar]] + [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.textpalette :refer [textpalette]] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.dom :as dom] diff --git a/frontend/src/app/main/ui/workspace/sidebar/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs similarity index 50% rename from frontend/src/app/main/ui/workspace/sidebar/sidebar.cljs rename to frontend/src/app/main/ui/workspace/sidebar.cljs index fe3b8a4037..2c67ca08b5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -4,13 +4,14 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.sidebar.sidebar +(ns app.main.ui.workspace.sidebar (:require-macros [app.main.style :refer [css]]) (:require + [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.tab-container :refer [tab-container tab-element]] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] @@ -30,120 +31,122 @@ ;; --- Left Sidebar (Component) (mf/defc left-sidebar - {:wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [{:keys [layout] :as props}] - (let [options-mode (mf/deref refs/options-mode-global) - mode-inspect? (= options-mode :inspect) - section (cond (or mode-inspect? (contains? layout :layers)) :layers - (contains? layout :assets) :assets) - shortcuts? (contains? layout :shortcuts) - show-debug? (contains? layout :debug-panel) - new-css-system (mf/use-ctx ctx/new-css-system) + (let [options-mode (mf/deref refs/options-mode-global) + mode-inspect? (= options-mode :inspect) + + section (cond (or mode-inspect? (contains? layout :layers)) :layers + (contains? layout :assets) :assets) + shortcuts? (contains? layout :shortcuts) + show-debug? (contains? layout :debug-panel) + new-css? (mf/use-ctx ctx/new-css-system) + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} (use-resize-hook :left-sidebar 255 255 500 :x false :left) handle-collapse - (fn [] - (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))] + (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + + on-tab-change + (mf/use-fn #(st/emit! (dw/go-to-layout %))) + ] [:aside {:ref parent-ref - :class (if new-css-system + :class (if ^boolean new-css? (dom/classnames (css :left-settings-bar) true) (dom/classnames :settings-bar true :settings-bar-left true :two-row (<= size 300) :three-row (and (> size 300) (<= size 400)) :four-row (> size 400))) - :style #js {"--width" (str size "px")}} + :style #js {"--width" (dm/str size "px")}} [:div {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move - :class (if new-css-system + :class (if ^boolean new-css? (dom/classnames (css :resize-area) true) (dom/classnames :resize-area true))}] - [:div {:class (if new-css-system + [:div {:class (if ^boolean new-css? (dom/classnames (css :settings-bar-inside) true) (dom/classnames :settings-bar-inside true))} (cond - shortcuts? + (true? shortcuts?) [:& shortcuts-container] - show-debug? + (true? show-debug?) [:& debug-panel] :else - [:* + (if ^boolean new-css? + [:& tab-container + {:on-change-tab on-tab-change + :selected section + :shortcuts? shortcuts? + :collapsable? true + :handle-collapse handle-collapse} - (if new-css-system - [:& tab-container {:on-change-tab #(st/emit! (dw/go-to-layout %)) - :selected section - :shortcuts? shortcuts? - :collapsable? true - :handle-collapse handle-collapse} - [:& tab-element {:id :layers - :title (tr "workspace.sidebar.layers")} - [:div {:class (dom/classnames (css :layers-tab) true)} + [:& tab-element {:id :layers :title (tr "workspace.sidebar.layers")} + [:div {:class (dom/classnames (css :layers-tab) true)} + [:& sitemap {:layout layout}] + [:& layers-toolbox {:size-parent size}]]] + + (when-not ^boolean mode-inspect? + [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} + [:& assets-toolbox]])] + + [:* + [:button.collapse-sidebar + {:on-click handle-collapse + :aria-label (tr "workspace.sidebar.collapse")} + i/arrow-slide] + + [:& tab-container + {:on-change-tab on-tab-change + :selected section + :shortcuts? shortcuts? + :collapsable? true + :handle-collapse handle-collapse} + + [:& tab-element {:id :layers :title (tr "workspace.sidebar.layers")} + [:div {:class (dom/classnames :layers-tab true)} [:& sitemap {:layout layout}] [:& layers-toolbox {:size-parent size}]]] - (when-not mode-inspect? + (when-not ^boolean mode-inspect? [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} - [:& assets-toolbox]])] - - [:* - [:button.collapse-sidebar - {:on-click handle-collapse - :aria-label (tr "workspace.sidebar.collapse")} - i/arrow-slide] - [:& tab-container {:on-change-tab #(st/emit! (dw/go-to-layout %)) - :selected section - :shortcuts? shortcuts? - :collapsable? true - :handle-collapse handle-collapse} - [:& tab-element {:id :layers - :title (tr "workspace.sidebar.layers")} - [:div {:class (dom/classnames :layers-tab true)} - [:& sitemap {:layout layout}] - [:& layers-toolbox {:size-parent size}]]] - - (when-not mode-inspect? - [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} - [:& assets-toolbox]])]])])]])) + [:& assets-toolbox]])]]))]])) ;; --- Right Sidebar (Component) (mf/defc right-sidebar {::mf/wrap-props false ::mf/wrap [mf/memo]} - [props] - (let [layout (obj/get props "layout") - section (obj/get props "section") - drawing-tool (:tool (mf/deref refs/workspace-drawing)) + [{:keys [layout section] :as props}] + (let [drawing-tool (:tool (mf/deref refs/workspace-drawing)) - is-comments? (= drawing-tool :comments) - is-history? (contains? layout :document-history) - is-inspect? (= section :inspect) + is-comments? (= drawing-tool :comments) + is-history? (contains? layout :document-history) + is-inspect? (= section :inspect) expanded? (mf/deref refs/inspect-expanded) - can-be-expanded? (and - (not is-comments?) - (not is-history?) - is-inspect?)] + can-be-expanded? (and (not is-comments?) + (not is-history?) + is-inspect?)] - (mf/use-effect - (mf/deps can-be-expanded?) - (fn [] - (when (not can-be-expanded?) - (st/emit! (dw/set-inspect-expanded false))))) + (mf/with-effect [can-be-expanded?] + (when (not can-be-expanded?) + (st/emit! (dw/set-inspect-expanded false)))) [:aside.settings-bar.settings-bar-right {:class (when (and can-be-expanded? expanded?) "expanded")} [:div.settings-bar-inside (cond - is-comments? + (true? is-comments?) [:& comments-sidebar] - is-history? + (true? is-history?) [:& history-toolbox] :else diff --git a/frontend/src/app/main/ui/workspace/sidebar/sidebar.css.json b/frontend/src/app/main/ui/workspace/sidebar.css.json similarity index 100% rename from frontend/src/app/main/ui/workspace/sidebar/sidebar.css.json rename to frontend/src/app/main/ui/workspace/sidebar.css.json diff --git a/frontend/src/app/main/ui/workspace/sidebar/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss similarity index 100% rename from frontend/src/app/main/ui/workspace/sidebar/sidebar.scss rename to frontend/src/app/main/ui/workspace/sidebar.scss From b85b479396da6869b5f867dc51f3927837a7d710 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 25 May 2023 14:15:28 +0200 Subject: [PATCH 22/22] :sparkles: Add more improvements to workspace initialization --- frontend/src/app/main/data/workspace.cljs | 8 +- .../src/app/main/data/workspace/viewport.cljs | 9 +- frontend/src/app/main/refs.cljs | 9 -- frontend/src/app/main/ui/workspace.cljs | 141 ++++++++++-------- .../src/app/main/ui/workspace/libraries.cljs | 2 +- .../src/app/main/ui/workspace/sidebar.cljs | 1 - .../app/main/ui/workspace/sidebar/assets.cljs | 6 +- .../src/app/main/ui/workspace/viewport.cljs | 1 - .../app/main/ui/workspace/viewport/hooks.cljs | 22 +-- 9 files changed, 94 insertions(+), 105 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 995327cf55..9ac8da39b6 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -391,7 +391,10 @@ exit? (not= :workspace (dm/get-in state [:route :data :name])) state (-> state (update :workspace-cache assoc page-id local) - (dissoc :current-page-id :workspace-local :workspace-trimmed-page :workspace-focus-selected))] + (dissoc :current-page-id + :workspace-local + :workspace-trimmed-page + :workspace-focus-selected))] (cond-> state exit? (dissoc :workspace-drawing)))))) @@ -611,9 +614,7 @@ (ptk/reify ::end-rename-shape ptk/WatchEvent (watch [_ state _] - (prn "end-rename-shape" name (string? name) (not (str/blank? name))) (when-let [shape-id (dm/get-in state [:workspace-local :shape-for-rename])] - (prn "end-rename-shape" shape-id) (let [shape (wsh/lookup-shape state shape-id)] (rx/concat ;; Remove rename state from workspace local state @@ -2299,7 +2300,6 @@ (dm/export dwv/update-viewport-size) (dm/export dwv/start-panning) (dm/export dwv/finish-panning) -(dm/export dwv/page-loaded) ;; Undo (dm/export dwu/reinitialize-undo) diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs index 28365db990..0629b33d93 100644 --- a/frontend/src/app/main/data/workspace/viewport.cljs +++ b/frontend/src/app/main/data/workspace/viewport.cljs @@ -20,6 +20,7 @@ (defn initialize-viewport [{:keys [width height] :as size}] + (letfn [(update* [{:keys [vport] :as local}] (let [wprop (/ (:width vport) width) hprop (/ (:height vport) height)] @@ -153,11 +154,3 @@ (update [_ state] (-> state (update :workspace-local dissoc :panning))))) - - -;; This event does nothing. Is only for subscibe and know when the page has been loaded -(defn page-loaded [_page-id] - (ptk/reify ::page-loaded - ptk/UpdateEvent - (update [_ state] - state))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index ebc7b162ab..92190f0311 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -104,15 +104,6 @@ (def workspace-drawing (l/derived :workspace-drawing st/state)) -(defn make-workspace-ready-ref - [file-id] - (l/derived (fn [state] - (let [data (:workspace-data state)] - (and (:workspace-ready? state) - (= file-id (:current-file-id state)) - (= file-id (:id data))))) - st/state)) - ;; TODO: rename to workspace-selected (?) ;; Don't use directly from components, this is a proxy to improve performance of selected-shapes (def ^:private selected-shapes-data diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 2e7c240ff5..2cdf179734 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -26,9 +26,9 @@ [app.main.ui.workspace.libraries] [app.main.ui.workspace.nudge] [app.main.ui.workspace.palette :refer [palette]] + [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] - [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.textpalette :refer [textpalette]] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.dom :as dom] @@ -40,7 +40,21 @@ [okulary.core :as l] [rumext.v2 :as mf])) -;; --- Workspace +(defn- make-file-ready-ref + [file-id] + (l/derived (fn [state] + (let [data (:workspace-data state)] + (and (:workspace-ready? state) + (= file-id (:current-file-id state)) + (= file-id (:id data))))) + st/state)) + +(defn- make-page-ready-ref + [page-id] + (l/derived (fn [state] + (and (some? page-id) + (= page-id (:current-page-id state)))) + st/state)) (mf/defc workspace-content {::mf/wrap-props false} @@ -50,13 +64,13 @@ {:keys [vport] :as wlocal} (mf/deref refs/workspace-local) {:keys [options-mode]} wglobal - colorpalette? (:colorpalette layout) - textpalette? (:textpalette layout) - hide-ui? (:hide-ui layout) + colorpalette? (:colorpalette layout) + textpalette? (:textpalette layout) + hide-ui? (:hide-ui layout) new-css-system (mf/use-ctx ctx/new-css-system) on-resize - (mf/use-callback + (mf/use-fn (mf/deps vport) (fn [resize-type size] (when (and vport (not= size vport)) @@ -73,8 +87,9 @@ (when (and textpalette? (not hide-ui?)) [:& textpalette])]) - [:section.workspace-content {:key (dm/str "workspace-" page-id) - :ref node-ref} + [:section.workspace-content + {:key (dm/str "workspace-" page-id) + :ref node-ref} [:section.workspace-viewport (when (debug? :coordinates) [:& coordinates/coordinates {:colorpalette? colorpalette?}]) @@ -83,6 +98,7 @@ [:div.history-debug-overlay [:button {:on-click #(st/emit! dw/reinitialize-undo)} "CLEAR"] [:& history-toolbox]]) + [:& viewport {:file file :wlocal wlocal :wglobal wglobal @@ -99,83 +115,83 @@ :selected selected :layout layout}]])])) -(def ^:private ref:page-loaded - (l/derived - (fn [state] - (some? (:workspace-trimmed-page state))) - st/state)) - -(mf/defc workspace-page - {::mf/wrap-props false} - [{:keys [file layout page-id wglobal]}] - (let [prev-page-id (hooks/use-previous page-id) - page-loaded? (mf/deref ref:page-loaded)] - - (mf/with-effect [page-id prev-page-id] - (when (and prev-page-id (not= prev-page-id page-id)) - (st/emit! (dw/finalize-page prev-page-id))) - (if (nil? page-id) - (st/emit! (dw/go-to-page)) - (st/emit! (dw/initialize-page page-id)))) - - (when ^boolean page-loaded? - [:& workspace-content {:page-id page-id - :file file - :wglobal wglobal - :layout layout}]))) - (mf/defc workspace-loader [] [:div.workspace-loader i/loader-pencil]) -(mf/defc workspace +(mf/defc workspace-page {::mf/wrap-props false} - [{:keys [project-id file-id page-id layout-name]}] - (let [file (mf/deref refs/workspace-file) - project (mf/deref refs/workspace-project) - layout (mf/deref refs/workspace-layout) - wglobal (mf/deref refs/workspace-global) - read-only? (mf/deref refs/workspace-read-only?) - - ready-lens (mf/with-memo [file-id] - (refs/make-workspace-ready-ref file-id)) - ready? (mf/deref ready-lens) - - team-id (:team-id project) - file-name (:name file) - - components-v2 (features/use-feature :components-v2) - new-css-system (features/use-feature :new-css-system) - - background-color (:background-color wglobal)] + [{:keys [page-id file layout wglobal]}] + (let [page-id (hooks/use-equal-memo page-id) + page-ready* (mf/with-memo [page-id] + (make-page-ready-ref page-id)) + page-ready? (mf/deref page-ready*)] (mf/with-effect [] (let [focus-out #(st/emit! (dw/workspace-focus-lost)) key (events/listen globals/document "blur" focus-out)] (partial events/unlistenByKey key))) + (mf/with-effect [page-id] + (if (some? page-id) + (st/emit! (dw/initialize-page page-id)) + (st/emit! (dw/go-to-page))) + (fn [] + (when (some? page-id) + (st/emit! (dw/finalize-page page-id))))) + + (if ^boolean page-ready? + [:& workspace-content {:page-id page-id + :file file + :wglobal wglobal + :layout layout}] + [:& workspace-loader]))) + +(mf/defc workspace + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [project-id file-id page-id layout-name]}] + + (let [layout (mf/deref refs/workspace-layout) + wglobal (mf/deref refs/workspace-global) + read-only? (mf/deref refs/workspace-read-only?) + + file (mf/deref refs/workspace-file) + project (mf/deref refs/workspace-project) + + team-id (:team-id project) + file-name (:name file) + + file-ready* (mf/with-memo [file-id] + (make-file-ready-ref file-id)) + file-ready? (mf/deref file-ready*) + + components-v2? (features/use-feature :components-v2) + new-css? (features/use-feature :new-css-system) + + background-color (:background-color wglobal)] + ;; Setting the layout preset by its name (mf/with-effect [layout-name] (st/emit! (dw/initialize-layout layout-name))) + (mf/with-effect [file-name] + (when file-name + (dom/set-html-title (tr "title.workspace" file-name)))) + (mf/with-effect [project-id file-id] (st/emit! (dw/initialize-file project-id file-id)) (fn [] (st/emit! ::dwp/force-persist (dw/finalize-file project-id file-id)))) - ;; Set properly the page title - (mf/with-effect [file-name] - (when file-name - (dom/set-html-title (tr "title.workspace" file-name)))) - [:& (mf/provider ctx/current-file-id) {:value file-id} - [:& (mf/provider ctx/current-team-id) {:value team-id} - [:& (mf/provider ctx/current-project-id) {:value project-id} + [:& (mf/provider ctx/current-project-id) {:value project-id} + [:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-page-id) {:value page-id} - [:& (mf/provider ctx/components-v2) {:value components-v2} - [:& (mf/provider ctx/new-css-system) {:value new-css-system} + [:& (mf/provider ctx/components-v2) {:value components-v2?} + [:& (mf/provider ctx/new-css-system) {:value new-css?} [:& (mf/provider ctx/workspace-read-only?) {:value read-only?} [:section#workspace {:style {:background-color background-color :touch-action "none"}} @@ -187,7 +203,7 @@ [:& context-menu] - (if ^boolean ready? + (if ^boolean file-ready? [:& workspace-page {:page-id page-id :file file :wglobal wglobal @@ -208,6 +224,7 @@ (mf/use-effect (fn [] #(st/emit! (dw/clear-remove-graphics)))) + [:div.modal-overlay [:div.modal-container.remove-graphics-dialog [:div.modal-header diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index ad25ce8311..063176d543 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -171,7 +171,7 @@ [:div.section-list [:div.section-list-item - [:div {:data-kaka "1"} + [:div [:div.item-name (tr "workspace.libraries.file-library")] [:div.item-contents (describe-library (count components) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 2c67ca08b5..2f6de0e75b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -25,7 +25,6 @@ [app.main.ui.workspace.sidebar.sitemap :refer [sitemap]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [app.util.object :as obj] [rumext.v2 :as mf])) ;; --- Left Sidebar (Component) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 7c0872ed96..e3fc89bb50 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -2435,7 +2435,7 @@ :ordering :asc :list-style :thumbs}) filters (deref filters*) - + term (:term filters) toggle-ordering (mf/use-fn #(swap! filters* update :ordering toggle-values [:asc :desc])) @@ -2488,11 +2488,11 @@ [:input.search-input {:placeholder (tr "workspace.assets.search") :type "text" - :value (:term filters) + :value term :on-change on-search-term-change :on-key-down handle-key-down}] - (if ^boolean (str/empty? (:term filters)) + (if ^boolean (str/empty? term) [:div.search-icon i/search] [:div.search-icon.close diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 86578c3636..663661093f 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -258,7 +258,6 @@ (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path? text-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) - (hooks/setup-page-loaded page-id) [:div.viewport [:div.viewport-overlays diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 42a7283316..e4787f4ec5 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -28,7 +28,6 @@ [app.main.worker :as uw] [app.util.dom :as dom] [app.util.globals :as globals] - [app.util.timers :as timers] [beicon.core :as rx] [debug :refer [debug?]] [goog.events :as events] @@ -55,22 +54,13 @@ (events/unlistenByKey key)))))))) (defn setup-viewport-size [vport viewport-ref] - (mf/use-layout-effect - (mf/deps vport) - (fn [] - (when-not vport - (let [node (mf/ref-val viewport-ref) - prnt (dom/get-parent node)] - ;; We schedule the event so it fires after `initialize-page` event - (timers/schedule #(st/emit! (dw/initialize-viewport (dom/get-client-size prnt))))))))) + (mf/with-effect [vport] + (let [node (mf/ref-val viewport-ref) + prnt (dom/get-parent node) + size (dom/get-client-size prnt)] - -(defn setup-page-loaded [page-id] - (mf/use-effect - (mf/deps page-id) - (fn [] - ;; We schedule the event so it fires after `initialize-page` event - (timers/schedule #(st/emit! (dw/page-loaded page-id)))))) + (when (not= size vport) + (st/emit! (dw/initialize-viewport (dom/get-client-size prnt))))))) (defn setup-cursor [cursor alt? mod? space? panning drawing-tool drawing-path? path-editing? z? workspace-read-only?] (mf/use-effect