mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): new share menu (#7838)
close AF-1224 AF-1216 
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@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-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
|
||||
@@ -24,6 +24,7 @@ export * from './ui/scrollbar';
|
||||
export * from './ui/skeleton';
|
||||
export * from './ui/switch';
|
||||
export * from './ui/table';
|
||||
export * from './ui/tabs';
|
||||
export * from './ui/toast';
|
||||
export * from './ui/tooltip';
|
||||
export * from './utils';
|
||||
|
||||
1
packages/frontend/component/src/ui/tabs/index.ts
Normal file
1
packages/frontend/component/src/ui/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tabs';
|
||||
52
packages/frontend/component/src/ui/tabs/tabs.css.ts
Normal file
52
packages/frontend/component/src/ui/tabs/tabs.css.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
});
|
||||
80
packages/frontend/component/src/ui/tabs/tabs.tsx
Normal file
80
packages/frontend/component/src/ui/tabs/tabs.tsx
Normal 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,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const headerStyle = style({
|
||||
display: 'flex',
|
||||
@@ -9,34 +10,133 @@ export const headerStyle = style({
|
||||
padding: '0 4px',
|
||||
gap: '4px',
|
||||
});
|
||||
export const content = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
});
|
||||
export const menuStyle = style({
|
||||
width: '410px',
|
||||
width: '390px',
|
||||
height: 'auto',
|
||||
padding: '12px',
|
||||
transform: 'translateX(-10px)',
|
||||
});
|
||||
export const menuTriggerStyle = style({
|
||||
width: '150px',
|
||||
padding: '4px 10px',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const menuItemStyle = style({
|
||||
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',
|
||||
});
|
||||
export const descriptionStyle = style({
|
||||
wordWrap: 'break-word',
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
textAlign: 'left',
|
||||
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({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
@@ -46,25 +146,13 @@ export const containerStyle = style({
|
||||
export const indicatorContainerStyle = style({
|
||||
position: 'relative',
|
||||
});
|
||||
export const inputButtonRowStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '16px',
|
||||
});
|
||||
export const titleContainerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
});
|
||||
export const subTitleStyle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontWeight: 400,
|
||||
padding: '8px 4px 0 4px',
|
||||
});
|
||||
export const columnContainerStyle = style({
|
||||
display: 'flex',
|
||||
@@ -78,33 +166,21 @@ export const rowContainerStyle = style({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '4px',
|
||||
});
|
||||
export const radioButtonGroup = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '2px',
|
||||
minWidth: '154px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
export const radioButton = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
selectors: {
|
||||
'&[data-state="checked"]': {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
},
|
||||
export const labelStyle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
});
|
||||
export const disableSharePage = style({
|
||||
color: cssVar('errorColor'),
|
||||
color: cssVarV2('button/error'),
|
||||
});
|
||||
export const localSharePage = style({
|
||||
padding: '12px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
minHeight: '84px',
|
||||
position: 'relative',
|
||||
});
|
||||
@@ -117,12 +193,6 @@ export const cloudSvgContainer = style({
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
});
|
||||
export const shareIconStyle = style({
|
||||
fontSize: '16px',
|
||||
color: cssVar('iconColor'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const shareLinkStyle = style({
|
||||
padding: '4px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
@@ -132,17 +202,38 @@ export const shareLinkStyle = style({
|
||||
gap: '4px',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > span`, {
|
||||
color: cssVar('linkColor'),
|
||||
color: cssVarV2('text/link'),
|
||||
});
|
||||
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,
|
||||
padding: '0px 8px',
|
||||
});
|
||||
export const shortcutStyle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
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'),
|
||||
});
|
||||
|
||||
@@ -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 { 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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CopyIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
|
||||
export const ShareExport = ({
|
||||
workspaceMetadata: workspace,
|
||||
currentPage,
|
||||
}: ShareMenuProps) => {
|
||||
export const ShareExport = ({ currentPage }: ShareMenuProps) => {
|
||||
const t = useI18n();
|
||||
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 currentMode = useLiveData(editor.mode$);
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExport']()}
|
||||
</div>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExportDescription']()}
|
||||
</div>
|
||||
<div>
|
||||
<ExportMenuItems
|
||||
exportHandler={exportHandler}
|
||||
className={styles.menuItemStyle}
|
||||
className={styles.exportItemStyle}
|
||||
pageMode={currentMode}
|
||||
/>
|
||||
</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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Tabs, Tooltip } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { Menu } from '@affine/component/ui/menu';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
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 {
|
||||
useLiveData,
|
||||
@@ -28,17 +28,20 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div className={styles.containerStyle}>
|
||||
<div className={styles.headerStyle}>
|
||||
<div className={styles.shareIconStyle}>
|
||||
<WebIcon />
|
||||
</div>
|
||||
{t['com.affine.share-menu.SharePage']()}
|
||||
</div>
|
||||
<Tabs.Root defaultValue="share">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="share">
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="export">{t['Export']()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="share">
|
||||
<SharePage {...props} />
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="export">
|
||||
<ShareExport {...props} />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -56,11 +59,20 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
|
||||
}, [shareInfoService]);
|
||||
|
||||
return (
|
||||
<Button ref={ref} className={styles.shareButton} variant="primary">
|
||||
{shared
|
||||
? t['com.affine.share-menu.sharedButton']()
|
||||
: t['com.affine.share-menu.shareButton']()}
|
||||
<Tooltip
|
||||
content={
|
||||
shared
|
||||
? 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>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,6 +83,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'local-share-menu',
|
||||
align: 'end',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
@@ -91,6 +104,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'cloud-share-menu',
|
||||
align: 'end',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
|
||||
@@ -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 { Button } from '@affine/component/ui/button';
|
||||
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
CollaborationIcon,
|
||||
DoneIcon,
|
||||
EdgelessIcon,
|
||||
LinkIcon,
|
||||
LockIcon,
|
||||
PageIcon,
|
||||
SingleSelectSelectSolidIcon,
|
||||
ViewIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
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 { 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 {
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
} = props;
|
||||
const doc = useService(DocService).doc;
|
||||
const editor = useService(EditorService).editor;
|
||||
const shareInfoService = useService(ShareInfoService);
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
useEffect(() => {
|
||||
@@ -71,44 +77,28 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
isSharedPage === null || sharedMode === null || baseUrl === null;
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
|
||||
const currentDocMode = useLiveData(doc.primaryMode$);
|
||||
const currentDocMode = useLiveData(editor.mode$);
|
||||
|
||||
const mode = useMemo(() => {
|
||||
if (isSharedPage && sharedMode) {
|
||||
// if it's a shared page, use the share mode
|
||||
return sharedMode.toLowerCase() as DocMode;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
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 {
|
||||
await shareInfoService.shareInfo.enableShare(
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||
);
|
||||
track.$.sharePanel.$.createShareLink({
|
||||
mode,
|
||||
});
|
||||
// TODO(@JimmFly): remove mode when we have a better way to handle it
|
||||
await shareInfoService.shareInfo.enableShare(PublicPageMode.Page);
|
||||
track.$.sharePanel.$.createShareLink();
|
||||
notify.success({
|
||||
title:
|
||||
t[
|
||||
@@ -121,11 +111,6 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
style: 'normal',
|
||||
icon: <SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />,
|
||||
});
|
||||
if (sharingUrl) {
|
||||
navigator.clipboard.writeText(sharingUrl).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
notify.error({
|
||||
title:
|
||||
@@ -139,7 +124,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
}, [mode, shareInfoService.shareInfo, sharingUrl, t]);
|
||||
}, [isSharedPage, shareInfoService.shareInfo, t]);
|
||||
|
||||
const onDisablePublic = useAsyncCallback(async () => {
|
||||
try {
|
||||
@@ -170,46 +155,29 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
setShowDisable(false);
|
||||
}, [shareInfoService, t]);
|
||||
|
||||
const onShareModeChange = useAsyncCallback(
|
||||
async (value: DocMode) => {
|
||||
try {
|
||||
const onClickDisable = useCallback(() => {
|
||||
if (isSharedPage) {
|
||||
await shareInfoService.shareInfo.changeShare(
|
||||
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')} />
|
||||
),
|
||||
});
|
||||
setShowDisable(true);
|
||||
}
|
||||
} catch (err) {
|
||||
notify.error({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
|
||||
](),
|
||||
}, [isSharedPage]);
|
||||
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId: editor.doc.id,
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
[isSharedPage, shareInfoService.shareInfo, t]
|
||||
);
|
||||
|
||||
const onCopyPageLink = useCallback(() => {
|
||||
onClickCopyLink('page');
|
||||
}, [onClickCopyLink]);
|
||||
const onCopyEdgelessLink = useCallback(() => {
|
||||
onClickCopyLink('edgeless');
|
||||
}, [onClickCopyLink]);
|
||||
const onCopyBlockLink = useCallback(() => {
|
||||
// TODO(@JimmFly): handle frame
|
||||
onClickCopyLink();
|
||||
}, [onClickCopyLink]);
|
||||
|
||||
if (isLoading) {
|
||||
// TODO(@eyhn): loading and error UI
|
||||
@@ -222,110 +190,175 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<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 className={styles.columnContainerStyle}>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.publish-to-web.description']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<Input
|
||||
inputStyle={{
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
<div className={styles.labelStyle}>
|
||||
{t['com.affine.share-menu.option.link.label']()}
|
||||
</div>
|
||||
<Menu
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
value={(isSharedPage && sharingUrl) || `${baseUrl}/...`}
|
||||
readOnly
|
||||
/>
|
||||
{isSharedPage ? (
|
||||
<Button
|
||||
onClick={onClickCopyLink}
|
||||
data-testid="share-menu-copy-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
disabled={!sharingUrl}
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
preFix={
|
||||
<LockIcon className={styles.publicMenuItemPrefixStyle} />
|
||||
}
|
||||
onSelect={onClickDisable}
|
||||
className={styles.menuItemStyle}
|
||||
>
|
||||
{t.Copy()}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onClickCreateLink}
|
||||
variant="primary"
|
||||
data-testid="share-menu-create-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{t.Create()}
|
||||
</Button>
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div>
|
||||
{t['com.affine.share-menu.option.link.no-access']()}
|
||||
</div>
|
||||
{!isSharedPage && (
|
||||
<DoneIcon className={styles.DoneIconStyle} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{t['com.affine.share-menu.ShareMode']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
preFix={
|
||||
<ViewIcon className={styles.publicMenuItemPrefixStyle} />
|
||||
}
|
||||
className={styles.menuItemStyle}
|
||||
onSelect={onClickAnyoneReadOnlyShare}
|
||||
data-testid="share-link-menu-enable-share"
|
||||
>
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div>
|
||||
<RadioGroup
|
||||
className={styles.radioButtonGroup}
|
||||
value={mode}
|
||||
onChange={onShareModeChange}
|
||||
items={modeOptions}
|
||||
/>
|
||||
{t['com.affine.share-menu.option.link.readonly']()}
|
||||
</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>
|
||||
{isSharedPage ? (
|
||||
<>
|
||||
{runtimeConfig.enableEnhanceShareMode && (
|
||||
<>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>Link expires</div>
|
||||
<div>
|
||||
<Menu items={<MenuItem>Never</MenuItem>}>
|
||||
<MenuTrigger>Never</MenuTrigger>
|
||||
<div className={styles.labelStyle}>
|
||||
{t['com.affine.share-menu.option.permission.label']()}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{'Show "Created with AFFiNE"'}
|
||||
</div>
|
||||
<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);
|
||||
}}
|
||||
{isOwner && (
|
||||
<div
|
||||
className={styles.openWorkspaceSettingsStyle}
|
||||
onClick={onOpenWorkspaceSettings}
|
||||
>
|
||||
<div className={styles.disableSharePage}>
|
||||
{t['Disable Public Link']()}
|
||||
<CollaborationIcon fontSize={16} />
|
||||
{t['com.affine.share-menu.navigate.workspace']()}
|
||||
</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
|
||||
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
|
||||
open={showDisable}
|
||||
onConfirm={onDisablePublic}
|
||||
onOpenChange={setShowDisable}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -339,7 +372,7 @@ export const SharePage = (props: ShareMenuProps) => {
|
||||
// TODO(@eyhn): refactor this part
|
||||
<ErrorBoundary fallback={null}>
|
||||
<Suspense>
|
||||
<AffineSharePage {...props} />
|
||||
<AFFiNESharePage {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
|
||||
const editor = useService(EditorService).editor;
|
||||
const mode = useLiveData(editor.mode$);
|
||||
|
||||
const isSharedMode = editor.isSharedMode;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ export function useRegisterCopyLinkCommands({
|
||||
const isActiveView = useIsActiveView();
|
||||
const workspaceId = workspaceMeta.id;
|
||||
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId: docId,
|
||||
urlType: 'workspace',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,8 +39,7 @@ export function useRegisterCopyLinkCommands({
|
||||
label: '',
|
||||
icon: null,
|
||||
run() {
|
||||
track.$.cmdk.general.copyShareLink({ type: 'private' });
|
||||
|
||||
track.$.cmdk.general.copyShareLink();
|
||||
isActiveView && isCloud && onClickCopyLink();
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,61 +2,115 @@ import { notify } from '@affine/component';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { Disposable } from '@blocksuite/global/utils';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { BaseSelection } from '@blocksuite/block-std';
|
||||
import { type DocMode } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useActiveBlocksuiteEditor } from '../use-block-suite-editor';
|
||||
|
||||
type UrlType = 'share' | 'workspace';
|
||||
|
||||
type UseSharingUrl = {
|
||||
workspaceId: 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 = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
blockId,
|
||||
}: UseSharingUrl & { blockId?: string }) => {
|
||||
// to generate a private url like https://app.affine.app/workspace/123/456
|
||||
// or https://app.affine.app/workspace/123/456#block-123
|
||||
|
||||
// to generate a public url like https://app.affine.app/share/123/456
|
||||
blockIds,
|
||||
elementIds,
|
||||
shareMode,
|
||||
xywh, // not needed currently
|
||||
}: UseSharingUrl) => {
|
||||
// Base URL construction
|
||||
const baseUrl = getAffineCloudBaseUrl();
|
||||
if (!baseUrl) return null;
|
||||
|
||||
try {
|
||||
return new URL(
|
||||
`${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' && blockId ? `#${blockId}` : ''}`
|
||||
).toString();
|
||||
const url = new URL(`${baseUrl}/workspace/${workspaceId}/${pageId}`);
|
||||
if (shareMode) {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useSharingUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
const t = useI18n();
|
||||
const [blockId, setBlockId] = useState<string>('');
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
const sharingUrl = useMemo(
|
||||
() =>
|
||||
generateUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
blockId: blockId.length > 0 ? blockId : undefined,
|
||||
}),
|
||||
[workspaceId, pageId, urlType, blockId]
|
||||
);
|
||||
const getShareLinkType = ({
|
||||
shareMode,
|
||||
blockIds,
|
||||
elementIds,
|
||||
}: {
|
||||
shareMode?: DocMode;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
}) => {
|
||||
if (shareMode === 'page') {
|
||||
return 'doc';
|
||||
} else if (shareMode === 'edgeless') {
|
||||
return 'whiteboard';
|
||||
} else if (blockIds && blockIds.length > 0) {
|
||||
return 'block';
|
||||
} else if (elementIds && elementIds.length > 0) {
|
||||
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) {
|
||||
navigator.clipboard
|
||||
.writeText(sharingUrl)
|
||||
@@ -69,45 +123,17 @@ export const useSharingUrl = ({
|
||||
console.error(err);
|
||||
});
|
||||
track.$.sharePanel.$.copyShareLink({
|
||||
type: urlType === 'share' ? 'public' : 'private',
|
||||
type,
|
||||
});
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Network not available',
|
||||
});
|
||||
}
|
||||
}, [sharingUrl, t, urlType]);
|
||||
|
||||
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]);
|
||||
},
|
||||
[editor, pageId, t, workspaceId]
|
||||
);
|
||||
return {
|
||||
sharingUrl,
|
||||
onClickCopyLink,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -376,7 +376,9 @@ export type EventArgs = {
|
||||
createDoc: { mode?: 'edgeless' | 'page' };
|
||||
switchPageMode: { mode: 'edgeless' | 'page' };
|
||||
createShareLink: { mode: 'edgeless' | 'page' };
|
||||
copyShareLink: { type: 'public' | 'private' };
|
||||
copyShareLink: {
|
||||
type: 'default' | 'doc' | 'whiteboard' | 'block' | 'element';
|
||||
};
|
||||
export: { type: string };
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
import { ShareFooter } from './share-footer';
|
||||
@@ -50,6 +51,18 @@ export const SharePage = ({
|
||||
const error = useLiveData(shareReaderService.reader.error$);
|
||||
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(() => {
|
||||
shareReaderService.reader.loadShare({ workspaceId, docId });
|
||||
}, [shareReaderService, docId, workspaceId]);
|
||||
@@ -70,7 +83,7 @@ export const SharePage = ({
|
||||
docId={data.docId}
|
||||
workspaceBinary={data.workspaceBinary}
|
||||
docBinary={data.docBinary}
|
||||
publishMode={data.publishMode}
|
||||
publishMode={mode || data.publishMode}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1279,6 +1279,19 @@
|
||||
"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.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.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.",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { skipOnboarding, test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspaceFromShareButton,
|
||||
enableShare,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
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.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-link-menu-copy-page').click();
|
||||
|
||||
// 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({
|
||||
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-link-menu-copy-edgeless').click();
|
||||
|
||||
// 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 importImage(page, 'http://localhost:8081/large-image.png');
|
||||
|
||||
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-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
|
||||
@@ -216,3 +216,9 @@ export async function enableCloudWorkspaceFromShareButton(page: Page) {
|
||||
await waitForEditorLoad(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();
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ __metadata:
|
||||
"@radix-ui/react-popover": "npm:^1.0.7"
|
||||
"@radix-ui/react-radio-group": "npm:^1.1.3"
|
||||
"@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-toolbar": "npm:^1.0.4"
|
||||
"@radix-ui/react-tooltip": "npm:^1.0.7"
|
||||
|
||||
Reference in New Issue
Block a user