feat(core): template doc setting ui (#9658)

close AF-2051, AF-2052
This commit is contained in:
CatsJuice
2025-01-14 02:10:34 +00:00
parent 57b89b5ad4
commit af3ad4bfe9
18 changed files with 481 additions and 206 deletions
@@ -2,6 +2,7 @@ import {
Loading,
Menu,
MenuItem,
type MenuProps,
MenuSeparator,
MenuTrigger,
RadioGroup,
@@ -19,6 +20,7 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
import { ServerService } from '@affine/core/modules/cloud';
import { DesktopApiService } from '@affine/core/modules/desktop-api';
import {
type EditorSettingSchema,
EditorSettingService,
type FontFamily,
fontStyleOptions,
@@ -30,7 +32,6 @@ import {
SystemFontFamilyService,
} from '@affine/core/modules/system-font-family';
import { Trans, useI18n } from '@affine/i18n';
import type { DocMode } from '@blocksuite/affine/blocks';
import { DoneIcon, SearchIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import clsx from 'clsx';
@@ -306,27 +307,39 @@ const CustomFontFamilySettings = () => {
</SettingRow>
);
};
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<RadioItem[]>(
() => [
{
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'
]()}
>
<RadioGroup
items={radioItems}
value={settings.newDocDefaultMode}
width={250}
className={styles.settingWrapper}
onChange={updateNewDocDefaultMode}
/>
<Menu
contentOptions={menuContentOptions}
items={items.map(item => {
return (
<MenuItem
key={item.value}
selected={item.value === settings.newDocDefaultMode}
onSelect={() => updateNewDocDefaultMode(item.value)}
>
{item.label}
</MenuItem>
);
})}
>
<MenuTrigger className={styles.menuTrigger}>
{items.find(item => item.value === settings.newDocDefaultMode)?.label}
</MenuTrigger>
</Menu>
</SettingRow>
);
};
@@ -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 = ({
<LabelsPanel />
</SettingRow>
</SettingWrapper>
<TemplateDocSetting />
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
<EnableCloudPanel onCloseSetting={onCloseSetting} />
<WorkspaceQuotaPanel />
@@ -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,
});
@@ -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 (
<SettingWrapper title={t['com.affine.settings.workspace.template.title']()}>
<SettingRow
name={t['com.affine.settings.workspace.template.journal']()}
desc={t['com.affine.settings.workspace.template.journal-desc']()}
>
<TemplateSelector
current={journalTemplateDocId}
onChange={updateJournalTemplate}
/>
</SettingRow>
<SettingRow
name={t['com.affine.settings.workspace.template.page']()}
desc={t['com.affine.settings.workspace.template.page-desc']()}
>
<Switch checked={enablePageTemplate} onChange={togglePageTemplate} />
</SettingRow>
{enablePageTemplate ? (
<SettingRow
name={t['com.affine.settings.workspace.template.journal']()}
desc={t['com.affine.settings.workspace.template.journal-desc']()}
>
<TemplateSelector
current={pageTemplateDocId}
onChange={updatePageTemplate}
/>
</SettingRow>
) : null}
</SettingWrapper>
);
};
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 (
<TemplateListMenu
onSelect={onChange}
contentOptions={{ align: 'end' }}
suffixItems={
<>
<MenuSeparator />
<MenuItem
prefixIcon={<DeleteIcon className={styles.menuItemIcon} />}
onClick={() => onChange?.()}
type="danger"
>
{t['com.affine.settings.workspace.template.remove']()}
</MenuItem>
</>
}
>
<MenuTrigger className={styles.menuTrigger}>
{/* TODO: in trash design */}
{title ?? t['com.affine.settings.workspace.template.keep-empty']()}
</MenuTrigger>
</TemplateListMenu>
);
};
@@ -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 (
<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>
);
};