mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08: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 />}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Cancel": "Cancel",
|
||||
"Click to replace photo": "Click to replace photo",
|
||||
"Collections": "Collections",
|
||||
"Complete": "Complete",
|
||||
"Confirm": "Confirm",
|
||||
"Continue": "Continue",
|
||||
"Convert to ": "Convert to ",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Favorite": "Favourite",
|
||||
"Favorited": "Favourited",
|
||||
"Favorites": "Favourites",
|
||||
"Feedback": "Feedback",
|
||||
"Find 0 result": "Found 0 results",
|
||||
"Go Back": "Go back",
|
||||
"Go Forward": "Go forward",
|
||||
@@ -82,6 +84,7 @@
|
||||
"Remove special filter": "Remove special filter",
|
||||
"Removed successfully": "Removed successfully",
|
||||
"Rename": "Rename",
|
||||
"Retry": "Retry",
|
||||
"Save": "Save",
|
||||
"Select": "Select",
|
||||
"Sign in": "Sign in AFFiNE Cloud",
|
||||
@@ -508,6 +511,19 @@
|
||||
"com.affine.import-template.dialog.errorImport": "Failed to import template, please try again.",
|
||||
"com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.",
|
||||
"com.affine.import_file": "Support Markdown/Notion",
|
||||
"com.affine.import.affine-workspace-data": "AFFiNE workspace data",
|
||||
"com.affine.import.markdown-files": "Markdown files (.md)",
|
||||
"com.affine.import.markdown-with-media-files": "Markdown with media files (.zip)",
|
||||
"com.affine.import.modal.tip": "If you'd like to request support for additional file types, feel free to let us know on",
|
||||
"com.affine.import.notion": "Notion",
|
||||
"com.affine.import.notion.tooltip": "Import your Notion data. Supported import formats: HTML with subpages.",
|
||||
"com.affine.import.status.failed.message": "Import failed, please try again.",
|
||||
"com.affine.import.status.failed.message.no-file-selected": "No file selected",
|
||||
"com.affine.import.status.failed.title": "Import failure",
|
||||
"com.affine.import.status.importing.message": "Importing your workspace data, please wait patiently.",
|
||||
"com.affine.import.status.importing.title": "Importing...",
|
||||
"com.affine.import.status.success.message": "Your document has been imported successfully, thank you for choosing AFFiNE. Any questions please feel free to feedback to us",
|
||||
"com.affine.import.status.success.title": "Import completed",
|
||||
"com.affine.inviteModal.button.cancel": "Cancel",
|
||||
"com.affine.issue-feedback.cancel": "Maybe later",
|
||||
"com.affine.issue-feedback.confirm": "Create issue on GitHub",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Cancel": "取消",
|
||||
"Click to replace photo": "点击以更换照片",
|
||||
"Collections": "精选",
|
||||
"Complete": "完成",
|
||||
"Confirm": "确认",
|
||||
"Continue": "继续",
|
||||
"Convert to ": "转换为",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Favorite": "收藏",
|
||||
"Favorited": "已收藏",
|
||||
"Favorites": "收藏夹",
|
||||
"Feedback": "反馈",
|
||||
"Find 0 result": "找到 0 个结果",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前进",
|
||||
@@ -82,6 +84,7 @@
|
||||
"Remove special filter": "移除特殊筛选",
|
||||
"Removed successfully": "成功移除",
|
||||
"Rename": "重命名",
|
||||
"Retry": "重试",
|
||||
"Save": "保存",
|
||||
"Select": "选择",
|
||||
"Sign in": "登录 AFFiNE Cloud",
|
||||
@@ -507,6 +510,19 @@
|
||||
"com.affine.import-template.dialog.errorImport": "插入模板失败,请重试。",
|
||||
"com.affine.import-template.dialog.errorLoad": "读取模版失败,请重试。",
|
||||
"com.affine.import_file": "支持 Markdown/Notion",
|
||||
"com.affine.import.affine-workspace-data": "AFFiNE 工作区数据",
|
||||
"com.affine.import.markdown-files": "Markdown 文件 (.md)",
|
||||
"com.affine.import.markdown-with-media-files": "Markdown 文件(带媒体文件) (.zip)",
|
||||
"com.affine.import.modal.tip": "如果您希望请求支持其他文件类型,请随时告诉我们",
|
||||
"com.affine.import.notion": "Notion",
|
||||
"com.affine.import.notion.tooltip": "导入您的 Notion 数据。支持导入格式:HTML 带子页面。",
|
||||
"com.affine.import.status.failed.message": "导入失败,请重试。",
|
||||
"com.affine.import.status.failed.message.no-file-selected": "未选择文件",
|
||||
"com.affine.import.status.failed.title": "导入失败",
|
||||
"com.affine.import.status.importing.message": "正在导入您的数据,请耐心等待。",
|
||||
"com.affine.import.status.importing.title": "导入中...",
|
||||
"com.affine.import.status.success.message": "您的文档已成功导入,感谢选择 AFFiNE。有任何问题请随时反馈给我们",
|
||||
"com.affine.import.status.success.title": "导入完成",
|
||||
"com.affine.inviteModal.button.cancel": "取消",
|
||||
"com.affine.issue-feedback.cancel": "稍后",
|
||||
"com.affine.issue-feedback.confirm": "在 GitHUb 分享反馈",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Cancel": "取消",
|
||||
"Click to replace photo": "点击以更换照片",
|
||||
"Collections": "收藏集",
|
||||
"Complete": "完成",
|
||||
"Confirm": "確認",
|
||||
"Continue": "繼續",
|
||||
"Convert to ": "轉換為",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Favorite": "收藏",
|
||||
"Favorited": "已收藏",
|
||||
"Favorites": "我的收藏",
|
||||
"Feedback": "反饋",
|
||||
"Find 0 result": "發現 0 個結果",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前進",
|
||||
@@ -82,6 +84,7 @@
|
||||
"Remove special filter": "移除特殊篩選器",
|
||||
"Removed successfully": "成功移除",
|
||||
"Rename": "重新命名",
|
||||
"Retry": "重試",
|
||||
"Save": "保存",
|
||||
"Select": "選擇",
|
||||
"Sign in": "登入 AFFiNE Cloud",
|
||||
@@ -504,6 +507,19 @@
|
||||
"com.affine.import-template.dialog.errorImport": "導入模板失敗,請重試。",
|
||||
"com.affine.import-template.dialog.errorLoad": "加載模板失敗,請重試。",
|
||||
"com.affine.import_file": "支援 Markdown/Notion",
|
||||
"com.affine.import.affine-workspace-data": "AFFiNE 工作區數據",
|
||||
"com.affine.import.markdown-files": "Markdown 文件 (.md)",
|
||||
"com.affine.import.markdown-with-media-files": "Markdown 文件(帶媒體文件) (.zip)",
|
||||
"com.affine.import.modal.tip": "如果您希望請求支持其他文件類型,請隨時告訴我們",
|
||||
"com.affine.import.notion": "Notion",
|
||||
"com.affine.import.notion.tooltip": "導入您的 Notion 數據。支持導入格式:HTML 帶子頁面。",
|
||||
"com.affine.import.status.failed.message": "導入失敗,請重試。",
|
||||
"com.affine.import.status.failed.message.no-file-selected": "未選擇文件",
|
||||
"com.affine.import.status.failed.title": "導入失敗",
|
||||
"com.affine.import.status.importing.message": "正在導入您的數據,請耐心等待。",
|
||||
"com.affine.import.status.importing.title": "導入中...",
|
||||
"com.affine.import.status.success.message": "您的文檔已成功導入,感謝選擇 AFFiNE。有任何問題請隨時反饋給我們",
|
||||
"com.affine.import.status.success.title": "導入完成",
|
||||
"com.affine.inviteModal.button.cancel": "取消",
|
||||
"com.affine.issue-feedback.cancel": "稍後",
|
||||
"com.affine.issue-feedback.confirm": "在 GitHub 分享反饋",
|
||||
|
||||
Reference in New Issue
Block a user