mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): template-doc settings for user-worksapce scope (#9566)
This commit is contained in:
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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$');
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
4
packages/frontend/core/src/modules/template-doc/type.ts
Normal file
4
packages/frontend/core/src/modules/template-doc/type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type TemplateDocSettings = {
|
||||
pageTemplateId?: string;
|
||||
journalTemplateId?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user