一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
202 行
7.3 KiB
TypeScript
202 行
7.3 KiB
TypeScript
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
|
import { FileSystemItem, ImportModuleOutputFormat, ModuleType } from '@joplin/lib/services/interop/types';
|
|
import bridge from '../../../services/bridge';
|
|
import { WindowControl } from '../utils/useWindowControl';
|
|
import { _ } from '@joplin/lib/locale';
|
|
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
import { PackageInfo } from '@joplin/lib/versionInfo';
|
|
import shim from '@joplin/lib/shim';
|
|
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
|
import { isRecoverableError } from '@joplin/lib/import-enex';
|
|
import Logger from '@joplin/utils/Logger';
|
|
import { Dispatch } from 'redux';
|
|
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
|
import { promptForImportedNoteLockKey } from '@joplin/lib/services/noteLock/noteLockPrompts';
|
|
import noteLockPrompts from '../../../utils/noteLockPrompts';
|
|
import { ErrorCode } from '@joplin/lib/errors';
|
|
const packageInfo: PackageInfo = require('../../../packageInfo.js');
|
|
|
|
const logger = Logger.create('importFrom');
|
|
|
|
const makeImportNoteLockKeyHandler = (modalMessage: string, dispatch: Dispatch) => async (backupKey: MasterKeyEntity) => {
|
|
// The password prompts are DOM dialogs and would render behind the import modal overlay.
|
|
void CommandService.instance().execute('hideModalMessage');
|
|
try {
|
|
return await promptForImportedNoteLockKey(backupKey, noteLockPrompts(dispatch));
|
|
} finally {
|
|
void CommandService.instance().execute('showModalMessage', modalMessage);
|
|
}
|
|
};
|
|
|
|
export const declaration: CommandDeclaration = {
|
|
name: 'importFrom',
|
|
label: () => _('Import...'),
|
|
};
|
|
|
|
export interface ImportCommandOptions {
|
|
sourcePath: string|undefined;
|
|
sourceType: FileSystemItem;
|
|
destinationFolderId: string|null;
|
|
importFormat: string;
|
|
outputFormat: ImportModuleOutputFormat;
|
|
}
|
|
|
|
const findImportModule = async (commandOptions: ImportCommandOptions|null, control: WindowControl) => {
|
|
if (commandOptions) {
|
|
const module = InteropService.instance().findModuleByFormat(
|
|
ModuleType.Importer, commandOptions.importFormat, commandOptions.sourceType, commandOptions.outputFormat);
|
|
if (module) {
|
|
return module as ImportModule;
|
|
}
|
|
}
|
|
|
|
const importModules = InteropService.instance().modules().filter(module => module.type === ModuleType.Importer) as ImportModule[];
|
|
return await control.showPrompt({
|
|
label: _('Select the type of file to be imported:'),
|
|
value: '',
|
|
suggestions: importModules.map(module => {
|
|
const label = module.fullLabel();
|
|
return {
|
|
key: `${module.type}--${label}`,
|
|
value: module,
|
|
label: module.fullLabel(),
|
|
};
|
|
}),
|
|
});
|
|
};
|
|
|
|
const promptForSourcePath = async (module: ImportModule, sourceType: FileSystemItem|undefined) => {
|
|
if (!sourceType) {
|
|
if (!module.sources.includes(FileSystemItem.Directory)) {
|
|
sourceType = FileSystemItem.File;
|
|
}
|
|
if (!module.sources.includes(FileSystemItem.File)) {
|
|
sourceType = FileSystemItem.Directory;
|
|
}
|
|
}
|
|
if (sourceType === FileSystemItem.File) {
|
|
return await bridge().showOpenDialog({
|
|
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
|
});
|
|
} else if (sourceType === FileSystemItem.Directory) {
|
|
return await bridge().showOpenDialog({
|
|
properties: ['openDirectory', 'createDirectory'],
|
|
});
|
|
} else {
|
|
return await bridge().showOpenDialog({
|
|
properties: ['openDirectory', 'openFile'],
|
|
});
|
|
}
|
|
};
|
|
|
|
export const runtime = (control: WindowControl): CommandRuntime => {
|
|
return {
|
|
// Since this can be run from "go to anything", partialOptions needs to support being null or empty.
|
|
execute: async (context: CommandContext, options: ImportCommandOptions|undefined) => {
|
|
const importModule = await findImportModule(options, control);
|
|
if (!importModule) return null; // E.g. if cancelled
|
|
|
|
let sourcePath = options?.sourcePath ?? await promptForSourcePath(importModule, options?.sourceType);
|
|
if (Array.isArray(sourcePath)) {
|
|
sourcePath = sourcePath[0];
|
|
}
|
|
// Handle the case where the directory picker action was cancelled
|
|
if (!sourcePath) return null;
|
|
|
|
if (!options) {
|
|
const isDirectory = await shim.fsDriver().isDirectory(sourcePath);
|
|
const importsMultipleNotes = importModule.isNoteArchive || isDirectory;
|
|
|
|
const destinationFolderId = importsMultipleNotes ? null : context.state.selectedFolderId;
|
|
const importFormat = importModule.format;
|
|
const outputFormat = importModule.outputFormat;
|
|
options = {
|
|
sourcePath,
|
|
destinationFolderId,
|
|
importFormat,
|
|
outputFormat,
|
|
sourceType: isDirectory ? FileSystemItem.Directory : FileSystemItem.File,
|
|
};
|
|
}
|
|
|
|
if (options.importFormat === 'one' && !shim.isWindows()) {
|
|
const response = bridge().showMessageBox(
|
|
_('This notebook contains Windows-specific files. For maximum compatibility, consider importing the notebook from Windows.'),
|
|
{
|
|
type: 'warning',
|
|
buttons: [_('Import anyway'), _('Cancel')],
|
|
defaultId: 0,
|
|
cancelId: 1,
|
|
},
|
|
);
|
|
if (response === 1) return null;
|
|
}
|
|
|
|
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', sourcePath, options.importFormat);
|
|
void CommandService.instance().execute('showModalMessage', modalMessage);
|
|
|
|
const errors: (string|Error)[] = [];
|
|
|
|
const importOptions = {
|
|
path: sourcePath,
|
|
format: options.importFormat,
|
|
outputFormat: options.outputFormat,
|
|
onProgress: (status: Record<string, unknown>) => {
|
|
const statusStrings: string[] = Object.keys(status).map((key: string) => {
|
|
return `${key}: ${status[key]}`;
|
|
});
|
|
|
|
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
|
},
|
|
onError: (error: string|Error) => {
|
|
// Recovered errors are logged but not reported, since the
|
|
// notes were imported successfully anyway.
|
|
if (!isRecoverableError(error)) errors.push(error);
|
|
console.warn(error);
|
|
},
|
|
destinationFolderId: options.destinationFolderId,
|
|
onNoteLockKey: makeImportNoteLockKeyHandler(modalMessage, context.dispatch),
|
|
};
|
|
|
|
const service = InteropService.instance();
|
|
try {
|
|
const result = await service.import(importOptions);
|
|
// eslint-disable-next-line no-console
|
|
console.info('Import result: ', result);
|
|
} catch (error) {
|
|
if (error.code !== ErrorCode.Cancelled) {
|
|
logger.error(error);
|
|
bridge().showErrorMessageBox(error.message);
|
|
}
|
|
}
|
|
|
|
void CommandService.instance().execute('hideModalMessage');
|
|
|
|
if (errors.length) {
|
|
const response = bridge().showErrorMessageBox('There were some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
|
|
buttons: [_('Close'), _('Send bug report')],
|
|
});
|
|
|
|
context.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
|
|
|
|
if (response === 1) {
|
|
const url = makeDiscourseDebugUrl(
|
|
`Error importing notes from format: ${options.importFormat}`,
|
|
`- Input format: ${options.importFormat}\n- Output format: ${options.outputFormat}`,
|
|
errors,
|
|
packageInfo,
|
|
PluginService.instance(),
|
|
Setting.value('plugins.states'),
|
|
);
|
|
|
|
void bridge().openExternal(url);
|
|
}
|
|
}
|
|
},
|
|
enabledCondition: '',
|
|
};
|
|
};
|