From cabedef426f1b8000646fcf8e634077d5a47db36 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 18 Jan 2024 07:17:14 +0000 Subject: [PATCH] feat(core): journal hooks and page header layout (#5549) feat(core): split page header items feat(core): journal page judgment and header layout feat(core): Add journal today button and style changes to share menu --- .../affine/share-page-modal/index.tsx | 8 +- .../share-page-modal/share-menu/index.css.ts | 5 + .../share-menu/share-menu.tsx | 14 +- .../block-suite-header-title/index.tsx | 159 ------------------ .../block-suite-header-title/styles.css.ts | 43 ----- .../block-suite-header/favorite/index.tsx | 46 +++++ .../journal/date-picker.tsx | 42 +++++ .../journal/today-button.tsx | 28 +++ .../menu/index.tsx} | 98 ++++++----- .../block-suite-header/title/index.tsx | 57 +++++++ .../block-suite-header/title/style.css.ts | 6 + .../src/components/pure/header/style.css.tsx | 1 - .../frontend/core/src/hooks/use-journal.ts | 113 +++++++++++++ .../core/src/pages/share/share-header.tsx | 22 ++- .../detail-page/detail-page-header.css.ts | 9 + .../detail-page/detail-page-header.tsx | 95 +++++++++-- 16 files changed, 465 insertions(+), 281 deletions(-) delete mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx delete mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header-title/styles.css.ts create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/journal/date-picker.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/journal/today-button.tsx rename packages/frontend/core/src/components/blocksuite/{block-suite-header-title/operation-menu.tsx => block-suite-header/menu/index.tsx} (73%) create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts create mode 100644 packages/frontend/core/src/hooks/use-journal.ts diff --git a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx index 2a7cca033e..f9b2eb1512 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx @@ -13,9 +13,14 @@ import { ShareMenu } from './share-menu'; type SharePageModalProps = { workspace: Workspace; page: Page; + isJournal?: boolean; }; -export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { +export const SharePageButton = ({ + workspace, + page, + isJournal, +}: SharePageModalProps) => { const [open, setOpen] = useState(false); const { openPage } = useNavigateHelper(); @@ -35,6 +40,7 @@ export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { return ( <> setOpen(true)} diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts index e8d56153eb..0275804daf 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts @@ -157,3 +157,8 @@ globalStyle(`${shareLinkStyle} > span`, { globalStyle(`${shareLinkStyle} > div > svg`, { color: 'var(--affine-link-color)', }); + +export const journalShareButton = style({ + height: 32, + padding: '0px 8px', +}); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index 2747fc8f0b..5e385c2aa7 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -6,6 +6,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { WorkspaceMetadata } from '@affine/workspace'; import { WebIcon } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; +import clsx from 'clsx'; import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page'; import * as styles from './index.css'; @@ -15,6 +16,7 @@ import { SharePage } from './share-page'; export interface ShareMenuProps { workspaceMetadata: WorkspaceMetadata; currentPage: Page; + isJournal?: boolean; onEnableAffineCloud: () => void; } @@ -50,7 +52,11 @@ const LocalShareMenu = (props: ShareMenuProps) => { modal: false, }} > - @@ -76,7 +82,11 @@ const CloudShareMenu = (props: ShareMenuProps) => { modal: false, }} > - + ); +}; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx similarity index 73% rename from packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx rename to packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 459570096c..dfe7f6bea2 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -4,13 +4,15 @@ import { MenuItem, MenuSeparator, } from '@affine/component/ui/menu'; -import { - Export, - FavoriteTag, - MoveToTrash, -} from '@affine/core/components/page-list'; +import { currentModeAtom } from '@affine/core/atoms/mode'; +import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal'; +import { Export, MoveToTrash } from '@affine/core/components/page-list'; +import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; +import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; +import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; +import { toast } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; @@ -27,21 +29,21 @@ import { import { useAtomValue } from 'jotai'; import { useCallback, useState } from 'react'; -import { currentModeAtom } from '../../../atoms/mode'; -import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; -import { useExportPage } from '../../../hooks/affine/use-export-page'; -import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; -import { toast } from '../../../utils'; -import { PageHistoryModal } from '../../affine/page-history-modal/history-modal'; -import { HeaderDropDownButton } from '../../pure/header-drop-down-button'; -import { usePageHelper } from '../block-suite-page-list/utils'; +import { HeaderDropDownButton } from '../../../pure/header-drop-down-button'; +import { usePageHelper } from '../../block-suite-page-list/utils'; +import { useFavorite } from '../favorite'; type PageMenuProps = { rename?: () => void; pageId: string; + isJournal?: boolean; }; // fixme: refactor this file -export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { +export const PageHeaderMenuButton = ({ + rename, + pageId, + isJournal, +}: PageMenuProps) => { const t = useAFFiNEI18N(); // fixme(himself65): remove these hooks ASAP @@ -54,9 +56,10 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { meta => meta.id === pageId ); const currentMode = useAtomValue(currentModeAtom); - const favorite = pageMeta?.favorite ?? false; - const { togglePageMode, toggleFavorite, duplicate } = + const { favorite, toggleFavorite } = useFavorite(pageId); + + const { togglePageMode, duplicate } = useBlockSuiteMetaHelper(blockSuiteWorkspace); const { importFile } = usePageHelper(blockSuiteWorkspace); const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); @@ -78,14 +81,6 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { }); }, [pageId, pageMeta, setTrashModal]); - const handleFavorite = useCallback(() => { - toggleFavorite(pageId); - toast( - favorite - ? t['com.affine.toastMessage.removedFavorites']() - : t['com.affine.toastMessage.addedFavorites']() - ); - }, [favorite, pageId, t, toggleFavorite]); const handleSwitchMode = useCallback(() => { togglePageMode(pageId); toast( @@ -107,18 +102,20 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { const EditMenu = ( <> - - - - } - data-testid="editor-option-menu-rename" - onSelect={rename} - style={menuItemStyle} - > - {t['Rename']()} - + {!isJournal && ( + + + + } + data-testid="editor-option-menu-rename" + onSelect={rename} + style={menuItemStyle} + > + {t['Rename']()} + + )} @@ -136,7 +133,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { @@ -162,18 +159,20 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { {t['com.affine.header.option.add-tag']()} */} - - - - } - data-testid="editor-option-menu-duplicate" - onSelect={handleDuplicate} - style={menuItemStyle} - > - {t['com.affine.header.option.duplicate']()} - + {!isJournal && ( + + + + } + data-testid="editor-option-menu-duplicate" + onSelect={handleDuplicate} + style={menuItemStyle} + > + {t['com.affine.header.option.duplicate']()} + + )} @@ -232,7 +231,6 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { onOpenChange={setHistoryModalOpen} /> ) : null} - ); }; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx new file mode 100644 index 0000000000..b55a64b507 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx @@ -0,0 +1,57 @@ +import { InlineEdit, type InlineEditProps } from '@affine/component'; +import { + useBlockSuitePageMeta, + usePageMetaHelper, +} from '@affine/core/hooks/use-block-suite-page-meta'; +import type { BlockSuiteWorkspace } from '@affine/core/shared'; +import type { HTMLAttributes } from 'react'; +import { useCallback } from 'react'; + +import * as styles from './style.css'; + +export interface BlockSuiteHeaderTitleProps { + blockSuiteWorkspace: BlockSuiteWorkspace; + pageId: string; + /** if set, title cannot be edited */ + isPublic?: boolean; + inputHandleRef?: InlineEditProps['handleRef']; +} + +const inputAttrs = { + 'data-testid': 'title-content', +} as HTMLAttributes; +export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => { + const { + blockSuiteWorkspace: workspace, + pageId, + isPublic, + inputHandleRef, + } = props; + const currentPage = workspace.getPage(pageId); + const pageMeta = useBlockSuitePageMeta(workspace).find( + meta => meta.id === currentPage?.id + ); + const title = pageMeta?.title; + const { setPageTitle } = usePageMetaHelper(workspace); + + const onChange = useCallback( + (v: string) => { + setPageTitle(currentPage?.id || '', v); + }, + [currentPage?.id, setPageTitle] + ); + + return ( + + ); +}; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts new file mode 100644 index 0000000000..8a3218f0af --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const title = style({ + fontWeight: 500, + color: 'var(--affine-text-primary-color)', +}); diff --git a/packages/frontend/core/src/components/pure/header/style.css.tsx b/packages/frontend/core/src/components/pure/header/style.css.tsx index faea9cac52..d989bc8418 100644 --- a/packages/frontend/core/src/components/pure/header/style.css.tsx +++ b/packages/frontend/core/src/components/pure/header/style.css.tsx @@ -122,5 +122,4 @@ export const headerDivider = style({ height: '20px', width: '1px', background: 'var(--affine-border-color)', - margin: '0 12px', }); diff --git a/packages/frontend/core/src/hooks/use-journal.ts b/packages/frontend/core/src/hooks/use-journal.ts new file mode 100644 index 0000000000..f864100867 --- /dev/null +++ b/packages/frontend/core/src/hooks/use-journal.ts @@ -0,0 +1,113 @@ +import type { PageMeta } from '@blocksuite/store'; +import { initEmptyPage } from '@toeverything/infra/blocksuite'; +import dayjs from 'dayjs'; +import { useCallback, useMemo } from 'react'; + +import type { BlockSuiteWorkspace } from '../shared'; +import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper'; +import { useNavigateHelper } from './use-navigate-helper'; + +type MaybeDate = Date | string | number; +export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD'; + +function isPageJournal(pageMeta?: PageMeta) { + return !!(pageMeta && pageMeta.title.match(/^\d{4}-\d{2}-\d{2}$/)); +} + +function getJournalDate(pageMeta?: PageMeta) { + if (!isPageJournal(pageMeta)) return null; + if (!pageMeta?.title) return null; + if (!dayjs(pageMeta.title).isValid()) return null; + return dayjs(pageMeta.title); +} + +export const useJournalHelper = (workspace: BlockSuiteWorkspace) => { + const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace); + const navigateHelper = useNavigateHelper(); + + /** + * @internal + */ + const _createJournal = useCallback( + (maybeDate: MaybeDate) => { + const title = dayjs(maybeDate).format(JOURNAL_DATE_FORMAT); + const page = bsWorkspaceHelper.createPage(); + initEmptyPage(page, title).catch(err => + console.error('Failed to load journal page', err) + ); + return page; + }, + [bsWorkspaceHelper] + ); + + /** + * query all journals by date + */ + const getJournalsByDate = useCallback( + (maybeDate: MaybeDate) => { + const day = dayjs(maybeDate); + return Array.from(workspace.pages.values()).filter(page => { + if (!isPageJournal(page.meta)) return false; + if (page.meta.trash) return false; + const journalDate = getJournalDate(page.meta); + if (!journalDate) return false; + return day.isSame(journalDate, 'day'); + }); + }, + [workspace.pages] + ); + + /** + * get journal by date, create one if not exist + */ + const getJournalByDate = useCallback( + (maybeDate: MaybeDate) => { + const pages = getJournalsByDate(maybeDate); + if (pages.length) return pages[0]; + return _createJournal(maybeDate); + }, + [_createJournal, getJournalsByDate] + ); + + /** + * 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( + () => ({ + getJournalsByDate, + getJournalByDate, + openJournal, + openToday, + }), + [getJournalByDate, getJournalsByDate, openJournal, openToday] + ); +}; + +export const useJournalInfoHelper = (pageMeta?: PageMeta) => { + const isJournal = isPageJournal(pageMeta); + const journalDate = useMemo(() => getJournalDate(pageMeta), [pageMeta]); + + return useMemo( + () => ({ + isJournal, + journalDate, + }), + [isJournal, journalDate] + ); +}; diff --git a/packages/frontend/core/src/pages/share/share-header.tsx b/packages/frontend/core/src/pages/share/share-header.tsx index 3cf7f9ef78..9ca199f1ae 100644 --- a/packages/frontend/core/src/pages/share/share-header.tsx +++ b/packages/frontend/core/src/pages/share/share-header.tsx @@ -1,7 +1,8 @@ +import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { PageMode } from '../../atoms'; -import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title'; +import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index'; import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item'; import ShareHeaderRightItem from '../../components/cloud/share-header-right-item'; import { Header } from '../../components/pure/header'; @@ -20,12 +21,19 @@ export function ShareHeader({ isFloat={publishMode === 'edgeless'} left={} center={ - + <> + + + } right={ -
+
{showSidebarSwitch ? : null}
@@ -100,27 +103,71 @@ function Header({ ); } -export function DetailPageHeader({ - page, - workspace, - showSidebarSwitch = true, -}: { +interface PageHeaderProps { page: Page; workspace: Workspace; showSidebarSwitch?: boolean; -}) { +} +const RightHeader = isWindowsDesktop + ? WindowsMainPageHeaderRight + : NonWindowsMainPageHeaderRight; +export function JournalPageHeader({ + page, + workspace, + showSidebarSwitch = true, +}: PageHeaderProps) { const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); - const RightHeader = isWindowsDesktop - ? WindowsMainPageHeaderRight - : NonWindowsMainPageHeaderRight; return (
{!leftSidebarOpen ? : null} - +
+ +
+ + + + {page ? ( + + ) : null} + +
+ ); +} + +export function NormalPageHeader({ + page, + workspace, + showSidebarSwitch = true, +}: PageHeaderProps) { + const titleInputHandleRef = useRef(null); + const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); + + const onRename = useCallback(() => { + setTimeout(() => titleInputHandleRef.current?.triggerEdit()); + }, []); + return ( +
+ + {!leftSidebarOpen ? : null} + + + +
{page ? : null} @@ -128,6 +175,18 @@ export function DetailPageHeader({ ); } +export function DetailPageHeader(props: PageHeaderProps) { + const { page } = props; + const { isJournal } = useJournalInfoHelper(page.meta); + const isInTrash = page.meta.trash; + + return isJournal && !isInTrash ? ( + + ) : ( + + ); +} + function WindowsSidebarHeader() { return ( <>