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 { 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}

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 { 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}

View File

@@ -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)',
});

View File

@@ -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')
); );

View File

@@ -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();

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 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

View File

@@ -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

View File

@@ -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');
});
});

View File

@@ -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();
});
});

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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,
]
); );
}; };

View File

@@ -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);
}); });

View File

@@ -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(() => {

View File

@@ -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
View 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)

View File

@@ -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: {