mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
@@ -10,3 +10,23 @@ export const root = style({
|
||||
borderColor: cssVarV2('layer/insideBorder/border'),
|
||||
background: cssVarV2('button/siderbarPrimary/background'),
|
||||
});
|
||||
|
||||
export const withAskRoot = style([
|
||||
root,
|
||||
{
|
||||
width: 'auto',
|
||||
padding: 6,
|
||||
},
|
||||
]);
|
||||
|
||||
export const withAskContent = style({
|
||||
fontSize: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
color: cssVarV2.icon.primary,
|
||||
});
|
||||
|
||||
export const templateMenu = style({
|
||||
width: 280,
|
||||
});
|
||||
|
||||
@@ -1,34 +1,190 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { Button, IconButton, Menu, MenuItem, MenuSub } from '@affine/component';
|
||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
TemplateDocService,
|
||||
TemplateListMenuContentScrollable,
|
||||
} from '@affine/core/modules/template-doc';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { inferOpenMode } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
EdgelessIcon,
|
||||
PageIcon,
|
||||
PlusIcon,
|
||||
TemplateIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import { type MouseEvent, useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
/**
|
||||
* @return a function to create a new doc, will duplicate the template doc if the page template is enabled
|
||||
*/
|
||||
const useNewDoc = () => {
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const templateDocService = useService(TemplateDocService);
|
||||
const docsService = useService(DocsService);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const enableTemplateDoc = useLiveData(
|
||||
featureFlagService.flags.enable_template_doc.$
|
||||
);
|
||||
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const enablePageTemplate = useLiveData(
|
||||
templateDocService.setting.enablePageTemplate$
|
||||
);
|
||||
const pageTemplateDocId = useLiveData(
|
||||
templateDocService.setting.pageTemplateDocId$
|
||||
);
|
||||
|
||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||
|
||||
const createPage = useAsyncCallback(
|
||||
async (e?: MouseEvent, mode?: DocMode) => {
|
||||
if (enableTemplateDoc && enablePageTemplate && pageTemplateDocId) {
|
||||
const docId =
|
||||
await docsService.duplicateFromTemplate(pageTemplateDocId);
|
||||
workbench.openDoc(docId, { at: inferOpenMode(e) });
|
||||
} else {
|
||||
pageHelper.createPage(mode, { at: inferOpenMode(e) });
|
||||
}
|
||||
},
|
||||
[
|
||||
docsService,
|
||||
enablePageTemplate,
|
||||
enableTemplateDoc,
|
||||
pageHelper,
|
||||
pageTemplateDocId,
|
||||
workbench,
|
||||
]
|
||||
);
|
||||
|
||||
return createPage;
|
||||
};
|
||||
|
||||
interface AddPageButtonProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const sideBottom = { side: 'bottom' as const };
|
||||
export function AddPageButton({ className, style }: AddPageButtonProps) {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||
export function AddPageButton(props: AddPageButtonProps) {
|
||||
const editorSetting = useService(EditorSettingService);
|
||||
const newDocDefaultMode = useLiveData(
|
||||
editorSetting.editorSetting.settings$.selector(s => s.newDocDefaultMode)
|
||||
);
|
||||
|
||||
return newDocDefaultMode === 'ask' ? (
|
||||
<AddPageWithAsk {...props} />
|
||||
) : (
|
||||
<AddPageWithoutAsk {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AddPageWithAsk({ className, style }: AddPageButtonProps) {
|
||||
const t = useI18n();
|
||||
const createDoc = useNewDoc();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const createPage = useCallback(
|
||||
(e?: MouseEvent) => {
|
||||
createDoc(e, 'page');
|
||||
track.$.navigationPanel.$.createDoc();
|
||||
},
|
||||
[createDoc]
|
||||
);
|
||||
const createEdgeless = useCallback(
|
||||
(e?: MouseEvent) => {
|
||||
createDoc(e, 'edgeless');
|
||||
track.$.navigationPanel.$.createDoc();
|
||||
},
|
||||
[createDoc]
|
||||
);
|
||||
|
||||
const createDocFromTemplate = useAsyncCallback(
|
||||
async (templateId: string) => {
|
||||
const docId = await docsService.duplicateFromTemplate(templateId);
|
||||
workbench.openDoc(docId);
|
||||
},
|
||||
[docsService, workbench]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
prefixIcon={<PageIcon />}
|
||||
onClick={createPage}
|
||||
onAuxClick={createPage}
|
||||
>
|
||||
{t['Page']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
prefixIcon={<EdgelessIcon />}
|
||||
onClick={createEdgeless}
|
||||
onAuxClick={createEdgeless}
|
||||
>
|
||||
{t['Edgeless']()}
|
||||
</MenuItem>
|
||||
<MenuSub
|
||||
triggerOptions={{
|
||||
prefixIcon: <TemplateIcon />,
|
||||
}}
|
||||
subContentOptions={{
|
||||
sideOffset: 16,
|
||||
className: styles.templateMenu,
|
||||
}}
|
||||
items={
|
||||
<TemplateListMenuContentScrollable
|
||||
onSelect={createDocFromTemplate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t['Template']()}
|
||||
</MenuSub>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
tooltip={t['New Page']()}
|
||||
tooltipOptions={sideBottom}
|
||||
data-testid="sidebar-new-page-button"
|
||||
className={clsx([styles.withAskRoot, className])}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.withAskContent}>
|
||||
<PlusIcon />
|
||||
<ArrowDownSmallIcon />
|
||||
</div>
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function AddPageWithoutAsk({ className, style }: AddPageButtonProps) {
|
||||
const createDoc = useNewDoc();
|
||||
|
||||
const onClickNewPage = useCallback(
|
||||
(e?: MouseEvent) => {
|
||||
pageHelper.createPage(undefined, { at: inferOpenMode(e) });
|
||||
createDoc(e);
|
||||
track.$.navigationPanel.$.createDoc();
|
||||
},
|
||||
[pageHelper]
|
||||
[createDoc]
|
||||
);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
@@ -19,7 +19,7 @@ export const fontStyleOptions = [
|
||||
const AffineEditorSettingSchema = z.object({
|
||||
fontFamily: z.enum(['Sans', 'Serif', 'Mono', 'Custom']).default('Sans'),
|
||||
customFontFamily: z.string().default(''),
|
||||
newDocDefaultMode: z.enum(['edgeless', 'page']).default('page'),
|
||||
newDocDefaultMode: z.enum(['edgeless', 'page', 'ask']).default('page'),
|
||||
fullWidthLayout: z.boolean().default(false),
|
||||
displayDocInfo: z.boolean().default(true),
|
||||
displayBiDirectionalLink: z.boolean().default(true),
|
||||
|
||||
@@ -20,7 +20,8 @@ export class EditorSettingService extends Service {
|
||||
workspace.docCollection.slots.docCreated.on(docId => {
|
||||
const preferMode = this.editorSetting.settings$.value.newDocDefaultMode;
|
||||
const docsService = workspace.scope.get(DocsService);
|
||||
docsService.list.setPrimaryMode(docId, preferMode);
|
||||
const mode = preferMode === 'ask' ? 'page' : preferMode;
|
||||
docsService.list.setPrimaryMode(docId, mode);
|
||||
});
|
||||
// never dispose, because this service always live longer than workspace
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { DocScope, DocService, DocsService } from '../doc';
|
||||
import { EditorSettingService } from '../editor-setting';
|
||||
import { FeatureFlagService } from '../feature-flag';
|
||||
import { TemplateDocService } from '../template-doc';
|
||||
import { WorkspaceScope } from '../workspace';
|
||||
import { JournalService } from './services/journal';
|
||||
import { JournalDocService } from './services/journal-doc';
|
||||
@@ -18,7 +20,13 @@ export { suggestJournalDate } from './suggest-journal-date';
|
||||
export function configureJournalModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(JournalService, [JournalStore, DocsService, EditorSettingService])
|
||||
.service(JournalService, [
|
||||
JournalStore,
|
||||
DocsService,
|
||||
EditorSettingService,
|
||||
TemplateDocService,
|
||||
FeatureFlagService,
|
||||
])
|
||||
.store(JournalStore, [DocsService])
|
||||
.scope(DocScope)
|
||||
.service(JournalDocService, [DocService, JournalService]);
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '../../../blocksuite/initialization';
|
||||
import type { DocsService } from '../../doc';
|
||||
import type { EditorSettingService } from '../../editor-setting';
|
||||
import type { FeatureFlagService } from '../../feature-flag';
|
||||
import type { TemplateDocService } from '../../template-doc';
|
||||
import type { JournalStore } from '../store/journal';
|
||||
|
||||
export type MaybeDate = Date | string | number;
|
||||
@@ -18,7 +20,9 @@ export class JournalService extends Service {
|
||||
constructor(
|
||||
private readonly store: JournalStore,
|
||||
private readonly docsService: DocsService,
|
||||
private readonly editorSettingService: EditorSettingService
|
||||
private readonly editorSettingService: EditorSettingService,
|
||||
private readonly templateDocService: TemplateDocService,
|
||||
private readonly featureFlagService: FeatureFlagService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -52,8 +56,6 @@ export class JournalService extends Service {
|
||||
const day = dayjs(maybeDate);
|
||||
const title = day.format(JOURNAL_DATE_FORMAT);
|
||||
const docRecord = this.docsService.createDoc();
|
||||
const { doc, release } = this.docsService.open(docRecord.id);
|
||||
this.docsService.list.setPrimaryMode(docRecord.id, 'page');
|
||||
// set created date to match the journal date
|
||||
docRecord.setMeta({
|
||||
createDate: dayjs()
|
||||
@@ -63,12 +65,36 @@ export class JournalService extends Service {
|
||||
.toDate()
|
||||
.getTime(),
|
||||
});
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(title) },
|
||||
note: this.editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
initDocFromProps(doc.blockSuiteDoc, docProps);
|
||||
release();
|
||||
|
||||
const enableTemplateDoc =
|
||||
this.featureFlagService.flags.enable_template_doc.value;
|
||||
const enablePageTemplate =
|
||||
this.templateDocService.setting.enablePageTemplate$.value;
|
||||
const pageTemplateDocId =
|
||||
this.templateDocService.setting.pageTemplateDocId$.value;
|
||||
const journalTemplateDocId =
|
||||
this.templateDocService.setting.journalTemplateDocId$.value;
|
||||
// if journal template configured
|
||||
if (enableTemplateDoc && journalTemplateDocId) {
|
||||
this.docsService
|
||||
.duplicateFromTemplate(journalTemplateDocId, docRecord.id)
|
||||
.catch(console.error);
|
||||
}
|
||||
// journal template not configured, use page template
|
||||
else if (enableTemplateDoc && enablePageTemplate && pageTemplateDocId) {
|
||||
this.docsService
|
||||
.duplicateFromTemplate(pageTemplateDocId, docRecord.id)
|
||||
.catch(console.error);
|
||||
} else {
|
||||
const { doc, release } = this.docsService.open(docRecord.id);
|
||||
this.docsService.list.setPrimaryMode(docRecord.id, 'page');
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(title) },
|
||||
note: this.editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
initDocFromProps(doc.blockSuiteDoc, docProps);
|
||||
release();
|
||||
}
|
||||
this.setJournalDate(docRecord.id, title);
|
||||
return docRecord;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ export class TemplateDocSetting extends Entity {
|
||||
|
||||
loading$ = this.store.watchIsLoading();
|
||||
setting$ = this.store.watchSetting();
|
||||
enablePageTemplate$ = this.store.watchSettingKey('enablePageTemplate');
|
||||
pageTemplateDocId$ = this.store.watchSettingKey('pageTemplateId');
|
||||
journalTemplateDocId$ = this.store.watchSettingKey('journalTemplateId');
|
||||
|
||||
togglePageTemplate(enable: boolean) {
|
||||
this.store.updateSetting('enablePageTemplate', enable);
|
||||
}
|
||||
|
||||
updatePageTemplateDocId(id?: string) {
|
||||
this.store.updateSetting('pageTemplateId', id);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ export class TemplateDocSettingStore extends Store {
|
||||
) {
|
||||
const db = this.dbService.userdataDB$.value;
|
||||
const prev = db.settings.find({ key: this.key })[0]?.value ?? {};
|
||||
db.settings.update(this.key, {
|
||||
db.settings.create({
|
||||
key: this.key,
|
||||
value: { ...prev, [key]: value },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type TemplateDocSettings = {
|
||||
enablePageTemplate?: boolean;
|
||||
pageTemplateId?: string;
|
||||
journalTemplateId?: string;
|
||||
};
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
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({
|
||||
width: 280,
|
||||
paddingRight: 0,
|
||||
});
|
||||
export const scrollableViewport = style({
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
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 { useState } from 'react';
|
||||
|
||||
import { type DocRecord, DocsService } from '../../doc';
|
||||
import { type DocRecord } from '../../doc';
|
||||
import { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
import { TemplateDocService } from '../services/template-doc';
|
||||
import * as styles from './styles.css';
|
||||
interface CommonProps {
|
||||
target?: string;
|
||||
onSelect?: (docId: string) => void;
|
||||
}
|
||||
|
||||
interface DocItemProps extends CommonProps {
|
||||
doc: DocRecord;
|
||||
}
|
||||
|
||||
const DocItem = ({ doc, target }: DocItemProps) => {
|
||||
const DocItem = ({ doc, onSelect }: 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]);
|
||||
onSelect?.(doc.id);
|
||||
}, [doc.id, onSelect]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} style={{ padding: 0 }}>
|
||||
<li className={styles.item}>
|
||||
<Icon className={styles.itemIcon} />
|
||||
<span className={styles.itemText}>{title}</span>
|
||||
</li>
|
||||
<MenuItem prefixIcon={<Icon />} onClick={onClick}>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateListMenuContent = ({ target }: CommonProps) => {
|
||||
interface TemplateListMenuContentProps extends CommonProps {
|
||||
prefixItems?: React.ReactNode;
|
||||
suffixItems?: React.ReactNode;
|
||||
}
|
||||
export const TemplateListMenuContent = ({
|
||||
prefixItems,
|
||||
suffixItems,
|
||||
...props
|
||||
}: TemplateListMenuContentProps) => {
|
||||
const templateDocService = useService(TemplateDocService);
|
||||
const [templateDocs] = useState(() =>
|
||||
templateDocService.list.getTemplateDocs()
|
||||
@@ -43,33 +47,48 @@ export const TemplateListMenuContent = ({ target }: CommonProps) => {
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{prefixItems}
|
||||
{templateDocs.map(doc => (
|
||||
<DocItem key={doc.id} doc={doc} target={target} />
|
||||
<DocItem key={doc.id} doc={doc} {...props} />
|
||||
))}
|
||||
{suffixItems}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateListMenuContentScrollable = ({ target }: CommonProps) => {
|
||||
export const TemplateListMenuContentScrollable = (
|
||||
props: TemplateListMenuContentProps
|
||||
) => {
|
||||
return (
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Scrollbar />
|
||||
<Scrollable.Viewport className={styles.scrollableViewport}>
|
||||
<TemplateListMenuContent target={target} />
|
||||
<TemplateListMenuContent {...props} />
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
);
|
||||
};
|
||||
|
||||
interface TemplateListMenuProps
|
||||
extends TemplateListMenuContentProps,
|
||||
Omit<MenuProps, 'items'> {}
|
||||
export const TemplateListMenu = ({
|
||||
children,
|
||||
target,
|
||||
onSelect,
|
||||
prefixItems,
|
||||
suffixItems,
|
||||
contentOptions,
|
||||
...otherProps
|
||||
}: PropsWithChildren<CommonProps> & Omit<MenuProps, 'items'>) => {
|
||||
}: TemplateListMenuProps) => {
|
||||
return (
|
||||
<Menu
|
||||
items={<TemplateListMenuContentScrollable target={target} />}
|
||||
items={
|
||||
<TemplateListMenuContentScrollable
|
||||
onSelect={onSelect}
|
||||
prefixItems={prefixItems}
|
||||
suffixItems={suffixItems}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
...contentOptions,
|
||||
className: styles.menuContent,
|
||||
|
||||
Reference in New Issue
Block a user