import * as React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { _ } from '@joplin/lib/locale'; import styled from 'styled-components'; import SearchPlugins from './SearchPlugins'; 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, ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types'; import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi'; import Setting, { AppType } from '@joplin/lib/models/Setting'; 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'; const { space } = require('styled-system'); const logger = Logger.create('PluginState'); const maxWidth = 320; const Root = styled.div` display: flex; flex-direction: column; `; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const UserPluginsRoot = styled.div` ${space} display: flex; flex-wrap: wrap; `; const ToolsButton = styled(Button)` margin-right: 6px; `; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const RepoApiErrorMessage = styled(StyledMessage)` max-width: ${props => props.maxWidth}px; margin-bottom: 10px; `; interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied value: any; themeId: number; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied onChange: Function; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied renderLabel: Function; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied renderDescription: Function; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied renderHeader: Function; } let repoApi_: RepositoryApi = null; function repoApi(): RepositoryApi { if (repoApi_) return repoApi_; const appInfo = { type: AppType.Desktop, version: PluginService.instance().appVersion }; repoApi_ = RepositoryApi.ofDefaultJoplinRepo(Setting.value('tempDir'), appInfo, InstallMode.Default); // repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir')); return repoApi_; } function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] { return useMemo(() => { const output: PluginItem[] = []; for (const pluginId in plugins) { const plugin = plugins[pluginId]; const setting: PluginSetting = { ...defaultPluginSetting(), ...settings[pluginId], }; output.push({ manifest: plugin.manifest, installed: true, enabled: setting.enabled, deleted: setting.deleted, devMode: plugin.devMode, builtIn: plugin.builtIn, hasBeenUpdated: setting.hasBeenUpdated, }); } output.sort((a: PluginItem, b: PluginItem) => { return a.manifest.name < b.manifest.name ? -1 : +1; }); return output; }, [plugins, settings]); } export default function(props: Props) { const [searchQuery, setSearchQuery] = useState(''); const [manifestsLoaded, setManifestsLoaded] = useState(false); const [updatingPluginsIds, setUpdatingPluginIds] = useState>({}); const [canBeUpdatedPluginIds, setCanBeUpdatedPluginIds] = useState>({}); const [repoApiError, setRepoApiError] = useState(null); const [fetchManifestTime, setFetchManifestTime] = useState(Date.now()); const pluginService = PluginService.instance(); const pluginSettings = useMemo(() => { return pluginService.unserializePluginSettings(props.value); // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [props.value]); const pluginItems = usePluginItems(pluginService.plugins, pluginSettings); useEffect(() => { let cancelled = false; async function fetchManifests() { setManifestsLoaded(false); setRepoApiError(null); let loadError: Error = null; try { await repoApi().initialize(); } catch (error) { logger.error(error); loadError = error; } if (cancelled) return; if (loadError) { setManifestsLoaded(false); setRepoApiError(loadError); } else { setManifestsLoaded(true); } } void fetchManifests(); return () => { cancelled = true; }; }, [fetchManifestTime]); useEffect(() => { if (!manifestsLoaded) return () => {}; let cancelled = false; async function fetchPluginIds() { // Built-in plugins can't be updated from the main repoApi const nonDefaultPlugins = pluginItems .filter(plugin => !plugin.builtIn) .map(p => p.manifest); const pluginIds = await repoApi().canBeUpdatedPlugins(nonDefaultPlugins); if (cancelled) return; const conv: Record = {}; for (const id of pluginIds) { conv[id] = true; } setCanBeUpdatedPluginIds(conv); } void fetchPluginIds(); return () => { cancelled = true; }; }, [manifestsLoaded, pluginItems, pluginService.appVersion]); const onToggle = useCallback((event: ItemEvent) => { const item = event.item; const newSettings = produce(pluginSettings, (draft: PluginSettings) => { if (!draft[item.manifest.id]) draft[item.manifest.id] = defaultPluginSetting(); draft[item.manifest.id].enabled = !draft[item.manifest.id].enabled; }); 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 onInstall = useCallback(async () => { const result = await bridge().showOpenDialog({ filters: [{ name: 'Joplin Plugin Archive', extensions: ['jpl'] }], }); const filePath = result && result.length ? result[0] : null; if (!filePath) return; const plugin = await pluginService.installPlugin(filePath); const newSettings = produce(pluginSettings, (draft: PluginSettings) => { draft[plugin.manifest.id] = defaultPluginSetting(); }); 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 onBrowsePlugins = useCallback(() => { void bridge().openExternal('https://joplinapp.org/plugins/'); }, []); const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => { props.onChange({ value: pluginService.serializePluginSettings(event.value) }); }, [pluginService, props.onChange]); const pluginSettingsRef = useRef(pluginSettings); pluginSettingsRef.current = pluginSettings; const onDelete = useOnDeleteHandler(pluginSettingsRef, onPluginSettingsChange, false); const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettingsRef, repoApi, onPluginSettingsChange, true); const onToolsClick = useCallback(async () => { const template = [ { label: _('Browse all plugins'), click: onBrowsePlugins, }, { label: _('Install from file'), click: onInstall, }, ]; const menu = bridge().Menu.buildFromTemplate(template); menu.popup({ window: bridge().window() }); }, [onInstall, onBrowsePlugins]); const onSearchQueryChange = useCallback((event: OnChangeEvent) => { setSearchQuery(event.value); }, []); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const onSearchPluginSettingsChange = useCallback((event: any) => { props.onChange({ value: pluginService.serializePluginSettings(event.value) }); // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [props.onChange]); function renderCells(items: PluginItem[]) { const output = []; for (const item of items) { if (item.deleted) continue; const isUpdating = updatingPluginsIds[item.manifest.id]; const onUpdateHandler = canBeUpdatedPluginIds[item.manifest.id] ? onUpdate : null; let updateState = UpdateState.Idle; if (onUpdateHandler) updateState = UpdateState.CanUpdate; if (isUpdating) updateState = UpdateState.Updating; if (item.hasBeenUpdated) updateState = UpdateState.HasBeenUpdated; output.push(); } return output; } function renderUserPlugins(pluginItems: PluginItem[]) { const allDeleted = !pluginItems.find(it => it.deleted !== true); if (!pluginItems.length || allDeleted) { return ( {props.renderDescription(props.themeId, _('You do not have any installed plugin.'))} ); } else { const nonDefaultPlugins = pluginItems.filter(item => !item.builtIn); const defaultPlugins = pluginItems.filter(item => item.builtIn); return ( <> {renderCells(nonDefaultPlugins)} {renderCells(defaultPlugins)} ); } } function renderSearchArea() { return (
); } function renderRepoApiError() { if (!repoApiError) return null; return {_('Could not connect to plugin repository.')}

- { setFetchManifestTime(Date.now()); }}>{_('Try again')}

- {_('Browse all plugins')}
; } function renderBottomArea() { if (searchQuery) return null; return (
{renderRepoApiError()}
{props.renderHeader(props.themeId, _('Manage your plugins'))}
{renderUserPlugins(pluginItems)}
); } return ( {renderSearchArea()} {renderBottomArea()} ); }