diff --git a/packages/common/infra/src/framework/core/components/component.ts b/packages/common/infra/src/framework/core/components/component.ts index 4f1446984..f843be771 100644 --- a/packages/common/infra/src/framework/core/components/component.ts +++ b/packages/common/infra/src/framework/core/components/component.ts @@ -4,8 +4,11 @@ import type { FrameworkProvider } from '../provider'; // eslint-disable-next-line @typescript-eslint/ban-types export class Component { readonly framework: FrameworkProvider; + readonly props: Props; + protected readonly disposables: (() => void)[] = []; + get eventBus() { return this.framework.eventBus; } @@ -19,7 +22,9 @@ export class Component { CONSTRUCTOR_CONTEXT.current = {}; } - dispose() {} + dispose() { + this.disposables.forEach(dispose => dispose()); + } [Symbol.dispose]() { this.dispose(); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connecter.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connecter.tsx deleted file mode 100644 index 6b72c7fce..000000000 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connecter.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { - Menu, - MenuItem, - MenuTrigger, - RadioGroup, - type RadioItem, - Slider, -} from '@affine/component'; -import { SettingRow } from '@affine/component/setting-components'; -import { useI18n } from '@affine/i18n'; -import { useMemo, useState } from 'react'; - -import { menuTrigger, settingWrapper } from '../style.css'; -import { EdgelessSnapshot } from './snapshot'; - -export const ConnecterSettings = () => { - const t = useI18n(); - - const [connecterStyle, setConnecterStyle] = useState<'general' | 'scribbled'>( - 'general' - ); - const [connectorShape, setConnectorShape] = useState< - 'elbowed' | 'curve' | 'straight' - >('elbowed'); - const [borderStyle, setBorderStyle] = useState<'solid' | 'dash'>('solid'); - const [borderThickness, setBorderThickness] = useState([3]); - - const connecterStyleItems = useMemo( - () => [ - { - value: 'general', - label: t['com.affine.settings.editorSettings.edgeless.style.general'](), - }, - { - value: 'scribbled', - label: - t['com.affine.settings.editorSettings.edgeless.style.scribbled'](), - }, - ], - [t] - ); - const connecterShapeItems = useMemo( - () => [ - { - value: 'elbowed', - label: - t[ - 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed' - ](), - }, - { - value: 'curve', - label: - t[ - 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve' - ](), - }, - { - value: 'straight', - label: - t[ - 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight' - ](), - }, - ], - [t] - ); - const borderStyleItems = useMemo( - () => [ - { - value: 'solid', - label: - t['com.affine.settings.editorSettings.edgeless.note.border.solid'](), - }, - { - value: 'dash', - label: - t['com.affine.settings.editorSettings.edgeless.note.border.dash'](), - }, - ], - [t] - ); - return ( - <> - - - Grey}> - - Grey - - - - - - - - - - - - - - - - - None}> - - None - - - - - Arrow}> - - Arrow - - - - - ); -}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx new file mode 100644 index 000000000..b127f658c --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx @@ -0,0 +1,278 @@ +import { + Menu, + MenuItem, + MenuTrigger, + RadioGroup, + type RadioItem, + Slider, +} from '@affine/component'; +import { SettingRow } from '@affine/component/setting-components'; +import { EditorSettingService } from '@affine/core/modules/editor-settting'; +import { useI18n } from '@affine/i18n'; +import { + ConnectorMode, + LineColor, + PointStyle, + StrokeStyle, +} from '@blocksuite/blocks'; +import { useFramework, useLiveData } from '@toeverything/infra'; +import { useCallback, useMemo } from 'react'; + +import { menuTrigger, settingWrapper } from '../style.css'; +import { EdgelessSnapshot } from './snapshot'; + +enum ConnecterStyle { + General = 'general', + Scribbled = 'scribbled', +} + +export const ConnectorSettings = () => { + const t = useI18n(); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); + + const connecterStyleItems = useMemo( + () => [ + { + value: ConnecterStyle.General, + label: t['com.affine.settings.editorSettings.edgeless.style.general'](), + }, + { + value: ConnecterStyle.Scribbled, + label: + t['com.affine.settings.editorSettings.edgeless.style.scribbled'](), + }, + ], + [t] + ); + const connecterStyle: ConnecterStyle = settings.connector.rough + ? ConnecterStyle.Scribbled + : ConnecterStyle.General; + const setConnecterStyle = useCallback( + (value: ConnecterStyle) => { + const isRough = value === ConnecterStyle.Scribbled; + editorSetting.set('connector', { + rough: isRough, + }); + }, + [editorSetting] + ); + + const connectorShapeItems = useMemo( + () => [ + { + value: ConnectorMode.Orthogonal as any, + label: + t[ + 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed' + ](), + }, + { + value: ConnectorMode.Curve as any, + label: + t[ + 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve' + ](), + }, + { + value: ConnectorMode.Straight as any, + label: + t[ + 'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight' + ](), + }, + ], + [t] + ); + const connectorShape: ConnectorMode = settings.connector.mode; + const setConnectorShape = useCallback( + (value: ConnectorMode) => { + editorSetting.set('connector', { + mode: value, + }); + }, + [editorSetting] + ); + + const borderStyleItems = useMemo( + () => [ + { + value: StrokeStyle.Solid, + label: + t['com.affine.settings.editorSettings.edgeless.note.border.solid'](), + }, + { + value: StrokeStyle.Dash, + label: + t['com.affine.settings.editorSettings.edgeless.note.border.dash'](), + }, + ], + [t] + ); + const borderStyle: StrokeStyle = settings.connector.strokeStyle; + const setBorderStyle = useCallback( + (value: StrokeStyle) => { + editorSetting.set('connector', { + strokeStyle: value, + }); + }, + [editorSetting] + ); + + const borderThickness = settings.connector.strokeWidth; + const setBorderThickness = useCallback( + (value: number[]) => { + editorSetting.set('connector', { + strokeWidth: value[0], + }); + }, + [editorSetting] + ); + + const colorItems = useMemo(() => { + const { stroke } = settings.connector; + return Object.entries(LineColor).map(([name, value]) => { + const handler = () => { + editorSetting.set('connector', { stroke: value }); + }; + const isSelected = stroke === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const startEndPointItems = useMemo(() => { + const { frontEndpointStyle } = settings.connector; + return Object.entries(PointStyle).map(([name, value]) => { + const handler = () => { + editorSetting.set('connector', { frontEndpointStyle: value }); + }; + const isSelected = frontEndpointStyle === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const endEndPointItems = useMemo(() => { + const { rearEndpointStyle } = settings.connector; + return Object.entries(PointStyle).map(([name, value]) => { + const handler = () => { + editorSetting.set('connector', { rearEndpointStyle: value }); + }; + const isSelected = rearEndpointStyle === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + return ( + <> + + + + + {String(settings.connector.stroke)} + + + + + + + + + + + + + + + + + + + {String(settings.connector.frontEndpointStyle)} + + + + + + + {String(settings.connector.rearEndpointStyle)} + + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/edgeless.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/edgeless.tsx index a29a5cd97..db5530d18 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/edgeless.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/edgeless.tsx @@ -1,7 +1,7 @@ import { SettingWrapper } from '@affine/component/setting-components'; import { useI18n } from '@affine/i18n'; -import { ConnecterSettings } from './connecter'; +import { ConnectorSettings } from './connector'; import { MindMapSettings } from './mind-map'; import { NoteSettings } from './note'; import { PenSettings } from './pen'; @@ -15,7 +15,7 @@ export const Edgeless = () => { - + diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/note.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/note.tsx index d6df662fe..5dd04915e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/note.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/note.tsx @@ -7,39 +7,139 @@ import { Slider, } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; +import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { useI18n } from '@affine/i18n'; -import { useMemo, useState } from 'react'; +import { + NoteBackgroundColor, + NoteShadow, + StrokeStyle, +} from '@blocksuite/blocks'; +import { useFramework, useLiveData } from '@toeverything/infra'; +import { useCallback, useMemo } from 'react'; import { menuTrigger, settingWrapper } from '../style.css'; import { EdgelessSnapshot } from './snapshot'; +const CORNER_SIZE = [ + { name: 'None', value: 0 }, + { name: 'Small', value: 8 }, + { name: 'Medium', value: 16 }, + { name: 'Large', value: 24 }, + { name: 'Huge', value: 32 }, +] as const; + export const NoteSettings = () => { const t = useI18n(); - const [borderStyle, setBorderStyle] = useState<'solid' | 'dash' | 'none'>( - 'solid' - ); - const [borderThickness, setBorderThickness] = useState([3]); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); const borderStyleItems = useMemo( () => [ { - value: 'solid', + value: StrokeStyle.Solid, label: t['com.affine.settings.editorSettings.edgeless.note.border.solid'](), }, { - value: 'dash', + value: StrokeStyle.Dash, label: t['com.affine.settings.editorSettings.edgeless.note.border.dash'](), }, { - value: 'none', + value: StrokeStyle.None, label: t['com.affine.settings.editorSettings.edgeless.note.border.none'](), }, ], [t] ); + + const { borderStyle } = settings['affine:note'].edgeless.style; + const setBorderStyle = useCallback( + (value: StrokeStyle) => { + editorSetting.set('affine:note', { + edgeless: { + style: { + borderStyle: value, + }, + }, + }); + }, + [editorSetting] + ); + + const { borderSize } = settings['affine:note'].edgeless.style; + const setBorderSize = useCallback( + (value: number[]) => { + editorSetting.set('affine:note', { + edgeless: { + style: { + borderSize: value[0], + }, + }, + }); + }, + [editorSetting] + ); + + const backgroundItems = useMemo(() => { + const { background } = settings['affine:note']; + return Object.entries(NoteBackgroundColor).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:note', { background: value }); + }; + const isSelected = background === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const cornerItems = useMemo(() => { + const { borderRadius } = settings['affine:note'].edgeless.style; + return CORNER_SIZE.map(({ name, value }) => { + const handler = () => { + editorSetting.set('affine:note', { + edgeless: { + style: { + borderRadius: value, + }, + }, + }); + }; + const isSelected = borderRadius === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const shadowItems = useMemo(() => { + const { shadowType } = settings['affine:note'].edgeless.style; + return Object.entries(NoteShadow).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:note', { + edgeless: { + style: { + shadowType: value, + }, + }, + }); + }; + const isSelected = shadowType === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + return ( <> { ]()} desc={''} > - Yellow}> - - Yellow + + + {String(settings['affine:note'].background)} @@ -63,9 +163,9 @@ export const NoteSettings = () => { name={t['com.affine.settings.editorSettings.edgeless.note.corners']()} desc={''} > - Small}> - - Small + + + {String(settings['affine:note'].edgeless.style.borderRadius)} @@ -73,9 +173,9 @@ export const NoteSettings = () => { name={t['com.affine.settings.editorSettings.edgeless.note.shadow']()} desc={''} > - Box shadow}> - - Box shadow + + + {String(settings['affine:note'].edgeless.style.shadowType)} @@ -98,12 +198,12 @@ export const NoteSettings = () => { desc={''} > diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/pen.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/pen.tsx index b6d2795cd..5e844adfe 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/pen.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/pen.tsx @@ -1,14 +1,45 @@ import { Menu, MenuItem, MenuTrigger, Slider } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; +import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { useI18n } from '@affine/i18n'; -import { useState } from 'react'; +import { LineColor } from '@blocksuite/blocks'; +import { useFramework, useLiveData } from '@toeverything/infra'; +import { useCallback, useMemo } from 'react'; import { menuTrigger } from '../style.css'; import { EdgelessSnapshot } from './snapshot'; export const PenSettings = () => { const t = useI18n(); - const [borderThickness, setBorderThickness] = useState([3]); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); + + const colorItems = useMemo(() => { + const { color } = settings.brush; + return Object.entries(LineColor).map(([name, value]) => { + const handler = () => { + editorSetting.set('brush', { color: value }); + }; + const isSelected = color === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const borderThickness = settings.brush.lineWidth; + const setBorderThickness = useCallback( + (value: number[]) => { + editorSetting.set('brush', { + lineWidth: value[0], + }); + }, + [editorSetting] + ); + return ( <> { name={t['com.affine.settings.editorSettings.edgeless.pen.color']()} desc={''} > - Yellow}> - - Yellow + + + {String(settings.brush.color)} @@ -31,12 +62,12 @@ export const PenSettings = () => { desc={''} > diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx index 4ce4cbc00..7b267c3cd 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx @@ -6,37 +6,46 @@ import { type RadioItem, } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; +import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { useI18n } from '@affine/i18n'; -import { useMemo, useState } from 'react'; +import { + FontFamily, + FontFamilyMap, + FontStyle, + FontWeight, + LineColor, + TextAlign, +} from '@blocksuite/blocks'; +import { useFramework, useLiveData } from '@toeverything/infra'; +import { useCallback, useMemo } from 'react'; import { menuTrigger, settingWrapper } from '../style.css'; import { EdgelessSnapshot } from './snapshot'; export const TextSettings = () => { const t = useI18n(); - - const [textAlignment, setTextAlignment] = useState< - 'left' | 'center' | 'right' - >('left'); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); const alignItems = useMemo( () => [ { - value: 'left', + value: TextAlign.Left, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.left' ](), }, { - value: 'center', + value: TextAlign.Center, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.center' ](), }, { - value: 'right', + value: TextAlign.Right, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.right' @@ -46,6 +55,76 @@ export const TextSettings = () => { [t] ); + const { textAlign } = settings['affine:edgeless-text']; + const setTextAlign = useCallback( + (value: TextAlign) => { + editorSetting.set('affine:edgeless-text', { + textAlign: value, + }); + }, + [editorSetting] + ); + + const colorItems = useMemo(() => { + const { color } = settings['affine:edgeless-text']; + return Object.entries(LineColor).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:edgeless-text', { color: value }); + }; + const isSelected = color === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const fontFamilyItems = useMemo(() => { + const { fontFamily } = settings['affine:edgeless-text']; + return Object.entries(FontFamily).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:edgeless-text', { fontFamily: value }); + }; + const isSelected = fontFamily === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const fontStyleItems = useMemo(() => { + const { fontStyle } = settings['affine:edgeless-text']; + return Object.entries(FontStyle).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:edgeless-text', { fontStyle: value }); + }; + const isSelected = fontStyle === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + + const fontWeightItems = useMemo(() => { + const { fontWeight } = settings['affine:edgeless-text']; + return Object.entries(FontWeight).map(([name, value]) => { + const handler = () => { + editorSetting.set('affine:edgeless-text', { fontWeight: value }); + }; + const isSelected = fontWeight === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings]); + return ( <> { name={t['com.affine.settings.editorSettings.edgeless.text.color']()} desc={''} > - Blue}> - - Blue + + + {String(settings['affine:edgeless-text'].color)} - Inter}> - - Inter + + + {FontFamilyMap[settings['affine:edgeless-text'].fontFamily]} - 15px}> - - 15px + + + {String(settings['affine:edgeless-text'].fontStyle)} @@ -89,9 +172,9 @@ export const TextSettings = () => { ]()} desc={''} > - Regular}> - - Regular + + + {settings['affine:edgeless-text'].fontWeight} @@ -101,10 +184,10 @@ export const TextSettings = () => { > diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts index fe1ddbd3f..6b5e12563 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts @@ -3,6 +3,7 @@ import { AIPageRootBlockSpec, } from '@affine/core/blocksuite/presets/ai'; import { mixpanel } from '@affine/core/mixpanel'; +import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { BlockFlavourIdentifier, BlockServiceIdentifier, @@ -54,6 +55,7 @@ function withAffineRootService(Service: typeof PageRootService) { export function createPageRootBlockSpec( framework: FrameworkProvider ): ExtensionType[] { + const editorSettingService = framework.get(EditorSettingService); return [ ...AIPageRootBlockSpec, { @@ -67,6 +69,7 @@ export function createPageRootBlockSpec( }, ConfigExtension('affine:page', { linkedWidget: createLinkedWidgetConfig(framework), + editorSetting: editorSettingService.editorSetting.settingSignal, }), ]; } @@ -74,6 +77,7 @@ export function createPageRootBlockSpec( export function createEdgelessRootBlockSpec( framework: FrameworkProvider ): ExtensionType[] { + const editorSettingService = framework.get(EditorSettingService); return [ ...AIEdgelessRootBlockSpec, { @@ -87,6 +91,7 @@ export function createEdgelessRootBlockSpec( }, ConfigExtension('affine:page', { linkedWidget: createLinkedWidgetConfig(framework), + editorSetting: editorSettingService.editorSetting.settingSignal, }), ]; } diff --git a/packages/frontend/core/src/modules/editor-settting/__test__/editor-setting.spec.ts b/packages/frontend/core/src/modules/editor-settting/__test__/editor-setting.spec.ts index a25ef5919..3284bab16 100644 --- a/packages/frontend/core/src/modules/editor-settting/__test__/editor-setting.spec.ts +++ b/packages/frontend/core/src/modules/editor-settting/__test__/editor-setting.spec.ts @@ -1,7 +1,6 @@ import { Framework, GlobalState, MemoryMemento } from '@toeverything/infra'; import { expect, test } from 'vitest'; -import { unflattenObject } from '../../../utils/unflatten-object'; import { EditorSetting } from '../entities/editor-setting'; import { GlobalStateEditorSettingProvider } from '../impls/global-state'; import { EditorSettingProvider } from '../provider/editor-setting-provider'; @@ -23,27 +22,23 @@ test('editor setting service', () => { const editorSettingService = provider.get(EditorSettingService); // default value - expect(editorSettingService.editorSetting.settings$.value).toMatchObject({ - fontFamily: 'Sans', - 'connector.stroke': '#000000', - }); + expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans'); + // set plain object editorSettingService.editorSetting.set('fontFamily', 'Serif'); - expect(editorSettingService.editorSetting.settings$.value).toMatchObject({ - fontFamily: 'Serif', - }); + expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Serif'); - // nested object, should be serialized - editorSettingService.editorSetting.set('connector.stroke', { + // set nested object + editorSettingService.editorSetting.set('connector', { + stroke: { + dark: '#000000', + light: '#ffffff', + }, + }); + expect(editorSettingService.editorSetting.get('connector').stroke).toEqual({ dark: '#000000', light: '#ffffff', }); - expect( - ( - editorSettingService.editorSetting - .provider as GlobalStateEditorSettingProvider - ).get('connector.stroke') - ).toBe('{"dark":"#000000","light":"#ffffff"}'); // invalid font family editorSettingService.editorSetting.provider.set( @@ -51,22 +46,6 @@ test('editor setting service', () => { JSON.stringify('abc') ); - // should fallback to default value - expect(editorSettingService.editorSetting.settings$.value['fontFamily']).toBe( - 'Sans' - ); - - // expend demo - const expended = unflattenObject( - editorSettingService.editorSetting.settings$.value - ); - expect(expended).toMatchObject({ - fontFamily: 'Sans', - connector: { - stroke: { - dark: '#000000', - light: '#ffffff', - }, - }, - }); + // fallback to default value + expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans'); }); diff --git a/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts b/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts index cb29c9753..c0f5c1f14 100644 --- a/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts +++ b/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts @@ -1,5 +1,12 @@ +import { + createSignalFromObservable, + type Signal, +} from '@blocksuite/affine-shared/utils'; +import type { DeepPartial } from '@blocksuite/global/utils'; import { Entity, LiveData } from '@toeverything/infra'; -import { map, type Observable } from 'rxjs'; +import { isObject, merge } from 'lodash-es'; +import type { Observable } from 'rxjs'; +import { map } from 'rxjs'; import type { EditorSettingProvider } from '../provider/editor-setting-provider'; import { EditorSettingSchema } from '../schema'; @@ -7,17 +14,30 @@ import { EditorSettingSchema } from '../schema'; export class EditorSetting extends Entity { constructor(public readonly provider: EditorSettingProvider) { super(); + + const { signal, cleanup } = createSignalFromObservable< + Partial + >(this.settings$, {}); + this.settingSignal = signal; + this.disposables.push(cleanup); } settings$ = LiveData.from(this.watchAll(), null as any); + settingSignal: Signal>; + + get(key: K) { + return this.settings$.value[key]; + } + set( key: K, - value: EditorSettingSchema[K] + value: DeepPartial ) { const schema = EditorSettingSchema.shape[key]; - - this.provider.set(key, JSON.stringify(schema.parse(value))); + const curValue = this.get(key); + const nextValue = isObject(curValue) ? merge(curValue, value) : value; + this.provider.set(key, JSON.stringify(schema.parse(nextValue))); } private watchAll(): Observable { diff --git a/packages/frontend/core/src/modules/editor-settting/schema.ts b/packages/frontend/core/src/modules/editor-settting/schema.ts index 35e7791bd..d6fd33f47 100644 --- a/packages/frontend/core/src/modules/editor-settting/schema.ts +++ b/packages/frontend/core/src/modules/editor-settting/schema.ts @@ -1,5 +1,21 @@ +import { + BrushSchema, + ConnectorSchema, + EdgelessTextSchema, + NoteSchema, + ShapeSchema, +} from '@blocksuite/affine-shared/utils'; import { z } from 'zod'; +// TODO import from BlockSuite +export const BSEditorSettingSchema = z.object({ + connector: ConnectorSchema, + brush: BrushSchema, + shape: ShapeSchema, + 'affine:edgeless-text': EdgelessTextSchema, + 'affine:note': NoteSchema, +}); + export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom'; export const fontStyleOptions = [ @@ -12,20 +28,6 @@ export const fontStyleOptions = [ value: string; }[]; -const BSEditorSettingSchema = z.object({ - // TODO: import from bs - connector: z.object({ - stroke: z - .union([ - z.string(), - z.object({ - dark: z.string(), - light: z.string(), - }), - ]) - .default('#000000'), - }), -}); const AffineEditorSettingSchema = z.object({ fontFamily: z.enum(['Sans', 'Serif', 'Mono', 'Custom']).default('Sans'), customFontFamily: z.string().default(''), @@ -36,44 +38,8 @@ const AffineEditorSettingSchema = z.object({ displayBiDirectionalLink: z.boolean().default(true), }); -type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( - x: infer I -) => void - ? I - : never; - -type FlattenZodObject = - O extends z.ZodObject - ? { - [A in keyof T]: T[A] extends z.ZodObject - ? A extends string - ? FlattenZodObject - : never - : A extends string - ? { [key in `${Prefix}${A}`]: T[A] } - : never; - }[keyof T] - : never; - -function flattenZodObject>( - schema: S, - target: z.ZodObject = z.object({}), - prefix = '' -) { - for (const key in schema.shape) { - const value = schema.shape[key]; - if (value instanceof z.ZodObject) { - flattenZodObject(value, target, prefix + key + '.'); - } else { - target.shape[prefix + key] = value; - } - } - type Result = UnionToIntersection>; - return target as Result extends z.ZodRawShape ? z.ZodObject : never; -} - -export const EditorSettingSchema = flattenZodObject( - BSEditorSettingSchema.merge(AffineEditorSettingSchema) +export const EditorSettingSchema = BSEditorSettingSchema.merge( + AffineEditorSettingSchema ); export type EditorSettingSchema = z.infer; diff --git a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts index 538303022..81b1fabeb 100644 --- a/packages/frontend/core/src/modules/telemetry/services/telemetry.ts +++ b/packages/frontend/core/src/modules/telemetry/services/telemetry.ts @@ -19,7 +19,6 @@ import { UserQuotaChanged } from '../../cloud/services/user-quota'; export class TelemetryService extends Service { private prevQuota: NonNullable['quota'] | null = null; - private readonly disposables: (() => void)[] = []; constructor( private readonly auth: AuthService, diff --git a/packages/frontend/core/src/utils/__tests__/unflatten-object.spec.ts b/packages/frontend/core/src/utils/__tests__/unflatten-object.spec.ts deleted file mode 100644 index 6c08c0927..000000000 --- a/packages/frontend/core/src/utils/__tests__/unflatten-object.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test } from 'vitest'; - -import { unflattenObject } from '../unflatten-object'; - -test('unflattenObject', () => { - const ob = { - 'a.b.c': 1, - d: 2, - }; - const result = unflattenObject(ob); - expect(result).toEqual({ - a: { - b: { - c: 1, - }, - }, - d: 2, - }); -}); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index f129013d4..1ddb0bea2 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1270,6 +1270,8 @@ "com.affine.settings.editorSettings.edgeless.text": "Text", "com.affine.settings.editorSettings.edgeless.text.color": "Text color", "com.affine.settings.editorSettings.edgeless.text.font": "Font", + "com.affine.settings.editorSettings.edgeless.text.font-family": "Font Family", + "com.affine.settings.editorSettings.edgeless.text.font-style": "Font Style", "com.affine.settings.editorSettings.edgeless.text.font-size": "Font size", "com.affine.settings.editorSettings.edgeless.text.font-weight": "Font weight", "com.affine.settings.editorSettings.edgeless.text.alignment": "Alignment",