From e6747054259422703bd66207fc28efa7ea008479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 24 Aug 2026 13:32:10 +0200 Subject: [PATCH] :tada: Make ui changes optional with config flag --- .../playwright/ui/specs/assets-tab.spec.js | 1 + .../src/app/main/ui/workspace/libraries.cljs | 387 ++++++++++++++++-- .../src/app/main/ui/workspace/libraries.scss | 31 ++ 3 files changed, 394 insertions(+), 25 deletions(-) diff --git a/frontend/playwright/ui/specs/assets-tab.spec.js b/frontend/playwright/ui/specs/assets-tab.spec.js index b9db97419a..77487c1356 100644 --- a/frontend/playwright/ui/specs/assets-tab.spec.js +++ b/frontend/playwright/ui/specs/assets-tab.spec.js @@ -3,6 +3,7 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-token-lib-sync"]); }); test("User adds a library and its automatically selected in the color palette", async ({ diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index c88cdb401d..a1c81f6012 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -140,6 +140,8 @@ token-sets-count (get summary :token-sets) token-themes-count (get summary :token-themes) + token-lib-sync? (contains? cf/flags :token-lib-sync) + elements-line (str/join " · " (cond-> [] @@ -167,18 +169,50 @@ (pos? token-themes-count) (conj (tr "workspace.libraries.token-themes" (c token-themes-count)))))] - [:* - (when-not (str/empty? elements-line) - [:li {:class (stl/css :element-count-line)} elements-line]) + (if token-lib-sync? + ;; New format: show elements and tokens in separate lines + [:* + (when-not (str/empty? elements-line) + [:li {:class (stl/css :element-count-line)} elements-line]) - (when-not (str/empty? tokens-line) - [:li {:class (stl/css :element-count-line)} tokens-line]) + (when-not (str/empty? tokens-line) + [:li {:class (stl/css :element-count-line)} tokens-line]) - (when hint-name - [:li {:class (stl/css :hint-line)} - "(" (tr "workspace.libraries.connected-through") " " - [:span {:class (stl/css :hint-library-name)} hint-name] - ")"])])) + (when hint-name + [:li {:class (stl/css :hint-line)} + "(" (tr "workspace.libraries.connected-through") " " + [:span {:class (stl/css :hint-library-name)} hint-name] + ")"])] + + ;; Old format: show each item individually + [:* + (when (pos? components-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.components" (c components-count))]) + + (when (pos? graphics-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.graphics" (c graphics-count))]) + + (when (pos? colors-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.colors" (c colors-count))]) + + (when (pos? typography-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.typography" (c typography-count))]) + + (when (pos? tokens-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.tokens" (c tokens-count))]) + + (when (pos? token-sets-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.token-sets" (c token-sets-count))]) + + (when (pos? token-themes-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.token-themes" (c token-themes-count))])]))) (mf/defc sample-library-entry* {::mf/private true} @@ -231,6 +265,296 @@ (when-let [tokens-lib (get data :tokens-lib)] (not (ctob/empty-lib? tokens-lib)))) +(mf/defc libraries-tab-legacy* + {::mf/private true} + [{:keys [is-shared linked-libraries shared-libraries]}] + (let [file-id (mf/use-ctx ctx/current-file-id) + search-term* (mf/use-state "") + search-term (deref search-term*) + + ;; The summary of the current/local library + ;; NOTE: we only need a snapshot of current library + local-library (deref refs/workspace-data) + summary (get-library-summary local-library) + empty-library? (empty-library? summary) + + selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) + dependencies (mf/with-memo [shared-libraries] + (into {} (map (juxt :id :library-file-ids) (vals shared-libraries)))) + + library-names (mf/with-memo [shared-libraries] + (into {} (map (fn [{:keys [id name]}] + [id name]) + (vals shared-libraries)))) + + find-connected-to + (mf/use-fn + (mf/deps dependencies) + (fn [library-id] + (->> dependencies + (keep (fn [[k v]] (when (contains? v library-id) k)))))) + + shared-libraries + (mf/with-memo [shared-libraries linked-libraries file-id search-term] + (when shared-libraries + (->> (vals shared-libraries) + (remove #(= (:id %) file-id)) + (remove #(contains? linked-libraries (:id %))) + (filter #(matches-search (:name %) search-term)) + (map #(assoc % :connected-to (find-connected-to (:id %)))) + (map #(assoc % :connected-to-names (->> (:connected-to %) + (keep library-names)))) + (sort-by (comp str/lower :name))))) + + linked-libraries + (mf/with-memo [linked-libraries find-connected-to library-names] + (->> (vals linked-libraries) + (map #(assoc % :connected-to (find-connected-to (:id %)))) + (map #(assoc % :connected-to-names (->> (:connected-to %) + (keep library-names)))) + (sort-by (comp str/lower :name)))) + + linked-libraries-ids + (mf/with-memo [linked-libraries] + (into #{} d/xf:map-id linked-libraries)) + + importing* + (mf/use-state nil) + + sample-libraries + (mf/with-memo [] + [{:id "penpot-design-system", :name "Design system example"} + {:id "wireframing-kit", :name "Wireframe library"} + {:id "whiteboarding-kit", :name "Whiteboarding Kit"}]) + + + change-search-term + (mf/use-fn + (fn [event] + (reset! search-term* event))) + + link-library + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (reset! selected library-id) + (st/emit! (dwl/link-file-to-library file-id library-id))))) + + unlink-library + (mf/use-fn + (mf/deps file-id local-library) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (when (= library-id @selected) + (reset! selected :file)) + (st/emit! (dwl/unlink-file-from-library file-id library-id) + (dwl/sync-file file-id library-id)) + ;; When the unlinked library is the current tokens source, + ;; we must reset it to the local library. + (when (cfo/effective-tokens-source? local-library library-id) + (st/emit! (dwtl/set-tokens-source (:id local-library))))))) + + import-tokens + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (st/emit! (modal/show + :tokens/import-from-library {:file-id file-id + :library-id library-id}))))) + + set-as-tokens-source + (mf/use-fn + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (st/emit! (dwtl/set-tokens-source library-id))))) + + on-delete-accept + (mf/use-fn + (mf/deps file-id) + #(st/emit! (dwl/set-file-shared file-id false) + (modal/show :libraries-dialog {:file-id file-id}))) + + on-delete-cancel + (mf/use-fn + (mf/deps file-id) + #(st/emit! (modal/show :libraries-dialog {:file-id file-id}))) + + publish + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [input-node (dom/get-target event) + publish-library #(st/emit! (dwl/set-file-shared file-id true)) + cancel-publish #(st/emit! (modal/show :libraries-dialog {:file-id file-id}))] + (if empty-library? + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.publish-empty-library.title") + :message (tr "modals.publish-empty-library.message") + :accept-label (tr "modals.publish-empty-library.accept") + :on-accept publish-library + :on-cancel cancel-publish})) + (publish-library)) + (dom/blur! input-node)))) + + 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}))))] + + [:div {:class (stl/css :libraries-content-legacy)} + [:div {:class (stl/css :lib-section)} + [:> title-bar* {:collapsable false + :title (tr "workspace.libraries.in-this-file") + :class (stl/css :title-spacing-lib)}] + [:div {:class (stl/css :section-list)} + + [:div {:class (stl/css :section-list-publish)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-title)} (tr "workspace.libraries.file-library")] + [:ul {:class (stl/css :item-contents-legacy)} + [:> library-description* {:summary summary}]] + (when (contains? cf/flags :token-lib-sync) + (if (cfo/effective-tokens-source? local-library (:id local-library)) + [:div (tr "workspace.libraries.tokens-source")] + (when (not= (cfo/get-tokens-source local-library) (:id local-library)) + [:> button* {:variant "secondary" + :type "button" + :data-library-id (dm/str (:id local-library)) + :on-click set-as-tokens-source} + (tr "workspace.libraries.set-as-tokens-source")])))] + + (if ^boolean is-shared + [:> button* {:variant "secondary" + :type "button" + :on-click unpublish} + (tr "common.unpublish")] + + [:> button* {:variant "primary" + :type "button" + :on-click publish} + (tr "common.publish")])] + + (for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries] + (let [disabled? (some #(contains? linked-libraries-ids %) connected-to) + has-tokens? (and (has-tokens? library) + (contains? cf/flags :token-import-from-library))] + [:div {:class (stl/css :section-list-item-legacy) + :key (dm/str id) + :data-testid "library-item"} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css-case :item-name true + :item-name-short has-tokens?)} name] + [:ul {:class (stl/css :item-contents-legacy)} + (let [summary (get-library-summary data)] + [:* + [:> library-description* {:summary summary}] + (when (seq connected-to) + [:div {:class (stl/css :connected-to-wrapper-legacy)} + [:span "(" (tr "workspace.libraries.connected-to") " "] + [:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)] + [:span ")"]])])] + (when (contains? cf/flags :token-lib-sync) + (when (cfo/effective-tokens-source? local-library id) + [:div (tr "workspace.libraries.tokens-source")]))] + + [:div {:class (stl/css :library-actions)} + (if (contains? cf/flags :token-lib-sync) + (when (and (cfo/tokens-provider? (:data library)) + (not (cfo/effective-tokens-source? local-library id))) + [:> button* {:variant "secondary" + :type "button" + :data-library-id (dm/str id) + :on-click set-as-tokens-source} + (tr "workspace.libraries.set-as-tokens-source")]) + (when ^boolean has-tokens? + [:> icon-button* + {:type "button" + :aria-label (tr "workspace.tokens.import-tokens") + :icon i/import-export + :data-library-id (dm/str id) + :variant "secondary" + :on-click import-tokens}])) + + [:> icon-button* {:type "button" + :aria-label (tr "workspace.libraries.unlink-library-btn") + :icon i/detach + :data-library-id (dm/str id) + :variant "secondary" + :disabled disabled? + :on-click unlink-library}]]]))]] + + [:div {:class (stl/css :shared-section)} + [:> title-bar* {:collapsable false + :title (tr "workspace.libraries.shared-libraries") + :class (stl/css :title-spacing-lib)}] + [:> search-bar* {:on-change change-search-term + :value search-term + :placeholder (tr "workspace.libraries.search-shared-libraries") + :icon-id i/search}] + + (if (seq shared-libraries) + [:div {:class (stl/css :section-list-shared)} + (for [{:keys [id name] :as library} shared-libraries] + [:div {:class (stl/css :section-list-item-legacy) + :key (dm/str id) + :data-testid "library-item"} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents-legacy)} + (let [summary (-> (:library-summary library) + (adapt-backend-summary))] + [:> library-description* {:summary summary}])]] + + [:> icon-button* {:class (stl/css :item-button-shared) + :variant "secondary" + :data-library-id (dm/str id) + :icon "add" + :aria-label (tr "workspace.libraries.shared-library-btn") + :on-click link-library}]])] + + (when (empty? shared-libraries) + [:div {:class (stl/css :section-list-empty)} + (cond + (nil? shared-libraries) + (tr "workspace.libraries.loading") + + (str/empty? search-term) + [:* + [:div {:class (stl/css :sample-libraries-info)} + (tr "workspace.libraries.empty.no-libraries") + [:a {:target "_blank" + :class (stl/css :sample-libraries-link) + :href "https://penpot.app/libraries-templates"} + (tr "workspace.libraries.empty.some-templates")]] + [:div {:class (stl/css :sample-libraries-container)} + (tr "workspace.libraries.empty.add-some") + (for [library sample-libraries] + [:> sample-library-entry* + {:library library + :key (dm/str (:id library)) + :importing importing*}])]] + + :else + (tr "workspace.libraries.no-matches-for" search-term))]))]])) + (mf/defc libraries-tab* {::mf/private true} [{:keys [linked-libraries shared-libraries]}] @@ -814,8 +1138,10 @@ (fn [_] (modal/hide!))) + token-lib-sync? (contains? cf/flags :token-lib-sync) + selected-tab* - (mf/use-state #(d/nilv starting-tab "file")) + (mf/use-state #(d/nilv starting-tab (if token-lib-sync? "file" "libraries"))) selected-tab (deref selected-tab*) @@ -824,13 +1150,18 @@ (mf/use-fn #(reset! selected-tab* %)) tabs - (mf/with-memo [] - [{:label "This file" - :id "file"} - {:label (tr "workspace.libraries.libraries") - :id "libraries"} - {:label (tr "workspace.libraries.updates") - :id "updates"}])] + (mf/with-memo [token-lib-sync?] + (if token-lib-sync? + [{:label "This file" + :id "file"} + {:label (tr "workspace.libraries.libraries") + :id "libraries"} + {:label (tr "workspace.libraries.updates") + :id "updates"}] + [{:label (tr "workspace.libraries.libraries") + :id "libraries"} + {:label (tr "workspace.libraries.updates") + :id "updates"}]))] (mf/with-effect [] (st/emit! (dtm/fetch-shared-files))) @@ -852,14 +1183,20 @@ :on-change on-change-tab} (case selected-tab "file" - [:> file-tab* {:is-shared shared? - :on-change-tab on-change-tab - :linked-libraries linked-libraries - :shared-libraries shared-libraries}] + (when token-lib-sync? + [:> file-tab* {:is-shared shared? + :on-change-tab on-change-tab + :linked-libraries linked-libraries + :shared-libraries shared-libraries}]) "libraries" - [:> libraries-tab* - {:linked-libraries linked-libraries - :shared-libraries shared-libraries}] + (if token-lib-sync? + [:> libraries-tab* + {:linked-libraries linked-libraries + :shared-libraries shared-libraries}] + [:> libraries-tab-legacy* + {:is-shared shared? + :linked-libraries linked-libraries + :shared-libraries shared-libraries}]) "updates" [:> updates-tab* diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 2a69454bff..4182dfb7d4 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -56,6 +56,7 @@ padding-block-start: var(--sp-l); } +.libraries-content-legacy, .updates-content { display: grid; grid-template-columns: 1fr 1fr; @@ -111,6 +112,10 @@ border: none; } +.section-list-item-legacy { + @extend %section-list-item-placeholder; +} + .section-list-item { @extend %section-list-item-placeholder; @@ -283,6 +288,15 @@ border-radius: $br-8; } +.item-contents-legacy { + @include t.use-typography("body-small"); + + color: var(--library-content-foreground-color); + display: flex; + flex-wrap: wrap; + margin: 0; +} + .item-contents { @include t.use-typography("body-small"); @@ -297,6 +311,15 @@ gap: var(--sp-xs); } +.element-count-legacy { + white-space: nowrap; + + &:not(:last-child)::after { + content: "·"; + margin-inline: var(--sp-xs); + } +} + .element-count-line { display: flex; align-items: baseline; @@ -307,6 +330,14 @@ } } +.connected-to-wrapper-legacy { + display: block; +} + +.connected-to-values-legacy { + color: var(--color-foreground-primary); +} + .hint-line { color: var(--color-foreground-secondary); }