🎉 Make ui changes optional with config flag

This commit is contained in:
Andrés Moya 2026-08-24 13:32:10 +02:00
parent 7fc5057556
commit e674705425
3 changed files with 394 additions and 25 deletions

View File

@ -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 ({

View File

@ -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*

View File

@ -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);
}