fix(editor): add missing zod schema for edgeless frame (#10024)

Related to https://github.com/toeverything/AFFiNE/pull/9970#discussion_r1944971309

### What changes:
- Add missing zod shcema for edgeless basic props
- Change `applyLastProps` to generic function for better return type inference of
- Fix: add `ZodIntersection` case to `makeDeepOptional`
This commit is contained in:
L-Sun
2025-02-07 12:49:59 +00:00
parent 36ed81bcc6
commit 459972fe6c
7 changed files with 80 additions and 28 deletions
@@ -1,6 +1,5 @@
import type { import type {
GfxBlockElementModel, GfxBlockElementModel,
GfxCompatibleProps,
GfxElementGeometry, GfxElementGeometry,
GfxGroupCompatibleInterface, GfxGroupCompatibleInterface,
GfxModel, GfxModel,
@@ -11,6 +10,7 @@ import {
descendantElementsImpl, descendantElementsImpl,
generateKeyBetweenV2, generateKeyBetweenV2,
GfxCompatible, GfxCompatible,
GfxCompatibleZodSchema,
gfxGroupCompatibleSymbol, gfxGroupCompatibleSymbol,
hasDescendantElementImpl, hasDescendantElementImpl,
} from '@blocksuite/block-std/gfx'; } from '@blocksuite/block-std/gfx';
@@ -18,31 +18,33 @@ import { Bound } from '@blocksuite/global/utils';
import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store'; import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store';
import { z } from 'zod'; import { z } from 'zod';
import { type Color, ColorSchema } from '../../themes/index.js'; import { ColorSchema, DefaultTheme } from '../../themes/index.js';
export type FrameBlockProps = {
title: Text;
background: Color;
childElementIds?: Record<string, boolean>;
presentationIndex?: string;
} & GfxCompatibleProps;
export const FrameZodSchema = z export const FrameZodSchema = z
.object({ .object({
background: ColorSchema.optional(), background: ColorSchema,
childElementIds: z.record(z.boolean()),
presentationIndex: z.string(),
}) })
.default({}); .and(GfxCompatibleZodSchema)
.default({
export const FrameBlockSchema = defineBlockSchema({ background: DefaultTheme.transparent,
flavour: 'affine:frame',
props: (internal): FrameBlockProps => ({
title: internal.Text(),
background: 'transparent',
xywh: `[0,0,100,100]`, xywh: `[0,0,100,100]`,
index: 'a0', index: 'a0',
childElementIds: Object.create(null), childElementIds: Object.create(null),
presentationIndex: generateKeyBetweenV2(null, null), presentationIndex: generateKeyBetweenV2(null, null),
lockedBySelf: false, lockedBySelf: false,
});
export type FrameBlockProps = z.infer<typeof FrameZodSchema> & {
title: Text;
};
export const FrameBlockSchema = defineBlockSchema({
flavour: 'affine:frame',
props: (internal): FrameBlockProps => ({
title: internal.Text(),
...FrameZodSchema.parse(undefined),
}), }),
metadata: { metadata: {
version: 1, version: 1,
@@ -139,7 +139,10 @@ export class EditPropsStore extends LifeCycleWatcher {
} }
} }
applyLastProps(key: LastPropsKey, props: Record<string, unknown>) { applyLastProps<K extends LastPropsKey>(
key: K,
props: Record<string, unknown>
) {
if (['__proto__', 'constructor', 'prototype'].includes(key)) { if (['__proto__', 'constructor', 'prototype'].includes(key)) {
throw new BlockSuiteError( throw new BlockSuiteError(
ErrorCode.DefaultRuntimeError, ErrorCode.DefaultRuntimeError,
@@ -25,7 +25,14 @@ import {
TextAlignSchema, TextAlignSchema,
TextVerticalAlign, TextVerticalAlign,
} from '@blocksuite/affine-model'; } 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 ConnectorEndpointSchema = z.nativeEnum(PointStyle);
const LineWidthSchema = z.nativeEnum(LineWidth); const LineWidthSchema = z.nativeEnum(LineWidth);
@@ -183,6 +190,11 @@ export function makeDeepOptional(schema: ZodTypeAny): ZodTypeAny {
return z.object(deepOptionalShape).optional(); return z.object(deepOptionalShape).optional();
} else if (schema instanceof ZodUnion) { } else if (schema instanceof ZodUnion) {
return schema.or(z.undefined()); return schema.or(z.undefined());
} else if (schema instanceof ZodIntersection) {
return z.intersection(
makeDeepOptional(schema._def.left),
makeDeepOptional(schema._def.right)
);
} else { } else {
return schema.optional(); return schema.optional();
} }
@@ -29,8 +29,10 @@ export {
export { export {
GfxBlockElementModel, GfxBlockElementModel,
type GfxCommonBlockProps, type GfxCommonBlockProps,
GfxCommonBlockZodSchema,
GfxCompatibleBlockModel as GfxCompatible, GfxCompatibleBlockModel as GfxCompatible,
type GfxCompatibleProps, type GfxCompatibleProps,
GfxCompatibleZodSchema,
} from './model/gfx-block-model.js'; } from './model/gfx-block-model.js';
export { type GfxModel } from './model/model.js'; export { type GfxModel } from './model/model.js';
export { export {
@@ -15,8 +15,10 @@ import {
polygonGetPointTangent, polygonGetPointTangent,
polygonNearestPoint, polygonNearestPoint,
rotatePoints, rotatePoints,
SerializedXYWHZodSchema,
} from '@blocksuite/global/utils'; } from '@blocksuite/global/utils';
import { BlockModel } from '@blocksuite/store'; import { BlockModel } from '@blocksuite/store';
import { z } from 'zod';
import { import {
isLockedByAncestorImpl, isLockedByAncestorImpl,
@@ -33,20 +35,24 @@ import type { SurfaceBlockModel } from './surface/surface-model.js';
/** /**
* The props that a graphics block model should have. * The props that a graphics block model should have.
*/ */
export type GfxCompatibleProps = { export const GfxCompatibleZodSchema = z.object({
xywh: SerializedXYWH; xywh: SerializedXYWHZodSchema,
index: string; index: z.string(),
lockedBySelf?: boolean; lockedBySelf: z.boolean().optional(),
}; });
export type GfxCompatibleProps = z.infer<typeof GfxCompatibleZodSchema>;
/** /**
* This type include the common props for the graphic block model. * 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. * You can use this type with Omit to define the props of a graphic block model.
*/ */
export type GfxCommonBlockProps = GfxCompatibleProps & { export const GfxCommonBlockZodSchema = GfxCompatibleZodSchema.and(
rotate: number; z.object({
scale: number; rotate: z.number(),
}; scale: z.number(),
})
);
export type GfxCommonBlockProps = z.infer<typeof GfxCommonBlockZodSchema>;
/** /**
* The graphic block model that can be rendered in the graphics mode. * The graphic block model that can be rendered in the graphics mode.
@@ -16,4 +16,5 @@ export * from './slot.js';
export * from './types.js'; export * from './types.js';
export * from './with-disposable.js'; export * from './with-disposable.js';
export type { SerializedXYWH, XYWH } from './xywh.js'; export type { SerializedXYWH, XYWH } from './xywh.js';
export { SerializedXYWHZodSchema } from './xywh.js';
export { deserializeXYWH, serializeXYWH } from './xywh.js'; export { deserializeXYWH, serializeXYWH } from './xywh.js';
@@ -1,3 +1,5 @@
import { z } from 'zod';
/** /**
* XYWH represents the x, y, width, and height of an element or block. * 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 type SerializedXYWH = `[${number},${number},${number},${number}]`;
export const SerializedXYWHZodSchema = z.custom<SerializedXYWH>((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( export function serializeXYWH(
x: number, x: number,
y: number, y: number,