From 87c6932b3b6dc55e09aea63e2fa9ce053bc2b86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Thu, 4 Dec 2025 14:19:12 +0100 Subject: [PATCH 1/8] wip(app-headless-cms): separator field --- .../fieldRenderers/separator/index.tsx | 1 + .../fieldRenderers/separator/separator.tsx | 41 +++++++++++++++++++ .../plugins/fields/separator/separator.tsx | 29 +++++++++++++ .../views/contentModels/ContentModels.tsx | 6 +-- packages/app-headless-cms/src/allPlugins.ts | 4 ++ 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/index.tsx create mode 100644 packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx create mode 100644 packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/index.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/index.tsx new file mode 100644 index 00000000000..482291445cc --- /dev/null +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/index.tsx @@ -0,0 +1 @@ +export * from "./separator.js"; diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx new file mode 100644 index 00000000000..b765e2284e8 --- /dev/null +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { i18n } from "@webiny/app/i18n/index.js"; +import type { CmsModelFieldRendererPlugin } from "~/types.js"; +import { Grid, Separator } from "@webiny/admin-ui"; + +const t = i18n.ns("app-headless-cms/admin/fields/text"); + +export const createSeparatorFieldRenderer = (): CmsModelFieldRendererPlugin => { + return { + type: "cms-editor-field-renderer", + name: "cms-editor-field-renderer-separator", + renderer: { + rendererName: "text-separator", + name: t`Separator`, + description: t`Renders a separator field.`, + canUse({ field }) { + return field.type === "text:separator"; + }, + render({ field }) { + return ( + + + + {field.label} + + } + /> + {field.label} ------------------------------ + + + ); + } + } + }; +}; diff --git a/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx b/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx new file mode 100644 index 00000000000..194da89e447 --- /dev/null +++ b/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import type { CmsModelFieldTypePlugin } from "@webiny/app-headless-cms-common/types/index.js"; +import { ReactComponent as SeparatorIcon } from "@webiny/icons/line_style.svg"; +import { i18n } from "@webiny/app/i18n/index.js"; + +const t = i18n.ns("app-headless-cms/admin/fields"); + +export const separatorField: CmsModelFieldTypePlugin = { + type: "cms-editor-field-type", + name: "cms-editor-field-type-text-separator", + field: { + type: "text:separator", + label: t`Separator`, + description: t`Show a separator field which is not stored in the database.`, + icon: , + allowLayout: false, + hideInAdmin: false, + allowMultipleValues: false, + allowPredefinedValues: false, + createField() { + return { + type: this.type, + renderer: { + name: "text-separator" + } + }; + } + } +}; diff --git a/packages/app-headless-cms/src/admin/views/contentModels/ContentModels.tsx b/packages/app-headless-cms/src/admin/views/contentModels/ContentModels.tsx index 1ce23c6e976..a29ece420c5 100644 --- a/packages/app-headless-cms/src/admin/views/contentModels/ContentModels.tsx +++ b/packages/app-headless-cms/src/admin/views/contentModels/ContentModels.tsx @@ -62,14 +62,14 @@ const ContentModels = () => { - {cloneContentModel && ( + {cloneContentModel ? ( - )} - {importModels && } + ) : null} + {importModels ? : null} ); }; diff --git a/packages/app-headless-cms/src/allPlugins.ts b/packages/app-headless-cms/src/allPlugins.ts index 5df6628e34a..aa8e4981ca6 100644 --- a/packages/app-headless-cms/src/allPlugins.ts +++ b/packages/app-headless-cms/src/allPlugins.ts @@ -42,6 +42,8 @@ import editorUpperCaseSpaceFieldValidator from "~/admin/plugins/fieldValidators/ import { dynamicZoneField } from "~/admin/plugins/fields/dynamicZone.js"; import { dynamicZoneFieldRenderer } from "~/admin/plugins/fieldRenderers/dynamicZone/dynamicZoneRenderer.js"; import { dynamicZoneFieldValidator } from "~/admin/plugins/fieldValidators/dynamicZone.js"; +import { createSeparatorFieldRenderer } from "~/admin/plugins/fieldRenderers/separator/index.js"; +import { separatorField } from "~/admin/plugins/fields/separator/separator.js"; export default [ headlessCmsPlugins(), @@ -62,6 +64,8 @@ export default [ selectFieldRenderer, checkboxesFieldRenderer, refFieldRenderer, + separatorField, + createSeparatorFieldRenderer(), editorGteFieldValidator, editorDateGteFieldValidator(), editorDateLteFieldValidator(), From 5de2791a301b0d0997b091c5102c5236bbc7f9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 5 Dec 2025 08:59:56 +0100 Subject: [PATCH 2/8] wip(app-headless-cms): separator field --- .../src/admin/plugins/fieldRenderers/separator/separator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx index b765e2284e8..27947454ef3 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx @@ -25,13 +25,13 @@ export const createSeparatorFieldRenderer = (): CmsModelFieldRendererPlugin => { // this is the color from the figma lineColor={"backgroundColor/backgroundColor-primary-default"} alignContent={"center"} + // @ts-expect-error content={ {field.label} } /> - {field.label} ------------------------------ ); From bc2fa8129ad422306861fa8e6a661cc74f9a7165 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Mon, 8 Dec 2025 13:43:38 +0100 Subject: [PATCH 3/8] fix: add "accent" variant --- .../src/Separator/Separator.stories.tsx | 138 +++++++++++++++++- packages/admin-ui/src/Separator/Separator.tsx | 3 +- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/packages/admin-ui/src/Separator/Separator.stories.tsx b/packages/admin-ui/src/Separator/Separator.stories.tsx index a4e787abc56..75739683dba 100644 --- a/packages/admin-ui/src/Separator/Separator.stories.tsx +++ b/packages/admin-ui/src/Separator/Separator.stories.tsx @@ -37,7 +37,7 @@ export const Documentation: Story = { variant: { description: "The visual style variant of the separator.", control: "select", - options: ["transparent", "base", "dimmed", "muted", "strong"] + options: ["transparent", "base", "dimmed", "muted", "strong", "accent"] }, orientation: { description: "The orientation of the separator.", @@ -101,7 +101,7 @@ export const Default: Story = { }, variant: { control: "select", - options: ["transparent", "base", "dimmed", "muted", "strong"] + options: ["transparent", "base", "dimmed", "muted", "strong", "accent"] } } }; @@ -213,3 +213,137 @@ export const MoreMargin: Story = { ); } }; + +export const Transparent: Story = { + render: () => { + return ( +
+
+ {"Transparent Variant"} + + {"This separator is transparent and not visible"} + +
+ +
+ {"This is text 1."} +
+
+ ); + } +}; + +export const Base: Story = { + render: () => { + return ( +
+
+ {"Base Variant"} + + {"This separator uses bg-white"} + +
+ +
+ {"This is text 1."} +
+
+ ); + } +}; + +export const Muted: Story = { + render: () => { + return ( +
+
+ {"Muted Variant"} + + {"This separator uses bg-neutral-muted"} + +
+ +
+ {"This is text 1."} +
+
+ ); + } +}; + +export const Strong: Story = { + render: () => { + return ( +
+
+ {"Strong Variant"} + + {"This separator uses bg-neutral-strong"} + +
+ +
+ {"This is text 1."} +
+
+ ); + } +}; + +export const Accent: Story = { + render: () => { + return ( +
+
+ {"Accent Variant"} + + {"This separator uses bg-primary"} + +
+ +
+ {"This is text 1."} +
+
+ ); + } +}; + +export const AllVariants: Story = { + render: () => { + return ( +
+
+ {"Transparent"} + + {"Not visible"} +
+
+ {"Base"} + + {"bg-white"} +
+
+ {"Dimmed (default)"} + + {"bg-neutral-dimmed"} +
+
+ {"Muted"} + + {"bg-neutral-muted"} +
+
+ {"Strong"} + + {"bg-neutral-strong"} +
+
+ {"Accent"} + + {"bg-primary"} +
+
+ ); + } +}; diff --git a/packages/admin-ui/src/Separator/Separator.tsx b/packages/admin-ui/src/Separator/Separator.tsx index c7e8933c318..8e5bbbea8bc 100644 --- a/packages/admin-ui/src/Separator/Separator.tsx +++ b/packages/admin-ui/src/Separator/Separator.tsx @@ -21,7 +21,8 @@ const separatorVariants = cva("shrink-0", { base: "bg-white", dimmed: "bg-neutral-dimmed", muted: "bg-neutral-muted", - strong: "bg-neutral-strong" + strong: "bg-neutral-strong", + accent: "bg-primary" } }, compoundVariants: [ From eb55ea52bf48ff4a0e998da952c72776aa4de417 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Mon, 8 Dec 2025 13:51:41 +0100 Subject: [PATCH 4/8] fix: add label support to Separator component --- .../src/Separator/Separator.stories.tsx | 136 +++++++++++++++++- packages/admin-ui/src/Separator/Separator.tsx | 92 ++++++++++-- 2 files changed, 217 insertions(+), 11 deletions(-) diff --git a/packages/admin-ui/src/Separator/Separator.stories.tsx b/packages/admin-ui/src/Separator/Separator.stories.tsx index 75739683dba..5b0e78e1549 100644 --- a/packages/admin-ui/src/Separator/Separator.stories.tsx +++ b/packages/admin-ui/src/Separator/Separator.stories.tsx @@ -26,7 +26,9 @@ export const Documentation: Story = { variant: "dimmed", margin: "none", orientation: "horizontal", - decorative: true + decorative: true, + children: undefined, + labelPosition: "middle" }, argTypes: { margin: { @@ -48,6 +50,16 @@ export const Documentation: Story = { description: "Whether the separator is purely decorative and should be hidden from screen readers.", control: "boolean" + }, + children: { + description: + "Optional label text to display with the separator. Text will use text-neutral-primary text-md font-semibold styles.", + control: "text" + }, + labelPosition: { + description: "Position of the label when children are provided.", + control: "select", + options: ["start", "middle", "end"] } }, render: props => { @@ -347,3 +359,125 @@ export const AllVariants: Story = { ); } }; + +export const WithLabelMiddle: Story = { + render: () => { + return ( +
+
+ {"Separator with label in the middle"} + + {"Label is positioned in the center (default)"} + + + {"OR"} + +
+
+ + {"Section Title"} + +
+
+ + {"Important"} + +
+
+ ); + } +}; + +export const WithLabelStart: Story = { + render: () => { + return ( +
+
+ {"Separator with label at start"} + + {"Label is positioned at the beginning"} + + + {"Start"} + +
+
+ + {"Section 1"} + +
+
+ + {"New Feature"} + +
+
+ ); + } +}; + +export const WithLabelEnd: Story = { + render: () => { + return ( +
+
+ {"Separator with label at end"} + + {"Label is positioned at the end"} + + + {"End"} + +
+
+ + {"See More"} + +
+
+ + {"Continue"} + +
+
+ ); + } +}; + +export const WithLabelVertical: Story = { + render: () => { + return ( +
+ + {"Vertical separator with labels"} + +
+
+ + {"Start"} + + + {"Top"} + +
+
+ + {"Middle"} + + + {"Center"} + +
+
+ + {"End"} + + + {"Bottom"} + +
+
+
+ ); + } +}; diff --git a/packages/admin-ui/src/Separator/Separator.tsx b/packages/admin-ui/src/Separator/Separator.tsx index 8e5bbbea8bc..0fdd4429d92 100644 --- a/packages/admin-ui/src/Separator/Separator.tsx +++ b/packages/admin-ui/src/Separator/Separator.tsx @@ -44,23 +44,95 @@ const separatorVariants = cva("shrink-0", { } }); -type SeparatorProps = SeparatorPrimitive.SeparatorProps & VariantProps; +type SeparatorPosition = "start" | "middle" | "end"; + +type SeparatorProps = Omit & + VariantProps & { + children?: React.ReactNode; + labelPosition?: SeparatorPosition; + }; const SeparatorBase = ({ className, - orientation, + orientation = "horizontal", margin, variant, decorative = true, + children, + labelPosition = "middle", ...props -}: SeparatorProps) => ( - -); +}: SeparatorProps) => { + // If no children, render simple separator + if (!children) { + return ( + + ); + } + + // With children, render separator with label + const isHorizontal = orientation === "horizontal"; + const containerClass = isHorizontal + ? "flex items-center w-full" + : "flex flex-col items-center h-full"; + const separatorClass = separatorVariants({ orientation, variant }); + const labelClass = "text-neutral-primary text-md font-semibold px-md"; + + const renderContent = () => { + if (labelPosition === "start") { + return ( + <> + {children} + + + ); + } + + if (labelPosition === "end") { + return ( + <> + + {children} + + ); + } + + // middle (default) + return ( + <> + + {children} + + + ); + }; + + return
{renderContent()}
; +}; const Separator = makeDecoratable("Separator", SeparatorBase); From fc5047cad4140bd2410df09c03a34fbc4ee2751e Mon Sep 17 00:00:00 2001 From: adrians5j Date: Mon, 8 Dec 2025 13:52:00 +0100 Subject: [PATCH 5/8] feat: update Separator component to use "accent" variant and simplify label rendering --- .../plugins/fieldRenderers/separator/separator.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx index 27947454ef3..6a95b3361dd 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/separator/separator.tsx @@ -20,18 +20,7 @@ export const createSeparatorFieldRenderer = (): CmsModelFieldRendererPlugin => { return ( - - {field.label} - - } - /> + {field.label} ); From 625e94cee6b89a7ac0a11e771be4bdf49a9503bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Thu, 11 Dec 2025 09:58:17 +0100 Subject: [PATCH 6/8] feat(api-headless-cms): user can disable showing field in api --- .../graphql/schema/createFieldResolvers.ts | 10 +++------ .../src/graphqlFields/text.ts | 22 +++++++++++++++++-- .../api-headless-cms/src/types/modelField.ts | 10 +++++++-- .../plugins/fields/separator/separator.tsx | 3 +++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts b/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts index 40802b9f58c..1c9d9019b39 100644 --- a/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts +++ b/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts @@ -1,13 +1,7 @@ import set from "lodash/set.js"; import type { Resolvers } from "@webiny/handler-graphql/types.js"; import WebinyError from "@webiny/error"; -import type { - ApiEndpoint, - CmsContext, - CmsFieldTypePlugins, - CmsModel, - CmsModelField -} from "~/types/index.js"; +import type { ApiEndpoint, CmsContext, CmsFieldTypePlugins, CmsModel, CmsModelField } from "~/types/index.js"; import { entryFieldFromStorageTransform } from "~/utils/entryStorage.js"; import { getBaseFieldType } from "~/utils/getBaseFieldType.js"; @@ -53,6 +47,8 @@ export const createFieldResolversFactory = (factoryParams: CreateFieldResolversF field } ); + } else if (field.settings?.showInApi === false) { + return null; } const createResolver = plugin[endpointType]?.createResolver || null; diff --git a/packages/api-headless-cms/src/graphqlFields/text.ts b/packages/api-headless-cms/src/graphqlFields/text.ts index d2eaa08251e..fa7454c4210 100644 --- a/packages/api-headless-cms/src/graphqlFields/text.ts +++ b/packages/api-headless-cms/src/graphqlFields/text.ts @@ -2,9 +2,15 @@ import type { CmsModelField, CmsModelFieldToGraphQLPlugin } from "~/types/index. import { createGraphQLInputField } from "./helpers.js"; interface CreateListFiltersParams { - field: CmsModelField; + field: Pick; } -const createListFilters = ({ field }: CreateListFiltersParams) => { +const createListFilters = ({ field }: CreateListFiltersParams): string => { + /** + * User settings can disable showing the field in the API + */ + if (field.settings?.showInApi === false) { + return ""; + } return ` ${field.fieldId}: String ${field.fieldId}_not: String @@ -27,12 +33,18 @@ export const createTextField = (): CmsModelFieldToGraphQLPlugin => { fullTextSearch: true, read: { createTypeField({ field }) { + if (field.settings?.showInApi === false) { + return ""; + } if (field.multipleValues) { return `${field.fieldId}: [String]`; } return `${field.fieldId}: String`; }, createGetFilters({ field }) { + if (field.settings?.showInApi === false) { + return ""; + } return `${field.fieldId}: String`; }, createListFilters @@ -40,12 +52,18 @@ export const createTextField = (): CmsModelFieldToGraphQLPlugin => { manage: { createListFilters, createTypeField({ field }) { + if (field.settings?.showInApi === false) { + return ""; + } if (field.multipleValues) { return `${field.fieldId}: [String]`; } return `${field.fieldId}: String`; }, createInputField({ field }) { + if (field.settings?.showInApi === false) { + return ""; + } return createGraphQLInputField(field, "String"); } } diff --git a/packages/api-headless-cms/src/types/modelField.ts b/packages/api-headless-cms/src/types/modelField.ts index 4cd07a43f48..e3d7b640252 100644 --- a/packages/api-headless-cms/src/types/modelField.ts +++ b/packages/api-headless-cms/src/types/modelField.ts @@ -168,7 +168,10 @@ export interface CmsModelFieldInput { /** * User defined settings. */ - settings?: Record; + settings?: { + showInApi?: boolean; + [key: string]: any; + }; } /** @@ -303,7 +306,10 @@ export interface CmsModelFieldSettings { * Disable full text search explicitly on this field. */ disableFullTextSearch?: boolean; - + /** + * Should the field be exposed in the API? + */ + showInApi?: boolean; /** * There are a lot of other settings that are possible to add, so we keep the type opened. */ diff --git a/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx b/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx index 194da89e447..68058f61e0d 100644 --- a/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fields/separator/separator.tsx @@ -22,6 +22,9 @@ export const separatorField: CmsModelFieldTypePlugin = { type: this.type, renderer: { name: "text-separator" + }, + settings: { + showInApi: false } }; } From 5e8b7d812be3dcb1b7e3f00314f774fa7ae131ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Thu, 11 Dec 2025 10:03:33 +0100 Subject: [PATCH 7/8] chore: prettier --- .../src/graphql/schema/createFieldResolvers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts b/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts index 1c9d9019b39..ba8ce4cad6e 100644 --- a/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts +++ b/packages/api-headless-cms/src/graphql/schema/createFieldResolvers.ts @@ -1,7 +1,13 @@ import set from "lodash/set.js"; import type { Resolvers } from "@webiny/handler-graphql/types.js"; import WebinyError from "@webiny/error"; -import type { ApiEndpoint, CmsContext, CmsFieldTypePlugins, CmsModel, CmsModelField } from "~/types/index.js"; +import type { + ApiEndpoint, + CmsContext, + CmsFieldTypePlugins, + CmsModel, + CmsModelField +} from "~/types/index.js"; import { entryFieldFromStorageTransform } from "~/utils/entryStorage.js"; import { getBaseFieldType } from "~/utils/getBaseFieldType.js"; From bb4fdb8f3e807aabfda47257edd5531d4da4eede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Thu, 11 Dec 2025 10:22:07 +0100 Subject: [PATCH 8/8] fix(api-headless-cms): tests --- .../__tests__/contentAPI/resolvers.read.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts index 1e9e1800f0f..6f862e661b7 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts @@ -956,9 +956,9 @@ describe("READ - Resolvers", () => { const { listCategories } = useCategoryReadHandler(readOpts); const from = new Date(vegetables.savedOn); - from.setTime(from.getTime() - 10); + from.setTime(from.getTime() - 5); const to = new Date(vegetables.savedOn); - to.setTime(to.getTime() + 10); + to.setTime(to.getTime() + 5); const [result] = await listCategories({ where: {