feat: use custom @ import dialog (#8723)
[BS-1747](https://linear.app/affine-design/issue/BS-1747/[ui]-通过导入的ui还是旧的,需要更新)
This commit is contained in:
@@ -167,6 +167,7 @@ export const AFFINE_FLAGS = {
|
|||||||
displayName: 'Enable Snapshot Import Export',
|
displayName: 'Enable Snapshot Import Export',
|
||||||
description:
|
description:
|
||||||
'Once enabled, users can import and export blocksuite snapshots',
|
'Once enabled, users can import and export blocksuite snapshots',
|
||||||
|
hide: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
defaultState: false,
|
defaultState: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
import { Button, IconButton, Modal } from '@affine/component';
|
|
||||||
import { UrlService } from '@affine/core/modules/url';
|
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import track from '@affine/track';
|
|
||||||
import {
|
|
||||||
MarkdownTransformer,
|
|
||||||
NotionHtmlTransformer,
|
|
||||||
openFileOrFiles,
|
|
||||||
} from '@blocksuite/affine/blocks';
|
|
||||||
import type { DocCollection } from '@blocksuite/affine/store';
|
|
||||||
import {
|
|
||||||
ExportToMarkdownIcon,
|
|
||||||
HelpIcon,
|
|
||||||
NotionIcon,
|
|
||||||
} from '@blocksuite/icons/rc';
|
|
||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
|
||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { type ReactElement, useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import { openImportModalAtom } from '../../atoms';
|
|
||||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
|
||||||
import * as style from './style.css';
|
|
||||||
|
|
||||||
type ImportType = 'markdown' | 'markdownZip' | 'notion';
|
|
||||||
type AcceptType = 'Markdown' | 'Zip';
|
|
||||||
type Status = 'idle' | 'importing' | 'success' | 'error';
|
|
||||||
|
|
||||||
type ImportConfig = {
|
|
||||||
fileOptions: { acceptType: AcceptType; multiple: boolean };
|
|
||||||
importFunction: (
|
|
||||||
docCollection: DocCollection,
|
|
||||||
file: File | File[]
|
|
||||||
) => Promise<string[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DISCORD_URL = 'https://discord.gg/whd5mjYqVw';
|
|
||||||
|
|
||||||
const importOptions = [
|
|
||||||
{
|
|
||||||
label: 'com.affine.import.markdown-files',
|
|
||||||
prefixIcon: (
|
|
||||||
<ExportToMarkdownIcon
|
|
||||||
color={cssVarV2('icon/primary')}
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
testId: 'editor-option-menu-import-markdown-files',
|
|
||||||
type: 'markdown' as ImportType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'com.affine.import.markdown-with-media-files',
|
|
||||||
prefixIcon: (
|
|
||||||
<ExportToMarkdownIcon
|
|
||||||
color={cssVarV2('icon/primary')}
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
testId: 'editor-option-menu-import-markdown-with-media',
|
|
||||||
type: 'markdownZip' as ImportType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'com.affine.import.notion',
|
|
||||||
prefixIcon: <NotionIcon color={cssVar('black')} width={20} height={20} />,
|
|
||||||
suffixIcon: (
|
|
||||||
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
|
||||||
),
|
|
||||||
suffixTooltip: 'com.affine.import.notion.tooltip',
|
|
||||||
testId: 'editor-option-menu-import-notion',
|
|
||||||
type: 'notion' as ImportType,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const importConfigs: Record<ImportType, ImportConfig> = {
|
|
||||||
markdown: {
|
|
||||||
fileOptions: { acceptType: 'Markdown', multiple: true },
|
|
||||||
importFunction: async (docCollection, files) => {
|
|
||||||
if (!Array.isArray(files)) {
|
|
||||||
throw new Error('Expected an array of files for markdown files import');
|
|
||||||
}
|
|
||||||
const pageIds: string[] = [];
|
|
||||||
for (const file of files) {
|
|
||||||
const text = await file.text();
|
|
||||||
const fileName = file.name.split('.').slice(0, -1).join('.');
|
|
||||||
const pageId = await MarkdownTransformer.importMarkdownToDoc({
|
|
||||||
collection: docCollection,
|
|
||||||
markdown: text,
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
if (pageId) pageIds.push(pageId);
|
|
||||||
}
|
|
||||||
return pageIds;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
markdownZip: {
|
|
||||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
|
||||||
importFunction: async (docCollection, file) => {
|
|
||||||
if (Array.isArray(file)) {
|
|
||||||
throw new Error('Expected a single zip file for markdownZip import');
|
|
||||||
}
|
|
||||||
return MarkdownTransformer.importMarkdownZip({
|
|
||||||
collection: docCollection,
|
|
||||||
imported: file,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
notion: {
|
|
||||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
|
||||||
importFunction: async (docCollection, file) => {
|
|
||||||
if (Array.isArray(file)) {
|
|
||||||
throw new Error('Expected a single zip file for notion import');
|
|
||||||
}
|
|
||||||
const { pageIds } = await NotionHtmlTransformer.importNotionZip({
|
|
||||||
collection: docCollection,
|
|
||||||
imported: file,
|
|
||||||
});
|
|
||||||
return pageIds;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportOptionItem = ({
|
|
||||||
label,
|
|
||||||
prefixIcon,
|
|
||||||
suffixIcon,
|
|
||||||
suffixTooltip,
|
|
||||||
type,
|
|
||||||
onImport,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
prefixIcon: ReactElement;
|
|
||||||
suffixIcon?: ReactElement;
|
|
||||||
suffixTooltip?: string;
|
|
||||||
type: ImportType;
|
|
||||||
onImport: (type: ImportType) => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<div className={style.importItem} onClick={() => onImport(type)}>
|
|
||||||
{prefixIcon}
|
|
||||||
<div className={style.importItemLabel}>{t[label]()}</div>
|
|
||||||
{suffixIcon && (
|
|
||||||
<IconButton
|
|
||||||
className={style.importItemSuffix}
|
|
||||||
icon={suffixIcon}
|
|
||||||
tooltip={suffixTooltip ? t[suffixTooltip]() : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportOptions = ({
|
|
||||||
onImport,
|
|
||||||
}: {
|
|
||||||
onImport: (type: ImportType) => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.importModalTitle}>{t['Import']()}</div>
|
|
||||||
<div className={style.importModalContent}>
|
|
||||||
{importOptions.map(
|
|
||||||
({ label, prefixIcon, suffixIcon, suffixTooltip, testId, type }) => (
|
|
||||||
<ImportOptionItem
|
|
||||||
key={testId}
|
|
||||||
prefixIcon={prefixIcon}
|
|
||||||
suffixIcon={suffixIcon}
|
|
||||||
suffixTooltip={suffixTooltip}
|
|
||||||
label={label}
|
|
||||||
data-testid={testId}
|
|
||||||
type={type}
|
|
||||||
onImport={onImport}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={style.importModalTip}>
|
|
||||||
{t['com.affine.import.modal.tip']()}{' '}
|
|
||||||
<a
|
|
||||||
className={style.link}
|
|
||||||
href="https://discord.gg/whd5mjYqVw"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</a>{' '}
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportingStatus = () => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.importModalTitle}>
|
|
||||||
{t['com.affine.import.status.importing.title']()}
|
|
||||||
</div>
|
|
||||||
<p className={style.importStatusContent}>
|
|
||||||
{t['com.affine.import.status.importing.message']()}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SuccessStatus = ({ onComplete }: { onComplete: () => void }) => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.importModalTitle}>
|
|
||||||
{t['com.affine.import.status.success.title']()}
|
|
||||||
</div>
|
|
||||||
<p className={style.importStatusContent}>
|
|
||||||
{t['com.affine.import.status.success.message']()}{' '}
|
|
||||||
<a
|
|
||||||
className={style.link}
|
|
||||||
href={DISCORD_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<div className={style.importModalButtonContainer}>
|
|
||||||
<Button onClick={onComplete} variant="primary">
|
|
||||||
{t['Complete']()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ErrorStatus = ({
|
|
||||||
error,
|
|
||||||
onRetry,
|
|
||||||
}: {
|
|
||||||
error: string | null;
|
|
||||||
onRetry: () => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const urlService = useService(UrlService);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.importModalTitle}>
|
|
||||||
{t['com.affine.import.status.failed.title']()}
|
|
||||||
</div>
|
|
||||||
<p className={style.importStatusContent}>
|
|
||||||
{error || 'Unknown error occurred'}
|
|
||||||
</p>
|
|
||||||
<div className={style.importModalButtonContainer}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
urlService.openPopupWindow(DISCORD_URL);
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{t['Feedback']()}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onRetry} variant="primary">
|
|
||||||
{t['Retry']()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ImportModal = ({ ...modalProps }) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const [status, setStatus] = useState<Status>('idle');
|
|
||||||
const [importError, setImportError] = useState<string | null>(null);
|
|
||||||
const [pageIds, setPageIds] = useState<string[]>([]);
|
|
||||||
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
|
||||||
const docCollection = workspace.docCollection;
|
|
||||||
|
|
||||||
const handleImport = useAsyncCallback(
|
|
||||||
async (type: ImportType) => {
|
|
||||||
setImportError(null);
|
|
||||||
try {
|
|
||||||
const importConfig = importConfigs[type];
|
|
||||||
const file = await openFileOrFiles(importConfig.fileOptions);
|
|
||||||
|
|
||||||
if (!file || (Array.isArray(file) && file.length === 0)) {
|
|
||||||
throw new Error(
|
|
||||||
t['com.affine.import.status.failed.message.no-file-selected']()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus('importing');
|
|
||||||
track.$.importModal.$.import({
|
|
||||||
type,
|
|
||||||
status: 'importing',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageIds = await importConfig.importFunction(docCollection, file);
|
|
||||||
|
|
||||||
setPageIds(pageIds);
|
|
||||||
setStatus('success');
|
|
||||||
track.$.importModal.$.import({
|
|
||||||
type,
|
|
||||||
status: 'success',
|
|
||||||
result: {
|
|
||||||
docCount: pageIds.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setImportError(
|
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred'
|
|
||||||
);
|
|
||||||
setStatus('error');
|
|
||||||
track.$.importModal.$.import({
|
|
||||||
type,
|
|
||||||
status: 'failed',
|
|
||||||
error: importError || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[docCollection, t, importError]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleComplete = useCallback(() => {
|
|
||||||
if (pageIds.length > 1) {
|
|
||||||
workbench.openAll();
|
|
||||||
} else if (pageIds.length === 1) {
|
|
||||||
workbench.openDoc(pageIds[0]);
|
|
||||||
}
|
|
||||||
setOpenImportModalAtom(false);
|
|
||||||
}, [pageIds, workbench, setOpenImportModalAtom]);
|
|
||||||
|
|
||||||
const handleRetry = () => {
|
|
||||||
setStatus('idle');
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusComponents = {
|
|
||||||
idle: <ImportOptions onImport={handleImport} />,
|
|
||||||
importing: <ImportingStatus />,
|
|
||||||
success: <SuccessStatus onComplete={handleComplete} />,
|
|
||||||
error: <ErrorStatus error={importError} onRetry={handleRetry} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
width={480}
|
|
||||||
contentOptions={{
|
|
||||||
['data-testid' as string]: 'import-modal',
|
|
||||||
style: {
|
|
||||||
maxHeight: '85vh',
|
|
||||||
maxWidth: '70vw',
|
|
||||||
minHeight: '126px',
|
|
||||||
padding: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
background: cssVarV2('layer/background/primary'),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
closeButtonOptions={{
|
|
||||||
className: style.closeButton,
|
|
||||||
}}
|
|
||||||
withoutCloseButton={status === 'importing'}
|
|
||||||
persistent={status === 'importing'}
|
|
||||||
{...modalProps}
|
|
||||||
>
|
|
||||||
<div className={style.importModalContainer}>
|
|
||||||
{statusComponents[status]}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const importModalContainer = style({
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: '20px 24px',
|
|
||||||
gap: '12px',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importModalTitle = style({
|
|
||||||
width: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
fontSize: cssVar('fontH6'),
|
|
||||||
fontWeight: '600',
|
|
||||||
lineHeight: cssVar('lineHeight'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importModalContent = style({
|
|
||||||
width: '100%',
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '12px',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const closeButton = style({
|
|
||||||
top: '24px',
|
|
||||||
right: '24px',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importModalTip = style({
|
|
||||||
width: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
lineHeight: cssVar('lineHeight'),
|
|
||||||
fontWeight: '400',
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const link = style({
|
|
||||||
color: cssVar('linkColor'),
|
|
||||||
cursor: 'pointer',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importStatusContent = style({
|
|
||||||
width: '100%',
|
|
||||||
fontSize: cssVar('fontBase'),
|
|
||||||
lineHeight: cssVar('lineHeight'),
|
|
||||||
fontWeight: '400',
|
|
||||||
color: cssVar('textPrimaryColor'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importModalButtonContainer = style({
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: '20px',
|
|
||||||
justifyContent: 'end',
|
|
||||||
marginTop: '20px',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importItem = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
gap: '4px',
|
|
||||||
padding: '8px 12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
|
||||||
background: cssVarV2('button/secondary'),
|
|
||||||
selectors: {
|
|
||||||
'&:hover': {
|
|
||||||
background: cssVarV2('layer/background/hoverOverlay'),
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background .30s',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importItemLabel = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0 4px',
|
|
||||||
textAlign: 'left',
|
|
||||||
flex: 1,
|
|
||||||
color: cssVar('textPrimaryColor'),
|
|
||||||
fontSize: cssVar('fontBase'),
|
|
||||||
lineHeight: cssVar('lineHeight'),
|
|
||||||
fontWeight: '500',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importItemPrefix = style({
|
|
||||||
marginRight: 'auto',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const importItemSuffix = style({
|
|
||||||
marginLeft: 'auto',
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { I18n } from '@affine/i18n';
|
import { I18n } from '@affine/i18n';
|
||||||
@@ -8,6 +9,68 @@ import { LinkedWidgetUtils } from '@blocksuite/affine/blocks';
|
|||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { type FrameworkProvider, WorkspaceService } from '@toeverything/infra';
|
import { type FrameworkProvider, WorkspaceService } from '@toeverything/infra';
|
||||||
|
|
||||||
|
function createNewDocMenuGroup(
|
||||||
|
framework: FrameworkProvider,
|
||||||
|
query: string,
|
||||||
|
abort: () => void,
|
||||||
|
editorHost: EditorHost,
|
||||||
|
inlineEditor: AffineInlineEditor
|
||||||
|
) {
|
||||||
|
const originalNewDocMenuGroup = LinkedWidgetUtils.createNewDocMenuGroup(
|
||||||
|
query,
|
||||||
|
abort,
|
||||||
|
editorHost,
|
||||||
|
inlineEditor
|
||||||
|
);
|
||||||
|
|
||||||
|
// Patch the import item, to use the custom import dialog.
|
||||||
|
const importItemIndex = originalNewDocMenuGroup.items.findIndex(
|
||||||
|
item => item.key === 'import'
|
||||||
|
);
|
||||||
|
if (importItemIndex === -1) {
|
||||||
|
return originalNewDocMenuGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalItems = originalNewDocMenuGroup.items;
|
||||||
|
const originalImportItem = originalItems[importItemIndex];
|
||||||
|
const customImportItem = {
|
||||||
|
...originalImportItem,
|
||||||
|
action: () => {
|
||||||
|
abort();
|
||||||
|
track.doc.editor.atMenu.import();
|
||||||
|
framework
|
||||||
|
.get(WorkspaceDialogService)
|
||||||
|
.open('import', undefined, payload => {
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the imported file is a workspace file, insert the entry page node.
|
||||||
|
const { docIds, entryId, isWorkspaceFile } = payload;
|
||||||
|
if (isWorkspaceFile && entryId) {
|
||||||
|
LinkedWidgetUtils.insertLinkedNode({
|
||||||
|
inlineEditor,
|
||||||
|
docId: entryId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, insert all the doc nodes.
|
||||||
|
for (const docId of docIds) {
|
||||||
|
LinkedWidgetUtils.insertLinkedNode({
|
||||||
|
inlineEditor,
|
||||||
|
docId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// only replace the original import item
|
||||||
|
originalItems.splice(importItemIndex, 1, customImportItem);
|
||||||
|
return originalNewDocMenuGroup;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: fix the type
|
// TODO: fix the type
|
||||||
export function createLinkedWidgetConfig(
|
export function createLinkedWidgetConfig(
|
||||||
framework: FrameworkProvider
|
framework: FrameworkProvider
|
||||||
@@ -70,7 +133,8 @@ export function createLinkedWidgetConfig(
|
|||||||
maxDisplay: MAX_DOCS,
|
maxDisplay: MAX_DOCS,
|
||||||
overflowText: `${docMetas.length - MAX_DOCS} more docs`,
|
overflowText: `${docMetas.length - MAX_DOCS} more docs`,
|
||||||
},
|
},
|
||||||
LinkedWidgetUtils.createNewDocMenuGroup(
|
createNewDocMenuGroup(
|
||||||
|
framework,
|
||||||
query,
|
query,
|
||||||
abort,
|
abort,
|
||||||
editorHost,
|
editorHost,
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import {
|
|||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||||
import {
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
GlobalDialogService,
|
|
||||||
WorkspaceDialogService,
|
|
||||||
} from '@affine/core/modules/dialogs';
|
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
@@ -79,7 +76,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
const globalDialogService = useService(GlobalDialogService);
|
|
||||||
const editorService = useService(EditorService);
|
const editorService = useService(EditorService);
|
||||||
const isInTrash = useLiveData(
|
const isInTrash = useLiveData(
|
||||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||||
@@ -204,10 +200,39 @@ export const PageHeaderMenuButton = ({
|
|||||||
});
|
});
|
||||||
}, [duplicate, pageId]);
|
}, [duplicate, pageId]);
|
||||||
|
|
||||||
|
const handleOpenDocs = useCallback(
|
||||||
|
(result: {
|
||||||
|
docIds: string[];
|
||||||
|
entryId?: string;
|
||||||
|
isWorkspaceFile?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { docIds, entryId, isWorkspaceFile } = result;
|
||||||
|
// If the imported file is a workspace file, open the entry page.
|
||||||
|
if (isWorkspaceFile && entryId) {
|
||||||
|
workbench.openDoc(entryId);
|
||||||
|
} else if (!docIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open all the docs when there are multiple docs imported.
|
||||||
|
if (docIds.length > 1) {
|
||||||
|
workbench.openAll();
|
||||||
|
} else {
|
||||||
|
// Otherwise, open the only doc.
|
||||||
|
workbench.openDoc(docIds[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workbench]
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenImportModal = useCallback(() => {
|
const handleOpenImportModal = useCallback(() => {
|
||||||
track.$.header.importModal.open();
|
track.$.header.importModal.open();
|
||||||
globalDialogService.open('import', undefined);
|
workspaceDialogService.open('import', undefined, payload => {
|
||||||
}, [globalDialogService]);
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleOpenDocs(payload);
|
||||||
|
});
|
||||||
|
}, [workspaceDialogService, handleOpenDocs]);
|
||||||
|
|
||||||
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
SidebarScrollableContainer,
|
SidebarScrollableContainer,
|
||||||
} from '@affine/core/modules/app-sidebar/views';
|
} from '@affine/core/modules/app-sidebar/views';
|
||||||
import { ExternalMenuLinkItem } from '@affine/core/modules/app-sidebar/views/menu-item/external-menu-link-item';
|
import { ExternalMenuLinkItem } from '@affine/core/modules/app-sidebar/views/menu-item/external-menu-link-item';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import {
|
||||||
|
GlobalDialogService,
|
||||||
|
WorkspaceDialogService,
|
||||||
|
} from '@affine/core/modules/dialogs';
|
||||||
import {
|
import {
|
||||||
ExplorerCollections,
|
ExplorerCollections,
|
||||||
ExplorerFavorites,
|
ExplorerFavorites,
|
||||||
@@ -84,6 +87,7 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
const currentWorkspace = workspaceService.workspace;
|
const currentWorkspace = workspaceService.workspace;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const globalDialogService = useService(GlobalDialogService);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
const currentPath = useLiveData(
|
const currentPath = useLiveData(
|
||||||
workbench.location$.map(location => location.pathname)
|
workbench.location$.map(location => location.pathname)
|
||||||
@@ -111,10 +115,39 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
track.$.navigationPanel.$.openSettings();
|
track.$.navigationPanel.$.openSettings();
|
||||||
}, [globalDialogService]);
|
}, [globalDialogService]);
|
||||||
|
|
||||||
|
const handleOpenDocs = useCallback(
|
||||||
|
(result: {
|
||||||
|
docIds: string[];
|
||||||
|
entryId?: string;
|
||||||
|
isWorkspaceFile?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { docIds, entryId, isWorkspaceFile } = result;
|
||||||
|
// If the imported file is a workspace file, open the entry page.
|
||||||
|
if (isWorkspaceFile && entryId) {
|
||||||
|
workbench.openDoc(entryId);
|
||||||
|
} else if (!docIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open all the docs when there are multiple docs imported.
|
||||||
|
if (docIds.length > 1) {
|
||||||
|
workbench.openAll();
|
||||||
|
} else {
|
||||||
|
// Otherwise, open the only doc.
|
||||||
|
workbench.openDoc(docIds[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workbench]
|
||||||
|
);
|
||||||
|
|
||||||
const onOpenImportModal = useCallback(() => {
|
const onOpenImportModal = useCallback(() => {
|
||||||
track.$.navigationPanel.importModal.open();
|
track.$.navigationPanel.importModal.open();
|
||||||
globalDialogService.open('import', undefined);
|
workspaceDialogService.open('import', undefined, payload => {
|
||||||
}, [globalDialogService]);
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleOpenDocs(payload);
|
||||||
|
});
|
||||||
|
}, [workspaceDialogService, handleOpenDocs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar>
|
<AppSidebar>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { Button, IconButton, Modal } from '@affine/component';
|
|||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import type {
|
import type {
|
||||||
DialogComponentProps,
|
DialogComponentProps,
|
||||||
GLOBAL_DIALOG_SCHEMA,
|
WORKSPACE_DIALOG_SCHEMA,
|
||||||
} from '@affine/core/modules/dialogs';
|
} from '@affine/core/modules/dialogs';
|
||||||
import { UrlService } from '@affine/core/modules/url';
|
import { UrlService } from '@affine/core/modules/url';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import track from '@affine/track';
|
||||||
import {
|
import {
|
||||||
MarkdownTransformer,
|
MarkdownTransformer,
|
||||||
NotionHtmlTransformer,
|
NotionHtmlTransformer,
|
||||||
@@ -28,13 +28,18 @@ import * as style from './styles.css';
|
|||||||
type ImportType = 'markdown' | 'markdownZip' | 'notion';
|
type ImportType = 'markdown' | 'markdownZip' | 'notion';
|
||||||
type AcceptType = 'Markdown' | 'Zip';
|
type AcceptType = 'Markdown' | 'Zip';
|
||||||
type Status = 'idle' | 'importing' | 'success' | 'error';
|
type Status = 'idle' | 'importing' | 'success' | 'error';
|
||||||
|
type ImportResult = {
|
||||||
|
docIds: string[];
|
||||||
|
entryId?: string;
|
||||||
|
isWorkspaceFile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ImportConfig = {
|
type ImportConfig = {
|
||||||
fileOptions: { acceptType: AcceptType; multiple: boolean };
|
fileOptions: { acceptType: AcceptType; multiple: boolean };
|
||||||
importFunction: (
|
importFunction: (
|
||||||
docCollection: DocCollection,
|
docCollection: DocCollection,
|
||||||
file: File | File[]
|
file: File | File[]
|
||||||
) => Promise<string[]>;
|
) => Promise<ImportResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DISCORD_URL = 'https://discord.gg/whd5mjYqVw';
|
const DISCORD_URL = 'https://discord.gg/whd5mjYqVw';
|
||||||
@@ -83,18 +88,20 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
|||||||
if (!Array.isArray(files)) {
|
if (!Array.isArray(files)) {
|
||||||
throw new Error('Expected an array of files for markdown files import');
|
throw new Error('Expected an array of files for markdown files import');
|
||||||
}
|
}
|
||||||
const pageIds: string[] = [];
|
const docIds: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const fileName = file.name.split('.').slice(0, -1).join('.');
|
const fileName = file.name.split('.').slice(0, -1).join('.');
|
||||||
const pageId = await MarkdownTransformer.importMarkdownToDoc({
|
const docId = await MarkdownTransformer.importMarkdownToDoc({
|
||||||
collection: docCollection,
|
collection: docCollection,
|
||||||
markdown: text,
|
markdown: text,
|
||||||
fileName,
|
fileName,
|
||||||
});
|
});
|
||||||
if (pageId) pageIds.push(pageId);
|
if (docId) docIds.push(docId);
|
||||||
}
|
}
|
||||||
return pageIds;
|
return {
|
||||||
|
docIds,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
markdownZip: {
|
markdownZip: {
|
||||||
@@ -103,10 +110,13 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
|||||||
if (Array.isArray(file)) {
|
if (Array.isArray(file)) {
|
||||||
throw new Error('Expected a single zip file for markdownZip import');
|
throw new Error('Expected a single zip file for markdownZip import');
|
||||||
}
|
}
|
||||||
return MarkdownTransformer.importMarkdownZip({
|
const docIds = await MarkdownTransformer.importMarkdownZip({
|
||||||
collection: docCollection,
|
collection: docCollection,
|
||||||
imported: file,
|
imported: file,
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
docIds,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notion: {
|
notion: {
|
||||||
@@ -115,11 +125,16 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
|||||||
if (Array.isArray(file)) {
|
if (Array.isArray(file)) {
|
||||||
throw new Error('Expected a single zip file for notion import');
|
throw new Error('Expected a single zip file for notion import');
|
||||||
}
|
}
|
||||||
const { pageIds } = await NotionHtmlTransformer.importNotionZip({
|
const { entryId, pageIds, isWorkspaceFile } =
|
||||||
collection: docCollection,
|
await NotionHtmlTransformer.importNotionZip({
|
||||||
imported: file,
|
collection: docCollection,
|
||||||
});
|
imported: file,
|
||||||
return pageIds;
|
});
|
||||||
|
return {
|
||||||
|
docIds: pageIds,
|
||||||
|
entryId,
|
||||||
|
isWorkspaceFile,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -274,13 +289,12 @@ const ErrorStatus = ({
|
|||||||
|
|
||||||
export const ImportDialog = ({
|
export const ImportDialog = ({
|
||||||
close,
|
close,
|
||||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['import']>) => {
|
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['import']>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [status, setStatus] = useState<Status>('idle');
|
const [status, setStatus] = useState<Status>('idle');
|
||||||
const [importError, setImportError] = useState<string | null>(null);
|
const [importError, setImportError] = useState<string | null>(null);
|
||||||
const [pageIds, setPageIds] = useState<string[]>([]);
|
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
|
|
||||||
const handleImport = useAsyncCallback(
|
const handleImport = useAsyncCallback(
|
||||||
@@ -297,29 +311,41 @@ export const ImportDialog = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStatus('importing');
|
setStatus('importing');
|
||||||
|
track.$.importModal.$.import({
|
||||||
|
type,
|
||||||
|
status: 'importing',
|
||||||
|
});
|
||||||
|
|
||||||
const pageIds = await importConfig.importFunction(docCollection, file);
|
const { docIds, entryId, isWorkspaceFile } =
|
||||||
|
await importConfig.importFunction(docCollection, file);
|
||||||
|
|
||||||
setPageIds(pageIds);
|
setImportResult({ docIds, entryId, isWorkspaceFile });
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
|
track.$.importModal.$.import({
|
||||||
|
type,
|
||||||
|
status: 'success',
|
||||||
|
result: {
|
||||||
|
docCount: docIds.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setImportError(
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred'
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
);
|
setImportError(errorMessage);
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
|
track.$.importModal.$.import({
|
||||||
|
type,
|
||||||
|
status: 'failed',
|
||||||
|
error: errorMessage || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[docCollection, t]
|
[docCollection, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleComplete = useCallback(() => {
|
const handleComplete = useCallback(() => {
|
||||||
if (pageIds.length > 1) {
|
close(importResult || undefined);
|
||||||
workbench.openAll();
|
}, [importResult, close]);
|
||||||
} else if (pageIds.length === 1) {
|
|
||||||
workbench.openDoc(pageIds[0]);
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}, [pageIds, close, workbench]);
|
|
||||||
|
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
@@ -335,8 +361,10 @@ export const ImportDialog = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
onOpenChange={() => {
|
onOpenChange={(open: boolean) => {
|
||||||
close();
|
if (!open) {
|
||||||
|
close(importResult || undefined);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
width={480}
|
width={480}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
@@ -357,7 +385,7 @@ export const ImportDialog = ({
|
|||||||
withoutCloseButton={status === 'importing'}
|
withoutCloseButton={status === 'importing'}
|
||||||
persistent={status === 'importing'}
|
persistent={status === 'importing'}
|
||||||
>
|
>
|
||||||
<div className={style.importModalContainer}>
|
<div className={style.importModalContainer} data-testid="import-dialog">
|
||||||
{statusComponents[status]}
|
{statusComponents[status]}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const GLOBAL_DIALOGS = {
|
|||||||
'import-workspace': ImportWorkspaceDialog,
|
'import-workspace': ImportWorkspaceDialog,
|
||||||
'import-template': ImportTemplateDialog,
|
'import-template': ImportTemplateDialog,
|
||||||
setting: SettingDialog,
|
setting: SettingDialog,
|
||||||
import: ImportDialog,
|
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||||
@@ -37,6 +36,7 @@ const WORKSPACE_DIALOGS = {
|
|||||||
'tag-selector': TagSelectorDialog,
|
'tag-selector': TagSelectorDialog,
|
||||||
'doc-selector': DocSelectorDialog,
|
'doc-selector': DocSelectorDialog,
|
||||||
'collection-selector': CollectionSelectorDialog,
|
'collection-selector': CollectionSelectorDialog,
|
||||||
|
import: ImportDialog,
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC<
|
[key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC<
|
||||||
DialogComponentProps<WORKSPACE_DIALOG_SCHEMA[key]>
|
DialogComponentProps<WORKSPACE_DIALOG_SCHEMA[key]>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export type GLOBAL_DIALOG_SCHEMA = {
|
|||||||
templateMode: DocMode;
|
templateMode: DocMode;
|
||||||
snapshotUrl: string;
|
snapshotUrl: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
import: () => void;
|
|
||||||
setting: (props: {
|
setting: (props: {
|
||||||
activeTab?: SettingTab;
|
activeTab?: SettingTab;
|
||||||
workspaceMetadata?: WorkspaceMetadata | null;
|
workspaceMetadata?: WorkspaceMetadata | null;
|
||||||
@@ -52,4 +51,9 @@ export type WORKSPACE_DIALOG_SCHEMA = {
|
|||||||
init: string[];
|
init: string[];
|
||||||
onBeforeConfirm?: (ids: string[], cb: () => void) => void;
|
onBeforeConfirm?: (ids: string[], cb: () => void) => void;
|
||||||
}) => string[];
|
}) => string[];
|
||||||
|
import: () => {
|
||||||
|
docIds: string[];
|
||||||
|
entryId?: string;
|
||||||
|
isWorkspaceFile?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ const PageEvents = {
|
|||||||
doc: {
|
doc: {
|
||||||
editor: {
|
editor: {
|
||||||
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
|
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
|
||||||
atMenu: ['linkDoc'],
|
atMenu: ['linkDoc', 'import'],
|
||||||
quickSearch: ['createDoc'],
|
quickSearch: ['createDoc'],
|
||||||
formatToolbar: ['bold'],
|
formatToolbar: ['bold'],
|
||||||
pageRef: ['navigate'],
|
pageRef: ['navigate'],
|
||||||
|
|||||||
57
tests/affine-local/e2e/import-dialog.spec.ts
Normal file
57
tests/affine-local/e2e/import-dialog.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import {
|
||||||
|
clickNewPageButton,
|
||||||
|
waitForEmptyEditor,
|
||||||
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await waitForEmptyEditor(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Open import dialog by click root sidebar import button', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('slider-bar-import-button').click();
|
||||||
|
|
||||||
|
const importDialog = page.getByTestId('import-dialog');
|
||||||
|
await expect(importDialog).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('modal-close-button').click();
|
||||||
|
await expect(importDialog).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Open import dialog by click header menu import button', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('header-dropDownButton').click();
|
||||||
|
await page.getByTestId('editor-option-menu-import').click();
|
||||||
|
|
||||||
|
const importDialog = page.getByTestId('import-dialog');
|
||||||
|
await expect(importDialog).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('modal-close-button').click();
|
||||||
|
await expect(importDialog).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Open import dialog by @ menu import button', async ({ page }) => {
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
await page.keyboard.type('@', { delay: 50 });
|
||||||
|
const linkedPagePopover = page.locator('.linked-doc-popover');
|
||||||
|
await expect(linkedPagePopover).toBeVisible();
|
||||||
|
|
||||||
|
const importButton = page.locator(
|
||||||
|
'.linked-doc-popover icon-button[data-id="import"]'
|
||||||
|
);
|
||||||
|
await expect(importButton).toBeVisible();
|
||||||
|
await importButton.click();
|
||||||
|
|
||||||
|
const importDialog = page.getByTestId('import-dialog');
|
||||||
|
await expect(importDialog).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('modal-close-button').click();
|
||||||
|
await expect(importDialog).not.toBeVisible();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user