feat(core): new template doc property (#9538)

close AF-2045, AF-2047, AF-2065
This commit is contained in:
CatsJuice
2025-01-14 02:10:33 +00:00
parent 777eea124d
commit 10196f6785
26 changed files with 719 additions and 8 deletions

View File

@@ -0,0 +1,24 @@
import { Entity, LiveData } from '@toeverything/infra';
import type { DocRecord, DocsService } from '../../doc';
import type { TemplateDocListStore } from '../store/list';
export class TemplateDocList extends Entity {
constructor(
public listStore: TemplateDocListStore,
public docsService: DocsService
) {
super();
}
public isTemplate$(docId: string) {
return LiveData.from(this.listStore.watchTemplateDoc(docId), false);
}
public getTemplateDocs() {
return this.listStore
.getTemplateDocIds()
.map(id => this.docsService.list.doc$(id).value)
.filter((doc): doc is DocRecord => !!doc && !doc.trash$.value);
}
}

View File

@@ -0,0 +1,12 @@
import { Entity } from '@toeverything/infra';
export type TemplateDocSettings = {
templateId?: string;
journalTemplateId?: string;
};
export class TemplateDocSetting extends Entity {
constructor() {
super();
}
}

View File

@@ -0,0 +1,21 @@
import type { Framework } from '@toeverything/infra';
import { WorkspaceDBService } from '../db';
import { DocsService } from '../doc';
import { WorkspaceScope } from '../workspace';
import { TemplateDocList } from './entities/list';
import { TemplateDocSetting } from './entities/setting';
import { TemplateDocService } from './services/template-doc';
import { TemplateDocListStore } from './store/list';
export { TemplateDocService };
export * from './view/template-list-menu';
export const configureTemplateDocModule = (framework: Framework) => {
framework
.scope(WorkspaceScope)
.service(TemplateDocService)
.store(TemplateDocListStore, [WorkspaceDBService])
.entity(TemplateDocList, [TemplateDocListStore, DocsService])
.entity(TemplateDocSetting);
};

View File

@@ -0,0 +1,9 @@
import { Service } from '@toeverything/infra';
import { TemplateDocList } from '../entities/list';
import { TemplateDocSetting } from '../entities/setting';
export class TemplateDocService extends Service {
public readonly list = this.framework.createEntity(TemplateDocList);
public readonly setting = this.framework.createEntity(TemplateDocSetting);
}

View File

@@ -0,0 +1,33 @@
import { Store } from '@toeverything/infra';
import { map } from 'rxjs';
import type { WorkspaceDBService } from '../../db';
export class TemplateDocListStore extends Store {
constructor(private readonly dbService: WorkspaceDBService) {
super();
}
isTemplateDoc(docId: string) {
return !!this.dbService.db.docProperties.find({
id: docId,
isTemplate: true,
})[0]?.isTemplate;
}
watchTemplateDoc(docId: string) {
return this.dbService.db.docProperties
.find$({ id: docId, isTemplate: true })
.pipe(map(res => res[0]?.isTemplate));
}
getTemplateDocIds() {
return this.dbService.db.docProperties
.find({ isTemplate: true })
.map(property => property.id);
}
watchTemplateDocs() {
return this.dbService.db.docProperties.find$({ isTemplate: true });
}
}

View File

@@ -0,0 +1,42 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const list = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
minWidth: 250,
maxWidth: 355,
});
export const item = style({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 4,
});
export const itemIcon = style({
fontSize: 20,
lineHeight: 0,
color: cssVarV2.icon.primary,
});
export const itemText = style({
width: 0,
flex: 1,
fontSize: 14,
lineHeight: '22px',
color: cssVarV2.text.primary,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
});
export const menuContent = style({
paddingRight: 0,
});
export const scrollableViewport = style({
paddingRight: 8,
maxHeight: 360,
});

View File

@@ -0,0 +1,82 @@
import { Menu, MenuItem, type MenuProps, Scrollable } from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useLiveData, useService } from '@toeverything/infra';
import { type PropsWithChildren, useState } from 'react';
import { type DocRecord, DocsService } from '../../doc';
import { DocDisplayMetaService } from '../../doc-display-meta';
import { TemplateDocService } from '../services/template-doc';
import * as styles from './styles.css';
interface CommonProps {
target?: string;
}
interface DocItemProps extends CommonProps {
doc: DocRecord;
}
const DocItem = ({ doc, target }: DocItemProps) => {
const docDisplayService = useService(DocDisplayMetaService);
const Icon = useLiveData(docDisplayService.icon$(doc.id));
const title = useLiveData(docDisplayService.title$(doc.id));
const docsService = useService(DocsService);
const onClick = useAsyncCallback(async () => {
await docsService.duplicateFromTemplate(doc.id, target);
}, [doc.id, docsService, target]);
return (
<MenuItem onClick={onClick} style={{ padding: 0 }}>
<li className={styles.item}>
<Icon className={styles.itemIcon} />
<span className={styles.itemText}>{title}</span>
</li>
</MenuItem>
);
};
export const TemplateListMenuContent = ({ target }: CommonProps) => {
const templateDocService = useService(TemplateDocService);
const [templateDocs] = useState(() =>
templateDocService.list.getTemplateDocs()
);
return (
<ul className={styles.list}>
{templateDocs.map(doc => (
<DocItem key={doc.id} doc={doc} target={target} />
))}
</ul>
);
};
export const TemplateListMenuContentScrollable = ({ target }: CommonProps) => {
return (
<Scrollable.Root>
<Scrollable.Scrollbar />
<Scrollable.Viewport className={styles.scrollableViewport}>
<TemplateListMenuContent target={target} />
</Scrollable.Viewport>
</Scrollable.Root>
);
};
export const TemplateListMenu = ({
children,
target,
contentOptions,
...otherProps
}: PropsWithChildren<CommonProps> & Omit<MenuProps, 'items'>) => {
return (
<Menu
items={<TemplateListMenuContentScrollable target={target} />}
contentOptions={{
...contentOptions,
className: styles.menuContent,
}}
{...otherProps}
>
{children}
</Menu>
);
};