mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
feat: add import modal (#8599)
[BS-1471](https://linear.app/affine-design/issue/BS-1471/新的-import-dialog-ui)
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
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 {
|
||||
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');
|
||||
|
||||
const pageIds = await importConfig.importFunction(docCollection, file);
|
||||
|
||||
setPageIds(pageIds);
|
||||
setStatus('success');
|
||||
} catch (error) {
|
||||
setImportError(
|
||||
error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
);
|
||||
setStatus('error');
|
||||
}
|
||||
},
|
||||
[docCollection, t]
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
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',
|
||||
});
|
||||
@@ -35,6 +35,8 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
export const openImportModalAtom = atom(false);
|
||||
|
||||
export type AuthAtomData =
|
||||
| { state: 'signIn' }
|
||||
| {
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
} from '@affine/component/ui/menu';
|
||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
||||
import { openHistoryTipsModalAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
openHistoryTipsModalAtom,
|
||||
openImportModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
@@ -47,7 +49,6 @@ import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
||||
import { usePageHelper } from '../../block-suite-page-list/utils';
|
||||
import { useFavorite } from '../favorite';
|
||||
|
||||
type PageMenuProps = {
|
||||
@@ -69,7 +70,6 @@ export const PageHeaderMenuButton = ({
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
|
||||
const editorService = useService(EditorService);
|
||||
const isInTrash = useLiveData(
|
||||
@@ -83,7 +83,6 @@ export const PageHeaderMenuButton = ({
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
const { importFile } = usePageHelper(docCollection);
|
||||
const { setTrashModal } = useTrashModalHelper();
|
||||
|
||||
const [isEditing, setEditing] = useState(!page.readonly);
|
||||
@@ -109,6 +108,7 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom);
|
||||
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
@@ -184,19 +184,10 @@ export const PageHeaderMenuButton = ({
|
||||
});
|
||||
}, [duplicate, pageId]);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
const handleOpenImportModal = useCallback(() => {
|
||||
track.$.header.docOptions.import();
|
||||
if (options.isWorkspaceFile) {
|
||||
track.$.header.actions.createWorkspace({
|
||||
control: 'import',
|
||||
});
|
||||
} else {
|
||||
track.$.header.actions.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
setOpenImportModalAtom(true);
|
||||
}, [setOpenImportModalAtom]);
|
||||
|
||||
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
@@ -361,7 +352,7 @@ export const PageHeaderMenuButton = ({
|
||||
<MenuItem
|
||||
prefixIcon={<ImportIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onSelect={onImportFile}
|
||||
onSelect={handleOpenImportModal}
|
||||
>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PeekViewManagerModal } from '../../modules/peek-view';
|
||||
import { AuthModal } from '../affine/auth';
|
||||
import { AiLoginRequiredModal } from '../affine/auth/ai-login-required';
|
||||
import { HistoryTipsModal } from '../affine/history-tips-modal';
|
||||
import { ImportModal } from '../affine/import-modal';
|
||||
import { IssueFeedbackModal } from '../affine/issue-feedback-modal';
|
||||
import {
|
||||
CloudQuotaModal,
|
||||
@@ -30,7 +31,11 @@ import { SettingModal } from '../affine/setting-modal';
|
||||
import { SignOutModal } from '../affine/sign-out-modal';
|
||||
import { StarAFFiNEModal } from '../affine/star-affine-modal';
|
||||
import type { SettingAtom } from '../atoms';
|
||||
import { openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||
import {
|
||||
openImportModalAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../atoms';
|
||||
import { InfoModal } from '../doc-properties/info-modal/info-modal';
|
||||
import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||
@@ -128,6 +133,7 @@ export function CurrentWorkspaceModals() {
|
||||
titles={deletePageTitles}
|
||||
/>
|
||||
{currentWorkspace ? <InfoModal /> : null}
|
||||
<Import />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -193,3 +199,20 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Import = () => {
|
||||
const [open, setOpenImportModalAtom] = useAtom(openImportModalAtom);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpenImportModalAtom(open);
|
||||
},
|
||||
[setOpenImportModalAtom]
|
||||
);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ImportModal open={open} onOpenChange={onOpenChange} />;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
openImportModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AddPageButton,
|
||||
@@ -27,6 +30,7 @@ import type { Doc } from '@blocksuite/affine/store';
|
||||
import {
|
||||
AllDocsIcon,
|
||||
GithubIcon,
|
||||
ImportIcon,
|
||||
JournalIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
@@ -43,7 +47,6 @@ import { useCallback } from 'react';
|
||||
import { WorkbenchService } from '../../modules/workbench';
|
||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
import { WorkspaceNavigator } from '../workspace-selector';
|
||||
import ImportPage from './import-page';
|
||||
import {
|
||||
quickSearch,
|
||||
quickSearchAndNewPage,
|
||||
@@ -82,7 +85,6 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
CMDKQuickSearchService,
|
||||
});
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const t = useI18n();
|
||||
const workbench = workbenchService.workbench;
|
||||
const currentPath = useLiveData(
|
||||
@@ -105,6 +107,7 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
);
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||
|
||||
const onOpenSettingModal = useCallback(() => {
|
||||
setOpenSettingModalAtom({
|
||||
@@ -114,6 +117,10 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
track.$.navigationPanel.$.openSettings();
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
const onOpenImportModal = useCallback(() => {
|
||||
setOpenImportModalAtom(true);
|
||||
}, [setOpenImportModalAtom]);
|
||||
|
||||
return (
|
||||
<AppSidebar>
|
||||
<SidebarContainer>
|
||||
@@ -163,7 +170,13 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||
<div style={{ padding: '0 8px' }}>
|
||||
<TrashButton />
|
||||
<ImportPage docCollection={docCollection} />
|
||||
<MenuItem
|
||||
data-testid="slider-bar-import-button"
|
||||
icon={<ImportIcon />}
|
||||
onClick={onOpenImportModal}
|
||||
>
|
||||
<span data-testid="import-modal-trigger">{t['Import']()}</span>
|
||||
</MenuItem>
|
||||
<ExternalMenuLinkItem
|
||||
href="https://affine.pro/blog?tag=Release+Note"
|
||||
icon={<JournalIcon />}
|
||||
|
||||
Reference in New Issue
Block a user