mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): new template doc property (#9538)
close AF-2045, AF-2047, AF-2065
This commit is contained in:
@@ -68,6 +68,7 @@ import {
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createEdgelessModeSpecs } from './specs/edgeless';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
import { StarterBar } from './starter-bar';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const adapted = {
|
||||
@@ -334,6 +335,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
data-testid="page-editor-blank"
|
||||
onClick={onClickBlank}
|
||||
></div>
|
||||
<StarterBar doc={page} />
|
||||
{!shared && displayBiDirectionalLink ? (
|
||||
<BiDirectionalLinkPanel />
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
import { container } from './bi-directional-link-panel.css';
|
||||
|
||||
export const root = style([
|
||||
container,
|
||||
{
|
||||
paddingBottom: 6,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
color: cssVarV2.text.primary,
|
||||
},
|
||||
]);
|
||||
|
||||
export const badges = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const badge = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 40,
|
||||
backgroundColor: cssVarV2.layer.background.secondary,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
|
||||
':before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0,0,0,.04)',
|
||||
borderRadius: 'inherit',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&:hover:before': {
|
||||
opacity: 1,
|
||||
},
|
||||
'&[data-active="true"]:before': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const badgeIcon = style({
|
||||
fontSize: 16,
|
||||
lineHeight: 0,
|
||||
});
|
||||
|
||||
export const badgeText = style({});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
TemplateDocService,
|
||||
TemplateListMenu,
|
||||
} from '@affine/core/modules/template-doc';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
AiIcon,
|
||||
EdgelessIcon,
|
||||
TemplateColoredIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './starter-bar.css';
|
||||
|
||||
const Badge = forwardRef<
|
||||
HTMLLIElement,
|
||||
HTMLAttributes<HTMLLIElement> & {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
active?: boolean;
|
||||
}
|
||||
>(function Badge({ icon, text, className, active, ...attrs }, ref) {
|
||||
return (
|
||||
<li
|
||||
data-active={active}
|
||||
className={clsx(styles.badge, className)}
|
||||
ref={ref}
|
||||
{...attrs}
|
||||
>
|
||||
<span className={styles.badgeText}>{text}</span>
|
||||
<span className={styles.badgeIcon}>{icon}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
|
||||
const t = useI18n();
|
||||
|
||||
const templateDocService = useService(TemplateDocService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
|
||||
const [templateMenuOpen, setTemplateMenuOpen] = useState(false);
|
||||
|
||||
const isTemplate = useLiveData(
|
||||
useMemo(
|
||||
() => templateDocService.list.isTemplate$(doc.id),
|
||||
[doc.id, templateDocService.list]
|
||||
)
|
||||
);
|
||||
const enableTemplateDoc = useLiveData(
|
||||
featureFlagService.flags.enable_template_doc.$
|
||||
);
|
||||
|
||||
const showAI = false;
|
||||
const showEdgeless = false;
|
||||
const showTemplate = !isTemplate && enableTemplateDoc;
|
||||
|
||||
if (!showAI && !showEdgeless && !showTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{t['com.affine.page-starter-bar.start']()}
|
||||
<ul className={styles.badges}>
|
||||
{showAI ? (
|
||||
<Badge
|
||||
icon={<AiIcon />}
|
||||
text={t['com.affine.page-starter-bar.ai']()}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showTemplate ? (
|
||||
<TemplateListMenu
|
||||
target={doc.id}
|
||||
rootOptions={{
|
||||
open: templateMenuOpen,
|
||||
onOpenChange: setTemplateMenuOpen,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
data-testid="template-docs-badge"
|
||||
icon={<TemplateColoredIcon />}
|
||||
text={t['com.affine.page-starter-bar.template']()}
|
||||
active={templateMenuOpen}
|
||||
/>
|
||||
</TemplateListMenu>
|
||||
) : null}
|
||||
|
||||
{showEdgeless ? (
|
||||
<Badge
|
||||
icon={<EdgelessIcon />}
|
||||
text={t['com.affine.page-starter-bar.edgeless']()}
|
||||
/>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StarterBar = ({ doc }: { doc: Store }) => {
|
||||
const [isEmpty, setIsEmpty] = useState(doc.isEmpty);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = doc.slots.blockUpdated.on(() => {
|
||||
setIsEmpty(doc.isEmpty);
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [doc]);
|
||||
|
||||
if (!isEmpty) return null;
|
||||
|
||||
return <StarterBarNotEmpty doc={doc} />;
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
DatabaseRow,
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -126,9 +127,13 @@ export const DocPropertyRow = ({
|
||||
const t = useI18n();
|
||||
const docService = useService(DocService);
|
||||
const docsService = useService(DocsService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const customPropertyValue = useLiveData(
|
||||
docService.doc.customProperty$(propertyInfo.id)
|
||||
);
|
||||
const enableTemplateDoc = useLiveData(
|
||||
featureFlagService.flags.enable_template_doc.$
|
||||
);
|
||||
const typeInfo = isSupportedDocPropertyType(propertyInfo.type)
|
||||
? DocPropertyTypes[propertyInfo.type]
|
||||
: undefined;
|
||||
@@ -203,6 +208,9 @@ export const DocPropertyRow = ({
|
||||
);
|
||||
|
||||
if (!ValueRenderer || typeof ValueRenderer !== 'function') return null;
|
||||
if (propertyInfo.id === 'template' && !enableTemplateDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PropertyRoot
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
LongerIcon,
|
||||
NumberIcon,
|
||||
TagIcon,
|
||||
TemplateOutlineIcon,
|
||||
TextIcon,
|
||||
TodayIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
@@ -22,6 +23,7 @@ import { JournalValue } from './journal';
|
||||
import { NumberValue } from './number';
|
||||
import { PageWidthValue } from './page-width';
|
||||
import { TagsValue } from './tags';
|
||||
import { TemplateValue } from './template';
|
||||
import { TextValue } from './text';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
@@ -108,6 +110,14 @@ export const DocPropertyTypes = {
|
||||
name: 'com.affine.page-properties.property.pageWidth',
|
||||
description: 'com.affine.page-properties.property.pageWidth.tooltips',
|
||||
},
|
||||
template: {
|
||||
uniqueId: 'template',
|
||||
icon: TemplateOutlineIcon,
|
||||
value: TemplateValue,
|
||||
name: 'com.affine.page-properties.property.template',
|
||||
renameable: false,
|
||||
description: 'com.affine.page-properties.property.template.tooltips',
|
||||
},
|
||||
} as Record<
|
||||
string,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const property = style({
|
||||
padding: '3px 4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const checkbox = style({
|
||||
fontSize: 24,
|
||||
color: cssVarV2.icon.primary,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Checkbox, PropertyValue } from '@affine/component';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type ChangeEvent, useCallback } from 'react';
|
||||
|
||||
import * as styles from './template.css';
|
||||
|
||||
export const TemplateValue = () => {
|
||||
const docService = useService(DocService);
|
||||
|
||||
const isTemplate = useLiveData(
|
||||
docService.doc.record.properties$.selector(p => p.isTemplate)
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.checked;
|
||||
docService.doc.record.setProperty('isTemplate', value);
|
||||
},
|
||||
[docService.doc.record]
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
docService.doc.record.setProperty('isTemplate', !isTemplate);
|
||||
}, [docService.doc.record, isTemplate]);
|
||||
|
||||
return (
|
||||
<PropertyValue className={styles.property} onClick={toggle}>
|
||||
<Checkbox
|
||||
data-testid="toggle-template-checkbox"
|
||||
checked={!!isTemplate}
|
||||
onChange={onChange}
|
||||
className={styles.checkbox}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user