mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
@@ -16,12 +16,17 @@ import {
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import { MoveTo, MoveToTrash } from '../../../affine/operation-menu-items';
|
||||
import {
|
||||
DisablePublicSharing,
|
||||
MoveTo,
|
||||
MoveToTrash,
|
||||
} from '../../../affine/operation-menu-items';
|
||||
|
||||
export type OperationCellProps = {
|
||||
pageMeta: PageMeta;
|
||||
@@ -40,12 +45,24 @@ export const OperationCell: React.FC<OperationCellProps> = ({
|
||||
onToggleFavoritePage,
|
||||
onToggleTrashPage,
|
||||
}) => {
|
||||
const { id, favorite } = pageMeta;
|
||||
const { id, favorite, isPublic } = pageMeta;
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||
|
||||
const page = blockSuiteWorkspace.getPage(id);
|
||||
assertExists(page);
|
||||
|
||||
const OperationMenu = (
|
||||
<>
|
||||
{isPublic && (
|
||||
<DisablePublicSharing
|
||||
testId="disable-public-sharing"
|
||||
onItemClick={() => {
|
||||
setOpenDisableShared(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onToggleFavoritePage(id);
|
||||
@@ -111,6 +128,13 @@ export const OperationCell: React.FC<OperationCellProps> = ({
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<DisablePublicSharing.DisablePublicSharingModal
|
||||
page={page}
|
||||
open={openDisableShared}
|
||||
onClose={() => {
|
||||
setOpenDisableShared(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -81,7 +81,7 @@ const FavoriteTag: React.FC<FavoriteTagProps> = ({
|
||||
type PageListProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
isPublic?: boolean;
|
||||
listType?: 'all' | 'trash' | 'favorite';
|
||||
listType?: 'all' | 'trash' | 'favorite' | 'shared';
|
||||
onClickPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@ const filter = {
|
||||
return !parentMeta?.trash && pageMeta.trash;
|
||||
},
|
||||
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
|
||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||
};
|
||||
|
||||
export const PageList: React.FC<PageListProps> = ({
|
||||
@@ -108,6 +109,7 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const isTrash = listType === 'trash';
|
||||
const isShared = listType === 'shared';
|
||||
const record = useAtomValue(workspacePreferredModeAtom);
|
||||
const list = useMemo(
|
||||
() =>
|
||||
@@ -130,7 +132,11 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
<TableCell proportion={0.5}>{t('Title')}</TableCell>
|
||||
<TableCell proportion={0.2}>{t('Created')}</TableCell>
|
||||
<TableCell proportion={0.2}>
|
||||
{isTrash ? t('Moved to Trash') : t('Updated')}
|
||||
{isTrash
|
||||
? t('Moved to Trash')
|
||||
: isShared
|
||||
? 'Shared'
|
||||
: t('Updated')}
|
||||
</TableCell>
|
||||
<TableCell proportion={0.1}></TableCell>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { displayFlex, styled, TextButton } from '@affine/component';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||
export const EditPage = () => {
|
||||
const router = useRouter();
|
||||
const pageId = router.query.pageId as string;
|
||||
const workspaceId = router.query.workspaceId as string;
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const onClickPage = useCallback(() => {
|
||||
if (workspaceId && pageId) {
|
||||
jumpToPage(workspaceId, pageId);
|
||||
}
|
||||
}, [jumpToPage, pageId, workspaceId]);
|
||||
return (
|
||||
<div>
|
||||
<StyledEditPageButton onClick={() => onClickPage()}>
|
||||
Edit Page
|
||||
</StyledEditPageButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditPage;
|
||||
|
||||
const StyledEditPageButton = styled(
|
||||
TextButton,
|
||||
{}
|
||||
)(({ theme }) => {
|
||||
return {
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
color: theme.colors.primaryColor,
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '0 16px',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertEquals } from '@blocksuite/store';
|
||||
import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-workspace-publish';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||
import { WorkspaceSubPath } from '../../../../shared';
|
||||
import { Unreachable } from '../../../affine/affine-error-eoundary';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
import type { BaseHeaderProps } from '../header';
|
||||
|
||||
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
// todo: these hooks should be moved to the top level
|
||||
const togglePublish = useToggleWorkspacePublish(
|
||||
props.workspace as AffineWorkspace
|
||||
);
|
||||
const helper = useRouterHelper(useRouter());
|
||||
return (
|
||||
<ShareMenu
|
||||
workspace={props.workspace as AffineWorkspace}
|
||||
currentPage={props.currentPage as Page}
|
||||
onEnableAffineCloud={useCallback(async () => {
|
||||
throw new Unreachable(
|
||||
'Affine workspace should not enable affine cloud again'
|
||||
);
|
||||
}, [])}
|
||||
onOpenWorkspaceSettings={useCallback(
|
||||
async workspace => {
|
||||
return helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
|
||||
},
|
||||
[helper]
|
||||
)}
|
||||
togglePagePublic={useCallback(async (page, isPublic) => {
|
||||
page.workspace.setPageMeta(page.id, { isPublic });
|
||||
}, [])}
|
||||
toggleWorkspacePublish={useCallback(
|
||||
async (workspace, publish) => {
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE);
|
||||
assertEquals(workspace.id, props.workspace.id);
|
||||
await togglePublish(publish);
|
||||
},
|
||||
[props.workspace.id, togglePublish]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
// todo: these hooks should be moved to the top level
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const helper = useRouterHelper(useRouter());
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<ShareMenu
|
||||
workspace={props.workspace as LocalWorkspace}
|
||||
currentPage={props.currentPage as Page}
|
||||
onEnableAffineCloud={useCallback(
|
||||
async workspace => {
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
|
||||
assertEquals(workspace.id, props.workspace.id);
|
||||
setOpen(true);
|
||||
},
|
||||
[props.workspace.id]
|
||||
)}
|
||||
onOpenWorkspaceSettings={useCallback(
|
||||
async workspace => {
|
||||
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
|
||||
},
|
||||
[helper]
|
||||
)}
|
||||
togglePagePublic={useCallback(async (page, isPublic) => {
|
||||
// local workspace should not have public page
|
||||
throw new Error('unreachable');
|
||||
}, [])}
|
||||
toggleWorkspacePublish={useCallback(
|
||||
async (workspace, publish) => {
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
|
||||
assertEquals(workspace.id, props.workspace.id);
|
||||
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
|
||||
},
|
||||
[helper, props.workspace.id]
|
||||
)}
|
||||
/>
|
||||
<TransformWorkspaceToAffineModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConform={() => {
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
props.workspace as LocalWorkspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
return <AffineHeaderShareMenu {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalHeaderShareMenu {...props} />;
|
||||
}
|
||||
throw new Error('unreachable');
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { AffineIcon, SignOutIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../../hooks/current/use-current-user';
|
||||
const EditMenu = (
|
||||
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
|
||||
Sign Out
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const UserAvatar = () => {
|
||||
const user = useCurrentUser();
|
||||
return (
|
||||
<Menu
|
||||
width={276}
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
{user ? (
|
||||
<WorkspaceAvatar
|
||||
size={24}
|
||||
name={user.name}
|
||||
avatar={user.avatar_url}
|
||||
></WorkspaceAvatar>
|
||||
) : (
|
||||
<WorkspaceAvatar size={24}></WorkspaceAvatar>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||
function WorkspaceAvatar(props, ref) {
|
||||
const size = props.size || 20;
|
||||
const sizeStr = size + 'px';
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.avatar ? (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<picture>
|
||||
<img
|
||||
style={{ width: sizeStr, height: sizeStr }}
|
||||
src={props.avatar}
|
||||
alt=""
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||
borderRadius: '50%',
|
||||
textAlign: 'center',
|
||||
lineHeight: size + 'px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{props.name ? (
|
||||
props.name.substring(0, 1)
|
||||
) : (
|
||||
<AffineIcon fontSize={24} color={'#5438FF'} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default UserAvatar;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||
import type React from 'react';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
||||
import { forwardRef, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
||||
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
|
||||
import EditPage from './header-right-items/EditPage';
|
||||
import { HeaderShareMenu } from './header-right-items/ShareMenu';
|
||||
import SyncUser from './header-right-items/SyncUser';
|
||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
|
||||
import UserAvatar from './header-right-items/UserAvatar';
|
||||
import {
|
||||
StyledBrowserWarning,
|
||||
StyledCloseButton,
|
||||
@@ -56,10 +59,12 @@ export const enum HeaderRightItemName {
|
||||
ThemeModeSwitch = 'themeModeSwitch',
|
||||
SyncUser = 'syncUser',
|
||||
ShareMenu = 'shareMenu',
|
||||
EditPage = 'editPage',
|
||||
UserAvatar = 'userAvatar',
|
||||
}
|
||||
|
||||
type HeaderItem = {
|
||||
Component: React.FC<BaseHeaderProps>;
|
||||
Component: FC<BaseHeaderProps>;
|
||||
// todo: public workspace should be one of the flavour
|
||||
availableWhen: (
|
||||
workspace: AffineOfficialWorkspace,
|
||||
@@ -70,7 +75,6 @@ type HeaderItem = {
|
||||
}
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
[HeaderRightItemName.TrashButtonGroup]: {
|
||||
Component: TrashButtonGroup,
|
||||
@@ -90,18 +94,30 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
return currentPage?.meta.trash !== true;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.ShareMenu]: {
|
||||
Component: HeaderShareMenu,
|
||||
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
|
||||
return workspace.flavour !== WorkspaceFlavour.PUBLIC && !!currentPage;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.EditPage]: {
|
||||
Component: EditPage,
|
||||
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
|
||||
return isPublic;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.UserAvatar]: {
|
||||
Component: UserAvatar,
|
||||
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
|
||||
return isPublic;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.EditorOptionMenu]: {
|
||||
Component: EditorOptionMenu,
|
||||
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
|
||||
return !!currentPage && !isPublic && !isPreview;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.ShareMenu]: {
|
||||
Component: () => null,
|
||||
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
|
||||
Reference in New Issue
Block a user