feat(core): template-doc settings for user-worksapce scope (#9566)

This commit is contained in:
CatsJuice
2025-01-14 02:10:34 +00:00
parent 10196f6785
commit 57b89b5ad4
11 changed files with 237 additions and 46 deletions

View File

@@ -0,0 +1,19 @@
import { style } from '@vanilla-extract/css';
export const menuItem = style({
display: 'flex',
alignItems: 'center',
gap: 8,
});
export const menuItemIcon = style({
fontSize: 24,
lineHeight: 0,
});
export const menuItemText = style({
fontSize: 14,
width: 0,
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
});

View File

@@ -0,0 +1,110 @@
import { Button, Menu, MenuItem } from '@affine/component';
import { type DocRecord, DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { TemplateDocService } from '@affine/core/modules/template-doc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useCallback, useState } from 'react';
import * as styles from './template.css';
export const TemplateDocSetting = () => {
const { featureFlagService, templateDocService } = useServices({
FeatureFlagService,
TemplateDocService,
});
const setting = templateDocService.setting;
const enabled = useLiveData(featureFlagService.flags.enable_template_doc.$);
const loading = useLiveData(setting.loading$);
const pageTemplateDocId = useLiveData(setting.pageTemplateDocId$);
const journalTemplateDocId = useLiveData(setting.journalTemplateDocId$);
const updatePageTemplate = useCallback(
(id?: string) => {
setting.updatePageTemplateDocId(id);
},
[setting]
);
const updateJournalTemplate = useCallback(
(id?: string) => {
setting.updateJournalTemplateDocId(id);
},
[setting]
);
if (!enabled) return null;
if (loading) return null;
return (
<div>
Normal Page:
<TemplateSelector
current={pageTemplateDocId}
onChange={updatePageTemplate}
/>
<br />
Journal:
<TemplateSelector
current={journalTemplateDocId}
onChange={updateJournalTemplate}
/>
</div>
);
};
interface TemplateSelectorProps {
current?: string;
onChange?: (id?: string) => void;
}
const TemplateSelector = ({ current, onChange }: TemplateSelectorProps) => {
const docsService = useService(DocsService);
const doc = useLiveData(current ? docsService.list.doc$(current) : null);
const isInTrash = useLiveData(doc?.trash$);
return (
<Menu items={<List onChange={onChange} />}>
<Button>{isInTrash ? 'Doc is removed' : (doc?.id ?? 'Unset')}</Button>
</Menu>
);
};
const List = ({ onChange }: { onChange?: (id?: string) => void }) => {
const list = useService(TemplateDocService).list;
const [docs] = useState(list.getTemplateDocs());
const handleClick = useCallback(
(id: string) => {
onChange?.(id);
},
[onChange]
);
return docs.map(doc => {
return <DocItem key={doc.id} doc={doc} onClick={handleClick} />;
});
};
interface DocItemProps {
doc: DocRecord;
onClick?: (id: string) => void;
}
const DocItem = ({ doc, onClick }: DocItemProps) => {
const docDisplayService = useService(DocDisplayMetaService);
const Icon = useLiveData(docDisplayService.icon$(doc.id));
const title = useLiveData(docDisplayService.title$(doc.id));
const handleClick = useCallback(() => {
onClick?.(doc.id);
}, [doc.id, onClick]);
return (
<MenuItem onClick={handleClick}>
<li className={styles.menuItem}>
<Icon className={styles.menuItemIcon} />
<span className={styles.menuItemText}>{title}</span>
</li>
</MenuItem>
);
};

View File

@@ -1,5 +1,6 @@
import type { Framework } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { WorkspaceDB } from './entities/db';
import { WorkspaceDBTable } from './entities/table';
@@ -11,7 +12,7 @@ export { WorkspaceDBService } from './services/db';
export function configureWorkspaceDBModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(WorkspaceDBService, [WorkspaceService])
.service(WorkspaceDBService, [WorkspaceService, WorkspaceServerService])
.entity(WorkspaceDB)
.entity(WorkspaceDBTable, [WorkspaceService]);
}

View File

@@ -48,6 +48,10 @@ export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
key: f.string().primaryKey(),
index: f.string(),
},
settings: {
key: f.string().primaryKey(),
value: f.json(),
},
} as const satisfies DBSchemaBuilder;
export type AFFiNEWorkspaceUserdataDbSchema =
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;

View File

@@ -1,11 +1,13 @@
import {
createORMClient,
LiveData,
ObjectPool,
Service,
YjsDBAdapter,
} from '@toeverything/infra';
import { Doc as YDoc } from 'yjs';
import { AuthService, type WorkspaceServerService } from '../../cloud';
import type { WorkspaceService } from '../../workspace';
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
import {
@@ -31,7 +33,10 @@ export class WorkspaceDBService extends Service {
},
});
constructor(private readonly workspaceService: WorkspaceService) {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly workspaceServerService: WorkspaceServerService
) {
super();
this.db = this.framework.createEntity(
WorkspaceDB<AFFiNEWorkspaceDbSchema>,
@@ -95,6 +100,25 @@ export class WorkspaceDBService extends Service {
return newDB as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
}
authService = this.workspaceServerService.server?.scope.get(AuthService);
public get userdataDB$() {
// if is local workspace or no account, use __local__ userdata
// sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata
if (
this.workspaceService.workspace.meta.flavour === 'local' ||
!this.authService
) {
return new LiveData(this.userdataDB('__local__'));
} else {
return this.authService.session.account$.map(account => {
if (!account) {
return this.userdataDB('__local__');
}
return this.userdataDB(account.id);
});
}
}
static isDBDocId(docId: string) {
return docId.startsWith('db$') || docId.startsWith('userdata$');
}

View File

@@ -1,6 +1,5 @@
import { type Framework } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceDBService } from '../db';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { FavoriteList } from './entities/favorite-list';
@@ -28,11 +27,7 @@ export function configureFavoriteModule(framework: Framework) {
.scope(WorkspaceScope)
.service(FavoriteService)
.entity(FavoriteList, [FavoriteStore])
.store(FavoriteStore, [
WorkspaceDBService,
WorkspaceService,
WorkspaceServerService,
])
.store(FavoriteStore, [WorkspaceDBService])
.service(MigrationFavoriteItemsAdapter, [WorkspaceService])
.service(CompatibleFavoriteItemsAdapter, [FavoriteService]);
}

View File

@@ -1,9 +1,7 @@
import { LiveData, Store } from '@toeverything/infra';
import { map } from 'rxjs';
import { AuthService, type WorkspaceServerService } from '../../cloud';
import type { WorkspaceDBService } from '../../db';
import type { WorkspaceService } from '../../workspace';
import type { FavoriteSupportTypeUnion } from '../constant';
import { isFavoriteSupportType } from '../constant';
@@ -14,41 +12,18 @@ export interface FavoriteRecord {
}
export class FavoriteStore extends Store {
authService = this.workspaceServerService.server?.scope.get(AuthService);
constructor(
private readonly workspaceDBService: WorkspaceDBService,
private readonly workspaceService: WorkspaceService,
private readonly workspaceServerService: WorkspaceServerService
) {
constructor(private readonly workspaceDBService: WorkspaceDBService) {
super();
}
private get userdataDB$() {
// if is local workspace or no account, use __local__ userdata
// sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata
if (
this.workspaceService.workspace.meta.flavour === 'local' ||
!this.authService
) {
return new LiveData(this.workspaceDBService.userdataDB('__local__'));
} else {
return this.authService.session.account$.map(account => {
if (!account) {
return this.workspaceDBService.userdataDB('__local__');
}
return this.workspaceDBService.userdataDB(account.id);
});
}
}
watchIsLoading() {
return this.userdataDB$
return this.workspaceDBService.userdataDB$
.map(db => LiveData.from(db.favorite.isLoading$, false))
.flat();
}
watchFavorites() {
return this.userdataDB$
return this.workspaceDBService.userdataDB$
.map(db => LiveData.from(db.favorite.find$(), []))
.flat()
.map(raw => {
@@ -63,7 +38,7 @@ export class FavoriteStore extends Store {
id: string,
index: string
): FavoriteRecord {
const db = this.userdataDB$.value;
const db = this.workspaceDBService.userdataDB$.value;
const raw = db.favorite.create({
key: this.encodeKey(type, id),
index,
@@ -72,17 +47,17 @@ export class FavoriteStore extends Store {
}
reorderFavorite(type: FavoriteSupportTypeUnion, id: string, index: string) {
const db = this.userdataDB$.value;
const db = this.workspaceDBService.userdataDB$.value;
db.favorite.update(this.encodeKey(type, id), { index });
}
removeFavorite(type: FavoriteSupportTypeUnion, id: string) {
const db = this.userdataDB$.value;
const db = this.workspaceDBService.userdataDB$.value;
db.favorite.delete(this.encodeKey(type, id));
}
watchFavorite(type: FavoriteSupportTypeUnion, id: string) {
const db = this.userdataDB$.value;
const db = this.workspaceDBService.userdataDB$.value;
return LiveData.from<FavoriteRecord | undefined>(
db.favorite
.get$(this.encodeKey(type, id))

View File

@@ -1,12 +1,22 @@
import { Entity } from '@toeverything/infra';
export type TemplateDocSettings = {
templateId?: string;
journalTemplateId?: string;
};
import type { TemplateDocSettingStore } from '../store/setting';
export class TemplateDocSetting extends Entity {
constructor() {
constructor(private readonly store: TemplateDocSettingStore) {
super();
}
loading$ = this.store.watchIsLoading();
setting$ = this.store.watchSetting();
pageTemplateDocId$ = this.store.watchSettingKey('pageTemplateId');
journalTemplateDocId$ = this.store.watchSettingKey('journalTemplateId');
updatePageTemplateDocId(id?: string) {
this.store.updateSetting('pageTemplateId', id);
}
updateJournalTemplateDocId(id?: string) {
this.store.updateSetting('journalTemplateId', id);
}
}

View File

@@ -7,6 +7,7 @@ import { TemplateDocList } from './entities/list';
import { TemplateDocSetting } from './entities/setting';
import { TemplateDocService } from './services/template-doc';
import { TemplateDocListStore } from './store/list';
import { TemplateDocSettingStore } from './store/setting';
export { TemplateDocService };
export * from './view/template-list-menu';
@@ -17,5 +18,6 @@ export const configureTemplateDocModule = (framework: Framework) => {
.service(TemplateDocService)
.store(TemplateDocListStore, [WorkspaceDBService])
.entity(TemplateDocList, [TemplateDocListStore, DocsService])
.entity(TemplateDocSetting);
.store(TemplateDocSettingStore, [WorkspaceDBService])
.entity(TemplateDocSetting, [TemplateDocSettingStore]);
};

View File

@@ -0,0 +1,47 @@
import { LiveData, Store } from '@toeverything/infra';
import type { WorkspaceDBService } from '../../db';
import type { TemplateDocSettings } from '../type';
export class TemplateDocSettingStore extends Store {
private readonly key = 'templateDoc';
constructor(private readonly dbService: WorkspaceDBService) {
super();
}
watchIsLoading() {
return this.dbService.userdataDB$
.map(db => LiveData.from(db.settings.isLoading$, false))
.flat();
}
watchSetting() {
return this.dbService.userdataDB$
.map(db => LiveData.from(db.settings.find$({ key: this.key }), []))
.flat()
.map(raw => raw?.[0]?.value as TemplateDocSettings);
}
watchSettingKey<T extends keyof TemplateDocSettings>(key: T) {
return this.dbService.userdataDB$
.map(db => LiveData.from(db.settings.find$({ key: this.key }), []))
.flat()
.map(raw => {
const value = raw?.[0]?.value as TemplateDocSettings;
if (!value) return undefined;
return value[key];
});
}
updateSetting<T extends keyof TemplateDocSettings>(
key: T,
value: TemplateDocSettings[T]
) {
const db = this.dbService.userdataDB$.value;
const prev = db.settings.find({ key: this.key })[0]?.value ?? {};
db.settings.update(this.key, {
value: { ...prev, [key]: value },
});
}
}

View File

@@ -0,0 +1,4 @@
export type TemplateDocSettings = {
pageTemplateId?: string;
journalTemplateId?: string;
};