diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index be3e9e8117..f045e65135 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -162,6 +162,14 @@ export const AFFINE_FLAGS = { configurable: false, defaultState: isMobile, }, + enable_snapshot_import_export: { + category: 'affine', + displayName: 'Enable Snapshot Import Export', + description: + 'Once enabled, users can import and export blocksuite snapshots', + configurable: true, + defaultState: false, + }, } satisfies { [key in string]: FlagInfo }; export type AFFINE_FLAGS = typeof AFFINE_FLAGS; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx index 9818e2aa19..2ffcee2965 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx @@ -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 ( <> { {t['com.affine.aboutAFFiNE.contact.community']()} + + +
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/style.css.ts index 2f705ef26b..8dcea50480 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/style.css.ts +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/style.css.ts @@ -71,3 +71,6 @@ globalStyle(`${appImageRow} .right-col`, { paddingLeft: '0', paddingRight: '20px', }); +export const snapshotImportExportRow = style({ + marginTop: '12px', +}); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 27bd190b4f..d1b04b6f56 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -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']()} + {enableSnapshotImportExport && } void; + className?: string; +} + +interface SnapshotMenuItemProps { + onSelect: () => void; + className?: string; + type: T; + icon: ReactNode; + label: string; +} + +interface SnapshotProps { + className?: string; +} + +export function SnapshotMenuItem({ + onSelect, + className, + type, + icon, + label, +}: SnapshotMenuItemProps) { + return ( + + {label} + + ); +} + +export const DisableSnapshotMenuItems = ({ + snapshotActionHandler, + className = transitionStyle, +}: SnapshotMenuItemsProps) => { + const t = useI18n(); + return ( + snapshotActionHandler('disable')} + className={className} + type="disable" + icon={} + label={t['Disable Snapshot']()} + /> + ); +}; + +export const SnapshotMenuItems = ({ + snapshotActionHandler, + className = transitionStyle, +}: SnapshotMenuItemsProps) => { + const t = useI18n(); + return ( + <> + snapshotActionHandler('import')} + className={className} + type="import" + icon={} + label={t['Import']()} + /> + snapshotActionHandler('export')} + className={className} + type="export" + icon={} + 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 = ( + <> + + + + + ); + + return ( + , + ['data-testid' as string]: 'snapshot-menu', + }} + subOptions={{}} + > + {t['Snapshot']()} + + ); +}; diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index dd126c0ec5..ed7d79e4a9 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -13,7 +13,7 @@ "ja": 100, "ko": 89, "pl": 0, - "pt-BR": 97, + "pt-BR": 96, "ru": 82, "sv-SE": 5, "ur": 3, diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index be2664f919..5578d0a19d 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -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.", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index fb2c63b260..fe1993518f 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -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 给我们一颗星。 这个简单的行动可以给我们很大的激励,并帮助我们继续为您提供最佳体验。", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index 3643060965..fa6b17e2c4 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -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 給我們一顆星。這個簡單的行動可以給我們很大的激勵,並幫助我們繼續為您提供最佳體驗。",