mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat(core): adopt editor features for journal (#5638)
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||||
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
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 { 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 { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { use } from 'foxact/use';
|
import { use } from 'foxact/use';
|
||||||
@@ -12,12 +14,14 @@ import {
|
|||||||
Suspense,
|
Suspense,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { type Map as YMap } from 'yjs';
|
import { type Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { BlocksuiteEditorContainer } from './blocksuite-editor-container';
|
import { BlocksuiteEditorContainer } from './blocksuite-editor-container';
|
||||||
import type { InlineRenderers } from './specs';
|
import type { InlineRenderers } from './specs';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export type ErrorBoundaryProps = {
|
export type ErrorBoundaryProps = {
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
@@ -67,54 +71,55 @@ function usePageRoot(page: Page) {
|
|||||||
return page.root;
|
return page.root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a placeholder proof-of-concept implementation
|
interface PageReferenceProps {
|
||||||
function CustomPageReference({
|
|
||||||
reference,
|
|
||||||
}: {
|
|
||||||
reference: HTMLElementTagNameMap['affine-reference'];
|
reference: HTMLElementTagNameMap['affine-reference'];
|
||||||
}) {
|
pageMetaHelper: ReturnType<typeof usePageMetaHelper>;
|
||||||
const workspace = reference.page.workspace;
|
journalHelper: ReturnType<typeof useJournalHelper>;
|
||||||
const meta = usePageMetaHelper(workspace);
|
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(
|
assertExists(
|
||||||
reference.delta.attributes?.reference?.pageId,
|
reference.delta.attributes?.reference?.pageId,
|
||||||
'pageId should exist for page reference'
|
'pageId should exist for page reference'
|
||||||
);
|
);
|
||||||
const referencedPage = meta.getPageMeta(
|
const pageId = reference.delta.attributes.reference.pageId;
|
||||||
reference.delta.attributes.reference.pageId
|
const referencedPage = pageMetaHelper.getPageMeta(pageId);
|
||||||
);
|
let title =
|
||||||
const title = referencedPage?.title ?? 'not found';
|
referencedPage?.title ?? t['com.affine.editor.reference-not-found']();
|
||||||
let icon = <PageIcon />;
|
let icon = <LinkedPageIcon className={styles.pageReferenceIcon} />;
|
||||||
let lTitle = title.toLowerCase();
|
const isJournal = isPageJournal(pageId);
|
||||||
if (title.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)) {
|
const localizedJournalDate = getLocalizedJournalDateString(pageId);
|
||||||
lTitle = new Date(title).toLocaleDateString(undefined, {
|
if (isJournal && localizedJournalDate) {
|
||||||
year: 'numeric',
|
title = localizedJournalDate;
|
||||||
month: 'short',
|
icon = <TodayIcon className={styles.pageReferenceIcon} />;
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
icon = <DateTimeIcon />;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<>
|
||||||
target="_blank"
|
{icon}
|
||||||
rel="noopener noreferrer"
|
<span className="affine-reference-title">{title}</span>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
pageReference(reference) {
|
||||||
return <CustomPageReference reference={reference} />;
|
return customPageReference({
|
||||||
|
...opts,
|
||||||
|
reference,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Define error to unexpected state together in the future.
|
* 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 (
|
return (
|
||||||
<BlocksuiteEditorContainer
|
<BlocksuiteEditorContainer
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createReactComponentFromLit } from '@affine/component';
|
import { createReactComponentFromLit } from '@affine/component';
|
||||||
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
import {
|
import {
|
||||||
BiDirectionalLinkPanel,
|
BiDirectionalLinkPanel,
|
||||||
DocEditor,
|
DocEditor,
|
||||||
@@ -8,8 +9,15 @@ import {
|
|||||||
} from '@blocksuite/presets';
|
} from '@blocksuite/presets';
|
||||||
import { type Page } from '@blocksuite/store';
|
import { type Page } from '@blocksuite/store';
|
||||||
import clsx from 'clsx';
|
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 {
|
import {
|
||||||
docModeSpecs,
|
docModeSpecs,
|
||||||
edgelessModeSpecs,
|
edgelessModeSpecs,
|
||||||
@@ -52,6 +60,22 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
BlocksuiteDocEditorProps
|
BlocksuiteDocEditorProps
|
||||||
>(function BlocksuiteDocEditor({ page, customRenderers }, ref) {
|
>(function BlocksuiteDocEditor({ page, customRenderers }, ref) {
|
||||||
const titleRef = useRef<DocTitle>(null);
|
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(() => {
|
const specs = useMemo(() => {
|
||||||
return patchSpecs(docModeSpecs, customRenderers);
|
return patchSpecs(docModeSpecs, customRenderers);
|
||||||
@@ -63,6 +87,8 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
if (titleRef.current) {
|
if (titleRef.current) {
|
||||||
const richText = titleRef.current.querySelector('rich-text');
|
const richText = titleRef.current.querySelector('rich-text');
|
||||||
richText?.inlineEditor?.focusEnd();
|
richText?.inlineEditor?.focusEnd();
|
||||||
|
} else {
|
||||||
|
docRef.current?.querySelector('affine-doc-page')?.focusFirstParagraph();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -70,12 +96,16 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
return (
|
return (
|
||||||
<div className={styles.docEditorRoot}>
|
<div className={styles.docEditorRoot}>
|
||||||
<div className={clsx('affine-doc-viewport', styles.affineDocViewport)}>
|
<div className={clsx('affine-doc-viewport', styles.affineDocViewport)}>
|
||||||
|
{!isJournal ? (
|
||||||
<adapted.DocTitle page={page} ref={titleRef} />
|
<adapted.DocTitle page={page} ref={titleRef} />
|
||||||
|
) : (
|
||||||
|
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||||
|
)}
|
||||||
{/* We will replace page meta tags with our own implementation */}
|
{/* We will replace page meta tags with our own implementation */}
|
||||||
<adapted.PageMetaTags page={page} />
|
<adapted.PageMetaTags page={page} />
|
||||||
<adapted.DocEditor
|
<adapted.DocEditor
|
||||||
className={styles.docContainer}
|
className={styles.docContainer}
|
||||||
ref={ref}
|
ref={onDocRef}
|
||||||
page={page}
|
page={page}
|
||||||
specs={specs}
|
specs={specs}
|
||||||
hasViewport={false}
|
hasViewport={false}
|
||||||
|
|||||||
@@ -30,3 +30,18 @@ export const docContainer = style({
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
flexGrow: 1,
|
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)',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { WeekDatePicker, type WeekDatePickerHandle } from '@affine/component';
|
import { WeekDatePicker, type WeekDatePickerHandle } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
useJournalHelper,
|
|
||||||
useJournalInfoHelper,
|
useJournalInfoHelper,
|
||||||
|
useJournalRouteHelper,
|
||||||
} from '@affine/core/hooks/use-journal';
|
} from '@affine/core/hooks/use-journal';
|
||||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
@@ -20,7 +20,7 @@ export const JournalWeekDatePicker = ({
|
|||||||
}: JournalWeekDatePickerProps) => {
|
}: JournalWeekDatePickerProps) => {
|
||||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
const handleRef = useRef<WeekDatePickerHandle>(null);
|
||||||
const { journalDate } = useJournalInfoHelper(workspace, page.id);
|
const { journalDate } = useJournalInfoHelper(workspace, page.id);
|
||||||
const { openJournal } = useJournalHelper(workspace);
|
const { openJournal } = useJournalRouteHelper(workspace);
|
||||||
const [date, setDate] = useState(
|
const [date, setDate] = useState(
|
||||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from '@affine/component';
|
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 type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -10,7 +10,7 @@ export interface JournalTodayButtonProps {
|
|||||||
|
|
||||||
export const JournalTodayButton = ({ workspace }: JournalTodayButtonProps) => {
|
export const JournalTodayButton = ({ workspace }: JournalTodayButtonProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const journalHelper = useJournalHelper(workspace);
|
const journalHelper = useJournalRouteHelper(workspace);
|
||||||
|
|
||||||
const onToday = useCallback(() => {
|
const onToday = useCallback(() => {
|
||||||
journalHelper.openToday();
|
journalHelper.openToday();
|
||||||
|
|||||||
@@ -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 type { Tag } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
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 type { PageMeta, Workspace } from '@blocksuite/store';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import clsx from 'clsx';
|
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(
|
function pageMetaToPageItemProp(
|
||||||
pageMeta: PageMeta,
|
pageMeta: PageMeta,
|
||||||
props: RequiredProps
|
props: RequiredProps
|
||||||
@@ -237,7 +266,7 @@ function pageMetaToPageItemProp(
|
|||||||
: undefined;
|
: undefined;
|
||||||
const itemProps: PageListItemProps = {
|
const itemProps: PageListItemProps = {
|
||||||
pageId: pageMeta.id,
|
pageId: pageMeta.id,
|
||||||
title: pageMeta.title,
|
title: <PageTitle id={pageMeta.id} workspace={props.blockSuiteWorkspace} />,
|
||||||
preview: (
|
preview: (
|
||||||
<PagePreview workspace={props.blockSuiteWorkspace} pageId={pageMeta.id} />
|
<PagePreview workspace={props.blockSuiteWorkspace} pageId={pageMeta.id} />
|
||||||
),
|
),
|
||||||
@@ -250,10 +279,12 @@ function pageMetaToPageItemProp(
|
|||||||
? `/workspace/${props.blockSuiteWorkspace.id}/${pageMeta.id}`
|
? `/workspace/${props.blockSuiteWorkspace.id}/${pageMeta.id}`
|
||||||
: undefined,
|
: undefined,
|
||||||
onClick: props.selectable ? toggleSelection : undefined,
|
onClick: props.selectable ? toggleSelection : undefined,
|
||||||
icon: props.isPreferredEdgeless?.(pageMeta.id) ? (
|
icon: (
|
||||||
<EdgelessIcon />
|
<UnifiedPageIcon
|
||||||
) : (
|
id={pageMeta.id}
|
||||||
<PageIcon />
|
workspace={props.blockSuiteWorkspace}
|
||||||
|
isPreferredEdgeless={props.isPreferredEdgeless}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
tags:
|
tags:
|
||||||
pageMeta.tags
|
pageMeta.tags
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { MenuItem } from '@affine/component/app-sidebar';
|
import { MenuItem } from '@affine/component/app-sidebar';
|
||||||
import { currentPageIdAtom } from '@affine/core/atoms/mode';
|
import { currentPageIdAtom } from '@affine/core/atoms/mode';
|
||||||
import {
|
import {
|
||||||
useJournalHelper,
|
|
||||||
useJournalInfoHelper,
|
useJournalInfoHelper,
|
||||||
|
useJournalRouteHelper,
|
||||||
} from '@affine/core/hooks/use-journal';
|
} from '@affine/core/hooks/use-journal';
|
||||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -19,7 +19,7 @@ export const AppSidebarJournalButton = ({
|
|||||||
}: AppSidebarJournalButtonProps) => {
|
}: AppSidebarJournalButtonProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const { openToday } = useJournalHelper(workspace);
|
const { openToday } = useJournalRouteHelper(workspace);
|
||||||
const { journalDate, isJournal } = useJournalInfoHelper(
|
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||||
workspace,
|
workspace,
|
||||||
currentPageId
|
currentPageId
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import 'fake-indexeddb/auto';
|
|
||||||
|
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { Page } from '@blocksuite/store';
|
|
||||||
import { Schema, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { describe, expect, test, vi } from 'vitest';
|
|
||||||
import { beforeEach } from 'vitest';
|
|
||||||
|
|
||||||
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
|
||||||
|
|
||||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
|
|
||||||
const schema = new Schema();
|
|
||||||
schema.register(AffineSchemas).register(__unstableSchemas);
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
|
||||||
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
|
|
||||||
const initPage = async (page: Page) => {
|
|
||||||
await page.waitForLoaded();
|
|
||||||
expect(page).not.toBeNull();
|
|
||||||
assertExists(page);
|
|
||||||
const pageBlockId = page.addBlock('affine:page', {
|
|
||||||
title: new page.Text(''),
|
|
||||||
});
|
|
||||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
|
||||||
page.addBlock('affine:paragraph', {}, frameId);
|
|
||||||
};
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useBlockSuiteWorkspacePageTitle', () => {
|
|
||||||
test('basic', async () => {
|
|
||||||
const pageTitleHook = renderHook(() =>
|
|
||||||
useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0')
|
|
||||||
);
|
|
||||||
expect(pageTitleHook.result.current).toBe('Untitled');
|
|
||||||
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
|
|
||||||
pageTitleHook.rerender();
|
|
||||||
expect(pageTitleHook.result.current).toBe('1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
import {
|
||||||
|
currentWorkspaceAtom,
|
||||||
|
WorkspacePropertiesAdapter,
|
||||||
|
} from '@affine/core/modules/workspace';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import type { Workspace } from '@affine/workspace/workspace';
|
||||||
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import { type Page, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
|
import { Schema } from '@blocksuite/store';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { createStore, Provider } from 'jotai';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
import { beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
||||||
|
|
||||||
|
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
|
const store = createStore();
|
||||||
|
|
||||||
|
const schema = new Schema();
|
||||||
|
schema.register(AffineSchemas).register(__unstableSchemas);
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0');
|
||||||
|
return <div>title: {title}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo: this module has some side-effects that will break the tests
|
||||||
|
vi.mock('@affine/workspace-impl', () => ({
|
||||||
|
default: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
||||||
|
|
||||||
|
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
|
||||||
|
|
||||||
|
const workspace = {
|
||||||
|
blockSuiteWorkspace,
|
||||||
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
|
} as Workspace;
|
||||||
|
|
||||||
|
store.set(currentWorkspaceAtom, workspace);
|
||||||
|
|
||||||
|
blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
|
|
||||||
|
const initPage = async (page: Page) => {
|
||||||
|
await page.waitForLoaded();
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
assertExists(page);
|
||||||
|
const pageBlockId = page.addBlock('affine:page', {
|
||||||
|
title: new page.Text(''),
|
||||||
|
});
|
||||||
|
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||||
|
page.addBlock('affine:paragraph', {}, frameId);
|
||||||
|
};
|
||||||
|
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
|
||||||
|
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
|
||||||
|
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useBlockSuiteWorkspacePageTitle', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
const { findByText, rerender } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
expect(await findByText('title: Untitled')).toBeDefined();
|
||||||
|
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
|
||||||
|
rerender(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
expect(await findByText('title: 1')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('journal', async () => {
|
||||||
|
const adapter = new WorkspacePropertiesAdapter(blockSuiteWorkspace);
|
||||||
|
adapter.setJournalPageDateString('page0', '2021-01-01');
|
||||||
|
const { findByText } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
expect(await findByText('title: Jan 1, 2021')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useReducer } from 'react';
|
|||||||
import {
|
import {
|
||||||
currentWorkspacePropertiesAdapterAtom,
|
currentWorkspacePropertiesAdapterAtom,
|
||||||
WorkspacePropertiesAdapter,
|
WorkspacePropertiesAdapter,
|
||||||
} from '../modules/workspace';
|
} from '../modules/workspace/properties';
|
||||||
|
|
||||||
const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
|
const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
|
||||||
const [, forceUpdate] = useReducer(c => c + 1, 0);
|
const [, forceUpdate] = useReducer(c => c + 1, 0);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { Workspace } from '@blocksuite/store';
|
|||||||
import type { Atom } from 'jotai';
|
import type { Atom } from 'jotai';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
|
|
||||||
|
import { useJournalInfoHelper } from './use-journal';
|
||||||
|
|
||||||
const weakMap = new WeakMap<Workspace, Map<string, Atom<string>>>();
|
const weakMap = new WeakMap<Workspace, Map<string, Atom<string>>>();
|
||||||
|
|
||||||
function getAtom(w: Workspace, pageId: string): Atom<string> {
|
function getAtom(w: Workspace, pageId: string): Atom<string> {
|
||||||
@@ -35,5 +37,10 @@ export function useBlockSuiteWorkspacePageTitle(
|
|||||||
) {
|
) {
|
||||||
const titleAtom = getAtom(blockSuiteWorkspace, pageId);
|
const titleAtom = getAtom(blockSuiteWorkspace, pageId);
|
||||||
assertExists(titleAtom);
|
assertExists(titleAtom);
|
||||||
return useAtomValue(titleAtom);
|
const title = useAtomValue(titleAtom);
|
||||||
|
const { localizedJournalDate } = useJournalInfoHelper(
|
||||||
|
blockSuiteWorkspace,
|
||||||
|
pageId
|
||||||
|
);
|
||||||
|
return localizedJournalDate || title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ function toDayjs(j?: string | false) {
|
|||||||
|
|
||||||
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
||||||
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
||||||
const navigateHelper = useNavigateHelper();
|
|
||||||
|
|
||||||
const adapter = useWorkspacePropertiesAdapter(workspace);
|
const adapter = useWorkspacePropertiesAdapter(workspace);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,24 +80,14 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
[_createJournal, getJournalsByDate]
|
[_createJournal, getJournalsByDate]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
const isPageTodayJournal = useCallback(
|
||||||
* open journal by date, create one if not exist
|
(pageId: string) => {
|
||||||
*/
|
|
||||||
const openJournal = useCallback(
|
|
||||||
(maybeDate: MaybeDate) => {
|
|
||||||
const page = getJournalByDate(maybeDate);
|
|
||||||
navigateHelper.openPage(workspace.id, page.id);
|
|
||||||
},
|
|
||||||
[getJournalByDate, navigateHelper, workspace.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* open today's journal
|
|
||||||
*/
|
|
||||||
const openToday = useCallback(() => {
|
|
||||||
const date = dayjs().format(JOURNAL_DATE_FORMAT);
|
const date = dayjs().format(JOURNAL_DATE_FORMAT);
|
||||||
openJournal(date);
|
const d = adapter.getJournalPageDateString(pageId);
|
||||||
}, [openJournal]);
|
return isPageJournal(pageId) && d === date;
|
||||||
|
},
|
||||||
|
[adapter, isPageJournal]
|
||||||
|
);
|
||||||
|
|
||||||
const getJournalDateString = useCallback(
|
const getJournalDateString = useCallback(
|
||||||
(pageId: string) => {
|
(pageId: string) => {
|
||||||
@@ -123,9 +111,8 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
getJournalByDate,
|
getJournalByDate,
|
||||||
getJournalDateString,
|
getJournalDateString,
|
||||||
getLocalizedJournalDateString,
|
getLocalizedJournalDateString,
|
||||||
openJournal,
|
|
||||||
openToday,
|
|
||||||
isPageJournal,
|
isPageJournal,
|
||||||
|
isPageTodayJournal,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
getJournalByDate,
|
getJournalByDate,
|
||||||
@@ -133,9 +120,40 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
getJournalsByDate,
|
getJournalsByDate,
|
||||||
getLocalizedJournalDateString,
|
getLocalizedJournalDateString,
|
||||||
isPageJournal,
|
isPageJournal,
|
||||||
|
isPageTodayJournal,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// split useJournalRouteHelper since it requires a <Route /> context, which may not work in lit
|
||||||
|
export const useJournalRouteHelper = (workspace: BlockSuiteWorkspace) => {
|
||||||
|
const navigateHelper = useNavigateHelper();
|
||||||
|
const { getJournalByDate } = useJournalHelper(workspace);
|
||||||
|
/**
|
||||||
|
* open journal by date, create one if not exist
|
||||||
|
*/
|
||||||
|
const openJournal = useCallback(
|
||||||
|
(maybeDate: MaybeDate) => {
|
||||||
|
const page = getJournalByDate(maybeDate);
|
||||||
|
navigateHelper.openPage(workspace.id, page.id);
|
||||||
|
},
|
||||||
|
[getJournalByDate, navigateHelper, workspace.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* open today's journal
|
||||||
|
*/
|
||||||
|
const openToday = useCallback(() => {
|
||||||
|
const date = dayjs().format(JOURNAL_DATE_FORMAT);
|
||||||
|
openJournal(date);
|
||||||
|
}, [openJournal]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
openJournal,
|
openJournal,
|
||||||
openToday,
|
openToday,
|
||||||
]
|
}),
|
||||||
|
[openJournal, openToday]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,17 +161,28 @@ export const useJournalInfoHelper = (
|
|||||||
workspace: BlockSuiteWorkspace,
|
workspace: BlockSuiteWorkspace,
|
||||||
pageId?: string | null
|
pageId?: string | null
|
||||||
) => {
|
) => {
|
||||||
const { isPageJournal, getJournalDateString } = useJournalHelper(workspace);
|
const {
|
||||||
const journalDate = useMemo(
|
isPageJournal,
|
||||||
() => (pageId ? toDayjs(getJournalDateString(pageId)) : null),
|
getJournalDateString,
|
||||||
[getJournalDateString, pageId]
|
getLocalizedJournalDateString,
|
||||||
);
|
isPageTodayJournal,
|
||||||
|
} = useJournalHelper(workspace);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isJournal: pageId ? isPageJournal(pageId) : false,
|
isJournal: pageId ? isPageJournal(pageId) : false,
|
||||||
journalDate,
|
journalDate: pageId ? toDayjs(getJournalDateString(pageId)) : null,
|
||||||
|
localizedJournalDate: pageId
|
||||||
|
? getLocalizedJournalDateString(pageId)
|
||||||
|
: null,
|
||||||
|
isTodayJournal: pageId ? isPageTodayJournal(pageId) : false,
|
||||||
}),
|
}),
|
||||||
[isPageJournal, journalDate, pageId]
|
[
|
||||||
|
getJournalDateString,
|
||||||
|
getLocalizedJournalDateString,
|
||||||
|
isPageJournal,
|
||||||
|
isPageTodayJournal,
|
||||||
|
pageId,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
setPageMode(currentPageId, mode);
|
setPageMode(currentPageId, mode);
|
||||||
|
// fixme: it seems pageLinkClicked is not triggered sometimes?
|
||||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||||
return openPage(blockSuiteWorkspace.id, pageId);
|
return openPage(blockSuiteWorkspace.id, pageId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
|
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
||||||
import {
|
import {
|
||||||
useJournalHelper,
|
useJournalHelper,
|
||||||
useJournalInfoHelper,
|
useJournalInfoHelper,
|
||||||
|
useJournalRouteHelper,
|
||||||
} from '@affine/core/hooks/use-journal';
|
} from '@affine/core/hooks/use-journal';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -44,6 +46,7 @@ interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
||||||
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
|
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
|
||||||
|
const title = useBlockSuiteWorkspacePageTitle(page.workspace, page.id);
|
||||||
|
|
||||||
const Icon = isJournal
|
const Icon = isJournal
|
||||||
? TodayIcon
|
? TodayIcon
|
||||||
@@ -59,7 +62,7 @@ const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
|||||||
<div className={styles.pageItemIcon}>
|
<div className={styles.pageItemIcon}>
|
||||||
<Icon width={20} height={20} />
|
<Icon width={20} height={20} />
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.pageItemLabel}>{page.meta.title}</span>
|
<span className={styles.pageItemLabel}>{title}</span>
|
||||||
{right}
|
{right}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -81,7 +84,7 @@ const EditorJournalPanel = (props: EditorExtensionProps) => {
|
|||||||
page.workspace,
|
page.workspace,
|
||||||
page.id
|
page.id
|
||||||
);
|
);
|
||||||
const { openJournal } = useJournalHelper(workspace);
|
const { openJournal } = useJournalRouteHelper(workspace);
|
||||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1049,5 +1049,6 @@
|
|||||||
"com.affine.journal.daily-count-created-empty-tips": "You haven't created anything yet",
|
"com.affine.journal.daily-count-created-empty-tips": "You haven't created anything yet",
|
||||||
"com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet",
|
"com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet",
|
||||||
"com.affine.journal.conflict-show-more": "{{count}} more articles",
|
"com.affine.journal.conflict-show-more": "{{count}} more articles",
|
||||||
"com.affine.journal.app-sidebar-title": "Journals"
|
"com.affine.journal.app-sidebar-title": "Journals",
|
||||||
|
"com.affine.editor.reference-not-found": "Linked page not found"
|
||||||
}
|
}
|
||||||
|
|||||||
16
test.txt
Normal file
16
test.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
RUN v1.1.3 /Users/pengx17/Documents/GitHub/AFFiNE
|
||||||
|
|
||||||
|
✓ packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx (1 test) 11ms
|
||||||
|
|
||||||
|
⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯
|
||||||
|
|
||||||
|
Vitest caught 2 unhandled errors during the test run.
|
||||||
|
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.
|
||||||
|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
||||||
|
Test Files 1 passed (1)
|
||||||
|
Tests 1 passed (1)
|
||||||
|
Errors 2 errors
|
||||||
|
Start at 18:09:25
|
||||||
|
Duration 1.63s (transform 536ms, setup 40ms, collect 1.31s, tests 11ms, environment 86ms, prepare 38ms)
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
// prevent tests using two different sources of yjs
|
// prevent tests using two different sources of yjs
|
||||||
yjs: resolve(rootDir, 'node_modules/yjs'),
|
yjs: resolve(rootDir, 'node_modules/yjs'),
|
||||||
|
'@affine/core': fileURLToPath(
|
||||||
|
new URL('./packages/frontend/core/src', import.meta.url)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user