From af3ad4bfe9c762ab0f82ee53de520aa78f3ec13d Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Tue, 14 Jan 2025 02:10:34 +0000 Subject: [PATCH] feat(core): template doc setting ui (#9658) close AF-2051, AF-2052 --- .../block-suite-editor/starter-bar.tsx | 12 +- .../general-setting/editor/general.tsx | 68 ++++--- .../new-workspace-setting-detail/index.tsx | 2 + .../template.css.ts | 8 +- .../new-workspace-setting-detail/template.tsx | 129 +++++++++++++ .../setting/workspace-setting/template.tsx | 110 ----------- .../views/add-page-button/index.css.ts | 20 ++ .../views/add-page-button/index.tsx | 174 +++++++++++++++++- .../core/src/modules/editor-setting/schema.ts | 2 +- .../editor-setting/services/editor-setting.ts | 3 +- .../core/src/modules/journal/index.ts | 10 +- .../src/modules/journal/services/journal.ts | 44 ++++- .../modules/template-doc/entities/setting.ts | 5 + .../src/modules/template-doc/store/setting.ts | 3 +- .../core/src/modules/template-doc/type.ts | 1 + .../modules/template-doc/view/styles.css.ts | 28 +-- .../template-doc/view/template-list-menu.tsx | 57 ++++-- packages/frontend/i18n/src/resources/en.json | 11 +- 18 files changed, 481 insertions(+), 206 deletions(-) rename packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/{ => new-workspace-setting-detail}/template.css.ts (76%) create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.tsx delete mode 100644 packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.tsx diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/starter-bar.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/starter-bar.tsx index 6c54b7d1d..5be045b1d 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/starter-bar.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/starter-bar.tsx @@ -1,3 +1,4 @@ +import { DocsService } from '@affine/core/modules/doc'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { TemplateDocService, @@ -20,6 +21,7 @@ import { useState, } from 'react'; +import { useAsyncCallback } from '../../hooks/affine-async-hooks'; import * as styles from './starter-bar.css'; const Badge = forwardRef< @@ -48,6 +50,7 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => { const templateDocService = useService(TemplateDocService); const featureFlagService = useService(FeatureFlagService); + const docsService = useService(DocsService); const [templateMenuOpen, setTemplateMenuOpen] = useState(false); @@ -61,6 +64,13 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => { featureFlagService.flags.enable_template_doc.$ ); + const handleSelectTemplate = useAsyncCallback( + async (templateId: string) => { + await docsService.duplicateFromTemplate(templateId, doc.id); + }, + [doc.id, docsService] + ); + const showAI = false; const showEdgeless = false; const showTemplate = !isTemplate && enableTemplateDoc; @@ -82,7 +92,7 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => { {showTemplate ? ( { ); }; + +const menuContentOptions: MenuProps['contentOptions'] = { + align: 'end', + sideOffset: 16, + style: { width: 250 }, +}; const NewDocDefaultModeSettings = () => { const t = useI18n(); const { editorSettingService } = useServices({ EditorSettingService }); const settings = useLiveData(editorSettingService.editorSetting.settings$); - const radioItems = useMemo( - () => [ - { - value: 'page', - label: t['Page'](), - testId: 'page-mode-trigger', - }, - { - value: 'edgeless', - label: t['Edgeless'](), - testId: 'edgeless-mode-trigger', - }, - ], + const items = useMemo( + () => + [ + { + value: 'page', + label: t['Page'](), + testId: 'page-mode-trigger', + }, + { + value: 'edgeless', + label: t['Edgeless'](), + testId: 'edgeless-mode-trigger', + }, + { + value: 'ask', + label: 'Ask every time', + testId: 'ask-every-time-trigger', + }, + ] as const, [t] ); const updateNewDocDefaultMode = useCallback( - (value: DocMode) => { + (value: EditorSettingSchema['newDocDefaultMode']) => { editorSettingService.editorSetting.set('newDocDefaultMode', value); }, [editorSettingService.editorSetting] @@ -340,13 +353,24 @@ const NewDocDefaultModeSettings = () => { 'com.affine.settings.editorSettings.general.default-new-doc.description' ]()} > - + { + return ( + updateNewDocDefaultMode(item.value)} + > + {item.label} + + ); + })} + > + + {items.find(item => item.value === settings.newDocDefaultMode)?.label} + + ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/index.tsx index bc897f4ba..e397474c8 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/index.tsx @@ -19,6 +19,7 @@ import { LabelsPanel } from './labels'; import { MembersPanel } from './members'; import { ProfilePanel } from './profile'; import { SharingPanel } from './sharing'; +import { TemplateDocSetting } from './template'; import type { WorkspaceSettingDetailProps } from './types'; import { WorkspaceQuotaPanel } from './workspace-quota'; @@ -70,6 +71,7 @@ export const WorkspaceSettingDetail = ({ + diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.css.ts similarity index 76% rename from packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.css.ts rename to packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.css.ts index 87713f6d1..1291e2142 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.css.ts @@ -5,10 +5,7 @@ export const menuItem = style({ alignItems: 'center', gap: 8, }); -export const menuItemIcon = style({ - fontSize: 24, - lineHeight: 0, -}); +export const menuItemIcon = style({}); export const menuItemText = style({ fontSize: 14, width: 0, @@ -17,3 +14,6 @@ export const menuItemText = style({ whiteSpace: 'nowrap', overflow: 'hidden', }); +export const menuTrigger = style({ + width: 250, +}); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.tsx new file mode 100644 index 000000000..a72115458 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/new-workspace-setting-detail/template.tsx @@ -0,0 +1,129 @@ +import { + MenuItem, + MenuSeparator, + MenuTrigger, + Switch, +} from '@affine/component'; +import { + SettingRow, + SettingWrapper, +} from '@affine/component/setting-components'; +import { 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, + TemplateListMenu, +} from '@affine/core/modules/template-doc'; +import { useI18n } from '@affine/i18n'; +import { DeleteIcon } from '@blocksuite/icons/rc'; +import { useLiveData, useService, useServices } from '@toeverything/infra'; +import { useCallback } from 'react'; + +import * as styles from './template.css'; + +export const TemplateDocSetting = () => { + const t = useI18n(); + const { featureFlagService, templateDocService } = useServices({ + FeatureFlagService, + TemplateDocService, + }); + const setting = templateDocService.setting; + + const enabled = useLiveData(featureFlagService.flags.enable_template_doc.$); + + const enablePageTemplate = useLiveData(setting.enablePageTemplate$); + const pageTemplateDocId = useLiveData(setting.pageTemplateDocId$); + const journalTemplateDocId = useLiveData(setting.journalTemplateDocId$); + + const togglePageTemplate = useCallback( + (enable: boolean) => { + setting.togglePageTemplate(enable); + }, + [setting] + ); + + const updatePageTemplate = useCallback( + (id?: string) => { + setting.updatePageTemplateDocId(id); + }, + [setting] + ); + + const updateJournalTemplate = useCallback( + (id?: string) => { + setting.updateJournalTemplateDocId(id); + }, + [setting] + ); + + if (!enabled) return null; + + return ( + + + + + + + + {enablePageTemplate ? ( + + + + ) : null} + + ); +}; + +interface TemplateSelectorProps { + current?: string; + onChange?: (id?: string) => void; +} +const TemplateSelector = ({ current, onChange }: TemplateSelectorProps) => { + const t = useI18n(); + const docsService = useService(DocsService); + const docDisplayService = useService(DocDisplayMetaService); + const doc = useLiveData(current ? docsService.list.doc$(current) : null); + const title = useLiveData(doc ? docDisplayService.title$(doc.id) : null); + // const isInTrash = useLiveData(doc?.trash$); + + return ( + + + } + onClick={() => onChange?.()} + type="danger" + > + {t['com.affine.settings.workspace.template.remove']()} + + + } + > + + {/* TODO: in trash design */} + {title ?? t['com.affine.settings.workspace.template.keep-empty']()} + + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.tsx deleted file mode 100644 index de9450303..000000000 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/template.tsx +++ /dev/null @@ -1,110 +0,0 @@ -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 ( -
- Normal Page: - -
- Journal: - -
- ); -}; - -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 ( - }> - - - ); -}; - -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 ; - }); -}; - -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 ( - -
  • - - {title} -
  • -
    - ); -}; diff --git a/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.css.ts b/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.css.ts index f234e77fa..ed9a78dba 100644 --- a/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.css.ts +++ b/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.css.ts @@ -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, +}); diff --git a/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.tsx b/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.tsx index 5af5e7aae..0edb82a87 100644 --- a/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.tsx +++ b/packages/frontend/core/src/modules/app-sidebar/views/add-page-button/index.tsx @@ -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' ? ( + + ) : ( + + ); +} + +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 ( + + } + onClick={createPage} + onAuxClick={createPage} + > + {t['Page']()} + + } + onClick={createEdgeless} + onAuxClick={createEdgeless} + > + {t['Edgeless']()} + + , + }} + subContentOptions={{ + sideOffset: 16, + className: styles.templateMenu, + }} + items={ + + } + > + {t['Template']()} + + + } + > + + + ); +} + +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(); diff --git a/packages/frontend/core/src/modules/editor-setting/schema.ts b/packages/frontend/core/src/modules/editor-setting/schema.ts index 96b80a137..4d0955277 100644 --- a/packages/frontend/core/src/modules/editor-setting/schema.ts +++ b/packages/frontend/core/src/modules/editor-setting/schema.ts @@ -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), diff --git a/packages/frontend/core/src/modules/editor-setting/services/editor-setting.ts b/packages/frontend/core/src/modules/editor-setting/services/editor-setting.ts index 3dd33c2e4..a0ad7b0b0 100644 --- a/packages/frontend/core/src/modules/editor-setting/services/editor-setting.ts +++ b/packages/frontend/core/src/modules/editor-setting/services/editor-setting.ts @@ -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 } diff --git a/packages/frontend/core/src/modules/journal/index.ts b/packages/frontend/core/src/modules/journal/index.ts index 3f581038b..968a3abac 100644 --- a/packages/frontend/core/src/modules/journal/index.ts +++ b/packages/frontend/core/src/modules/journal/index.ts @@ -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]); diff --git a/packages/frontend/core/src/modules/journal/services/journal.ts b/packages/frontend/core/src/modules/journal/services/journal.ts index 6a21c7040..e96feb570 100644 --- a/packages/frontend/core/src/modules/journal/services/journal.ts +++ b/packages/frontend/core/src/modules/journal/services/journal.ts @@ -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; } diff --git a/packages/frontend/core/src/modules/template-doc/entities/setting.ts b/packages/frontend/core/src/modules/template-doc/entities/setting.ts index 7fbcb0ea1..368b388fb 100644 --- a/packages/frontend/core/src/modules/template-doc/entities/setting.ts +++ b/packages/frontend/core/src/modules/template-doc/entities/setting.ts @@ -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); } diff --git a/packages/frontend/core/src/modules/template-doc/store/setting.ts b/packages/frontend/core/src/modules/template-doc/store/setting.ts index b56f2149f..f220fb937 100644 --- a/packages/frontend/core/src/modules/template-doc/store/setting.ts +++ b/packages/frontend/core/src/modules/template-doc/store/setting.ts @@ -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 }, }); } diff --git a/packages/frontend/core/src/modules/template-doc/type.ts b/packages/frontend/core/src/modules/template-doc/type.ts index d7a288a25..c72abff98 100644 --- a/packages/frontend/core/src/modules/template-doc/type.ts +++ b/packages/frontend/core/src/modules/template-doc/type.ts @@ -1,4 +1,5 @@ export type TemplateDocSettings = { + enablePageTemplate?: boolean; pageTemplateId?: string; journalTemplateId?: string; }; diff --git a/packages/frontend/core/src/modules/template-doc/view/styles.css.ts b/packages/frontend/core/src/modules/template-doc/view/styles.css.ts index cace8db5d..66b00a57f 100644 --- a/packages/frontend/core/src/modules/template-doc/view/styles.css.ts +++ b/packages/frontend/core/src/modules/template-doc/view/styles.css.ts @@ -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({ diff --git a/packages/frontend/core/src/modules/template-doc/view/template-list-menu.tsx b/packages/frontend/core/src/modules/template-doc/view/template-list-menu.tsx index 8c7f3c108..9645c2b67 100644 --- a/packages/frontend/core/src/modules/template-doc/view/template-list-menu.tsx +++ b/packages/frontend/core/src/modules/template-doc/view/template-list-menu.tsx @@ -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 ( - -
  • - - {title} -
  • + } onClick={onClick}> + {title} ); }; -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 (
      + {prefixItems} {templateDocs.map(doc => ( - + ))} + {suffixItems}
    ); }; -export const TemplateListMenuContentScrollable = ({ target }: CommonProps) => { +export const TemplateListMenuContentScrollable = ( + props: TemplateListMenuContentProps +) => { return ( - + ); }; +interface TemplateListMenuProps + extends TemplateListMenuContentProps, + Omit {} export const TemplateListMenu = ({ children, - target, + onSelect, + prefixItems, + suffixItems, contentOptions, ...otherProps -}: PropsWithChildren & Omit) => { +}: TemplateListMenuProps) => { return ( } + items={ + + } contentOptions={{ ...contentOptions, className: styles.menuContent, diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 0276acd40..5e6efd3e1 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1628,5 +1628,14 @@ "com.affine.page-starter-bar.start": "Start", "com.affine.page-starter-bar.template": "Template", "com.affine.page-starter-bar.ai": "With AI", - "com.affine.page-starter-bar.edgeless": "Edgeless" + "com.affine.page-starter-bar.edgeless": "Edgeless", + "Template": "Template", + "com.affine.settings.workspace.template.title": "My Templates", + "com.affine.settings.workspace.template.journal": "Template for journal", + "com.affine.settings.workspace.template.journal-desc": "Select a template for your journal", + "com.affine.settings.workspace.template.keep-empty": "Keep empty", + "com.affine.settings.workspace.template.page": "New doc with template", + "com.affine.settings.workspace.template.page-desc": "New docs will use the specified template, ignoring default settings.", + "com.affine.settings.workspace.template.page-select": "Template for new doc", + "com.affine.settings.workspace.template.remove": "Remove template" }