feat(core): add clipper import interface (#10619)
This commit is contained in:
@@ -217,6 +217,7 @@ export class DocFrontend {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
|
|
||||||
const docId = await this.status.jobDocQueue.asyncPop(signal);
|
const docId = await this.status.jobDocQueue.asyncPop(signal);
|
||||||
const jobs = this.status.jobMap.get(docId);
|
const jobs = this.status.jobMap.get(docId);
|
||||||
this.status.jobMap.delete(docId);
|
this.status.jobMap.delete(docId);
|
||||||
@@ -244,6 +245,9 @@ export class DocFrontend {
|
|||||||
if (save?.length) {
|
if (save?.length) {
|
||||||
await this.jobs.save(docId, save as any, signal);
|
await this.jobs.save(docId, save as any, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.status.currentJob = null;
|
||||||
|
this.statusUpdatedSubject$.next(docId);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
dispose();
|
dispose();
|
||||||
@@ -486,7 +490,7 @@ export class DocFrontend {
|
|||||||
return Promise.race([
|
return Promise.race([
|
||||||
new Promise<void>(resolve => {
|
new Promise<void>(resolve => {
|
||||||
sub = this.docState$(docId).subscribe(state => {
|
sub = this.docState$(docId).subscribe(state => {
|
||||||
if (state.syncing) {
|
if (state.synced && !state.updating) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
path: '/theme-editor',
|
||||||
lazy: () => import('./pages/theme-editor'),
|
lazy: () => import('./pages/theme-editor'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/clipper/import',
|
||||||
|
lazy: () => import('./pages/import-clipper'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/template/import',
|
path: '/template/import',
|
||||||
lazy: () => import('./pages/import-template'),
|
lazy: () => import('./pages/import-template'),
|
||||||
|
|||||||
10
packages/frontend/core/src/modules/import-clipper/index.ts
Normal file
10
packages/frontend/core/src/modules/import-clipper/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspacesService } from '../workspace';
|
||||||
|
import { ImportClipperService } from './services/import';
|
||||||
|
|
||||||
|
export { type ClipperInput, ImportClipperService } from './services/import';
|
||||||
|
|
||||||
|
export function configureImportClipperModule(framework: Framework) {
|
||||||
|
framework.service(ImportClipperService, [WorkspacesService]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { MarkdownTransformer } from '@blocksuite/affine/blocks';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { DocsService } from '../../doc';
|
||||||
|
import {
|
||||||
|
getAFFiNEWorkspaceSchema,
|
||||||
|
type WorkspaceMetadata,
|
||||||
|
type WorkspacesService,
|
||||||
|
} from '../../workspace';
|
||||||
|
|
||||||
|
export interface ClipperInput {
|
||||||
|
title: string;
|
||||||
|
contentMarkdown: string;
|
||||||
|
contentHtml: string;
|
||||||
|
attachments: Record<string, Blob>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImportClipperService extends Service {
|
||||||
|
constructor(private readonly workspacesService: WorkspacesService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async importToWorkspace(
|
||||||
|
workspaceMetadata: WorkspaceMetadata,
|
||||||
|
clipperInput: ClipperInput
|
||||||
|
) {
|
||||||
|
const { workspace, dispose: disposeWorkspace } =
|
||||||
|
this.workspacesService.open({
|
||||||
|
metadata: workspaceMetadata,
|
||||||
|
});
|
||||||
|
await workspace.engine.doc.waitForDocReady(workspace.id); // wait for root doc ready
|
||||||
|
const docId = await MarkdownTransformer.importMarkdownToDoc({
|
||||||
|
collection: workspace.docCollection,
|
||||||
|
schema: getAFFiNEWorkspaceSchema(),
|
||||||
|
markdown: clipperInput.contentMarkdown,
|
||||||
|
});
|
||||||
|
const docsService = workspace.scope.get(DocsService);
|
||||||
|
if (docId) {
|
||||||
|
// only support page mode for now
|
||||||
|
docsService.list.setPrimaryMode(docId, 'page');
|
||||||
|
workspace.engine.doc.addPriority(workspace.id, 100);
|
||||||
|
workspace.engine.doc.addPriority(docId, 100);
|
||||||
|
await workspace.engine.doc.waitForDocSynced(workspace.id);
|
||||||
|
await workspace.engine.doc.waitForDocSynced(docId);
|
||||||
|
disposeWorkspace();
|
||||||
|
return docId;
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to import doc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importToNewWorkspace(
|
||||||
|
flavour: string,
|
||||||
|
workspaceName: string,
|
||||||
|
clipperInput: ClipperInput
|
||||||
|
) {
|
||||||
|
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
let docId: string | undefined;
|
||||||
|
const { id: workspaceId } = await this.workspacesService.create(
|
||||||
|
flavour,
|
||||||
|
async docCollection => {
|
||||||
|
docCollection.meta.initialize();
|
||||||
|
docCollection.meta.setName(workspaceName);
|
||||||
|
docId = await MarkdownTransformer.importMarkdownToDoc({
|
||||||
|
collection: docCollection,
|
||||||
|
schema: getAFFiNEWorkspaceSchema(),
|
||||||
|
markdown: clipperInput.contentMarkdown,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!docId) {
|
||||||
|
throw new Error('Failed to import doc');
|
||||||
|
}
|
||||||
|
return { workspaceId, docId };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
|
||||||
import { Entity, LiveData } from '@toeverything/infra';
|
|
||||||
|
|
||||||
interface TemplateOptions {
|
|
||||||
templateName: string;
|
|
||||||
snapshotUrl: string;
|
|
||||||
templateMode: DocMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ImportTemplateDialog extends Entity {
|
|
||||||
readonly isOpen$ = new LiveData(false);
|
|
||||||
readonly template$ = new LiveData<TemplateOptions | null>(null);
|
|
||||||
|
|
||||||
open(options: TemplateOptions) {
|
|
||||||
this.template$.next(options);
|
|
||||||
this.isOpen$.next(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.isOpen$.next(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { WorkspacesService } from '../workspace';
|
import { WorkspacesService } from '../workspace';
|
||||||
import { ImportTemplateDialog } from './entities/dialog';
|
|
||||||
import { TemplateDownloader } from './entities/downloader';
|
import { TemplateDownloader } from './entities/downloader';
|
||||||
import { TemplateDownloaderService } from './services/downloader';
|
import { TemplateDownloaderService } from './services/downloader';
|
||||||
import { ImportTemplateService } from './services/import';
|
import { ImportTemplateService } from './services/import';
|
||||||
@@ -12,7 +11,6 @@ export { ImportTemplateService } from './services/import';
|
|||||||
|
|
||||||
export function configureImportTemplateModule(framework: Framework) {
|
export function configureImportTemplateModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.entity(ImportTemplateDialog)
|
|
||||||
.service(TemplateDownloaderService)
|
.service(TemplateDownloaderService)
|
||||||
.entity(TemplateDownloader, [TemplateDownloaderStore])
|
.entity(TemplateDownloader, [TemplateDownloaderStore])
|
||||||
.store(TemplateDownloaderStore)
|
.store(TemplateDownloaderStore)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { configureFavoriteModule } from './favorite';
|
|||||||
import { configureFeatureFlagModule } from './feature-flag';
|
import { configureFeatureFlagModule } from './feature-flag';
|
||||||
import { configureGlobalContextModule } from './global-context';
|
import { configureGlobalContextModule } from './global-context';
|
||||||
import { configureI18nModule } from './i18n';
|
import { configureI18nModule } from './i18n';
|
||||||
|
import { configureImportClipperModule } from './import-clipper';
|
||||||
import { configureImportTemplateModule } from './import-template';
|
import { configureImportTemplateModule } from './import-template';
|
||||||
import { configureJournalModule } from './journal';
|
import { configureJournalModule } from './journal';
|
||||||
import { configureLifecycleModule } from './lifecycle';
|
import { configureLifecycleModule } from './lifecycle';
|
||||||
@@ -100,4 +101,5 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureAIButtonModule(framework);
|
configureAIButtonModule(framework);
|
||||||
configureTemplateDocModule(framework);
|
configureTemplateDocModule(framework);
|
||||||
configureBlobManagementModule(framework);
|
configureBlobManagementModule(framework);
|
||||||
|
configureImportClipperModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2221,6 +2221,28 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Failed to load template, please try again.`
|
* `Failed to load template, please try again.`
|
||||||
*/
|
*/
|
||||||
["com.affine.import-template.dialog.errorLoad"](): string;
|
["com.affine.import-template.dialog.errorLoad"](): string;
|
||||||
|
/**
|
||||||
|
* `Create into a New Workspace`
|
||||||
|
*/
|
||||||
|
["com.affine.import-clipper.dialog.createDocToNewWorkspace"](): string;
|
||||||
|
/**
|
||||||
|
* `Create doc to "{{workspace}}"`
|
||||||
|
*/
|
||||||
|
["com.affine.import-clipper.dialog.createDocToWorkspace"](options: {
|
||||||
|
readonly workspace: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Create doc from Web Clipper`
|
||||||
|
*/
|
||||||
|
["com.affine.import-clipper.dialog.createDocFromClipper"](): string;
|
||||||
|
/**
|
||||||
|
* `Failed to import content, please try again.`
|
||||||
|
*/
|
||||||
|
["com.affine.import-clipper.dialog.errorImport"](): string;
|
||||||
|
/**
|
||||||
|
* `Failed to load content, please try again.`
|
||||||
|
*/
|
||||||
|
["com.affine.import-clipper.dialog.errorLoad"](): string;
|
||||||
/**
|
/**
|
||||||
* `Support Markdown/Notion`
|
* `Support Markdown/Notion`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -555,6 +555,11 @@
|
|||||||
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
|
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
|
||||||
"com.affine.import-template.dialog.errorImport": "Failed to import template, please try again.",
|
"com.affine.import-template.dialog.errorImport": "Failed to import template, please try again.",
|
||||||
"com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.",
|
"com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.",
|
||||||
|
"com.affine.import-clipper.dialog.createDocToNewWorkspace": "Create into a New Workspace",
|
||||||
|
"com.affine.import-clipper.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
|
||||||
|
"com.affine.import-clipper.dialog.createDocFromClipper": "Create doc from Web Clipper",
|
||||||
|
"com.affine.import-clipper.dialog.errorImport": "Failed to import content, please try again.",
|
||||||
|
"com.affine.import-clipper.dialog.errorLoad": "Failed to load content, please try again.",
|
||||||
"com.affine.import_file": "Support Markdown/Notion",
|
"com.affine.import_file": "Support Markdown/Notion",
|
||||||
"com.affine.import.affine-workspace-data": "AFFiNE workspace data",
|
"com.affine.import.affine-workspace-data": "AFFiNE workspace data",
|
||||||
"com.affine.import.html-files": "HTML",
|
"com.affine.import.html-files": "HTML",
|
||||||
|
|||||||
Reference in New Issue
Block a user