From f8ffae75c450a5a5509f2fee928ce95b10ca25e7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 1 Apr 2025 20:01:21 +0200 Subject: [PATCH 1/5] :zap: Make feature resolved on team load That simplifies features retrieval to simple get --- backend/src/app/http/errors.clj | 1 - frontend/src/app/main.cljs | 2 - frontend/src/app/main/data/changes.cljs | 5 +- frontend/src/app/main/data/common.cljs | 7 +- frontend/src/app/main/data/dashboard.cljs | 3 +- frontend/src/app/main/data/exports/files.cljs | 3 +- frontend/src/app/main/data/team.cljs | 12 +- frontend/src/app/main/data/viewer.cljs | 2 +- frontend/src/app/main/data/workspace.cljs | 177 +++++++++--------- .../app/main/data/workspace/libraries.cljs | 2 +- frontend/src/app/main/features.cljs | 116 +++++------- frontend/src/app/main/ui/dashboard/grid.cljs | 3 +- .../src/app/main/ui/dashboard/import.cljs | 40 ++-- frontend/src/app/plugins/file.cljs | 3 +- frontend/src/app/render.cljs | 7 +- frontend/src/debug.cljs | 7 +- frontend/src/features.cljs | 5 +- 17 files changed, 171 insertions(+), 224 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 35e58bbca1..cd32928017 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -25,7 +25,6 @@ (let [claims (-> {} (into (::session/token-claims request)) (into (::actoken/token-claims request)))] - {:request/path (:path request) :request/method (:method request) :request/params (:params request) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index e36985b178..b13c47dd83 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -15,7 +15,6 @@ [app.main.data.profile :as dp] [app.main.data.websocket :as ws] [app.main.errors] - [app.main.features :as feat] [app.main.rasterizer :as thr] [app.main.store :as st] [app.main.ui :as ui] @@ -67,7 +66,6 @@ (watch [_ _ stream] (rx/merge (rx/of (ev/initialize) - (feat/initialize) (dp/refresh-profile)) ;; Watch for profile deletion events diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 72de74ea9f..c63832693e 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -13,7 +13,6 @@ [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.helpers :as dsh] - [app.main.features :as features] [app.main.worker :as uw] [app.util.time :as dt] [beicon.v2.core :as rx] @@ -182,8 +181,8 @@ (let [file-id (or file-id (:current-file-id state)) uchg (vec undo-changes) rchg (vec redo-changes) - features (features/get-team-enabled-features state) - permissions (:permissions state)] + features (get state :features) + permissions (get state :permissions)] ;; Prevent commit changes by a viewer team member (it really should never happen) (when (:can-edit permissions) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 064d1901d5..b7ebf27166 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -16,7 +16,6 @@ [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.persistence :as-alias dps] - [app.main.features :as features] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] @@ -112,7 +111,7 @@ (ptk/reify ::show-shared-dialog ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state) + (let [features (get state :features) file (dsh/lookup-file state) data (get file :data)] @@ -169,8 +168,8 @@ (ptk/reify ::export-files ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state) - team-id (:current-team-id state)] + (let [features (get state :features) + team-id (get state :current-team-id)] (->> (rx/from files) (rx/mapcat (fn [file] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 57a886340a..f43278450a 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -19,7 +19,6 @@ [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] [app.main.data.websocket :as dws] - [app.main.features :as features] [app.main.repo :as rp] [app.util.i18n :as i18n :refer [tr]] [app.util.sse :as sse] @@ -497,7 +496,7 @@ base-name (tr "dashboard.new-file-prefix") name (or name (cfh/generate-unique-name base-name unames :immediate-suffix? true)) - features (-> (features/get-team-enabled-features state) + features (-> (get state :features) (set/difference cfeat/frontend-only-features)) params (-> params (assoc :name name) diff --git a/frontend/src/app/main/data/exports/files.cljs b/frontend/src/app/main/data/exports/files.cljs index 56ab281a70..b89c027fe3 100644 --- a/frontend/src/app/main/data/exports/files.cljs +++ b/frontend/src/app/main/data/exports/files.cljs @@ -12,7 +12,6 @@ [app.common.schema :as sm] [app.main.data.event :as ev] [app.main.data.modal :as modal] - [app.main.features :as features] [app.main.repo :as rp] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -47,7 +46,7 @@ (ptk/reify ::export-files ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state) + (let [features (get state :features) team-id (:current-team-id state) evname (if (= format :legacy-zip) "export-standard-files" diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 2ee1e5828e..ce96d45b25 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -101,7 +101,7 @@ (let [permissions (get team :permissions) features (get team :features)] (rx/of #(assoc % :permissions permissions) - (features/initialize (or features #{})) + (features/initialize features) (fetch-members team-id)))))) ptk/EffectEvent @@ -255,12 +255,12 @@ (dm/assert! (string? name)) (ptk/reify ::create-team ptk/WatchEvent - (watch [it state _] + (watch [it _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - features (features/get-enabled-features state) - params {:name name :features features}] + features features/global-enabled-features + params {:name name :features features}] (->> (rp/cmd! :create-team (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) @@ -272,11 +272,11 @@ [{:keys [name emails role] :as params}] (ptk/reify ::create-team-with-invitations ptk/WatchEvent - (watch [it state _] + (watch [it _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - features (features/get-enabled-features state) + features features/global-enabled-features params {:name name :emails emails :role role diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index d3baca1317..dbd96a2900 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -184,7 +184,7 @@ ptk/UpdateEvent (update [_ state] (let [team-id (:id team) - team {:members users}] + team (assoc team :members users)] (-> state (assoc :share-links share-links) (assoc :current-team-id team-id) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 43937ac454..5f1f2822b4 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -80,7 +80,6 @@ [app.main.data.workspace.viewport :as dwv] [app.main.data.workspace.zoom :as dwz] [app.main.errors] - [app.main.features :as features] [app.main.features.pointer-map :as fpmap] [app.main.repo :as rp] [app.main.router :as rt] @@ -208,30 +207,29 @@ (d/index-by :id)))))) (defn- fetch-libraries - [file-id] + [file-id features] (ptk/reify ::fetch-libries ptk/WatchEvent - (watch [_ state _] - (let [features (features/get-team-enabled-features state)] - (->> (rp/cmd! :get-file-libraries {:file-id file-id}) - (rx/mapcat - (fn [libraries] - (rx/concat - (rx/of (libraries-fetched file-id libraries)) - (rx/merge - (->> (rx/from libraries) - (rx/merge-map - (fn [{:keys [id synced-at]}] - (->> (rp/cmd! :get-file {:id id :features features}) - (rx/map #(assoc % :synced-at synced-at :library-of file-id))))) - (rx/mapcat resolve-file) - (rx/map library-resolved)) - (->> (rx/from libraries) - (rx/map :id) - (rx/mapcat (fn [file-id] - (rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"}))) - (rx/map dwl/library-thumbnails-fetched))) - (rx/of (check-libraries-synchronozation file-id libraries)))))))))) + (watch [_ _ _] + (->> (rp/cmd! :get-file-libraries {:file-id file-id}) + (rx/mapcat + (fn [libraries] + (rx/concat + (rx/of (libraries-fetched file-id libraries)) + (rx/merge + (->> (rx/from libraries) + (rx/merge-map + (fn [{:keys [id synced-at]}] + (->> (rp/cmd! :get-file {:id id :features features}) + (rx/map #(assoc % :synced-at synced-at :library-of file-id))))) + (rx/mapcat resolve-file) + (rx/map library-resolved)) + (->> (rx/from libraries) + (rx/map :id) + (rx/mapcat (fn [file-id] + (rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"}))) + (rx/map dwl/library-thumbnails-fetched))) + (rx/of (check-libraries-synchronozation file-id libraries))))))))) (defn- workspace-initialized [file-id] @@ -249,28 +247,16 @@ (fbs/fix-broken-shapes))))) (defn- bundle-fetched - [{:keys [features file thumbnails]}] + [{:keys [file file-id thumbnails] :as bundle}] (ptk/reify ::bundle-fetched IDeref - (-deref [_] - {:features features - :file file - :thumbnails thumbnails}) + (-deref [_] bundle) ptk/UpdateEvent (update [_ state] - (let [file-id (:id file)] - (-> state - (assoc :thumbnails thumbnails) - (update :files assoc file-id file)))) - - ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state) - file-id (:id file)] - (rx/of (dwn/initialize team-id file-id) - (dwsl/initialize-shape-layout) - (fetch-libraries file-id)))))) + (-> state + (assoc :thumbnails thumbnails) + (update :files assoc file-id file))))) (defn zoom-to-frame [] @@ -299,46 +285,29 @@ (defn- fetch-bundle "Multi-stage file bundle fetch coordinator" - [file-id] + [file-id features] (ptk/reify ::fetch-bundle ptk/WatchEvent - (watch [_ state stream] - (let [features (features/get-team-enabled-features state) - render-wasm? (contains? features "render-wasm/v1") - stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream) - team-id (:current-team-id state)] - - (->> (rx/concat - ;; Firstly load wasm module if it is enabled and fonts - (rx/merge - (if ^boolean render-wasm? - (->> (rx/from @wasm/module) - (rx/ignore)) - (rx/empty)) - - (->> stream - (rx/filter (ptk/type? ::df/fonts-loaded)) - (rx/take 1) - (rx/ignore)) - (rx/of (df/fetch-fonts team-id))) - - ;; Then fetch file and thumbnails - (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) - (get-file-object-thumbnails file-id)) - (rx/take 1) - (rx/mapcat - (fn [[file thumbnails]] - (->> (resolve-file file) - (rx/map (fn [file] - {:file file - :features features - :thumbnails thumbnails}))))) - (rx/map bundle-fetched))) + (watch [_ _ stream] + (let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)] + (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) + (get-file-object-thumbnails file-id)) + (rx/take 1) + (rx/mapcat + (fn [[file thumbnails]] + (->> (resolve-file file) + (rx/map (fn [file] + {:file file + :file-id file-id + :features features + :thumbnails thumbnails}))))) + (rx/map bundle-fetched) (rx/take-until stopper-s)))))) (defn initialize-workspace [file-id] (assert (uuid? file-id) "expected valud uuid for `file-id`") + (ptk/reify ::initialize-workspace ptk/UpdateEvent (update [_ state] @@ -350,26 +319,52 @@ ptk/WatchEvent (watch [_ state stream] - (log/debug :hint "initialize-workspace" :file-id (dm/str file-id)) (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) rparams (rt/get-params state) - features (features/get-team-enabled-features state) + team-id (get state :current-team-id) + features (get state :features) render-wasm? (contains? features "render-wasm/v1")] + (log/debug :hint "initialize-workspace" + :team-id (dm/str team-id) + :file-id (dm/str file-id)) + (->> (rx/merge - (rx/of (ntf/hide) - (dcmt/retrieve-comment-threads file-id) - (dcmt/fetch-profiles) - (fetch-bundle file-id)) + (rx/concat + ;; Fetch all essential data that should be loaded before the file + (rx/merge + (if ^boolean render-wasm? + (->> (rx/from @wasm/module) + (rx/ignore)) + (rx/empty)) + + (->> stream + (rx/filter (ptk/type? ::df/fonts-loaded)) + (rx/take 1) + (rx/ignore)) + + (rx/of (ntf/hide) + (dcmt/retrieve-comment-threads file-id) + (dcmt/fetch-profiles) + (df/fetch-fonts team-id))) + + ;; Once the essential data is fetched, lets proceed to + ;; fetch teh file bunldle + (rx/of (fetch-bundle file-id features))) (->> stream (rx/filter (ptk/type? ::bundle-fetched)) (rx/take 1) (rx/map deref) - (rx/mapcat (fn [{:keys [file]}] - (rx/of (dpj/initialize-project (:project-id file)) - (-> (workspace-initialized file-id) - (with-meta {:file-id file-id})))))) + (rx/mapcat + (fn [{:keys [file]}] + (rx/of (dpj/initialize-project (:project-id file)) + (dwn/initialize team-id file-id) + (dwsl/initialize-shape-layout) + (fetch-libraries file-id features) + (-> (workspace-initialized file-id) + (with-meta {:team-id team-id + :file-id file-id})))))) (->> stream (rx/filter (ptk/type? ::dps/persistence-notification)) @@ -442,7 +437,6 @@ ptk/WatchEvent (watch [_ state _] (let [project-id (:current-project-id state)] - (rx/of (dwn/finalize file-id) (dpj/finalize-project project-id) (dwsl/finalize-shape-layout) @@ -462,8 +456,6 @@ ;; Make this event callable through dynamic resolution (defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file)) - - (def ^:private xf:collect-file-media "Resolve and collect all file media on page objects" (comp (map second) @@ -1435,7 +1427,7 @@ (let [objects (dsh/lookup-page-objects state) selected (->> (dsh/lookup-selected state) (cfh/clean-loops objects)) - features (-> (features/get-team-enabled-features state) + features (-> (get state :features) (set/difference cfeat/frontend-only-features)) file-id (:current-file-id state) @@ -1673,9 +1665,10 @@ objects (dsh/lookup-page-objects state)] (when-let [shape (get objects selected)] (let [props (cts/extract-props shape) - features (-> (features/get-team-enabled-features state) + features (-> (get state :features) (set/difference cfeat/frontend-only-features)) - version (-> (dsh/lookup-file state) :version) + version (-> (dsh/lookup-file state) + (get :version)) copy-data {:type :copied-props :features features @@ -1809,8 +1802,8 @@ (ptk/reify ::paste-transit-shapes ptk/WatchEvent (watch [_ state _] - (let [file-id (:current-file-id state) - features (features/get-team-enabled-features state)] + (let [file-id (:current-file-id state) + features (get state :features)] (when-not (paste-data-valid? pdata) (ex/raise :type :validation @@ -1881,7 +1874,7 @@ (ptk/reify ::paste-transit-props ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state) + (let [features (get state :features) selected (dsh/lookup-selected state)] (when (paste-data-valid? pdata) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index dadb2af945..2f73ce76cc 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1398,7 +1398,7 @@ ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state)] + (let [features (get state :features)] (rx/concat (rx/merge (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 16fc4222bc..49612ff483 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -13,7 +13,6 @@ [app.config :as cf] [app.main.store :as st] [app.render-wasm :as wasm] - [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [okulary.core :as l] @@ -25,39 +24,24 @@ (def global-enabled-features (cfeat/get-enabled-features cf/flags)) -(defn get-enabled-features - [state] - (-> (get state :features-runtime #{}) - (set/intersection cfeat/no-migration-features) - (set/union global-enabled-features))) - -(defn get-team-enabled-features - [state] - (let [runtime-features (:features-runtime state #{}) - team-features (->> (:features-team state #{}) - (into #{} cfeat/xf-remove-ephimeral))] - (-> global-enabled-features - (set/union runtime-features) - (set/intersection cfeat/no-migration-features) - (set/union team-features)))) - -(def features-ref - (l/derived get-team-enabled-features st/state =)) - (defn active-feature? - "Given a state and feature, check if feature is enabled" + "Given a state and feature, check if feature is enabled." [state feature] - (assert (contains? cfeat/supported-features feature) "not supported feature") - (or (contains? (get state :features-runtime) feature) - (if (contains? cfeat/no-migration-features feature) - (or (contains? global-enabled-features feature) - (contains? (get state :features-team) feature)) - (contains? (get state :features-team state) feature)))) + (assert (contains? cfeat/supported-features feature) "feature not supported") + (let [runtime-features (get state :features-runtime) + enabled-features (get state :features)] + (or (contains? runtime-features feature) + (if (contains? cfeat/no-migration-features feature) + (or (contains? global-enabled-features feature) + (contains? enabled-features feature)) + (contains? enabled-features feature))))) + +(def ^:private features-ref + (l/derived (l/key :features) st/state)) (defn use-feature "A react hook that checks if feature is currently enabled" [feature] - (assert (contains? cfeat/supported-features feature) "Not supported feature") (let [enabled-features (mf/deref features-ref)] (contains? enabled-features feature))) @@ -71,14 +55,16 @@ ptk/UpdateEvent (update [_ state] (assert (contains? cfeat/supported-features feature) "not supported feature") - (update state :features-runtime (fn [features] - (if (contains? features feature) - (do - (log/trc :hint "feature disabled" :feature feature) - (disj features feature)) - (do - (log/trc :hint "feature enabled" :feature feature) - (conj features feature)))))))) + (-> state + (update :features-runtime (fn [features] + (if (contains? features feature) + (do + (log/trc :hint "feature disabled" :feature feature) + (disj features feature)) + (do + (log/trc :hint "feature enabled" :feature feature) + (conj features feature))))) + (update :features-runtime set/intersection cfeat/no-migration-features))))) (defn enable-feature [feature] @@ -90,46 +76,28 @@ state (do (log/trc :hint "feature enabled" :feature feature) - (update state :features-runtime (fnil conj #{}) feature)))))) + (-> state + (update :features-runtime (fnil conj #{}) feature) + (update :features-runtime set/intersection cfeat/no-migration-features))))))) (defn initialize - ([] (initialize #{})) - ([team-features] - (assert (set? team-features) "expected a set of features") - (assert (every? string? team-features) "expected a set of strings") + [features] + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (let [features (-> global-enabled-features + (set/union (get state :features-runtime #{})) + (set/union features))] + (assoc state :features features))) - (ptk/reify ::initialize - ptk/UpdateEvent - (update [_ state] - (let [runtime-features (get state :features/runtime #{}) - team-features (into #{} - cfeat/xf-supported-features - team-features)] - (-> state - (assoc :features-runtime runtime-features) - (assoc :features-team team-features)))) + ptk/EffectEvent + (effect [_ state _] + (let [features (get state :features)] + (if (contains? features "render-wasm/v1") + (wasm/initialize true) + (wasm/initialize false)) - ptk/WatchEvent - (watch [_ _ _] - (when *assert* - (->> (rx/from cfeat/no-migration-features) - ;; text editor v2 isn't enabled by default even in devenv - ;; wasm render v1 isn't enabled by default even in devenv - (rx/filter #(not (or (contains? cfeat/backend-only-features %) - (= "text-editor/v2" %) - (= "render-wasm/v1" %) - (= "design-tokens/v1" %)))) - (rx/observe-on :async) - (rx/map enable-feature)))) - - ptk/EffectEvent - (effect [_ state _] - (let [features (get-team-enabled-features state)] - (if (contains? features "render-wasm/v1") - (wasm/initialize true) - (wasm/initialize false)) - - (log/inf :hint "initialized" - :enabled (str/join "," features) - :runtime (str/join "," (:features-runtime state)))))))) + (log/inf :hint "initialized" + :enabled (str/join "," features) + :runtime (str/join "," (:features-runtime state))))))) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 2a2bf20f76..fd5804760b 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -17,7 +17,6 @@ [app.main.data.notifications :as ntf] [app.main.data.project :as dpj] [app.main.data.team :as dtm] - [app.main.features :as features] [app.main.fonts :as fonts] [app.main.rasterizer :as thr] [app.main.refs :as refs] @@ -60,7 +59,7 @@ (->> (wrk/ask! {:cmd :thumbnails/generate-for-file :revn revn :file-id file-id - :features (features/get-team-enabled-features @st/state)}) + :features (get @st/state :features)}) (rx/mapcat (fn [{:keys [fonts] :as result}] (->> (fonts/render-font-styles fonts) (rx/map (fn [styles] diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index ce6b83afb4..dc0afa8ca0 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -15,7 +15,6 @@ [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.errors :as errors] - [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.ds.product.loader :refer [loader*]] @@ -162,29 +161,32 @@ (defn- analyze-entries [state entries] - (->> (uw/ask-many! - {:cmd :analyze-import - :files entries - :features @features/features-ref}) - (rx/mapcat #(rx/delay emit-delay (rx/of %))) - (rx/filter some?) - (rx/subs! - (fn [message] - (swap! state update-with-analyze-result message))))) + (let [features (get @st/state :features)] + (->> (uw/ask-many! + {:cmd :analyze-import + :files entries + :features features}) + (rx/mapcat #(rx/delay emit-delay (rx/of %))) + (rx/filter some?) + (rx/subs! + (fn [message] + (swap! state update-with-analyze-result message)))))) (defn- import-files [state project-id entries] (st/emit! (ptk/data-event ::ev/event {::ev/name "import-files" :num-files (count entries)})) - (->> (uw/ask-many! - {:cmd :import-files - :project-id project-id - :files entries - :features @features/features-ref}) - (rx/filter (comp uuid? :file-id)) - (rx/subs! - (fn [message] - (swap! state update-entry-status message))))) + + (let [features (get @st/state :features)] + (->> (uw/ask-many! + {:cmd :import-files + :project-id project-id + :files entries + :features features}) + (rx/filter (comp uuid? :file-id)) + (rx/subs! + (fn [message] + (swap! state update-entry-status message)))))) (mf/defc import-entry* {::mf/props :obj diff --git a/frontend/src/app/plugins/file.cljs b/frontend/src/app/plugins/file.cljs index decedbf43d..22e545dcc5 100644 --- a/frontend/src/app/plugins/file.cljs +++ b/frontend/src/app/plugins/file.cljs @@ -13,7 +13,6 @@ [app.main.data.exports.files :as exports.files] [app.main.data.workspace :as dw] [app.main.data.workspace.versions :as dwv] - [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] [app.main.worker :as uw] @@ -237,7 +236,7 @@ :else (let [file (u/locate-file id) - features (features/get-team-enabled-features @st/state) + features (:features @st/state) team-id (:current-team-id @st/state) format (case format "zip" :legacy-zip diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 258c988128..bc1fc3f967 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -41,7 +41,7 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (features/initialize (or features #{})))))) + (rx/of (features/initialize features))))) (defn- fetch-team [& {:keys [file-id]}] @@ -98,7 +98,7 @@ (ptk/reify ::fetch-objects-bundle ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state)] + (let [features (get state :features)] (->> (rx/zip (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) (repo/cmd! :get-page {:file-id file-id @@ -237,7 +237,7 @@ (ptk/reify ::fetch-components-bundle ptk/WatchEvent (watch [_ state _] - (let [features (features/get-team-enabled-features state)] + (let [features (get state :features)] (->> (repo/cmd! :get-file {:id file-id :features features}) (rx/map (fn [file] #(assoc % :file file)))))))) @@ -309,7 +309,6 @@ (defn ^:export init [] - (st/emit! (features/initialize)) (init-ui)) (defn reinit diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index ad0491fda0..4d635b99b5 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -27,7 +27,6 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shortcuts] [app.main.errors :as errors] - [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] [app.util.debug :as dbg] @@ -393,7 +392,7 @@ (ptk/reify ::repair-current-file ptk/EffectEvent (effect [_ state _] - (let [features (features/get-team-enabled-features state) + (let [features (:features state) sid (:session-id state) file (dsh/lookup-file state) @@ -430,7 +429,3 @@ (defn ^:export set-shape-ref [id shape-ref] (st/emit! (dw/set-shape-ref id shape-ref))) - -(defn ^:export enable-text-v2 - [] - (st/emit! (features/enable-feature "text-editor/v2"))) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs index 0fa5ec46ba..9c30a86244 100644 --- a/frontend/src/features.cljs +++ b/frontend/src/features.cljs @@ -20,13 +20,12 @@ nil) (defn ^:export get-enabled [] - (clj->js (features/get-enabled-features @st/state))) + (clj->js features/global-enabled-features)) (defn ^:export get-team-enabled [] - (clj->js (features/get-team-enabled-features @st/state))) + (clj->js (get @st/state :features))) (defn ^:export plugins [] (st/emit! (features/enable-feature "plugins/runtime")) (plugins/init-plugins-runtime!) nil) - From ff7b77bda7f47ee0807c6b4420a18269a5779917 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 1 Apr 2025 18:49:42 +0200 Subject: [PATCH 2/5] :arrow_up: Update yarn --- backend/package.json | 2 +- common/package.json | 2 +- exporter/package.json | 2 +- frontend/package.json | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/package.json b/backend/package.json index c0981d82c0..126166f4cc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728", + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/common/package.json b/common/package.json index faf1e14d24..5a449d1286 100644 --- a/common/package.json +++ b/common/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728", + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", "type": "module", "repository": { "type": "git", diff --git a/exporter/package.json b/exporter/package.json index b76d60eb0f..7ab00e7701 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.8.0+sha512.85ec3149b1ec48f47c2690e13b29197a8a84b09c2c936cc596dddfb49c517e3bc3b1881ec52b5d35ca4b7c437c3f0daae3a80e39438c93bbcc5fcece2df5f15a", + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/frontend/package.json b/frontend/package.json index 12312dd156..ba740928eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.8.0+sha512.85ec3149b1ec48f47c2690e13b29197a8a84b09c2c936cc596dddfb49c517e3bc3b1881ec52b5d35ca4b7c437c3f0daae3a80e39438c93bbcc5fcece2df5f15a", + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", "browserslist": [ "defaults" ], diff --git a/package.json b/package.json index 0a560c87a5..b3e2845bc9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.2.2", + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" From 02220d02ed1fecb6e594d68359f1ca37521c5142 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 1 Apr 2025 18:50:16 +0200 Subject: [PATCH 3/5] :arrow_up: Update svgo --- exporter/package.json | 2 +- exporter/yarn.lock | 28 ++++++++++++++-------------- frontend/package.json | 2 +- frontend/yarn.lock | 28 ++++++++++++++-------------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/exporter/package.json b/exporter/package.json index 7ab00e7701..0361d3b1e5 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -18,7 +18,7 @@ "luxon": "^3.5.0", "playwright": "^1.50.0", "raw-body": "^3.0.0", - "svgo": "penpot/svgo#v3", + "svgo": "penpot/svgo#v3.1", "xml-js": "^1.6.11", "xregexp": "^5.1.1" }, diff --git a/exporter/yarn.lock b/exporter/yarn.lock index 2364d4cea6..3c86f61390 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -620,13 +620,13 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^3.0.0": - version: 3.0.0 - resolution: "css-tree@npm:3.0.0" +"css-tree@npm:^3.1.0": + version: 3.1.0 + resolution: "css-tree@npm:3.1.0" dependencies: - mdn-data: "npm:2.10.0" + mdn-data: "npm:2.12.2" source-map-js: "npm:^1.0.1" - checksum: 10c0/43d44fdf7004ae91d73d486f17894fef77efa33747a6752b9241cf0f5fb47fabc16ec34a96a993651d9014dfdeee803d7c5fcd3548214252ee19f4e5c98999b2 + checksum: 10c0/b5715852c2f397c715ca00d56ec53fc83ea596295ae112eb1ba6a1bda3b31086380e596b1d8c4b980fe6da09e7d0fc99c64d5bb7313030dd0fba9c1415f30979 languageName: node linkType: hard @@ -898,7 +898,7 @@ __metadata: raw-body: "npm:^3.0.0" shadow-cljs: "npm:2.28.20" source-map-support: "npm:^0.5.21" - svgo: "penpot/svgo#v3" + svgo: "penpot/svgo#v3.1" xml-js: "npm:^1.6.11" xregexp: "npm:^5.1.1" languageName: unknown @@ -1383,10 +1383,10 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:2.10.0": - version: 2.10.0 - resolution: "mdn-data@npm:2.10.0" - checksum: 10c0/f6f1a6a6eb092bab250d06f6f6c7cb1733a77a17e7119aac829ad67d4322bbf6a30df3c6d88686e71942e66bd49274b2ddfede22a1d3df0d6c49a56fbd09eb7c +"mdn-data@npm:2.12.2": + version: 2.12.2 + resolution: "mdn-data@npm:2.12.2" + checksum: 10c0/b22443b71d70f72ccc3c6ba1608035431a8fc18c3c8fc53523f06d20e05c2ac10f9b53092759a2ca85cf02f0d37036f310b581ce03e7b99ac74d388ef8152ade languageName: node linkType: hard @@ -2286,16 +2286,16 @@ __metadata: languageName: node linkType: hard -"svgo@penpot/svgo#v3": +"svgo@penpot/svgo#v3.1": version: 4.0.0 - resolution: "svgo@https://github.com/penpot/svgo.git#commit=71c0db44c3c2665f2ffc0c4c5383acaebd5c524f" + resolution: "svgo@https://github.com/penpot/svgo.git#commit=a46262c12c0d967708395972c374eb2adead4180" dependencies: "@trysound/sax": "npm:0.2.0" css-select: "npm:^5.1.0" - css-tree: "npm:^3.0.0" + css-tree: "npm:^3.1.0" csso: "npm:^5.0.5" lodash: "npm:^4.17.21" - checksum: 10c0/642c583372a610e484382cbf8a8fe28256dd354598d2e65ade2a3a63bf841b4d3dab4106f929f183ae3610007db2fc1413e82acc23793fe1a2e882bc923acc72 + checksum: 10c0/87a51a0cd1168a31c07ddfa9ffa544d0cad1412b3549dc20146143a179c66e36420a88ae40221cdb23146775876d684b47972663b08b3f62335eb4f98773677e languageName: node linkType: hard diff --git a/frontend/package.json b/frontend/package.json index ba740928eb..92020f1825 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -103,7 +103,7 @@ "@penpot/draft-js": "portal:./vendor/draft-js", "@penpot/hljs": "portal:./vendor/hljs", "@penpot/mousetrap": "portal:./vendor/mousetrap", - "@penpot/svgo": "penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b", + "@penpot/svgo": "penpot/svgo#v3.1", "@penpot/text-editor": "portal:./text-editor", "@tokens-studio/sd-transforms": "^0.16.1", "compression": "^1.7.5", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0168c8bc8a..753bce59a4 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1464,16 +1464,16 @@ __metadata: languageName: node linkType: soft -"@penpot/svgo@penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b": +"@penpot/svgo@penpot/svgo#v3.1": version: 4.0.0 - resolution: "@penpot/svgo@https://github.com/penpot/svgo.git#commit=c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b" + resolution: "@penpot/svgo@https://github.com/penpot/svgo.git#commit=a46262c12c0d967708395972c374eb2adead4180" dependencies: "@trysound/sax": "npm:0.2.0" css-select: "npm:^5.1.0" - css-tree: "npm:^3.0.0" + css-tree: "npm:^3.1.0" csso: "npm:^5.0.5" lodash: "npm:^4.17.21" - checksum: 10c0/af452f31196bcd237c390a12fea2da4c1d4005ae6d759c38f2169c9975c2178f85ec72077e96a8a40ded38748c2c1449dbdaf0d15f37ca3237622d766ac49ec8 + checksum: 10c0/db5f81c99dec2765721d73b69bb30594869ebf657380dfb46709c79775b6c0dc1af678fe9fe51bbe2272a2c78d19c2694a12ec6578bcc41235fa4aff475c9416 languageName: node linkType: hard @@ -4326,13 +4326,13 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^3.0.0": - version: 3.0.1 - resolution: "css-tree@npm:3.0.1" +"css-tree@npm:^3.1.0": + version: 3.1.0 + resolution: "css-tree@npm:3.1.0" dependencies: - mdn-data: "npm:2.12.1" + mdn-data: "npm:2.12.2" source-map-js: "npm:^1.0.1" - checksum: 10c0/9f117f3067e68e9edb0b3db0134f420db1a62bede3e84d8835767ecfaa6f8ced5e87989cf39b65ffe65d788c134c8ea9abd7393d7c35838a9da84326adf57a9b + checksum: 10c0/b5715852c2f397c715ca00d56ec53fc83ea596295ae112eb1ba6a1bda3b31086380e596b1d8c4b980fe6da09e7d0fc99c64d5bb7313030dd0fba9c1415f30979 languageName: node linkType: hard @@ -5883,7 +5883,7 @@ __metadata: "@penpot/draft-js": "portal:./vendor/draft-js" "@penpot/hljs": "portal:./vendor/hljs" "@penpot/mousetrap": "portal:./vendor/mousetrap" - "@penpot/svgo": "penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b" + "@penpot/svgo": "penpot/svgo#v3.1" "@penpot/text-editor": "portal:./text-editor" "@playwright/test": "npm:1.48.1" "@storybook/addon-essentials": "npm:^8.5.2" @@ -8270,10 +8270,10 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:2.12.1": - version: 2.12.1 - resolution: "mdn-data@npm:2.12.1" - checksum: 10c0/1a09f441bdd423f2b0ab712665a1a3329fe7b15e9a2dad8c1c10c521ddb204ed186e7ac91052fd53a5ae0a07ac6eae53b5bcbb59ba8a1fb654268611297eea4a +"mdn-data@npm:2.12.2": + version: 2.12.2 + resolution: "mdn-data@npm:2.12.2" + checksum: 10c0/b22443b71d70f72ccc3c6ba1608035431a8fc18c3c8fc53523f06d20e05c2ac10f9b53092759a2ca85cf02f0d37036f310b581ce03e7b99ac74d388ef8152ade languageName: node linkType: hard From e6e71e9278778f3aabd99194925cf1b25e2d1968 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 1 Apr 2025 20:24:07 +0200 Subject: [PATCH 4/5] :sparkles: Add minor enhacement for error reporting --- backend/src/app/http/errors.clj | 7 ++++--- backend/src/app/loggers/database.clj | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index cd32928017..eaeb9eef7e 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -61,7 +61,8 @@ ::yres/body data} (binding [l/*context* (request->context request)] - (l/err :hint "restriction error" :data data) + (l/err :hint "restriction error" + :cause err) {::yres/status 400 ::yres/body data})))) @@ -101,7 +102,7 @@ (= code :invalid-image) (binding [l/*context* (request->context request)] (let [cause (or parent-cause err)] - (l/warn :hint "unexpected error on processing image" :cause cause) + (l/warn :hint "image process error" :cause cause) {::yres/status 400 ::yres/body data})) :else @@ -176,7 +177,7 @@ (let [state (.getSQLState ^java.sql.SQLException error) cause (or parent-cause error)] (binding [l/*context* (request->context request)] - (l/error :hint "PSQL error" + (l/error :hint "postgresql error" :cause cause) (cond (= state "57014") diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj index 41584eddca..196845a967 100644 --- a/backend/src/app/loggers/database.clj +++ b/backend/src/app/loggers/database.clj @@ -53,11 +53,16 @@ (assoc :logger/name logger) (assoc :logger/level level) (dissoc :request/params :value :params :data))] + (merge {:context (-> (into (sorted-map) ctx) (pp/pprint-str :length 50)) :props (pp/pprint-str props :length 50) - :hint (or (ex-message cause) @message) + :hint (or (when-let [message (ex-message cause)] + (if-let [props-hint (:hint props)] + (str props-hint ": " message) + message)) + @message) :trace (or (::trace record) (some-> cause (ex/format-throwable :data? false :explain? false :header? false :summary? false)))} From 87ef98dad58429c6268584d51c094796c0b342ba Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 1 Apr 2025 21:15:38 +0200 Subject: [PATCH 5/5] :sparkles: Consolidate layout/grid feature --- common/src/app/common/features.cljc | 1 - .../options/menus/layout_container.cljs | 69 ++++++++----------- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc index b396e19370..95367a6063 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -106,7 +106,6 @@ (case flag :feature-components-v2 "components/v2" :feature-styles-v2 "styles/v2" - :feature-grid-layout "layout/grid" :feature-fdata-objects-map "fdata/objects-map" :feature-fdata-pointer-map "fdata/pointer-map" :feature-plugins "plugins/runtime" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 425fbc5d2f..476e1f120d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -16,7 +16,6 @@ [app.main.data.workspace :as udw] [app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.shape-layout :as dwsl] - [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] @@ -976,8 +975,6 @@ grid-justify-content-row (:layout-justify-content values) grid-justify-content-column (:layout-align-content values) - grid-enabled? (features/use-feature "layout/grid") - on-column-justify-change (mf/use-fn (mf/deps ids) @@ -1007,24 +1004,22 @@ (if (and (not multiple) (:layout values)) [:div {:class (stl/css :title-actions)} - (when ^boolean grid-enabled? - [:* - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.shape.menu.add-layout") - :on-click on-toggle-dropdown-visibility - :icon "menu"}] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.add-layout") + :on-click on-toggle-dropdown-visibility + :icon "menu"}] - [:& dropdown {:show show-dropdown? - :on-close on-hide-dropdown} - [:div {:class (stl/css :layout-options)} - [:button {:class (stl/css :layout-option) - :data-type "flex" - :on-click on-add-layout} - "Flex layout"] - [:button {:class (stl/css :layout-option) - :data-type "grid" - :on-click on-add-layout} - "Grid layout"]]]]) + [:& dropdown {:show show-dropdown? + :on-close on-hide-dropdown} + [:div {:class (stl/css :layout-options)} + [:button {:class (stl/css :layout-option) + :data-type "flex" + :on-click on-add-layout} + "Flex layout"] + [:button {:class (stl/css :layout-option) + :data-type "grid" + :on-click on-add-layout} + "Grid layout"]]] (when has-layout? [:> icon-button* {:variant "ghost" @@ -1033,29 +1028,23 @@ :icon "remove"}])] [:div {:class (stl/css :title-actions)} - (if ^boolean grid-enabled? - [:* - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.shape.menu.add-layout") - :on-click on-toggle-dropdown-visibility - :icon "add"}] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.add-layout") + :on-click on-toggle-dropdown-visibility + :icon "add"}] - [:& dropdown {:show show-dropdown? - :on-close on-hide-dropdown} - [:div {:class (stl/css :layout-options)} - [:button {:class (stl/css :layout-option) - :data-type "flex" - :on-click on-add-layout} - "Flex layout"] - [:button {:class (stl/css :layout-option) - :data-type "grid" - :on-click on-add-layout} - "Grid layout"]]]] - - [:button {:class (stl/css :add-layout) + [:& dropdown {:show show-dropdown? + :on-close on-hide-dropdown} + [:div {:class (stl/css :layout-options)} + [:button {:class (stl/css :layout-option) :data-type "flex" :on-click on-add-layout} - i/add]) + "Flex layout"] + [:button {:class (stl/css :layout-option) + :data-type "grid" + :on-click on-add-layout} + "Grid layout"]]] + (when has-layout? [:> icon-button* {:variant "ghost" :aria-label (tr "workspace.shape.menu.delete")