mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat: add snapshot import export config (#8584)
[BS-1470](https://linear.app/affine-design/issue/BS-1470/提供-snapshot-导入导出开关)
This commit is contained in:
@@ -13,7 +13,11 @@ import { UrlService } from '@affine/core/modules/url';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
@@ -23,9 +27,15 @@ import { UpdateCheckSection } from './update-check-section';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
const t = useI18n();
|
||||
const urlService = useService(UrlService);
|
||||
const { urlService, featureFlagService } = useServices({
|
||||
UrlService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater();
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
@@ -58,6 +68,13 @@ export const AboutAffine = () => {
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchSnapshotImportExport = useCallback(
|
||||
(checked: boolean) => {
|
||||
featureFlagService.flags.enable_snapshot_import_export.set(checked);
|
||||
},
|
||||
[featureFlagService]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -141,6 +158,16 @@ export const AboutAffine = () => {
|
||||
{t['com.affine.aboutAFFiNE.contact.community']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<SettingRow
|
||||
name={t['com.affine.snapshot.import-export.enable']()}
|
||||
desc={t['com.affine.snapshot.import-export.enable.desc']()}
|
||||
className={styles.snapshotImportExportRow}
|
||||
>
|
||||
<Switch
|
||||
checked={enableSnapshotImportExport}
|
||||
onChange={onSwitchSnapshotImportExport}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
|
||||
<div className={styles.communityWrapper}>
|
||||
|
||||
@@ -71,3 +71,6 @@ globalStyle(`${appImageRow} .right-col`, {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '20px',
|
||||
});
|
||||
export const snapshotImportExportRow = style({
|
||||
marginTop: '12px',
|
||||
});
|
||||
|
||||
@@ -16,7 +16,11 @@ import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import {
|
||||
Export,
|
||||
MoveToTrash,
|
||||
Snapshot,
|
||||
} 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 { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
@@ -44,7 +48,12 @@ import {
|
||||
SplitViewIcon,
|
||||
TocIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -79,6 +88,10 @@ export const PageHeaderMenuButton = ({
|
||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
@@ -357,6 +370,7 @@ export const PageHeaderMenuButton = ({
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
<Export exportHandler={exportHandler} pageMode={currentMode} />
|
||||
{enableSnapshotImportExport && <Snapshot />}
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { toast } from '@affine/component';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { type DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import { type DocProps, DocsService, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
HtmlTransformer,
|
||||
MarkdownTransformer,
|
||||
printToPdf,
|
||||
ZipTransformer,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
@@ -20,7 +21,7 @@ import { nanoid } from 'nanoid';
|
||||
|
||||
import { useAsyncCallback } from '../affine-async-hooks';
|
||||
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown';
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot';
|
||||
|
||||
interface ExportHandlerOptions {
|
||||
page: Doc;
|
||||
@@ -44,6 +45,9 @@ async function exportHandler({
|
||||
case 'markdown':
|
||||
await MarkdownTransformer.exportDoc(page);
|
||||
return;
|
||||
case 'snapshot':
|
||||
await ZipTransformer.exportDocs(page.collection, [page]);
|
||||
return;
|
||||
case 'pdf':
|
||||
await printToPdf(editorContainer);
|
||||
return;
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './disable-public-sharing';
|
||||
export * from './export';
|
||||
// export * from './MoveTo';
|
||||
export * from './move-to-trash';
|
||||
export * from './snapshot';
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { MenuItem, MenuSeparator, MenuSub, notify } from '@affine/component';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { openFileOrFiles, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { ExportIcon, ImportIcon, ToneIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
|
||||
import { useExportPage } from '../../hooks/affine/use-export-page';
|
||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
||||
import { transitionStyle } from './index.css';
|
||||
|
||||
interface SnapshotMenuItemsProps {
|
||||
snapshotActionHandler: (action: 'import' | 'export' | 'disable') => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface SnapshotMenuItemProps<T> {
|
||||
onSelect: () => void;
|
||||
className?: string;
|
||||
type: T;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SnapshotProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SnapshotMenuItem<T>({
|
||||
onSelect,
|
||||
className,
|
||||
type,
|
||||
icon,
|
||||
label,
|
||||
}: SnapshotMenuItemProps<T>) {
|
||||
return (
|
||||
<MenuItem
|
||||
className={className}
|
||||
data-testid={`snapshot-${type}`}
|
||||
onSelect={onSelect}
|
||||
block
|
||||
prefixIcon={icon}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const DisableSnapshotMenuItems = ({
|
||||
snapshotActionHandler,
|
||||
className = transitionStyle,
|
||||
}: SnapshotMenuItemsProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('disable')}
|
||||
className={className}
|
||||
type="disable"
|
||||
icon={<ToneIcon />}
|
||||
label={t['Disable Snapshot']()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SnapshotMenuItems = ({
|
||||
snapshotActionHandler,
|
||||
className = transitionStyle,
|
||||
}: SnapshotMenuItemsProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('import')}
|
||||
className={className}
|
||||
type="import"
|
||||
icon={<ImportIcon />}
|
||||
label={t['Import']()}
|
||||
/>
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('export')}
|
||||
className={className}
|
||||
type="export"
|
||||
icon={<ExportIcon />}
|
||||
label={t['Export']()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Snapshot = ({ className }: SnapshotProps) => {
|
||||
const t = useI18n();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const exportHandler = useExportPage();
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
|
||||
const importSnapshot = useCallback(async () => {
|
||||
try {
|
||||
const file = await openFileOrFiles({ acceptType: 'Zip' });
|
||||
if (!file) return null;
|
||||
|
||||
const importedDocs = (
|
||||
await ZipTransformer.importDocs(docCollection, file)
|
||||
).filter(doc => doc !== undefined);
|
||||
if (importedDocs.length === 0) {
|
||||
notify.error({
|
||||
title: 'Import Snapshot Failed',
|
||||
message: 'No valid documents found in the imported file.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
notify.success({
|
||||
title: 'Imported Snapshot Successfully',
|
||||
message: `Imported ${importedDocs.length} doc(s)`,
|
||||
});
|
||||
return importedDocs;
|
||||
} catch (error) {
|
||||
console.error('Error importing snapshot:', error);
|
||||
notify.error({
|
||||
title: 'Import Snapshot Failed',
|
||||
message: 'Failed to import snapshot. Please try again.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}, [docCollection]);
|
||||
|
||||
const openImportedDocs = useCallback(
|
||||
(importedDocs: Doc[]) => {
|
||||
if (importedDocs.length > 1) {
|
||||
workbench.openAll();
|
||||
} else if (importedDocs[0]?.id) {
|
||||
workbench.openDoc(importedDocs[0].id);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const handleImportSnapshot = useAsyncCallback(async () => {
|
||||
const importedDocs = await importSnapshot();
|
||||
if (importedDocs) {
|
||||
openImportedDocs(importedDocs);
|
||||
track.$.header.docOptions.import();
|
||||
track.$.header.actions.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
}
|
||||
}, [importSnapshot, openImportedDocs]);
|
||||
|
||||
const disableSnapshotActionOption = useCallback(() => {
|
||||
featureFlagService.flags.enable_snapshot_import_export.set(false);
|
||||
}, [featureFlagService]);
|
||||
|
||||
const snapshotActionHandler = useCallback(
|
||||
(action: 'import' | 'export' | 'disable') => {
|
||||
switch (action) {
|
||||
case 'import':
|
||||
return handleImportSnapshot();
|
||||
case 'export':
|
||||
return exportHandler('snapshot');
|
||||
case 'disable':
|
||||
return disableSnapshotActionOption();
|
||||
}
|
||||
},
|
||||
[handleImportSnapshot, exportHandler, disableSnapshotActionOption]
|
||||
);
|
||||
|
||||
const items = (
|
||||
<>
|
||||
<SnapshotMenuItems
|
||||
snapshotActionHandler={snapshotActionHandler}
|
||||
className={className}
|
||||
/>
|
||||
<MenuSeparator />
|
||||
<DisableSnapshotMenuItems
|
||||
snapshotActionHandler={snapshotActionHandler}
|
||||
className={className}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuSub
|
||||
items={items}
|
||||
triggerOptions={{
|
||||
className: transitionStyle,
|
||||
prefixIcon: <ToneIcon />,
|
||||
['data-testid' as string]: 'snapshot-menu',
|
||||
}}
|
||||
subOptions={{}}
|
||||
>
|
||||
{t['Snapshot']()}
|
||||
</MenuSub>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user