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
This commit is contained in:
Oleg Isonen authored and GitHub committed 2026-10-21 21:06:41 +02:00
1 parent 534401c66b
commit dabaeede02
8 files changed
+194 -13

No files matched your search

@@ -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 }),
@@ -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);
@@ -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" });
}
);
+1
View File
@@ -3,3 +3,4 @@ export const internalComponents = false;
export const unsupportedBrowsers = false;
export const resourceProp = false;
export const multiplayer = false;
export const deprecatedComponents = false;
@@ -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<string, ComponentCatalogMeta>([
["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<string, ComponentCatalogTemplate>([
[
"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<string, ComponentCatalogMeta>([
["Body", { category: "hidden" }],
@@ -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<string, ComponentCatalogTemplate>;
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;
};
@@ -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: (
<WebhookForm
state={expression`${formState}`}
@@ -7,7 +7,7 @@ import { props } from "./__generated__/webhook-form.props";
export const meta: WsComponentMeta = {
label: "Webhook Form",
description:
"Deprecated. Existing Webhook Forms remain supported through the next release. Use Form for new forms.",
"Deprecated. Existing Webhook Forms remain supported through the next release. Use the new Form component in the Forms section instead.",
deprecated: true,
icon: WebhookFormIcon,
presetStyle: {