mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"@blocksuite/blocks": "0.0.0-20230409084303-221991d4-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230409084303-221991d4-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly",
|
||||
"@blocksuite/icons": "2.1.7",
|
||||
"@blocksuite/icons": "2.1.10",
|
||||
"@blocksuite/store": "0.0.0-20230409084303-221991d4-nightly"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -31,9 +31,9 @@
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/base": "5.0.0-alpha.124",
|
||||
"@mui/base": "5.0.0-alpha.125",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/material": "^5.11.16",
|
||||
"@mui/material": "^5.12.0",
|
||||
"@radix-ui/react-avatar": "^1.0.2",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -48,22 +48,22 @@
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/icons": "^2.1.9",
|
||||
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@storybook/addon-actions": "^7.0.2",
|
||||
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"@blocksuite/icons": "^2.1.10",
|
||||
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"@storybook/addon-actions": "^7.0.4",
|
||||
"@storybook/addon-coverage": "^0.0.8",
|
||||
"@storybook/addon-essentials": "^7.0.2",
|
||||
"@storybook/addon-interactions": "^7.0.2",
|
||||
"@storybook/addon-links": "^7.0.2",
|
||||
"@storybook/addon-storysource": "^7.0.2",
|
||||
"@storybook/blocks": "^7.0.2",
|
||||
"@storybook/builder-vite": "^7.0.2",
|
||||
"@storybook/addon-essentials": "^7.0.4",
|
||||
"@storybook/addon-interactions": "^7.0.4",
|
||||
"@storybook/addon-links": "^7.0.4",
|
||||
"@storybook/addon-storysource": "^7.0.4",
|
||||
"@storybook/blocks": "^7.0.4",
|
||||
"@storybook/builder-vite": "^7.0.4",
|
||||
"@storybook/jest": "^0.1.0",
|
||||
"@storybook/react": "^7.0.2",
|
||||
"@storybook/react-vite": "^7.0.2",
|
||||
"@storybook/react": "^7.0.4",
|
||||
"@storybook/react-vite": "^7.0.4",
|
||||
"@storybook/test-runner": "^0.10.0",
|
||||
"@storybook/testing-library": "^0.1.0",
|
||||
"@types/react": "=18.0.31",
|
||||
@@ -74,7 +74,7 @@
|
||||
"concurrently": "^8.0.1",
|
||||
"jest-mock": "^29.5.0",
|
||||
"serve": "^14.2.0",
|
||||
"storybook": "^7.0.2",
|
||||
"storybook": "^7.0.4",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.2.1",
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||
import { ExportToHtmlIcon, ExportToMarkdownIcon } from '@blocksuite/icons';
|
||||
import type { FC } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from './index';
|
||||
import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
import {
|
||||
actionsStyle,
|
||||
descriptionStyle,
|
||||
exportButtonStyle,
|
||||
menuItemStyle,
|
||||
svgStyle,
|
||||
} from './index.css';
|
||||
import type { ShareMenuProps } from './ShareMenu';
|
||||
|
||||
export const Export: FC<ShareMenuProps> = props => {
|
||||
const contentParserRef = useRef<ContentParser>();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Download a static copy of your page to share with others.
|
||||
{t('Export Shared Pages Description')}
|
||||
</div>
|
||||
<div className={actionsStyle}>
|
||||
<Button
|
||||
className={exportButtonStyle}
|
||||
onClick={() => {
|
||||
if (!contentParserRef.current) {
|
||||
contentParserRef.current = new ContentParser(props.currentPage);
|
||||
@@ -22,9 +32,11 @@ export const Export: FC<ShareMenuProps> = props => {
|
||||
return contentParserRef.current.onExportHtml();
|
||||
}}
|
||||
>
|
||||
Export to HTML
|
||||
<ExportToHtmlIcon className={svgStyle} />
|
||||
{t('Export to HTML')}
|
||||
</Button>
|
||||
<Button
|
||||
className={exportButtonStyle}
|
||||
onClick={() => {
|
||||
if (!contentParserRef.current) {
|
||||
contentParserRef.current = new ContentParser(props.currentPage);
|
||||
@@ -32,7 +44,8 @@ export const Export: FC<ShareMenuProps> = props => {
|
||||
return contentParserRef.current.onExportMarkdown();
|
||||
}}
|
||||
>
|
||||
Export to Markdown
|
||||
<ExportToMarkdownIcon className={svgStyle} />
|
||||
{t('Export to Markdown')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
145
packages/component/src/components/share-menu/ShareMenu.tsx
Normal file
145
packages/component/src/components/share-menu/ShareMenu.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||
import type { FC } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Menu } from '../..';
|
||||
import { Export } from './Export';
|
||||
import { containerStyle, indicatorContainerStyle, tabStyle } from './index.css';
|
||||
import { SharePage } from './SharePage';
|
||||
import { ShareWorkspace } from './ShareWorkspace';
|
||||
import { StyledIndicator, StyledShareButton, TabItem } from './styles';
|
||||
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
|
||||
const MenuItems: Record<SharePanel, FC<ShareMenuProps>> = {
|
||||
SharePage: SharePage,
|
||||
Export: Export,
|
||||
ShareWorkspace: ShareWorkspace,
|
||||
};
|
||||
const tabIcons = {
|
||||
SharePage: <ShareIcon />,
|
||||
Export: <ExportIcon />,
|
||||
ShareWorkspace: <PublishIcon />,
|
||||
};
|
||||
export type ShareMenuProps<
|
||||
Workspace extends AffineWorkspace | LocalWorkspace =
|
||||
| AffineWorkspace
|
||||
| LocalWorkspace
|
||||
> = {
|
||||
workspace: Workspace;
|
||||
currentPage: Page;
|
||||
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
|
||||
onOpenWorkspaceSettings: (workspace: Workspace) => void;
|
||||
togglePagePublic: (page: Page, isPublic: boolean) => Promise<void>;
|
||||
toggleWorkspacePublish: (
|
||||
workspace: Workspace,
|
||||
publish: boolean
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
function assertInstanceOf<T, U extends T>(
|
||||
obj: T,
|
||||
type: new (...args: any[]) => U
|
||||
): asserts obj is U {
|
||||
if (!(obj instanceof type)) {
|
||||
throw new Error('Object is not instance of type');
|
||||
}
|
||||
}
|
||||
|
||||
export const ShareMenu: FC<ShareMenuProps> = props => {
|
||||
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
|
||||
const [isPublic] = useBlockSuiteWorkspacePageIsPublic(props.currentPage);
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const indicatorRef = useRef<HTMLDivElement | null>(null);
|
||||
const startTransaction = useCallback(() => {
|
||||
if (indicatorRef.current && containerRef.current) {
|
||||
const indicator = indicatorRef.current;
|
||||
const activeTabElement = containerRef.current.querySelector(
|
||||
`[data-tab-key="${activeItem}"]`
|
||||
);
|
||||
assertInstanceOf(activeTabElement, HTMLElement);
|
||||
requestAnimationFrame(() => {
|
||||
indicator.style.left = `${activeTabElement.offsetLeft}px`;
|
||||
indicator.style.width = `${activeTabElement.offsetWidth}px`;
|
||||
});
|
||||
}
|
||||
}, [activeItem]);
|
||||
const handleMenuChange = useCallback(
|
||||
(selectedItem: SharePanel) => {
|
||||
setActiveItem(selectedItem);
|
||||
startTransaction();
|
||||
},
|
||||
[setActiveItem, startTransaction]
|
||||
);
|
||||
|
||||
const ActiveComponent = MenuItems[activeItem];
|
||||
interface ShareMenuProps {
|
||||
activeItem: SharePanel;
|
||||
onChangeTab: (selectedItem: SharePanel) => void;
|
||||
}
|
||||
const ShareMenu: FC<ShareMenuProps> = ({ activeItem, onChangeTab }) => {
|
||||
const handleButtonClick = (itemName: SharePanel) => {
|
||||
onChangeTab(itemName);
|
||||
setActiveItem(itemName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={tabStyle} ref={containerRef}>
|
||||
{Object.keys(MenuItems).map(item => (
|
||||
<TabItem
|
||||
isActive={activeItem === item}
|
||||
key={item}
|
||||
data-tab-key={item}
|
||||
onClick={() => handleButtonClick(item as SharePanel)}
|
||||
>
|
||||
{tabIcons[item as SharePanel]}
|
||||
{isPublic ? (item === 'SharePage' ? 'SharedPage' : item) : item}
|
||||
</TabItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Share = (
|
||||
<>
|
||||
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
|
||||
<div className={indicatorContainerStyle}>
|
||||
<StyledIndicator
|
||||
ref={(ref: HTMLDivElement | null) => {
|
||||
indicatorRef.current = ref;
|
||||
startTransaction();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={containerStyle}>
|
||||
<ActiveComponent {...props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
content={Share}
|
||||
visible={open}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
width={439}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<StyledShareButton
|
||||
data-testid="share-menu-button"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
isShared={isPublic}
|
||||
>
|
||||
<div>{isPublic ? 'Shared' : 'Share'}</div>
|
||||
</StyledShareButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -3,11 +3,22 @@ import type { LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||
import type { FC } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from './index';
|
||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
import { PublicLinkDisableModal } from './disable-public-link';
|
||||
import {
|
||||
descriptionStyle,
|
||||
inputButtonRowStyle,
|
||||
menuItemStyle,
|
||||
} from './index.css';
|
||||
import type { ShareMenuProps } from './ShareMenu';
|
||||
import {
|
||||
StyledButton,
|
||||
StyledDisableButton,
|
||||
StyledInput,
|
||||
StyledLinkSpan,
|
||||
} from './styles';
|
||||
|
||||
export const LocalSharePage: FC<ShareMenuProps> = props => {
|
||||
return (
|
||||
@@ -15,17 +26,14 @@ export const LocalSharePage: FC<ShareMenuProps> = props => {
|
||||
<div className={descriptionStyle}>
|
||||
Sharing page publicly requires AFFiNE Cloud service.
|
||||
</div>
|
||||
<Button
|
||||
<StyledButton
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
className={buttonStyle}
|
||||
type="light"
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||
}}
|
||||
>
|
||||
Enable AFFiNE Cloud
|
||||
</Button>
|
||||
</StyledButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +42,7 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
|
||||
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
|
||||
props.currentPage
|
||||
);
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
const sharingUrl = useMemo(() => {
|
||||
const env = getEnvironment();
|
||||
if (env.isBrowser) {
|
||||
@@ -48,14 +57,59 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard.writeText(sharingUrl);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Create a link you can easily share with anyone.
|
||||
</div>
|
||||
<span>{isPublic ? sharingUrl : 'not public'}</span>
|
||||
{!isPublic && <Button onClick={onClickCreateLink}>Create</Button>}
|
||||
{isPublic && <Button onClick={onClickCopyLink}>Copy Link</Button>}
|
||||
<div className={inputButtonRowStyle}>
|
||||
<StyledInput
|
||||
type="text"
|
||||
readOnly
|
||||
value={isPublic ? sharingUrl : 'https://app.affine.pro/xxxx'}
|
||||
/>
|
||||
{!isPublic && (
|
||||
<StyledButton
|
||||
data-testid="affine-share-create-link"
|
||||
onClick={onClickCreateLink}
|
||||
>
|
||||
Create
|
||||
</StyledButton>
|
||||
)}
|
||||
{isPublic && (
|
||||
<StyledButton
|
||||
data-testid="affine-share-copy-link"
|
||||
onClick={onClickCopyLink}
|
||||
>
|
||||
Copy Link
|
||||
</StyledButton>
|
||||
)}
|
||||
</div>
|
||||
<div className={descriptionStyle}>
|
||||
The entire Workspace is published on the web and can be edited via
|
||||
<StyledLinkSpan
|
||||
onClick={() => {
|
||||
props.onOpenWorkspaceSettings(props.workspace);
|
||||
}}
|
||||
>
|
||||
Workspace Settings.
|
||||
</StyledLinkSpan>
|
||||
</div>
|
||||
{isPublic && (
|
||||
<>
|
||||
<StyledDisableButton onClick={() => setShowDisable(true)}>
|
||||
Disable Public Link
|
||||
</StyledDisableButton>
|
||||
<PublicLinkDisableModal
|
||||
page={props.currentPage}
|
||||
open={showDisable}
|
||||
onClose={() => {
|
||||
setShowDisable(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,27 +2,24 @@ import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from '.';
|
||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
import { descriptionStyle, menuItemStyle } from './index.css';
|
||||
import type { ShareMenuProps } from './ShareMenu';
|
||||
import { StyledButton } from './styles';
|
||||
|
||||
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Sharing page publicly requires AFFiNE Cloud service.
|
||||
Invite others to join the Workspace or publish it to web.
|
||||
</div>
|
||||
<Button
|
||||
<StyledButton
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
className={buttonStyle}
|
||||
type="light"
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||
props.onOpenWorkspaceSettings(props.workspace);
|
||||
}}
|
||||
>
|
||||
Enable AFFiNE Cloud
|
||||
</Button>
|
||||
Open Workspace Settings
|
||||
</StyledButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -36,16 +33,14 @@ const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
|
||||
? `Current workspace has been published to the web as a public workspace.`
|
||||
: `Invite others to join the Workspace or publish it to web`}
|
||||
</div>
|
||||
<Button
|
||||
<StyledButton
|
||||
data-testid="share-menu-publish-to-web-button"
|
||||
onClick={() => {
|
||||
props.onOpenWorkspaceSettings(props.workspace);
|
||||
}}
|
||||
type="light"
|
||||
shape="circle"
|
||||
>
|
||||
Open Workspace Settings
|
||||
</Button>
|
||||
</StyledButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Modal, ModalCloseButton, toast } from '../../..';
|
||||
import {
|
||||
StyledButton,
|
||||
StyledButtonContent,
|
||||
StyledDangerButton,
|
||||
StyledModalHeader,
|
||||
StyledModalWrapper,
|
||||
StyledTextContent,
|
||||
} from './style';
|
||||
|
||||
export type PublicLinkDisableProps = {
|
||||
page: Page;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const PublicLinkDisableModal = ({
|
||||
page,
|
||||
open,
|
||||
onClose,
|
||||
}: PublicLinkDisableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(page);
|
||||
const handleDisable = useCallback(() => {
|
||||
setIsPublic(false);
|
||||
toast('Successfully disabled', {
|
||||
portal: document.body,
|
||||
});
|
||||
onClose();
|
||||
}, []);
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} top={12} right={12} />
|
||||
<StyledModalHeader>{t('Disable Public Link ?')}</StyledModalHeader>
|
||||
|
||||
<StyledTextContent>
|
||||
{t('Disable Public Link Description')}
|
||||
</StyledTextContent>
|
||||
|
||||
<StyledButtonContent>
|
||||
<StyledButton onClick={onClose}>{t('Cancel')}</StyledButton>
|
||||
<StyledDangerButton
|
||||
data-testid="disable-public-link-confirm-button"
|
||||
onClick={handleDisable}
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t('Disable')}
|
||||
</StyledDangerButton>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { styled, TextButton } from '@affine/component';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '560px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
// height: '312px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '560px',
|
||||
fontWeight: '600',
|
||||
fontSize: theme.font.h6,
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTextContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '560px',
|
||||
padding: '0px 84px',
|
||||
fontWeight: '400',
|
||||
fontSize: theme.font.base,
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
margin: '32px 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
});
|
||||
export const StyledButton = styled(TextButton)(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.primaryColor,
|
||||
height: '32px',
|
||||
background: '#F3F0FF',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '4px 20px',
|
||||
};
|
||||
});
|
||||
export const StyledDangerButton = styled(TextButton)(({ theme }) => {
|
||||
return {
|
||||
color: '#FF631F',
|
||||
height: '32px',
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(255, 99, 31, 0.1), rgba(255, 99, 31, 0.1)), #FFFFFF;',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '4px 20px',
|
||||
};
|
||||
});
|
||||
@@ -2,22 +2,27 @@ import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const tabStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
flex: '1',
|
||||
width: '100%',
|
||||
padding: '0 10px',
|
||||
margin: '0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
marginTop: '4px',
|
||||
marginLeft: '10px',
|
||||
marginRight: '10px',
|
||||
});
|
||||
|
||||
export const menuItemStyle = style({
|
||||
marginLeft: '20px',
|
||||
marginRight: '20px',
|
||||
marginTop: '30px',
|
||||
padding: '4px 18px',
|
||||
paddingBottom: '16px',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const descriptionStyle = style({
|
||||
fontSize: '1rem',
|
||||
wordWrap: 'break-word',
|
||||
// wordBreak: 'break-all',
|
||||
fontSize: '16px',
|
||||
marginTop: '16px',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
export const buttonStyle = style({
|
||||
@@ -30,5 +35,32 @@ export const actionsStyle = style({
|
||||
gap: '9px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'start',
|
||||
alignItems: 'flex-start',
|
||||
});
|
||||
|
||||
export const containerStyle = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const indicatorContainerStyle = style({
|
||||
position: 'relative',
|
||||
});
|
||||
export const inputButtonRowStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '16px',
|
||||
});
|
||||
export const exportButtonStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
});
|
||||
export const svgStyle = style({
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
verticalAlign: 'top',
|
||||
});
|
||||
|
||||
@@ -1,100 +1,2 @@
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { ExportIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Menu } from '../..';
|
||||
import { Export } from './Export';
|
||||
import { tabStyle } from './index.css';
|
||||
import { SharePage } from './SharePage';
|
||||
import { ShareWorkspace } from './ShareWorkspace';
|
||||
import { StyledIndicator, StyledShareButton, TabItem } from './styles';
|
||||
|
||||
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
|
||||
const MenuItems: Record<SharePanel, FC<ShareMenuProps>> = {
|
||||
SharePage: SharePage,
|
||||
Export: Export,
|
||||
ShareWorkspace: ShareWorkspace,
|
||||
};
|
||||
|
||||
export type ShareMenuProps<
|
||||
Workspace extends AffineWorkspace | LocalWorkspace =
|
||||
| AffineWorkspace
|
||||
| LocalWorkspace
|
||||
> = {
|
||||
workspace: Workspace;
|
||||
currentPage: Page;
|
||||
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
|
||||
onOpenWorkspaceSettings: (workspace: Workspace) => void;
|
||||
togglePagePublic: (page: Page, publish: boolean) => Promise<void>;
|
||||
toggleWorkspacePublish: (
|
||||
workspace: Workspace,
|
||||
publish: boolean
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ShareMenu: FC<ShareMenuProps> = props => {
|
||||
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleMenuChange = useCallback((selectedItem: SharePanel) => {
|
||||
setActiveItem(selectedItem);
|
||||
}, []);
|
||||
|
||||
const ActiveComponent = MenuItems[activeItem];
|
||||
interface ShareMenuProps {
|
||||
activeItem: SharePanel;
|
||||
onChangeTab: (selectedItem: SharePanel) => void;
|
||||
}
|
||||
const ShareMenu: FC<ShareMenuProps> = ({ activeItem, onChangeTab }) => {
|
||||
const handleButtonClick = (itemName: SharePanel) => {
|
||||
onChangeTab(itemName);
|
||||
setActiveItem(itemName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={tabStyle}>
|
||||
{Object.keys(MenuItems).map(item => (
|
||||
<TabItem
|
||||
isActive={activeItem === item}
|
||||
key={item}
|
||||
onClick={() => handleButtonClick(item as SharePanel)}
|
||||
>
|
||||
{item}
|
||||
</TabItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const activeIndex = Object.keys(MenuItems).indexOf(activeItem);
|
||||
const Share = (
|
||||
<>
|
||||
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
|
||||
<StyledIndicator activeIndex={activeIndex} />
|
||||
<ActiveComponent {...props} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
content={Share}
|
||||
visible={open}
|
||||
width={439}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<StyledShareButton
|
||||
data-testid="share-menu-button"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<ExportIcon />
|
||||
<div>Share</div>
|
||||
</StyledShareButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
export * from './disable-public-link';
|
||||
export * from './ShareMenu';
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { displayFlex, styled, TextButton } from '../..';
|
||||
import { Button, displayFlex, styled, TextButton } from '../..';
|
||||
|
||||
export const StyledShareButton = styled(TextButton)(({ theme }) => {
|
||||
export const StyledShareButton = styled(TextButton, {
|
||||
shouldForwardProp: (prop: string) => prop !== 'isShared',
|
||||
})<{ isShared?: boolean }>(({ theme, isShared }) => {
|
||||
return {
|
||||
padding: '4px 8px',
|
||||
marginLeft: '4px',
|
||||
marginRight: '16px',
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
color: theme.colors.primaryColor,
|
||||
border: `1px solid ${
|
||||
isShared ? theme.colors.primaryColor : theme.colors.iconColor
|
||||
}`,
|
||||
color: isShared ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||
borderRadius: '8px',
|
||||
':hover': {
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
span: {
|
||||
...displayFlex('center', 'center'),
|
||||
},
|
||||
@@ -26,21 +33,41 @@ export const TabItem = styled('li')<{ isActive?: boolean }>(
|
||||
{
|
||||
return {
|
||||
...displayFlex('center', 'center'),
|
||||
width: 'calc(100% / 3)',
|
||||
height: '34px',
|
||||
flex: '1',
|
||||
height: '30px',
|
||||
color: theme.colors.textColor,
|
||||
opacity: isActive ? 1 : 0.2,
|
||||
fontWeight: '500',
|
||||
fontSize: theme.font.h6,
|
||||
fontSize: theme.font.base,
|
||||
lineHeight: theme.font.lineHeight,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
padding: '0 10px',
|
||||
marginBottom: '4px',
|
||||
borderRadius: '4px',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
background: theme.colors.hoverBackground,
|
||||
opacity: 1,
|
||||
color: isActive
|
||||
? theme.colors.textColor
|
||||
: theme.colors.secondaryTextColor,
|
||||
svg: {
|
||||
fill: isActive
|
||||
? theme.colors.textColor
|
||||
: theme.colors.secondaryTextColor,
|
||||
},
|
||||
},
|
||||
svg: {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
bottom: '-2px',
|
||||
left: '-2px',
|
||||
width: 'calc(100% + 4px)',
|
||||
bottom: '-6px',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: theme.colors.textColor,
|
||||
opacity: 0.2,
|
||||
@@ -49,16 +76,54 @@ export const TabItem = styled('li')<{ isActive?: boolean }>(
|
||||
}
|
||||
}
|
||||
);
|
||||
export const StyledIndicator = styled('div')<{ activeIndex: number }>(
|
||||
({ theme, activeIndex }) => {
|
||||
return {
|
||||
height: '2px',
|
||||
margin: '0 10px',
|
||||
background: theme.colors.textColor,
|
||||
position: 'absolute',
|
||||
left: `calc(${activeIndex * 100}% / 3)`,
|
||||
width: `calc(100% / 3)`,
|
||||
transition: 'left .3s, width .3s',
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledIndicator = styled('div')(({ theme }) => {
|
||||
return {
|
||||
height: '2px',
|
||||
background: theme.colors.textColor,
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
transition: 'left .3s, width .3s',
|
||||
};
|
||||
});
|
||||
export const StyledInput = styled('input')(({ theme }) => {
|
||||
return {
|
||||
padding: '4px 8px',
|
||||
height: '28px',
|
||||
color: theme.colors.placeHolderColor,
|
||||
border: `1px solid ${theme.colors.placeHolderColor}`,
|
||||
cursor: 'default',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'text',
|
||||
borderRadius: '4px',
|
||||
flexGrow: 1,
|
||||
marginRight: '10px',
|
||||
};
|
||||
});
|
||||
export const StyledButton = styled(TextButton)(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.primaryColor,
|
||||
height: '32px',
|
||||
background: '#F3F0FF',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '4px 20px',
|
||||
};
|
||||
});
|
||||
export const StyledDisableButton = styled(Button)(() => {
|
||||
return {
|
||||
color: '#FF631F',
|
||||
height: '32px',
|
||||
border: 'none',
|
||||
marginTop: '16px',
|
||||
borderRadius: '8px',
|
||||
padding: '0',
|
||||
};
|
||||
});
|
||||
export const StyledLinkSpan = styled('span')(({ theme }) => {
|
||||
return {
|
||||
marginLeft: '4px',
|
||||
color: theme.colors.primaryColor,
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Page } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
import { ShareMenu } from '../components/share-menu';
|
||||
import { ShareMenu } from '../components/share-menu/ShareMenu';
|
||||
import toast from '../ui/toast/toast';
|
||||
|
||||
export default {
|
||||
|
||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -4,7 +4,7 @@
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"devDependencies": {
|
||||
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"next": "=13.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -204,5 +204,11 @@
|
||||
"Discover what's new!": "Discover what's new!",
|
||||
"Navigation Path": "Navigation Path",
|
||||
"View Navigation Path": "View Navigation Path",
|
||||
"Back to Quick Search": "Back to Quick Search"
|
||||
"Back to Quick Search": "Back to Quick Search",
|
||||
"Shared Pages": "Shared Pages",
|
||||
"Disable Public Sharing": "Disable Public Sharing",
|
||||
"Disable": "Disable",
|
||||
"Disable Public Link ?": "Disable Public Link ?",
|
||||
"Disable Public Link Description": "Disabling this public link will prevent anyone with the link from accessing this page.",
|
||||
"Export Shared Pages Description": "Download a static copy of your page to share with others."
|
||||
}
|
||||
|
||||
@@ -381,7 +381,9 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
||||
method: 'GET',
|
||||
}
|
||||
).then(r =>
|
||||
r.ok ? r.arrayBuffer() : Promise.reject(new Error(`${r.status}`))
|
||||
r.ok
|
||||
? r.arrayBuffer()
|
||||
: Promise.reject(new RequestError(MessageCode.noPermission))
|
||||
);
|
||||
},
|
||||
downloadWorkspace: async (
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"idb": "^7.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
|
||||
"vite": "^4.2.1",
|
||||
"vite-plugin-dts": "^2.2.0",
|
||||
"y-indexeddb": "^9.0.10"
|
||||
|
||||
Reference in New Issue
Block a user