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,
|
open: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const openImportModalAtom = atom(false);
|
||||||
|
|
||||||
export type AuthAtomData =
|
export type AuthAtomData =
|
||||||
| { state: 'signIn' }
|
| { state: 'signIn' }
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||||
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
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 { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
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 { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
@@ -47,7 +49,6 @@ import { useSetAtom } from 'jotai';
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
||||||
import { usePageHelper } from '../../block-suite-page-list/utils';
|
|
||||||
import { useFavorite } from '../favorite';
|
import { useFavorite } from '../favorite';
|
||||||
|
|
||||||
type PageMenuProps = {
|
type PageMenuProps = {
|
||||||
@@ -69,7 +70,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const docCollection = workspace.docCollection;
|
|
||||||
|
|
||||||
const editorService = useService(EditorService);
|
const editorService = useService(EditorService);
|
||||||
const isInTrash = useLiveData(
|
const isInTrash = useLiveData(
|
||||||
@@ -83,7 +83,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||||
|
|
||||||
const { duplicate } = useBlockSuiteMetaHelper();
|
const { duplicate } = useBlockSuiteMetaHelper();
|
||||||
const { importFile } = usePageHelper(docCollection);
|
|
||||||
const { setTrashModal } = useTrashModalHelper();
|
const { setTrashModal } = useTrashModalHelper();
|
||||||
|
|
||||||
const [isEditing, setEditing] = useState(!page.readonly);
|
const [isEditing, setEditing] = useState(!page.readonly);
|
||||||
@@ -109,6 +108,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
|
|
||||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||||
const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom);
|
const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom);
|
||||||
|
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||||
|
|
||||||
const openHistoryModal = useCallback(() => {
|
const openHistoryModal = useCallback(() => {
|
||||||
track.$.header.history.open();
|
track.$.header.history.open();
|
||||||
@@ -184,19 +184,10 @@ export const PageHeaderMenuButton = ({
|
|||||||
});
|
});
|
||||||
}, [duplicate, pageId]);
|
}, [duplicate, pageId]);
|
||||||
|
|
||||||
const onImportFile = useAsyncCallback(async () => {
|
const handleOpenImportModal = useCallback(() => {
|
||||||
const options = await importFile();
|
|
||||||
track.$.header.docOptions.import();
|
track.$.header.docOptions.import();
|
||||||
if (options.isWorkspaceFile) {
|
setOpenImportModalAtom(true);
|
||||||
track.$.header.actions.createWorkspace({
|
}, [setOpenImportModalAtom]);
|
||||||
control: 'import',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
track.$.header.actions.createDoc({
|
|
||||||
control: 'import',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [importFile]);
|
|
||||||
|
|
||||||
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -361,7 +352,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<ImportIcon />}
|
prefixIcon={<ImportIcon />}
|
||||||
data-testid="editor-option-menu-import"
|
data-testid="editor-option-menu-import"
|
||||||
onSelect={onImportFile}
|
onSelect={handleOpenImportModal}
|
||||||
>
|
>
|
||||||
{t['Import']()}
|
{t['Import']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { PeekViewManagerModal } from '../../modules/peek-view';
|
|||||||
import { AuthModal } from '../affine/auth';
|
import { AuthModal } from '../affine/auth';
|
||||||
import { AiLoginRequiredModal } from '../affine/auth/ai-login-required';
|
import { AiLoginRequiredModal } from '../affine/auth/ai-login-required';
|
||||||
import { HistoryTipsModal } from '../affine/history-tips-modal';
|
import { HistoryTipsModal } from '../affine/history-tips-modal';
|
||||||
|
import { ImportModal } from '../affine/import-modal';
|
||||||
import { IssueFeedbackModal } from '../affine/issue-feedback-modal';
|
import { IssueFeedbackModal } from '../affine/issue-feedback-modal';
|
||||||
import {
|
import {
|
||||||
CloudQuotaModal,
|
CloudQuotaModal,
|
||||||
@@ -30,7 +31,11 @@ import { SettingModal } from '../affine/setting-modal';
|
|||||||
import { SignOutModal } from '../affine/sign-out-modal';
|
import { SignOutModal } from '../affine/sign-out-modal';
|
||||||
import { StarAFFiNEModal } from '../affine/star-affine-modal';
|
import { StarAFFiNEModal } from '../affine/star-affine-modal';
|
||||||
import type { SettingAtom } from '../atoms';
|
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 { InfoModal } from '../doc-properties/info-modal/info-modal';
|
||||||
import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
||||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||||
@@ -128,6 +133,7 @@ export function CurrentWorkspaceModals() {
|
|||||||
titles={deletePageTitles}
|
titles={deletePageTitles}
|
||||||
/>
|
/>
|
||||||
{currentWorkspace ? <InfoModal /> : null}
|
{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 { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import {
|
import {
|
||||||
AddPageButton,
|
AddPageButton,
|
||||||
@@ -27,6 +30,7 @@ import type { Doc } from '@blocksuite/affine/store';
|
|||||||
import {
|
import {
|
||||||
AllDocsIcon,
|
AllDocsIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
|
ImportIcon,
|
||||||
JournalIcon,
|
JournalIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
@@ -43,7 +47,6 @@ import { useCallback } from 'react';
|
|||||||
import { WorkbenchService } from '../../modules/workbench';
|
import { WorkbenchService } from '../../modules/workbench';
|
||||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||||
import { WorkspaceNavigator } from '../workspace-selector';
|
import { WorkspaceNavigator } from '../workspace-selector';
|
||||||
import ImportPage from './import-page';
|
|
||||||
import {
|
import {
|
||||||
quickSearch,
|
quickSearch,
|
||||||
quickSearchAndNewPage,
|
quickSearchAndNewPage,
|
||||||
@@ -82,7 +85,6 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
CMDKQuickSearchService,
|
CMDKQuickSearchService,
|
||||||
});
|
});
|
||||||
const currentWorkspace = workspaceService.workspace;
|
const currentWorkspace = workspaceService.workspace;
|
||||||
const docCollection = currentWorkspace.docCollection;
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
const currentPath = useLiveData(
|
const currentPath = useLiveData(
|
||||||
@@ -105,6 +107,7 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
const setOpenImportModalAtom = useSetAtom(openImportModalAtom);
|
||||||
|
|
||||||
const onOpenSettingModal = useCallback(() => {
|
const onOpenSettingModal = useCallback(() => {
|
||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
@@ -114,6 +117,10 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
track.$.navigationPanel.$.openSettings();
|
track.$.navigationPanel.$.openSettings();
|
||||||
}, [setOpenSettingModalAtom]);
|
}, [setOpenSettingModalAtom]);
|
||||||
|
|
||||||
|
const onOpenImportModal = useCallback(() => {
|
||||||
|
setOpenImportModalAtom(true);
|
||||||
|
}, [setOpenImportModalAtom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar>
|
<AppSidebar>
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
@@ -163,7 +170,13 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||||
<div style={{ padding: '0 8px' }}>
|
<div style={{ padding: '0 8px' }}>
|
||||||
<TrashButton />
|
<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
|
<ExternalMenuLinkItem
|
||||||
href="https://affine.pro/blog?tag=Release+Note"
|
href="https://affine.pro/blog?tag=Release+Note"
|
||||||
icon={<JournalIcon />}
|
icon={<JournalIcon />}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Click to replace photo": "Click to replace photo",
|
"Click to replace photo": "Click to replace photo",
|
||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
|
"Complete": "Complete",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
"Continue": "Continue",
|
"Continue": "Continue",
|
||||||
"Convert to ": "Convert to ",
|
"Convert to ": "Convert to ",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"Favorite": "Favourite",
|
"Favorite": "Favourite",
|
||||||
"Favorited": "Favourited",
|
"Favorited": "Favourited",
|
||||||
"Favorites": "Favourites",
|
"Favorites": "Favourites",
|
||||||
|
"Feedback": "Feedback",
|
||||||
"Find 0 result": "Found 0 results",
|
"Find 0 result": "Found 0 results",
|
||||||
"Go Back": "Go back",
|
"Go Back": "Go back",
|
||||||
"Go Forward": "Go forward",
|
"Go Forward": "Go forward",
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
"Remove special filter": "Remove special filter",
|
"Remove special filter": "Remove special filter",
|
||||||
"Removed successfully": "Removed successfully",
|
"Removed successfully": "Removed successfully",
|
||||||
"Rename": "Rename",
|
"Rename": "Rename",
|
||||||
|
"Retry": "Retry",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Select": "Select",
|
"Select": "Select",
|
||||||
"Sign in": "Sign in AFFiNE Cloud",
|
"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.errorImport": "Failed to import template, please try again.",
|
||||||
"com.affine.import-template.dialog.errorLoad": "Failed to load 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_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.inviteModal.button.cancel": "Cancel",
|
||||||
"com.affine.issue-feedback.cancel": "Maybe later",
|
"com.affine.issue-feedback.cancel": "Maybe later",
|
||||||
"com.affine.issue-feedback.confirm": "Create issue on GitHub",
|
"com.affine.issue-feedback.confirm": "Create issue on GitHub",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"Cancel": "取消",
|
"Cancel": "取消",
|
||||||
"Click to replace photo": "点击以更换照片",
|
"Click to replace photo": "点击以更换照片",
|
||||||
"Collections": "精选",
|
"Collections": "精选",
|
||||||
|
"Complete": "完成",
|
||||||
"Confirm": "确认",
|
"Confirm": "确认",
|
||||||
"Continue": "继续",
|
"Continue": "继续",
|
||||||
"Convert to ": "转换为",
|
"Convert to ": "转换为",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"Favorite": "收藏",
|
"Favorite": "收藏",
|
||||||
"Favorited": "已收藏",
|
"Favorited": "已收藏",
|
||||||
"Favorites": "收藏夹",
|
"Favorites": "收藏夹",
|
||||||
|
"Feedback": "反馈",
|
||||||
"Find 0 result": "找到 0 个结果",
|
"Find 0 result": "找到 0 个结果",
|
||||||
"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": "重试",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
"Select": "选择",
|
"Select": "选择",
|
||||||
"Sign in": "登录 AFFiNE Cloud",
|
"Sign in": "登录 AFFiNE Cloud",
|
||||||
@@ -507,6 +510,19 @@
|
|||||||
"com.affine.import-template.dialog.errorImport": "插入模板失败,请重试。",
|
"com.affine.import-template.dialog.errorImport": "插入模板失败,请重试。",
|
||||||
"com.affine.import-template.dialog.errorLoad": "读取模版失败,请重试。",
|
"com.affine.import-template.dialog.errorLoad": "读取模版失败,请重试。",
|
||||||
"com.affine.import_file": "支持 Markdown/Notion",
|
"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.inviteModal.button.cancel": "取消",
|
||||||
"com.affine.issue-feedback.cancel": "稍后",
|
"com.affine.issue-feedback.cancel": "稍后",
|
||||||
"com.affine.issue-feedback.confirm": "在 GitHUb 分享反馈",
|
"com.affine.issue-feedback.confirm": "在 GitHUb 分享反馈",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"Cancel": "取消",
|
"Cancel": "取消",
|
||||||
"Click to replace photo": "点击以更换照片",
|
"Click to replace photo": "点击以更换照片",
|
||||||
"Collections": "收藏集",
|
"Collections": "收藏集",
|
||||||
|
"Complete": "完成",
|
||||||
"Confirm": "確認",
|
"Confirm": "確認",
|
||||||
"Continue": "繼續",
|
"Continue": "繼續",
|
||||||
"Convert to ": "轉換為",
|
"Convert to ": "轉換為",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"Favorite": "收藏",
|
"Favorite": "收藏",
|
||||||
"Favorited": "已收藏",
|
"Favorited": "已收藏",
|
||||||
"Favorites": "我的收藏",
|
"Favorites": "我的收藏",
|
||||||
|
"Feedback": "反饋",
|
||||||
"Find 0 result": "發現 0 個結果",
|
"Find 0 result": "發現 0 個結果",
|
||||||
"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": "重試",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
"Select": "選擇",
|
"Select": "選擇",
|
||||||
"Sign in": "登入 AFFiNE Cloud",
|
"Sign in": "登入 AFFiNE Cloud",
|
||||||
@@ -504,6 +507,19 @@
|
|||||||
"com.affine.import-template.dialog.errorImport": "導入模板失敗,請重試。",
|
"com.affine.import-template.dialog.errorImport": "導入模板失敗,請重試。",
|
||||||
"com.affine.import-template.dialog.errorLoad": "加載模板失敗,請重試。",
|
"com.affine.import-template.dialog.errorLoad": "加載模板失敗,請重試。",
|
||||||
"com.affine.import_file": "支援 Markdown/Notion",
|
"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.inviteModal.button.cancel": "取消",
|
||||||
"com.affine.issue-feedback.cancel": "稍後",
|
"com.affine.issue-feedback.cancel": "稍後",
|
||||||
"com.affine.issue-feedback.confirm": "在 GitHub 分享反饋",
|
"com.affine.issue-feedback.confirm": "在 GitHub 分享反饋",
|
||||||
|
|||||||
Reference in New Issue
Block a user