mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
feat(core): add clipper import interface (#10619)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useWorkspaceName } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceSelector } from '@affine/core/components/workspace-selector';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type ClipperInput,
|
||||
ImportClipperService,
|
||||
} from '@affine/core/modules/import-clipper';
|
||||
import {
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AllDocsIcon } from '@blocksuite/icons/rc';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
const clipperInput$ = new LiveData<ClipperInput | null>(null);
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (
|
||||
typeof event.data === 'object' &&
|
||||
event.data.type === 'affine-clipper:import'
|
||||
) {
|
||||
clipperInput$.value = event.data.payload;
|
||||
}
|
||||
});
|
||||
|
||||
export const Component = () => {
|
||||
const importClipperService = useService(ImportClipperService);
|
||||
const t = useI18n();
|
||||
const session = useService(AuthService).session;
|
||||
const notLogin = useLiveData(session.status$) === 'unauthenticated';
|
||||
const isSessionRevalidating = useLiveData(session.isRevalidating$);
|
||||
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importingError, setImportingError] = useState<any>(null);
|
||||
const clipperInput = useLiveData(clipperInput$);
|
||||
const [clipperInputSnapshot, setClipperInputSnapshot] =
|
||||
useState<ClipperInput | null>(null);
|
||||
const isMissingInput = !clipperInputSnapshot;
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const [rawSelectedWorkspace, setSelectedWorkspace] =
|
||||
useState<WorkspaceMetadata | null>(null);
|
||||
const selectedWorkspace =
|
||||
rawSelectedWorkspace ??
|
||||
workspaces.find(w => w.flavour !== 'local') ??
|
||||
workspaces.at(0);
|
||||
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
|
||||
const noWorkspace = workspaces.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
workspacesService.list.revalidate();
|
||||
}, [workspacesService]);
|
||||
|
||||
useEffect(() => {
|
||||
session.revalidate();
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSessionRevalidating && notLogin) {
|
||||
jumpToSignIn('/clipper/import');
|
||||
}
|
||||
}, [isSessionRevalidating, jumpToSignIn, notLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clipperInputSnapshot) {
|
||||
setClipperInputSnapshot(clipperInput);
|
||||
}
|
||||
}, [clipperInput, clipperInputSnapshot]);
|
||||
|
||||
const handleSelectedWorkspace = useCallback(
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
return setSelectedWorkspace(workspaceMetadata);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreatedWorkspace = useCallback(
|
||||
(payload: { metadata: WorkspaceMetadata; defaultDocId?: string }) => {
|
||||
return setSelectedWorkspace(payload.metadata);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleImportToSelectedWorkspace = useAsyncCallback(async () => {
|
||||
if (clipperInputSnapshot && selectedWorkspace) {
|
||||
setImporting(true);
|
||||
try {
|
||||
await importClipperService.importToWorkspace(
|
||||
selectedWorkspace,
|
||||
clipperInputSnapshot
|
||||
);
|
||||
window.close();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}
|
||||
}, [clipperInputSnapshot, importClipperService, selectedWorkspace]);
|
||||
|
||||
const handleImportToNewWorkspace = useAsyncCallback(async () => {
|
||||
if (!clipperInputSnapshot) {
|
||||
return;
|
||||
}
|
||||
setImporting(true);
|
||||
try {
|
||||
await importClipperService.importToNewWorkspace(
|
||||
'affine-cloud',
|
||||
'Workspace',
|
||||
clipperInputSnapshot
|
||||
);
|
||||
window.close();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}, [clipperInputSnapshot, importClipperService]);
|
||||
|
||||
const disabled = isMissingInput || importing || notLogin;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AllDocsIcon className={styles.mainIcon} />
|
||||
<h6 className={styles.mainTitle}>
|
||||
{t['com.affine.import-clipper.dialog.createDocFromClipper']()}
|
||||
</h6>
|
||||
{noWorkspace ? (
|
||||
<p className={styles.desc}>A new workspace will be created.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className={styles.desc}>Choose a workspace.</p>
|
||||
<WorkspaceSelector
|
||||
workspaceMetadata={selectedWorkspace}
|
||||
onSelectWorkspace={handleSelectedWorkspace}
|
||||
onCreatedWorkspace={handleCreatedWorkspace}
|
||||
className={styles.workspaceSelector}
|
||||
showArrowDownIcon
|
||||
disable={disabled}
|
||||
menuContentOptions={{
|
||||
side: 'top',
|
||||
style: {
|
||||
maxHeight: 'min(600px, calc(50vh + 50px))',
|
||||
width: 352,
|
||||
maxWidth: 'calc(100vw - 20px)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.buttonContainer}>
|
||||
{importingError && (
|
||||
<span style={{ color: cssVar('warningColor') }}>
|
||||
{t['com.affine.import-clipper.dialog.errorImport']()}
|
||||
</span>
|
||||
)}
|
||||
{isMissingInput ? (
|
||||
<span style={{ color: cssVar('warningColor') }}>
|
||||
{t['com.affine.import-clipper.dialog.errorLoad']()}
|
||||
</span>
|
||||
) : selectedWorkspace ? (
|
||||
<Button
|
||||
className={styles.mainButton}
|
||||
variant={disabled ? 'secondary' : 'primary'}
|
||||
loading={disabled}
|
||||
disabled={disabled}
|
||||
onClick={handleImportToSelectedWorkspace}
|
||||
data-testid="import-clipper-to-workspace-btn"
|
||||
>
|
||||
{selectedWorkspaceName &&
|
||||
t['com.affine.import-clipper.dialog.createDocToWorkspace']({
|
||||
workspace: selectedWorkspaceName,
|
||||
})}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={styles.mainButton}
|
||||
variant="primary"
|
||||
loading={disabled}
|
||||
disabled={disabled}
|
||||
onClick={handleImportToNewWorkspace}
|
||||
>
|
||||
{t['com.affine.import-clipper.dialog.createDocToNewWorkspace']()}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
maxWidth: '400px',
|
||||
margin: '0 auto',
|
||||
color: cssVarV2('text/primary'),
|
||||
padding: '16px',
|
||||
});
|
||||
|
||||
export const buttonContainer = style({
|
||||
paddingTop: '16px',
|
||||
});
|
||||
|
||||
export const mainIcon = style({
|
||||
width: 36,
|
||||
height: 36,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
export const mainTitle = style({
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
marginTop: '16px',
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const desc = style({
|
||||
textAlign: 'center',
|
||||
color: cssVarV2('text/secondary'),
|
||||
marginBottom: '20px',
|
||||
});
|
||||
|
||||
export const mainButton = style({
|
||||
width: '100%',
|
||||
fontSize: '14px',
|
||||
height: '42px',
|
||||
});
|
||||
|
||||
export const workspaceSelector = style({
|
||||
margin: '0 -16px',
|
||||
width: 'calc(100% + 32px)',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
padding: '0 16px',
|
||||
});
|
||||
@@ -103,6 +103,10 @@ export const topLevelRoutes = [
|
||||
path: '/theme-editor',
|
||||
lazy: () => import('./pages/theme-editor'),
|
||||
},
|
||||
{
|
||||
path: '/clipper/import',
|
||||
lazy: () => import('./pages/import-clipper'),
|
||||
},
|
||||
{
|
||||
path: '/template/import',
|
||||
lazy: () => import('./pages/import-template'),
|
||||
|
||||
Reference in New Issue
Block a user