mirror of
https://github.com/laurent22/joplin.git
synced 2026-06-18 20:16:34 +02:00
Desktop: Plugins: Add an "importFrom" command to allow importing notes and notebooks (#13534)
This commit is contained in:
@@ -470,6 +470,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
@@ -512,6 +513,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
|
||||
@@ -442,6 +442,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
@@ -484,6 +485,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
|
||||
@@ -9,7 +9,6 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
||||
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||
import InteropServiceHelper from '../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
@@ -29,6 +28,8 @@ import { EventName } from '@joplin/lib/eventManager';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ImportCommandOptions } from './WindowCommandsAndDialogs/commands/importFrom';
|
||||
import { FileSystemItem } from '@joplin/lib/services/interop/types';
|
||||
|
||||
const logger = Logger.create('MenuBar');
|
||||
|
||||
@@ -304,83 +305,16 @@ function useMenu(props: Props) {
|
||||
void CommandService.instance().execute(commandName);
|
||||
}, []);
|
||||
|
||||
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => {
|
||||
let path = null;
|
||||
|
||||
if (moduleSource === 'file') {
|
||||
path = await bridge().showOpenDialog({
|
||||
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
||||
});
|
||||
} else {
|
||||
path = await bridge().showOpenDialog({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!path || (Array.isArray(path) && !path.length)) return;
|
||||
|
||||
if (Array.isArray(path)) path = path[0];
|
||||
|
||||
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', path, module.format);
|
||||
|
||||
void CommandService.instance().execute('showModalMessage', modalMessage);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const errors: any[] = [];
|
||||
|
||||
const importOptions = {
|
||||
path,
|
||||
format: module.format,
|
||||
outputFormat: module.outputFormat,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onProgress: (status: any) => {
|
||||
const statusStrings: string[] = Object.keys(status).map((key: string) => {
|
||||
return `${key}: ${status[key]}`;
|
||||
});
|
||||
|
||||
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onError: (error: any) => {
|
||||
errors.push(error);
|
||||
console.warn(error);
|
||||
},
|
||||
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: FileSystemItem) => {
|
||||
const options: ImportCommandOptions = {
|
||||
destinationFolderId: !module.isNoteArchive && moduleSource === 'file' ? props.selectedFolderId : null,
|
||||
sourcePath: undefined, // Show a file picker
|
||||
sourceType: moduleSource,
|
||||
importFormat: module.format,
|
||||
outputFormat: module.outputFormat,
|
||||
};
|
||||
|
||||
const service = InteropService.instance();
|
||||
try {
|
||||
const result = await service.import(importOptions);
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Import result: ', result);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
||||
void CommandService.instance().execute('hideModalMessage');
|
||||
|
||||
if (errors.length) {
|
||||
const response = bridge().showErrorMessageBox('There was 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')],
|
||||
});
|
||||
|
||||
props.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
|
||||
|
||||
if (response === 1) {
|
||||
const url = makeDiscourseDebugUrl(
|
||||
`Error importing notes from format: ${module.format}`,
|
||||
`- Input format: ${module.format}\n- Output format: ${module.outputFormat}`,
|
||||
errors,
|
||||
packageInfo,
|
||||
PluginService.instance(),
|
||||
props.pluginSettings,
|
||||
);
|
||||
|
||||
void bridge().openExternal(url);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.selectedFolderId, props.pluginSettings]);
|
||||
await CommandService.instance().execute('importFrom', options);
|
||||
}, [props.selectedFolderId]);
|
||||
|
||||
const onMenuItemClickRef = useRef(null);
|
||||
onMenuItemClickRef.current = onMenuItemClick;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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';
|
||||
const packageInfo: PackageInfo = require('../../../packageInfo.js');
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', sourcePath, options.importFormat);
|
||||
void CommandService.instance().execute('showModalMessage', modalMessage);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const errors: any[] = [];
|
||||
|
||||
const importOptions = {
|
||||
path: sourcePath,
|
||||
format: options.importFormat,
|
||||
outputFormat: options.outputFormat,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onProgress: (status: any) => {
|
||||
const statusStrings: string[] = Object.keys(status).map((key: string) => {
|
||||
return `${key}: ${status[key]}`;
|
||||
});
|
||||
|
||||
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onError: (error: any) => {
|
||||
errors.push(error);
|
||||
console.warn(error);
|
||||
},
|
||||
destinationFolderId: options.destinationFolderId,
|
||||
};
|
||||
|
||||
const service = InteropService.instance();
|
||||
try {
|
||||
const result = await service.import(importOptions);
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Import result: ', result);
|
||||
} catch (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: '',
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import * as editAlarm from './editAlarm';
|
||||
import * as exportPdf from './exportPdf';
|
||||
import * as gotoAnything from './gotoAnything';
|
||||
import * as hideModalMessage from './hideModalMessage';
|
||||
import * as importFrom from './importFrom';
|
||||
import * as linkToNote from './linkToNote';
|
||||
import * as moveToFolder from './moveToFolder';
|
||||
import * as newFolder from './newFolder';
|
||||
@@ -55,6 +56,7 @@ const index: any[] = [
|
||||
exportPdf,
|
||||
gotoAnything,
|
||||
hideModalMessage,
|
||||
importFrom,
|
||||
linkToNote,
|
||||
moveToFolder,
|
||||
newFolder,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import showFolderPicker from '../utils/showFolderPicker';
|
||||
|
||||
const logger = Logger.create('commands/moveToFolder');
|
||||
|
||||
@@ -31,71 +32,37 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
}
|
||||
}
|
||||
|
||||
const folders = await Folder.sortFolderTree();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const startFolders: any[] = [];
|
||||
const maxDepth = 15;
|
||||
|
||||
// It's okay for folders (but not notes) to have no parent folder:
|
||||
if (allAreFolders) {
|
||||
startFolders.push({
|
||||
key: '',
|
||||
value: '',
|
||||
label: _('None'),
|
||||
indentDepth: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const addOptions = (folders: FolderEntityWithChildren[], depth: number) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
|
||||
// Disallow making a folder a subfolder of itself.
|
||||
if (itemIdToType.has(folder.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
|
||||
if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
|
||||
}
|
||||
};
|
||||
|
||||
addOptions(folders, 0);
|
||||
|
||||
comp.setState({
|
||||
promptOptions: {
|
||||
label: _('Move to notebook:'),
|
||||
inputType: 'dropdown',
|
||||
value: '',
|
||||
autocomplete: startFolders,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onClose: async (answer: any) => {
|
||||
if (answer) {
|
||||
try {
|
||||
const targetFolderId = answer.value;
|
||||
for (const id of itemIds) {
|
||||
if (id === targetFolderId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const itemType = itemIdToType.get(id);
|
||||
if (itemType === ModelType.Note) {
|
||||
await Note.moveToFolder(id, targetFolderId);
|
||||
} else if (itemType === ModelType.Folder) {
|
||||
await Folder.moveToFolder(id, targetFolderId);
|
||||
} else {
|
||||
throw new Error(`Cannot move item with type ${itemType}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error moving items', error);
|
||||
void shim.showMessageBox(`Error: ${error}`);
|
||||
}
|
||||
}
|
||||
comp.setState({ promptOptions: null });
|
||||
},
|
||||
},
|
||||
const targetFolderId = await showFolderPicker(comp, {
|
||||
label: _('Move to notebook:'),
|
||||
// It's okay for folders (but not notes) to have no parent folder:
|
||||
allowSelectNone: allAreFolders,
|
||||
// Don't allow setting a folder as its own parent
|
||||
showFolder: (folder) => !itemIdToType.has(folder.id),
|
||||
});
|
||||
|
||||
// It's important to allow the case where targetFolderId is the empty string,
|
||||
// since that corresponds to the toplevel notebook.
|
||||
if (targetFolderId !== null) {
|
||||
try {
|
||||
for (const id of itemIds) {
|
||||
if (id === targetFolderId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const itemType = itemIdToType.get(id);
|
||||
if (itemType === ModelType.Note) {
|
||||
await Note.moveToFolder(id, targetFolderId);
|
||||
} else if (itemType === ModelType.Folder) {
|
||||
await Folder.moveToFolder(id, targetFolderId);
|
||||
} else {
|
||||
throw new Error(`Cannot move item with type ${itemType}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error moving items', error);
|
||||
void shim.showMessageBox(`Error: ${error}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
enabledCondition: 'someNotesSelected && !noteIsReadOnly',
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface DialogState {
|
||||
description?: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
autocomplete?: unknown;
|
||||
onClose?: (answer: unknown, buttonType: unknown)=> void;
|
||||
}|null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
import { WindowControl } from './useWindowControl';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
interface FolderEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
indentDepth: number;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
label: string;
|
||||
allowSelectNone: boolean;
|
||||
showFolder: (entity: FolderEntity)=> boolean;
|
||||
}
|
||||
|
||||
const showFolderPicker = async (control: WindowControl, { label, allowSelectNone, showFolder }: Options) => {
|
||||
const folders = await Folder.sortFolderTree();
|
||||
const startFolders: FolderEntry[] = [];
|
||||
const maxDepth = 15;
|
||||
|
||||
if (allowSelectNone) {
|
||||
startFolders.push({
|
||||
key: '',
|
||||
value: '',
|
||||
label: _('None'),
|
||||
indentDepth: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const addOptions = (folders: FolderEntityWithChildren[], depth: number) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
|
||||
if (!showFolder(folder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
|
||||
if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
|
||||
}
|
||||
};
|
||||
|
||||
addOptions(folders, 0);
|
||||
|
||||
const folderId = await control.showPrompt({
|
||||
label,
|
||||
value: '',
|
||||
suggestions: startFolders,
|
||||
});
|
||||
return folderId;
|
||||
};
|
||||
|
||||
export default showFolderPicker;
|
||||
@@ -5,8 +5,22 @@ import { PrintCallback } from './usePrintToCallback';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import announceForAccessibility from '../../utils/announceForAccessibility';
|
||||
|
||||
interface PromptSuggestion<T> {
|
||||
key: string;
|
||||
value: T;
|
||||
label: string;
|
||||
indentDepth?: number;
|
||||
}
|
||||
|
||||
interface PromptOptions<T> {
|
||||
label: string;
|
||||
value: string;
|
||||
suggestions: PromptSuggestion<T>[];
|
||||
}
|
||||
|
||||
export interface WindowControl {
|
||||
setState: (update: Partial<DialogState>)=> void;
|
||||
showPrompt: <T>(options: PromptOptions<T>)=> Promise<T>;
|
||||
printTo: PrintCallback;
|
||||
announcePanelVisibility(panelName: string, visible: boolean): void;
|
||||
}
|
||||
@@ -19,7 +33,7 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
|
||||
onPrintRef.current = onPrint;
|
||||
|
||||
return useMemo((): WindowControl => {
|
||||
return {
|
||||
const control: WindowControl = {
|
||||
setState: (newPartialState: Partial<DialogState>) => {
|
||||
setDialogState(oldState => ({
|
||||
...oldState,
|
||||
@@ -32,7 +46,29 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
|
||||
visible ? _('Panel "%s" is visible', panelName) : _('Panel %s is hidden', panelName),
|
||||
);
|
||||
},
|
||||
showPrompt: <T> (options: PromptOptions<T>) => {
|
||||
return new Promise<T>((resolve) => {
|
||||
control.setState({
|
||||
promptOptions: {
|
||||
label: options.label,
|
||||
inputType: 'dropdown',
|
||||
value: options.value,
|
||||
autocomplete: options.suggestions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored code before rule was applied
|
||||
onClose: async (answer: any) => {
|
||||
if (answer) {
|
||||
resolve(answer.value);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
control.setState({ promptOptions: null });
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
return control;
|
||||
}, [setDialogState]);
|
||||
};
|
||||
|
||||
|
||||
@@ -212,5 +212,28 @@ test.describe('main', () => {
|
||||
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
test('should import an HTML directory', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
||||
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
||||
await importedFolder.click();
|
||||
|
||||
const importedNote1 = await mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await expect(importedNote1).toBeAttached();
|
||||
const importedNote2 = await mainScreen.noteList.getNoteItemByTitle('test-html-file-2');
|
||||
await expect(importedNote2).toBeAttached();
|
||||
});
|
||||
|
||||
test('should import a single HTML file', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlFile(electronApp, join(__dirname, 'resources', 'html-import', 'test-html-file-with-image.html'));
|
||||
const importedNote = await mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await expect(importedNote).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -68,9 +68,17 @@ export default class MainScreen {
|
||||
await searchBar.fill(text);
|
||||
}
|
||||
|
||||
public async importHtmlDirectory(electronApp: ElectronApplication, path: string) {
|
||||
private async importFromModule_(electronApp: ElectronApplication, moduleName: string, path: string) {
|
||||
await setFilePickerResponse(electronApp, [path]);
|
||||
await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
|
||||
await activateMainMenuItem(electronApp, moduleName, 'Import');
|
||||
}
|
||||
|
||||
public async importHtmlDirectory(electronApp: ElectronApplication, path: string) {
|
||||
return this.importFromModule_(electronApp, 'HTML - HTML document (Directory)', path);
|
||||
}
|
||||
|
||||
public async importHtmlFile(electronApp: ElectronApplication, path: string) {
|
||||
return this.importFromModule_(electronApp, 'HTML - HTML document (File)', path);
|
||||
}
|
||||
|
||||
public async pluginPanelLocator(pluginId: string) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Test HTML file 2!</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -229,7 +229,7 @@ export default class InteropService {
|
||||
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
|
||||
// is returned. This is useful to auto-detect the module based on the format.
|
||||
// For more precise matching, newModuleFromPath_ should be used.
|
||||
private findModuleByFormat_(type: ModuleType, format: string, target: FileSystemItem = null, outputFormat: ImportModuleOutputFormat = null) {
|
||||
public findModuleByFormat(type: ModuleType, format: string, target: FileSystemItem = null, outputFormat: ImportModuleOutputFormat = null) {
|
||||
const modules = this.modules();
|
||||
const matches = [];
|
||||
|
||||
@@ -268,7 +268,7 @@ export default class InteropService {
|
||||
// https://github.com/laurent22/joplin/pull/1795#discussion_r322379121) but
|
||||
// we can do it if it ever becomes necessary.
|
||||
private newModuleByFormat_(type: ModuleType, format: string, outputFormat: ImportModuleOutputFormat = ImportModuleOutputFormat.Markdown) {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, format, null, outputFormat);
|
||||
const moduleMetadata = this.findModuleByFormat(type, format, null, outputFormat);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and output "%s"', type, format, outputFormat));
|
||||
|
||||
return moduleMetadata.factory();
|
||||
@@ -281,7 +281,7 @@ export default class InteropService {
|
||||
//
|
||||
// https://github.com/laurent22/joplin/pull/1795#pullrequestreview-281574417
|
||||
private newModuleFromPath_(type: ModuleType, options: ExportOptions&ImportOptions) {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
|
||||
const moduleMetadata = this.findModuleByFormat(type, options.format, options.target);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and target "%s"', type, options.format, options.target));
|
||||
|
||||
return moduleMetadata.factory(options);
|
||||
|
||||
Reference in New Issue
Block a user