mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 02:21:49 +03:00
feat: single page sharing support (#1805)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
parent
f3af128baf
commit
2e823c2fee
@ -106,6 +106,7 @@ export const Header = forwardRef<
|
|||||||
}),
|
}),
|
||||||
[rightItems]
|
[rightItems]
|
||||||
)}
|
)}
|
||||||
|
{/*<ShareMenu />*/}
|
||||||
</StyledHeaderRightSide>
|
</StyledHeaderRightSide>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
</StyledHeaderContainer>
|
</StyledHeaderContainer>
|
||||||
|
@ -109,7 +109,11 @@ export const BlockSuiteEditorHeader = forwardRef<
|
|||||||
? ['themeModeSwitch']
|
? ['themeModeSwitch']
|
||||||
: isTrash
|
: isTrash
|
||||||
? ['trashButtonGroup']
|
? ['trashButtonGroup']
|
||||||
: ['syncUser', 'themeModeSwitch', 'editorOptionMenu']
|
: [
|
||||||
|
'syncUser',
|
||||||
|
/* 'shareMenu', */ 'themeModeSwitch',
|
||||||
|
'editorOptionMenu',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
40
packages/component/src/components/share-menu/Export.tsx
Normal file
40
packages/component/src/components/share-menu/Export.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '../..';
|
||||||
|
import type { ShareMenuProps } from './index';
|
||||||
|
import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||||
|
|
||||||
|
export const Export: FC<ShareMenuProps> = props => {
|
||||||
|
const contentParserRef = useRef<ContentParser>();
|
||||||
|
return (
|
||||||
|
<div className={menuItemStyle}>
|
||||||
|
<div className={descriptionStyle}>
|
||||||
|
Download a static copy of your page to share with others.
|
||||||
|
</div>
|
||||||
|
<div className={actionsStyle}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (!contentParserRef.current) {
|
||||||
|
contentParserRef.current = new ContentParser(props.currentPage);
|
||||||
|
}
|
||||||
|
return contentParserRef.current.onExportHtml();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export to HTML
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (!contentParserRef.current) {
|
||||||
|
contentParserRef.current = new ContentParser(props.currentPage);
|
||||||
|
}
|
||||||
|
return contentParserRef.current.onExportMarkdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export to Markdown
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
70
packages/component/src/components/share-menu/SharePage.tsx
Normal file
70
packages/component/src/components/share-menu/SharePage.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { getEnvironment } from '@affine/env';
|
||||||
|
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 { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '../..';
|
||||||
|
import type { ShareMenuProps } from './index';
|
||||||
|
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||||
|
|
||||||
|
export const LocalSharePage: FC<ShareMenuProps> = props => {
|
||||||
|
return (
|
||||||
|
<div className={menuItemStyle}>
|
||||||
|
<div className={descriptionStyle}>
|
||||||
|
Sharing page publicly requires AFFiNE Cloud service.
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AffineSharePage: FC<ShareMenuProps> = props => {
|
||||||
|
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
|
||||||
|
props.currentPage
|
||||||
|
);
|
||||||
|
const sharingUrl = useMemo(() => {
|
||||||
|
const env = getEnvironment();
|
||||||
|
if (env.isBrowser) {
|
||||||
|
return `${env.origin}/public-workspace/${props.workspace.id}/${props.currentPage.id}`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [props.workspace.id, props.currentPage.id]);
|
||||||
|
const onClickCreateLink = useCallback(() => {
|
||||||
|
setIsPublic(true);
|
||||||
|
}, [isPublic]);
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SharePage: FC<ShareMenuProps> = props => {
|
||||||
|
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
return <LocalSharePage {...props} />;
|
||||||
|
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||||
|
return <AffineSharePage {...props} />;
|
||||||
|
}
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
};
|
@ -0,0 +1,64 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
|
||||||
|
return (
|
||||||
|
<div className={menuItemStyle}>
|
||||||
|
<div className={descriptionStyle}>
|
||||||
|
Sharing page publicly requires AFFiNE Cloud service.
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
data-testid="share-menu-enable-affine-cloud-button"
|
||||||
|
className={buttonStyle}
|
||||||
|
type="light"
|
||||||
|
shape="circle"
|
||||||
|
onClick={() => {
|
||||||
|
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enable AFFiNE Cloud
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
|
||||||
|
const isPublicWorkspace = props.workspace.public;
|
||||||
|
return (
|
||||||
|
<div className={menuItemStyle}>
|
||||||
|
<div className={descriptionStyle}>
|
||||||
|
{isPublicWorkspace
|
||||||
|
? `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
|
||||||
|
data-testid="share-menu-publish-to-web-button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onOpenWorkspaceSettings(props.workspace);
|
||||||
|
}}
|
||||||
|
type="light"
|
||||||
|
shape="circle"
|
||||||
|
>
|
||||||
|
Open Workspace Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareWorkspace: FC<ShareMenuProps> = props => {
|
||||||
|
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
return (
|
||||||
|
<ShareLocalWorkspace {...(props as ShareMenuProps<LocalWorkspace>)} />
|
||||||
|
);
|
||||||
|
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||||
|
return (
|
||||||
|
<ShareAffineWorkspace {...(props as ShareMenuProps<AffineWorkspace>)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
};
|
34
packages/component/src/components/share-menu/index.css.ts
Normal file
34
packages/component/src/components/share-menu/index.css.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const tabStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
marginTop: '4px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
marginRight: '10px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menuItemStyle = style({
|
||||||
|
marginLeft: '20px',
|
||||||
|
marginRight: '20px',
|
||||||
|
marginTop: '30px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const descriptionStyle = style({
|
||||||
|
fontSize: '1rem',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buttonStyle = style({
|
||||||
|
marginTop: '18px',
|
||||||
|
// todo: new color scheme should be used
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionsStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '9px',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'start',
|
||||||
|
});
|
100
packages/component/src/components/share-menu/index.tsx
Normal file
100
packages/component/src/components/share-menu/index.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
64
packages/component/src/components/share-menu/styles.ts
Normal file
64
packages/component/src/components/share-menu/styles.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { displayFlex, styled, TextButton } from '../..';
|
||||||
|
|
||||||
|
export const StyledShareButton = styled(TextButton)(({ theme }) => {
|
||||||
|
return {
|
||||||
|
padding: '4px 8px',
|
||||||
|
marginLeft: '4px',
|
||||||
|
marginRight: '16px',
|
||||||
|
border: `1px solid ${theme.colors.primaryColor}`,
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
borderRadius: '8px',
|
||||||
|
span: {
|
||||||
|
...displayFlex('center', 'center'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledTabsWrapper = styled('div')(() => {
|
||||||
|
return {
|
||||||
|
...displayFlex('space-around', 'center'),
|
||||||
|
position: 'relative',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TabItem = styled('li')<{ isActive?: boolean }>(
|
||||||
|
({ theme, isActive }) => {
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
...displayFlex('center', 'center'),
|
||||||
|
width: 'calc(100% / 3)',
|
||||||
|
height: '34px',
|
||||||
|
color: theme.colors.textColor,
|
||||||
|
opacity: isActive ? 1 : 0.2,
|
||||||
|
fontWeight: '500',
|
||||||
|
fontSize: theme.font.h6,
|
||||||
|
lineHeight: theme.font.lineHeight,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
':after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-2px',
|
||||||
|
left: '-2px',
|
||||||
|
width: 'calc(100% + 4px)',
|
||||||
|
height: '2px',
|
||||||
|
background: theme.colors.textColor,
|
||||||
|
opacity: 0.2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
@ -12,6 +12,7 @@ export * from './ui/modal';
|
|||||||
export * from './ui/mui';
|
export * from './ui/mui';
|
||||||
export * from './ui/popper';
|
export * from './ui/popper';
|
||||||
export * from './ui/shared/Container';
|
export * from './ui/shared/Container';
|
||||||
|
export * from './ui/switch';
|
||||||
export * from './ui/table';
|
export * from './ui/table';
|
||||||
export * from './ui/toast';
|
export * from './ui/toast';
|
||||||
export * from './ui/tooltip';
|
export * from './ui/tooltip';
|
||||||
|
102
packages/component/src/stories/ShareMenu.stories.tsx
Normal file
102
packages/component/src/stories/ShareMenu.stories.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { PermissionType, WorkspaceType } from '@affine/workspace/affine/api';
|
||||||
|
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||||
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import type { StoryFn } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ShareMenu } from '../components/share-menu';
|
||||||
|
import toast from '../ui/toast/toast';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'AFFiNE/ShareMenu',
|
||||||
|
component: ShareMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
function initPage(page: Page): void {
|
||||||
|
// Add page block and surface block at root level
|
||||||
|
const pageBlockId = page.addBlock('affine:page', {
|
||||||
|
title: new page.Text('Hello, world!'),
|
||||||
|
});
|
||||||
|
page.addBlock('affine:surface', {}, null);
|
||||||
|
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
|
||||||
|
page.addBlock(
|
||||||
|
'affine:paragraph',
|
||||||
|
{
|
||||||
|
text: new page.Text('This is a paragraph.'),
|
||||||
|
},
|
||||||
|
frameId
|
||||||
|
);
|
||||||
|
page.resetHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace('test-workspace');
|
||||||
|
|
||||||
|
initPage(blockSuiteWorkspace.createPage('page0'));
|
||||||
|
initPage(blockSuiteWorkspace.createPage('page1'));
|
||||||
|
initPage(blockSuiteWorkspace.createPage('page2'));
|
||||||
|
|
||||||
|
const localWorkspace: LocalWorkspace = {
|
||||||
|
id: 'test-workspace',
|
||||||
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
|
blockSuiteWorkspace,
|
||||||
|
providers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const affineWorkspace: AffineWorkspace = {
|
||||||
|
id: 'test-workspace',
|
||||||
|
flavour: WorkspaceFlavour.AFFINE,
|
||||||
|
blockSuiteWorkspace,
|
||||||
|
providers: [],
|
||||||
|
public: false,
|
||||||
|
type: WorkspaceType.Normal,
|
||||||
|
permission: PermissionType.Owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function unimplemented() {
|
||||||
|
toast('work in progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Basic: StoryFn = () => {
|
||||||
|
return (
|
||||||
|
<ShareMenu
|
||||||
|
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||||
|
workspace={localWorkspace}
|
||||||
|
onEnableAffineCloud={unimplemented}
|
||||||
|
onOpenWorkspaceSettings={unimplemented}
|
||||||
|
togglePagePublic={unimplemented}
|
||||||
|
toggleWorkspacePublish={unimplemented}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Basic.play = async ({ canvasElement }) => {
|
||||||
|
{
|
||||||
|
const button = canvasElement.querySelector(
|
||||||
|
'[data-testid="share-menu-button"]'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
button.click();
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
{
|
||||||
|
const button = canvasElement.querySelector(
|
||||||
|
'[data-testid="share-menu-enable-affine-cloud-button"]'
|
||||||
|
);
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AffineBasic: StoryFn = () => {
|
||||||
|
return (
|
||||||
|
<ShareMenu
|
||||||
|
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||||
|
workspace={affineWorkspace}
|
||||||
|
onEnableAffineCloud={unimplemented}
|
||||||
|
onOpenWorkspaceSettings={unimplemented}
|
||||||
|
togglePagePublic={unimplemented}
|
||||||
|
toggleWorkspacePublish={unimplemented}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
16
packages/component/src/stories/Switch.stories.tsx
Normal file
16
packages/component/src/stories/Switch.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||||
|
import type { StoryFn } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Switch } from '..';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'AFFiNE/Switch',
|
||||||
|
component: Switch,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Basic: StoryFn = () => {
|
||||||
|
return <Switch />;
|
||||||
|
};
|
||||||
|
Basic.args = {
|
||||||
|
logoSrc: '/imgs/affine-text-logo.png',
|
||||||
|
};
|
@ -117,7 +117,6 @@ export const StyledTextButton = styled('button', {
|
|||||||
// type = 'default',
|
// type = 'default',
|
||||||
}) => {
|
}) => {
|
||||||
const { fontSize, borderRadius, padding, height } = getSize(size);
|
const { fontSize, borderRadius, padding, height } = getSize(size);
|
||||||
console.log('size', size, height);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
height,
|
height,
|
||||||
|
80
packages/component/src/ui/switch/Switch.tsx
Normal file
80
packages/component/src/ui/switch/Switch.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// components/Switch.tsx
|
||||||
|
import { styled } from '@affine/component';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const StyledLabel = styled('label')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const StyledInput = styled('input')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
|
||||||
|
'&:checked': {
|
||||||
|
'& + span': {
|
||||||
|
background: '#6880FF',
|
||||||
|
'&:before': {
|
||||||
|
transform: 'translate(28px, -50%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const StyledSwitch = styled('span')(() => {
|
||||||
|
return {
|
||||||
|
position: 'relative',
|
||||||
|
width: '60px',
|
||||||
|
height: '28px',
|
||||||
|
background: '#b3b3b3',
|
||||||
|
borderRadius: '32px',
|
||||||
|
padding: '4px',
|
||||||
|
transition: '300ms all',
|
||||||
|
|
||||||
|
'&:before': {
|
||||||
|
transition: '300ms all',
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
borderRadius: '35px',
|
||||||
|
top: '50%',
|
||||||
|
left: '4px',
|
||||||
|
background: 'white',
|
||||||
|
transform: 'translate(-4px, -50%)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type SwitchProps = {
|
||||||
|
checked?: boolean;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Switch = (props: SwitchProps) => {
|
||||||
|
const { checked, onChange, children } = props;
|
||||||
|
const [isChecked, setIsChecked] = useState(checked);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newChecked = event.target.checked;
|
||||||
|
setIsChecked(newChecked);
|
||||||
|
onChange?.(newChecked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledLabel>
|
||||||
|
{children}
|
||||||
|
<StyledInput
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<StyledSwitch />
|
||||||
|
</StyledLabel>
|
||||||
|
);
|
||||||
|
};
|
1
packages/component/src/ui/switch/index.ts
Normal file
1
packages/component/src/ui/switch/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Switch';
|
65
packages/env/src/index.ts
vendored
65
packages/env/src/index.ts
vendored
@ -4,7 +4,41 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { getUaHelper } from './ua-helper';
|
import { getUaHelper } from './ua-helper';
|
||||||
|
|
||||||
|
export const publicRuntimeConfigSchema = z.object({
|
||||||
|
PROJECT_NAME: z.string(),
|
||||||
|
BUILD_DATE: z.string(),
|
||||||
|
gitVersion: z.string(),
|
||||||
|
hash: z.string(),
|
||||||
|
serverAPI: z.string(),
|
||||||
|
editorVersion: z.string(),
|
||||||
|
enableIndexedDBProvider: z.boolean(),
|
||||||
|
enableBroadCastChannelProvider: z.boolean(),
|
||||||
|
prefetchWorkspace: z.boolean(),
|
||||||
|
enableDebugPage: z.boolean(),
|
||||||
|
// expose internal api to globalThis, **development only**
|
||||||
|
exposeInternal: z.boolean(),
|
||||||
|
enableSubpage: z.boolean(),
|
||||||
|
enableChangeLog: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PublicRuntimeConfig = z.infer<typeof publicRuntimeConfigSchema>;
|
||||||
|
|
||||||
|
const { publicRuntimeConfig: config } =
|
||||||
|
getConfig() ??
|
||||||
|
({
|
||||||
|
publicRuntimeConfig: {},
|
||||||
|
} as {
|
||||||
|
publicRuntimeConfig: PublicRuntimeConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
publicRuntimeConfigSchema.parse(config);
|
||||||
|
|
||||||
type BrowserBase = {
|
type BrowserBase = {
|
||||||
|
/**
|
||||||
|
* @example https://app.affine.pro
|
||||||
|
* @example http://localhost:3000
|
||||||
|
*/
|
||||||
|
origin: string;
|
||||||
isDesktop: boolean;
|
isDesktop: boolean;
|
||||||
isBrowser: true;
|
isBrowser: true;
|
||||||
isServer: false;
|
isServer: false;
|
||||||
@ -66,7 +100,9 @@ export function getEnvironment() {
|
|||||||
} satisfies Server;
|
} satisfies Server;
|
||||||
} else {
|
} else {
|
||||||
const uaHelper = getUaHelper();
|
const uaHelper = getUaHelper();
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
|
origin: window.location.origin,
|
||||||
isDesktop: window.appInfo?.electron,
|
isDesktop: window.appInfo?.electron,
|
||||||
isBrowser: true,
|
isBrowser: true,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
@ -97,35 +133,6 @@ export function getEnvironment() {
|
|||||||
return environment;
|
return environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicRuntimeConfigSchema = z.object({
|
|
||||||
PROJECT_NAME: z.string(),
|
|
||||||
BUILD_DATE: z.string(),
|
|
||||||
gitVersion: z.string(),
|
|
||||||
hash: z.string(),
|
|
||||||
serverAPI: z.string(),
|
|
||||||
editorVersion: z.string(),
|
|
||||||
enableIndexedDBProvider: z.boolean(),
|
|
||||||
enableBroadCastChannelProvider: z.boolean(),
|
|
||||||
prefetchWorkspace: z.boolean(),
|
|
||||||
enableDebugPage: z.boolean(),
|
|
||||||
// expose internal api to globalThis, **development only**
|
|
||||||
exposeInternal: z.boolean(),
|
|
||||||
enableSubpage: z.boolean(),
|
|
||||||
enableChangeLog: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PublicRuntimeConfig = z.infer<typeof publicRuntimeConfigSchema>;
|
|
||||||
|
|
||||||
const { publicRuntimeConfig: config } =
|
|
||||||
getConfig() ??
|
|
||||||
({
|
|
||||||
publicRuntimeConfig: {},
|
|
||||||
} as {
|
|
||||||
publicRuntimeConfig: PublicRuntimeConfig;
|
|
||||||
});
|
|
||||||
|
|
||||||
publicRuntimeConfigSchema.parse(config);
|
|
||||||
|
|
||||||
function printBuildInfo() {
|
function printBuildInfo() {
|
||||||
console.group('Build info');
|
console.group('Build info');
|
||||||
console.log('Project:', config.PROJECT_NAME);
|
console.log('Project:', config.PROJECT_NAME);
|
||||||
|
@ -8,6 +8,7 @@ import type { Page } from '@blocksuite/store';
|
|||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||||
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-blocksuite-workspace-page-title';
|
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-blocksuite-workspace-page-title';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import { beforeEach } from 'vitest';
|
import { beforeEach } from 'vitest';
|
||||||
@ -60,3 +61,16 @@ describe('useBlockSuiteWorkspacePageTitle', () => {
|
|||||||
expect(pageTitleHook.result.current).toBe('1');
|
expect(pageTitleHook.result.current).toBe('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useBlockSuiteWorkspacePageIsPublic', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
const page = blockSuiteWorkspace.getPage('page0') as Page;
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
const hook = renderHook(() => useBlockSuiteWorkspacePageIsPublic(page));
|
||||||
|
expect(hook.result.current[0]).toBe(false);
|
||||||
|
hook.result.current[1](true);
|
||||||
|
expect(page.meta.isPublic).toBe(true);
|
||||||
|
hook.rerender();
|
||||||
|
expect(hook.result.current[0]).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
declare module '@blocksuite/store' {
|
||||||
|
interface PageMeta {
|
||||||
|
isPublic?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlockSuiteWorkspacePageIsPublic(page: Page) {
|
||||||
|
const [isPublic, set] = useState<boolean>(() => page.meta.isPublic ?? false);
|
||||||
|
useEffect(() => {
|
||||||
|
page.workspace.meta.pageMetasUpdated.on(() => {
|
||||||
|
set(page.meta.isPublic ?? false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const setIsPublic = useCallback((isPublic: boolean) => {
|
||||||
|
set(isPublic);
|
||||||
|
page.workspace.setPageMeta(page.id, {
|
||||||
|
isPublic,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return [isPublic, setIsPublic] as const;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user