mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): support share edgeless mode (#4856)
Close #3287 <!-- copilot:all --> ### <samp>🤖 Generated by Copilot at d3fdf86</samp> ### Summary 📄🚀🔗 <!-- 1. 📄 - This emoji represents the page and edgeless modes of sharing a page, as well as the GraphQL operations and types related to public pages. 2. 🚀 - This emoji represents the functionality of publishing and revoking public pages, as well as the confirmation modal and the notifications for the user. 3. 🔗 - This emoji represents the sharing URL and the query parameter for the share mode, as well as the hooks and functions that generate and use the URL. --> This pull request adds a feature to the frontend component of AFFiNE that allows the user to share a page in either `page` or `edgeless` mode, which affects the appearance and functionality of the page. It also adds the necessary GraphQL operations, types, and schema to support this feature in the backend, and updates the tests and the storybook stories accordingly. * Modify the `useIsSharedPage` hook to accept an optional `shareMode` argument and use the `getWorkspacePublicPagesQuery`, `publishPageMutation`, and `revokePublicPageMutation` from `@affine/graphql`
This commit is contained in:
@@ -12,12 +12,13 @@ import {
|
|||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import format from 'pretty-time';
|
import format from 'pretty-time';
|
||||||
|
|
||||||
|
import { PrismaService } from '../../prisma';
|
||||||
import { StorageProvide } from '../../storage';
|
import { StorageProvide } from '../../storage';
|
||||||
import { DocID } from '../../utils/doc';
|
import { DocID } from '../../utils/doc';
|
||||||
import { Auth, CurrentUser, Publicable } from '../auth';
|
import { Auth, CurrentUser, Publicable } from '../auth';
|
||||||
import { DocManager } from '../doc';
|
import { DocManager } from '../doc';
|
||||||
import { UserType } from '../users';
|
import { UserType } from '../users';
|
||||||
import { PermissionService } from './permission';
|
import { PermissionService, PublicPageMode } from './permission';
|
||||||
|
|
||||||
@Controller('/api/workspaces')
|
@Controller('/api/workspaces')
|
||||||
export class WorkspacesController {
|
export class WorkspacesController {
|
||||||
@@ -26,7 +27,8 @@ export class WorkspacesController {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(StorageProvide) private readonly storage: Storage,
|
@Inject(StorageProvide) private readonly storage: Storage,
|
||||||
private readonly permission: PermissionService,
|
private readonly permission: PermissionService,
|
||||||
private readonly docManager: DocManager
|
private readonly docManager: DocManager,
|
||||||
|
private readonly prisma: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// get workspace blob
|
// get workspace blob
|
||||||
@@ -82,6 +84,22 @@ export class WorkspacesController {
|
|||||||
throw new NotFoundException('Doc not found');
|
throw new NotFoundException('Doc not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!docId.isWorkspace) {
|
||||||
|
// fetch the publish page mode for publish page
|
||||||
|
const publishPage = await this.prisma.workspacePage.findUnique({
|
||||||
|
where: {
|
||||||
|
workspaceId_pageId: {
|
||||||
|
workspaceId: docId.workspace,
|
||||||
|
pageId: docId.guid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const publishPageMode =
|
||||||
|
publishPage?.mode === PublicPageMode.Edgeless ? 'edgeless' : 'page';
|
||||||
|
|
||||||
|
res.setHeader('publish-mode', publishPageMode);
|
||||||
|
}
|
||||||
|
|
||||||
res.setHeader('content-type', 'application/octet-stream');
|
res.setHeader('content-type', 'application/octet-stream');
|
||||||
res.send(update);
|
res.send(update);
|
||||||
this.logger.debug(`workspaces doc api: ${format(process.hrtime(start))}`);
|
this.logger.debug(`workspaces doc api: ${format(process.hrtime(start))}`);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type MenuItemProps,
|
type MenuItemProps,
|
||||||
} from '@toeverything/components/menu';
|
} from '@toeverything/components/menu';
|
||||||
|
|
||||||
import { PublicLinkDisableModal } from '../../share-menu';
|
import { PublicLinkDisableModal } from '../../disable-public-link';
|
||||||
|
|
||||||
export const DisablePublicSharing = (props: MenuItemProps) => {
|
export const DisablePublicSharing = (props: MenuItemProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './disable-public-link';
|
|
||||||
export * from './share-menu';
|
|
||||||
export * from './styles';
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { Button } from '@toeverything/components/button';
|
|
||||||
|
|
||||||
import { displayFlex, styled } from '../..';
|
|
||||||
|
|
||||||
export const TabItem = styled('li')<{ isActive?: boolean }>(({ isActive }) => {
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
...displayFlex('center', 'center'),
|
|
||||||
flex: '1',
|
|
||||||
height: '30px',
|
|
||||||
color: 'var(--affine-text-primary-color)',
|
|
||||||
opacity: isActive ? 1 : 0.2,
|
|
||||||
fontWeight: '500',
|
|
||||||
fontSize: 'var(--affine-font-base)',
|
|
||||||
lineHeight: 'var(--affine-line-height)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.15s ease',
|
|
||||||
padding: '0 10px',
|
|
||||||
marginBottom: '4px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
position: 'relative',
|
|
||||||
':hover': {
|
|
||||||
background: 'var(--affine-hover-color)',
|
|
||||||
opacity: 1,
|
|
||||||
color: isActive
|
|
||||||
? 'var(--affine-t/ext-primary-color)'
|
|
||||||
: 'var(--affine-text-secondary-color)',
|
|
||||||
svg: {
|
|
||||||
fill: isActive
|
|
||||||
? 'var(--affine-text-primary-color)'
|
|
||||||
: 'var(--affine-text-secondary-color)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
fontSize: '20px',
|
|
||||||
marginRight: '12px',
|
|
||||||
},
|
|
||||||
':after': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '-6px',
|
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '2px',
|
|
||||||
background: 'var(--affine-text-primary-color)',
|
|
||||||
opacity: 0.2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export const StyledIndicator = styled('div')(() => {
|
|
||||||
return {
|
|
||||||
height: '2px',
|
|
||||||
background: 'var(--affine-text-primary-color)',
|
|
||||||
position: 'absolute',
|
|
||||||
left: '0',
|
|
||||||
transition: 'left .3s, width .3s',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export const StyledInput = styled('input')(() => {
|
|
||||||
return {
|
|
||||||
padding: '4px 8px',
|
|
||||||
height: '28px',
|
|
||||||
color: 'var(--affine-placeholder-color)',
|
|
||||||
border: `1px solid ${'var(--affine-placeholder-color)'}`,
|
|
||||||
cursor: 'default',
|
|
||||||
overflow: 'hidden',
|
|
||||||
userSelect: 'text',
|
|
||||||
borderRadius: '4px',
|
|
||||||
flexGrow: 1,
|
|
||||||
marginRight: '10px',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export const StyledDisableButton = styled(Button)(() => {
|
|
||||||
return {
|
|
||||||
color: '#FF631F',
|
|
||||||
height: '32px',
|
|
||||||
border: 'none',
|
|
||||||
marginTop: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '0',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export const StyledLinkSpan = styled('span')(() => {
|
|
||||||
return {
|
|
||||||
marginLeft: '4px',
|
|
||||||
color: 'var(--affine-primary-color)',
|
|
||||||
fontWeight: '500',
|
|
||||||
cursor: 'pointer',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -40,7 +40,7 @@ export const authAtom = atom<AuthAtom>({
|
|||||||
|
|
||||||
export const openDisableCloudAlertModalAtom = atom(false);
|
export const openDisableCloudAlertModalAtom = atom(false);
|
||||||
|
|
||||||
type PageMode = 'page' | 'edgeless';
|
export type PageMode = 'page' | 'edgeless';
|
||||||
type PageLocalSetting = {
|
type PageLocalSetting = {
|
||||||
mode: PageMode;
|
mode: PageMode;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ShareMenu } from '@affine/component/share-menu';
|
|
||||||
import {
|
import {
|
||||||
type AffineOfficialWorkspace,
|
type AffineOfficialWorkspace,
|
||||||
WorkspaceFlavour,
|
WorkspaceFlavour,
|
||||||
@@ -6,10 +5,9 @@ import {
|
|||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
|
||||||
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
|
|
||||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||||
|
import { ShareMenu } from './share-menu';
|
||||||
|
|
||||||
type SharePageModalProps = {
|
type SharePageModalProps = {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: AffineOfficialWorkspace;
|
||||||
@@ -19,7 +17,7 @@ type SharePageModalProps = {
|
|||||||
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||||
const onTransformWorkspace = useOnTransformWorkspace();
|
const onTransformWorkspace = useOnTransformWorkspace();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const exportHandler = useExportPage(page);
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
return;
|
return;
|
||||||
@@ -31,15 +29,13 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
|||||||
);
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [onTransformWorkspace, workspace]);
|
}, [onTransformWorkspace, workspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
useIsSharedPage={useIsSharedPage}
|
|
||||||
onEnableAffineCloud={() => setOpen(true)}
|
onEnableAffineCloud={() => setOpen(true)}
|
||||||
togglePagePublic={async () => {}}
|
|
||||||
exportHandler={exportHandler}
|
|
||||||
/>
|
/>
|
||||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||||
<EnableAffineCloudModal
|
<EnableAffineCloudModal
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './share-menu';
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
|
import { ExportMenuItems } from '@affine/component/page-list';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { LinkIcon } from '@blocksuite/icons';
|
import { LinkIcon } from '@blocksuite/icons';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { Divider } from '@toeverything/components/divider';
|
import { Divider } from '@toeverything/components/divider';
|
||||||
|
|
||||||
import { ExportMenuItems } from '../page-list/operation-menu-items/export';
|
import { useExportPage } from '../../../../hooks/affine/use-export-page';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
import { useSharingUrl } from './use-share-url';
|
||||||
|
|
||||||
export const ShareExport = ({
|
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => {
|
||||||
workspace,
|
|
||||||
currentPage,
|
|
||||||
exportHandler,
|
|
||||||
}: ShareMenuProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspaceId = workspace.id;
|
const workspaceId = workspace.id;
|
||||||
const pageId = currentPage.id;
|
const pageId = currentPage.id;
|
||||||
@@ -22,6 +19,7 @@ export const ShareExport = ({
|
|||||||
pageId,
|
pageId,
|
||||||
urlType: 'workspace',
|
urlType: 'workspace',
|
||||||
});
|
});
|
||||||
|
const exportHandler = useExportPage(currentPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -12,9 +12,11 @@ import { Button } from '@toeverything/components/button';
|
|||||||
import { Divider } from '@toeverything/components/divider';
|
import { Divider } from '@toeverything/components/divider';
|
||||||
import { Menu } from '@toeverything/components/menu';
|
import { Menu } from '@toeverything/components/menu';
|
||||||
|
|
||||||
|
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { ShareExport } from './share-export';
|
import { ShareExport } from './share-export';
|
||||||
import { SharePage } from './share-page';
|
import { SharePage } from './share-page';
|
||||||
|
|
||||||
export interface ShareMenuProps<
|
export interface ShareMenuProps<
|
||||||
Workspace extends AffineOfficialWorkspace =
|
Workspace extends AffineOfficialWorkspace =
|
||||||
| AffineCloudWorkspace
|
| AffineCloudWorkspace
|
||||||
@@ -23,13 +25,7 @@ export interface ShareMenuProps<
|
|||||||
> {
|
> {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
currentPage: Page;
|
currentPage: Page;
|
||||||
useIsSharedPage: (
|
|
||||||
workspaceId: string,
|
|
||||||
pageId: string
|
|
||||||
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
|
|
||||||
onEnableAffineCloud: () => void;
|
onEnableAffineCloud: () => void;
|
||||||
togglePagePublic: () => Promise<void>;
|
|
||||||
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
const ShareMenuContent = (props: ShareMenuProps) => {
|
||||||
@@ -73,9 +69,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
|||||||
|
|
||||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const {
|
||||||
const { workspace, currentPage, useIsSharedPage } = props;
|
workspace: { id: workspaceId },
|
||||||
const [isSharedPage] = useIsSharedPage(workspace.id, currentPage.id);
|
currentPage,
|
||||||
|
} = props;
|
||||||
|
const { isSharedPage } = useIsSharedPage(
|
||||||
|
workspaceId,
|
||||||
|
currentPage.spaceDoc.guid
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Input,
|
||||||
|
RadioButton,
|
||||||
|
RadioButtonGroup,
|
||||||
|
Switch,
|
||||||
|
toast,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||||
import { useState } from 'react';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { RadioButton, RadioButtonGroup } from '../../ui/button';
|
import type { PageMode } from '../../../../atoms';
|
||||||
import Input from '../../ui/input';
|
import { currentModeAtom } from '../../../../atoms/mode';
|
||||||
import { Switch } from '../../ui/switch';
|
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||||
import { toast } from '../../ui/toast';
|
|
||||||
import { PublicLinkDisableModal } from './disable-public-link';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
import { useSharingUrl } from './use-share-url';
|
||||||
@@ -63,10 +70,29 @@ export const LocalSharePage = (props: ShareMenuProps) => {
|
|||||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||||
const {
|
const {
|
||||||
workspace: { id: workspaceId },
|
workspace: { id: workspaceId },
|
||||||
currentPage: { id: pageId },
|
currentPage,
|
||||||
} = props;
|
} = props;
|
||||||
const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId);
|
const pageId = currentPage.id;
|
||||||
const [showDisable, setShowDisable] = useState(false);
|
const [showDisable, setShowDisable] = useState(false);
|
||||||
|
const {
|
||||||
|
isSharedPage,
|
||||||
|
enableShare,
|
||||||
|
changeShare,
|
||||||
|
currentShareMode,
|
||||||
|
disableShare,
|
||||||
|
} = useIsSharedPage(workspaceId, currentPage.spaceDoc.guid);
|
||||||
|
const currentPageMode = useAtomValue(currentModeAtom);
|
||||||
|
|
||||||
|
const defaultMode = useMemo(() => {
|
||||||
|
if (isSharedPage) {
|
||||||
|
// if it's a shared page, use the share mode
|
||||||
|
return currentShareMode;
|
||||||
|
}
|
||||||
|
// default to current page mode
|
||||||
|
return currentPageMode;
|
||||||
|
}, [currentPageMode, currentShareMode, isSharedPage]);
|
||||||
|
const [mode, setMode] = useState<PageMode>(defaultMode);
|
||||||
|
|
||||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId,
|
pageId,
|
||||||
@@ -75,16 +101,26 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const onClickCreateLink = useCallback(() => {
|
const onClickCreateLink = useCallback(() => {
|
||||||
setIsPublic(true);
|
enableShare(mode);
|
||||||
}, [setIsPublic]);
|
}, [enableShare, mode]);
|
||||||
|
|
||||||
const onDisablePublic = useCallback(() => {
|
const onDisablePublic = useCallback(() => {
|
||||||
setIsPublic(false);
|
disableShare();
|
||||||
toast('Successfully disabled', {
|
toast('Successfully disabled', {
|
||||||
portal: document.body,
|
portal: document.body,
|
||||||
});
|
});
|
||||||
setShowDisable(false);
|
setShowDisable(false);
|
||||||
}, [setIsPublic]);
|
}, [disableShare]);
|
||||||
|
|
||||||
|
const onShareModeChange = useCallback(
|
||||||
|
(value: PageMode) => {
|
||||||
|
setMode(value);
|
||||||
|
if (isSharedPage) {
|
||||||
|
changeShare(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[changeShare, isSharedPage]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -103,10 +139,12 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
fontSize: 'var(--affine-font-xs)',
|
fontSize: 'var(--affine-font-xs)',
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
}}
|
}}
|
||||||
value={isPublic ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`}
|
value={
|
||||||
|
isSharedPage ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`
|
||||||
|
}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
{isPublic ? (
|
{isSharedPage ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={onClickCopyLink}
|
onClick={onClickCopyLink}
|
||||||
data-testid="share-menu-copy-link-button"
|
data-testid="share-menu-copy-link-button"
|
||||||
@@ -125,7 +163,6 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{runtimeConfig.enableEnhanceShareMode ? (
|
|
||||||
<div className={styles.rowContainerStyle}>
|
<div className={styles.rowContainerStyle}>
|
||||||
<div className={styles.subTitleStyle}>
|
<div className={styles.subTitleStyle}>
|
||||||
{t['com.affine.share-menu.ShareMode']()}
|
{t['com.affine.share-menu.ShareMode']()}
|
||||||
@@ -133,8 +170,9 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
className={styles.radioButtonGroup}
|
className={styles.radioButtonGroup}
|
||||||
defaultValue={'page'}
|
defaultValue={defaultMode}
|
||||||
onValueChange={() => {}}
|
value={mode}
|
||||||
|
onValueChange={onShareModeChange}
|
||||||
>
|
>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
className={styles.radioButton}
|
className={styles.radioButton}
|
||||||
@@ -153,8 +191,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{isSharedPage ? (
|
||||||
{isPublic ? (
|
|
||||||
<>
|
<>
|
||||||
{runtimeConfig.enableEnhanceShareMode && (
|
{runtimeConfig.enableEnhanceShareMode && (
|
||||||
<>
|
<>
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { toast } from '@affine/component';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { toast } from '../../ui/toast';
|
|
||||||
|
|
||||||
type UrlType = 'share' | 'workspace';
|
type UrlType = 'share' | 'workspace';
|
||||||
|
|
||||||
type UseSharingUrl = {
|
type UseSharingUrl = {
|
||||||
@@ -16,7 +15,14 @@ export const generateUrl = ({
|
|||||||
pageId,
|
pageId,
|
||||||
urlType,
|
urlType,
|
||||||
}: UseSharingUrl) => {
|
}: UseSharingUrl) => {
|
||||||
return `${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`;
|
// to generate a private url like https://affine.app/workspace/123/456
|
||||||
|
// to generate a public url like https://affine.app/share/123/456
|
||||||
|
// or https://affine.app/share/123/456?mode=edgeless
|
||||||
|
|
||||||
|
const url = new URL(
|
||||||
|
`${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`
|
||||||
|
);
|
||||||
|
return url.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSharingUrl = ({
|
export const useSharingUrl = ({
|
||||||
@@ -27,7 +33,7 @@ export const useSharingUrl = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const sharingUrl = useMemo(
|
const sharingUrl = useMemo(
|
||||||
() => generateUrl({ workspaceId, pageId, urlType }),
|
() => generateUrl({ workspaceId, pageId, urlType }),
|
||||||
[urlType, workspaceId, pageId]
|
[workspaceId, pageId, urlType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickCopyLink = useCallback(() => {
|
const onClickCopyLink = useCallback(() => {
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import {
|
import {
|
||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import type { PageMode } from '../../../atoms';
|
||||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||||
import { PageMenu } from './operation-menu';
|
import { PageMenu } from './operation-menu';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -20,6 +20,8 @@ import * as styles from './styles.css';
|
|||||||
export interface BlockSuiteHeaderTitleProps {
|
export interface BlockSuiteHeaderTitleProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: AffineOfficialWorkspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
isPublic?: boolean;
|
||||||
|
publicMode?: PageMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditableTitle = ({
|
const EditableTitle = ({
|
||||||
@@ -54,6 +56,8 @@ const StableTitle = ({
|
|||||||
workspace,
|
workspace,
|
||||||
pageId,
|
pageId,
|
||||||
onRename,
|
onRename,
|
||||||
|
isPublic,
|
||||||
|
publicMode,
|
||||||
}: BlockSuiteHeaderTitleProps & {
|
}: BlockSuiteHeaderTitleProps & {
|
||||||
onRename?: () => void;
|
onRename?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -64,11 +68,19 @@ const StableTitle = ({
|
|||||||
|
|
||||||
const title = pageMeta?.title;
|
const title = pageMeta?.title;
|
||||||
|
|
||||||
|
const handleRename = useCallback(() => {
|
||||||
|
if (!isPublic && onRename) {
|
||||||
|
onRename();
|
||||||
|
}
|
||||||
|
}, [isPublic, onRename]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.headerTitleContainer}>
|
<div className={styles.headerTitleContainer}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
|
isPublic={isPublic}
|
||||||
|
publicMode={publicMode}
|
||||||
style={{
|
style={{
|
||||||
marginRight: '12px',
|
marginRight: '12px',
|
||||||
}}
|
}}
|
||||||
@@ -76,11 +88,11 @@ const StableTitle = ({
|
|||||||
<span
|
<span
|
||||||
data-testid="title-edit-button"
|
data-testid="title-edit-button"
|
||||||
className={styles.titleEditButton}
|
className={styles.titleEditButton}
|
||||||
onDoubleClick={onRename}
|
onDoubleClick={handleRename}
|
||||||
>
|
>
|
||||||
{title || 'Untitled'}
|
{title || 'Untitled'}
|
||||||
</span>
|
</span>
|
||||||
<PageMenu rename={onRename} pageId={pageId} />
|
{isPublic ? null : <PageMenu rename={onRename} pageId={pageId} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -139,7 +151,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
|
if (props.isPublic) {
|
||||||
return <StableTitle {...props} />;
|
return <StableTitle {...props} />;
|
||||||
}
|
}
|
||||||
return <BlockSuiteTitleWithRename {...props} />;
|
return <BlockSuiteTitleWithRename {...props} />;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAtomValue } from 'jotai';
|
|||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import type { PageMode } from '../../../atoms';
|
||||||
import { currentModeAtom } from '../../../atoms/mode';
|
import { currentModeAtom } from '../../../atoms/mode';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||||
@@ -18,6 +19,8 @@ export type EditorModeSwitchProps = {
|
|||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
isPublic?: boolean;
|
||||||
|
publicMode?: PageMode;
|
||||||
};
|
};
|
||||||
const TooltipContent = () => {
|
const TooltipContent = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@@ -34,6 +37,8 @@ export const EditorModeSwitch = ({
|
|||||||
style,
|
style,
|
||||||
blockSuiteWorkspace,
|
blockSuiteWorkspace,
|
||||||
pageId,
|
pageId,
|
||||||
|
isPublic,
|
||||||
|
publicMode,
|
||||||
}: EditorModeSwitchProps) => {
|
}: EditorModeSwitchProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
@@ -47,7 +52,7 @@ export const EditorModeSwitch = ({
|
|||||||
const currentMode = useAtomValue(currentModeAtom);
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trash) {
|
if (trash || isPublic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keydown = (e: KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
@@ -64,41 +69,58 @@ export const EditorModeSwitch = ({
|
|||||||
document.addEventListener('keydown', keydown, { capture: true });
|
document.addEventListener('keydown', keydown, { capture: true });
|
||||||
return () =>
|
return () =>
|
||||||
document.removeEventListener('keydown', keydown, { capture: true });
|
document.removeEventListener('keydown', keydown, { capture: true });
|
||||||
}, [currentMode, pageId, t, togglePageMode, trash]);
|
}, [currentMode, isPublic, pageId, t, togglePageMode, trash]);
|
||||||
|
|
||||||
const onSwitchToPageMode = useCallback(() => {
|
const onSwitchToPageMode = useCallback(() => {
|
||||||
if (currentMode === 'page') {
|
if (currentMode === 'page' || isPublic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switchToPageMode(pageId);
|
switchToPageMode(pageId);
|
||||||
toast(t['com.affine.toastMessage.pageMode']());
|
toast(t['com.affine.toastMessage.pageMode']());
|
||||||
}, [currentMode, pageId, switchToPageMode, t]);
|
}, [currentMode, isPublic, pageId, switchToPageMode, t]);
|
||||||
|
|
||||||
const onSwitchToEdgelessMode = useCallback(() => {
|
const onSwitchToEdgelessMode = useCallback(() => {
|
||||||
if (currentMode === 'edgeless') {
|
if (currentMode === 'edgeless' || isPublic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switchToEdgelessMode(pageId);
|
switchToEdgelessMode(pageId);
|
||||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
toast(t['com.affine.toastMessage.edgelessMode']());
|
||||||
}, [currentMode, pageId, switchToEdgelessMode, t]);
|
}, [currentMode, isPublic, pageId, switchToEdgelessMode, t]);
|
||||||
|
|
||||||
|
const shouldHide = useCallback(
|
||||||
|
(mode: PageMode) =>
|
||||||
|
(trash && currentMode !== mode) || (isPublic && publicMode !== mode),
|
||||||
|
[currentMode, isPublic, publicMode, trash]
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldActive = useCallback(
|
||||||
|
(mode: PageMode) => (isPublic ? false : currentMode === mode),
|
||||||
|
[currentMode, isPublic]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content={<TooltipContent />}>
|
<Tooltip
|
||||||
|
content={<TooltipContent />}
|
||||||
|
options={{
|
||||||
|
hidden: isPublic || trash,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<StyledEditorModeSwitch
|
<StyledEditorModeSwitch
|
||||||
style={style}
|
style={style}
|
||||||
switchLeft={currentMode === 'page'}
|
switchLeft={currentMode === 'page'}
|
||||||
showAlone={trash}
|
showAlone={trash || isPublic}
|
||||||
>
|
>
|
||||||
<PageSwitchItem
|
<PageSwitchItem
|
||||||
data-testid="switch-page-mode-button"
|
data-testid="switch-page-mode-button"
|
||||||
active={currentMode === 'page'}
|
active={shouldActive('page')}
|
||||||
hide={trash && currentMode !== 'page'}
|
hide={shouldHide('page')}
|
||||||
trash={trash}
|
trash={trash}
|
||||||
onClick={onSwitchToPageMode}
|
onClick={onSwitchToPageMode}
|
||||||
/>
|
/>
|
||||||
<EdgelessSwitchItem
|
<EdgelessSwitchItem
|
||||||
data-testid="switch-edgeless-mode-button"
|
data-testid="switch-edgeless-mode-button"
|
||||||
active={currentMode === 'edgeless'}
|
active={shouldActive('edgeless')}
|
||||||
hide={trash && currentMode !== 'edgeless'}
|
hide={shouldHide('edgeless')}
|
||||||
trash={trash}
|
trash={trash}
|
||||||
onClick={onSwitchToEdgelessMode}
|
onClick={onSwitchToEdgelessMode}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
|
|
||||||
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
import { PublishPageUserAvatar } from './user-avatar';
|
||||||
|
|
||||||
|
const ShareHeaderLeftItem = () => {
|
||||||
|
const loginStatus = useCurrentLoginStatus();
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
return <PublishPageUserAvatar />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="https://affine.pro/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.iconWrapper}
|
||||||
|
data-testid="share-page-logo"
|
||||||
|
>
|
||||||
|
<Logo1Icon />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareHeaderLeftItem;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const iconWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
selectors: {
|
||||||
|
'&:visited': {
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { SignOutIcon } from '@blocksuite/icons';
|
||||||
|
import { Avatar } from '@toeverything/components/avatar';
|
||||||
|
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||||
|
import { signOutCloud } from '../../../utils/cloud-utils';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export const PublishPageUserAvatar = () => {
|
||||||
|
const user = useCurrentUser();
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const handleSignOut = useAsyncCallback(async () => {
|
||||||
|
await signOutCloud({ callbackUrl: location.pathname });
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
const menuItem = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
preFix={
|
||||||
|
<MenuIcon>
|
||||||
|
<SignOutIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
|
data-testid="share-page-sign-out-option"
|
||||||
|
onClick={handleSignOut}
|
||||||
|
>
|
||||||
|
{t['com.affine.workspace.cloud.account.logout']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}, [handleSignOut, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu items={menuItem}>
|
||||||
|
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
|
||||||
|
<Avatar size={24} url={user.image} name={user.name} />
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { Button } from '@toeverything/components/button';
|
||||||
|
|
||||||
|
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||||
|
import { useMembers } from '../../../hooks/affine/use-members';
|
||||||
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
|
import type { ShareHeaderRightItemProps } from '.';
|
||||||
|
|
||||||
|
export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => {
|
||||||
|
const { workspaceId, pageId } = props;
|
||||||
|
const user = useCurrentUser();
|
||||||
|
const members = useMembers(workspaceId, 0);
|
||||||
|
const isMember = members.some(m => m.id === user.id);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const { jumpToPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
if (isMember) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="plain"
|
||||||
|
onClick={() => jumpToPage(workspaceId, pageId)}
|
||||||
|
data-testid="share-page-edit-button"
|
||||||
|
>
|
||||||
|
{t['Edit']()}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
|
import { AuthenticatedItem } from './authenticated-item';
|
||||||
|
|
||||||
|
export type ShareHeaderRightItemProps = {
|
||||||
|
workspaceId: string;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
|
||||||
|
const loginStatus = useCurrentLoginStatus();
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
return <AuthenticatedItem {...props} />;
|
||||||
|
}
|
||||||
|
// TODO: Add TOC
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareHeaderRightItem;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const iconWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
selectors: {
|
||||||
|
'&:visited': {
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { pageSettingFamily } from '../atoms';
|
import { type PageMode, pageSettingFamily } from '../atoms';
|
||||||
import { fontStyleOptions } from '../atoms/settings';
|
import { fontStyleOptions } from '../atoms/settings';
|
||||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||||
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
|
||||||
@@ -50,6 +50,7 @@ export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void;
|
|||||||
|
|
||||||
export interface PageDetailEditorProps {
|
export interface PageDetailEditorProps {
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
|
publishMode?: PageMode;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
onLoad?: OnLoadEditor;
|
onLoad?: OnLoadEditor;
|
||||||
@@ -91,6 +92,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
pageId,
|
pageId,
|
||||||
onLoad,
|
onLoad,
|
||||||
isPublic,
|
isPublic,
|
||||||
|
publishMode,
|
||||||
}: PageDetailEditorProps) {
|
}: PageDetailEditorProps) {
|
||||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@@ -105,7 +107,16 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
|
|
||||||
const pageSettingAtom = pageSettingFamily(pageId);
|
const pageSettingAtom = pageSettingFamily(pageId);
|
||||||
const pageSetting = useAtomValue(pageSettingAtom);
|
const pageSetting = useAtomValue(pageSettingAtom);
|
||||||
const currentMode = pageSetting?.mode ?? 'page';
|
|
||||||
|
const mode = useMemo(() => {
|
||||||
|
const currentMode = pageSetting.mode;
|
||||||
|
const shareMode = publishMode || currentMode;
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return shareMode;
|
||||||
|
}
|
||||||
|
return currentMode;
|
||||||
|
}, [isPublic, publishMode, pageSetting.mode]);
|
||||||
|
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
|
||||||
@@ -120,13 +131,16 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
|
|
||||||
const setEditorMode = useCallback(
|
const setEditorMode = useCallback(
|
||||||
(mode: 'page' | 'edgeless') => {
|
(mode: 'page' | 'edgeless') => {
|
||||||
|
if (isPublic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mode === 'edgeless') {
|
if (mode === 'edgeless') {
|
||||||
switchToEdgelessMode(pageId);
|
switchToEdgelessMode(pageId);
|
||||||
} else {
|
} else {
|
||||||
switchToPageMode(pageId);
|
switchToPageMode(pageId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[switchToEdgelessMode, switchToPageMode, pageId]
|
[isPublic, switchToEdgelessMode, pageId, switchToPageMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [editor, setEditor] = useState<EditorContainer>();
|
const [editor, setEditor] = useState<EditorContainer>();
|
||||||
@@ -191,7 +205,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
'--affine-font-family': value,
|
'--affine-font-family': value,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
mode={isPublic ? 'page' : currentMode}
|
mode={mode}
|
||||||
page={page}
|
page={page}
|
||||||
onModeChange={setEditorMode}
|
onModeChange={setEditorMode}
|
||||||
defaultSelectedBlockId={blockId}
|
defaultSelectedBlockId={blockId}
|
||||||
|
|||||||
44
packages/frontend/core/src/components/share-header.tsx
Normal file
44
packages/frontend/core/src/components/share-header.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import { useSetAtom } from 'jotai/react';
|
||||||
|
|
||||||
|
import type { PageMode } from '../atoms';
|
||||||
|
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
|
||||||
|
import { useWorkspace } from '../hooks/use-workspace';
|
||||||
|
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
||||||
|
import ShareHeaderLeftItem from './cloud/share-header-left-item';
|
||||||
|
import ShareHeaderRightItem from './cloud/share-header-right-item';
|
||||||
|
import { Header } from './pure/header';
|
||||||
|
|
||||||
|
export function ShareHeader({
|
||||||
|
workspace,
|
||||||
|
pageId,
|
||||||
|
publishMode,
|
||||||
|
}: {
|
||||||
|
workspace: Workspace;
|
||||||
|
pageId: string;
|
||||||
|
publishMode: PageMode;
|
||||||
|
}) {
|
||||||
|
const setAppHeader = useSetAtom(appHeaderAtom);
|
||||||
|
|
||||||
|
const currentWorkspace = useWorkspace(workspace.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
mainContainerAtom={mainContainerAtom}
|
||||||
|
ref={setAppHeader}
|
||||||
|
left={<ShareHeaderLeftItem />}
|
||||||
|
center={
|
||||||
|
<BlockSuiteHeaderTitle
|
||||||
|
workspace={currentWorkspace}
|
||||||
|
pageId={pageId}
|
||||||
|
isPublic={true}
|
||||||
|
publicMode={publishMode}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<ShareHeaderRightItem workspaceId={workspace.id} pageId={pageId} />
|
||||||
|
}
|
||||||
|
bottomBorder
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const iconWrapper = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '16px',
|
||||||
|
left: '16px',
|
||||||
|
fontSize: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
selectors: {
|
||||||
|
'&:visited': {
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,57 +1,190 @@
|
|||||||
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import {
|
import {
|
||||||
getWorkspaceSharedPagesQuery,
|
getWorkspacePublicPagesQuery,
|
||||||
revokePageMutation,
|
PublicPageMode,
|
||||||
sharePageMutation,
|
publishPageMutation,
|
||||||
|
revokePublicPageMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import type { PageMode } from '../../atoms';
|
||||||
|
|
||||||
|
type NoParametersKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends () => any ? K : never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
type i18nKey = NoParametersKeys<ReturnType<typeof useAFFiNEI18N>>;
|
||||||
|
|
||||||
|
type NotificationKey =
|
||||||
|
| 'enableSuccessTitle'
|
||||||
|
| 'enableSuccessMessage'
|
||||||
|
| 'enableErrorTitle'
|
||||||
|
| 'enableErrorMessage'
|
||||||
|
| 'changeSuccessTitle'
|
||||||
|
| 'changeErrorTitle'
|
||||||
|
| 'changeErrorMessage'
|
||||||
|
| 'disableSuccessTitle'
|
||||||
|
| 'disableSuccessMessage'
|
||||||
|
| 'disableErrorTitle'
|
||||||
|
| 'disableErrorMessage';
|
||||||
|
|
||||||
|
const notificationToI18nKey: Record<NotificationKey, i18nKey> = {
|
||||||
|
enableSuccessTitle:
|
||||||
|
'com.affine.share-menu.create-public-link.notification.success.title',
|
||||||
|
enableSuccessMessage:
|
||||||
|
'com.affine.share-menu.create-public-link.notification.success.message',
|
||||||
|
enableErrorTitle:
|
||||||
|
'com.affine.share-menu.create-public-link.notification.fail.title',
|
||||||
|
enableErrorMessage:
|
||||||
|
'com.affine.share-menu.create-public-link.notification.fail.message',
|
||||||
|
changeSuccessTitle:
|
||||||
|
'com.affine.share-menu.confirm-modify-mode.notification.success.title',
|
||||||
|
changeErrorTitle:
|
||||||
|
'com.affine.share-menu.confirm-modify-mode.notification.fail.title',
|
||||||
|
changeErrorMessage:
|
||||||
|
'com.affine.share-menu.confirm-modify-mode.notification.fail.message',
|
||||||
|
disableSuccessTitle:
|
||||||
|
'com.affine.share-menu.disable-publish-link.notification.success.title',
|
||||||
|
disableSuccessMessage:
|
||||||
|
'com.affine.share-menu.disable-publish-link.notification.success.message',
|
||||||
|
disableErrorTitle:
|
||||||
|
'com.affine.share-menu.disable-publish-link.notification.fail.title',
|
||||||
|
disableErrorMessage:
|
||||||
|
'com.affine.share-menu.disable-publish-link.notification.fail.message',
|
||||||
|
};
|
||||||
|
|
||||||
export function useIsSharedPage(
|
export function useIsSharedPage(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
pageId: string
|
pageId: string
|
||||||
): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
|
): {
|
||||||
|
isSharedPage: boolean;
|
||||||
|
changeShare: (mode: PageMode) => void;
|
||||||
|
disableShare: () => void;
|
||||||
|
currentShareMode: PageMode;
|
||||||
|
enableShare: (mode: PageMode) => void;
|
||||||
|
} {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
const { data, mutate } = useQuery({
|
const { data, mutate } = useQuery({
|
||||||
query: getWorkspaceSharedPagesQuery,
|
query: getWorkspacePublicPagesQuery,
|
||||||
variables: {
|
variables: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { trigger: enableSharePage } = useMutation({
|
const { trigger: enableSharePage } = useMutation({
|
||||||
mutation: sharePageMutation,
|
mutation: publishPageMutation,
|
||||||
});
|
});
|
||||||
const { trigger: disableSharePage } = useMutation({
|
const { trigger: disableSharePage } = useMutation({
|
||||||
mutation: revokePageMutation,
|
mutation: revokePublicPageMutation,
|
||||||
});
|
});
|
||||||
return [
|
|
||||||
useMemo(
|
const [isSharedPage, currentShareMode] = useMemo(() => {
|
||||||
() => data.workspace.sharedPages.some(id => id === pageId),
|
const publicPage = data?.workspace.publicPages.find(
|
||||||
[data.workspace.sharedPages, pageId]
|
publicPage => publicPage.id === pageId
|
||||||
),
|
);
|
||||||
useCallback(
|
const isPageShared = !!publicPage;
|
||||||
(enable: boolean) => {
|
|
||||||
// todo: push notification
|
const currentShareMode: PageMode =
|
||||||
if (enable) {
|
publicPage?.mode === PublicPageMode.Edgeless ? 'edgeless' : 'page';
|
||||||
enableSharePage({
|
|
||||||
workspaceId,
|
return [isPageShared, currentShareMode];
|
||||||
pageId,
|
}, [data?.workspace.publicPages, pageId]);
|
||||||
})
|
|
||||||
|
const enableShare = useCallback(
|
||||||
|
(mode: PageMode) => {
|
||||||
|
const publishMode =
|
||||||
|
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||||
|
|
||||||
|
enableSharePage({ workspaceId, pageId, mode: publishMode })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
pushNotification({
|
||||||
|
title: t[notificationToI18nKey['enableSuccessTitle']](),
|
||||||
|
message: t[notificationToI18nKey['enableSuccessMessage']](),
|
||||||
|
type: 'success',
|
||||||
|
theme: 'default',
|
||||||
|
});
|
||||||
return mutate();
|
return mutate();
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(e => {
|
||||||
} else {
|
pushNotification({
|
||||||
disableSharePage({
|
title: t[notificationToI18nKey['enableErrorTitle']](),
|
||||||
workspaceId,
|
message: t[notificationToI18nKey['enableErrorMessage']](),
|
||||||
pageId,
|
type: 'error',
|
||||||
})
|
});
|
||||||
.then(() => {
|
console.error(e);
|
||||||
return mutate();
|
});
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
mutate().catch(console.error);
|
|
||||||
},
|
},
|
||||||
[disableSharePage, enableSharePage, mutate, pageId, workspaceId]
|
[enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
|
||||||
),
|
);
|
||||||
];
|
|
||||||
|
const changeShare = useCallback(
|
||||||
|
(mode: PageMode) => {
|
||||||
|
const publishMode =
|
||||||
|
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||||
|
|
||||||
|
enableSharePage({ workspaceId, pageId, mode: publishMode })
|
||||||
|
.then(() => {
|
||||||
|
pushNotification({
|
||||||
|
title: t[notificationToI18nKey['changeSuccessTitle']](),
|
||||||
|
message: t[
|
||||||
|
'com.affine.share-menu.confirm-modify-mode.notification.success.message'
|
||||||
|
]({
|
||||||
|
preMode:
|
||||||
|
publishMode === PublicPageMode.Edgeless
|
||||||
|
? PublicPageMode.Page
|
||||||
|
: PublicPageMode.Edgeless,
|
||||||
|
currentMode: publishMode,
|
||||||
|
}),
|
||||||
|
type: 'success',
|
||||||
|
theme: 'default',
|
||||||
|
});
|
||||||
|
return mutate();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
pushNotification({
|
||||||
|
title: t[notificationToI18nKey['changeErrorTitle']](),
|
||||||
|
message: t[notificationToI18nKey['changeErrorMessage']](),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const disableShare = useCallback(() => {
|
||||||
|
disableSharePage({ workspaceId, pageId })
|
||||||
|
.then(() => {
|
||||||
|
pushNotification({
|
||||||
|
title: t[notificationToI18nKey['disableSuccessTitle']](),
|
||||||
|
message: t[notificationToI18nKey['disableSuccessMessage']](),
|
||||||
|
type: 'success',
|
||||||
|
theme: 'default',
|
||||||
|
});
|
||||||
|
return mutate();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
pushNotification({
|
||||||
|
title: t[notificationToI18nKey['disableErrorTitle']](),
|
||||||
|
message: t[notificationToI18nKey['disableErrorMessage']](),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}, [disableSharePage, mutate, pageId, pushNotification, t, workspaceId]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
isSharedPage,
|
||||||
|
currentShareMode,
|
||||||
|
enableShare,
|
||||||
|
disableShare,
|
||||||
|
changeShare,
|
||||||
|
}),
|
||||||
|
[isSharedPage, currentShareMode, enableShare, disableShare, changeShare]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DebugLogger } from '@affine/debug';
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||||
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
|
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
|
||||||
|
import type { CloudDoc } from '@affine/workspace/providers/cloud';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { noop } from 'foxact/noop';
|
import { noop } from 'foxact/noop';
|
||||||
@@ -18,12 +19,25 @@ import {
|
|||||||
import { applyUpdate } from 'yjs';
|
import { applyUpdate } from 'yjs';
|
||||||
|
|
||||||
import { PageDetailEditor } from '../../adapters/shared';
|
import { PageDetailEditor } from '../../adapters/shared';
|
||||||
|
import type { PageMode } from '../../atoms';
|
||||||
import { AppContainer } from '../../components/affine/app-container';
|
import { AppContainer } from '../../components/affine/app-container';
|
||||||
|
import { ShareHeader } from '../../components/share-header';
|
||||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||||
|
|
||||||
function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
|
type LoaderData = {
|
||||||
if (!(value instanceof ArrayBuffer)) {
|
page: Page;
|
||||||
throw new Error('value is not ArrayBuffer');
|
publishMode: PageMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function assertDownloadResponse(
|
||||||
|
value: CloudDoc | boolean
|
||||||
|
): asserts value is CloudDoc {
|
||||||
|
if (
|
||||||
|
!value ||
|
||||||
|
!((value as CloudDoc).arrayBuffer instanceof ArrayBuffer) ||
|
||||||
|
typeof (value as CloudDoc).publishMode !== 'string'
|
||||||
|
) {
|
||||||
|
throw new Error('value is not a valid download response');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,33 +55,42 @@ export const loader: LoaderFunction = async ({ params }) => {
|
|||||||
);
|
);
|
||||||
// download root workspace
|
// download root workspace
|
||||||
{
|
{
|
||||||
const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||||
assertArrayBuffer(buffer);
|
assertDownloadResponse(response);
|
||||||
applyUpdate(workspace.doc, new Uint8Array(buffer));
|
const { arrayBuffer } = response;
|
||||||
|
applyUpdate(workspace.doc, new Uint8Array(arrayBuffer));
|
||||||
}
|
}
|
||||||
const page = workspace.getPage(pageId);
|
const page = workspace.getPage(pageId);
|
||||||
assertExists(page, 'cannot find page');
|
assertExists(page, 'cannot find page');
|
||||||
// download page
|
// download page
|
||||||
{
|
|
||||||
const buffer = await downloadBinaryFromCloud(
|
const response = await downloadBinaryFromCloud(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
page.spaceDoc.guid
|
page.spaceDoc.guid
|
||||||
);
|
);
|
||||||
assertArrayBuffer(buffer);
|
assertDownloadResponse(response);
|
||||||
applyUpdate(page.spaceDoc, new Uint8Array(buffer));
|
const { arrayBuffer, publishMode } = response;
|
||||||
}
|
|
||||||
|
applyUpdate(page.spaceDoc, new Uint8Array(arrayBuffer));
|
||||||
|
|
||||||
logger.info('workspace', workspace);
|
logger.info('workspace', workspace);
|
||||||
workspace.awarenessStore.setReadonly(page, true);
|
workspace.awarenessStore.setReadonly(page, true);
|
||||||
return page;
|
return { page, publishMode };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = (): ReactElement => {
|
export const Component = (): ReactElement => {
|
||||||
const page = useLoaderData() as Page;
|
const { page, publishMode } = useLoaderData() as LoaderData;
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
|
<ShareHeader
|
||||||
|
workspace={page.workspace}
|
||||||
|
pageId={page.id}
|
||||||
|
publishMode={publishMode}
|
||||||
|
/>
|
||||||
<PageDetailEditor
|
<PageDetailEditor
|
||||||
isPublic
|
isPublic
|
||||||
|
publishMode={publishMode}
|
||||||
workspace={page.workspace}
|
workspace={page.workspace}
|
||||||
pageId={page.id}
|
pageId={page.id}
|
||||||
onLoad={useCallback(() => noop, [])}
|
onLoad={useCallback(() => noop, [])}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
query getWorkspacePublicPages($workspaceId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
publicPages {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
query getWorkspaceSharedPages($workspaceId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
sharedPages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -320,15 +320,18 @@ query getWorkspacePublicById($id: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceSharedPagesQuery = {
|
export const getWorkspacePublicPagesQuery = {
|
||||||
id: 'getWorkspaceSharedPagesQuery' as const,
|
id: 'getWorkspacePublicPagesQuery' as const,
|
||||||
operationName: 'getWorkspaceSharedPages',
|
operationName: 'getWorkspacePublicPages',
|
||||||
definitionName: 'workspace',
|
definitionName: 'workspace',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
query getWorkspaceSharedPages($workspaceId: String!) {
|
query getWorkspacePublicPages($workspaceId: String!) {
|
||||||
workspace(id: $workspaceId) {
|
workspace(id: $workspaceId) {
|
||||||
sharedPages
|
publicPages {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
@@ -428,6 +431,20 @@ query prices {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publishPageMutation = {
|
||||||
|
id: 'publishPageMutation' as const,
|
||||||
|
operationName: 'publishPage',
|
||||||
|
definitionName: 'publishPage',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
mutation publishPage($workspaceId: String!, $pageId: String!, $mode: PublicPageMode = Page) {
|
||||||
|
publishPage(workspaceId: $workspaceId, pageId: $pageId, mode: $mode) {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const removeAvatarMutation = {
|
export const removeAvatarMutation = {
|
||||||
id: 'removeAvatarMutation' as const,
|
id: 'removeAvatarMutation' as const,
|
||||||
operationName: 'removeAvatar',
|
operationName: 'removeAvatar',
|
||||||
@@ -469,14 +486,18 @@ mutation revokeMemberPermission($workspaceId: String!, $userId: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const revokePageMutation = {
|
export const revokePublicPageMutation = {
|
||||||
id: 'revokePageMutation' as const,
|
id: 'revokePublicPageMutation' as const,
|
||||||
operationName: 'revokePage',
|
operationName: 'revokePublicPage',
|
||||||
definitionName: 'revokePage',
|
definitionName: 'revokePublicPage',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation revokePage($workspaceId: String!, $pageId: String!) {
|
mutation revokePublicPage($workspaceId: String!, $pageId: String!) {
|
||||||
revokePage(workspaceId: $workspaceId, pageId: $pageId)
|
revokePublicPage(workspaceId: $workspaceId, pageId: $pageId) {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
public
|
||||||
|
}
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -537,17 +558,6 @@ mutation setWorkspacePublicById($id: ID!, $public: Boolean!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sharePageMutation = {
|
|
||||||
id: 'sharePageMutation' as const,
|
|
||||||
operationName: 'sharePage',
|
|
||||||
definitionName: 'sharePage',
|
|
||||||
containsFile: false,
|
|
||||||
query: `
|
|
||||||
mutation sharePage($workspaceId: String!, $pageId: String!) {
|
|
||||||
sharePage(workspaceId: $workspaceId, pageId: $pageId)
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const signInMutation = {
|
export const signInMutation = {
|
||||||
id: 'signInMutation' as const,
|
id: 'signInMutation' as const,
|
||||||
operationName: 'signIn',
|
operationName: 'signIn',
|
||||||
|
|||||||
10
packages/frontend/graphql/src/graphql/public-page.gql
Normal file
10
packages/frontend/graphql/src/graphql/public-page.gql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
mutation publishPage(
|
||||||
|
$workspaceId: String!
|
||||||
|
$pageId: String!
|
||||||
|
$mode: PublicPageMode = Page
|
||||||
|
) {
|
||||||
|
publishPage(workspaceId: $workspaceId, pageId: $pageId, mode: $mode) {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation revokePage($workspaceId: String!, $pageId: String!) {
|
|
||||||
revokePage(workspaceId: $workspaceId, pageId: $pageId)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mutation revokePublicPage($workspaceId: String!, $pageId: String!) {
|
||||||
|
revokePublicPage(workspaceId: $workspaceId, pageId: $pageId) {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
public
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation sharePage($workspaceId: String!, $pageId: String!) {
|
|
||||||
sharePage(workspaceId: $workspaceId, pageId: $pageId)
|
|
||||||
}
|
|
||||||
@@ -340,13 +340,20 @@ export type GetWorkspacePublicByIdQuery = {
|
|||||||
workspace: { __typename?: 'WorkspaceType'; public: boolean };
|
workspace: { __typename?: 'WorkspaceType'; public: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceSharedPagesQueryVariables = Exact<{
|
export type GetWorkspacePublicPagesQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type GetWorkspaceSharedPagesQuery = {
|
export type GetWorkspacePublicPagesQuery = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
workspace: { __typename?: 'WorkspaceType'; sharedPages: Array<string> };
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
publicPages: Array<{
|
||||||
|
__typename?: 'WorkspacePage';
|
||||||
|
id: string;
|
||||||
|
mode: PublicPageMode;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceQueryVariables = Exact<{
|
export type GetWorkspaceQueryVariables = Exact<{
|
||||||
@@ -422,6 +429,21 @@ export type PricesQuery = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PublishPageMutationVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
pageId: Scalars['String']['input'];
|
||||||
|
mode?: InputMaybe<PublicPageMode>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type PublishPageMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
publishPage: {
|
||||||
|
__typename?: 'WorkspacePage';
|
||||||
|
id: string;
|
||||||
|
mode: PublicPageMode;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
|
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type RemoveAvatarMutation = {
|
export type RemoveAvatarMutation = {
|
||||||
@@ -455,14 +477,19 @@ export type RevokeMemberPermissionMutation = {
|
|||||||
revoke: boolean;
|
revoke: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RevokePageMutationVariables = Exact<{
|
export type RevokePublicPageMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
pageId: Scalars['String']['input'];
|
pageId: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type RevokePageMutation = {
|
export type RevokePublicPageMutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
revokePage: boolean;
|
revokePublicPage: {
|
||||||
|
__typename?: 'WorkspacePage';
|
||||||
|
id: string;
|
||||||
|
mode: PublicPageMode;
|
||||||
|
public: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SendChangeEmailMutationVariables = Exact<{
|
export type SendChangeEmailMutationVariables = Exact<{
|
||||||
@@ -516,13 +543,6 @@ export type SetWorkspacePublicByIdMutation = {
|
|||||||
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SharePageMutationVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
pageId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type SharePageMutation = { __typename?: 'Mutation'; sharePage: boolean };
|
|
||||||
|
|
||||||
export type SignInMutationVariables = Exact<{
|
export type SignInMutationVariables = Exact<{
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
password: Scalars['String']['input'];
|
password: Scalars['String']['input'];
|
||||||
@@ -683,9 +703,9 @@ export type Queries =
|
|||||||
response: GetWorkspacePublicByIdQuery;
|
response: GetWorkspacePublicByIdQuery;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspaceSharedPagesQuery';
|
name: 'getWorkspacePublicPagesQuery';
|
||||||
variables: GetWorkspaceSharedPagesQueryVariables;
|
variables: GetWorkspacePublicPagesQueryVariables;
|
||||||
response: GetWorkspaceSharedPagesQuery;
|
response: GetWorkspacePublicPagesQuery;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspaceQuery';
|
name: 'getWorkspaceQuery';
|
||||||
@@ -774,6 +794,11 @@ export type Mutations =
|
|||||||
variables: LeaveWorkspaceMutationVariables;
|
variables: LeaveWorkspaceMutationVariables;
|
||||||
response: LeaveWorkspaceMutation;
|
response: LeaveWorkspaceMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'publishPageMutation';
|
||||||
|
variables: PublishPageMutationVariables;
|
||||||
|
response: PublishPageMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'removeAvatarMutation';
|
name: 'removeAvatarMutation';
|
||||||
variables: RemoveAvatarMutationVariables;
|
variables: RemoveAvatarMutationVariables;
|
||||||
@@ -790,9 +815,9 @@ export type Mutations =
|
|||||||
response: RevokeMemberPermissionMutation;
|
response: RevokeMemberPermissionMutation;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'revokePageMutation';
|
name: 'revokePublicPageMutation';
|
||||||
variables: RevokePageMutationVariables;
|
variables: RevokePublicPageMutationVariables;
|
||||||
response: RevokePageMutation;
|
response: RevokePublicPageMutation;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'sendChangeEmailMutation';
|
name: 'sendChangeEmailMutation';
|
||||||
@@ -819,11 +844,6 @@ export type Mutations =
|
|||||||
variables: SetWorkspacePublicByIdMutationVariables;
|
variables: SetWorkspacePublicByIdMutationVariables;
|
||||||
response: SetWorkspacePublicByIdMutation;
|
response: SetWorkspacePublicByIdMutation;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'sharePageMutation';
|
|
||||||
variables: SharePageMutationVariables;
|
|
||||||
response: SharePageMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'signInMutation';
|
name: 'signInMutation';
|
||||||
variables: SignInMutationVariables;
|
variables: SignInMutationVariables;
|
||||||
|
|||||||
@@ -334,6 +334,21 @@
|
|||||||
"com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your page to share with others.",
|
"com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your page to share with others.",
|
||||||
"com.affine.share-menu.ShareWithLink": "Share with link",
|
"com.affine.share-menu.ShareWithLink": "Share with link",
|
||||||
"com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your page in the form od a document",
|
"com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your page in the form od a document",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.title": "Modify the sharing method?",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.description": "Once modified, new public link will be created. Please share it with others again.",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.confirm-button": "Modify",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.notification.success.title": "Modified successfully",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.notification.success.message": "You have changed the public link from {{preMode}} Mode to {{currentMode}} Mode.",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.notification.fail.title": "Failed to modify",
|
||||||
|
"com.affine.share-menu.confirm-modify-mode.notification.fail.message": "Please try again later.",
|
||||||
|
"com.affine.share-menu.create-public-link.notification.success.title": "Public link created",
|
||||||
|
"com.affine.share-menu.create-public-link.notification.success.message": "You can share this document with link.",
|
||||||
|
"com.affine.share-menu.create-public-link.notification.fail.title": "Failed to create public link",
|
||||||
|
"com.affine.share-menu.create-public-link.notification.fail.message": "Please try again later.",
|
||||||
|
"com.affine.share-menu.disable-publish-link.notification.success.title": "Public link disabled",
|
||||||
|
"com.affine.share-menu.disable-publish-link.notification.success.message": "This page is no longer shared publicly.",
|
||||||
|
"com.affine.share-menu.disable-publish-link.notification.fail.title": "Failed to disable public link",
|
||||||
|
"com.affine.share-menu.disable-publish-link.notification.fail.message": "Please try again later.",
|
||||||
"com.affine.shortcutsTitle.edgeless": "Edgeless",
|
"com.affine.shortcutsTitle.edgeless": "Edgeless",
|
||||||
"com.affine.shortcutsTitle.general": "General",
|
"com.affine.shortcutsTitle.general": "General",
|
||||||
"com.affine.shortcutsTitle.markdownSyntax": "Markdown Syntax",
|
"com.affine.shortcutsTitle.markdownSyntax": "Markdown Syntax",
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ const logger = new DebugLogger('affine:cloud');
|
|||||||
|
|
||||||
const hashMap = new Map<string, ArrayBuffer>();
|
const hashMap = new Map<string, ArrayBuffer>();
|
||||||
|
|
||||||
|
type DocPublishMode = 'edgeless' | 'page';
|
||||||
|
|
||||||
|
export type CloudDoc = {
|
||||||
|
arrayBuffer: ArrayBuffer;
|
||||||
|
publishMode: DocPublishMode;
|
||||||
|
};
|
||||||
|
|
||||||
export async function downloadBinaryFromCloud(
|
export async function downloadBinaryFromCloud(
|
||||||
rootGuid: string,
|
rootGuid: string,
|
||||||
pageGuid: string
|
pageGuid: string
|
||||||
): Promise<boolean | ArrayBuffer> {
|
): Promise<CloudDoc | boolean> {
|
||||||
if (hashMap.has(`${rootGuid}/${pageGuid}`)) {
|
if (hashMap.has(`${rootGuid}/${pageGuid}`)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -25,17 +32,22 @@ export async function downloadBinaryFromCloud(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
const publishMode = (response.headers.get('publish-mode') ||
|
||||||
|
'page') as DocPublishMode;
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
hashMap.set(`${rootGuid}/${pageGuid}`, arrayBuffer);
|
hashMap.set(`${rootGuid}/${pageGuid}`, arrayBuffer);
|
||||||
return arrayBuffer;
|
|
||||||
|
// return both arrayBuffer and publish mode
|
||||||
|
return { arrayBuffer, publishMode };
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadBinary(rootGuid: string, doc: Doc) {
|
async function downloadBinary(rootGuid: string, doc: Doc) {
|
||||||
const buffer = await downloadBinaryFromCloud(rootGuid, doc.guid);
|
const response = await downloadBinaryFromCloud(rootGuid, doc.guid);
|
||||||
if (typeof buffer !== 'boolean') {
|
if (typeof response !== 'boolean') {
|
||||||
Y.applyUpdate(doc, new Uint8Array(buffer), 'affine-cloud');
|
const { arrayBuffer } = response;
|
||||||
|
Y.applyUpdate(doc, new Uint8Array(arrayBuffer), 'affine-cloud');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
loginUser,
|
loginUser,
|
||||||
} from '@affine-test/kit/utils/cloud';
|
} from '@affine-test/kit/utils/cloud';
|
||||||
import { dropFile } from '@affine-test/kit/utils/drop-file';
|
import { dropFile } from '@affine-test/kit/utils/drop-file';
|
||||||
|
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
@@ -78,6 +79,52 @@ test.describe('collaboration', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('share page with default edgeless', async ({ page, browser }) => {
|
||||||
|
await page.reload();
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await createLocalWorkspace(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
);
|
||||||
|
await enableCloudWorkspaceFromShareButton(page);
|
||||||
|
const title = getBlockSuiteEditorTitle(page);
|
||||||
|
await title.pressSequentially('TEST TITLE', {
|
||||||
|
delay: 50,
|
||||||
|
});
|
||||||
|
await page.keyboard.press('Enter', { delay: 50 });
|
||||||
|
await page.keyboard.type('TEST CONTENT', { delay: 50 });
|
||||||
|
await clickEdgelessModeButton(page);
|
||||||
|
await expect(page.locator('affine-edgeless-page')).toBeVisible({
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
await page.getByTestId('cloud-share-menu-button').click();
|
||||||
|
await page.getByTestId('share-menu-create-link-button').click();
|
||||||
|
await page.getByTestId('share-menu-copy-link-button').click();
|
||||||
|
|
||||||
|
// check share page is accessible
|
||||||
|
{
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const url: string = await page.evaluate(() =>
|
||||||
|
navigator.clipboard.readText()
|
||||||
|
);
|
||||||
|
const page2 = await context.newPage();
|
||||||
|
await page2.goto(url);
|
||||||
|
await waitForEditorLoad(page2);
|
||||||
|
await expect(page.locator('affine-edgeless-page')).toBeVisible({
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
expect(await page2.textContent('affine-paragraph')).toContain(
|
||||||
|
'TEST CONTENT'
|
||||||
|
);
|
||||||
|
const logo = page2.getByTestId('share-page-logo');
|
||||||
|
const editButton = page2.getByTestId('share-page-edit-button');
|
||||||
|
await expect(editButton).not.toBeVisible();
|
||||||
|
await expect(logo).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('can collaborate with other user and name should display when editing', async ({
|
test('can collaborate with other user and name should display when editing', async ({
|
||||||
page,
|
page,
|
||||||
browser,
|
browser,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import {
|
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
||||||
PublicLinkDisableModal,
|
import { ShareMenu } from '@affine/core/components/affine/share-page-modal/share-menu';
|
||||||
StyledDisableButton,
|
|
||||||
} from '@affine/component/share-menu';
|
|
||||||
import { ShareMenu } from '@affine/component/share-menu';
|
|
||||||
import type {
|
import type {
|
||||||
AffineCloudWorkspace,
|
AffineCloudWorkspace,
|
||||||
LocalWorkspace,
|
LocalWorkspace,
|
||||||
@@ -24,20 +21,6 @@ export default {
|
|||||||
},
|
},
|
||||||
} satisfies Meta;
|
} satisfies Meta;
|
||||||
|
|
||||||
const sharePageMap = new Map<string, boolean>([]);
|
|
||||||
// todo: use a real hook
|
|
||||||
const useIsSharedPage = (
|
|
||||||
_workspaceId: string,
|
|
||||||
pageId: string
|
|
||||||
): [isSharePage: boolean, setIsSharePage: (enable: boolean) => void] => {
|
|
||||||
const [isShared, setIsShared] = useState(sharePageMap.get(pageId) ?? false);
|
|
||||||
const togglePagePublic = (enable: boolean) => {
|
|
||||||
setIsShared(enable);
|
|
||||||
sharePageMap.set(pageId, enable);
|
|
||||||
};
|
|
||||||
return [isShared, togglePagePublic];
|
|
||||||
};
|
|
||||||
|
|
||||||
async function initPage(page: Page) {
|
async function initPage(page: Page) {
|
||||||
await page.waitForLoaded();
|
await page.waitForLoaded();
|
||||||
// Add page block and surface block at root level
|
// Add page block and surface block at root level
|
||||||
@@ -88,11 +71,8 @@ export const Basic: StoryFn = () => {
|
|||||||
return (
|
return (
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||||
useIsSharedPage={useIsSharedPage}
|
|
||||||
workspace={localWorkspace}
|
workspace={localWorkspace}
|
||||||
onEnableAffineCloud={unimplemented}
|
onEnableAffineCloud={unimplemented}
|
||||||
togglePagePublic={unimplemented}
|
|
||||||
exportHandler={unimplemented}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -119,11 +99,8 @@ export const AffineBasic: StoryFn = () => {
|
|||||||
return (
|
return (
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||||
useIsSharedPage={useIsSharedPage}
|
|
||||||
workspace={affineWorkspace}
|
workspace={affineWorkspace}
|
||||||
onEnableAffineCloud={unimplemented}
|
onEnableAffineCloud={unimplemented}
|
||||||
togglePagePublic={unimplemented}
|
|
||||||
exportHandler={unimplemented}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -133,9 +110,7 @@ export const DisableModal: StoryFn = () => {
|
|||||||
use(promise);
|
use(promise);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledDisableButton onClick={() => setOpen(!open)}>
|
<div onClick={() => setOpen(!open)}>Disable Public Link</div>
|
||||||
Disable Public Link
|
|
||||||
</StyledDisableButton>
|
|
||||||
<PublicLinkDisableModal
|
<PublicLinkDisableModal
|
||||||
open={open}
|
open={open}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user