mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +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>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@
|
||||
"ja": 100,
|
||||
"ko": 89,
|
||||
"pl": 0,
|
||||
"pt-BR": 97,
|
||||
"pt-BR": 96,
|
||||
"ru": 82,
|
||||
"sv-SE": 5,
|
||||
"ur": 3,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 给我们一颗星。 这个简单的行动可以给我们很大的激励,并帮助我们继续为您提供最佳体验。",
|
||||
|
||||
@@ -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 給我們一顆星。這個簡單的行動可以給我們很大的激勵,並幫助我們繼續為您提供最佳體驗。",
|
||||
|
||||
Reference in New Issue
Block a user