studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
737 lines (669 loc) • 21.3 kB
text/typescript
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;
}