Skip to content

Commit

Permalink
feat(core): editor setting service
Browse files Browse the repository at this point in the history
  • Loading branch information
EYHN committed Aug 26, 2024
1 parent b57388f commit fea3451
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Framework, GlobalState, MemoryMemento } from '@toeverything/infra';
import { expect, test } from 'vitest';

import { unflattenObject } from '../../../utils/flatten-object';
import { EditorSetting } from '../entities/editor-setting';
import { GlobalStateEditorSettingProvider } from '../impls/global-state';
import { EditorSettingProvider } from '../provider/editor-setting-provider';
import { EditorSettingService } from '../services/editor-setting';

test('editor setting service', () => {
const framework = new Framework();

framework
.service(EditorSettingService)
.entity(EditorSetting, [EditorSettingProvider])
.impl(EditorSettingProvider, GlobalStateEditorSettingProvider, [
GlobalState,
])
.impl(GlobalState, MemoryMemento);

const provider = framework.provider();

const editorSettingService = provider.get(EditorSettingService);

// default value
expect(editorSettingService.editorSetting.settings$.value).toMatchObject({
fontFamily: 'Sans',
'connector.stroke': '#000000',
});

editorSettingService.editorSetting.set('fontFamily', 'Serif');
expect(editorSettingService.editorSetting.settings$.value).toMatchObject({
fontFamily: 'Serif',
});

// nested object, should be serialized
editorSettingService.editorSetting.set('connector.stroke', {
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(
'fontFamily',
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',
},
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Entity, LiveData } from '@toeverything/infra';
import { map, type Observable } from 'rxjs';

import type { EditorSettingProvider } from '../provider/editor-setting-provider';
import { EditorSettingSchema } from '../schema';

export class EditorSetting extends Entity {
constructor(public readonly provider: EditorSettingProvider) {
super();
}

settings$ = LiveData.from<EditorSettingSchema>(this.watchAll(), null as any);

set<K extends keyof EditorSettingSchema>(
key: K,
value: EditorSettingSchema[K]
) {
const schema = EditorSettingSchema.shape[key];

this.provider.set(key, JSON.stringify(schema.parse(value)));
}

private watchAll(): Observable<EditorSettingSchema> {
return this.provider.watchAll().pipe(
map(
all =>
Object.fromEntries(
Object.entries(EditorSettingSchema.shape).map(([key, schema]) => {
const value = all[key];
const parsed = schema.safeParse(
value ? JSON.parse(value) : undefined
);
return [
key,
// if parsing fails, return the default value
parsed.success ? parsed.data : schema.parse(undefined),
];
})
) as EditorSettingSchema
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { GlobalState } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { map, type Observable } from 'rxjs';

import type { EditorSettingProvider } from '../provider/editor-setting-provider';

const storageKey = 'editor-setting';

/**
* just for testing, vary poor performance
*/
export class GlobalStateEditorSettingProvider
extends Service
implements EditorSettingProvider
{
constructor(public readonly globalState: GlobalState) {
super();
}
set(key: string, value: string): void {
const all = this.globalState.get<Record<string, string>>(storageKey) ?? {};
const after = {
...all,
[key]: value,
};
this.globalState.set(storageKey, after);
}
get(key: string): string | undefined {
return this.globalState.get<Record<string, string>>(storageKey)?.[key];
}
watchAll(): Observable<Record<string, string>> {
return this.globalState
.watch<Record<string, string>>(storageKey)
.pipe(map(all => all ?? {}));
}
}
15 changes: 15 additions & 0 deletions packages/frontend/core/src/modules/editor-settting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type Framework, GlobalState } from '@toeverything/infra';

import { EditorSetting } from './entities/editor-setting';
import { GlobalStateEditorSettingProvider } from './impls/global-state';
import { EditorSettingProvider } from './provider/editor-setting-provider';
import { EditorSettingService } from './services/editor-setting';

export function configureEditorSettingModule(framework: Framework) {
framework
.service(EditorSettingService)
.entity(EditorSetting, [EditorSettingProvider])
.impl(EditorSettingProvider, GlobalStateEditorSettingProvider, [
GlobalState,
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createIdentifier } from '@toeverything/infra';
import type { Observable } from 'rxjs';

export interface EditorSettingProvider {
set(key: string, value: string): void;
watchAll(): Observable<Record<string, string>>;
}

export const EditorSettingProvider = createIdentifier<EditorSettingProvider>(
'EditorSettingProvider'
);
62 changes: 62 additions & 0 deletions packages/frontend/core/src/modules/editor-settting/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { z } from 'zod';

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'),
});

type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
x: infer I
) => void
? I
: never;

type FlattenZodObject<O, Prefix extends string = ''> =
O extends z.ZodObject<infer T>
? {
[A in keyof T]: T[A] extends z.ZodObject<any>
? A extends string
? FlattenZodObject<T[A], `${Prefix}${A}.`>
: never
: A extends string
? { [key in `${Prefix}${A}`]: T[A] }
: never;
}[keyof T]
: never;

function flattenZodObject<S extends z.ZodObject<any>>(
schema: S,
target: z.ZodObject<any> = 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<FlattenZodObject<S>>;
return target as Result extends z.ZodRawShape ? z.ZodObject<Result> : never;
}

export const EditorSettingSchema = flattenZodObject(
BSEditorSettingSchema.merge(AffineEditorSettingSchema)
);

export type EditorSettingSchema = z.infer<typeof EditorSettingSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';

import { EditorSetting } from '../entities/editor-setting';

export class EditorSettingService extends Service {
editorSetting = this.framework.createEntity(EditorSetting);
}
2 changes: 2 additions & 0 deletions packages/frontend/core/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { configureCollectionModule } from './collection';
import { configureDocLinksModule } from './doc-link';
import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor';
import { configureEditorSettingModule } from './editor-settting';
import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite';
import { configureFindInPageModule } from './find-in-page';
Expand Down Expand Up @@ -43,4 +44,5 @@ export function configureCommonModules(framework: Framework) {
configureThemeEditorModule(framework);
configureEditorModule(framework);
configureSystemFontFamilyModule(framework);
configureEditorSettingModule(framework);
}
19 changes: 19 additions & 0 deletions packages/frontend/core/src/utils/__tests__/flatten-object.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from 'vitest';

import { unflattenObject } from '../flatten-object';

test('unflattenObject', () => {
const ob = {
'a.b.c': 1,
d: 2,
};
const result = unflattenObject(ob);
expect(result).toEqual({
a: {
b: {
c: 1,
},
},
d: 2,
});
});
22 changes: 22 additions & 0 deletions packages/frontend/core/src/utils/flatten-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function unflattenObject(ob: any) {
const result: any = {};

for (const key in ob) {
if (!Object.prototype.hasOwnProperty.call(ob, key)) continue;

const keys = key.split('.');
let current = result;

for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (i === keys.length - 1) {
current[k] = ob[key];
} else {
current[k] = current[k] || {};
current = current[k];
}
}
}

return result;
}
1 change: 1 addition & 0 deletions packages/frontend/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './create-emotion-cache';
export * from './event';
export * from './extract-emoji-icon';
export * from './flatten-object';
export * from './fractional-indexing';
export * from './popup';
export * from './string2color';
Expand Down

0 comments on commit fea3451

Please sign in to comment.