feat(core): add editor commanads (#4514)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
JimmFly
2023-10-02 11:22:12 +08:00
committed by GitHub
parent aab1a1e50a
commit 69db99636b
29 changed files with 673 additions and 413 deletions
@@ -13,7 +13,6 @@ import {
blockSuiteEditorHeaderStyle,
blockSuiteEditorStyle,
} from './index.css';
import { useRegisterBlocksuiteEditorCommands } from './use-register-blocksuite-editor-commands';
export type EditorProps = {
page: Page;
@@ -165,7 +164,6 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
props: EditorProps & ErrorBoundaryProps
): ReactElement {
useRegisterBlocksuiteEditorCommands();
return (
<ErrorBoundary
fallbackRender={useCallback(
@@ -1,35 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import { useEffect } from 'react';
export function useRegisterBlocksuiteEditorCommands() {
const t = useAFFiNEI18N();
useEffect(() => {
const unsubs: Array<() => void> = [];
const getEdgeless = () => {
return document.querySelector('affine-edgeless-page');
};
unsubs.push(
registerAffineCommand({
id: 'editor:edgeless-presentation-start',
preconditionStrategy: () => !!getEdgeless(),
category: 'editor:edgeless',
icon: <EdgelessIcon />,
label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
run() {
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
document
.querySelector<HTMLElement>('edgeless-toolbar')
?.shadowRoot?.querySelector<HTMLElement>(
'.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
)
?.click();
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}, [t]);
}
@@ -131,7 +131,6 @@ export const AllPagesBody = ({
active={!!favorite}
/>
<OperationCell
title={title}
favorite={favorite}
isPublic={isPublicPage}
onOpenPageInNewTab={onOpenPageInNewTab}
@@ -17,7 +17,6 @@ import { FlexWrapper } from '../../..';
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
export interface OperationCellProps {
title: string;
favorite: boolean;
isPublic: boolean;
onOpenPageInNewTab: () => void;
@@ -27,7 +26,6 @@ export interface OperationCellProps {
}
export const OperationCell = ({
title,
favorite,
isPublic,
onOpenPageInNewTab,
@@ -36,7 +34,6 @@ export const OperationCell = ({
onDisablePublicSharing,
}: OperationCellProps) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const [openDisableShared, setOpenDisableShared] = useState(false);
const OperationMenu = (
@@ -77,12 +74,7 @@ export const OperationCell = ({
{t['com.affine.openPageOperation.newTab']()}
</MenuItem>
)}
<MoveToTrash
data-testid="move-to-trash"
onSelect={() => {
setOpen(true);
}}
/>
<MoveToTrash data-testid="move-to-trash" onSelect={onRemoveToTrash} />
</>
);
return (
@@ -99,15 +91,6 @@ export const OperationCell = ({
</IconButton>
</Menu>
</FlexWrapper>
<MoveToTrash.ConfirmModal
open={open}
title={title}
onConfirm={() => {
onRemoveToTrash();
setOpen(false);
}}
onOpenChange={setOpen}
/>
<DisablePublicSharing.DisablePublicSharingModal
onConfirm={onDisablePublicSharing}
open={openDisableShared}
@@ -1,6 +1,4 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageBlockModel } from '@blocksuite/blocks';
import {
ExportIcon,
ExportToHtmlIcon,
@@ -9,264 +7,107 @@ import {
ExportToPngIcon,
} from '@blocksuite/icons';
import { MenuIcon, MenuItem, MenuSub } from '@toeverything/components/menu';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { type ReactNode, useMemo } from 'react';
import { getContentParser } from './get-content-parser';
import { transitionStyle } from './index.css';
import type { CommonMenuItemProps } from './types';
export const ExportToPdfMenuItem = ({
interface ExportMenuItemProps<T> {
onSelect: () => void;
className?: string;
type: T;
icon: ReactNode;
label: string;
}
interface ExportProps {
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
className?: string;
}
export function ExportMenuItem<T>({
onSelect,
className,
}: CommonMenuItemProps<{ type: 'pdf' }>) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
const onClickDownloadPDF = useCallback(() => {
if (!currentEditor) {
return;
}
if (environment.isDesktop && currentEditor.mode === 'page') {
window.apis?.export
.savePDFFileAs(
(currentEditor.page.root as PageBlockModel).title.toString()
)
.then(() => {
onSelect?.({ type: 'pdf' });
setPushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
})
.catch(err => {
console.error(err);
setPushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
});
} else {
const contentParser = getContentParser(currentEditor.page);
contentParser
.exportPdf()
.then(() => {
onSelect?.({ type: 'pdf' });
setPushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
})
.catch(err => {
console.error(err);
setPushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
});
}
}, [currentEditor, onSelect, setPushNotification, t]);
type,
icon,
label,
}: ExportMenuItemProps<T>) {
return (
<MenuItem
className={className}
data-testid="export-to-pdf"
onSelect={onClickDownloadPDF}
data-testid={`export-to-${type}`}
onSelect={onSelect}
block
preFix={
<MenuIcon>
<ExportToPdfIcon />
</MenuIcon>
}
preFix={<MenuIcon>{icon}</MenuIcon>}
>
{t['Export to PDF']()}
{label}
</MenuItem>
);
};
}
export const ExportToHtmlMenuItem = ({
onSelect,
className,
}: CommonMenuItemProps<{ type: 'html' }>) => {
export const ExportMenuItems = ({
exportHandler,
className = transitionStyle,
}: ExportProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
const onClickExportHtml = useCallback(() => {
if (!currentEditor) {
return;
}
const contentParser = getContentParser(currentEditor.page);
contentParser
.exportHtml()
.then(() => {
onSelect?.({ type: 'html' });
setPushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
})
.catch(err => {
console.error(err);
setPushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
});
onSelect?.({ type: 'html' });
}, [currentEditor, onSelect, setPushNotification, t]);
return (
<>
<MenuItem
className={className}
data-testid="export-to-html"
onSelect={onClickExportHtml}
block
preFix={
<MenuIcon>
<ExportToHtmlIcon />
</MenuIcon>
}
>
{t['Export to HTML']()}
</MenuItem>
</>
const itemMap = useMemo(
() => [
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('pdf'),
className: className,
type: 'pdf',
icon: <ExportToPdfIcon />,
label: t['Export to PDF'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('html'),
className: className,
type: 'html',
icon: <ExportToHtmlIcon />,
label: t['Export to HTML'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('png'),
className: className,
type: 'png',
icon: <ExportToPngIcon />,
label: t['Export to PNG'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('markdown'),
className: className,
type: 'markdown',
icon: <ExportToMarkdownIcon />,
label: t['Export to Markdown'](),
},
},
],
[className, exportHandler, t]
);
const items = itemMap.map(({ component: Component, props }) => (
<Component key={props.label} {...props} />
));
return <>{items}</>;
};
export const ExportToPngMenuItem = ({
onSelect,
className,
}: CommonMenuItemProps<{ type: 'png' }>) => {
export const Export = ({ exportHandler, className }: ExportProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
const onClickDownloadPNG = useCallback(() => {
if (!currentEditor) {
return;
}
const contentParser = getContentParser(currentEditor.page);
contentParser
.exportPng()
.then(() => {
onSelect?.({ type: 'png' });
setPushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
})
.catch(err => {
console.error(err);
setPushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
});
}, [currentEditor, onSelect, setPushNotification, t]);
return (
<>
<MenuItem
className={className}
data-testid="export-to-png"
onSelect={onClickDownloadPNG}
block
preFix={
<MenuIcon>
<ExportToPngIcon />
</MenuIcon>
}
>
{t['Export to PNG']()}
</MenuItem>
</>
const items = (
<ExportMenuItems exportHandler={exportHandler} className={className} />
);
};
export const ExportToMarkdownMenuItem = ({
onSelect,
className,
}: CommonMenuItemProps<{ type: 'markdown' }>) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
const onClickExportMarkdown = useCallback(() => {
if (!currentEditor) {
return;
}
const contentParser = getContentParser(currentEditor.page);
contentParser
.exportMarkdown()
.then(() => {
onSelect?.({ type: 'markdown' });
setPushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
})
.catch(err => {
console.error(err);
setPushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
});
onSelect?.({ type: 'markdown' });
}, [currentEditor, onSelect, setPushNotification, t]);
return (
<>
<MenuItem
className={className}
data-testid="export-to-markdown"
onSelect={onClickExportMarkdown}
block
preFix={
<MenuIcon>
<ExportToMarkdownIcon />
</MenuIcon>
}
>
{t['Export to Markdown']()}
</MenuItem>
</>
);
};
// fixme: refactor this file, export function may should be passed by 'props', this file is just a ui component
export const Export = () => {
const t = useAFFiNEI18N();
return (
<MenuSub
items={
<>
<ExportToPdfMenuItem
className={transitionStyle}
></ExportToPdfMenuItem>
<ExportToHtmlMenuItem
className={transitionStyle}
></ExportToHtmlMenuItem>
<ExportToPngMenuItem
className={transitionStyle}
></ExportToPngMenuItem>
<ExportToMarkdownMenuItem
className={transitionStyle}
></ExportToMarkdownMenuItem>
</>
}
items={items}
triggerOptions={{
className: transitionStyle,
preFix: (
@@ -3,20 +3,19 @@ import { LinkIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Divider } from '@toeverything/components/divider';
import {
ExportToHtmlMenuItem,
ExportToMarkdownMenuItem,
ExportToPdfMenuItem,
ExportToPngMenuItem,
} from '../page-list/operation-menu-items/export';
import { ExportMenuItems } from '../page-list/operation-menu-items/export';
import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu';
import { useSharingUrl } from './use-share-url';
export const ShareExport = (props: ShareMenuProps) => {
export const ShareExport = ({
workspace,
currentPage,
exportHandler,
}: ShareMenuProps) => {
const t = useAFFiNEI18N();
const workspaceId = props.workspace.id;
const pageId = props.currentPage.id;
const workspaceId = workspace.id;
const pageId = currentPage.id;
const { onClickCopyLink } = useSharingUrl({
workspaceId,
pageId,
@@ -29,10 +28,10 @@ export const ShareExport = (props: ShareMenuProps) => {
{t['com.affine.share-menu.ShareViaExport']()}
</div>
<div>
<ExportToPdfMenuItem className={styles.menuItemStyle} />
<ExportToHtmlMenuItem className={styles.menuItemStyle} />
<ExportToPngMenuItem className={styles.menuItemStyle} />
<ExportToMarkdownMenuItem className={styles.menuItemStyle} />
<ExportMenuItems
exportHandler={exportHandler}
className={styles.menuItemStyle}
/>
</div>
<div className={styles.columnContainerStyle}>
<div className={styles.descriptionStyle}>
@@ -27,6 +27,7 @@ export interface ShareMenuProps<
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
onEnableAffineCloud: () => void;
togglePagePublic: () => Promise<void>;
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
}
export const ShareMenu = (props: ShareMenuProps) => {