mirror of
https://github.com/docmost/docmost.git
synced 2026-05-20 16:44:05 +08:00
feat: bases - WIP
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BasePropertyType = {
|
||||
TEXT: 'text',
|
||||
NUMBER: 'number',
|
||||
SELECT: 'select',
|
||||
STATUS: 'status',
|
||||
MULTI_SELECT: 'multiSelect',
|
||||
DATE: 'date',
|
||||
PERSON: 'person',
|
||||
FILE: 'file',
|
||||
CHECKBOX: 'checkbox',
|
||||
URL: 'url',
|
||||
EMAIL: 'email',
|
||||
CREATED_AT: 'createdAt',
|
||||
LAST_EDITED_AT: 'lastEditedAt',
|
||||
LAST_EDITED_BY: 'lastEditedBy',
|
||||
} as const;
|
||||
|
||||
const SYSTEM_PROPERTY_TYPES: Set<string> = new Set([
|
||||
BasePropertyType.CREATED_AT,
|
||||
BasePropertyType.LAST_EDITED_AT,
|
||||
BasePropertyType.LAST_EDITED_BY,
|
||||
]);
|
||||
|
||||
export function isSystemPropertyType(type: string): boolean {
|
||||
return SYSTEM_PROPERTY_TYPES.has(type);
|
||||
}
|
||||
|
||||
export type BasePropertyTypeValue =
|
||||
(typeof BasePropertyType)[keyof typeof BasePropertyType];
|
||||
|
||||
export const BASE_PROPERTY_TYPES = Object.values(BasePropertyType);
|
||||
|
||||
export const choiceSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
color: z.string(),
|
||||
category: z.enum(['todo', 'inProgress', 'complete']).optional(),
|
||||
});
|
||||
|
||||
export const selectTypeOptionsSchema = z
|
||||
.object({
|
||||
choices: z.array(choiceSchema).default([]),
|
||||
choiceOrder: z.array(z.string().uuid()).default([]),
|
||||
disableColors: z.boolean().optional(),
|
||||
defaultValue: z
|
||||
.union([z.string().uuid(), z.array(z.string().uuid())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const numberTypeOptionsSchema = z
|
||||
.object({
|
||||
format: z
|
||||
.enum(['plain', 'currency', 'percent', 'progress'])
|
||||
.optional()
|
||||
.default('plain'),
|
||||
precision: z.number().int().min(0).max(10).optional(),
|
||||
currencySymbol: z.string().max(5).optional(),
|
||||
defaultValue: z.number().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const dateTypeOptionsSchema = z
|
||||
.object({
|
||||
dateFormat: z.string().optional(),
|
||||
timeFormat: z.enum(['12h', '24h']).optional(),
|
||||
includeTime: z.boolean().optional(),
|
||||
defaultValue: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const textTypeOptionsSchema = z
|
||||
.object({
|
||||
richText: z.boolean().optional(),
|
||||
defaultValue: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const checkboxTypeOptionsSchema = z
|
||||
.object({
|
||||
defaultValue: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const urlTypeOptionsSchema = z
|
||||
.object({
|
||||
defaultValue: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const emailTypeOptionsSchema = z
|
||||
.object({
|
||||
defaultValue: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const emptyTypeOptionsSchema = z.object({}).passthrough();
|
||||
|
||||
const typeOptionsSchemaMap: Record<BasePropertyTypeValue, z.ZodType> = {
|
||||
[BasePropertyType.TEXT]: textTypeOptionsSchema,
|
||||
[BasePropertyType.NUMBER]: numberTypeOptionsSchema,
|
||||
[BasePropertyType.SELECT]: selectTypeOptionsSchema,
|
||||
[BasePropertyType.STATUS]: selectTypeOptionsSchema,
|
||||
[BasePropertyType.MULTI_SELECT]: selectTypeOptionsSchema,
|
||||
[BasePropertyType.DATE]: dateTypeOptionsSchema,
|
||||
[BasePropertyType.PERSON]: emptyTypeOptionsSchema,
|
||||
[BasePropertyType.FILE]: emptyTypeOptionsSchema,
|
||||
[BasePropertyType.CHECKBOX]: checkboxTypeOptionsSchema,
|
||||
[BasePropertyType.URL]: urlTypeOptionsSchema,
|
||||
[BasePropertyType.EMAIL]: emailTypeOptionsSchema,
|
||||
[BasePropertyType.CREATED_AT]: emptyTypeOptionsSchema,
|
||||
[BasePropertyType.LAST_EDITED_AT]: emptyTypeOptionsSchema,
|
||||
[BasePropertyType.LAST_EDITED_BY]: emptyTypeOptionsSchema,
|
||||
};
|
||||
|
||||
export function validateTypeOptions(
|
||||
type: BasePropertyTypeValue,
|
||||
typeOptions: unknown,
|
||||
): z.SafeParseReturnType<unknown, unknown> {
|
||||
const schema = typeOptionsSchemaMap[type];
|
||||
if (!schema) {
|
||||
return { success: false, error: new z.ZodError([{ code: 'custom', message: `Unknown property type: ${type}`, path: ['type'] }]) } as z.SafeParseError<unknown>;
|
||||
}
|
||||
return schema.safeParse(typeOptions ?? {});
|
||||
}
|
||||
|
||||
export function parseTypeOptions(
|
||||
type: BasePropertyTypeValue,
|
||||
typeOptions: unknown,
|
||||
): unknown {
|
||||
const result = validateTypeOptions(type, typeOptions);
|
||||
if (!result.success) {
|
||||
throw result.error;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const cellValueSchemaMap: Partial<Record<BasePropertyTypeValue, z.ZodType>> = {
|
||||
[BasePropertyType.TEXT]: z.string(),
|
||||
[BasePropertyType.NUMBER]: z.number(),
|
||||
[BasePropertyType.SELECT]: z.string().uuid(),
|
||||
[BasePropertyType.STATUS]: z.string().uuid(),
|
||||
[BasePropertyType.MULTI_SELECT]: z.array(z.string().uuid()),
|
||||
[BasePropertyType.DATE]: z.string(),
|
||||
[BasePropertyType.PERSON]: z.array(z.string().uuid()),
|
||||
[BasePropertyType.FILE]: z.array(z.string().uuid()),
|
||||
[BasePropertyType.CHECKBOX]: z.boolean(),
|
||||
[BasePropertyType.URL]: z.string().url(),
|
||||
[BasePropertyType.EMAIL]: z.string().email(),
|
||||
};
|
||||
|
||||
export function getCellValueSchema(
|
||||
type: BasePropertyTypeValue,
|
||||
): z.ZodType | undefined {
|
||||
return cellValueSchemaMap[type];
|
||||
}
|
||||
|
||||
export function validateCellValue(
|
||||
type: BasePropertyTypeValue,
|
||||
value: unknown,
|
||||
): z.SafeParseReturnType<unknown, unknown> {
|
||||
const schema = cellValueSchemaMap[type];
|
||||
if (!schema) {
|
||||
return { success: false, error: new z.ZodError([{ code: 'custom', message: `Unknown property type: ${type}`, path: [] }]) } as z.SafeParseError<unknown>;
|
||||
}
|
||||
return schema.safeParse(value);
|
||||
}
|
||||
|
||||
export function attemptCellConversion(
|
||||
fromType: BasePropertyTypeValue,
|
||||
toType: BasePropertyTypeValue,
|
||||
value: unknown,
|
||||
): { converted: boolean; value: unknown } {
|
||||
if (value === null || value === undefined) {
|
||||
return { converted: true, value: null };
|
||||
}
|
||||
|
||||
const targetSchema = cellValueSchemaMap[toType];
|
||||
if (!targetSchema) {
|
||||
return { converted: false, value: null };
|
||||
}
|
||||
|
||||
const directResult = targetSchema.safeParse(value);
|
||||
if (directResult.success) {
|
||||
return { converted: true, value: directResult.data };
|
||||
}
|
||||
|
||||
if (toType === BasePropertyType.TEXT) {
|
||||
return { converted: true, value: String(value) };
|
||||
}
|
||||
|
||||
if (toType === BasePropertyType.NUMBER && typeof value === 'string') {
|
||||
const num = Number(value);
|
||||
if (!isNaN(num)) {
|
||||
return { converted: true, value: num };
|
||||
}
|
||||
}
|
||||
|
||||
if (toType === BasePropertyType.CHECKBOX) {
|
||||
if (typeof value === 'string') {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'true' || lower === '1' || lower === 'yes') {
|
||||
return { converted: true, value: true };
|
||||
}
|
||||
if (lower === 'false' || lower === '0' || lower === 'no' || lower === '') {
|
||||
return { converted: true, value: false };
|
||||
}
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return { converted: true, value: value !== 0 };
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
toType === BasePropertyType.MULTI_SELECT &&
|
||||
fromType === BasePropertyType.SELECT &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
return { converted: true, value: [value] };
|
||||
}
|
||||
|
||||
if (
|
||||
toType === BasePropertyType.SELECT &&
|
||||
fromType === BasePropertyType.MULTI_SELECT &&
|
||||
Array.isArray(value) &&
|
||||
value.length > 0
|
||||
) {
|
||||
return { converted: true, value: value[0] };
|
||||
}
|
||||
|
||||
return { converted: false, value: null };
|
||||
}
|
||||
|
||||
export const viewSortSchema = z.object({
|
||||
propertyId: z.string().uuid(),
|
||||
direction: z.enum(['asc', 'desc']),
|
||||
});
|
||||
|
||||
export const viewFilterSchema = z.object({
|
||||
propertyId: z.string().uuid(),
|
||||
operator: z.enum([
|
||||
'equals',
|
||||
'notEquals',
|
||||
'contains',
|
||||
'notContains',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'greaterThan',
|
||||
'lessThan',
|
||||
'before',
|
||||
'after',
|
||||
]),
|
||||
value: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const viewConfigSchema = z
|
||||
.object({
|
||||
sorts: z.array(viewSortSchema).optional(),
|
||||
filters: z.array(viewFilterSchema).optional(),
|
||||
visiblePropertyIds: z.array(z.string().uuid()).optional(),
|
||||
hiddenPropertyIds: z.array(z.string().uuid()).optional(),
|
||||
propertyWidths: z.record(z.string(), z.number().positive()).optional(),
|
||||
propertyOrder: z.array(z.string().uuid()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type ViewConfig = z.infer<typeof viewConfigSchema>;
|
||||
Reference in New Issue
Block a user