feat(core): adopt editor features for journal (#5638)

This commit is contained in:
Peng Xiao
2024-01-22 08:25:31 +00:00
parent f41b7d7e71
commit 0ed26f51af
18 changed files with 362 additions and 134 deletions

View File

@@ -1,7 +1,9 @@
import { EditorLoading } from '@affine/component/page-detail-skeleton';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useJournalHelper } from '@affine/core/hooks/use-journal';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { DateTimeIcon, PageIcon } from '@blocksuite/icons';
import { LinkedPageIcon, TodayIcon } from '@blocksuite/icons';
import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Page } from '@blocksuite/store';
import { use } from 'foxact/use';
@@ -12,12 +14,14 @@ import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { type Map as YMap } from 'yjs';
import { BlocksuiteEditorContainer } from './blocksuite-editor-container';
import type { InlineRenderers } from './specs';
import * as styles from './styles.css';
export type ErrorBoundaryProps = {
onReset?: () => void;
@@ -67,54 +71,55 @@ function usePageRoot(page: Page) {
return page.root;
}
// TODO: this is a placeholder proof-of-concept implementation
function CustomPageReference({
reference,
}: {
interface PageReferenceProps {
reference: HTMLElementTagNameMap['affine-reference'];
}) {
const workspace = reference.page.workspace;
const meta = usePageMetaHelper(workspace);
pageMetaHelper: ReturnType<typeof usePageMetaHelper>;
journalHelper: ReturnType<typeof useJournalHelper>;
t: ReturnType<typeof useAFFiNEI18N>;
}
// TODO: this is a placeholder proof-of-concept implementation
function customPageReference({
reference,
pageMetaHelper,
journalHelper,
t,
}: PageReferenceProps) {
const { isPageJournal, getLocalizedJournalDateString } = journalHelper;
assertExists(
reference.delta.attributes?.reference?.pageId,
'pageId should exist for page reference'
);
const referencedPage = meta.getPageMeta(
reference.delta.attributes.reference.pageId
);
const title = referencedPage?.title ?? 'not found';
let icon = <PageIcon />;
let lTitle = title.toLowerCase();
if (title.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)) {
lTitle = new Date(title).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
icon = <DateTimeIcon />;
const pageId = reference.delta.attributes.reference.pageId;
const referencedPage = pageMetaHelper.getPageMeta(pageId);
let title =
referencedPage?.title ?? t['com.affine.editor.reference-not-found']();
let icon = <LinkedPageIcon className={styles.pageReferenceIcon} />;
const isJournal = isPageJournal(pageId);
const localizedJournalDate = getLocalizedJournalDateString(pageId);
if (isJournal && localizedJournalDate) {
title = localizedJournalDate;
icon = <TodayIcon className={styles.pageReferenceIcon} />;
}
return (
<a
target="_blank"
rel="noopener noreferrer"
className="page-reference"
style={{
display: 'inline-flex',
alignItems: 'center',
padding: '0 0.25em',
columnGap: '0.25em',
}}
>
{icon} <span className="affine-reference-title">{lTitle}</span>
</a>
<>
{icon}
<span className="affine-reference-title">{title}</span>
</>
);
}
const customRenderers: InlineRenderers = {
// we cannot pass components to lit renderers, but give them the rendered elements
const customRenderersFactory: (
opts: Omit<PageReferenceProps, 'reference'>
) => InlineRenderers = opts => ({
pageReference(reference) {
return <CustomPageReference reference={reference} />;
return customPageReference({
...opts,
reference,
});
},
};
});
/**
* TODO: Define error to unexpected state together in the future.
@@ -177,6 +182,18 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
};
}, []);
const pageMetaHelper = usePageMetaHelper(page.workspace);
const journalHelper = useJournalHelper(page.workspace);
const t = useAFFiNEI18N();
const customRenderers = useMemo(() => {
return customRenderersFactory({
pageMetaHelper,
journalHelper,
t,
});
}, [journalHelper, pageMetaHelper, t]);
return (
<BlocksuiteEditorContainer
mode={mode}

View File

@@ -0,0 +1,22 @@
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Page } from '@blocksuite/store';
import * as styles from './styles.css';
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Page }) => {
const { localizedJournalDate, isTodayJournal } = useJournalInfoHelper(
page.workspace,
page.id
);
const t = useAFFiNEI18N();
return (
<span className="doc-title-container">
<span>{localizedJournalDate}</span>
{isTodayJournal ? (
<span className={styles.titleTodayTag}>{t['com.affine.today']()}</span>
) : null}
</span>
);
};

View File

@@ -1,4 +1,5 @@
import { createReactComponentFromLit } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import {
BiDirectionalLinkPanel,
DocEditor,
@@ -8,8 +9,15 @@ import {
} from '@blocksuite/presets';
import { type Page } from '@blocksuite/store';
import clsx from 'clsx';
import React, { forwardRef, useEffect, useMemo, useRef } from 'react';
import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
import {
docModeSpecs,
edgelessModeSpecs,
@@ -52,6 +60,22 @@ export const BlocksuiteDocEditor = forwardRef<
BlocksuiteDocEditorProps
>(function BlocksuiteDocEditor({ page, customRenderers }, ref) {
const titleRef = useRef<DocTitle>(null);
const docRef = useRef<DocEditor | null>(null);
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
const onDocRef = useCallback(
(el: DocEditor) => {
docRef.current = el;
if (ref) {
if (typeof ref === 'function') {
ref(el);
} else {
ref.current = el;
}
}
},
[ref]
);
const specs = useMemo(() => {
return patchSpecs(docModeSpecs, customRenderers);
@@ -63,6 +87,8 @@ export const BlocksuiteDocEditor = forwardRef<
if (titleRef.current) {
const richText = titleRef.current.querySelector('rich-text');
richText?.inlineEditor?.focusEnd();
} else {
docRef.current?.querySelector('affine-doc-page')?.focusFirstParagraph();
}
});
}, []);
@@ -70,12 +96,16 @@ export const BlocksuiteDocEditor = forwardRef<
return (
<div className={styles.docEditorRoot}>
<div className={clsx('affine-doc-viewport', styles.affineDocViewport)}>
<adapted.DocTitle page={page} ref={titleRef} />
{!isJournal ? (
<adapted.DocTitle page={page} ref={titleRef} />
) : (
<BlocksuiteEditorJournalDocTitle page={page} />
)}
{/* We will replace page meta tags with our own implementation */}
<adapted.PageMetaTags page={page} />
<adapted.DocEditor
className={styles.docContainer}
ref={ref}
ref={onDocRef}
page={page}
specs={specs}
hasViewport={false}

View File

@@ -30,3 +30,18 @@ export const docContainer = style({
display: 'block',
flexGrow: 1,
});
export const titleTodayTag = style({
fontSize: 'var(--affine-font-base)',
fontWeight: 400,
color: 'var(--affine-brand-color)',
padding: '0 4px',
borderRadius: '4px',
marginLeft: '4px',
});
export const pageReferenceIcon = style({
verticalAlign: 'middle',
fontSize: '1.1em',
transform: 'translate(2px, -1px)',
});

View File

@@ -1,7 +1,7 @@
import { WeekDatePicker, type WeekDatePickerHandle } from '@affine/component';
import {
useJournalHelper,
useJournalInfoHelper,
useJournalRouteHelper,
} from '@affine/core/hooks/use-journal';
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import type { Page } from '@blocksuite/store';
@@ -20,7 +20,7 @@ export const JournalWeekDatePicker = ({
}: JournalWeekDatePickerProps) => {
const handleRef = useRef<WeekDatePickerHandle>(null);
const { journalDate } = useJournalInfoHelper(workspace, page.id);
const { openJournal } = useJournalHelper(workspace);
const { openJournal } = useJournalRouteHelper(workspace);
const [date, setDate] = useState(
(journalDate ?? dayjs()).format('YYYY-MM-DD')
);

View File

@@ -1,5 +1,5 @@
import { Button } from '@affine/component';
import { useJournalHelper } from '@affine/core/hooks/use-journal';
import { useJournalRouteHelper } from '@affine/core/hooks/use-journal';
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback } from 'react';
@@ -10,7 +10,7 @@ export interface JournalTodayButtonProps {
export const JournalTodayButton = ({ workspace }: JournalTodayButtonProps) => {
const t = useAFFiNEI18N();
const journalHelper = useJournalHelper(workspace);
const journalHelper = useJournalRouteHelper(workspace);
const onToday = useCallback(() => {
journalHelper.openToday();

View File

@@ -1,7 +1,14 @@
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import type { Tag } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon, ToggleCollapseIcon } from '@blocksuite/icons';
import {
EdgelessIcon,
PageIcon,
TodayIcon,
ToggleCollapseIcon,
} from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import clsx from 'clsx';
@@ -212,6 +219,28 @@ function tagIdToTagOption(
);
}
const PageTitle = ({ id, workspace }: { id: string; workspace: Workspace }) => {
const title = useBlockSuiteWorkspacePageTitle(workspace, id);
return title;
};
const UnifiedPageIcon = ({
id,
workspace,
isPreferredEdgeless,
}: {
id: string;
workspace: Workspace;
isPreferredEdgeless: (id: string) => boolean;
}) => {
const isEdgeless = isPreferredEdgeless(id);
const { isJournal } = useJournalInfoHelper(workspace, id);
if (isJournal) {
return <TodayIcon />;
}
return isEdgeless ? <EdgelessIcon /> : <PageIcon />;
};
function pageMetaToPageItemProp(
pageMeta: PageMeta,
props: RequiredProps
@@ -237,7 +266,7 @@ function pageMetaToPageItemProp(
: undefined;
const itemProps: PageListItemProps = {
pageId: pageMeta.id,
title: pageMeta.title,
title: <PageTitle id={pageMeta.id} workspace={props.blockSuiteWorkspace} />,
preview: (
<PagePreview workspace={props.blockSuiteWorkspace} pageId={pageMeta.id} />
),
@@ -250,10 +279,12 @@ function pageMetaToPageItemProp(
? `/workspace/${props.blockSuiteWorkspace.id}/${pageMeta.id}`
: undefined,
onClick: props.selectable ? toggleSelection : undefined,
icon: props.isPreferredEdgeless?.(pageMeta.id) ? (
<EdgelessIcon />
) : (
<PageIcon />
icon: (
<UnifiedPageIcon
id={pageMeta.id}
workspace={props.blockSuiteWorkspace}
isPreferredEdgeless={props.isPreferredEdgeless}
/>
),
tags:
pageMeta.tags

View File

@@ -1,8 +1,8 @@
import { MenuItem } from '@affine/component/app-sidebar';
import { currentPageIdAtom } from '@affine/core/atoms/mode';
import {
useJournalHelper,
useJournalInfoHelper,
useJournalRouteHelper,
} from '@affine/core/hooks/use-journal';
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -19,7 +19,7 @@ export const AppSidebarJournalButton = ({
}: AppSidebarJournalButtonProps) => {
const t = useAFFiNEI18N();
const currentPageId = useAtomValue(currentPageIdAtom);
const { openToday } = useJournalHelper(workspace);
const { openToday } = useJournalRouteHelper(workspace);
const { journalDate, isJournal } = useJournalInfoHelper(
workspace,
currentPageId