mirror of
https://github.com/laurent22/joplin.git
synced 2025-02-19 20:00:20 +02:00
Chore: Apply changes from mobile plugins to lib/
and app-desktop/
(#10079)
This commit is contained in:
parent
91004f5714
commit
25cd5affca
@ -171,8 +171,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@ -717,6 +715,10 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.test.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
@ -745,6 +747,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -151,8 +151,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@ -697,6 +695,10 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.test.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
@ -725,6 +727,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
|
@ -279,17 +279,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
try {
|
||||
const devPluginOptions = { devMode: true, builtIn: false };
|
||||
|
||||
if (Setting.value('plugins.devPluginPaths')) {
|
||||
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
|
||||
await service.loadAndRunPlugins(paths, pluginSettings, devPluginOptions);
|
||||
}
|
||||
|
||||
// Also load dev plugins that have passed via command line arguments
|
||||
if (Setting.value('startupDevPlugins')) {
|
||||
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, devPluginOptions);
|
||||
}
|
||||
await service.loadAndRunDevPlugins(pluginSettings);
|
||||
} catch (error) {
|
||||
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
|
||||
}
|
||||
|
@ -288,9 +288,7 @@ export class Bridge {
|
||||
}
|
||||
|
||||
/* returns the index of the clicked button */
|
||||
public showMessageBox(message: string, options: MessageDialogOptions = null) {
|
||||
if (options === null) options = { message: '' };
|
||||
|
||||
public showMessageBox(message: string, options: MessageDialogOptions = {}) {
|
||||
const result = this.showMessageBox_(this.window(), { type: 'question',
|
||||
message: message,
|
||||
buttons: [_('OK'), _('Cancel')], ...options });
|
||||
|
@ -345,10 +345,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.setState({ needRestart: true });
|
||||
}
|
||||
shared.updateSettingValue(this, key, value);
|
||||
|
||||
if (md.autoSave) {
|
||||
shared.scheduleSaveSettings(this);
|
||||
}
|
||||
};
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const styled = require('styled-components').default;
|
||||
@ -72,23 +71,6 @@ export const StyledListItemIcon = styled.i`
|
||||
export default function Sidebar(props: Props) {
|
||||
const buttons: any[] = [];
|
||||
|
||||
const sortedSections = useMemo(() => {
|
||||
const output = props.sections.slice();
|
||||
output.sort((a: any, b: any) => {
|
||||
const s1 = a.source || SettingSectionSource.Default;
|
||||
const s2 = b.source || SettingSectionSource.Default;
|
||||
if (s1 === SettingSectionSource.Default && s2 === SettingSectionSource.Default) return props.sections.indexOf(s1) - props.sections.indexOf(s2);
|
||||
if (s1 === SettingSectionSource.Default && s2 === SettingSectionSource.Plugin) return -1;
|
||||
if (s1 === SettingSectionSource.Plugin && s2 === SettingSectionSource.Default) return +1;
|
||||
|
||||
const l1 = Setting.sectionNameToLabel(a.name);
|
||||
const l2 = Setting.sectionNameToLabel(b.name);
|
||||
if (s1 === SettingSectionSource.Plugin && s2 === SettingSectionSource.Plugin) return l1.toLowerCase() < l2.toLowerCase() ? -1 : +1;
|
||||
return 0;
|
||||
});
|
||||
return output;
|
||||
}, [props.sections]);
|
||||
|
||||
function renderButton(section: any) {
|
||||
const selected = props.selection === section.name;
|
||||
return (
|
||||
@ -121,7 +103,7 @@ export default function Sidebar(props: Props) {
|
||||
|
||||
let pluginDividerAdded = false;
|
||||
|
||||
for (const section of sortedSections) {
|
||||
for (const section of props.sections) {
|
||||
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
|
||||
buttons.push(renderDivider('divider-plugins'));
|
||||
pluginDividerAdded = true;
|
||||
|
@ -6,6 +6,7 @@ import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
||||
import Button, { ButtonLevel } from '../../../Button/Button';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled = 1,
|
||||
@ -20,10 +21,6 @@ export enum UpdateState {
|
||||
HasBeenUpdated = 4,
|
||||
}
|
||||
|
||||
export interface ItemEvent {
|
||||
item: PluginItem;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item?: PluginItem;
|
||||
manifest?: PluginManifest;
|
||||
@ -48,15 +45,6 @@ function manifestToItem(manifest: PluginManifest): PluginItem {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
deleted: boolean;
|
||||
devMode: boolean;
|
||||
builtIn: boolean;
|
||||
hasBeenUpdated: boolean;
|
||||
}
|
||||
|
||||
const CellRoot = styled.div<{ isCompatible: boolean }>`
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
@ -4,15 +4,16 @@ import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSett
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
import SearchPlugins from './SearchPlugins';
|
||||
import PluginBox, { ItemEvent, UpdateState } from './PluginBox';
|
||||
import PluginBox, { UpdateState } from './PluginBox';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../../../Button/Button';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import produce from 'immer';
|
||||
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||
import { PluginItem } from './PluginBox';
|
||||
import { PluginItem, ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useOnInstallHandler, { OnPluginSettingChangeEvent } from './useOnInstallHandler';
|
||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/useOnDeleteHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import StyledMessage from '../../../style/StyledMessage';
|
||||
import StyledLink from '../../../style/StyledLink';
|
||||
@ -59,7 +60,7 @@ let repoApi_: RepositoryApi = null;
|
||||
|
||||
function repoApi(): RepositoryApi {
|
||||
if (repoApi_) return repoApi_;
|
||||
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
|
||||
repoApi_ = RepositoryApi.ofDefaultJoplinRepo(Setting.value('tempDir'));
|
||||
// repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir'));
|
||||
return repoApi_;
|
||||
}
|
||||
@ -170,20 +171,6 @@ export default function(props: Props) {
|
||||
};
|
||||
}, [manifestsLoaded, pluginItems, pluginService.appVersion]);
|
||||
|
||||
const onDelete = useCallback(async (event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.manifest.name));
|
||||
if (!confirm) return;
|
||||
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
if (!draft[item.manifest.id]) draft[item.manifest.id] = defaultPluginSetting();
|
||||
draft[item.manifest.id].deleted = true;
|
||||
});
|
||||
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onToggle = useCallback((event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
|
||||
@ -220,9 +207,9 @@ export default function(props: Props) {
|
||||
|
||||
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}, [pluginService, props.onChange]);
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettings, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
|
||||
const onToolsClick = useCallback(async () => {
|
||||
|
@ -8,7 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import PluginBox, { InstallState } from './PluginBox';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Root = styled.div`
|
||||
@ -32,14 +32,6 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
function sortManifestResults(results: PluginManifest[]): PluginManifest[] {
|
||||
return results.sort((m1, m2) => {
|
||||
if (m1._recommended && !m2._recommended) return -1;
|
||||
if (!m1._recommended && m2._recommended) return +1;
|
||||
return m1.name.toLowerCase() < m2.name.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [searchStarted, setSearchStarted] = useState(false);
|
||||
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
||||
@ -57,7 +49,7 @@ export default function(props: Props) {
|
||||
setSearchResultCount(null);
|
||||
} else {
|
||||
const r = await props.repoApi().search(props.searchQuery);
|
||||
setManifests(sortManifestResults(r));
|
||||
setManifests(r);
|
||||
setSearchResultCount(r.length);
|
||||
}
|
||||
});
|
||||
|
@ -1,63 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import PluginService, { defaultPluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import produce from 'immer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ItemEvent } from './PluginBox';
|
||||
|
||||
const logger = Logger.create('useOnInstallHandler');
|
||||
|
||||
export interface OnPluginSettingChangeEvent {
|
||||
value: PluginSettings;
|
||||
}
|
||||
|
||||
type OnPluginSettingChangeHandler = (event: OnPluginSettingChangeEvent)=> void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: OnPluginSettingChangeHandler, isUpdate: boolean) {
|
||||
return useCallback(async (event: ItemEvent) => {
|
||||
const pluginId = event.item.manifest.id;
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: true,
|
||||
};
|
||||
});
|
||||
|
||||
let installError = null;
|
||||
|
||||
try {
|
||||
if (isUpdate) {
|
||||
await PluginService.instance().updatePluginFromRepo(repoApi(), pluginId);
|
||||
} else {
|
||||
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
|
||||
}
|
||||
} catch (error) {
|
||||
installError = error;
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
if (!installError) {
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
draft[pluginId] = defaultPluginSetting();
|
||||
if (isUpdate) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
draft[pluginId].enabled = pluginSettings[pluginId].enabled;
|
||||
}
|
||||
draft[pluginId].hasBeenUpdated = true;
|
||||
}
|
||||
});
|
||||
|
||||
onPluginSettingsChange({ value: newSettings });
|
||||
}
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (installError) alert(_('Could not install plugin: %s', installError.message));
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, onPluginSettingsChange]);
|
||||
}
|
@ -125,7 +125,7 @@ const useEditorCommands = (props: Props) => {
|
||||
}
|
||||
},
|
||||
search: () => {
|
||||
editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
return editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
};
|
||||
}, [
|
||||
|
@ -32,6 +32,7 @@ export default class PlatformImplementation extends BasePlatformImplementation {
|
||||
version: packageInfo.version,
|
||||
syncVersion: Setting.value('syncVersion'),
|
||||
profileVersion: reg.db().version(),
|
||||
platform: 'desktop',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Setting, { AppType } from '../../../models/Setting';
|
||||
import Setting, { AppType, SettingMetadataSection, SettingSectionSource } from '../../../models/Setting';
|
||||
import SyncTargetRegistry from '../../../SyncTargetRegistry';
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
const { _ } = require('../../../locale');
|
||||
@ -127,6 +127,11 @@ export const updateSettingValue = (comp: ConfigScreenComponent, key: string, val
|
||||
changedSettingKeys: changedSettingKeys,
|
||||
};
|
||||
}, callback);
|
||||
|
||||
const metadata = Setting.settingMetadata(key);
|
||||
if (metadata.autoSave) {
|
||||
scheduleSaveSettings(comp);
|
||||
}
|
||||
};
|
||||
|
||||
let scheduleSaveSettingsIID: ReturnType<typeof setTimeout>|null = null;
|
||||
@ -245,10 +250,27 @@ export const settingsSections = createSelector(
|
||||
|
||||
const order = Setting.sectionOrder();
|
||||
|
||||
const sortOrderFor = (section: SettingMetadataSection) => {
|
||||
if (section.source === SettingSectionSource.Plugin) {
|
||||
// Plugins should go after all other sections
|
||||
return order.length + 1;
|
||||
}
|
||||
|
||||
return order.indexOf(section.name);
|
||||
};
|
||||
|
||||
output.sort((a, b) => {
|
||||
const o1 = order.indexOf(a.name);
|
||||
const o2 = order.indexOf(b.name);
|
||||
return o1 < o2 ? -1 : +1;
|
||||
const o1 = sortOrderFor(a);
|
||||
const o2 = sortOrderFor(b);
|
||||
|
||||
if (o1 === o2) {
|
||||
const l1 = Setting.sectionNameToLabel(a.name);
|
||||
const l2 = Setting.sectionNameToLabel(b.name);
|
||||
|
||||
return l1.toLowerCase() < l2.toLowerCase() ? -1 : +1;
|
||||
}
|
||||
|
||||
return o1 - o2;
|
||||
});
|
||||
|
||||
return output;
|
||||
|
23
packages/lib/components/shared/config/plugins/types.ts
Normal file
23
packages/lib/components/shared/config/plugins/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { PluginSettings } from '../../../../services/plugins/PluginService';
|
||||
import { PluginManifest } from '../../../../services/plugins/utils/types';
|
||||
|
||||
|
||||
export interface PluginItem {
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
deleted: boolean;
|
||||
devMode: boolean;
|
||||
builtIn: boolean;
|
||||
hasBeenUpdated: boolean;
|
||||
}
|
||||
|
||||
export interface ItemEvent {
|
||||
item: PluginItem;
|
||||
}
|
||||
|
||||
|
||||
export interface OnPluginSettingChangeEvent {
|
||||
value: PluginSettings;
|
||||
}
|
||||
|
||||
export type OnPluginSettingChangeHandler = (event: OnPluginSettingChangeEvent)=> void;
|
@ -0,0 +1,40 @@
|
||||
import { _ } from '../../../../locale';
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '../../../../services/plugins/PluginService';
|
||||
import shim from '../../../../shim';
|
||||
import produce from 'immer';
|
||||
import { ItemEvent, OnPluginSettingChangeHandler } from './types';
|
||||
|
||||
const useOnDeleteHandler = (
|
||||
pluginSettings: PluginSettings,
|
||||
onSettingsChange: OnPluginSettingChangeHandler,
|
||||
deleteNow: boolean,
|
||||
) => {
|
||||
const React = shim.react();
|
||||
return React.useCallback(async (event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
const confirmed = await shim.showConfirmationDialog(_('Delete plugin "%s"?', item.manifest.name));
|
||||
if (!confirmed) return;
|
||||
|
||||
let newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
if (!draft[item.manifest.id]) draft[item.manifest.id] = defaultPluginSetting();
|
||||
draft[item.manifest.id].deleted = true;
|
||||
});
|
||||
|
||||
if (deleteNow) {
|
||||
const pluginService = PluginService.instance();
|
||||
|
||||
// We first unload the plugin. This is done here rather than in pluginService.uninstallPlugins
|
||||
// because unloadPlugin may not work on desktop.
|
||||
const plugin = pluginService.plugins[item.manifest.id];
|
||||
if (plugin) {
|
||||
await pluginService.unloadPlugin(item.manifest.id);
|
||||
}
|
||||
|
||||
newSettings = await pluginService.uninstallPlugins(newSettings);
|
||||
}
|
||||
|
||||
onSettingsChange({ value: newSettings });
|
||||
}, [pluginSettings, onSettingsChange, deleteNow]);
|
||||
};
|
||||
|
||||
export default useOnDeleteHandler;
|
@ -1,10 +1,10 @@
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import PluginService, { defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { ItemEvent } from './PluginBox';
|
||||
import PluginService, { defaultPluginSetting } from '../../../../services/plugins/PluginService';
|
||||
import { ItemEvent } from './types';
|
||||
|
||||
jest.mock('@joplin/lib/services/plugins/PluginService');
|
||||
jest.mock('../../../../services/plugins/PluginService');
|
||||
|
||||
const pluginServiceInstance = {
|
||||
updatePluginFromRepo: jest.fn(),
|
||||
@ -37,7 +37,7 @@ describe('useOnInstallHandler', () => {
|
||||
beforeAll(() => {
|
||||
(PluginService.instance as jest.Mock).mockReturnValue(pluginServiceInstance);
|
||||
(defaultPluginSetting as jest.Mock).mockImplementation(
|
||||
jest.requireActual('@joplin/lib/services/plugins/PluginService').defaultPluginSetting,
|
||||
jest.requireActual('../../../../services/plugins/PluginService').defaultPluginSetting,
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,75 @@
|
||||
import produce from 'immer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ItemEvent, OnPluginSettingChangeHandler } from './types';
|
||||
import type * as React from 'react';
|
||||
import shim from '../../../../shim';
|
||||
import RepositoryApi from '../../../../services/plugins/RepositoryApi';
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '../../../../services/plugins/PluginService';
|
||||
import { _ } from '../../../../locale';
|
||||
|
||||
const logger = Logger.create('useOnInstallHandler');
|
||||
|
||||
type GetRepoApiCallback = ()=> RepositoryApi;
|
||||
|
||||
const useOnInstallHandler = (
|
||||
setInstallingPluginIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
||||
pluginSettings: PluginSettings,
|
||||
getRepoApi: GetRepoApiCallback|RepositoryApi,
|
||||
onPluginSettingsChange: OnPluginSettingChangeHandler,
|
||||
isUpdate: boolean,
|
||||
) => {
|
||||
const React = shim.react();
|
||||
return React.useCallback(async (event: ItemEvent) => {
|
||||
const pluginId = event.item.manifest.id;
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: true,
|
||||
};
|
||||
});
|
||||
|
||||
let installError = null;
|
||||
|
||||
try {
|
||||
const repoApi = typeof getRepoApi === 'function' ? getRepoApi() : getRepoApi;
|
||||
|
||||
if (isUpdate) {
|
||||
await PluginService.instance().updatePluginFromRepo(repoApi, pluginId);
|
||||
} else {
|
||||
await PluginService.instance().installPluginFromRepo(repoApi, pluginId);
|
||||
}
|
||||
} catch (error) {
|
||||
installError = error;
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
if (!installError) {
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
draft[pluginId] = defaultPluginSetting();
|
||||
if (isUpdate) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
draft[pluginId].enabled = pluginSettings[pluginId].enabled;
|
||||
}
|
||||
draft[pluginId].hasBeenUpdated = true;
|
||||
}
|
||||
});
|
||||
|
||||
onPluginSettingsChange({ value: newSettings });
|
||||
}
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (installError) {
|
||||
await shim.showMessageBox(
|
||||
_('Could not install plugin: %s', installError.message),
|
||||
{ buttons: [_('OK')] },
|
||||
);
|
||||
}
|
||||
}, [getRepoApi, isUpdate, pluginSettings, onPluginSettingsChange, setInstallingPluginIds]);
|
||||
};
|
||||
|
||||
export default useOnInstallHandler;
|
13
packages/lib/hooks/usePrevious.ts
Normal file
13
packages/lib/hooks/usePrevious.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import shim from '../shim';
|
||||
|
||||
const { useRef, useEffect } = shim.react();
|
||||
|
||||
const usePrevious = (value: any, initialValue: any = null) => {
|
||||
const ref = useRef(initialValue);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
export default usePrevious;
|
@ -5,9 +5,12 @@ const nodeSqlite = require('sqlite3');
|
||||
const pdfJs = require('pdfjs-dist');
|
||||
const packageInfo = require('./package.json');
|
||||
|
||||
// Used for testing some shared components
|
||||
const React = require('react');
|
||||
|
||||
require('../../jest.base-setup.js')();
|
||||
|
||||
shimInit({ sharp, nodeSqlite, pdfJs, appVersion: () => packageInfo.version });
|
||||
shimInit({ sharp, nodeSqlite, pdfJs, React, appVersion: () => packageInfo.version });
|
||||
|
||||
global.afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
|
@ -406,4 +406,18 @@ describe('models/Setting', () => {
|
||||
expect(Setting.value('myCustom')).toBe('');
|
||||
});
|
||||
|
||||
test('should not fail Sqlite UNIQUE constraint when re-registering saved settings', async () => {
|
||||
// Re-registering a saved database setting previously caused issues with saving.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await Setting.registerSetting('myCustom', {
|
||||
public: true,
|
||||
value: `${i}`,
|
||||
type: Setting.TYPE_STRING,
|
||||
storage: SettingStorage.Database,
|
||||
});
|
||||
Setting.setValue('myCustom', 'test');
|
||||
await Setting.saveAll();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -227,6 +227,8 @@ export type SettingMetadataSection = {
|
||||
name: string;
|
||||
isScreen?: boolean;
|
||||
metadatas: SettingItem[];
|
||||
|
||||
source?: SettingSectionSource;
|
||||
};
|
||||
export type MetadataBySection = SettingMetadataSection[];
|
||||
|
||||
@ -1933,6 +1935,11 @@ class Setting extends BaseModel {
|
||||
// Reload the value from the database, if it was already present
|
||||
const valueRow = await this.loadOne(key);
|
||||
if (valueRow) {
|
||||
// Remove any duplicate copies of the setting -- if multiple items in cache_
|
||||
// have the same key, we may encounter unique key errors while saving to the
|
||||
// database.
|
||||
this.cache_ = this.cache_.filter(setting => setting.key !== key);
|
||||
|
||||
this.cache_.push({
|
||||
key: key,
|
||||
value: this.formatValue(key, valueRow.value),
|
||||
@ -2271,7 +2278,7 @@ class Setting extends BaseModel {
|
||||
}
|
||||
|
||||
for (const k in enumOptions) {
|
||||
if (!enumOptions.hasOwnProperty(k)) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(enumOptions, k)) continue;
|
||||
if (order.includes(k)) continue;
|
||||
|
||||
output.push({
|
||||
@ -2702,10 +2709,29 @@ class Setting extends BaseModel {
|
||||
'revisionService': _('Toggle note history, keep notes for'),
|
||||
'tools': _('Logs, profiles, sync status'),
|
||||
'export': _('Export your data'),
|
||||
'plugins': _('Enable or disable plugins'),
|
||||
'moreInfo': _('Donate, website'),
|
||||
};
|
||||
|
||||
return sectionNameToSummary[metadata.name] ?? '';
|
||||
// In some cases (e.g. plugin settings pages) there is no preset summary.
|
||||
// In those cases, we generate the summary:
|
||||
const generateSummary = () => {
|
||||
const summary = [];
|
||||
for (const item of metadata.metadatas) {
|
||||
if (!item.public || item.advanced) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.label) {
|
||||
const label = item.label?.();
|
||||
summary.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
return summary.join(', ');
|
||||
};
|
||||
|
||||
return sectionNameToSummary[metadata.name] ?? generateSummary();
|
||||
}
|
||||
|
||||
public static sectionNameToIcon(name: string, appType: AppType) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
"test-ci": "yarn test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@ -29,6 +30,8 @@
|
||||
"clean-html": "1.5.0",
|
||||
"jest": "29.7.0",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "18.2.0",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"sharp": "0.33.2",
|
||||
"tesseract.js": "5.0.4",
|
||||
"typescript": "5.2.2"
|
||||
|
@ -6,7 +6,7 @@ export default class NavService {
|
||||
public static dispatch: Function = () => {};
|
||||
private static handlers_: OnNavigateCallback[] = [];
|
||||
|
||||
public static async go(routeName: string) {
|
||||
public static async go(routeName: string, additionalProps: Record<string, any>|null = null) {
|
||||
if (this.handlers_.length) {
|
||||
const r = await this.handlers_[this.handlers_.length - 1]();
|
||||
if (r) return r;
|
||||
@ -15,6 +15,7 @@ export default class NavService {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: routeName,
|
||||
...additionalProps,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
@ -22,35 +22,35 @@ export interface Joplin {
|
||||
export default class BasePlatformImplementation {
|
||||
|
||||
public get versionInfo(): VersionInfo {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: versionInfo');
|
||||
}
|
||||
|
||||
public get clipboard(): any {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: clipboard');
|
||||
}
|
||||
|
||||
public get nativeImage(): any {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: nativeImage');
|
||||
}
|
||||
|
||||
public get window(): WindowImplementation {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: window');
|
||||
}
|
||||
|
||||
public registerComponent(_name: string, _component: any) {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: registerComponent');
|
||||
}
|
||||
|
||||
public unregisterComponent(_name: string) {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: unregisterComponent');
|
||||
}
|
||||
|
||||
public get joplin(): Joplin {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: joplin');
|
||||
}
|
||||
|
||||
public get imaging(): ImagingImplementation {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: imaging');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ export default abstract class BasePluginRunner extends BaseService {
|
||||
throw new Error(`Not implemented: ${plugin} / ${sandbox}`);
|
||||
}
|
||||
|
||||
public async stop(plugin: Plugin): Promise<void> {
|
||||
throw new Error(`Not implemented ${plugin} stop`);
|
||||
}
|
||||
|
||||
public async waitForSandboxCalls(): Promise<void> {
|
||||
throw new Error('Not implemented: waitForSandboxCalls');
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ export default class Plugin {
|
||||
private contentScriptMessageListeners_: Record<string, Function> = {};
|
||||
private dataDir_: string;
|
||||
private dataDirCreated_ = false;
|
||||
private hasErrors_ = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function, dataDir: string) {
|
||||
@ -97,6 +98,14 @@ export default class Plugin {
|
||||
return Object.keys(this.viewControllers_).length;
|
||||
}
|
||||
|
||||
public get hasErrors(): boolean {
|
||||
return this.hasErrors_;
|
||||
}
|
||||
|
||||
public set hasErrors(hasErrors: boolean) {
|
||||
this.hasErrors_ = hasErrors;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public on(eventName: string, callback: Function) {
|
||||
return this.eventEmitter_.on(eventName, callback);
|
||||
@ -190,4 +199,11 @@ export default class Plugin {
|
||||
return this.contentScriptMessageListeners_[id](message);
|
||||
}
|
||||
|
||||
public onUnload() {
|
||||
this.dispatch_({
|
||||
type: 'PLUGIN_UNLOAD',
|
||||
pluginId: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -144,6 +144,22 @@ export default class PluginService extends BaseService {
|
||||
delete this.plugins_[pluginId];
|
||||
}
|
||||
|
||||
public async unloadPlugin(pluginId: string) {
|
||||
const plugin = this.plugins_[pluginId];
|
||||
if (plugin) {
|
||||
this.logger().info(`Unloading plugin ${pluginId}`);
|
||||
|
||||
plugin.onUnload();
|
||||
await this.runner_.stop(plugin);
|
||||
|
||||
this.deletePluginAt(pluginId);
|
||||
this.startedPlugins_ = { ...this.startedPlugins_ };
|
||||
delete this.startedPlugins_[pluginId];
|
||||
} else {
|
||||
this.logger().info(`Unable to unload plugin ${pluginId} -- already unloaded`);
|
||||
}
|
||||
}
|
||||
|
||||
private async deletePluginFiles(plugin: Plugin) {
|
||||
await shim.fsDriver().remove(plugin.baseDir);
|
||||
}
|
||||
@ -167,7 +183,7 @@ export default class PluginService extends BaseService {
|
||||
return output;
|
||||
}
|
||||
|
||||
public serializePluginSettings(settings: PluginSettings): any {
|
||||
public serializePluginSettings(settings: PluginSettings): string {
|
||||
return JSON.stringify(settings);
|
||||
}
|
||||
|
||||
@ -343,7 +359,7 @@ export default class PluginService extends BaseService {
|
||||
|
||||
private pluginEnabled(settings: PluginSettings, pluginId: string): boolean {
|
||||
if (!settings[pluginId]) return true;
|
||||
return settings[pluginId].enabled !== false;
|
||||
return settings[pluginId].enabled !== false && settings[pluginId].deleted !== true;
|
||||
}
|
||||
|
||||
public callStatsSummary(pluginId: string, duration: number) {
|
||||
@ -407,6 +423,20 @@ export default class PluginService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
public async loadAndRunDevPlugins(settings: PluginSettings) {
|
||||
const devPluginOptions = { devMode: true, builtIn: false };
|
||||
|
||||
if (Setting.value('plugins.devPluginPaths')) {
|
||||
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
|
||||
await this.loadAndRunPlugins(paths, settings, devPluginOptions);
|
||||
}
|
||||
|
||||
// Also load dev plugins that have passed via command line arguments
|
||||
if (Setting.value('startupDevPlugins')) {
|
||||
await this.loadAndRunPlugins(Setting.value('startupDevPlugins'), settings, devPluginOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public isCompatible(pluginVersion: string): boolean {
|
||||
return compareVersions(this.appVersion_, pluginVersion) >= 0;
|
||||
}
|
||||
@ -450,6 +480,7 @@ export default class PluginService extends BaseService {
|
||||
public async installPluginFromRepo(repoApi: RepositoryApi, pluginId: string): Promise<Plugin> {
|
||||
const pluginPath = await repoApi.downloadPlugin(pluginId);
|
||||
const plugin = await this.installPlugin(pluginPath);
|
||||
|
||||
await shim.fsDriver().remove(pluginPath);
|
||||
return plugin;
|
||||
}
|
||||
@ -467,6 +498,13 @@ export default class PluginService extends BaseService {
|
||||
const preloadedPlugin = await this.loadPluginFromPath(jplPath);
|
||||
await this.deletePluginFiles(preloadedPlugin);
|
||||
|
||||
// On mobile, it's necessary to create the plugin directory before we can copy
|
||||
// into it.
|
||||
if (!(await shim.fsDriver().exists(Setting.value('pluginDir')))) {
|
||||
logger.info(`Creating plugin directory: ${Setting.value('pluginDir')}`);
|
||||
await shim.fsDriver().mkdir(Setting.value('pluginDir'));
|
||||
}
|
||||
|
||||
const destPath = `${Setting.value('pluginDir')}/${preloadedPlugin.id}.jpl`;
|
||||
await shim.fsDriver().copy(jplPath, destPath);
|
||||
|
||||
|
@ -68,6 +68,10 @@ export default class RepositoryApi {
|
||||
this.tempDir_ = tempDir;
|
||||
}
|
||||
|
||||
public static ofDefaultJoplinRepo(tempDirPath: string) {
|
||||
return new RepositoryApi('https://github.com/joplin/plugins', tempDirPath);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
// https://github.com/joplin/plugins
|
||||
// https://api.github.com/repos/joplin/plugins/releases
|
||||
@ -183,6 +187,12 @@ export default class RepositoryApi {
|
||||
}
|
||||
}
|
||||
|
||||
output.sort((m1, m2) => {
|
||||
if (m1._recommended && !m2._recommended) return -1;
|
||||
if (!m1._recommended && m2._recommended) return +1;
|
||||
return m1.name.toLowerCase() < m2.name.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,9 @@ export default class WebviewController extends ViewController {
|
||||
private messageListener_: Function = null;
|
||||
private closeResponse_: CloseResponse = null;
|
||||
|
||||
// True if a **panel** is shown in a modal window.
|
||||
private panelInModalMode_ = false;
|
||||
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
super(handle, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
@ -150,8 +153,26 @@ export default class WebviewController extends ViewController {
|
||||
return this.show(false);
|
||||
}
|
||||
|
||||
// This method allows us to determine whether a panel is shown in dialog mode,
|
||||
// which is used on mobile.
|
||||
public setIsShownInModal(shown: boolean) {
|
||||
this.panelInModalMode_ = shown;
|
||||
}
|
||||
|
||||
public get visible(): boolean {
|
||||
const mainLayout = this.store.getState().mainLayout;
|
||||
const appState = this.store.getState();
|
||||
|
||||
if (this.panelInModalMode_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const mainLayout = appState.mainLayout;
|
||||
|
||||
// Mobile: There is no appState.mainLayout
|
||||
if (!mainLayout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const item = findItemByKey(mainLayout, this.handle);
|
||||
return item ? item.visible : false;
|
||||
}
|
||||
|
@ -55,8 +55,7 @@ import { Command } from './types';
|
||||
export default class JoplinCommands {
|
||||
|
||||
/**
|
||||
* <span class="platform-desktop">desktop</span> Executes the given
|
||||
* command.
|
||||
* Executes the given command.
|
||||
*
|
||||
* The command can take any number of arguments, and the supported
|
||||
* arguments will vary based on the command. For custom commands, this
|
||||
@ -78,7 +77,7 @@ export default class JoplinCommands {
|
||||
}
|
||||
|
||||
/**
|
||||
* <span class="platform-desktop">desktop</span> Registers a new command.
|
||||
* Registers a new command.
|
||||
*
|
||||
* ```typescript
|
||||
* // Register a new commmand called "testCommand1"
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import shim from '../../../shim';
|
||||
import Plugin from '../Plugin';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
export interface Implementation {
|
||||
injectCustomStyles(elementId: string, cssFilePath: string): Promise<void>;
|
||||
@ -36,7 +36,7 @@ export default class JoplinWindow {
|
||||
* for an example.
|
||||
*/
|
||||
public async loadNoteCssFile(filePath: string) {
|
||||
const cssString = await fs.readFile(filePath, 'utf8');
|
||||
const cssString = await shim.fsDriver().readFile(filePath, 'utf8');
|
||||
|
||||
this.store_.dispatch({
|
||||
type: 'CUSTOM_CSS_APPEND',
|
||||
|
@ -227,6 +227,8 @@ export interface VersionInfo {
|
||||
version: string;
|
||||
profileVersion: number;
|
||||
syncVersion: number;
|
||||
|
||||
platform: 'desktop'|'mobile';
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { Draft } from 'immer';
|
||||
import { ContainerType } from './WebviewController';
|
||||
import { ButtonSpec } from './api/types';
|
||||
|
||||
export interface ViewInfo {
|
||||
view: any;
|
||||
plugin: any;
|
||||
}
|
||||
|
||||
interface PluginViewState {
|
||||
export interface PluginViewState {
|
||||
id: string;
|
||||
type: string;
|
||||
opened: boolean;
|
||||
buttons: ButtonSpec[];
|
||||
fitToContent?: boolean;
|
||||
scripts?: string[];
|
||||
html?: string;
|
||||
commandName?: string;
|
||||
location?: string;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
interface PluginViewStates {
|
||||
@ -29,6 +34,11 @@ interface PluginState {
|
||||
views: PluginViewStates;
|
||||
}
|
||||
|
||||
export interface ViewInfo {
|
||||
view: PluginViewState;
|
||||
plugin: PluginState;
|
||||
}
|
||||
|
||||
export interface PluginStates {
|
||||
[key: string]: PluginState;
|
||||
}
|
||||
@ -181,6 +191,10 @@ const reducer = (draftRoot: Draft<any>, action: any) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'PLUGIN_UNLOAD':
|
||||
delete draft.plugins[action.pluginId];
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In plugin reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
|
@ -83,7 +83,6 @@ async function requestNoteToNote(requestNote: RequestNote): Promise<NoteEntity>
|
||||
if (requestNote.body_html) {
|
||||
if (requestNote.convert_to === 'html') {
|
||||
const style = await buildNoteStyleSheet(requestNote.stylesheets);
|
||||
const minify = require('html-minifier').minify;
|
||||
|
||||
const minifyOptions = {
|
||||
// Remove all spaces and, especially, newlines from tag attributes, as that would
|
||||
@ -106,6 +105,9 @@ async function requestNoteToNote(requestNote: RequestNote): Promise<NoteEntity>
|
||||
const styleTag = style.length ? `<style>${styleString}</style>` + '\n' : '';
|
||||
let minifiedHtml = '';
|
||||
try {
|
||||
// We use requireDynamic here -- html-minifier seems to not work in environments
|
||||
// that lack `fs`.
|
||||
const minify = shim.requireDynamic('html-minifier').minify;
|
||||
minifiedHtml = minify(requestNote.body_html, minifyOptions);
|
||||
} catch (error) {
|
||||
console.warn('Could not minify HTML - using non-minified HTML instead', error);
|
||||
|
@ -195,7 +195,7 @@ function shimInit(options: ShimInitOptions = null) {
|
||||
}
|
||||
};
|
||||
|
||||
shim.showMessageBox = (message, options = null) => {
|
||||
shim.showMessageBox = async (message, options = null) => {
|
||||
if (shim.isElectron()) {
|
||||
return shim.electronBridge().showMessageBox(message, options);
|
||||
} else {
|
||||
@ -253,7 +253,7 @@ function shimInit(options: ShimInitOptions = null) {
|
||||
if (canResize) {
|
||||
if (resizeLargeImages === 'alwaysAsk') {
|
||||
const Yes = 0, No = 1, Cancel = 2;
|
||||
const userAnswer = shim.showMessageBox(`${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', image.width, image.height, maxDim)}\n\n${_('(You may disable this prompt in the options)')}`, {
|
||||
const userAnswer = await shim.showMessageBox(`${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', image.width, image.height, maxDim)}\n\n${_('(You may disable this prompt in the options)')}`, {
|
||||
buttons: [_('Yes'), _('No'), _('Cancel')],
|
||||
});
|
||||
if (userAnswer === Yes) return await saveResizedImage();
|
||||
|
@ -38,7 +38,7 @@ const shim = {
|
||||
proxyAgent: null as any,
|
||||
|
||||
electronBridge: (): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: electronBridge');
|
||||
},
|
||||
|
||||
msleep_: (ms: number) => {
|
||||
@ -215,7 +215,7 @@ const shim = {
|
||||
},
|
||||
|
||||
fetch: (_url: string, _options: any = null): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: fetch');
|
||||
},
|
||||
|
||||
fetchText: async (url: string, options: any = null): Promise<string> => {
|
||||
@ -225,56 +225,56 @@ const shim = {
|
||||
},
|
||||
|
||||
createResourceFromPath: async (_filePath: string, _defaultProps: ResourceEntity = null, _options: CreateResourceFromPathOptions = null): Promise<ResourceEntity> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: createResourceFromPath');
|
||||
},
|
||||
|
||||
FormData: typeof FormData !== 'undefined' ? FormData : null,
|
||||
|
||||
fsDriver: (): FsDriverBase => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: fsDriver');
|
||||
},
|
||||
|
||||
FileApiDriverLocal: null as any,
|
||||
|
||||
readLocalFileBase64: (_path: string): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: readLocalFileBase64');
|
||||
},
|
||||
|
||||
uploadBlob: (_url: string, _options: any): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: uploadBlob');
|
||||
},
|
||||
|
||||
sjclModule: null as any,
|
||||
|
||||
randomBytes: async (_count: number): Promise<any> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: randomBytes');
|
||||
},
|
||||
|
||||
stringByteLength: (_s: string): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: stringByteLength');
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
detectAndSetLocale: null as Function,
|
||||
|
||||
attachFileToNote: async (_note: any, _filePath: string): Promise<NoteEntity> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: attachFileToNote');
|
||||
},
|
||||
|
||||
attachFileToNoteBody: async (_body: string, _filePath: string, _position: number, _options: any): Promise<string> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: attachFileToNoteBody');
|
||||
},
|
||||
|
||||
imageToDataUrl: async (_filePath: string, _maxSize = 0): Promise<string> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: imageToDataUrl');
|
||||
},
|
||||
|
||||
imageFromDataUrl: async (_imageDataUrl: string, _filePath: string, _options: any = null): Promise<any> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: imageFromDataUrl');
|
||||
},
|
||||
|
||||
fetchBlob: function(_url: string, _options: any = null): any {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: fetchBlob');
|
||||
},
|
||||
|
||||
// Does not do OCR -- just extracts existing text from a PDF.
|
||||
@ -283,29 +283,29 @@ const shim = {
|
||||
},
|
||||
|
||||
pdfToImages: async (_pdfPath: string, _outputDirectoryPath: string): Promise<string[]> => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: pdfToImages');
|
||||
},
|
||||
|
||||
Buffer: null as any,
|
||||
|
||||
openUrl: (_url: string): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: openUrl');
|
||||
},
|
||||
|
||||
httpAgent: (_url: string): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: httpAgent');
|
||||
},
|
||||
|
||||
openOrCreateFile: (_path: string, _defaultContents: any): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: openOrCreateFile');
|
||||
},
|
||||
|
||||
waitForFrame: (): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: waitForFrame');
|
||||
},
|
||||
|
||||
appVersion: (): any => {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error('Not implemented: appVersion');
|
||||
},
|
||||
|
||||
injectedJs: (_name: string) => '',
|
||||
@ -322,10 +322,17 @@ const shim = {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
showMessageBox: (_message: string, _options: any = null): any => {
|
||||
// Returns the index of the button that was clicked. By default,
|
||||
// 0 -> OK
|
||||
// 1 -> Cancel
|
||||
showMessageBox: (_message: string, _options: any = null): Promise<number> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
showConfirmationDialog: async (message: string): Promise<boolean> => {
|
||||
return await shim.showMessageBox(message) === 0;
|
||||
},
|
||||
|
||||
writeImageToFile: (_image: any, _format: any, _filePath: string): void => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
@ -6846,6 +6846,7 @@ __metadata:
|
||||
"@joplin/turndown": ^4.0.73
|
||||
"@joplin/turndown-plugin-gfm": ^1.0.55
|
||||
"@joplin/utils": ~3.0
|
||||
"@testing-library/react-hooks": 8.0.1
|
||||
"@types/fs-extra": 11.0.4
|
||||
"@types/jest": 29.5.8
|
||||
"@types/js-yaml": 4.0.9
|
||||
@ -6894,6 +6895,8 @@ __metadata:
|
||||
promise: 8.3.0
|
||||
query-string: 7.1.3
|
||||
re-reselect: 4.0.1
|
||||
react: 18.2.0
|
||||
react-test-renderer: 18.2.0
|
||||
read-chunk: 2.1.0
|
||||
redux: 4.2.1
|
||||
relative: 3.0.2
|
||||
|
Loading…
x
Reference in New Issue
Block a user