studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
360 lines (359 loc) • 10.3 kB
JavaScript
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
};