diff --git a/blocksuite/affine/model/src/blocks/frame/frame-model.ts b/blocksuite/affine/model/src/blocks/frame/frame-model.ts index 58dd9aa58..c3092f59e 100644 --- a/blocksuite/affine/model/src/blocks/frame/frame-model.ts +++ b/blocksuite/affine/model/src/blocks/frame/frame-model.ts @@ -1,6 +1,5 @@ import type { GfxBlockElementModel, - GfxCompatibleProps, GfxElementGeometry, GfxGroupCompatibleInterface, GfxModel, @@ -11,6 +10,7 @@ import { descendantElementsImpl, generateKeyBetweenV2, GfxCompatible, + GfxCompatibleZodSchema, gfxGroupCompatibleSymbol, hasDescendantElementImpl, } from '@blocksuite/block-std/gfx'; @@ -18,31 +18,33 @@ import { Bound } from '@blocksuite/global/utils'; import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store'; import { z } from 'zod'; -import { type Color, ColorSchema } from '../../themes/index.js'; - -export type FrameBlockProps = { - title: Text; - background: Color; - childElementIds?: Record; - presentationIndex?: string; -} & GfxCompatibleProps; +import { ColorSchema, DefaultTheme } from '../../themes/index.js'; export const FrameZodSchema = z .object({ - background: ColorSchema.optional(), + background: ColorSchema, + childElementIds: z.record(z.boolean()), + presentationIndex: z.string(), }) - .default({}); - -export const FrameBlockSchema = defineBlockSchema({ - flavour: 'affine:frame', - props: (internal): FrameBlockProps => ({ - title: internal.Text(), - background: 'transparent', + .and(GfxCompatibleZodSchema) + .default({ + background: DefaultTheme.transparent, xywh: `[0,0,100,100]`, index: 'a0', childElementIds: Object.create(null), presentationIndex: generateKeyBetweenV2(null, null), lockedBySelf: false, + }); + +export type FrameBlockProps = z.infer & { + title: Text; +}; + +export const FrameBlockSchema = defineBlockSchema({ + flavour: 'affine:frame', + props: (internal): FrameBlockProps => ({ + title: internal.Text(), + ...FrameZodSchema.parse(undefined), }), metadata: { version: 1, diff --git a/blocksuite/affine/shared/src/services/edit-props-store.ts b/blocksuite/affine/shared/src/services/edit-props-store.ts index 8e81dfc78..c59f044a0 100644 --- a/blocksuite/affine/shared/src/services/edit-props-store.ts +++ b/blocksuite/affine/shared/src/services/edit-props-store.ts @@ -139,7 +139,10 @@ export class EditPropsStore extends LifeCycleWatcher { } } - applyLastProps(key: LastPropsKey, props: Record) { + applyLastProps( + key: K, + props: Record + ) { if (['__proto__', 'constructor', 'prototype'].includes(key)) { throw new BlockSuiteError( ErrorCode.DefaultRuntimeError, diff --git a/blocksuite/affine/shared/src/utils/zod-schema.ts b/blocksuite/affine/shared/src/utils/zod-schema.ts index 63f6ae2b9..5697101fc 100644 --- a/blocksuite/affine/shared/src/utils/zod-schema.ts +++ b/blocksuite/affine/shared/src/utils/zod-schema.ts @@ -25,7 +25,14 @@ import { TextAlignSchema, TextVerticalAlign, } from '@blocksuite/affine-model'; -import { z, ZodDefault, ZodObject, type ZodTypeAny, ZodUnion } from 'zod'; +import { + z, + ZodDefault, + ZodIntersection, + ZodObject, + type ZodTypeAny, + ZodUnion, +} from 'zod'; const ConnectorEndpointSchema = z.nativeEnum(PointStyle); const LineWidthSchema = z.nativeEnum(LineWidth); @@ -183,6 +190,11 @@ export function makeDeepOptional(schema: ZodTypeAny): ZodTypeAny { return z.object(deepOptionalShape).optional(); } else if (schema instanceof ZodUnion) { return schema.or(z.undefined()); + } else if (schema instanceof ZodIntersection) { + return z.intersection( + makeDeepOptional(schema._def.left), + makeDeepOptional(schema._def.right) + ); } else { return schema.optional(); } diff --git a/blocksuite/framework/block-std/src/gfx/index.ts b/blocksuite/framework/block-std/src/gfx/index.ts index c5264aa8b..4c7cb0e59 100644 --- a/blocksuite/framework/block-std/src/gfx/index.ts +++ b/blocksuite/framework/block-std/src/gfx/index.ts @@ -29,8 +29,10 @@ export { export { GfxBlockElementModel, type GfxCommonBlockProps, + GfxCommonBlockZodSchema, GfxCompatibleBlockModel as GfxCompatible, type GfxCompatibleProps, + GfxCompatibleZodSchema, } from './model/gfx-block-model.js'; export { type GfxModel } from './model/model.js'; export { diff --git a/blocksuite/framework/block-std/src/gfx/model/gfx-block-model.ts b/blocksuite/framework/block-std/src/gfx/model/gfx-block-model.ts index 4174a53eb..b50c0cad6 100644 --- a/blocksuite/framework/block-std/src/gfx/model/gfx-block-model.ts +++ b/blocksuite/framework/block-std/src/gfx/model/gfx-block-model.ts @@ -15,8 +15,10 @@ import { polygonGetPointTangent, polygonNearestPoint, rotatePoints, + SerializedXYWHZodSchema, } from '@blocksuite/global/utils'; import { BlockModel } from '@blocksuite/store'; +import { z } from 'zod'; import { isLockedByAncestorImpl, @@ -33,20 +35,24 @@ import type { SurfaceBlockModel } from './surface/surface-model.js'; /** * The props that a graphics block model should have. */ -export type GfxCompatibleProps = { - xywh: SerializedXYWH; - index: string; - lockedBySelf?: boolean; -}; +export const GfxCompatibleZodSchema = z.object({ + xywh: SerializedXYWHZodSchema, + index: z.string(), + lockedBySelf: z.boolean().optional(), +}); +export type GfxCompatibleProps = z.infer; /** * This type include the common props for the graphic block model. * You can use this type with Omit to define the props of a graphic block model. */ -export type GfxCommonBlockProps = GfxCompatibleProps & { - rotate: number; - scale: number; -}; +export const GfxCommonBlockZodSchema = GfxCompatibleZodSchema.and( + z.object({ + rotate: z.number(), + scale: z.number(), + }) +); +export type GfxCommonBlockProps = z.infer; /** * The graphic block model that can be rendered in the graphics mode. diff --git a/blocksuite/framework/global/src/utils/index.ts b/blocksuite/framework/global/src/utils/index.ts index 3f47a5de6..77a428be3 100644 --- a/blocksuite/framework/global/src/utils/index.ts +++ b/blocksuite/framework/global/src/utils/index.ts @@ -16,4 +16,5 @@ export * from './slot.js'; export * from './types.js'; export * from './with-disposable.js'; export type { SerializedXYWH, XYWH } from './xywh.js'; +export { SerializedXYWHZodSchema } from './xywh.js'; export { deserializeXYWH, serializeXYWH } from './xywh.js'; diff --git a/blocksuite/framework/global/src/utils/xywh.ts b/blocksuite/framework/global/src/utils/xywh.ts index ed54df1be..e65869572 100644 --- a/blocksuite/framework/global/src/utils/xywh.ts +++ b/blocksuite/framework/global/src/utils/xywh.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + /** * XYWH represents the x, y, width, and height of an element or block. */ @@ -8,6 +10,30 @@ export type XYWH = [number, number, number, number]; */ export type SerializedXYWH = `[${number},${number},${number},${number}]`; +export const SerializedXYWHZodSchema = z.custom((val: any) => { + if (typeof val !== 'string') { + throw new Error('SerializedXYWH should be a string'); + } + + if (!val.startsWith('[') || !val.endsWith(']')) { + throw new Error('SerializedXYWH should be wrapped in square brackets'); + } + + const parts = val.slice(1, -1).split(','); + + if (parts.length !== 4) { + throw new Error('SerializedXYWH should have 4 parts'); + } + + for (const part of parts) { + if (!/^\d+$/.test(part)) { + throw new Error('Each part of SerializedXYWH should be a number'); + } + } + + return val as SerializedXYWH; +}); + export function serializeXYWH( x: number, y: number,