UNPKG

studiocms

Version:

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

360 lines (359 loc) 10.3 kB
import { z } from "astro/zod"; import { availableTranslationFileKeys } from "../../virtuals/i18n/v-files.js"; const astroIntegrationSchema = z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }); const StudioCMSColorway = z.enum([ "primary", "success", "warning", "danger", "info", "mono" ]); 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() }); const SupportsColorSchema = BaseFieldSchema.extend({ color: StudioCMSColorway.optional() }); const SupportsPlaceHolderSchema = BaseFieldSchema.extend({ placeholder: z.string().optional() }); const CheckboxFieldSchema = SupportsColorSchema.extend({ input: z.literal("checkbox"), defaultChecked: z.boolean().optional(), size: z.enum(["sm", "md", "lg"]).optional() }); const TextInputFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal("input"), type: z.enum(["text", "password", "email", "number", "tel", "url", "search"]).optional(), defaultValue: z.string().optional() }); const TextAreaFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal("textarea"), defaultValue: z.string().optional() }); const SharedOptionsSchema = z.array( z.object({ label: z.string(), value: z.string(), disabled: z.boolean().optional() }) ); const RadioGroupFieldSchema = SupportsColorSchema.extend({ input: z.literal("radio"), direction: z.enum(["horizontal", "vertical"]).optional(), defaultValue: z.string().optional(), options: SharedOptionsSchema }); const SelectFieldSchema = SupportsPlaceHolderSchema.extend({ input: z.literal("select"), type: z.enum(["basic", "search"]).optional(), defaultValue: z.string().optional(), options: SharedOptionsSchema }); const FieldSchema = z.union([ CheckboxFieldSchema, TextInputFieldSchema, TextAreaFieldSchema, RadioGroupFieldSchema, SelectFieldSchema ]); 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 }); const SettingsFieldSchema = z.union([FieldSchema, RowFieldSchema]); 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(", ")}.` }); } }); 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() }); const AvailableBaseSchema = BaseDashboardPagePropsSchema.extend({ /** * The slug of the dashboard page */ slug: z.string() }); const AstroComponentSchema = z.custom(); const FinalBaseSchema = AvailableBaseSchema.extend({ components: z.object({ PageActionsComponent: AstroComponentSchema.optional(), PageBodyComponent: AstroComponentSchema, InnerSidebarComponent: AstroComponentSchema.optional() }) }); const SingleSidebarSchema = BaseDashboardPagePropsSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("single") }); const AvailableSingleSchema = AvailableBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("single") }); const FinalSingleSchema = FinalBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("single") }); const DoubleSidebarSchema = BaseDashboardPagePropsSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("double"), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string() }); const AvailableDoubleSchema = AvailableBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("double"), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string() }); const FinalDoubleSchema = FinalBaseSchema.extend({ /** * The sidebar layout */ sidebar: z.literal("double"), /** * The component to render in the inner sidebar */ innerSidebarComponent: z.string() }); const DashboardPageSchema = z.union([SingleSidebarSchema, DoubleSidebarSchema]); const AvailableDashboardBaseSchema = z.union([AvailableSingleSchema, AvailableDoubleSchema]); const FinalDashboardBaseSchema = z.union([FinalSingleSchema, FinalDoubleSchema]); const AvailableDashboardPagesSchema = z.object({ /** * Available dashboard pages for users */ user: z.array(AvailableDashboardBaseSchema).optional(), /** * Available dashboard pages for admins */ admin: z.array(AvailableDashboardBaseSchema).optional() }); 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(); const FrontendNavigationLinksSchema = z.array( z.object({ /** * Display label for the link */ label: z.string(), /** * URL to link to */ href: z.string() }) ).optional(); 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: z.string().optional(), /** * The path to the file that contains the default exported object with the `PluginRenderer` interface. * * This is used to render the content on the frontend. * * ```ts * import { createResolver } from 'astro-integration-kit'; * const { resolve } = createResolver(import.meta.url); * * { * rendererComponent: resolve('./renderers/MyRenderer.ts'), * } * ``` * * with the following export in `MyRenderer.ts`: * ```ts * import type { PluginRenderer } from 'studiocms/types'; * * const MyRenderer: PluginRenderer = { * name: 'my-renderer', * sanitizeOpts: { * // ultrahtml sanitize options * }, * renderer: async (content) => { * // Custom rendering logic here * return `<div class="my-rendered-content">${content}</div>`; * }, * }; * * export default MyRenderer; * ``` */ rendererComponent: 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 { PluginAPIRoute } from 'studiocms/plugins'; * * export const onCreate: PluginAPIRoute<'onCreate'> = async ({ AstroCtx, pageData }) => { * // Custom logic here * return new Response(); * } * ``` */ apiEndpoint: z.string().optional() }) ).optional(); export { AvailableDashboardBaseSchema, AvailableDashboardPagesSchema, DashboardPageSchema, FieldSchema, FinalDashboardBaseSchema, FrontendNavigationLinksSchema, PageTypesSchema, SettingsFieldSchema, SettingsPageSchema, StudioCMSColorway, astroIntegrationSchema, i18nLabelSchema };