mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 18:09:58 +08:00
feat(core): add editor commanads (#4514)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
-35
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user