feat(core): responsive detail page header (#7263)

This commit is contained in:
CatsJuice
2024-06-20 05:47:43 +00:00
parent 78429166da
commit 1fdfa834a0
15 changed files with 264 additions and 108 deletions
@@ -7,19 +7,13 @@ import { ShareMenu } from './share-menu';
type SharePageModalProps = {
workspace: Workspace;
page: Doc;
isJournal?: boolean;
};
export const SharePageButton = ({
workspace,
page,
isJournal,
}: SharePageModalProps) => {
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
const confirmEnableCloud = useEnableCloud();
return (
<ShareMenu
isJournal={isJournal}
workspaceMetadata={workspace.meta}
currentPage={page}
onEnableAffineCloud={() =>
@@ -138,7 +138,7 @@ globalStyle(`${shareLinkStyle} > span`, {
globalStyle(`${shareLinkStyle} > div > svg`, {
color: cssVar('linkColor'),
});
export const journalShareButton = style({
export const shareButton = style({
height: 32,
padding: '0px 8px',
});
@@ -1,27 +1,24 @@
import { Button } from '@affine/component/ui/button';
import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu';
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useIsActiveView } from '@affine/core/modules/workbench';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { WebIcon } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store';
import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx';
import { forwardRef, type PropsWithChildren, type Ref } from 'react';
import * as styles from './index.css';
import { ShareExport } from './share-export';
import { SharePage } from './share-page';
export interface ShareMenuProps {
export interface ShareMenuProps extends PropsWithChildren {
workspaceMetadata: WorkspaceMetadata;
currentPage: Doc;
isJournal?: boolean;
onEnableAffineCloud: () => void;
}
const ShareMenuContent = (props: ShareMenuProps) => {
export const ShareMenuContent = (props: ShareMenuProps) => {
const t = useI18n();
return (
<div className={styles.containerStyle}>
@@ -40,8 +37,20 @@ const ShareMenuContent = (props: ShareMenuProps) => {
);
};
const LocalShareMenu = (props: ShareMenuProps) => {
const DefaultShareButton = forwardRef(function DefaultShareButton(
_,
ref: Ref<HTMLButtonElement>
) {
const t = useI18n();
return (
<Button ref={ref} className={styles.shareButton} type="primary">
{t['com.affine.share-menu.shareButton']()}
</Button>
);
});
const LocalShareMenu = (props: ShareMenuProps) => {
return (
<Menu
items={<ShareMenuContent {...props} />}
@@ -53,27 +62,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
modal: false,
}}
>
<Button
className={clsx({ [styles.journalShareButton]: props.isJournal })}
data-testid="local-share-menu-button"
type="primary"
>
{t['com.affine.share-menu.shareButton']()}
</Button>
<div data-testid="local-share-menu-button">
{props.children || <DefaultShareButton />}
</div>
</Menu>
);
};
const CloudShareMenu = (props: ShareMenuProps) => {
const t = useI18n();
// only enable copy link commands when the view is active and the workspace is cloud
const isActiveView = useIsActiveView();
useRegisterCopyLinkCommands({
workspaceId: props.workspaceMetadata.id,
docId: props.currentPage.id,
isActiveView,
});
return (
<Menu
items={<ShareMenuContent {...props} />}
@@ -85,13 +81,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
modal: false,
}}
>
<Button
className={clsx({ [styles.journalShareButton]: props.isJournal })}
data-testid="cloud-share-menu-button"
type="primary"
>
{t['com.affine.share-menu.shareButton']()}
</Button>
<div data-testid="cloud-share-menu-button">
{props.children || <DefaultShareButton />}
</div>
</Menu>
);
};
@@ -4,14 +4,18 @@ import {
MenuIcon,
MenuItem,
MenuSeparator,
MenuSub,
} from '@affine/component/ui/menu';
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
import { Export, MoveToTrash } from '@affine/core/components/page-list';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
import { mixpanel } from '@affine/core/utils';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
@@ -24,7 +28,9 @@ import {
HistoryIcon,
ImportIcon,
PageIcon,
ShareIcon,
} from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store';
import {
DocService,
useLiveData,
@@ -40,16 +46,19 @@ import { useFavorite } from '../favorite';
type PageMenuProps = {
rename?: () => void;
pageId: string;
page: Doc;
isJournal?: boolean;
};
// fixme: refactor this file
export const PageHeaderMenuButton = ({
rename,
pageId,
page,
isJournal,
}: PageMenuProps) => {
const pageId = page?.id;
const t = useI18n();
const { hideShare } = useDetailPageHeaderResponsive();
const confirmEnableCloud = useEnableCloud();
const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection;
@@ -127,8 +136,46 @@ export const PageHeaderMenuButton = ({
}
}, [importFile]);
const showResponsiveMenu = hideShare;
const ResponsiveMenuItems = (
<>
{hideShare ? (
<MenuSub
subContentOptions={{
sideOffset: 12,
alignOffset: -8,
}}
items={
<div style={{ padding: 4 }}>
<ShareMenuContent
workspaceMetadata={workspace.meta}
currentPage={page}
onEnableAffineCloud={() =>
confirmEnableCloud(workspace, {
openPageId: page.id,
})
}
/>
</div>
}
triggerOptions={{
preFix: (
<MenuIcon>
<ShareIcon />
</MenuIcon>
),
}}
>
{t['com.affine.share-menu.shareButton']()}
</MenuSub>
) : null}
<MenuSeparator />
</>
);
const EditMenu = (
<>
{showResponsiveMenu ? ResponsiveMenuItems : null}
{!isJournal && (
<MenuItem
preFix={
@@ -0,0 +1,17 @@
import { IconButton } from '@affine/component';
import { PresentationIcon } from '@blocksuite/icons/rc';
import { usePresent } from './use-present';
export const DetailPageHeaderPresentButton = () => {
const { isPresent, handlePresent } = usePresent();
return (
<IconButton
style={{ flexShrink: 0 }}
size={'large'}
icon={<PresentationIcon />}
onClick={() => handlePresent(!isPresent)}
></IconButton>
);
};
@@ -0,0 +1,59 @@
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import type { EdgelessRootService } from '@blocksuite/blocks';
import { useCallback, useEffect, useState } from 'react';
export const usePresent = () => {
const [isPresent, setIsPresent] = useState(false);
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(
(enable = true) => {
isPresent;
const editorHost = editor?.host;
if (!editorHost) return;
// TODO: use surfaceService subAtom
const enterOrLeavePresentationMode = () => {
const edgelessRootService = editorHost.spec.getService(
'affine:page'
) as EdgelessRootService;
if (!edgelessRootService) {
return;
}
const activeTool = edgelessRootService.tool.edgelessTool.type;
const isFrameNavigator = activeTool === 'frameNavigator';
if ((enable && isFrameNavigator) || (!enable && !isFrameNavigator))
return;
edgelessRootService.tool.setEdgelessTool({
type: enable ? 'frameNavigator' : 'default',
});
};
enterOrLeavePresentationMode();
setIsPresent(enable);
},
[editor?.host, isPresent]
);
useEffect(() => {
if (!isPresent) return;
const editorHost = editor?.host;
if (!editorHost) return;
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
if (!edgelessPage) return;
return edgelessPage.slots.edgelessToolUpdated.on(() => {
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
}).dispose;
}, [editor?.host, isPresent]);
return {
isPresent,
handlePresent,
};
};
@@ -11,7 +11,7 @@ export const StyledEditorModeSwitch = styled('div')<{
background: showAlone
? 'transparent'
: 'var(--affine-background-secondary-color)',
borderRadius: '12px',
borderRadius: '8px',
...displayFlex('space-between', 'center'),
padding: '4px 4px',
position: 'relative',
@@ -23,7 +23,7 @@ export const StyledEditorModeSwitch = styled('div')<{
height: '24px',
background: 'var(--affine-background-primary-color)',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '8px',
borderRadius: '4px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '32px'})`,
@@ -1,60 +1,19 @@
import { Button } from '@affine/component/ui/button';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { useI18n } from '@affine/i18n';
import type { EdgelessRootService } from '@blocksuite/blocks';
import { PresentationIcon } from '@blocksuite/icons/rc';
import { useCallback, useEffect, useState } from 'react';
import { usePresent } from '../../blocksuite/block-suite-header/present/use-present';
import * as styles from './styles.css';
export const PresentButton = () => {
const t = useI18n();
const [isPresent, setIsPresent] = useState(false);
const [editor] = useActiveBlocksuiteEditor();
const handlePresent = useCallback(() => {
const editorHost = editor?.host;
if (!editorHost || isPresent) return;
// TODO: use surfaceService subAtom
const enterPresentationMode = () => {
const edgelessRootService = editorHost.spec.getService(
'affine:page'
) as EdgelessRootService;
if (
!edgelessRootService ||
edgelessRootService.tool.edgelessTool.type === 'frameNavigator'
) {
return;
}
edgelessRootService.tool.setEdgelessTool({ type: 'frameNavigator' });
};
enterPresentationMode();
setIsPresent(true);
}, [editor?.host, isPresent]);
useEffect(() => {
if (!isPresent) return;
const editorHost = editor?.host;
if (!editorHost) return;
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
if (!edgelessPage) return;
return edgelessPage.slots.edgelessToolUpdated.on(() => {
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
}).dispose;
}, [editor?.host, isPresent]);
const { isPresent, handlePresent } = usePresent();
return (
<Button
icon={<PresentationIcon />}
className={styles.presentButton}
onClick={handlePresent}
onClick={() => handlePresent()}
disabled={isPresent}
withoutHoverStyle
>