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:
Cats Juice
2024-01-18 07:17:14 +00:00
parent a64854319e
commit cabedef426
16 changed files with 465 additions and 281 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const title = style({
fontWeight: 500,
color: 'var(--affine-text-primary-color)',
});

View File

@@ -122,5 +122,4 @@ export const headerDivider = style({
height: '20px',
width: '1px',
background: 'var(--affine-border-color)',
margin: '0 12px',
});