Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): new template doc property #9538

Open
wants to merge 6 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -334,6 +335,7 @@ export const BlocksuiteDocEditor = forwardRef<
data-testid="page-editor-blank"
onClick={onClickBlank}
></div>
<StarterBar doc={page} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May not render this if there is no template docs in the workspace

{!shared && displayBiDirectionalLink ? (
<BiDirectionalLinkPanel />
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -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({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import {
TemplateDocService,
TemplateListMenu,
} from '@affine/core/modules/template-doc';
import { useI18n } from '@affine/i18n';
import type { Blocks } 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: Blocks }) => {
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: Blocks }) => {
const [isEmpty, setIsEmpty] = useState(doc.isEmpty);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this will never be empty, because doc has at least one note

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty judgment logic has been refactored in #9570


useEffect(() => {
const disposable = doc.slots.blockUpdated.on(() => {
const empty = doc.isEmpty;
setIsEmpty(empty);
// once the doc is not empty, stop checking
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if user delete all the content?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is acceptable to display the Starter only when it is empty for the first time; otherwise, any edits need to check whether the document is empty.

if (!empty) disposable.dispose();
});
return () => {
disposable.dispose();
};
}, [doc]);

if (!isEmpty) return null;

return <StarterBarNotEmpty doc={doc} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -203,6 +208,9 @@ export const DocPropertyRow = ({
);

if (!ValueRenderer || typeof ValueRenderer !== 'function') return null;
if (propertyInfo.id === 'template' && !enableTemplateDoc) {
return null;
}

return (
<PropertyRoot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
LongerIcon,
NumberIcon,
TagIcon,
TemplateOutlineIcon,
TextIcon,
TodayIcon,
} from '@blocksuite/icons/rc';
Expand All @@ -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';

Expand Down Expand Up @@ -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,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ const feedbackLink: Record<NonNullable<Flag['feedbackType']>, string> = {
github: 'https://github.com/toeverything/AFFiNE/issues',
};

const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => {
const ExperimentalFeaturesItem = ({
flag,
name,
}: {
flag: Flag;
name: string;
}) => {
const value = useLiveData(flag.$);
const t = useI18n();
const onChange = useCallback(
Expand All @@ -125,10 +131,17 @@ const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => {
}

return (
<div className={styles.rowContainer}>
<div
className={styles.rowContainer}
data-testid={`experimental-feature-${name}`}
>
<div className={styles.switchRow}>
{t[flag.displayName]()}
<Switch checked={value} onChange={onChange} />
<Switch
data-testid="feature-switch"
checked={value}
onChange={onChange}
/>
</div>
{!!flag.description && (
<Tooltip content={t[flag.description]()}>
Expand Down Expand Up @@ -175,6 +188,7 @@ const ExperimentalFeaturesMain = () => {
{Object.keys(AFFINE_FLAGS).map(key => (
<ExperimentalFeaturesItem
key={key}
name={key}
flag={featureFlagService.flags[key as keyof AFFINE_FLAGS]}
/>
))}
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/core/src/modules/db/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
edgelessColorTheme: f.string().optional(),
journal: f.string().optional(),
pageWidth: f.string().optional(),
isTemplate: f.boolean().optional(),
}),
docCustomPropertyInfo: {
id: f.string().primaryKey().optional().default(nanoid),
Expand Down
Loading
Loading