UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

737 lines (669 loc) 21.3 kB
import type { AvailableIcons } from 'studiocms:ui/icons'; import type { AstroIntegration } from 'astro'; import { z } from 'astro/zod'; import type { SanitizeOptions } from 'ultrahtml/transformers/sanitize'; import { availableTranslationFileKeys } from '../../virtuals/i18n/v-files.js'; // https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113 export const astroIntegrationSchema = z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}), }) as z.Schema<AstroIntegration>; // export const ValidationFunction = z.function().args(z.any()).returns(z.string().or(z.boolean())); // export const TransformFunction = z.function().args(z.any()).returns(z.any()); /** * Schema for a StudioCMS colorway. */ export const StudioCMSColorway = z.enum([ 'primary', 'success', 'warning', 'danger', 'info', 'mono', ]); /** * Schema for a base field. * * This schema includes the following properties: * - `name`: The name of the field used in the form submission data. * - `label`: The label of the field displayed in the form. * - `required`: A boolean indicating whether the field is required. * - `readOnly`: A boolean indicating whether the field is read-only (disabled). */ const BaseFieldSchema = z.object({ /** * The name of the field used in the form submission data */ name: z.string(), /** * The label of the field displayed in the form */ label: z.string(), /** * Is the field required */ required: z.boolean().optional(), /** * Is the field read only (disabled) */ readOnly: z.boolean().optional(), }); /** * Schema for a field that supports a colorway. * * This schema extends the `BaseFieldSchema` and includes the following properties: * - `color`: An optional enum specifying the colorway of the field. Possible values are 'primary', 'success', 'warning', 'danger', 'info', and 'mono'. */ const SupportsColorSchema = BaseFieldSchema.extend({ color: StudioCMSColorway.optional(), }); /** * Schema for a field that supports a placeholder. * * This schema extends the `BaseFieldSchema` and includes the following properties: * - `placeholder`: An optional string representing the placeholder text for the field. */ const SupportsPlaceHolderSchema = BaseFieldSchema.extend({ placeholder: z.string().optional(), }); /** * Schema for a checkbox field. * * This schema extends the `SupportsColorSchema` and includes the following properties: * - `input`: A literal string 'checkbox' indicating the type of input field. * - `defaultChecked`: An optional boolean indicating whether the checkbox is checked by default. * - `size`: An optional enum specifying the size of the checkbox. Possible values are 'sm', 'md', and 'lg'. */ const CheckboxFieldSchema = SupportsColorSchema.extend({ input: z.literal('checkbox'), defaultChecked: z.boolean().optional(), size: z.enum(['sm', 'md', 'lg']).optional(), }); /** * Schema for a text input field. * * This schema extends the `SupportsPlaceHolderSchema` and includes the following properties: * - `input`: A literal string 'input' indicating the type of input field. * - `type`: An optional enum specifying the type of the input field. Possible values are 'text', 'password', 'email', 'number', 'tel', 'url', and 'search'. * - `defaultValue`: An optional string representing the default value of the input field. */ const TextInputFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal('input'), type: z.enum(['text', 'password', 'email', 'number', 'tel', 'url', 'search']).optional(), defaultValue: z.string().optional(), }); /** * Schema for a text area field. * * This schema extends the `SupportsPlaceHolderSchema` and includes the following properties: * - `input`: A literal string 'textarea' indicating the type of input field. * - `defaultValue`: An optional string representing the default value of the text area. */ const TextAreaFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal('textarea'), defaultValue: z.string().optional(), }); /** * Schema for shared options. * * This schema defines an array of objects, where each object represents an option with the following properties: * - `label`: A string representing the label of the option. * - `value`: A string representing the value of the option. * - `disabled`: An optional boolean indicating whether the option is disabled. */ const SharedOptionsSchema = z.array( z.object({ label: z.string(), value: z.string(), disabled: z.boolean().optional(), }) ); /** * Schema for a radio group field. * * This schema extends the SupportsColorSchema and includes additional properties * specific to a radio group field. * * @property {string} input - Must be the literal string 'radio'. * @property {'horizontal' | 'vertical'} [direction] - Optional direction of the radio group, can be either 'horizontal' or 'vertical'. * @property {string} [defaultValue] - Optional default value for the radio group. * @property {SharedOptionsSchema} options - Options for the radio group, defined by the SharedOptionsSchema. */ const RadioGroupFieldSchema = SupportsColorSchema.extend({ input: z.literal('radio'), direction: z.enum(['horizontal', 'vertical']).optional(), defaultValue: z.string().optional(), options: SharedOptionsSchema, }); /** * Schema for a select field, extending the SupportsPlaceHolderSchema. * * @extends SupportsPlaceHolderSchema * * @property {z.Literal<'select'>} input - Specifies that the input type is 'select'. * @property {z.ZodOptional<z.ZodEnum<['basic', 'search']>>} type - Optional type of the select field, can be 'basic' or 'search'. * @property {z.ZodOptional<z.ZodString>} defaultValue - Optional default value for the select field. * @property {SharedOptionsSchema} options - Schema for the options available in the select field. */ const SelectFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal('select'), type: z.enum(['basic', 'search']).optional(), defaultValue: z.string().optional(), options: SharedOptionsSchema, }); /** * A union schema that represents different types of field schemas. * This schema can be one of the following: * - CheckboxFieldSchema * - TextInputFieldSchema * - TextAreaFieldSchema * - RadioGroupFieldSchema * - SelectFieldSchema */ export const FieldSchema = z.union([ CheckboxFieldSchema, TextInputFieldSchema, TextAreaFieldSchema, RadioGroupFieldSchema, SelectFieldSchema, ]); /** * Schema definition for a row field. * * This schema extends the BaseFieldSchema and includes additional properties specific to a row field. * * Properties: * - `input`: A literal type with the value 'row'. * - `alignCenter`: An optional boolean indicating whether the row should be center-aligned. * - `gapSize`: An optional enum specifying the gap size between fields. Possible values are 'sm', 'md', and 'lg'. * - `fields`: A lazy-loaded array of FieldSchema, allowing for recursive field definitions. */ const RowFieldSchema = BaseFieldSchema.extend({ input: z.literal('row'), alignCenter: z.boolean().optional(), gapSize: z.enum(['sm', 'md', 'lg']).optional(), fields: z.lazy(() => FieldSchema.array()), // Recursive definition }); /** * A schema that represents the settings field. * It is a union of `FieldSchema` and `RowFieldSchema`. */ export const SettingsFieldSchema = z.union([FieldSchema, RowFieldSchema]); /** * Represents the type inferred from the `SettingsFieldSchema` schema. * * This type is used to define the structure of settings fields within the application. */ export type SettingsField = z.infer<typeof SettingsFieldSchema>; /** * A custom schema for i18n label translations. */ export const i18nLabelSchema = z.record(z.string().min(1)).superRefine((val, ctx) => { const unknown = Object.keys(val).filter((k) => !availableTranslationFileKeys.includes(k)); if (unknown.length) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unsupported locale keys: ${unknown.join(', ')}. Allowed: ${availableTranslationFileKeys.join(', ')}.`, }); } }); /** * Schema for a base dashboard page props. * * This schema includes the following properties: * - `title`: The title of the dashboard page. * - `description`: The description of the dashboard page. * - `requiredPermissions`: A string literal representing the required permissions to access the page. * - `pageHeaderComponent`: The component to render in the page header. * - `pageBodyComponent`: The component to render in the page body. */ const BaseDashboardPagePropsSchema = z.object({ /** * The title of the dashboard page */ title: i18nLabelSchema, /** * The description of the dashboard page */ description: z.string(), /** * The desired route of the dashboard page */ route: z.string(), /** * The icon to display in the sidebar * * @default 'cube-transparent' * @optional */ icon: z.string().default('heroicons:cube-transparent').optional(), /** * The required permissions to access the page */ requiredPermissions: z .union([ z.literal('owner'), z.literal('admin'), z.literal('editor'), z.literal('visitor'), z.literal('none'), ]) .default('none') .optional(), /** * The component to render in the page header to display action buttons */ pageActionsComponent: z.string().optional(), /** * The component to render in the page body */ pageBodyComponent: z.string(), }); /** * Schema for a base dashboard page props. * * This schema extends the `BaseDashboardPagePropsSchema` and includes the following properties: * - `slug`: The slug of the dashboard page. */ const AvailableBaseSchema = BaseDashboardPagePropsSchema.extend({ /** * The slug of the dashboard page */ slug: z.string(), }); /** * Schema for a custom Astro component. */ // biome-ignore lint/suspicious/noExplicitAny: This is a valid use case for explicit any. const AstroComponentSchema = z.custom<(_props: any) => any>(); /** * Schema for a base dashboard page props. * * This schema extends the `AvailableBaseSchema` and includes the following properties: * - `components`: An object containing the components to render in the dashboard page. */ const FinalBaseSchema = AvailableBaseSchema.extend({ components: z.object({ PageActionsComponent: AstroComponentSchema.optional(), PageBodyComponent: AstroComponentSchema, InnerSidebarComponent: AstroComponentSchema.optional(), }), }); /** * Schema for a single sidebar dashboard page. * * This schema extends the `BaseDashboardPagePropsSchema` and includes the following properties: * - `sidebar`: A literal string 'single' indicating a single sidebar layout. */ const SingleSidebarSchema = BaseDashboardPagePropsSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('single'), }); /** * Schema for a single sidebar dashboard page. * * This schema extends the `AvailableBaseSchema` and includes the following properties: * - `sidebar`: A literal string 'single' indicating a single sidebar layout. */ const AvailableSingleSchema = AvailableBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('single'), }); /** * Schema for a single sidebar dashboard page. * * This schema extends the `FinalBaseSchema` and includes the following properties: * - `sidebar`: A literal string 'single' indicating a single sidebar layout. */ const FinalSingleSchema = FinalBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('single'), }); /** * Schema for a double sidebar dashboard page. * * This schema extends the `BaseDashboardPagePropsSchema` and includes the following properties: * - `sidebar`: A literal string 'double' indicating a double sidebar layout. * - `innerSidebarComponent`: The component to render in the inner sidebar. */ const DoubleSidebarSchema = BaseDashboardPagePropsSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('double'), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string(), }); /** * Schema for a double sidebar dashboard page. * * This schema extends the `AvailableBaseSchema` and includes the following properties: * - `sidebar`: A literal string 'double' indicating a double sidebar layout. * - `innerSidebarComponent`: The component to render in the inner sidebar. */ const AvailableDoubleSchema = AvailableBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('double'), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string(), }); /** * Schema for a double sidebar dashboard page. * * This schema extends the `FinalBaseSchema` and includes the following properties: * - `sidebar`: A literal string 'double' indicating a double sidebar layout. * - `innerSidebarComponent`: The component to render in the inner sidebar. */ const FinalDoubleSchema = FinalBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal('double'), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string(), }); /** * A union schema that represents different types of dashboard page schemas. * This schema can be one of the following: * - SingleSidebarSchema * - DoubleSidebarSchema */ export const DashboardPageSchema = z.union([SingleSidebarSchema, DoubleSidebarSchema]); /** * A union schema that represents different types of available dashboard base schemas. * This schema can be one of the following: * - AvailableSingleSchema * - AvailableDoubleSchema */ export const AvailableDashboardBaseSchema = z.union([AvailableSingleSchema, AvailableDoubleSchema]); /** * A union schema that represents different types of final dashboard base schemas. * This schema can be one of the following: * - FinalSingleSchema * - FinalDoubleSchema */ export const FinalDashboardBaseSchema = z.union([FinalSingleSchema, FinalDoubleSchema]); /** * Schema for an array of available dashboard pages. * * This schema defines an object with the following properties: * - `user`: An optional array of `AvailableDashboardBaseSchema` representing the available dashboard pages for users. * - `admin`: An optional array of `AvailableDashboardBaseSchema` representing the available dashboard pages for admins. */ export const AvailableDashboardPagesSchema = z.object({ /** * Available dashboard pages for users */ user: z.array(AvailableDashboardBaseSchema).optional(), /** * Available dashboard pages for admins */ admin: z.array(AvailableDashboardBaseSchema).optional(), }); export type PageTypeDefaultsOrStringT = 'studiocms/markdown' | 'studiocms/html'; export const PageTypeDefaultsOrString = z .enum(['studiocms/markdown', 'studiocms/html']) .or(z.string()); export const SettingsPageSchema = z .object({ /** * Fields according to specification */ fields: z.array(SettingsFieldSchema), /** * The endpoint for the settings * * Should export a APIRoute named `onSave` that runs when the settings page is saved */ endpoint: z.string(), }) .optional(); export const FrontendNavigationLinksSchema = z .array( z.object({ /** * Display label for the link */ label: z.string(), /** * URL to link to */ href: z.string(), }) ) .optional(); export const PageTypesSchema = z .array( z.object({ /** * Label that is shown in the select input */ label: z.string(), /** * Identifier that is saved in the database * @example * // Single page type per plugin * 'studiocms' * '@studiocms/blog' * // Multiple page types per plugin (Use unique identifiers for each type to avoid conflicts) * '@mystudiocms/plugin:pageType1' * '@mystudiocms/plugin:pageType2' * '@mystudiocms/plugin:pageType3' * '@mystudiocms/plugin:pageType4' */ identifier: z.string(), /** * Description that is shown below the "Page Content" header if this type is selected */ description: z.string().optional(), /** * The path to the actual component that is displayed for the page content * * Component should have a `content` prop that is a string to be able to display current content. * * **NOTE:** If you storing a single string in the database, you can use the form name `page-content` for the content output. and it will be stored in the normal `content` field in the database. * You can also use the apiEndpoints to create custom endpoints for the page type. * * @example * ```ts * import { createResolver } from 'astro-integration-kit'; * const { resolve } = createResolver(import.meta.url) * * { * pageContentComponent: resolve('./components/MyContentEditor.astro'), * } * ``` */ pageContentComponent: PageTypeDefaultsOrString.or(z.string()).optional(), /** * The path to the actual component that is displayed for the page renderer */ rendererComponent: PageTypeDefaultsOrString.or(z.string()).optional(), /** * Fields that are shown in the page metadata tab when creating or editing a page of this type */ fields: z.array(SettingsFieldSchema).optional(), /** * API Endpoint file for the page type * * API endpoints are used to create, edit, and delete pages of this type, * endpoints will be provided the full Astro APIContext from the Astro APIRoute. * * File should export at least one of the following: * - `onCreate` * - `onEdit` * - `onDelete` * * @example * ```ts * // my-plugin.ts * import { createResolver } from 'astro-integration-kit'; * const { resolve } = createResolver(import.meta.url) * * { * apiEndpoint: resolve('./api/pageTypeApi.ts'), * } * * // api/pageTypeApi.ts * import { APIRoute } from 'astro'; * * export const onCreate: APIRoute = async (context) => { * // Custom logic here * return new Response(); * } * ``` */ apiEndpoint: z.string().optional(), }) ) .optional(); /** * Represents the type inferred from the `DashboardPageSchema` schema. * * This type is used to define the structure of dashboard pages within the application. */ export type DashboardPage = typeof DashboardPageSchema._input; /** * Represents the type inferred from the `AvailableDashboardBaseSchema` schema. * * This type is used to define the structure of available dashboard pages within the application. */ export type AvailableDashboardPages = typeof AvailableDashboardPagesSchema._output; /** * Represents the type inferred from the `FinalDashboardBaseSchema` schema. * * This type is used to define the structure of final dashboard pages within the application. */ export type FinalDashboardPage = typeof FinalDashboardBaseSchema._output; /** * Represents the input for a grid item in the dashboard. */ export interface GridItemInput { /** * The name of the grid item. */ name: string; /** * The span of the grid item, which can be 1, 2, or 3. */ span: 1 | 2 | 3; /** * The variant of the grid item, which can be 'default' or 'filled'. */ variant: 'default' | 'filled'; /** * The required permission level to view the grid item. * Optional. Can be 'owner', 'admin', 'editor', or 'visitor'. */ requiresPermission?: 'owner' | 'admin' | 'editor' | 'visitor'; /** * The header of the grid item. * Optional. */ header?: { /** * The title of the header. */ title: string; /** * The icon of the header. * Optional. */ icon?: AvailableIcons; }; /** * The body of the grid item. * Optional. */ body?: { /** * The HTML content of the body. */ html: string; /** * The components within the body. * Optional. */ components?: Record<string, string>; /** * The options for sanitizing the HTML content. * Optional. */ sanitizeOpts?: SanitizeOptions; }; } /** * Represents an item that can be used in a dashboard grid. */ export interface GridItemUsable { /** * The name of the grid item. */ name: string; /** * The span of the grid item, which can be 1, 2, or 3. */ span: 1 | 2 | 3; /** * The variant of the grid item, which can be 'default' or 'filled'. */ variant: 'default' | 'filled'; /** * Optional. The permission required to use the grid item. * Can be 'owner', 'admin', 'editor', or 'visitor'. */ requiresPermission?: 'owner' | 'admin' | 'editor' | 'visitor'; /** * Optional. The header of the grid item. */ header?: { /** * The title of the header. */ title: string; /** * Optional. The icon of the header. */ icon?: AvailableIcons; }; /** * Optional. The body of the grid item. */ body?: { /** * The HTML content of the body. */ html: string; /** * Optional. The components of the body. * */ // biome-ignore lint/suspicious/noExplicitAny: This is a valid use case for explicit any. components?: Record<string, any>; /** * Optional. The options for sanitizing the HTML content. */ sanitizeOpts?: SanitizeOptions; }; } /** * Represents an item in the dashboard grid. * Extends the properties of `GridItemUsable` and adds an `enabled` flag. */ export interface GridItem extends GridItemUsable { enabled: boolean; }