feat: add snapshot import export config (#8584)

[BS-1470](https://linear.app/affine-design/issue/BS-1470/提供-snapshot-导入导出开关)
This commit is contained in:
donteatfriedrice
2024-10-31 05:18:14 +00:00
parent 0f9d11fd5c
commit ba3aa7f153
12 changed files with 279 additions and 7 deletions

View File

@@ -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}>

View File

@@ -71,3 +71,6 @@ globalStyle(`${appImageRow} .right-col`, {
paddingLeft: '0',
paddingRight: '20px',
});
export const snapshotImportExportRow = style({
marginTop: '12px',
});

View File

@@ -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"

View File

@@ -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';

View File

@@ -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;

View File

@@ -2,3 +2,4 @@ export * from './disable-public-sharing';
export * from './export';
// export * from './MoveTo';
export * from './move-to-trash';
export * from './snapshot';

View File

@@ -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>
);
};

View File

@@ -13,7 +13,7 @@
"ja": 100,
"ko": 89,
"pl": 0,
"pt-BR": 97,
"pt-BR": 96,
"ru": 82,
"sv-SE": 5,
"ur": 3,

View File

@@ -27,6 +27,7 @@
"Delete": "Delete",
"Disable": "Disable",
"Disable Public Sharing": "Disable public sharing",
"Disable Snapshot": "Disable snapshot",
"Divider": "Divider",
"Edgeless": "Edgeless",
"Edit": "Edit",
@@ -90,6 +91,7 @@
"Sign in": "Sign in AFFiNE Cloud",
"Sign in and Enable": "Sign in and enable",
"Sign out": "Sign out",
"Snapshot": "Snapshot",
"Storage": "Storage",
"Storage and Export": "Storage and export",
"Successfully deleted": "Successfully deleted",
@@ -1218,6 +1220,8 @@
"com.affine.shortcutsTitle.page": "Page",
"com.affine.sidebarSwitch.collapse": "Collapse sidebar",
"com.affine.sidebarSwitch.expand": "Expand sidebar",
"com.affine.snapshot.import-export.enable": "Snapshot Imp. & Exp.",
"com.affine.snapshot.import-export.enable.desc": "Snapshot import and export support. When your document has a data error, turn this on and ask the AFFiNE team for help. Once enabled you can find the Snapshot Export Import option in the document's More menu.",
"com.affine.star-affine.cancel": "Maybe later",
"com.affine.star-affine.confirm": "Star on GitHub",
"com.affine.star-affine.description": "Are you finding our app useful and enjoyable? We'd love your support to keep improving! A great way to help us out is by giving us a star on GitHub. This simple action can make a big difference and helps us continue to deliver the best experience for you.",

View File

@@ -27,6 +27,7 @@
"Delete": "删除",
"Disable": "禁用",
"Disable Public Sharing": "禁用公开分享",
"Disable Snapshot": "禁用快照",
"Divider": "分割线",
"Edgeless": "无界",
"Edit": "编辑",
@@ -90,6 +91,7 @@
"Sign in": "登录 AFFiNE Cloud",
"Sign in and Enable": "登录并启用",
"Sign out": "登出 AFFiNE 云",
"Snapshot": "快照",
"Storage": "储存",
"Storage and Export": "储存与导出",
"Successfully deleted": "成功删除。",
@@ -1195,6 +1197,8 @@
"com.affine.shortcutsTitle.page": "页面",
"com.affine.sidebarSwitch.collapse": "折叠侧边栏",
"com.affine.sidebarSwitch.expand": "展开侧边栏",
"com.affine.snapshot.import-export.enable": "启用快照导入导出",
"com.affine.snapshot.import-export.enable.desc": "支持快照导入和导出。当您的文档出现数据错误时,请启用此功能并寻求 AFFiNE 团队的帮助。启用后,您可以在文档的更多菜单中找到快照导出导入选项。",
"com.affine.star-affine.cancel": "稍后",
"com.affine.star-affine.confirm": "在 GitHub 点亮星标",
"com.affine.star-affine.description": "您觉得我们的应用程序有用且有趣吗? 我们希望得到您的支持,以不断进步! 帮助我们的一个好方法是在 GitHub 给我们一颗星。 这个简单的行动可以给我们很大的激励,并帮助我们继续为您提供最佳体验。",

View File

@@ -27,6 +27,7 @@
"Delete": "刪除",
"Disable": "停用",
"Disable Public Sharing": "停用公開分享",
"Disable Snapshot": "停用快照",
"Divider": "分割線",
"Edgeless": "無界",
"Edit": "編輯",
@@ -90,6 +91,7 @@
"Sign in": "登入 AFFiNE Cloud",
"Sign in and Enable": "登入並啟用",
"Sign out": "登出",
"Snapshot": "快照",
"Storage": "儲存",
"Storage and Export": "存儲和匯出",
"Successfully deleted": "已成功刪除",
@@ -1176,6 +1178,8 @@
"com.affine.shortcutsTitle.page": "頁面",
"com.affine.sidebarSwitch.collapse": "收起側欄",
"com.affine.sidebarSwitch.expand": "展開側欄",
"com.affine.snapshot.import-export.enable": "啟用快照匯入匯出",
"com.affine.snapshot.import-export.enable.desc": "支援快照匯入和匯出。當您的文件出現資料錯誤時,請啟用此功能並尋求 AFFiNE 團隊的協助。啟用後,您可以在文件的更多選單中找到快照匯出匯入選項。",
"com.affine.star-affine.cancel": "稍後",
"com.affine.star-affine.confirm": "在 GitHub 點亮星標",
"com.affine.star-affine.description": "您覺得我們的應用程序有用且有趣嗎?我們希望得到您的支持,以不斷進步!幫助我們的一個好方法是在 GitHub 給我們一顆星。這個簡單的行動可以給我們很大的激勵,並幫助我們繼續為您提供最佳體驗。",