From d4f0c53a0c10309a286a893a94eb2ea27938066e Mon Sep 17 00:00:00 2001 From: Oleg <67059482+OlegDev1@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:23:32 +0100 Subject: [PATCH] feat(core): frame editor settings (#9970) Co-authored-by: L-Sun Co-authored-by: Mirone --- .../model/src/blocks/frame/frame-model.ts | 30 +++---- .../auto-complete/auto-complete-panel.ts | 15 ++-- .../src/root-block/edgeless/frame-manager.ts | 15 ++-- .../edgeless/gfx-tool/frame-tool.ts | 18 ++-- .../framework/block-std/src/gfx/index.ts | 2 - .../src/gfx/model/gfx-block-model.ts | 24 ++--- .../framework/global/src/utils/index.ts | 1 - blocksuite/framework/global/src/utils/xywh.ts | 26 ------ .../editor/edgeless/docs/frame.json | 61 +++++++++++++ .../editor/edgeless/docs/index.ts | 8 +- .../editor/edgeless/edgeless.tsx | 2 + .../general-setting/editor/edgeless/frame.tsx | 88 +++++++++++++++++++ .../editor/edgeless/snapshot.tsx | 3 +- packages/frontend/i18n/src/resources/en.json | 2 + 14 files changed, 211 insertions(+), 84 deletions(-) create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/frame.json create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/frame.tsx diff --git a/blocksuite/affine/model/src/blocks/frame/frame-model.ts b/blocksuite/affine/model/src/blocks/frame/frame-model.ts index c3092f59e20f0..80fc928ceae60 100644 --- a/blocksuite/affine/model/src/blocks/frame/frame-model.ts +++ b/blocksuite/affine/model/src/blocks/frame/frame-model.ts @@ -1,5 +1,6 @@ import type { GfxBlockElementModel, + GfxCompatibleProps, GfxElementGeometry, GfxGroupCompatibleInterface, GfxModel, @@ -10,7 +11,6 @@ import { descendantElementsImpl, generateKeyBetweenV2, GfxCompatible, - GfxCompatibleZodSchema, gfxGroupCompatibleSymbol, hasDescendantElementImpl, } from '@blocksuite/block-std/gfx'; @@ -18,33 +18,33 @@ import { Bound } from '@blocksuite/global/utils'; import { BlockModel, defineBlockSchema, type Text } from '@blocksuite/store'; import { z } from 'zod'; -import { ColorSchema, DefaultTheme } from '../../themes/index.js'; +import { type Color, ColorSchema, DefaultTheme } from '../../themes/index.js'; + +export type FrameBlockProps = { + title: Text; + background: Color; + childElementIds?: Record; + presentationIndex?: string; +} & GfxCompatibleProps; export const FrameZodSchema = z .object({ background: ColorSchema, - childElementIds: z.record(z.boolean()), - presentationIndex: z.string(), }) - .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), + background: 'transparent', + xywh: `[0,0,100,100]`, + index: 'a0', + childElementIds: Object.create(null), + presentationIndex: generateKeyBetweenV2(null, null), + lockedBySelf: false, }), metadata: { version: 1, diff --git a/blocksuite/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts b/blocksuite/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts index 68d1f263b541c..8685b8edcb7f6 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts @@ -158,15 +158,12 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) { const { service, surfaceBlockModel } = edgeless; const frameMgr = service.frame; const frameIndex = service.frames.length + 1; - const id = this.crud.addBlock( - 'affine:frame', - { - title: new Y.Text(`Frame ${frameIndex}`), - xywh: serializeXYWH(...xywh), - presentationIndex: frameMgr.generatePresentationIndex(), - }, - surfaceBlockModel - ); + const props = this.std.get(EditPropsStore).applyLastProps('affine:frame', { + title: new Y.Text(`Frame ${frameIndex}`), + xywh: serializeXYWH(...xywh), + presentationIndex: frameMgr.generatePresentationIndex(), + }); + const id = this.crud.addBlock('affine:frame', props, surfaceBlockModel); edgeless.doc.captureSync(); const frame = this.crud.getElementById(id); if (!frame) return; diff --git a/blocksuite/blocks/src/root-block/edgeless/frame-manager.ts b/blocksuite/blocks/src/root-block/edgeless/frame-manager.ts index b25b0d1bfbbdb..4e0858ee85432 100644 --- a/blocksuite/blocks/src/root-block/edgeless/frame-manager.ts +++ b/blocksuite/blocks/src/root-block/edgeless/frame-manager.ts @@ -1,5 +1,7 @@ import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import { Overlay } from '@blocksuite/affine-block-surface'; +import type { FrameBlockModel, NoteBlockModel } from '@blocksuite/affine-model'; +import { EditPropsStore } from '@blocksuite/affine-shared/services'; import { generateKeyBetweenV2, getTopElements, @@ -23,7 +25,6 @@ import type { Store } from '@blocksuite/store'; import { Text } from '@blocksuite/store'; import * as Y from 'yjs'; -import type { FrameBlockModel, NoteBlockModel } from '../../index.js'; import { areSetsEqual } from './utils/misc.js'; import { isFrameBlock } from './utils/query.js'; @@ -194,16 +195,16 @@ export class EdgelessFrameManager extends GfxExtension { private _addFrameBlock(bound: Bound) { const surfaceModel = this.gfx.surface as SurfaceBlockModel; - const id = this.gfx.doc.addBlock( - 'affine:frame', - { + const props = this.gfx.std + .get(EditPropsStore) + .applyLastProps('affine:frame', { title: new Text(new Y.Text(`Frame ${this.frames.length + 1}`)), xywh: bound.serialize(), index: this.gfx.layer.generateIndex(true), presentationIndex: this.generatePresentationIndex(), - }, - surfaceModel - ); + }); + + const id = this.gfx.doc.addBlock('affine:frame', props, surfaceModel); const frameModel = this.gfx.getElementById(id); if (!frameModel || !isFrameBlock(frameModel)) { diff --git a/blocksuite/blocks/src/root-block/edgeless/gfx-tool/frame-tool.ts b/blocksuite/blocks/src/root-block/edgeless/gfx-tool/frame-tool.ts index 9c801f89d182b..7da7f9fff0206 100644 --- a/blocksuite/blocks/src/root-block/edgeless/gfx-tool/frame-tool.ts +++ b/blocksuite/blocks/src/root-block/edgeless/gfx-tool/frame-tool.ts @@ -1,6 +1,9 @@ import { OverlayIdentifier } from '@blocksuite/affine-block-surface'; import type { FrameBlockModel } from '@blocksuite/affine-model'; -import { TelemetryProvider } from '@blocksuite/affine-shared/services'; +import { + EditPropsStore, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import type { PointerEventState } from '@blocksuite/block-std'; import { BaseTool, @@ -70,16 +73,17 @@ export class FrameTool extends BaseTool { const frames = this.gfx.layer.blocks.filter( block => block.flavour === 'affine:frame' ) as FrameBlockModel[]; - const id = this.doc.addBlock( - 'affine:frame', - { + + const props = this.std + .get(EditPropsStore) + .applyLastProps('affine:frame', { title: new Text(new Y.Text(`Frame ${frames.length + 1}`)), xywh: Bound.fromPoints([this._startPoint, currentPoint]).serialize(), index: this.gfx.layer.generateIndex(true), presentationIndex: this.frameManager.generatePresentationIndex(), - }, - this.gfx.surface - ); + }); + + const id = this.doc.addBlock('affine:frame', props, this.gfx.surface); this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', { control: 'canvas:draw', diff --git a/blocksuite/framework/block-std/src/gfx/index.ts b/blocksuite/framework/block-std/src/gfx/index.ts index 4c7cb0e599c6a..c5264aa8b08f8 100644 --- a/blocksuite/framework/block-std/src/gfx/index.ts +++ b/blocksuite/framework/block-std/src/gfx/index.ts @@ -29,10 +29,8 @@ 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 b50c0cad6cc6d..4174a53eb0d55 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,10 +15,8 @@ import { polygonGetPointTangent, polygonNearestPoint, rotatePoints, - SerializedXYWHZodSchema, } from '@blocksuite/global/utils'; import { BlockModel } from '@blocksuite/store'; -import { z } from 'zod'; import { isLockedByAncestorImpl, @@ -35,24 +33,20 @@ import type { SurfaceBlockModel } from './surface/surface-model.js'; /** * The props that a graphics block model should have. */ -export const GfxCompatibleZodSchema = z.object({ - xywh: SerializedXYWHZodSchema, - index: z.string(), - lockedBySelf: z.boolean().optional(), -}); -export type GfxCompatibleProps = z.infer; +export type GfxCompatibleProps = { + xywh: SerializedXYWH; + index: string; + lockedBySelf?: boolean; +}; /** * 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 const GfxCommonBlockZodSchema = GfxCompatibleZodSchema.and( - z.object({ - rotate: z.number(), - scale: z.number(), - }) -); -export type GfxCommonBlockProps = z.infer; +export type GfxCommonBlockProps = GfxCompatibleProps & { + rotate: number; + scale: number; +}; /** * 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 77a428be31a86..3f47a5de6b4ba 100644 --- a/blocksuite/framework/global/src/utils/index.ts +++ b/blocksuite/framework/global/src/utils/index.ts @@ -16,5 +16,4 @@ 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 e65869572056e..ed54df1beeb56 100644 --- a/blocksuite/framework/global/src/utils/xywh.ts +++ b/blocksuite/framework/global/src/utils/xywh.ts @@ -1,5 +1,3 @@ -import { z } from 'zod'; - /** * XYWH represents the x, y, width, and height of an element or block. */ @@ -10,30 +8,6 @@ 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, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/frame.json b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/frame.json new file mode 100644 index 0000000000000..5def0f933a8a2 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/frame.json @@ -0,0 +1,61 @@ +{ + "type": "page", + "meta": { + "id": "gIUEIHL42k", + "title": "FrameDoc", + "createDate": 1738858597791, + "tags": [] + }, + "blocks": { + "type": "block", + "id": "MCYCDPz_QH", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "FrameDoc" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "I_s0rS2Pbl", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "mHE8GI9UWx", + "flavour": "affine:frame", + "version": 1, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "Frame 1" + } + ] + }, + "background": "transparent", + "xywh": "[-186.37939453125,-68.03814697265625,375.8616943359375,114.787109375]", + "index": "Zz", + "childElementIds": {}, + "presentationIndex": "a00zHdtY37OLhj7JOhCr9ywYQXy1SbAI6kM", + "lockedBySelf": false + }, + "children": [] + } + ] + } + ] + } +} diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts index ce6db90f36f8b..15a399e3f42a7 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts @@ -24,7 +24,8 @@ export type DocName = | 'flow' | 'text' | 'connector' - | 'mindmap'; + | 'mindmap' + | 'frame'; const docMap = new Map>(); @@ -40,6 +41,10 @@ async function loadShape() { return (await import('./shape.json')).default; } +async function loadFrame() { + return (await import('./frame.json')).default; +} + async function loadFlow() { return (await import('./flow.json')).default; } @@ -60,6 +65,7 @@ const loaders = { note: loadNote, pen: loadPen, shape: loadShape, + frame: loadFrame, flow: loadFlow, text: loadText, connector: loadConnector, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/edgeless.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/edgeless.tsx index df9914b2b285f..2ed1c2d1585fa 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/edgeless.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/edgeless.tsx @@ -2,6 +2,7 @@ import { SettingWrapper } from '@affine/component/setting-components'; import { useI18n } from '@affine/i18n'; import { ConnectorSettings } from './connector'; +import { FrameSettings } from './frame'; import { GeneralEdgelessSetting } from './general'; import { MindMapSettings } from './mind-map'; import { NoteSettings } from './note'; @@ -17,6 +18,7 @@ export const Edgeless = () => { + diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/frame.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/frame.tsx new file mode 100644 index 0000000000000..ebc47c77580f4 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/frame.tsx @@ -0,0 +1,88 @@ +import { MenuItem, MenuTrigger } from '@affine/component'; +import { SettingRow } from '@affine/component/setting-components'; +import { EditorSettingService } from '@affine/core/modules/editor-setting'; +import { useI18n } from '@affine/i18n'; +import { DefaultTheme } from '@blocksuite/affine/blocks'; +import type { Store } from '@blocksuite/affine/store'; +import { useFramework, useLiveData } from '@toeverything/infra'; +import { isEqual } from 'lodash-es'; +import { useCallback, useMemo } from 'react'; + +import { DropdownMenu } from '../menu'; +import { menuTrigger } from '../style.css'; +import { usePalettes } from '../utils'; +import { Point } from './point'; +import { EdgelessSnapshot } from './snapshot'; + +export const FrameSettings = () => { + const t = useI18n(); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); + const { palettes, getCurrentColor } = usePalettes( + [ + { key: 'Transparent', value: DefaultTheme.transparent }, + ...DefaultTheme.FillColorPalettes, + ], + DefaultTheme.transparent + ); + + const { background } = settings['affine:frame']; + const currentColor = useMemo(() => { + return getCurrentColor(background); + }, [getCurrentColor, background]); + + const colorItems = useMemo(() => { + return palettes.map(({ key, value, resolvedValue }) => { + const handler = () => { + editorSetting.set('affine:frame', { background: value }); + }; + const isSelected = isEqual(background, value); + return ( + } + > + {key} + + ); + }); + }, [editorSetting, background, palettes]); + + const getElements = useCallback((doc: Store) => { + return doc.getBlocksByFlavour('affine:frame') || []; + }, []); + + return ( + <> + + + {currentColor ? ( + } + > + {currentColor.key} + + } + /> + ) : null} + + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/snapshot.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/snapshot.tsx index a917435ea497f..f652fe73e91bb 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/snapshot.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/snapshot.tsx @@ -115,7 +115,8 @@ export const EdgelessSnapshot = (props: Props) => { doc.readonly = false; edgelessBlock.editorViewportSelector = 'ref-viewport'; const frame = getFrameBlock(doc); - if (frame) { + if (frame && docName !== 'frame') { + // docName with value 'frame' shouldn't be deleted, it is a part of frame settings boundMap.set(docName, Bound.deserialize(frame.xywh)); doc.deleteBlock(frame); } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 6d69acc729386..812705abde99e 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1212,6 +1212,8 @@ "com.affine.settings.editorSettings.edgeless.shape.text-alignment": "Text alignment", "com.affine.settings.editorSettings.edgeless.shape.text-color": "Text color", "com.affine.settings.editorSettings.edgeless.shape.triangle": "Triangle", + "com.affine.settings.editorSettings.edgeless.frame": "Frame", + "com.affine.settings.editorSettings.edgeless.frame.background": "Background", "com.affine.settings.editorSettings.edgeless.style": "Style", "com.affine.settings.editorSettings.edgeless.style.general": "General", "com.affine.settings.editorSettings.edgeless.style.scribbled": "Scribbled",