Skip to content

Commit

Permalink
feat: add snapshot import export config (#8584)
Browse files Browse the repository at this point in the history
  • Loading branch information
donteatfriedrice committed Oct 31, 2024
1 parent 0f9d11f commit ba3aa7f
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 7 deletions.
8 changes: 8 additions & 0 deletions packages/common/infra/src/modules/feature-flag/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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];
Expand Down Expand Up @@ -58,6 +68,13 @@ export const AboutAffine = () => {
[updateSettings]
);

const onSwitchSnapshotImportExport = useCallback(
(checked: boolean) => {
featureFlagService.flags.enable_snapshot_import_export.set(checked);
},
[featureFlagService]
);

return (
<>
<SettingHeader
Expand Down Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ globalStyle(`${appImageRow} .right-col`, {
paddingLeft: '0',
paddingRight: '20px',
});
export const snapshotImportExportRow = style({
marginTop: '12px',
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './disable-public-sharing';
export * from './export';
// export * from './MoveTo';
export * from './move-to-trash';
export * from './snapshot';
Original file line number Diff line number Diff line change
@@ -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>
);
};
2 changes: 1 addition & 1 deletion packages/frontend/i18n/src/i18n-completenesses.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"ja": 100,
"ko": 89,
"pl": 0,
"pt-BR": 97,
"pt-BR": 96,
"ru": 82,
"sv-SE": 5,
"ur": 3,
Expand Down
Loading

0 comments on commit ba3aa7f

Please sign in to comment.