From c323e5ae9313b54ad47364e4f7a832ad1071fafc Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Fri, 8 Nov 2024 01:53:29 +0000 Subject: [PATCH] feat: use custom @ import dialog (#8723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [BS-1747](https://linear.app/affine-design/issue/BS-1747/[ui]-通过导入的ui还是旧的,需要更新) --- .../src/modules/feature-flag/constant.ts | 1 + .../components/affine/import-modal/index.tsx | 376 ------------------ .../affine/import-modal/style.css.ts | 110 ----- .../specs/custom/widgets/linked.ts | 66 ++- .../block-suite-header/menu/index.tsx | 39 +- .../src/components/root-app-sidebar/index.tsx | 39 +- .../core/src/desktop/dialogs/import/index.tsx | 90 +++-- .../core/src/desktop/dialogs/index.tsx | 2 +- .../core/src/modules/dialogs/constant.ts | 6 +- packages/frontend/track/src/events.ts | 2 +- tests/affine-local/e2e/import-dialog.spec.ts | 57 +++ 11 files changed, 257 insertions(+), 531 deletions(-) delete mode 100644 packages/frontend/core/src/components/affine/import-modal/index.tsx delete mode 100644 packages/frontend/core/src/components/affine/import-modal/style.css.ts create mode 100644 tests/affine-local/e2e/import-dialog.spec.ts diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index f045e65135b5d..cb081d37264fc 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -167,6 +167,7 @@ export const AFFINE_FLAGS = { displayName: 'Enable Snapshot Import Export', description: 'Once enabled, users can import and export blocksuite snapshots', + hide: true, configurable: true, defaultState: false, }, diff --git a/packages/frontend/core/src/components/affine/import-modal/index.tsx b/packages/frontend/core/src/components/affine/import-modal/index.tsx deleted file mode 100644 index b6766c38f726b..0000000000000 --- a/packages/frontend/core/src/components/affine/import-modal/index.tsx +++ /dev/null @@ -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; -}; - -const DISCORD_URL = 'https://discord.gg/whd5mjYqVw'; - -const importOptions = [ - { - label: 'com.affine.import.markdown-files', - prefixIcon: ( - - ), - testId: 'editor-option-menu-import-markdown-files', - type: 'markdown' as ImportType, - }, - { - label: 'com.affine.import.markdown-with-media-files', - prefixIcon: ( - - ), - testId: 'editor-option-menu-import-markdown-with-media', - type: 'markdownZip' as ImportType, - }, - { - label: 'com.affine.import.notion', - prefixIcon: , - suffixIcon: ( - - ), - suffixTooltip: 'com.affine.import.notion.tooltip', - testId: 'editor-option-menu-import-notion', - type: 'notion' as ImportType, - }, -]; - -const importConfigs: Record = { - 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 ( -
onImport(type)}> - {prefixIcon} -
{t[label]()}
- {suffixIcon && ( - - )} -
- ); -}; - -const ImportOptions = ({ - onImport, -}: { - onImport: (type: ImportType) => void; -}) => { - const t = useI18n(); - return ( - <> -
{t['Import']()}
-
- {importOptions.map( - ({ label, prefixIcon, suffixIcon, suffixTooltip, testId, type }) => ( - - ) - )} -
-
- {t['com.affine.import.modal.tip']()}{' '} - - Discord - {' '} - . -
- - ); -}; - -const ImportingStatus = () => { - const t = useI18n(); - return ( - <> -
- {t['com.affine.import.status.importing.title']()} -
-

- {t['com.affine.import.status.importing.message']()} -

- - ); -}; - -const SuccessStatus = ({ onComplete }: { onComplete: () => void }) => { - const t = useI18n(); - return ( - <> -
- {t['com.affine.import.status.success.title']()} -
-

- {t['com.affine.import.status.success.message']()}{' '} - - Discord - - . -

-
- -
- - ); -}; - -const ErrorStatus = ({ - error, - onRetry, -}: { - error: string | null; - onRetry: () => void; -}) => { - const t = useI18n(); - const urlService = useService(UrlService); - return ( - <> -
- {t['com.affine.import.status.failed.title']()} -
-

- {error || 'Unknown error occurred'} -

-
- - -
- - ); -}; - -export const ImportModal = ({ ...modalProps }) => { - const t = useI18n(); - const [status, setStatus] = useState('idle'); - const [importError, setImportError] = useState(null); - const [pageIds, setPageIds] = useState([]); - 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: , - importing: , - success: , - error: , - }; - - return ( - -
- {statusComponents[status]} -
-
- ); -}; diff --git a/packages/frontend/core/src/components/affine/import-modal/style.css.ts b/packages/frontend/core/src/components/affine/import-modal/style.css.ts deleted file mode 100644 index 3876a60bb69f8..0000000000000 --- a/packages/frontend/core/src/components/affine/import-modal/style.css.ts +++ /dev/null @@ -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', -}); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts index e9d490c7e14a9..dc2b36ebc31e3 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts @@ -1,3 +1,4 @@ +import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { JournalService } from '@affine/core/modules/journal'; import { I18n } from '@affine/i18n'; @@ -8,6 +9,68 @@ import { LinkedWidgetUtils } from '@blocksuite/affine/blocks'; import type { DocMeta } from '@blocksuite/affine/store'; 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 export function createLinkedWidgetConfig( framework: FrameworkProvider @@ -70,7 +133,8 @@ export function createLinkedWidgetConfig( maxDisplay: MAX_DOCS, overflowText: `${docMetas.length - MAX_DOCS} more docs`, }, - LinkedWidgetUtils.createNewDocMenuGroup( + createNewDocMenuGroup( + framework, query, abort, editorHost, diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index a7f8003d4add0..ba7f85212b8fd 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -18,10 +18,7 @@ import { } from '@affine/core/components/page-list'; import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive'; -import { - GlobalDialogService, - WorkspaceDialogService, -} from '@affine/core/modules/dialogs'; +import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { EditorService } from '@affine/core/modules/editor'; import { OpenInAppService } from '@affine/core/modules/open-in-app/services'; import { WorkbenchService } from '@affine/core/modules/workbench'; @@ -79,7 +76,6 @@ export const PageHeaderMenuButton = ({ const workspace = useService(WorkspaceService).workspace; - const globalDialogService = useService(GlobalDialogService); const editorService = useService(EditorService); const isInTrash = useLiveData( editorService.editor.doc.meta$.map(meta => meta.trash) @@ -204,10 +200,39 @@ export const PageHeaderMenuButton = ({ }); }, [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(() => { track.$.header.importModal.open(); - globalDialogService.open('import', undefined); - }, [globalDialogService]); + workspaceDialogService.open('import', undefined, payload => { + if (!payload) { + return; + } + handleOpenDocs(payload); + }); + }, [workspaceDialogService, handleOpenDocs]); const handleShareMenuOpenChange = useCallback((open: boolean) => { if (open) { diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 8a23f209952be..0b3b1c94d7fd1 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -11,7 +11,10 @@ import { SidebarScrollableContainer, } from '@affine/core/modules/app-sidebar/views'; 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 { ExplorerCollections, ExplorerFavorites, @@ -84,6 +87,7 @@ export const RootAppSidebar = (): ReactElement => { const currentWorkspace = workspaceService.workspace; const t = useI18n(); const globalDialogService = useService(GlobalDialogService); + const workspaceDialogService = useService(WorkspaceDialogService); const workbench = workbenchService.workbench; const currentPath = useLiveData( workbench.location$.map(location => location.pathname) @@ -111,10 +115,39 @@ export const RootAppSidebar = (): ReactElement => { track.$.navigationPanel.$.openSettings(); }, [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(() => { track.$.navigationPanel.importModal.open(); - globalDialogService.open('import', undefined); - }, [globalDialogService]); + workspaceDialogService.open('import', undefined, payload => { + if (!payload) { + return; + } + handleOpenDocs(payload); + }); + }, [workspaceDialogService, handleOpenDocs]); return ( diff --git a/packages/frontend/core/src/desktop/dialogs/import/index.tsx b/packages/frontend/core/src/desktop/dialogs/import/index.tsx index 174ace83c2b3e..855aa5e00f60a 100644 --- a/packages/frontend/core/src/desktop/dialogs/import/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import/index.tsx @@ -2,11 +2,11 @@ import { Button, IconButton, Modal } from '@affine/component'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import type { DialogComponentProps, - GLOBAL_DIALOG_SCHEMA, + WORKSPACE_DIALOG_SCHEMA, } from '@affine/core/modules/dialogs'; 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, @@ -28,13 +28,18 @@ import * as style from './styles.css'; type ImportType = 'markdown' | 'markdownZip' | 'notion'; type AcceptType = 'Markdown' | 'Zip'; type Status = 'idle' | 'importing' | 'success' | 'error'; +type ImportResult = { + docIds: string[]; + entryId?: string; + isWorkspaceFile?: boolean; +}; type ImportConfig = { fileOptions: { acceptType: AcceptType; multiple: boolean }; importFunction: ( docCollection: DocCollection, file: File | File[] - ) => Promise; + ) => Promise; }; const DISCORD_URL = 'https://discord.gg/whd5mjYqVw'; @@ -83,18 +88,20 @@ const importConfigs: Record = { if (!Array.isArray(files)) { throw new Error('Expected an array of files for markdown files import'); } - const pageIds: string[] = []; + const docIds: 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({ + const docId = await MarkdownTransformer.importMarkdownToDoc({ collection: docCollection, markdown: text, fileName, }); - if (pageId) pageIds.push(pageId); + if (docId) docIds.push(docId); } - return pageIds; + return { + docIds, + }; }, }, markdownZip: { @@ -103,10 +110,13 @@ const importConfigs: Record = { if (Array.isArray(file)) { throw new Error('Expected a single zip file for markdownZip import'); } - return MarkdownTransformer.importMarkdownZip({ + const docIds = await MarkdownTransformer.importMarkdownZip({ collection: docCollection, imported: file, }); + return { + docIds, + }; }, }, notion: { @@ -115,11 +125,16 @@ const importConfigs: Record = { 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 { entryId, pageIds, isWorkspaceFile } = + await NotionHtmlTransformer.importNotionZip({ + collection: docCollection, + imported: file, + }); + return { + docIds: pageIds, + entryId, + isWorkspaceFile, + }; }, }, }; @@ -274,13 +289,12 @@ const ErrorStatus = ({ export const ImportDialog = ({ close, -}: DialogComponentProps) => { +}: DialogComponentProps) => { const t = useI18n(); const [status, setStatus] = useState('idle'); const [importError, setImportError] = useState(null); - const [pageIds, setPageIds] = useState([]); + const [importResult, setImportResult] = useState(null); const workspace = useService(WorkspaceService).workspace; - const workbench = useService(WorkbenchService).workbench; const docCollection = workspace.docCollection; const handleImport = useAsyncCallback( @@ -297,29 +311,41 @@ export const ImportDialog = ({ } 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'); + track.$.importModal.$.import({ + type, + status: 'success', + result: { + docCount: docIds.length, + }, + }); } catch (error) { - setImportError( - error instanceof Error ? error.message : 'Unknown error occurred' - ); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred'; + setImportError(errorMessage); setStatus('error'); + track.$.importModal.$.import({ + type, + status: 'failed', + error: errorMessage || undefined, + }); } }, [docCollection, t] ); const handleComplete = useCallback(() => { - if (pageIds.length > 1) { - workbench.openAll(); - } else if (pageIds.length === 1) { - workbench.openDoc(pageIds[0]); - } - close(); - }, [pageIds, close, workbench]); + close(importResult || undefined); + }, [importResult, close]); const handleRetry = () => { setStatus('idle'); @@ -335,8 +361,10 @@ export const ImportDialog = ({ return ( { - close(); + onOpenChange={(open: boolean) => { + if (!open) { + close(importResult || undefined); + } }} width={480} contentOptions={{ @@ -357,7 +385,7 @@ export const ImportDialog = ({ withoutCloseButton={status === 'importing'} persistent={status === 'importing'} > -
+
{statusComponents[status]}
diff --git a/packages/frontend/core/src/desktop/dialogs/index.tsx b/packages/frontend/core/src/desktop/dialogs/index.tsx index 453cc271bb95d..c8b827a61efad 100644 --- a/packages/frontend/core/src/desktop/dialogs/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/index.tsx @@ -24,7 +24,6 @@ const GLOBAL_DIALOGS = { 'import-workspace': ImportWorkspaceDialog, 'import-template': ImportTemplateDialog, setting: SettingDialog, - import: ImportDialog, } satisfies { [key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC< DialogComponentProps @@ -37,6 +36,7 @@ const WORKSPACE_DIALOGS = { 'tag-selector': TagSelectorDialog, 'doc-selector': DocSelectorDialog, 'collection-selector': CollectionSelectorDialog, + import: ImportDialog, } satisfies { [key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC< DialogComponentProps diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 47d436b83a8a8..9b1b04c9506ba 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -26,7 +26,6 @@ export type GLOBAL_DIALOG_SCHEMA = { templateMode: DocMode; snapshotUrl: string; }) => void; - import: () => void; setting: (props: { activeTab?: SettingTab; workspaceMetadata?: WorkspaceMetadata | null; @@ -52,4 +51,9 @@ export type WORKSPACE_DIALOG_SCHEMA = { init: string[]; onBeforeConfirm?: (ids: string[], cb: () => void) => void; }) => string[]; + import: () => { + docIds: string[]; + entryId?: string; + isWorkspaceFile?: boolean; + }; }; diff --git a/packages/frontend/track/src/events.ts b/packages/frontend/track/src/events.ts index a08a6761458e8..3ca457678818f 100644 --- a/packages/frontend/track/src/events.ts +++ b/packages/frontend/track/src/events.ts @@ -273,7 +273,7 @@ const PageEvents = { doc: { editor: { slashMenu: ['linkDoc', 'createDoc', 'bookmark'], - atMenu: ['linkDoc'], + atMenu: ['linkDoc', 'import'], quickSearch: ['createDoc'], formatToolbar: ['bold'], pageRef: ['navigate'], diff --git a/tests/affine-local/e2e/import-dialog.spec.ts b/tests/affine-local/e2e/import-dialog.spec.ts new file mode 100644 index 0000000000000..f4b6ff7fb93e0 --- /dev/null +++ b/tests/affine-local/e2e/import-dialog.spec.ts @@ -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(); +});