From dabaeede024afca8fd3db63406958be3cae75ba9 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 21 Oct 2026 20:06:41 +0100 Subject: [PATCH] fix: Hide deprecated components in Builder (#6576) The Components panel now hides deprecated components by default. The default-off `deprecatedComponents` feature flag shows all nine registered deprecated components in a separate **Deprecated** section, including Webhook Form and RemixForm. Webhook Form keeps its saved component ID, icon, behavior, navigator label, and `w-webhook-form` CSS class. The command picker continues to hide deprecated components. ## Checklist - [ ] Document each feature flag beside its declaration using its current behavior. - [x] Keep Webhook Form metadata and picker guidance pointing to Form. - [x] Preserve the generated `w-webhook-form` class without custom `presetClassName` metadata. - [x] Hide deprecated components from the Components panel by default; show all nine eligible components in a dedicated Deprecated section behind the default-off `deprecatedComponents` flag. - [x] Keep deprecated components out of the command picker. - [x] Add focused tests for default and flagged visibility, section membership, exclusions, and template insertion data. - [x] Review documentation impact. Existing Webhook Form docs describe its default unavailability; the flag is for development, so no docs change is needed. - [x] Verify focused tests, typechecks, lint, and fixture generation. The fixture pipeline passed; its unrelated live fixture path drift was restored. - [x] Update the Components panel E2E filter assertion for the flagged deprecated section and verify the rerun. - [x] Verify required CI on the latest head. Focused tests: component catalog 16/16, Builder 3/3. Builder, project-build, and feature-flags typechecks passed. Lint and the full fixture pipeline passed. The new section tests failed as expected with section creation temporarily disabled and passed after restoration. Latest-head CI: Main workflow, Visual regression, Lint PR, and Check submodules all passed, including all six Builder E2E shards. Ref #4093 --- .../features/components/components.tsx | 1 + .../shared/nano-states/components.test.tsx | 68 ++++++++++++++++-- ...ridge.[shard-2].[shard-5].[shard-6].e2e.ts | 1 - packages/feature-flags/src/flags.ts | 1 + .../src/runtime/component-catalog.test.ts | 72 +++++++++++++++++++ .../src/runtime/component-catalog.ts | 58 +++++++++++++-- .../src/webhook-form.template.tsx | 4 +- .../src/webhook-form.ws.ts | 2 +- 8 files changed, 194 insertions(+), 13 deletions(-) diff --git a/apps/builder/app/builder/features/components/components.tsx b/apps/builder/app/builder/features/components/components.tsx index 8d205ee0f3..800cb98cd3 100644 --- a/apps/builder/app/builder/features/components/components.tsx +++ b/apps/builder/app/builder/features/components/components.tsx @@ -47,6 +47,7 @@ const $metas = computed( const metasByCategory = listBuilderComponentPanelItems({ metas: componentMetas, templates, + showDeprecatedSection: isFeatureEnabled("deprecatedComponents"), documentType: selectedPage?.meta.documentType ?? "html", showInternal: isFeatureEnabled("internalComponents"), getFallbackLabel: (component) => getInstanceLabel({ component }), diff --git a/apps/builder/app/shared/nano-states/components.test.tsx b/apps/builder/app/shared/nano-states/components.test.tsx index 344be1dfdc..bbbc12b57e 100644 --- a/apps/builder/app/shared/nano-states/components.test.tsx +++ b/apps/builder/app/shared/nano-states/components.test.tsx @@ -5,6 +5,11 @@ import { CodeText } from "@webstudio-is/sdk-components-react/components"; import { canvasComponentLibraries } from "@webstudio-is/sdk-components-registry/canvas"; import { componentIds } from "@webstudio-is/sdk-components-registry/components"; import { getInstanceLabel } from "~/builder/shared/instance-label"; +import { $componentOptions } from "~/builder/features/command-panel/groups/components-group"; +import { + closeCommandPanel, + openCommandPanel, +} from "~/builder/features/command-panel/command-state"; import { $registeredComponentMetas, $registeredComponents, @@ -13,11 +18,27 @@ import { } from "./components"; afterEach(() => { + closeCommandPanel(); $registeredComponents.set(new Map()); $registeredComponentMetas.set(new Map()); $registeredTemplates.set(new Map()); }); +test("hides deprecated components from the command panel", () => { + for (const library of canvasComponentLibraries) { + registerComponentLibrary(library); + } + openCommandPanel(); + + expect( + $componentOptions + .get() + .filter( + ({ name }) => $registeredComponentMetas.get().get(name)?.deprecated + ) + ).toEqual([]); +}); + test("renders templates that import an alternate registered implementation", () => { registerComponentLibrary({ components: {}, @@ -35,7 +56,7 @@ test("renders templates that import an alternate registered implementation", () expect(template?.instances[0]?.component).toBe("CodeText"); }); -test("lists the managed Form while retaining saved Webhook Forms", () => { +test("lists all deprecated components only in the flagged section", () => { for (const library of canvasComponentLibraries) { registerComponentLibrary(library); } @@ -57,10 +78,49 @@ test("lists the managed Form while retaining saved Webhook Forms", () => { }), ]); - // New sites use Form while saved Webhook Forms remain registered for rendering. + expect(itemsByCategory.has("deprecated")).toBe(false); + const flaggedItemsByCategory = listBuilderComponentPanelItems({ + metas: $registeredComponentMetas.get(), + templates: $registeredTemplates.get(), + showDeprecatedSection: true, + getFallbackLabel: (component) => getInstanceLabel({ component }), + getMetaLabel: (component) => getInstanceLabel({ component }), + }); + expect(flaggedItemsByCategory.get("deprecated")).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "WebhookForm", + label: "Webhook Form (Deprecated)", + firstInstance: expect.objectContaining({ component: "WebhookForm" }), + }), + expect.objectContaining({ + name: "RemixForm", + firstInstance: { component: "RemixForm" }, + }), + ]) + ); expect( - itemsByCategory.get("data")?.some(({ name }) => name === "WebhookForm") - ).toBe(false); + flaggedItemsByCategory + .get("deprecated") + ?.map(({ name }) => name) + .sort() + ).toEqual([ + "Blockquote", + "Box", + "Heading", + "Link", + "List", + "Paragraph", + "RemixForm", + "RichTextLink", + "WebhookForm", + ]); + expect( + Array.from(flaggedItemsByCategory.entries()) + .filter(([category]) => category !== "deprecated") + .flatMap(([, items]) => items.map(({ name }) => name)) + .filter((name) => $registeredComponentMetas.get().get(name)?.deprecated) + ).toEqual([]); // Saved instances keep their original component IDs and implementations. expect($registeredComponents.get().has("ResourceForm")).toBe(true); diff --git a/apps/builder/e2e/tests/pages-actions-bridge.[shard-2].[shard-5].[shard-6].e2e.ts b/apps/builder/e2e/tests/pages-actions-bridge.[shard-2].[shard-5].[shard-6].e2e.ts index 635bad8525..411d00ca5a 100644 --- a/apps/builder/e2e/tests/pages-actions-bridge.[shard-2].[shard-5].[shard-6].e2e.ts +++ b/apps/builder/e2e/tests/pages-actions-bridge.[shard-2].[shard-5].[shard-6].e2e.ts @@ -280,7 +280,6 @@ test("Builder Components panel filters catalog-only entries and inserts a persis "pages actions verify components panel catalog filters", async () => { await openComponentsPanel({ page }); - await expectComponentPanelOptionHidden({ page, name: "Box" }); await expectComponentPanelOptionHidden({ page, name: "Body" }); } ); diff --git a/packages/feature-flags/src/flags.ts b/packages/feature-flags/src/flags.ts index 4e8ce063a0..bb765c6ffd 100644 --- a/packages/feature-flags/src/flags.ts +++ b/packages/feature-flags/src/flags.ts @@ -3,3 +3,4 @@ export const internalComponents = false; export const unsupportedBrowsers = false; export const resourceProp = false; export const multiplayer = false; +export const deprecatedComponents = false; diff --git a/packages/project-build/src/runtime/component-catalog.test.ts b/packages/project-build/src/runtime/component-catalog.test.ts index 4668bb8795..64af513917 100644 --- a/packages/project-build/src/runtime/component-catalog.test.ts +++ b/packages/project-build/src/runtime/component-catalog.test.ts @@ -521,6 +521,23 @@ describe("component catalog", () => { .flat() .map((item) => item.name) ).not.toEqual(expect.arrayContaining(["Box", "HiddenWidget"])); + + const panelItemsWithDeprecatedBox = listBuilderComponentPanelItems({ + metas, + templates, + showDeprecatedSection: true, + }); + expect( + panelItemsWithDeprecatedBox.get("general")?.map((item) => item.name) + ).toEqual(["LocalTemplateOnly"]); + expect( + panelItemsWithDeprecatedBox.get("deprecated")?.map((item) => item.name) + ).toEqual(["Box"]); + expect( + Array.from(panelItemsWithDeprecatedBox.values()) + .flat() + .map((item) => item.name) + ).not.toContain("HiddenWidget"); }); test("preserves builder components panel raw grouping semantics", () => { @@ -594,6 +611,61 @@ describe("component catalog", () => { expect(itemsByCategory.has("hidden")).toBe(false); }); + test("keeps unavailable deprecated components out of the opt-in section", () => { + const metas = new Map([ + ["Legacy", { deprecated: true }], + ["Hidden", { category: "hidden", deprecated: true }], + ["Internal", { category: "internal", deprecated: true }], + ["Xml", { category: "xml", deprecated: true }], + [ + "Part", + { contentModel: { category: "none", children: [] }, deprecated: true }, + ], + ]); + const templates = new Map([ + [ + "Legacy", + { category: "general", template: createEmptyWebstudioFragment() }, + ], + [ + "Hidden", + { category: "general", template: createEmptyWebstudioFragment() }, + ], + [ + "Xml", + { category: "general", template: createEmptyWebstudioFragment() }, + ], + [ + "HiddenTemplate", + { category: "hidden", template: createEmptyWebstudioFragment() }, + ], + ]); + metas.set("HiddenTemplate", { deprecated: true }); + + const items = listBuilderComponentPanelItems({ + metas, + templates, + showDeprecatedSection: true, + }); + expect(items.get("deprecated")?.map(({ name }) => name)).toEqual([ + "Legacy", + ]); + expect( + items.get("general")?.some(({ name }) => name === "Legacy") ?? false + ).toBe(false); + expect( + listBuilderComponentPanelItems({ metas, templates }).has("deprecated") + ).toBe(false); + expect( + listBuilderComponentPanelItems({ + metas, + templates, + showDeprecatedSection: true, + documentType: "xml", + }).has("deprecated") + ).toBe(false); + }); + test("filters catalog items by visibility and document type", () => { const metas = new Map([ ["Body", { category: "hidden" }], diff --git a/packages/project-build/src/runtime/component-catalog.ts b/packages/project-build/src/runtime/component-catalog.ts index c312f07898..e157a515f9 100644 --- a/packages/project-build/src/runtime/component-catalog.ts +++ b/packages/project-build/src/runtime/component-catalog.ts @@ -404,9 +404,12 @@ export const isComponentDeprecatedInCatalog = ( export const isComponentMetaUnavailableInCatalog = ( meta: ComponentCatalogMeta, - { showInternal = false }: { showInternal?: boolean } = {} + { + showInternal = false, + includeDeprecated = false, + }: { showInternal?: boolean; includeDeprecated?: boolean } = {} ) => { - if (isComponentDeprecatedInCatalog(meta)) { + if (isComponentDeprecatedInCatalog(meta) && !includeDeprecated) { return true; } if (meta.category !== undefined) { @@ -716,6 +719,7 @@ export const listBuilderComponentPanelItems = ({ templates, documentType = "html", showInternal = false, + showDeprecatedSection = false, getFallbackLabel = (component) => component, getMetaLabel = (component) => getFallbackLabel(component), getTemplateIcon = (_component, template) => template.icon, @@ -724,6 +728,7 @@ export const listBuilderComponentPanelItems = ({ templates: ReadonlyMap; documentType?: ComponentCatalogDocumentType; showInternal?: boolean; + showDeprecatedSection?: boolean; getFallbackLabel?: (component: string) => string; getMetaLabel?: (component: string, meta: ComponentCatalogMeta) => string; getTemplateIcon?: ( @@ -734,12 +739,25 @@ export const listBuilderComponentPanelItems = ({ }) => { const items: BuilderComponentPanelItem[] = []; for (const [name, meta] of metas) { - if (isComponentDeprecatedInCatalog(meta)) { + const deprecated = isComponentDeprecatedInCatalog(meta); + if (deprecated && !showDeprecatedSection) { + continue; + } + if ( + deprecated && + (meta.category === "hidden" || + meta.category === "xml" || + (meta.category === "internal" && !showInternal) || + meta.contentModel?.category === "none") + ) { + continue; + } + if (deprecated && templates.has(name)) { continue; } items.push({ name, - category: meta.category ?? "hidden", + category: deprecated ? "deprecated" : (meta.category ?? "hidden"), order: meta.order, label: getMetaLabel(name, meta), description: meta.description, @@ -748,12 +766,32 @@ export const listBuilderComponentPanelItems = ({ } for (const [name, template] of templates) { const meta = metas.get(name); - if (meta !== undefined && isComponentMetaUnavailableInCatalog(meta)) { + const deprecated = isComponentDeprecatedInCatalog(meta); + if (deprecated && !showDeprecatedSection) { + continue; + } + if ( + deprecated && + (template.category === "hidden" || + template.category === "xml" || + (template.category === "internal" && !showInternal) || + meta?.category === "xml" || + meta?.contentModel?.category === "none") + ) { + continue; + } + if ( + meta !== undefined && + isComponentMetaUnavailableInCatalog(meta, { + includeDeprecated: deprecated, + showInternal: deprecated && showInternal, + }) + ) { continue; } items.push({ name, - category: template.category ?? "hidden", + category: deprecated ? "deprecated" : (template.category ?? "hidden"), order: template.order, label: template.label ?? meta?.label ?? getFallbackLabel(name), description: template.description, @@ -798,6 +836,14 @@ export const listBuilderComponentPanelItems = ({ }); itemsByCategory.set(category, categoryItems); } + if (showDeprecatedSection && documentType === "html") { + const deprecatedItems = items.filter( + (item) => item.category === "deprecated" + ); + if (deprecatedItems.length > 0) { + itemsByCategory.set("deprecated", deprecatedItems); + } + } return itemsByCategory; }; diff --git a/packages/sdk-components-react/src/webhook-form.template.tsx b/packages/sdk-components-react/src/webhook-form.template.tsx index 2681530b0b..f67b243122 100644 --- a/packages/sdk-components-react/src/webhook-form.template.tsx +++ b/packages/sdk-components-react/src/webhook-form.template.tsx @@ -17,7 +17,9 @@ const formState = new Variable("formState", "initial"); export const meta: TemplateMeta = { category: "data", order: 1, - description: "Collect user data and send it to any webhook.", + label: "Webhook Form (Deprecated)", + description: + "Deprecated. Existing Webhook Forms remain supported through the next release. Use the new Form component in the Forms section instead.", template: (