mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-09-11 19:47:35 +03:00
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
This commit is contained in:
parent
a64854319e
commit
cabedef426
@ -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 (
|
||||
<>
|
||||
<ShareMenu
|
||||
isJournal={isJournal}
|
||||
workspaceMetadata={workspace.meta}
|
||||
currentPage={page}
|
||||
onEnableAffineCloud={() => setOpen(true)}
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="local-share-menu-button" type="primary">
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="local-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
@ -76,7 +82,11 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="cloud-share-menu-button" type="primary">
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="cloud-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.sharedButton']()
|
||||
: t['com.affine.share-menu.shareButton']()}
|
||||
|
@ -1,159 +0,0 @@
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import {
|
||||
type FocusEvent,
|
||||
type InputHTMLAttributes,
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { PageMode } from '../../../atoms';
|
||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||
import { PageHeaderMenuButton } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
pageId: string;
|
||||
isPublic?: boolean;
|
||||
publicMode?: PageMode;
|
||||
}
|
||||
|
||||
const EditableTitle = ({
|
||||
value,
|
||||
onFocus: propsOnFocus,
|
||||
...inputProps
|
||||
}: InputHTMLAttributes<HTMLInputElement>) => {
|
||||
const onFocus = useCallback(
|
||||
(e: FocusEvent<HTMLInputElement>) => {
|
||||
e.target.select();
|
||||
propsOnFocus?.(e);
|
||||
},
|
||||
[propsOnFocus]
|
||||
);
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<input
|
||||
className={styles.titleInput}
|
||||
autoFocus={true}
|
||||
value={value}
|
||||
type="text"
|
||||
data-testid="title-content"
|
||||
onFocus={onFocus}
|
||||
{...inputProps}
|
||||
/>
|
||||
<span className={styles.shadowTitle}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StableTitle = ({
|
||||
blockSuiteWorkspace: workspace,
|
||||
pageId,
|
||||
onRename,
|
||||
isPublic,
|
||||
publicMode,
|
||||
}: BlockSuiteHeaderTitleProps & {
|
||||
onRename?: () => void;
|
||||
}) => {
|
||||
const currentPage = workspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
|
||||
const title = pageMeta?.title;
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
if (!isPublic && onRename) {
|
||||
onRename();
|
||||
}
|
||||
}, [isPublic, onRename]);
|
||||
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace}
|
||||
pageId={pageId}
|
||||
isPublic={isPublic}
|
||||
publicMode={publicMode}
|
||||
/>
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
className={styles.titleEditButton}
|
||||
onDoubleClick={handleRename}
|
||||
>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
{isPublic ? null : (
|
||||
<PageHeaderMenuButton rename={onRename} pageId={pageId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
const { blockSuiteWorkspace: workspace, pageId } = props;
|
||||
const currentPage = workspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace);
|
||||
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setIsEditable(true);
|
||||
}, []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setIsEditable(false);
|
||||
if (!currentPage?.id) {
|
||||
return;
|
||||
}
|
||||
pageTitleMeta.setPageTitle(currentPage.id, title);
|
||||
}, [currentPage?.id, pageTitleMeta, title]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||
onBlur();
|
||||
}
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPageTitle(pageMeta?.title || '');
|
||||
}, [pageMeta?.title]);
|
||||
|
||||
if (isEditable) {
|
||||
return (
|
||||
<EditableTitle
|
||||
onBlur={onBlur}
|
||||
value={title}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
setPageTitle(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <StableTitle {...props} onRename={onRename} />;
|
||||
};
|
||||
|
||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
if (props.isPublic) {
|
||||
return <StableTitle {...props} />;
|
||||
}
|
||||
return <BlockSuiteTitleWithRename {...props} />;
|
||||
};
|
||||
|
||||
BlockSuiteHeaderTitle.displayName = 'BlockSuiteHeaderTitle';
|
@ -1,43 +0,0 @@
|
||||
import { type ComplexStyleRule, style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerTitleContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
columnGap: 12,
|
||||
});
|
||||
|
||||
export const titleEditButton = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const titleInput = style({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
selectors: {
|
||||
'&:focus': {
|
||||
border: '1px solid var(--affine-black-10)',
|
||||
borderRadius: '8px',
|
||||
height: '32px',
|
||||
padding: '6px 8px',
|
||||
borderColor: 'var(--affine-primary-color)',
|
||||
boxShadow: 'var(--affine-active-shadow)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const shadowTitle = style({
|
||||
visibility: 'hidden',
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export interface FavoriteButtonProps {
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
export const useFavorite = (pageId: string) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const favorite = pageMeta?.favorite ?? false;
|
||||
|
||||
const { toggleFavorite: _toggleFavorite } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
|
||||
const toggleFavorite = useCallback(() => {
|
||||
_toggleFavorite(pageId);
|
||||
toast(
|
||||
favorite
|
||||
? t['com.affine.toastMessage.removedFavorites']()
|
||||
: t['com.affine.toastMessage.addedFavorites']()
|
||||
);
|
||||
}, [favorite, pageId, t, _toggleFavorite]);
|
||||
|
||||
return { favorite, toggleFavorite };
|
||||
};
|
||||
|
||||
export const FavoriteButton = ({ pageId }: FavoriteButtonProps) => {
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
return <FavoriteTag active={!!favorite} onClick={toggleFavorite} />;
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
import { WeekDatePicker, type WeekDatePickerHandle } from '@affine/component';
|
||||
import {
|
||||
useJournalHelper,
|
||||
useJournalInfoHelper,
|
||||
} from '@affine/core/hooks/use-journal';
|
||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface JournalWeekDatePickerProps {
|
||||
workspace: BlockSuiteWorkspace;
|
||||
page: Page;
|
||||
}
|
||||
|
||||
const weekStyle = { maxWidth: 548, width: '100%' };
|
||||
export const JournalWeekDatePicker = ({
|
||||
workspace,
|
||||
page,
|
||||
}: JournalWeekDatePickerProps) => {
|
||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
||||
const { journalDate } = useJournalInfoHelper(page.meta);
|
||||
const { openJournal } = useJournalHelper(workspace);
|
||||
const [date, setDate] = useState(
|
||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!journalDate) return;
|
||||
setDate(journalDate.format('YYYY-MM-DD'));
|
||||
handleRef.current?.setCursor?.(journalDate);
|
||||
}, [journalDate]);
|
||||
|
||||
return (
|
||||
<WeekDatePicker
|
||||
handleRef={handleRef}
|
||||
style={weekStyle}
|
||||
value={date}
|
||||
onChange={openJournal}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export interface JournalTodayButtonProps {
|
||||
workspace: BlockSuiteWorkspace;
|
||||
}
|
||||
|
||||
export const JournalTodayButton = ({ workspace }: JournalTodayButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const journalHelper = useJournalHelper(workspace);
|
||||
|
||||
const onToday = useCallback(() => {
|
||||
journalHelper.openToday();
|
||||
}, [journalHelper]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="default"
|
||||
onClick={onToday}
|
||||
style={{ height: 32, padding: '0px 8px' }}
|
||||
>
|
||||
{t['com.affine.today']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -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 = (
|
||||
<>
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<EditIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onSelect={rename}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<EditIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onSelect={rename}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
@ -136,7 +133,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onSelect={handleFavorite}
|
||||
onSelect={toggleFavorite}
|
||||
style={menuItemStyle}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
@ -162,18 +159,20 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
{t['com.affine.header.option.add-tag']()}
|
||||
</MenuItem> */}
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<DuplicateIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-duplicate"
|
||||
onSelect={handleDuplicate}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<DuplicateIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-duplicate"
|
||||
onSelect={handleDuplicate}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
@ -232,7 +231,6 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
<FavoriteTag active={!!pageMeta?.favorite} onClick={handleFavorite} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<HTMLInputElement>;
|
||||
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 (
|
||||
<InlineEdit
|
||||
className={styles.title}
|
||||
autoSelect
|
||||
value={title}
|
||||
onChange={onChange}
|
||||
editable={!isPublic}
|
||||
placeholder="Untitled"
|
||||
data-testid="title-edit-button"
|
||||
handleRef={inputHandleRef}
|
||||
inputAttrs={inputAttrs}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const title = style({
|
||||
fontWeight: 500,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
});
|
@ -122,5 +122,4 @@ export const headerDivider = style({
|
||||
height: '20px',
|
||||
width: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
margin: '0 12px',
|
||||
});
|
||||
|
113
packages/frontend/core/src/hooks/use-journal.ts
Normal file
113
packages/frontend/core/src/hooks/use-journal.ts
Normal file
@ -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]
|
||||
);
|
||||
};
|
@ -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={<ShareHeaderLeftItem />}
|
||||
center={
|
||||
<BlockSuiteHeaderTitle
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
pageId={pageId}
|
||||
isPublic={true}
|
||||
publicMode={publishMode}
|
||||
/>
|
||||
<>
|
||||
<EditorModeSwitch
|
||||
isPublic
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
pageId={pageId}
|
||||
publicMode={publishMode}
|
||||
/>
|
||||
<BlocksuiteHeaderTitle
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
pageId={pageId}
|
||||
isPublic={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<ShareHeaderRightItem
|
||||
|
@ -28,6 +28,7 @@ export const mainHeader = style([
|
||||
header,
|
||||
{
|
||||
padding: '0 16px',
|
||||
gap: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
@ -58,3 +59,11 @@ export const standaloneExtensionSwitcherWrapper = style({
|
||||
height: '52px',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const journalWeekPicker = style({
|
||||
minWidth: 100,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
@ -1,16 +1,24 @@
|
||||
import type { InlineEditHandle } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { RightSidebarIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||
import { BlockSuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header-title';
|
||||
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
||||
import { HeaderDivider } from '../../../components/pure/header';
|
||||
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
|
||||
import * as styles from './detail-page-header.css';
|
||||
@ -46,12 +54,7 @@ const WindowsMainPageHeaderRight = ({
|
||||
return (
|
||||
<>
|
||||
<HeaderDivider />
|
||||
<div
|
||||
className={styles.mainHeaderRight}
|
||||
style={{
|
||||
marginRight: editorSidebarOpen ? 0 : -16,
|
||||
}}
|
||||
>
|
||||
<div className={styles.mainHeaderRight} style={{ marginRight: -16 }}>
|
||||
{showSidebarSwitch ? <ToggleSidebarButton /> : null}
|
||||
<WindowsAppControls />
|
||||
</div>
|
||||
@ -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 (
|
||||
<Header className={styles.mainHeader}>
|
||||
<SidebarSwitch show={!leftSidebarOpen} />
|
||||
{!leftSidebarOpen ? <HeaderDivider /> : null}
|
||||
<BlockSuiteHeaderTitle
|
||||
pageId={page.id}
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={page?.id}
|
||||
/>
|
||||
<div className={styles.journalWeekPicker}>
|
||||
<JournalWeekDatePicker
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
page={page}
|
||||
/>
|
||||
</div>
|
||||
<JournalTodayButton workspace={workspace.blockSuiteWorkspace} />
|
||||
<HeaderDivider />
|
||||
<PageHeaderMenuButton isJournal pageId={page?.id} />
|
||||
{page ? (
|
||||
<SharePageButton isJournal workspace={workspace} page={page} />
|
||||
) : null}
|
||||
<RightHeader showSidebarSwitch={showSidebarSwitch} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
export function NormalPageHeader({
|
||||
page,
|
||||
workspace,
|
||||
showSidebarSwitch = true,
|
||||
}: PageHeaderProps) {
|
||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||
}, []);
|
||||
return (
|
||||
<Header className={styles.mainHeader}>
|
||||
<SidebarSwitch show={!leftSidebarOpen} />
|
||||
{!leftSidebarOpen ? <HeaderDivider /> : null}
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={page?.id}
|
||||
/>
|
||||
<BlocksuiteHeaderTitle
|
||||
inputHandleRef={titleInputHandleRef}
|
||||
pageId={page?.id}
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
<PageHeaderMenuButton rename={onRename} pageId={page?.id} />
|
||||
<FavoriteButton pageId={page?.id} />
|
||||
<div className={styles.spacer} />
|
||||
{page ? <SharePageButton workspace={workspace} page={page} /> : null}
|
||||
<RightHeader showSidebarSwitch={showSidebarSwitch} />
|
||||
@ -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 ? (
|
||||
<JournalPageHeader {...props} />
|
||||
) : (
|
||||
<NormalPageHeader {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsSidebarHeader() {
|
||||
return (
|
||||
<>
|
||||
|
Loading…
Reference in New Issue
Block a user