feat(core): add copy link button to local share menu (#9271)
close AF-1838
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
import { Button, Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||||
|
import {
|
||||||
|
getSelectedNodes,
|
||||||
|
useSharingUrl,
|
||||||
|
} from '@affine/core/components/hooks/affine/use-share-url';
|
||||||
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
|
import { BlockIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.css';
|
||||||
|
|
||||||
|
export const CopyLinkButton = ({
|
||||||
|
workspaceId,
|
||||||
|
secondary,
|
||||||
|
}: {
|
||||||
|
secondary?: boolean;
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const editor = useService(EditorService).editor;
|
||||||
|
const currentMode = useLiveData(editor.mode$);
|
||||||
|
const editorContainer = useLiveData(editor.editorContainer$);
|
||||||
|
|
||||||
|
const { blockIds, elementIds } = useMemo(
|
||||||
|
() => getSelectedNodes(editorContainer?.host || null, currentMode),
|
||||||
|
[editorContainer, currentMode]
|
||||||
|
);
|
||||||
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
|
workspaceId,
|
||||||
|
pageId: editor.doc.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCopyPageLink = useCallback(() => {
|
||||||
|
onClickCopyLink('page' as DocMode);
|
||||||
|
}, [onClickCopyLink]);
|
||||||
|
const onCopyEdgelessLink = useCallback(() => {
|
||||||
|
onClickCopyLink('edgeless' as DocMode);
|
||||||
|
}, [onClickCopyLink]);
|
||||||
|
const onCopyBlockLink = useCallback(() => {
|
||||||
|
onClickCopyLink(currentMode, blockIds, elementIds);
|
||||||
|
}, [onClickCopyLink, currentMode, blockIds, elementIds]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.copyLinkContainerStyle, { secondary: secondary })}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={styles.copyLinkButtonStyle}
|
||||||
|
onClick={onCopyBlockLink}
|
||||||
|
withoutHover
|
||||||
|
variant={secondary ? 'secondary' : 'primary'}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={clsx(styles.copyLinkLabelStyle, {
|
||||||
|
secondary: secondary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.copy']()}
|
||||||
|
</span>
|
||||||
|
{BUILD_CONFIG.isDesktopEdition && (
|
||||||
|
<span
|
||||||
|
className={clsx(styles.copyLinkShortcutStyle, {
|
||||||
|
secondary: secondary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{environment.isMacOs ? '⌘ + ⌥ + C' : 'Ctrl + Shift + C'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
contentOptions={{
|
||||||
|
align: 'end',
|
||||||
|
}}
|
||||||
|
items={
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<PageIcon />}
|
||||||
|
onSelect={onCopyPageLink}
|
||||||
|
data-testid="share-link-menu-copy-page"
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.copy.page']()}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<EdgelessIcon />}
|
||||||
|
onSelect={onCopyEdgelessLink}
|
||||||
|
data-testid="share-link-menu-copy-edgeless"
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.copy.edgeless']()}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<BlockIcon />}
|
||||||
|
onSelect={onCopyBlockLink}
|
||||||
|
disabled={blockIds.length + elementIds.length === 0}
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.copy.block']()}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuTrigger
|
||||||
|
variant={secondary ? 'secondary' : 'primary'}
|
||||||
|
className={clsx(styles.copyLinkTriggerStyle, {
|
||||||
|
secondary: secondary,
|
||||||
|
})}
|
||||||
|
data-testid="share-menu-copy-link-button"
|
||||||
|
suffixStyle={{ width: 20, height: 20 }}
|
||||||
|
withoutHover
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -54,6 +54,12 @@ export const copyLinkContainerStyle = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
selectors: {
|
||||||
|
'&.secondary': {
|
||||||
|
padding: 0,
|
||||||
|
marginTop: '12px',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const copyLinkButtonStyle = style({
|
export const copyLinkButtonStyle = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -64,6 +70,14 @@ export const copyLinkButtonStyle = style({
|
|||||||
borderBottomRightRadius: '0',
|
borderBottomRightRadius: '0',
|
||||||
color: 'transparent',
|
color: 'transparent',
|
||||||
position: 'initial',
|
position: 'initial',
|
||||||
|
selectors: {
|
||||||
|
'&.dark': {
|
||||||
|
backgroundColor: cssVarV2('layer/pureBlack'),
|
||||||
|
},
|
||||||
|
'&.dark::hover': {
|
||||||
|
backgroundColor: cssVarV2('layer/pureBlack'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const copyLinkLabelContainerStyle = style({
|
export const copyLinkLabelContainerStyle = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -80,6 +94,11 @@ export const copyLinkLabelStyle = style({
|
|||||||
transform: 'translateX(-50%) translateY(-50%)',
|
transform: 'translateX(-50%) translateY(-50%)',
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
color: cssVarV2('text/pureWhite'),
|
color: cssVarV2('text/pureWhite'),
|
||||||
|
selectors: {
|
||||||
|
'&.secondary': {
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const copyLinkShortcutStyle = style({
|
export const copyLinkShortcutStyle = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -90,6 +109,11 @@ export const copyLinkShortcutStyle = style({
|
|||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
color: cssVarV2('text/pureWhite'),
|
color: cssVarV2('text/pureWhite'),
|
||||||
|
selectors: {
|
||||||
|
'&.secondary': {
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const copyLinkTriggerStyle = style({
|
export const copyLinkTriggerStyle = style({
|
||||||
padding: '4px 12px 4px 8px',
|
padding: '4px 12px 4px 8px',
|
||||||
@@ -109,11 +133,25 @@ export const copyLinkTriggerStyle = style({
|
|||||||
width: '1px',
|
width: '1px',
|
||||||
backgroundColor: cssVarV2('button/innerBlackBorder'),
|
backgroundColor: cssVarV2('button/innerBlackBorder'),
|
||||||
},
|
},
|
||||||
|
selectors: {
|
||||||
|
'&.secondary': {
|
||||||
|
backgroundColor: cssVarV2('button/secondary'),
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
},
|
||||||
|
'&.secondary:hover': {
|
||||||
|
backgroundColor: cssVarV2('button/secondary'),
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
globalStyle(`${copyLinkTriggerStyle} svg`, {
|
globalStyle(`${copyLinkTriggerStyle} svg`, {
|
||||||
color: cssVarV2('button/pureWhiteText'),
|
color: cssVarV2('button/pureWhiteText'),
|
||||||
transform: 'translateX(2px)',
|
transform: 'translateX(2px)',
|
||||||
});
|
});
|
||||||
|
globalStyle(`${copyLinkTriggerStyle}.secondary svg`, {
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
transform: 'translateX(2px)',
|
||||||
|
});
|
||||||
export const copyLinkMenuItemStyle = style({
|
export const copyLinkMenuItemStyle = style({
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
import { notify, Skeleton } from '@affine/component';
|
import { notify, Skeleton } from '@affine/component';
|
||||||
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 {
|
|
||||||
getSelectedNodes,
|
|
||||||
useSharingUrl,
|
|
||||||
} from '@affine/core/components/hooks/affine/use-share-url';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { ServerService } from '@affine/core/modules/cloud';
|
import { ServerService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
|
||||||
import {
|
import {
|
||||||
BlockIcon,
|
|
||||||
CollaborationIcon,
|
CollaborationIcon,
|
||||||
DoneIcon,
|
DoneIcon,
|
||||||
EdgelessIcon,
|
|
||||||
LockIcon,
|
LockIcon,
|
||||||
PageIcon,
|
|
||||||
SingleSelectCheckSolidIcon,
|
SingleSelectCheckSolidIcon,
|
||||||
ViewIcon,
|
ViewIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
import { Suspense, useCallback, useEffect } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
import { CloudSvg } from '../cloud-svg';
|
import { CloudSvg } from '../cloud-svg';
|
||||||
|
import { CopyLinkButton } from './copy-link-button';
|
||||||
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 LocalSharePage = (props: ShareMenuProps) => {
|
export const LocalSharePage = (props: ShareMenuProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const {
|
||||||
|
workspaceMetadata: { id: workspaceId },
|
||||||
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div className={styles.localSharePage}>
|
<>
|
||||||
<div className={styles.columnContainerStyle} style={{ gap: '12px' }}>
|
<div className={styles.localSharePage}>
|
||||||
<div className={styles.descriptionStyle} style={{ maxWidth: '230px' }}>
|
<div className={styles.columnContainerStyle} style={{ gap: '12px' }}>
|
||||||
{t['com.affine.share-menu.EnableCloudDescription']()}
|
<div
|
||||||
</div>
|
className={styles.descriptionStyle}
|
||||||
<div>
|
style={{ maxWidth: '230px' }}
|
||||||
<Button
|
|
||||||
onClick={props.onEnableAffineCloud}
|
|
||||||
variant="primary"
|
|
||||||
data-testid="share-menu-enable-affine-cloud-button"
|
|
||||||
>
|
>
|
||||||
{t['Enable AFFiNE Cloud']()}
|
{t['com.affine.share-menu.EnableCloudDescription']()}
|
||||||
</Button>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={props.onEnableAffineCloud}
|
||||||
|
variant="primary"
|
||||||
|
data-testid="share-menu-enable-affine-cloud-button"
|
||||||
|
>
|
||||||
|
{t['Enable AFFiNE Cloud']()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cloudSvgContainer}>
|
||||||
|
<CloudSvg />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.cloudSvgContainer}>
|
<CopyLinkButton workspaceId={workspaceId} secondary />
|
||||||
<CloudSvg />
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,9 +65,6 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
const {
|
const {
|
||||||
workspaceMetadata: { id: workspaceId },
|
workspaceMetadata: { id: workspaceId },
|
||||||
} = props;
|
} = props;
|
||||||
const editor = useService(EditorService).editor;
|
|
||||||
const currentMode = useLiveData(editor.mode$);
|
|
||||||
const editorContainer = useLiveData(editor.editorContainer$);
|
|
||||||
const shareInfoService = useService(ShareInfoService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const serverService = useService(ServerService);
|
const serverService = useService(ServerService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -152,25 +149,6 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
}
|
}
|
||||||
}, [shareInfoService, t]);
|
}, [shareInfoService, t]);
|
||||||
|
|
||||||
const { blockIds, elementIds } = useMemo(
|
|
||||||
() => getSelectedNodes(editorContainer?.host || null, currentMode),
|
|
||||||
[editorContainer, currentMode]
|
|
||||||
);
|
|
||||||
const { onClickCopyLink } = useSharingUrl({
|
|
||||||
workspaceId,
|
|
||||||
pageId: editor.doc.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCopyPageLink = useCallback(() => {
|
|
||||||
onClickCopyLink('page' as DocMode);
|
|
||||||
}, [onClickCopyLink]);
|
|
||||||
const onCopyEdgelessLink = useCallback(() => {
|
|
||||||
onClickCopyLink('edgeless' as DocMode);
|
|
||||||
}, [onClickCopyLink]);
|
|
||||||
const onCopyBlockLink = useCallback(() => {
|
|
||||||
onClickCopyLink(currentMode, blockIds, elementIds);
|
|
||||||
}, [onClickCopyLink, currentMode, blockIds, elementIds]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// TODO(@eyhn): loading and error UI
|
// TODO(@eyhn): loading and error UI
|
||||||
return (
|
return (
|
||||||
@@ -254,61 +232,7 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
{t['com.affine.share-menu.navigate.workspace']()}
|
{t['com.affine.share-menu.navigate.workspace']()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.copyLinkContainerStyle}>
|
<CopyLinkButton workspaceId={workspaceId} />
|
||||||
<Button
|
|
||||||
className={styles.copyLinkButtonStyle}
|
|
||||||
onClick={onCopyBlockLink}
|
|
||||||
variant="primary"
|
|
||||||
withoutHover
|
|
||||||
>
|
|
||||||
<span className={styles.copyLinkLabelStyle}>
|
|
||||||
{t['com.affine.share-menu.copy']()}
|
|
||||||
</span>
|
|
||||||
{BUILD_CONFIG.isDesktopEdition && (
|
|
||||||
<span className={styles.copyLinkShortcutStyle}>
|
|
||||||
{environment.isMacOs ? '⌘ + ⌥ + C' : 'Ctrl + Shift + C'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Menu
|
|
||||||
contentOptions={{
|
|
||||||
align: 'end',
|
|
||||||
}}
|
|
||||||
items={
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<PageIcon />}
|
|
||||||
onSelect={onCopyPageLink}
|
|
||||||
data-testid="share-link-menu-copy-page"
|
|
||||||
>
|
|
||||||
{t['com.affine.share-menu.copy.page']()}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<EdgelessIcon />}
|
|
||||||
onSelect={onCopyEdgelessLink}
|
|
||||||
data-testid="share-link-menu-copy-edgeless"
|
|
||||||
>
|
|
||||||
{t['com.affine.share-menu.copy.edgeless']()}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<BlockIcon />}
|
|
||||||
onSelect={onCopyBlockLink}
|
|
||||||
disabled={blockIds.length + elementIds.length === 0}
|
|
||||||
>
|
|
||||||
{t['com.affine.share-menu.copy.block']()}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuTrigger
|
|
||||||
variant="primary"
|
|
||||||
className={styles.copyLinkTriggerStyle}
|
|
||||||
data-testid="share-menu-copy-link-button"
|
|
||||||
suffixStyle={{ width: 20, height: 20 }}
|
|
||||||
withoutHover
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user