mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08: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 type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceServerService } from '../cloud';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
import { WorkspaceDB } from './entities/db';
|
import { WorkspaceDB } from './entities/db';
|
||||||
import { WorkspaceDBTable } from './entities/table';
|
import { WorkspaceDBTable } from './entities/table';
|
||||||
@@ -11,7 +12,7 @@ export { WorkspaceDBService } from './services/db';
|
|||||||
export function configureWorkspaceDBModule(framework: Framework) {
|
export function configureWorkspaceDBModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(WorkspaceDBService, [WorkspaceService])
|
.service(WorkspaceDBService, [WorkspaceService, WorkspaceServerService])
|
||||||
.entity(WorkspaceDB)
|
.entity(WorkspaceDB)
|
||||||
.entity(WorkspaceDBTable, [WorkspaceService]);
|
.entity(WorkspaceDBTable, [WorkspaceService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
|||||||
key: f.string().primaryKey(),
|
key: f.string().primaryKey(),
|
||||||
index: f.string(),
|
index: f.string(),
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
key: f.string().primaryKey(),
|
||||||
|
value: f.json(),
|
||||||
|
},
|
||||||
} as const satisfies DBSchemaBuilder;
|
} as const satisfies DBSchemaBuilder;
|
||||||
export type AFFiNEWorkspaceUserdataDbSchema =
|
export type AFFiNEWorkspaceUserdataDbSchema =
|
||||||
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;
|
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
createORMClient,
|
createORMClient,
|
||||||
|
LiveData,
|
||||||
ObjectPool,
|
ObjectPool,
|
||||||
Service,
|
Service,
|
||||||
YjsDBAdapter,
|
YjsDBAdapter,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { Doc as YDoc } from 'yjs';
|
import { Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
|
import { AuthService, type WorkspaceServerService } from '../../cloud';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
|
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
|
||||||
import {
|
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();
|
super();
|
||||||
this.db = this.framework.createEntity(
|
this.db = this.framework.createEntity(
|
||||||
WorkspaceDB<AFFiNEWorkspaceDbSchema>,
|
WorkspaceDB<AFFiNEWorkspaceDbSchema>,
|
||||||
@@ -95,6 +100,25 @@ export class WorkspaceDBService extends Service {
|
|||||||
return newDB as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
|
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) {
|
static isDBDocId(docId: string) {
|
||||||
return docId.startsWith('db$') || docId.startsWith('userdata$');
|
return docId.startsWith('db$') || docId.startsWith('userdata$');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { WorkspaceServerService } from '../cloud';
|
|
||||||
import { WorkspaceDBService } from '../db';
|
import { WorkspaceDBService } from '../db';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
import { FavoriteList } from './entities/favorite-list';
|
import { FavoriteList } from './entities/favorite-list';
|
||||||
@@ -28,11 +27,7 @@ export function configureFavoriteModule(framework: Framework) {
|
|||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(FavoriteService)
|
.service(FavoriteService)
|
||||||
.entity(FavoriteList, [FavoriteStore])
|
.entity(FavoriteList, [FavoriteStore])
|
||||||
.store(FavoriteStore, [
|
.store(FavoriteStore, [WorkspaceDBService])
|
||||||
WorkspaceDBService,
|
|
||||||
WorkspaceService,
|
|
||||||
WorkspaceServerService,
|
|
||||||
])
|
|
||||||
.service(MigrationFavoriteItemsAdapter, [WorkspaceService])
|
.service(MigrationFavoriteItemsAdapter, [WorkspaceService])
|
||||||
.service(CompatibleFavoriteItemsAdapter, [FavoriteService]);
|
.service(CompatibleFavoriteItemsAdapter, [FavoriteService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { LiveData, Store } from '@toeverything/infra';
|
import { LiveData, Store } from '@toeverything/infra';
|
||||||
import { map } from 'rxjs';
|
import { map } from 'rxjs';
|
||||||
|
|
||||||
import { AuthService, type WorkspaceServerService } from '../../cloud';
|
|
||||||
import type { WorkspaceDBService } from '../../db';
|
import type { WorkspaceDBService } from '../../db';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
|
||||||
import type { FavoriteSupportTypeUnion } from '../constant';
|
import type { FavoriteSupportTypeUnion } from '../constant';
|
||||||
import { isFavoriteSupportType } from '../constant';
|
import { isFavoriteSupportType } from '../constant';
|
||||||
|
|
||||||
@@ -14,41 +12,18 @@ export interface FavoriteRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FavoriteStore extends Store {
|
export class FavoriteStore extends Store {
|
||||||
authService = this.workspaceServerService.server?.scope.get(AuthService);
|
constructor(private readonly workspaceDBService: WorkspaceDBService) {
|
||||||
constructor(
|
|
||||||
private readonly workspaceDBService: WorkspaceDBService,
|
|
||||||
private readonly workspaceService: WorkspaceService,
|
|
||||||
private readonly workspaceServerService: WorkspaceServerService
|
|
||||||
) {
|
|
||||||
super();
|
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() {
|
watchIsLoading() {
|
||||||
return this.userdataDB$
|
return this.workspaceDBService.userdataDB$
|
||||||
.map(db => LiveData.from(db.favorite.isLoading$, false))
|
.map(db => LiveData.from(db.favorite.isLoading$, false))
|
||||||
.flat();
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFavorites() {
|
watchFavorites() {
|
||||||
return this.userdataDB$
|
return this.workspaceDBService.userdataDB$
|
||||||
.map(db => LiveData.from(db.favorite.find$(), []))
|
.map(db => LiveData.from(db.favorite.find$(), []))
|
||||||
.flat()
|
.flat()
|
||||||
.map(raw => {
|
.map(raw => {
|
||||||
@@ -63,7 +38,7 @@ export class FavoriteStore extends Store {
|
|||||||
id: string,
|
id: string,
|
||||||
index: string
|
index: string
|
||||||
): FavoriteRecord {
|
): FavoriteRecord {
|
||||||
const db = this.userdataDB$.value;
|
const db = this.workspaceDBService.userdataDB$.value;
|
||||||
const raw = db.favorite.create({
|
const raw = db.favorite.create({
|
||||||
key: this.encodeKey(type, id),
|
key: this.encodeKey(type, id),
|
||||||
index,
|
index,
|
||||||
@@ -72,17 +47,17 @@ export class FavoriteStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reorderFavorite(type: FavoriteSupportTypeUnion, id: string, index: string) {
|
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 });
|
db.favorite.update(this.encodeKey(type, id), { index });
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFavorite(type: FavoriteSupportTypeUnion, id: string) {
|
removeFavorite(type: FavoriteSupportTypeUnion, id: string) {
|
||||||
const db = this.userdataDB$.value;
|
const db = this.workspaceDBService.userdataDB$.value;
|
||||||
db.favorite.delete(this.encodeKey(type, id));
|
db.favorite.delete(this.encodeKey(type, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFavorite(type: FavoriteSupportTypeUnion, id: string) {
|
watchFavorite(type: FavoriteSupportTypeUnion, id: string) {
|
||||||
const db = this.userdataDB$.value;
|
const db = this.workspaceDBService.userdataDB$.value;
|
||||||
return LiveData.from<FavoriteRecord | undefined>(
|
return LiveData.from<FavoriteRecord | undefined>(
|
||||||
db.favorite
|
db.favorite
|
||||||
.get$(this.encodeKey(type, id))
|
.get$(this.encodeKey(type, id))
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { Entity } from '@toeverything/infra';
|
import { Entity } from '@toeverything/infra';
|
||||||
|
|
||||||
export type TemplateDocSettings = {
|
import type { TemplateDocSettingStore } from '../store/setting';
|
||||||
templateId?: string;
|
|
||||||
journalTemplateId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TemplateDocSetting extends Entity {
|
export class TemplateDocSetting extends Entity {
|
||||||
constructor() {
|
constructor(private readonly store: TemplateDocSettingStore) {
|
||||||
super();
|
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 { TemplateDocSetting } from './entities/setting';
|
||||||
import { TemplateDocService } from './services/template-doc';
|
import { TemplateDocService } from './services/template-doc';
|
||||||
import { TemplateDocListStore } from './store/list';
|
import { TemplateDocListStore } from './store/list';
|
||||||
|
import { TemplateDocSettingStore } from './store/setting';
|
||||||
|
|
||||||
export { TemplateDocService };
|
export { TemplateDocService };
|
||||||
export * from './view/template-list-menu';
|
export * from './view/template-list-menu';
|
||||||
@@ -17,5 +18,6 @@ export const configureTemplateDocModule = (framework: Framework) => {
|
|||||||
.service(TemplateDocService)
|
.service(TemplateDocService)
|
||||||
.store(TemplateDocListStore, [WorkspaceDBService])
|
.store(TemplateDocListStore, [WorkspaceDBService])
|
||||||
.entity(TemplateDocList, [TemplateDocListStore, DocsService])
|
.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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type TemplateDocSettings = {
|
||||||
|
pageTemplateId?: string;
|
||||||
|
journalTemplateId?: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user