feat(core): new share menu (#7838)

close AF-1224 AF-1216

![image](https://github.com/user-attachments/assets/204c408a-3dab-4068-86f6-e20abcfa863c)
This commit is contained in:
JimmFly
2024-08-19 05:51:05 +00:00
parent cfac3ebf1f
commit 4916eea24f
18 changed files with 698 additions and 408 deletions

View File

@@ -43,6 +43,7 @@
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",

View File

@@ -24,6 +24,7 @@ export * from './ui/scrollbar';
export * from './ui/skeleton'; export * from './ui/skeleton';
export * from './ui/switch'; export * from './ui/switch';
export * from './ui/table'; export * from './ui/table';
export * from './ui/tabs';
export * from './ui/toast'; export * from './ui/toast';
export * from './ui/tooltip'; export * from './ui/tooltip';
export * from './utils'; export * from './utils';

View File

@@ -0,0 +1 @@
export * from './tabs';

View File

@@ -0,0 +1,52 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const tabsRoot = style({
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '8px',
});
export const tabsList = style({
display: 'flex',
gap: '12px',
boxSizing: 'border-box',
position: 'relative',
'::after': {
content: '""',
position: 'absolute',
bottom: '0px',
width: '100%',
height: '1px',
backgroundColor: cssVarV2('layer/border'),
},
});
export const tabsTrigger = style({
all: 'unset',
fontWeight: 500,
padding: '6px 4px',
cursor: 'pointer',
fontSize: cssVar('fontSm'),
color: cssVarV2('text/secondary'),
borderBottom: '2px solid transparent',
selectors: {
'&[data-state="active"]': {
color: cssVarV2('text/primary'),
borderColor: cssVarV2('button/primary'),
},
},
});
export const tabsContent = style({
display: 'flex',
flexDirection: 'column',
selectors: {
'&[data-state="inactive"]': {
display: 'none',
},
},
});

View File

@@ -0,0 +1,80 @@
import * as TabsGroup from '@radix-ui/react-tabs';
import clsx from 'clsx';
import { forwardRef, type RefAttributes } from 'react';
import * as styles from './tabs.css';
export const TabsRoot = forwardRef<
HTMLDivElement,
TabsGroup.TabsProps & RefAttributes<HTMLDivElement>
>(({ children, className, ...props }, ref) => {
return (
<TabsGroup.Root
{...props}
ref={ref}
className={clsx(className, styles.tabsRoot)}
>
{children}
</TabsGroup.Root>
);
});
TabsRoot.displayName = 'TabsRoot';
export const TabsList = forwardRef<
HTMLDivElement,
TabsGroup.TabsListProps & RefAttributes<HTMLDivElement>
>(({ children, className, ...props }, ref) => {
return (
<TabsGroup.List
{...props}
ref={ref}
className={clsx(className, styles.tabsList)}
>
{children}
</TabsGroup.List>
);
});
TabsList.displayName = 'TabsList';
export const TabsTrigger = forwardRef<
HTMLButtonElement,
TabsGroup.TabsTriggerProps & RefAttributes<HTMLButtonElement>
>(({ children, className, ...props }, ref) => {
return (
<TabsGroup.Trigger
{...props}
ref={ref}
className={clsx(className, styles.tabsTrigger)}
>
{children}
</TabsGroup.Trigger>
);
});
TabsTrigger.displayName = 'TabsTrigger';
export const TabsContent = forwardRef<
HTMLDivElement,
TabsGroup.TabsContentProps & RefAttributes<HTMLDivElement>
>(({ children, className, ...props }, ref) => {
return (
<TabsGroup.Content
{...props}
ref={ref}
className={clsx(className, styles.tabsContent)}
>
{children}
</TabsGroup.Content>
);
});
TabsContent.displayName = 'TabsContent';
export const Tabs = {
Root: TabsRoot,
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
};

View File

@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const headerStyle = style({ export const headerStyle = style({
display: 'flex', display: 'flex',
@@ -9,34 +10,133 @@ export const headerStyle = style({
padding: '0 4px', padding: '0 4px',
gap: '4px', gap: '4px',
}); });
export const content = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
});
export const menuStyle = style({ export const menuStyle = style({
width: '410px', width: '390px',
height: 'auto', height: 'auto',
padding: '12px', padding: '12px',
transform: 'translateX(-10px)', });
export const menuTriggerStyle = style({
width: '150px',
padding: '4px 10px',
justifyContent: 'space-between',
}); });
export const menuItemStyle = style({ export const menuItemStyle = style({
padding: '4px', padding: '4px',
});
export const publicItemRowStyle = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});
export const publicMenuItemPrefixStyle = style({
fontSize: cssVar('fontH5'),
color: cssVarV2('icon/primary'),
marginRight: '8px',
});
export const DoneIconStyle = style({
color: cssVarV2('button/primary'),
fontSize: cssVar('fontH5'),
marginLeft: '8px',
});
export const exportItemStyle = style({
padding: '4px',
transition: 'all 0.3s',
});
export const copyLinkContainerStyle = style({
padding: '4px',
display: 'flex',
alignItems: 'center',
width: '100%',
position: 'relative',
});
export const copyLinkButtonStyle = style({
flex: 1,
padding: '4px 12px',
paddingRight: '6px',
borderRight: 'none',
borderTopRightRadius: '0',
borderBottomRightRadius: '0',
color: 'transparent',
position: 'initial',
});
export const copyLinkLabelContainerStyle = style({
width: '100%',
padding: '4px 12px',
borderRight: 'none',
borderTopRightRadius: '0',
borderBottomRightRadius: '0',
position: 'relative',
});
export const copyLinkLabelStyle = style({
position: 'absolute',
textAlign: 'end',
top: '50%',
left: '50%',
transform: 'translateX(-50%) translateY(-50%)',
lineHeight: '20px',
color: cssVarV2('text/pureWhite'),
});
export const copyLinkShortcutStyle = style({
position: 'absolute',
textAlign: 'end',
top: '50%',
right: '52px',
transform: 'translateY(-50%)',
opacity: 0.5,
lineHeight: '20px',
color: cssVarV2('text/pureWhite'),
});
export const copyLinkTriggerStyle = style({
width: '100%',
padding: '4px 12px',
paddingLeft: '4px',
display: 'flex',
border: `1px solid ${cssVarV2('button/innerBlackBorder')}`,
flex: 0,
justifyContent: 'end',
alignItems: 'center',
gap: '4px',
position: 'relative',
backgroundColor: cssVarV2('button/primary'),
color: cssVarV2('button/pureWhiteText'),
borderLeft: 'none',
borderTopLeftRadius: '0',
borderBottomLeftRadius: '0',
':hover': {
backgroundColor: cssVarV2('button/primary'),
color: cssVarV2('button/pureWhiteText'),
},
'::before': {
content: '""',
position: 'absolute',
left: '0',
top: '0',
height: '100%',
width: '1px',
backgroundColor: cssVarV2('button/innerBlackBorder'),
},
});
globalStyle(`${copyLinkTriggerStyle} svg`, {
color: cssVarV2('button/pureWhiteText'),
transform: 'translateX(2px)',
});
export const copyLinkMenuItemStyle = style({
padding: '4px',
transition: 'all 0.3s', transition: 'all 0.3s',
}); });
export const descriptionStyle = style({ export const descriptionStyle = style({
wordWrap: 'break-word', wordWrap: 'break-word',
fontSize: cssVar('fontXs'), fontSize: cssVar('fontXs'),
lineHeight: '20px', lineHeight: '20px',
color: cssVar('textSecondaryColor'), color: cssVarV2('text/secondary'),
textAlign: 'left', textAlign: 'left',
padding: '0 6px', padding: '0 6px',
}); });
export const buttonStyle = style({
marginTop: '18px',
});
export const actionsStyle = style({
display: 'flex',
gap: '9px',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
});
export const containerStyle = style({ export const containerStyle = style({
display: 'flex', display: 'flex',
width: '100%', width: '100%',
@@ -46,25 +146,13 @@ export const containerStyle = style({
export const indicatorContainerStyle = style({ export const indicatorContainerStyle = style({
position: 'relative', position: 'relative',
}); });
export const inputButtonRowStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '16px',
});
export const titleContainerStyle = style({ export const titleContainerStyle = style({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', fontSize: cssVar('fontXs'),
fontSize: cssVar('fontSm'), color: cssVarV2('text/secondary'),
fontWeight: 500, fontWeight: 400,
lineHeight: '22px', padding: '8px 4px 0 4px',
padding: '0 4px',
});
export const subTitleStyle = style({
fontSize: cssVar('fontSm'),
fontWeight: 500,
lineHeight: '22px',
}); });
export const columnContainerStyle = style({ export const columnContainerStyle = style({
display: 'flex', display: 'flex',
@@ -78,33 +166,21 @@ export const rowContainerStyle = style({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
gap: '12px',
padding: '4px', padding: '4px',
}); });
export const radioButtonGroup = style({ export const labelStyle = style({
display: 'flex', fontSize: cssVar('fontSm'),
justifyContent: 'flex-end', fontWeight: 500,
padding: '2px',
minWidth: '154px',
maxWidth: '250px',
});
export const radioButton = style({
color: cssVar('textSecondaryColor'),
selectors: {
'&[data-state="checked"]': {
color: cssVar('textPrimaryColor'),
},
},
}); });
export const disableSharePage = style({ export const disableSharePage = style({
color: cssVar('errorColor'), color: cssVarV2('button/error'),
}); });
export const localSharePage = style({ export const localSharePage = style({
padding: '12px 8px', padding: '12px 8px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
borderRadius: '8px', borderRadius: '8px',
backgroundColor: cssVar('backgroundSecondaryColor'), backgroundColor: cssVarV2('layer/background/secondary'),
minHeight: '84px', minHeight: '84px',
position: 'relative', position: 'relative',
}); });
@@ -117,12 +193,6 @@ export const cloudSvgContainer = style({
bottom: '0', bottom: '0',
right: '0', right: '0',
}); });
export const shareIconStyle = style({
fontSize: '16px',
color: cssVar('iconColor'),
display: 'flex',
alignItems: 'center',
});
export const shareLinkStyle = style({ export const shareLinkStyle = style({
padding: '4px', padding: '4px',
fontSize: cssVar('fontXs'), fontSize: cssVar('fontXs'),
@@ -132,17 +202,38 @@ export const shareLinkStyle = style({
gap: '4px', gap: '4px',
}); });
globalStyle(`${shareLinkStyle} > span`, { globalStyle(`${shareLinkStyle} > span`, {
color: cssVar('linkColor'), color: cssVarV2('text/link'),
}); });
globalStyle(`${shareLinkStyle} > div > svg`, { globalStyle(`${shareLinkStyle} > div > svg`, {
color: cssVar('linkColor'), color: cssVarV2('text/link'),
}); });
export const shareButton = style({ export const buttonContainer = style({
display: 'flex',
alignItems: 'center',
gap: '4px',
fontWeight: 500,
});
export const button = style({
padding: '6px 8px',
height: 32, height: 32,
padding: '0px 8px',
}); });
export const shortcutStyle = style({ export const shortcutStyle = style({
fontSize: cssVar('fontXs'), fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'), color: cssVarV2('text/secondary'),
fontWeight: 400, fontWeight: 400,
}); });
export const openWorkspaceSettingsStyle = style({
color: cssVarV2('text/link'),
fontSize: cssVar('fontXs'),
fontWeight: 500,
display: 'flex',
gap: '8px',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
padding: '4px',
cursor: 'pointer',
});
globalStyle(`${openWorkspaceSettingsStyle} svg`, {
color: cssVarV2('text/link'),
});

View File

@@ -1,80 +1,30 @@
import { MenuIcon, MenuItem } from '@affine/component';
import { Divider } from '@affine/component/ui/divider';
import { ExportMenuItems } from '@affine/core/components/page-list'; import { ExportMenuItems } from '@affine/core/components/page-list';
import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { CopyIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import * as styles from './index.css'; import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu'; import type { ShareMenuProps } from './share-menu';
export const ShareExport = ({ export const ShareExport = ({ currentPage }: ShareMenuProps) => {
workspaceMetadata: workspace,
currentPage,
}: ShareMenuProps) => {
const t = useI18n(); const t = useI18n();
const editor = useService(EditorService).editor; const editor = useService(EditorService).editor;
const workspaceId = workspace.id;
const pageId = currentPage.id;
const { sharingUrl, onClickCopyLink } = useSharingUrl({
workspaceId,
pageId,
urlType: 'workspace',
});
const exportHandler = useExportPage(currentPage); const exportHandler = useExportPage(currentPage);
const currentMode = useLiveData(editor.mode$); const currentMode = useLiveData(editor.mode$);
const isMac = environment.isBrowser && environment.isMacOs;
return ( return (
<> <>
<div className={styles.titleContainerStyle}>
{t['com.affine.share-menu.ShareViaExport']()}
</div>
<div className={styles.descriptionStyle}> <div className={styles.descriptionStyle}>
{t['com.affine.share-menu.ShareViaExportDescription']()} {t['com.affine.share-menu.ShareViaExportDescription']()}
</div> </div>
<div> <div>
<ExportMenuItems <ExportMenuItems
exportHandler={exportHandler} exportHandler={exportHandler}
className={styles.menuItemStyle} className={styles.exportItemStyle}
pageMode={currentMode} pageMode={currentMode}
/> />
</div> </div>
{workspace.flavour !== WorkspaceFlavour.LOCAL ? (
<div className={styles.columnContainerStyle}>
<Divider size="thinner" />
<div className={styles.titleContainerStyle}>
{t['com.affine.share-menu.share-privately']()}
</div>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.share-privately.description']()}
</div>
<div>
<MenuItem
className={styles.shareLinkStyle}
onSelect={onClickCopyLink}
block
disabled={!sharingUrl}
preFix={
<MenuIcon>
<CopyIcon fontSize={16} />
</MenuIcon>
}
endFix={
<div className={styles.shortcutStyle}>
{isMac ? '⌘ + ⌥ + C' : 'Ctrl + Shift + C'}
</div>
}
>
{t['com.affine.share-menu.copy-private-link']()}
</MenuItem>
</div>
</div>
) : null}
</> </>
); );
}; };

View File

@@ -1,10 +1,10 @@
import { Tabs, Tooltip } from '@affine/component';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { ShareInfoService } from '@affine/core/modules/share-doc'; import { ShareInfoService } from '@affine/core/modules/share-doc';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { WebIcon } from '@blocksuite/icons/rc'; import { LockIcon, PublishIcon } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { import {
useLiveData, useLiveData,
@@ -28,17 +28,20 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
const t = useI18n(); const t = useI18n();
return ( return (
<div className={styles.containerStyle}> <div className={styles.containerStyle}>
<div className={styles.headerStyle}> <Tabs.Root defaultValue="share">
<div className={styles.shareIconStyle}> <Tabs.List>
<WebIcon /> <Tabs.Trigger value="share">
</div> {t['com.affine.share-menu.shareButton']()}
{t['com.affine.share-menu.SharePage']()} </Tabs.Trigger>
</div> <Tabs.Trigger value="export">{t['Export']()}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="share">
<SharePage {...props} /> <SharePage {...props} />
<div className={styles.columnContainerStyle}> </Tabs.Content>
<Divider size="thinner" /> <Tabs.Content value="export">
</div>
<ShareExport {...props} /> <ShareExport {...props} />
</Tabs.Content>
</Tabs.Root>
</div> </div>
); );
}; };
@@ -56,11 +59,20 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
}, [shareInfoService]); }, [shareInfoService]);
return ( return (
<Button ref={ref} className={styles.shareButton} variant="primary"> <Tooltip
{shared content={
? t['com.affine.share-menu.sharedButton']() shared
: t['com.affine.share-menu.shareButton']()} ? t['com.affine.share-menu.option.link.readonly.description']()
: t['com.affine.share-menu.option.link.no-access.description']()
}
>
<Button ref={ref} className={styles.button}>
<div className={styles.buttonContainer}>
{shared ? <PublishIcon fontSize={16} /> : <LockIcon fontSize={16} />}
{t['com.affine.share-menu.shareButton']()}
</div>
</Button> </Button>
</Tooltip>
); );
}); });
@@ -71,6 +83,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
contentOptions={{ contentOptions={{
className: styles.menuStyle, className: styles.menuStyle,
['data-testid' as string]: 'local-share-menu', ['data-testid' as string]: 'local-share-menu',
align: 'end',
}} }}
rootOptions={{ rootOptions={{
modal: false, modal: false,
@@ -91,6 +104,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
contentOptions={{ contentOptions={{
className: styles.menuStyle, className: styles.menuStyle,
['data-testid' as string]: 'cloud-share-menu', ['data-testid' as string]: 'cloud-share-menu',
align: 'end',
}} }}
rootOptions={{ rootOptions={{
modal: false, modal: false,

View File

@@ -1,27 +1,32 @@
import { Input, notify, RadioGroup, Skeleton, Switch } from '@affine/component'; import { notify, Skeleton } from '@affine/component';
import { PublicLinkDisableModal } from '@affine/component/disable-public-link'; import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu'; import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url'; import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { ServerConfigService } from '@affine/core/modules/cloud'; import { ServerConfigService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { ShareInfoService } from '@affine/core/modules/share-doc'; import { ShareInfoService } from '@affine/core/modules/share-doc';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { PublicPageMode } from '@affine/graphql'; import { PublicPageMode } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
ArrowRightSmallIcon, CollaborationIcon,
DoneIcon,
EdgelessIcon,
LinkIcon,
LockIcon,
PageIcon,
SingleSelectSelectSolidIcon, SingleSelectSelectSolidIcon,
ViewIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import { import { useLiveData, useService } from '@toeverything/infra';
type DocMode,
DocService,
useLiveData,
useService,
} from '@toeverything/infra';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { Suspense, useEffect, useMemo, useState } from 'react'; import { useSetAtom } from 'jotai';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { CloudSvg } from '../cloud-svg'; import { CloudSvg } from '../cloud-svg';
@@ -54,11 +59,12 @@ export const LocalSharePage = (props: ShareMenuProps) => {
); );
}; };
export const AffineSharePage = (props: ShareMenuProps) => { export const AFFiNESharePage = (props: ShareMenuProps) => {
const t = useI18n();
const { const {
workspaceMetadata: { id: workspaceId }, workspaceMetadata: { id: workspaceId },
} = props; } = props;
const doc = useService(DocService).doc; const editor = useService(EditorService).editor;
const shareInfoService = useService(ShareInfoService); const shareInfoService = useService(ShareInfoService);
const serverConfig = useService(ServerConfigService).serverConfig; const serverConfig = useService(ServerConfigService).serverConfig;
useEffect(() => { useEffect(() => {
@@ -71,44 +77,28 @@ export const AffineSharePage = (props: ShareMenuProps) => {
isSharedPage === null || sharedMode === null || baseUrl === null; isSharedPage === null || sharedMode === null || baseUrl === null;
const [showDisable, setShowDisable] = useState(false); const [showDisable, setShowDisable] = useState(false);
const currentDocMode = useLiveData(doc.primaryMode$); const currentDocMode = useLiveData(editor.mode$);
const mode = useMemo(() => { const permissionService = useService(WorkspacePermissionService);
if (isSharedPage && sharedMode) { const isOwner = useLiveData(permissionService.permission.isOwner$);
// if it's a shared page, use the share mode const setSettingModalAtom = useSetAtom(openSettingModalAtom);
return sharedMode.toLowerCase() as DocMode;
const onOpenWorkspaceSettings = useCallback(() => {
setSettingModalAtom({
open: true,
activeTab: 'workspace:preference',
workspaceMetadata: props.workspaceMetadata,
});
}, [props.workspaceMetadata, setSettingModalAtom]);
const onClickAnyoneReadOnlyShare = useAsyncCallback(async () => {
if (isSharedPage) {
return;
} }
// default to page mode
return currentDocMode;
}, [currentDocMode, isSharedPage, sharedMode]);
const { sharingUrl, onClickCopyLink } = useSharingUrl({
workspaceId,
pageId: doc.id,
urlType: 'share',
});
const t = useI18n();
const modeOptions = useMemo(
() => [
{ value: 'page', label: t['com.affine.pageMode.page']() },
{
value: 'edgeless',
label: t['com.affine.pageMode.edgeless'](),
},
],
[t]
);
const onClickCreateLink = useAsyncCallback(async () => {
try { try {
await shareInfoService.shareInfo.enableShare( // TODO(@JimmFly): remove mode when we have a better way to handle it
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page await shareInfoService.shareInfo.enableShare(PublicPageMode.Page);
); track.$.sharePanel.$.createShareLink();
track.$.sharePanel.$.createShareLink({
mode,
});
notify.success({ notify.success({
title: title:
t[ t[
@@ -121,11 +111,6 @@ export const AffineSharePage = (props: ShareMenuProps) => {
style: 'normal', style: 'normal',
icon: <SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />, icon: <SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />,
}); });
if (sharingUrl) {
navigator.clipboard.writeText(sharingUrl).catch(err => {
console.error(err);
});
}
} catch (err) { } catch (err) {
notify.error({ notify.error({
title: title:
@@ -139,7 +124,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
}); });
console.error(err); console.error(err);
} }
}, [mode, shareInfoService.shareInfo, sharingUrl, t]); }, [isSharedPage, shareInfoService.shareInfo, t]);
const onDisablePublic = useAsyncCallback(async () => { const onDisablePublic = useAsyncCallback(async () => {
try { try {
@@ -170,46 +155,29 @@ export const AffineSharePage = (props: ShareMenuProps) => {
setShowDisable(false); setShowDisable(false);
}, [shareInfoService, t]); }, [shareInfoService, t]);
const onShareModeChange = useAsyncCallback( const onClickDisable = useCallback(() => {
async (value: DocMode) => {
try {
if (isSharedPage) { if (isSharedPage) {
await shareInfoService.shareInfo.changeShare( setShowDisable(true);
value === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
);
notify.success({
title:
t[
'com.affine.share-menu.confirm-modify-mode.notification.success.title'
](),
message: t[
'com.affine.share-menu.confirm-modify-mode.notification.success.message'
]({
preMode: value === 'edgeless' ? t['Page']() : t['Edgeless'](),
currentMode: value === 'edgeless' ? t['Edgeless']() : t['Page'](),
}),
style: 'normal',
icon: (
<SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />
),
});
} }
} catch (err) { }, [isSharedPage]);
notify.error({
title: const isMac = environment.isBrowser && environment.isMacOs;
t[
'com.affine.share-menu.confirm-modify-mode.notification.fail.title' const { onClickCopyLink } = useSharingUrl({
](), workspaceId,
message: pageId: editor.doc.id,
t[
'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
](),
}); });
console.error(err);
} const onCopyPageLink = useCallback(() => {
}, onClickCopyLink('page');
[isSharedPage, shareInfoService.shareInfo, t] }, [onClickCopyLink]);
); const onCopyEdgelessLink = useCallback(() => {
onClickCopyLink('edgeless');
}, [onClickCopyLink]);
const onCopyBlockLink = useCallback(() => {
// TODO(@JimmFly): handle frame
onClickCopyLink();
}, [onClickCopyLink]);
if (isLoading) { if (isLoading) {
// TODO(@eyhn): loading and error UI // TODO(@eyhn): loading and error UI
@@ -222,110 +190,175 @@ export const AffineSharePage = (props: ShareMenuProps) => {
} }
return ( return (
<> <div className={styles.content}>
<div className={styles.titleContainerStyle}> <div className={styles.titleContainerStyle}>
{t['com.affine.share-menu.publish-to-web']()} {isSharedPage
? t['com.affine.share-menu.option.link.readonly.description']()
: t['com.affine.share-menu.option.link.no-access.description']()}
</div> </div>
<div className={styles.columnContainerStyle}> <div className={styles.columnContainerStyle}>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.publish-to-web.description']()}
</div>
</div>
<div className={styles.rowContainerStyle}> <div className={styles.rowContainerStyle}>
<Input <div className={styles.labelStyle}>
inputStyle={{ {t['com.affine.share-menu.option.link.label']()}
color: 'var(--affine-text-secondary-color)', </div>
fontSize: 'var(--affine-font-xs)', <Menu
lineHeight: '20px', contentOptions={{
align: 'end',
}} }}
value={(isSharedPage && sharingUrl) || `${baseUrl}/...`} items={
readOnly <>
/> <MenuItem
{isSharedPage ? ( preFix={
<Button <LockIcon className={styles.publicMenuItemPrefixStyle} />
onClick={onClickCopyLink} }
data-testid="share-menu-copy-link-button" onSelect={onClickDisable}
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }} className={styles.menuItemStyle}
disabled={!sharingUrl}
> >
{t.Copy()} <div className={styles.publicItemRowStyle}>
</Button> <div>
) : ( {t['com.affine.share-menu.option.link.no-access']()}
<Button </div>
onClick={onClickCreateLink} {!isSharedPage && (
variant="primary" <DoneIcon className={styles.DoneIconStyle} />
data-testid="share-menu-create-link-button"
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
>
{t.Create()}
</Button>
)} )}
</div> </div>
<div className={styles.rowContainerStyle}> </MenuItem>
<div className={styles.subTitleStyle}> <MenuItem
{t['com.affine.share-menu.ShareMode']()} preFix={
</div> <ViewIcon className={styles.publicMenuItemPrefixStyle} />
}
className={styles.menuItemStyle}
onSelect={onClickAnyoneReadOnlyShare}
data-testid="share-link-menu-enable-share"
>
<div className={styles.publicItemRowStyle}>
<div> <div>
<RadioGroup {t['com.affine.share-menu.option.link.readonly']()}
className={styles.radioButtonGroup}
value={mode}
onChange={onShareModeChange}
items={modeOptions}
/>
</div> </div>
{isSharedPage && (
<DoneIcon className={styles.DoneIconStyle} />
)}
</div>
</MenuItem>
</>
}
>
<MenuTrigger
className={styles.menuTriggerStyle}
data-testid="share-link-menu-trigger"
>
{isSharedPage
? t['com.affine.share-menu.option.link.readonly']()
: t['com.affine.share-menu.option.link.no-access']()}
</MenuTrigger>
</Menu>
</div> </div>
{isSharedPage ? (
<>
{runtimeConfig.enableEnhanceShareMode && (
<>
<div className={styles.rowContainerStyle}> <div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>Link expires</div> <div className={styles.labelStyle}>
<div> {t['com.affine.share-menu.option.permission.label']()}
<Menu items={<MenuItem>Never</MenuItem>}> </div>
<MenuTrigger>Never</MenuTrigger> <Menu
contentOptions={{
align: 'end',
}}
items={
<MenuItem>
{t['com.affine.share-menu.option.permission.can-edit']()}
</MenuItem>
}
>
<MenuTrigger className={styles.menuTriggerStyle} disabled>
{t['com.affine.share-menu.option.permission.can-edit']()}
</MenuTrigger>
</Menu> </Menu>
</div> </div>
</div> </div>
<div className={styles.rowContainerStyle}> {isOwner && (
<div className={styles.subTitleStyle}> <div
{'Show "Created with AFFiNE"'} className={styles.openWorkspaceSettingsStyle}
</div> onClick={onOpenWorkspaceSettings}
<div>
<Switch />
</div>
</div>
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>
Search engine indexing
</div>
<div>
<Switch />
</div>
</div>
</>
)}
<MenuItem
endFix={<ArrowRightSmallIcon />}
block
type="danger"
className={styles.menuItemStyle}
onSelect={e => {
e.preventDefault();
setShowDisable(true);
}}
> >
<div className={styles.disableSharePage}> <CollaborationIcon fontSize={16} />
{t['Disable Public Link']()} {t['com.affine.share-menu.navigate.workspace']()}
</div> </div>
)}
<div className={styles.copyLinkContainerStyle}>
<Button
className={styles.copyLinkButtonStyle}
onClick={onCopyBlockLink}
variant="primary"
withoutHover
>
<span className={styles.copyLinkLabelStyle}>
{t['com.affine.share-menu.copy']()}
</span>
<span className={styles.copyLinkShortcutStyle}>
{isMac ? '⌘ + ⌥ + C' : 'Ctrl + Shift + C'}
</span>
{t['com.affine.share-menu.copy']()}
</Button>
<Menu
contentOptions={{
align: 'end',
}}
items={
<>
<MenuItem
preFix={
<PageIcon className={styles.publicMenuItemPrefixStyle} />
}
className={styles.menuItemStyle}
onSelect={onCopyPageLink}
data-testid="share-link-menu-copy-page"
>
{t['com.affine.share-menu.copy.page']()}
</MenuItem> </MenuItem>
<MenuItem
preFix={
<EdgelessIcon className={styles.publicMenuItemPrefixStyle} />
}
className={styles.menuItemStyle}
onSelect={onCopyEdgelessLink}
data-testid="share-link-menu-copy-edgeless"
>
{t['com.affine.share-menu.copy.edgeless']()}
</MenuItem>
<MenuItem
preFix={
<LinkIcon className={styles.publicMenuItemPrefixStyle} />
}
className={styles.menuItemStyle}
onSelect={onCopyBlockLink}
>
{t['com.affine.share-menu.copy.block']()}
</MenuItem>
{currentDocMode === 'edgeless' && (
<MenuItem
preFix={
<LinkIcon className={styles.publicMenuItemPrefixStyle} />
}
className={styles.menuItemStyle}
onSelect={onCopyBlockLink}
>
{t['com.affine.share-menu.copy.frame']()}
</MenuItem>
)}
</>
}
>
<MenuTrigger
className={styles.copyLinkTriggerStyle}
data-testid="share-menu-copy-link-button"
/>
</Menu>
</div>
<PublicLinkDisableModal <PublicLinkDisableModal
open={showDisable} open={showDisable}
onConfirm={onDisablePublic} onConfirm={onDisablePublic}
onOpenChange={setShowDisable} onOpenChange={setShowDisable}
/> />
</> </div>
) : null}
</>
); );
}; };
@@ -339,7 +372,7 @@ export const SharePage = (props: ShareMenuProps) => {
// TODO(@eyhn): refactor this part // TODO(@eyhn): refactor this part
<ErrorBoundary fallback={null}> <ErrorBoundary fallback={null}>
<Suspense> <Suspense>
<AffineSharePage {...props} /> <AFFiNESharePage {...props} />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -48,6 +48,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
}: PageDetailEditorProps & { page: BlockSuiteDoc }) { }: PageDetailEditorProps & { page: BlockSuiteDoc }) {
const editor = useService(EditorService).editor; const editor = useService(EditorService).editor;
const mode = useLiveData(editor.mode$); const mode = useLiveData(editor.mode$);
const isSharedMode = editor.isSharedMode; const isSharedMode = editor.isSharedMode;
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();

View File

@@ -19,10 +19,10 @@ export function useRegisterCopyLinkCommands({
const isActiveView = useIsActiveView(); const isActiveView = useIsActiveView();
const workspaceId = workspaceMeta.id; const workspaceId = workspaceMeta.id;
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD; const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
const { onClickCopyLink } = useSharingUrl({ const { onClickCopyLink } = useSharingUrl({
workspaceId, workspaceId,
pageId: docId, pageId: docId,
urlType: 'workspace',
}); });
useEffect(() => { useEffect(() => {
@@ -39,8 +39,7 @@ export function useRegisterCopyLinkCommands({
label: '', label: '',
icon: null, icon: null,
run() { run() {
track.$.cmdk.general.copyShareLink({ type: 'private' }); track.$.cmdk.general.copyShareLink();
isActiveView && isCloud && onClickCopyLink(); isActiveView && isCloud && onClickCopyLink();
}, },
}) })

View File

@@ -2,61 +2,115 @@ import { notify } from '@affine/component';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { Disposable } from '@blocksuite/global/utils'; import type { BaseSelection } from '@blocksuite/block-std';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { type DocMode } from '@toeverything/infra';
import { useCallback } from 'react';
import { useActiveBlocksuiteEditor } from '../use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '../use-block-suite-editor';
type UrlType = 'share' | 'workspace';
type UseSharingUrl = { type UseSharingUrl = {
workspaceId: string; workspaceId: string;
pageId: string; pageId: string;
urlType: UrlType; shareMode?: DocMode;
blockIds?: string[];
elementIds?: string[];
xywh?: string; // not needed currently
}; };
/**
* to generate a url like https://app.affine.pro/workspace/workspaceId/docId?mode=DocMode?element=seletedBlockid#seletedBlockid
*/
const generateUrl = ({ const generateUrl = ({
workspaceId, workspaceId,
pageId, pageId,
urlType, blockIds,
blockId, elementIds,
}: UseSharingUrl & { blockId?: string }) => { shareMode,
// to generate a private url like https://app.affine.app/workspace/123/456 xywh, // not needed currently
// or https://app.affine.app/workspace/123/456#block-123 }: UseSharingUrl) => {
// Base URL construction
// to generate a public url like https://app.affine.app/share/123/456
const baseUrl = getAffineCloudBaseUrl(); const baseUrl = getAffineCloudBaseUrl();
if (!baseUrl) return null; if (!baseUrl) return null;
try { try {
return new URL( const url = new URL(`${baseUrl}/workspace/${workspaceId}/${pageId}`);
`${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' && blockId ? `#${blockId}` : ''}` if (shareMode) {
).toString(); url.searchParams.append('mode', shareMode);
}
// TODO(@JimmFly): use query string to handle blockIds
if (blockIds && blockIds.length > 0) {
// hash is used to store blockIds
url.hash = blockIds.join(',');
}
if (elementIds && elementIds.length > 0) {
url.searchParams.append('element', elementIds.join(','));
}
if (xywh) {
url.searchParams.append('xywh', xywh);
}
return url.toString();
} catch (e) { } catch (e) {
return null; return null;
} }
}; };
export const useSharingUrl = ({ const getShareLinkType = ({
workspaceId, shareMode,
pageId, blockIds,
urlType, elementIds,
}: UseSharingUrl) => { }: {
const t = useI18n(); shareMode?: DocMode;
const [blockId, setBlockId] = useState<string>(''); blockIds?: string[];
const [editor] = useActiveBlocksuiteEditor(); elementIds?: string[];
const sharingUrl = useMemo( }) => {
() => if (shareMode === 'page') {
generateUrl({ return 'doc';
workspaceId, } else if (shareMode === 'edgeless') {
pageId, return 'whiteboard';
urlType, } else if (blockIds && blockIds.length > 0) {
blockId: blockId.length > 0 ? blockId : undefined, return 'block';
}), } else if (elementIds && elementIds.length > 0) {
[workspaceId, pageId, urlType, blockId] return 'element';
); } else {
return 'default';
}
};
const onClickCopyLink = useCallback(() => { const getSelectionIds = (selections?: BaseSelection[]) => {
if (!selections || selections.length === 0) {
return { blockIds: [], elementIds: [] };
}
const blockIds: string[] = [];
const elementIds: string[] = [];
// TODO(@JimmFly): handle multiple selections and elementIds
if (selections[0].type === 'block') {
blockIds.push(selections[0].blockId);
}
return { blockIds, elementIds };
};
export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
const t = useI18n();
const [editor] = useActiveBlocksuiteEditor();
const onClickCopyLink = useCallback(
(shareMode?: DocMode) => {
const selectManager = editor?.host?.selection;
const selections = selectManager?.value;
const { blockIds, elementIds } = getSelectionIds(selections);
const sharingUrl = generateUrl({
workspaceId,
pageId,
blockIds,
elementIds,
shareMode, // if view is not provided, use the current view
});
const type = getShareLinkType({
shareMode,
blockIds,
elementIds,
});
if (sharingUrl) { if (sharingUrl) {
navigator.clipboard navigator.clipboard
.writeText(sharingUrl) .writeText(sharingUrl)
@@ -69,45 +123,17 @@ export const useSharingUrl = ({
console.error(err); console.error(err);
}); });
track.$.sharePanel.$.copyShareLink({ track.$.sharePanel.$.copyShareLink({
type: urlType === 'share' ? 'public' : 'private', type,
}); });
} else { } else {
notify.error({ notify.error({
title: 'Network not available', title: 'Network not available',
}); });
} }
}, [sharingUrl, t, urlType]); },
[editor, pageId, t, workspaceId]
useEffect(() => { );
let disposable: Disposable | null = null;
const selectManager = editor?.host?.selection;
if (urlType !== 'workspace' || !selectManager) {
return;
}
// if the block is already selected, set the blockId
const currentBlockSelection = selectManager.find('block');
if (currentBlockSelection) {
setBlockId(`#${currentBlockSelection.blockId}`);
}
disposable = selectManager.slots.changed.on(selections => {
setBlockId(prev => {
if (selections[0] && selections[0].type === 'block') {
return `#${selections[0].blockId}`;
} else if (prev.length > 0) {
return '';
} else {
return prev;
}
});
});
return () => {
disposable?.dispose();
};
}, [editor?.host?.selection, urlType]);
return { return {
sharingUrl,
onClickCopyLink, onClickCopyLink,
}; };
}; };

View File

@@ -376,7 +376,9 @@ export type EventArgs = {
createDoc: { mode?: 'edgeless' | 'page' }; createDoc: { mode?: 'edgeless' | 'page' };
switchPageMode: { mode: 'edgeless' | 'page' }; switchPageMode: { mode: 'edgeless' | 'page' };
createShareLink: { mode: 'edgeless' | 'page' }; createShareLink: { mode: 'edgeless' | 'page' };
copyShareLink: { type: 'public' | 'private' }; copyShareLink: {
type: 'default' | 'doc' | 'whiteboard' | 'block' | 'element';
};
export: { type: string }; export: { type: string };
}; };

View File

@@ -29,6 +29,7 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import { ShareFooter } from './share-footer'; import { ShareFooter } from './share-footer';
@@ -50,6 +51,18 @@ export const SharePage = ({
const error = useLiveData(shareReaderService.reader.error$); const error = useLiveData(shareReaderService.reader.error$);
const data = useLiveData(shareReaderService.reader.data$); const data = useLiveData(shareReaderService.reader.data$);
const location = useLocation();
const [mode, setMode] = useState<DocMode | null>(null);
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const queryStringMode = searchParams.get('mode') as DocMode | null;
if (queryStringMode && ['edgeless', 'page'].includes(queryStringMode)) {
setMode(queryStringMode);
}
}, [location.search]);
useEffect(() => { useEffect(() => {
shareReaderService.reader.loadShare({ workspaceId, docId }); shareReaderService.reader.loadShare({ workspaceId, docId });
}, [shareReaderService, docId, workspaceId]); }, [shareReaderService, docId, workspaceId]);
@@ -70,7 +83,7 @@ export const SharePage = ({
docId={data.docId} docId={data.docId}
workspaceBinary={data.workspaceBinary} workspaceBinary={data.workspaceBinary}
docBinary={data.docBinary} docBinary={data.docBinary}
publishMode={data.publishMode} publishMode={mode || data.publishMode}
/> />
); );
} else { } else {

View File

@@ -1279,6 +1279,19 @@
"com.affine.share-menu.ShareWithLink": "Share with link", "com.affine.share-menu.ShareWithLink": "Share with link",
"com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your doc in the form od a document", "com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your doc in the form od a document",
"com.affine.share-menu.SharedPage": "Shared doc", "com.affine.share-menu.SharedPage": "Shared doc",
"com.affine.share-menu.option.link.label": "Anyone with the link",
"com.affine.share-menu.option.link.readonly": "Read Only",
"com.affine.share-menu.option.link.readonly.description": "Anyone can access this link.",
"com.affine.share-menu.option.link.no-access": "No Access",
"com.affine.share-menu.option.link.no-access.description": "Only workspace members can access this link.",
"com.affine.share-menu.option.permission.label": "Members in Workspace",
"com.affine.share-menu.option.permission.can-edit": "Can Edit",
"com.affine.share-menu.navigate.workspace": "Manage Workspace Members",
"com.affine.share-menu.copy": "Copy Link",
"com.affine.share-menu.copy.page": "Copy Link to Page Mode",
"com.affine.share-menu.copy.edgeless": "Copy Link to Edgeless Mode",
"com.affine.share-menu.copy.block": "Copy Link to Selected Block",
"com.affine.share-menu.copy.frame": "Copy Link to Selected Frame",
"com.affine.share-menu.confirm-modify-mode.notification.fail.message": "Please try again later.", "com.affine.share-menu.confirm-modify-mode.notification.fail.message": "Please try again later.",
"com.affine.share-menu.confirm-modify-mode.notification.fail.title": "Failed to modify", "com.affine.share-menu.confirm-modify-mode.notification.fail.title": "Failed to modify",
"com.affine.share-menu.confirm-modify-mode.notification.success.message": "You have changed the public link from {{preMode}} Mode to {{currentMode}} Mode.", "com.affine.share-menu.confirm-modify-mode.notification.success.message": "You have changed the public link from {{preMode}} Mode to {{currentMode}} Mode.",

View File

@@ -2,6 +2,7 @@ import { skipOnboarding, test } from '@affine-test/kit/playwright';
import { import {
createRandomUser, createRandomUser,
enableCloudWorkspaceFromShareButton, enableCloudWorkspaceFromShareButton,
enableShare,
loginUser, loginUser,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor'; import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
@@ -44,9 +45,11 @@ test('can enable share page', async ({ page, browser }) => {
}); });
await page.keyboard.press('Enter', { delay: 50 }); await page.keyboard.press('Enter', { delay: 50 });
await page.keyboard.type('TEST CONTENT', { delay: 50 }); await page.keyboard.type('TEST CONTENT', { delay: 50 });
await page.getByTestId('cloud-share-menu-button').click();
await page.getByTestId('share-menu-create-link-button').click(); // enable share page and copy page link
await enableShare(page);
await page.getByTestId('share-menu-copy-link-button').click(); await page.getByTestId('share-menu-copy-link-button').click();
await page.getByTestId('share-link-menu-copy-page').click();
// check share page is accessible // check share page is accessible
{ {
@@ -86,9 +89,11 @@ test('share page with default edgeless', async ({ page, browser }) => {
await expect(page.locator('affine-edgeless-root')).toBeVisible({ await expect(page.locator('affine-edgeless-root')).toBeVisible({
timeout: 1000, timeout: 1000,
}); });
await page.getByTestId('cloud-share-menu-button').click();
await page.getByTestId('share-menu-create-link-button').click(); // enable share page and copy page link
await enableShare(page);
await page.getByTestId('share-menu-copy-link-button').click(); await page.getByTestId('share-menu-copy-link-button').click();
await page.getByTestId('share-link-menu-copy-edgeless').click();
// check share page is accessible // check share page is accessible
{ {
@@ -126,9 +131,10 @@ test('image preview should should be shown', async ({ page, browser }) => {
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await importImage(page, 'http://localhost:8081/large-image.png'); await importImage(page, 'http://localhost:8081/large-image.png');
await page.getByTestId('cloud-share-menu-button').click(); // enable share page and copy page link
await page.getByTestId('share-menu-create-link-button').click(); await enableShare(page);
await page.getByTestId('share-menu-copy-link-button').click(); await page.getByTestId('share-menu-copy-link-button').click();
await page.getByTestId('share-link-menu-copy-page').click();
// check share page is accessible // check share page is accessible
{ {

View File

@@ -216,3 +216,9 @@ export async function enableCloudWorkspaceFromShareButton(page: Page) {
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickNewPageButton(page); await clickNewPageButton(page);
} }
export async function enableShare(page: Page) {
await page.getByTestId('cloud-share-menu-button').click();
await page.getByTestId('share-link-menu-trigger').click();
await page.getByTestId('share-link-menu-enable-share').click();
}

View File

@@ -310,6 +310,7 @@ __metadata:
"@radix-ui/react-popover": "npm:^1.0.7" "@radix-ui/react-popover": "npm:^1.0.7"
"@radix-ui/react-radio-group": "npm:^1.1.3" "@radix-ui/react-radio-group": "npm:^1.1.3"
"@radix-ui/react-scroll-area": "npm:^1.0.5" "@radix-ui/react-scroll-area": "npm:^1.0.5"
"@radix-ui/react-tabs": "npm:^1.1.0"
"@radix-ui/react-toast": "npm:^1.1.5" "@radix-ui/react-toast": "npm:^1.1.5"
"@radix-ui/react-toolbar": "npm:^1.0.4" "@radix-ui/react-toolbar": "npm:^1.0.4"
"@radix-ui/react-tooltip": "npm:^1.0.7" "@radix-ui/react-tooltip": "npm:^1.0.7"