diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index ad52691c0..266586083 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -58,7 +58,6 @@ "@types/p-queue": "^2.3.1", "@types/ps-tree": "^1.1.0", "@types/react-tabs": "^2.3.2", - "@types/react-virtualized": "^9.21.21", "@types/temp": "^0.8.34", "@types/which": "^1.3.1", "@vscode/debugprotocol": "^1.51.0", @@ -95,7 +94,6 @@ "react-perfect-scrollbar": "^1.5.8", "react-select": "^5.6.0", "react-tabs": "^3.1.2", - "react-virtualized": "^9.22.3", "react-window": "^1.8.6", "semver": "^7.3.2", "string-natural-compare": "^2.0.3", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 7dd6fc1b9..73ed052e9 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -79,7 +79,10 @@ import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browse import { ProblemManager } from './theia/markers/problem-manager'; import { BoardsAutoInstaller } from './boards/boards-auto-installer'; import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer'; -import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; +import { + ArduinoComponentContextMenuRenderer, + ListItemRenderer, +} from './widgets/component-list/list-item-renderer'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { @@ -1021,4 +1024,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(SidebarBottomMenuWidget).toSelf(); rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget); + + bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts index f516258f6..6d95ea661 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -174,7 +174,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { // CLI returns the packages already sorted with the deprecated ones at the end of the list // in order to ensure the new ones are preferred const candidates = packagesForBoard.filter( - ({ installable, installedVersion }) => installable && !installedVersion + ({ installedVersion }) => !installedVersion ); return candidates[0]; diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index a87c58860..b3d5d82c1 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -1,6 +1,6 @@ import * as PQueue from 'p-queue'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { CommandHandler } from '@theia/core/lib/common/command'; +import { CommandHandler, CommandService } from '@theia/core/lib/common/command'; import { MenuPath, CompositeMenuNode, @@ -11,7 +11,11 @@ import { DisposableCollection, } from '@theia/core/lib/common/disposable'; import { OpenSketch } from './open-sketch'; -import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; +import { + ArduinoMenus, + examplesLabel, + PlaceholderMenuNode, +} from '../menu/arduino-menus'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { ExamplesService } from '../../common/protocol/examples-service'; import { @@ -25,11 +29,73 @@ import { SketchRef, SketchContainer, SketchesError, - Sketch, CoreService, + SketchesService, + Sketch, } from '../../common/protocol'; -import { nls } from '@theia/core/lib/common'; +import { nls } from '@theia/core/lib/common/nls'; import { unregisterSubmenu } from '../menu/arduino-menus'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { ApplicationError } from '@theia/core/lib/common/application-error'; + +/** + * Creates a cloned copy of the example sketch and opens it in a new window. + */ +export async function openClonedExample( + uri: string, + services: { + sketchesService: SketchesService; + commandService: CommandService; + }, + onError: { + onDidFailClone?: ( + err: ApplicationError< + number, + { + uri: string; + } + >, + uri: string + ) => MaybePromise; + onDidFailOpen?: ( + err: ApplicationError< + number, + { + uri: string; + } + >, + sketch: Sketch + ) => MaybePromise; + } = {} +): Promise { + const { sketchesService, commandService } = services; + const { onDidFailClone, onDidFailOpen } = onError; + try { + const sketch = await sketchesService.cloneExample(uri); + try { + await commandService.executeCommand( + OpenSketch.Commands.OPEN_SKETCH.id, + sketch + ); + } catch (openError) { + if (SketchesError.NotFound.is(openError)) { + if (onDidFailOpen) { + await onDidFailOpen(openError, sketch); + return; + } + } + throw openError; + } + } catch (cloneError) { + if (SketchesError.NotFound.is(cloneError)) { + if (onDidFailClone) { + await onDidFailClone(cloneError, uri); + return; + } + } + throw cloneError; + } +} @injectable() export abstract class Examples extends SketchContribution { @@ -94,7 +160,7 @@ export abstract class Examples extends SketchContribution { // TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300 registry.registerSubmenu( ArduinoMenus.FILE__EXAMPLES_SUBMENU, - nls.localize('arduino/examples/menu', 'Examples'), + examplesLabel, { order: '4', } @@ -174,47 +240,33 @@ export abstract class Examples extends SketchContribution { } protected createHandler(uri: string): CommandHandler { + const forceRefresh = () => + this.update({ + board: this.boardsServiceClient.boardsConfig.selectedBoard, + forceRefresh: true, + }); return { execute: async () => { - const sketch = await this.clone(uri); - if (sketch) { - try { - return this.commandService.executeCommand( - OpenSketch.Commands.OPEN_SKETCH.id, - sketch - ); - } catch (err) { - if (SketchesError.NotFound.is(err)) { + await openClonedExample( + uri, + { + sketchesService: this.sketchesService, + commandService: this.commandRegistry, + }, + { + onDidFailClone: () => { // Do not toast the error message. It's handled by the `Open Sketch` command. - this.update({ - board: this.boardsServiceClient.boardsConfig.selectedBoard, - forceRefresh: true, - }); - } else { - throw err; - } + forceRefresh(); + }, + onDidFailOpen: (err) => { + this.messageService.error(err.message); + forceRefresh(); + }, } - } + ); }, }; } - - private async clone(uri: string): Promise { - try { - const sketch = await this.sketchesService.cloneExample(uri); - return sketch; - } catch (err) { - if (SketchesError.NotFound.is(err)) { - this.messageService.error(err.message); - this.update({ - board: this.boardsServiceClient.boardsConfig.selectedBoard, - forceRefresh: true, - }); - } else { - throw err; - } - } - } } @injectable() diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts index 18b52b32c..9ecfec550 100644 --- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts +++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts @@ -1,4 +1,3 @@ -import { isOSX } from '@theia/core/lib/common/os'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { MAIN_MENU_BAR, @@ -7,6 +6,8 @@ import { MenuPath, SubMenuOptions, } from '@theia/core/lib/common/menu'; +import { nls } from '@theia/core/lib/common/nls'; +import { isOSX } from '@theia/core/lib/common/os'; export namespace ArduinoMenus { // Main menu @@ -173,6 +174,17 @@ export namespace ArduinoMenus { '3_sign_out', ]; + // Context menu from the library and boards manager widget + export const ARDUINO_COMPONENT__CONTEXT = ['arduino-component--context']; + export const ARDUINO_COMPONENT__CONTEXT__INFO_GROUP = [ + ...ARDUINO_COMPONENT__CONTEXT, + '0_info', + ]; + export const ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP = [ + ...ARDUINO_COMPONENT__CONTEXT, + '1_action', + ]; + // -- ROOT SSL CERTIFICATES export const ROOT_CERTIFICATES__CONTEXT = [ 'arduino-root-certificates--context', @@ -230,3 +242,5 @@ export class PlaceholderMenuNode implements MenuNode { return [...this.menuPath, 'placeholder'].join('-'); } } + +export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples'); diff --git a/arduino-ide-extension/src/browser/style/boards-config-dialog.css b/arduino-ide-extension/src/browser/style/boards-config-dialog.css index 59633efb4..a7c474e8d 100644 --- a/arduino-ide-extension/src/browser/style/boards-config-dialog.css +++ b/arduino-ide-extension/src/browser/style/boards-config-dialog.css @@ -165,7 +165,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i { border: 1px solid var(--theia-arduino-toolbar-dropdown-border); display: flex; gap: 10px; - height: 28px; + height: var(--arduino-button-height); margin: 0 4px; overflow: hidden; padding: 0 10px; diff --git a/arduino-ide-extension/src/browser/style/dialogs.css b/arduino-ide-extension/src/browser/style/dialogs.css index f48e7e25b..cb73abd60 100644 --- a/arduino-ide-extension/src/browser/style/dialogs.css +++ b/arduino-ide-extension/src/browser/style/dialogs.css @@ -12,7 +12,7 @@ min-width: 424px; max-height: 560px; - padding: 0 28px; + padding: 0 var(--arduino-button-height); } .p-Widget.dialogOverlay .dialogBlock .dialogTitle { @@ -35,7 +35,7 @@ } .p-Widget.dialogOverlay .dialogBlock .dialogContent > input { - margin-bottom: 28px; + margin-bottom: var(--arduino-button-height); } .p-Widget.dialogOverlay .dialogBlock .dialogContent > div { @@ -43,7 +43,7 @@ } .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection { - margin-top: 28px; + margin-top: var(--arduino-button-height); } .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child { margin-top: 0; diff --git a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css index abdf2eec6..5bd5fac44 100644 --- a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css +++ b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css @@ -15,7 +15,7 @@ } .ide-updater-dialog--logo-container { - margin-right: 28px; + margin-right: var(--arduino-button-height); } .ide-updater-dialog--logo { @@ -76,7 +76,7 @@ .ide-updater-dialog .buttons-container { display: flex; justify-content: flex-end; - margin-top: 28px; + margin-top: var(--arduino-button-height); } .ide-updater-dialog .buttons-container a.theia-button { diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 6aa967304..23773df64 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -20,6 +20,10 @@ @import './progress-bar.css'; @import './settings-step-input.css'; +:root { + --arduino-button-height: 28px; +} + /* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */ /* The SVG icons are still part of Theia (1.31.1) */ /* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */ @@ -64,9 +68,9 @@ body.theia-dark { /* Makes the sidepanel a bit wider when opening the widget */ .p-DockPanel-widget { - min-width: 200px; + min-width: 220px; min-height: 20px; - height: 200px; + height: 220px; } /* Overrule the default Theia CSS button styles. */ @@ -95,7 +99,7 @@ button.theia-button, } button.theia-button { - height: 28px; + height: var(--arduino-button-height); max-width: none; } @@ -154,10 +158,6 @@ button.theia-button.message-box-dialog-button { font-size: 14px; } -.uppercase { - text-transform: uppercase; -} - /* High Contrast Theme rules */ /* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/ .hc-black.hc-theia.theia-hc button.theia-button:hover, diff --git a/arduino-ide-extension/src/browser/style/list-widget.css b/arduino-ide-extension/src/browser/style/list-widget.css index c77820d6f..c9c3acf06 100644 --- a/arduino-ide-extension/src/browser/style/list-widget.css +++ b/arduino-ide-extension/src/browser/style/list-widget.css @@ -44,24 +44,8 @@ height: 100%; /* This has top be 100% down to the `scrollContainer`. */ } -.filterable-list-container .items-container > div > div:nth-child(odd) { - background-color: var(--theia-sideBar-background); - filter: contrast(105%); -} - -.filterable-list-container .items-container > div > div:nth-child(even) { - background-color: var(--theia-sideBar-background); - filter: contrast(95%); -} - -.filterable-list-container .items-container > div > div:hover { - background-color: var(--theia-sideBar-background); - filter: contrast(90%); -} - .component-list-item { - padding: 10px 10px 10px 15px; - font-size: var(--theia-ui-font-size1); + padding: 20px 15px 25px; } .component-list-item:hover { @@ -70,22 +54,42 @@ .component-list-item .header { padding-bottom: 2px; +} + +.component-list-item .header > div { display: flex; - flex-direction: column; } -.component-list-item .header .version-info { +.component-list-item .header > div .p-TabBar-toolbar { + align-self: start; + padding: unset; + margin-right: unset; +} + +.component-list-item .header .title { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 auto; +} + +.component-list-item .header .title .name { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + font-size: 14px; +} + +.component-list-item .header .version { display: flex; justify-content: space-between; align-items: center; } -.component-list-item .header .name { - font-weight: bold; -} - .component-list-item .header .author { - font-weight: bold; color: var(--theia-panelTitle-inactiveForeground); } @@ -95,51 +99,101 @@ .component-list-item .header .version { color: var(--theia-panelTitle-inactiveForeground); + padding-top: 4px; } .component-list-item .footer .theia-button.install { height: auto; /* resets the default Theia button height in the filterable list widget */ } -.component-list-item .header .installed:before { - margin-left: 4px; +.component-list-item .header .installed-version:before { + min-width: 79px; display: inline-block; justify-self: end; - background-color: var(--theia-button-background); + text-align: center; + background-color: var(--theia-secondaryButton-hoverBackground); padding: 2px 4px 2px 4px; - font-size: 10px; - font-weight: bold; + font-size: 12px; max-height: calc(1em + 4px); - color: var(--theia-button-foreground); - content: attr(install); + color: var(--theia-titleBar-activeBackground); + content: attr(version); } -.component-list-item .header .installed:hover:before { +.component-list-item .header .installed-version:hover:before { background-color: var(--theia-button-foreground); color: var(--theia-button-background); - content: attr(uninstall); + content: attr(remove); + text-transform: uppercase; + outline-width: 1px; + outline-style: solid; + outline-offset: -1px; + opacity: 1; + outline-color: var(--theia-secondaryButton-hoverBackground); } -.component-list-item[min-width~="170px"] .footer { - padding: 5px 5px 0px 0px; - min-height: 35px; +.component-list-item .content { display: flex; - flex-direction: row-reverse; + flex-direction: column; + padding-top: 4px; +} + +.component-list-item .content > p { + margin-block-start: unset; + margin-block-end: unset; + + font-style: normal; + font-weight: 400; + font-size: 12px; + + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; +} + +.component-list-item .content > .info { + white-space: nowrap; } .component-list-item .footer { flex-direction: column-reverse; + padding-top: 8px; } .component-list-item .footer > * { display: inline-block; - margin: 5px 0px 0px 10px; +} + +.filterable-list-container .separator { + display: flex; + flex-direction: row; +} + +.filterable-list-container .separator :last-child, +.filterable-list-container .separator :first-child { + min-height: 8px; + max-height: 8px; + min-width: 8px; + max-width: 8px; +} + +div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :first-child, +div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :last-child { + display: none; +} + +.filterable-list-container .separator .line { + max-height: 1px; + height: 1px; + background-color: var(--theia-activityBar-inactiveForeground); + flex: 1 1 auto; } .component-list-item:hover .footer > label { display: inline-block; align-self: center; - margin: 5px 0px 0px 10px; } .component-list-item .info a { @@ -151,13 +205,33 @@ text-decoration: underline; } +.component-list-item .theia-button.secondary.no-border { + border: 2px solid var(--theia-button-foreground) +} + +.component-list-item .theia-button.secondary.no-border:hover { + border: 2px solid var(--theia-secondaryButton-foreground) +} + +.component-list-item .theia-button { + margin-left: 12px; +} + +.component-list-item .theia-select { + height: var(--arduino-button-height); + min-height: var(--arduino-button-height); + width: 65px; + min-width: 65px; +} + /* High Contrast Theme rules */ /* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/ -.hc-black.hc-theia.theia-hc .component-list-item .header .installed:hover:before { +.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:hover:before { background-color: transparent; - outline: 1px dashed var(--theia-focusBorder); + outline: 1px dashed var(--theia-focusBorder); } -.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before { +.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:before { + color: var(--theia-button-background); border: 1px solid var(--theia-button-border); } diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index 438277dee..fa98d63f3 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -28,8 +28,8 @@ display: flex; justify-content: center; align-items: center; - height: 28px; - width: 28px; + height: var(--arduino-button-height); + width: var(--arduino-button-height); } .p-TabBar-toolbar .item.arduino-tool-item .arduino-upload-sketch--toolbar, @@ -66,8 +66,8 @@ } .arduino-tool-icon { - height: 28px; - width: 28px; + height: var(--arduino-button-height); + width: var(--arduino-button-height); } .arduino-verify-sketch--toolbar-icon { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx b/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx index 121c01e03..b997fc23c 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx @@ -1,60 +1,69 @@ import * as React from '@theia/core/shared/react'; +import type { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { Installable } from '../../../common/protocol/installable'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; -import { ListItemRenderer } from './list-item-renderer'; +import type { ListItemRenderer } from './list-item-renderer'; export class ComponentListItem< T extends ArduinoComponent > extends React.Component, ComponentListItem.State> { constructor(props: ComponentListItem.Props) { super(props); - if (props.item.installable) { - const version = props.item.availableVersions.filter( - (version) => version !== props.item.installedVersion - )[0]; - this.state = { - selectedVersion: version, - }; - } + this.state = {}; } override render(): React.ReactNode { const { item, itemRenderer } = this.props; + const selectedVersion = + this.props.edited?.item.name === item.name + ? this.props.edited.selectedVersion + : this.latestVersion; return ( <> - {itemRenderer.renderItem( - Object.assign(this.state, { item }), - this.install.bind(this), - this.uninstall.bind(this), - this.onVersionChange.bind(this) - )} + {itemRenderer.renderItem({ + item, + selectedVersion, + state: this.state.state, + install: (item) => this.install(item), + uninstall: (item) => this.uninstall(item), + onVersionChange: (version) => this.onVersionChange(version), + })} ); } private async install(item: T): Promise { - const toInstall = this.state.selectedVersion; - const version = this.props.item.availableVersions.filter( - (version) => version !== this.state.selectedVersion - )[0]; - this.setState({ - selectedVersion: version, - }); - try { - await this.props.install(item, toInstall); - } catch { - this.setState({ - selectedVersion: toInstall, - }); - } + await this.withState('installing', () => + this.props.install( + item, + this.props.edited?.item.name === item.name + ? this.props.edited.selectedVersion + : Installable.latest(this.props.item.availableVersions) + ) + ); } private async uninstall(item: T): Promise { - await this.props.uninstall(item); + await this.withState('uninstalling', () => this.props.uninstall(item)); + } + + private async withState( + state: 'installing' | 'uninstalling', + task: () => Promise + ): Promise { + this.setState({ state }); + try { + await task(); + } finally { + this.setState({ state: undefined }); + } } private onVersionChange(version: Installable.Version): void { - this.setState({ selectedVersion: version }); + this.props.onItemEdit(this.props.item, version); + } + + private get latestVersion(): Installable.Version | undefined { + return Installable.latest(this.props.item.availableVersions); } } @@ -63,10 +72,18 @@ export namespace ComponentListItem { readonly item: T; readonly install: (item: T, version?: Installable.Version) => Promise; readonly uninstall: (item: T) => Promise; + readonly edited?: { + item: T; + selectedVersion: Installable.Version; + }; + readonly onItemEdit: ( + item: T, + selectedVersion: Installable.Version + ) => void; readonly itemRenderer: ListItemRenderer; } export interface State { - selectedVersion?: Installable.Version; + state?: 'installing' | 'uninstalling' | undefined; } } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx b/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx index 0f0dc9430..86f4d3df7 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx @@ -1,148 +1,32 @@ -import 'react-virtualized/styles.css'; import * as React from '@theia/core/shared/react'; -import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; -import { - CellMeasurer, - CellMeasurerCache, -} from 'react-virtualized/dist/commonjs/CellMeasurer'; -import type { - ListRowProps, - ListRowRenderer, -} from 'react-virtualized/dist/commonjs/List'; -import List from 'react-virtualized/dist/commonjs/List'; +import { Virtuoso } from '@theia/core/shared/react-virtuoso'; import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { Installable } from '../../../common/protocol/installable'; import { ComponentListItem } from './component-list-item'; import { ListItemRenderer } from './list-item-renderer'; -function sameAs( - left: T[], - right: T[], - ...compareProps: (keyof T)[] -): boolean { - if (left === right) { - return true; - } - const leftLength = left.length; - if (leftLength !== right.length) { - return false; - } - for (let i = 0; i < leftLength; i++) { - for (const prop of compareProps) { - const leftValue = left[i][prop]; - const rightValue = right[i][prop]; - if (leftValue !== rightValue) { - return false; - } - } - } - return true; -} - export class ComponentList extends React.Component< ComponentList.Props > { - private readonly cache: CellMeasurerCache; - private resizeAllFlag: boolean; - private list: List | undefined; - private mostRecentWidth: number | undefined; - - constructor(props: ComponentList.Props) { - super(props); - this.cache = new CellMeasurerCache({ - defaultHeight: 140, - fixedWidth: true, - }); - } - override render(): React.ReactNode { return ( - - {({ width, height }) => { - if (this.mostRecentWidth && this.mostRecentWidth !== width) { - this.resizeAllFlag = true; - setTimeout(() => this.clearAll(), 0); - } - this.mostRecentWidth = width; - return ( - ` won't be visible even if the mouse cursor is over the `
`. - // See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42 - scrollingResetTimeInterval={0} - /> - ); - }} - - ); - } - - override componentDidUpdate(prevProps: ComponentList.Props): void { - if ( - this.resizeAllFlag || - !sameAs(this.props.items, prevProps.items, 'name', 'installedVersion') - ) { - this.clearAll(true); - } - } - - private readonly setListRef = (ref: List | null): void => { - this.list = ref || undefined; - }; - - private clearAll(scrollToTop = false): void { - this.resizeAllFlag = false; - this.cache.clearAll(); - if (this.list) { - this.list.recomputeRowHeights(); - if (scrollToTop) { - this.list.scrollToPosition(0); - } - } - } - - private readonly createItem: ListRowRenderer = ({ - index, - parent, - key, - style, - }: ListRowProps): React.ReactNode => { - const item = this.props.items[index]; - return ( - - {({ registerChild }) => ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore -
- - key={this.props.itemLabel(item)} - item={item} - itemRenderer={this.props.itemRenderer} - install={this.props.install} - uninstall={this.props.uninstall} - /> -
+ ( + + key={this.props.itemLabel(item)} + item={item} + itemRenderer={this.props.itemRenderer} + install={this.props.install} + uninstall={this.props.uninstall} + edited={this.props.edited} + onItemEdit={this.props.onItemEdit} + /> )} -
+ /> ); - }; + } } - export namespace ComponentList { export interface Props { readonly items: T[]; @@ -150,5 +34,13 @@ export namespace ComponentList { readonly itemRenderer: ListItemRenderer; readonly install: (item: T, version?: Installable.Version) => Promise; readonly uninstall: (item: T) => Promise; + readonly edited?: { + item: T; + selectedVersion: Installable.Version; + }; + readonly onItemEdit: ( + item: T, + selectedVersion: Installable.Version + ) => void; } } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 07a1379ef..05e0e95be 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -15,6 +15,7 @@ import { ListItemRenderer } from './list-item-renderer'; import { ResponseServiceClient } from '../../../common/protocol'; import { nls } from '@theia/core/lib/common'; import { FilterRenderer } from './filter-renderer'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; export class FilterableListContainer< T extends ArduinoComponent, @@ -23,21 +24,30 @@ export class FilterableListContainer< FilterableListContainer.Props, FilterableListContainer.State > { + private readonly toDispose: DisposableCollection; + constructor(props: Readonly>) { super(props); this.state = { searchOptions: props.defaultSearchOptions, items: [], }; + this.toDispose = new DisposableCollection(); } override componentDidMount(): void { this.search = debounce(this.search, 500, { trailing: true }); this.search(this.state.searchOptions); - this.props.searchOptionsDidChange((newSearchOptions) => { - const { searchOptions } = this.state; - this.setSearchOptionsAndUpdate({ ...searchOptions, ...newSearchOptions }); - }); + this.toDispose.pushAll([ + this.props.searchOptionsDidChange((newSearchOptions) => { + const { searchOptions } = this.state; + this.setSearchOptionsAndUpdate({ + ...searchOptions, + ...newSearchOptions, + }); + }), + this.props.onDidShow(() => this.setState({ edited: undefined })), + ]); } override componentDidUpdate(): void { @@ -46,6 +56,10 @@ export class FilterableListContainer< this.props.container.updateScrollBar(); } + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + override render(): React.ReactNode { return (
@@ -90,11 +104,13 @@ export class FilterableListContainer< itemRenderer={itemRenderer} install={this.install.bind(this)} uninstall={this.uninstall.bind(this)} + edited={this.state.edited} + onItemEdit={this.onItemEdit.bind(this)} /> ); } - protected handlePropChange = (prop: keyof S, value: S[keyof S]): void => { + private handlePropChange = (prop: keyof S, value: S[keyof S]): void => { const searchOptions = { ...this.state.searchOptions, [prop]: value, @@ -106,15 +122,14 @@ export class FilterableListContainer< this.setState({ searchOptions }, () => this.search(searchOptions)); } - protected search(searchOptions: S): void { + private search(searchOptions: S): void { const { searchable } = this.props; - searchable.search(searchOptions).then((items) => this.setState({ items })); + searchable + .search(searchOptions) + .then((items) => this.setState({ items, edited: undefined })); } - protected async install( - item: T, - version: Installable.Version - ): Promise { + private async install(item: T, version: Installable.Version): Promise { const { install, searchable } = this.props; await ExecuteWithProgress.doWithProgress({ ...this.props, @@ -124,10 +139,10 @@ export class FilterableListContainer< run: ({ progressId }) => install({ item, progressId, version }), }); const items = await searchable.search(this.state.searchOptions); - this.setState({ items }); + this.setState({ items, edited: undefined }); } - protected async uninstall(item: T): Promise { + private async uninstall(item: T): Promise { const ok = await new ConfirmDialog({ title: nls.localize('arduino/component/uninstall', 'Uninstall'), msg: nls.localize( @@ -152,7 +167,11 @@ export class FilterableListContainer< run: ({ progressId }) => uninstall({ item, progressId }), }); const items = await searchable.search(this.state.searchOptions); - this.setState({ items }); + this.setState({ items, edited: undefined }); + } + + private onItemEdit(item: T, selectedVersion: Installable.Version): void { + this.setState({ edited: { item, selectedVersion } }); } } @@ -171,6 +190,7 @@ export namespace FilterableListContainer { readonly searchOptionsDidChange: Event | undefined>; readonly messageService: MessageService; readonly responseService: ResponseServiceClient; + readonly onDidShow: Event; readonly install: ({ item, progressId, @@ -193,5 +213,9 @@ export namespace FilterableListContainer { export interface State { searchOptions: S; items: T[]; + edited?: { + item: T; + selectedVersion: Installable.Version; + }; } } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx index 4e6d56364..5674e6537 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx @@ -1,137 +1,782 @@ -import * as React from '@theia/core/shared/react'; -import { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationError } from '@theia/core'; +import { + Anchor, + ContextMenuRenderer, +} from '@theia/core/lib/browser/context-menu-renderer'; +import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { codicon } from '@theia/core/lib/browser/widgets/widget'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { Installable } from '../../../common/protocol/installable'; -import { ArduinoComponent } from '../../../common/protocol/arduino-component'; -import { ComponentListItem } from './component-list-item'; -import { nls } from '@theia/core/lib/common'; +import { + CommandHandler, + CommandRegistry, + CommandService, +} from '@theia/core/lib/common/command'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { + MenuModelRegistry, + MenuPath, + SubMenuOptions, +} from '@theia/core/lib/common/menu'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; import { Unknown } from '../../../common/nls'; +import { + CoreService, + ExamplesService, + LibraryPackage, + Sketch, + SketchContainer, + SketchesService, + SketchRef, +} from '../../../common/protocol'; +import type { ArduinoComponent } from '../../../common/protocol/arduino-component'; +import { Installable } from '../../../common/protocol/installable'; +import { openClonedExample } from '../../contributions/examples'; +import { + ArduinoMenus, + examplesLabel, + unregisterSubmenu, +} from '../../menu/arduino-menus'; + +const moreInfoLabel = nls.localize('arduino/component/moreInfo', 'More info'); +const otherVersionsLabel = nls.localize( + 'arduino/component/otherVersions', + 'Other versions' +); +const installLabel = nls.localize('arduino/component/install', 'Install'); +const installLatestLabel = nls.localize( + 'arduino/component/installLatest', + 'Install Latest' +); +function installVersionLabel(selectedVersion: string) { + return nls.localize( + 'arduino/component/installVersion', + 'Install {0}', + selectedVersion + ); +} +const updateLabel = nls.localize('arduino/component/update', 'Update'); +const removeLabel = nls.localize('arduino/component/remove', 'Remove'); +const byLabel = nls.localize('arduino/component/by', 'by'); +function nameAuthorLabel(name: string, author: string) { + return nls.localize('arduino/component/title', '{0} by {1}', name, author); +} +function installedLabel(installedVersion: string) { + return nls.localize( + 'arduino/component/installed', + '{0} installed', + installedVersion + ); +} +function clickToOpenInBrowserLabel(href: string): string | undefined { + return nls.localize( + 'arduino/component/clickToOpen', + 'Click to open in browser: {0}', + href + ); +} + +interface MenuTemplate { + readonly menuLabel: string; +} +interface MenuActionTemplate extends MenuTemplate { + readonly menuPath: MenuPath; + readonly handler: CommandHandler; + /** + * If not defined the insertion oder will be the order string. + */ + readonly order?: string; +} +interface SubmenuTemplate extends MenuTemplate { + readonly menuLabel: string; + readonly submenuPath: MenuPath; + readonly options?: SubMenuOptions; +} +function isMenuTemplate(arg: unknown): arg is MenuTemplate { + return ( + typeof arg === 'object' && + (arg as MenuTemplate).menuLabel !== undefined && + typeof (arg as MenuTemplate).menuLabel === 'string' + ); +} +function isMenuActionTemplate(arg: MenuTemplate): arg is MenuActionTemplate { + return ( + isMenuTemplate(arg) && + (arg as MenuActionTemplate).handler !== undefined && + typeof (arg as MenuActionTemplate).handler === 'object' && + (arg as MenuActionTemplate).menuPath !== undefined && + Array.isArray((arg as MenuActionTemplate).menuPath) + ); +} + +@injectable() +export class ArduinoComponentContextMenuRenderer { + @inject(CommandRegistry) + private readonly commandRegistry: CommandRegistry; + @inject(MenuModelRegistry) + private readonly menuRegistry: MenuModelRegistry; + @inject(ContextMenuRenderer) + private readonly contextMenuRenderer: ContextMenuRenderer; + + private readonly toDisposeBeforeRender = new DisposableCollection(); + private menuIndexCounter = 0; + + async render( + anchor: Anchor, + ...templates: (MenuActionTemplate | SubmenuTemplate)[] + ): Promise { + this.toDisposeBeforeRender.dispose(); + this.toDisposeBeforeRender.pushAll([ + Disposable.create(() => (this.menuIndexCounter = 0)), + ...templates.map((template) => this.registerMenu(template)), + ]); + const options = { + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT, + anchor, + showDisabled: true, + }; + this.contextMenuRenderer.render(options); + } + + private registerMenu( + template: MenuActionTemplate | SubmenuTemplate + ): Disposable { + if (isMenuActionTemplate(template)) { + const { menuLabel, menuPath, handler, order } = template; + const id = this.generateCommandId(menuLabel, menuPath); + const index = this.menuIndexCounter++; + return new DisposableCollection( + this.commandRegistry.registerCommand({ id }, handler), + this.menuRegistry.registerMenuAction(menuPath, { + commandId: id, + label: menuLabel, + order: typeof order === 'string' ? order : String(index).padStart(4), + }) + ); + } else { + const { menuLabel, submenuPath, options } = template; + return new DisposableCollection( + this.menuRegistry.registerSubmenu(submenuPath, menuLabel, options), + Disposable.create(() => + unregisterSubmenu(submenuPath, this.menuRegistry) + ) + ); + } + } + + private generateCommandId(menuLabel: string, menuPath: MenuPath): string { + return `arduino--component-context-${menuPath.join('-')}-${menuLabel}`; + } +} + +interface ListItemRendererParams { + readonly item: T; + readonly selectedVersion: Installable.Version | undefined; + readonly state?: 'installing' | 'uninstalling' | undefined; + readonly install: (item: T) => Promise; + readonly uninstall: (item: T) => Promise; + readonly onVersionChange: (version: Installable.Version) => void; +} + +interface ListItemRendererServices { + readonly windowService: WindowService; + readonly messagesService: MessageService; + readonly commandService: CommandService; + readonly coreService: CoreService; + readonly examplesService: ExamplesService; + readonly sketchesService: SketchesService; + readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer; +} @injectable() export class ListItemRenderer { @inject(WindowService) - protected windowService: WindowService; + private readonly windowService: WindowService; + @inject(MessageService) + private readonly messageService: MessageService; + @inject(CommandService) + private readonly commandService: CommandService; + @inject(CoreService) + private readonly coreService: CoreService; + @inject(ExamplesService) + private readonly examplesService: ExamplesService; + @inject(SketchesService) + private readonly sketchesService: SketchesService; + @inject(ArduinoComponentContextMenuRenderer) + private readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer; - protected onMoreInfoClick = ( - event: React.SyntheticEvent - ): void => { - const { target } = event.nativeEvent; - if (target instanceof HTMLAnchorElement) { - this.windowService.openNewWindow(target.href, { external: true }); - event.nativeEvent.preventDefault(); + private readonly onMoreInfo = (href: string | undefined): void => { + if (href) { + this.windowService.openNewWindow(href, { external: true }); } }; - renderItem( - input: ComponentListItem.State & { item: T }, - install: (item: T) => Promise, - uninstall: (item: T) => Promise, - onVersionChange: (version: Installable.Version) => void - ): React.ReactNode { - const { item } = input; - let nameAndAuthor: JSX.Element; - if (item.name && item.author) { - const name = {item.name}; - const author = {item.author}; - nameAndAuthor = ( - - {name} {nls.localize('arduino/component/by', 'by')} {author} - + renderItem(params: ListItemRendererParams): React.ReactNode { + const action = this.action(params); + return ( + <> + +
+
+ +
+
+ + ); + } + + private action(params: ListItemRendererParams): Installable.Action { + const { + item: { installedVersion, availableVersions }, + selectedVersion, + } = params; + return Installable.action({ + installed: installedVersion, + available: availableVersions, + selected: selectedVersion, + }); + } + + private get services(): ListItemRendererServices { + return { + windowService: this.windowService, + messagesService: this.messageService, + commandService: this.commandService, + coreService: this.coreService, + sketchesService: this.sketchesService, + examplesService: this.examplesService, + contextMenuRenderer: this.contextMenuRenderer, + }; + } +} + +class Separator extends React.Component { + override render(): React.ReactNode { + return ( +
+
+
+
+
+ ); + } +} + +class Header extends React.Component< + Readonly<{ + params: ListItemRendererParams; + action: Installable.Action; + services: ListItemRendererServices; + onMoreInfo: (href: string | undefined) => void; + }> +> { + override render(): React.ReactNode { + const { params, services, onMoreInfo, action } = this.props; + return ( +
+
+ + <Toolbar + params={params} + action={action} + services={services} + onMoreInfo={onMoreInfo} + /> + </div> + <InstalledVersion params={params} /> + </div> + ); + } +} + +class Toolbar<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + action: Installable.Action; + services: ListItemRendererServices; + onMoreInfo: (href: string | undefined) => void; + }> +> { + private readonly onClick = (event: React.MouseEvent): void => { + event.stopPropagation(); + event.preventDefault(); + const anchor = this.toAnchor(event); + this.showContextMenu(anchor); + }; + + override render(): React.ReactNode { + return ( + <div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}> + <div className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}> + <div + id="__more__" + className={codicon('ellipsis', true)} + title={nls.localizeByDefault('More Actions...')} + onClick={this.onClick} + /> + </div> + </div> + ); + } + + private toAnchor(event: React.MouseEvent): Anchor { + const itemBox = event.currentTarget + .closest('.' + TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM) + ?.getBoundingClientRect(); + return itemBox ? { y: itemBox.bottom, x: itemBox.left } : event.nativeEvent; + } + + private async showContextMenu(anchor: Anchor): Promise<void> { + this.props.services.contextMenuRenderer.render( + anchor, + this.moreInfo, + ...(await this.examples), + ...this.otherVersions, + ...this.actions + ); + } + + private get moreInfo(): MenuActionTemplate { + const { + params: { + item: { moreInfoLink }, + }, + } = this.props; + return { + menuLabel: moreInfoLabel, + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT, + handler: { + execute: () => this.props.onMoreInfo(moreInfoLink), + isEnabled: () => Boolean(moreInfoLink), + }, + }; + } + + private get examples(): Promise<(MenuActionTemplate | SubmenuTemplate)[]> { + const { + params: { + item, + item: { installedVersion, name }, + }, + services: { examplesService }, + } = this.props; + // TODO: `LibraryPackage.is` should not be here but it saves one extra `lib list` + // gRPC equivalent call with the name of a platform which will result an empty array. + if (!LibraryPackage.is(item) || !installedVersion) { + return Promise.resolve([]); + } + const submenuPath = [ + ...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT, + 'examples', + ]; + return examplesService.find({ libraryName: name }).then((containers) => [ + { + submenuPath, + menuLabel: examplesLabel, + options: { order: String(0) }, + }, + ...containers + .map((container) => this.flattenContainers(container, submenuPath)) + .reduce((acc, curr) => acc.concat(curr), []), + ]); + } + + private flattenContainers( + container: SketchContainer, + menuPath: MenuPath, + depth = 0 + ): (MenuActionTemplate | SubmenuTemplate)[] { + const templates: (MenuActionTemplate | SubmenuTemplate)[] = []; + const { label } = container; + if (depth > 0) { + menuPath = [...menuPath, label]; + templates.push({ + submenuPath: menuPath, + menuLabel: label, + options: { order: label.toLocaleLowerCase() }, + }); + } + return templates + .concat( + ...container.sketches.map((sketch) => + this.sketchToMenuTemplate(sketch, menuPath) + ) + ) + .concat( + container.children + .map((childContainer) => + this.flattenContainers(childContainer, menuPath, ++depth) + ) + .reduce((acc, curr) => acc.concat(curr), []) ); - } else if (item.name) { - nameAndAuthor = <span className="name">{item.name}</span>; - } else if ((item as any).id) { - nameAndAuthor = <span className="name">{(item as any).id}</span>; - } else { - nameAndAuthor = <span className="name">{Unknown}</span>; + } + + private sketchToMenuTemplate( + sketch: SketchRef, + menuPath: MenuPath + ): MenuActionTemplate { + const { name, uri } = sketch; + const { sketchesService, commandService } = this.props.services; + return { + menuLabel: name, + menuPath, + handler: { + execute: () => + openClonedExample( + uri, + { sketchesService, commandService }, + this.onExampleOpenError + ), + }, + order: name.toLocaleLowerCase(), + }; + } + + private get onExampleOpenError(): { + onDidFailClone: ( + err: ApplicationError<number, unknown>, + uri: string + ) => unknown; + onDidFailOpen: ( + err: ApplicationError<number, unknown>, + sketch: Sketch + ) => unknown; + } { + const { + services: { messagesService, coreService }, + } = this.props; + const handle = async (err: ApplicationError<number, unknown>) => { + messagesService.error(err.message); + return coreService.refresh(); + }; + return { + onDidFailClone: handle, + onDidFailOpen: handle, + }; + } + + private get otherVersions(): (MenuActionTemplate | SubmenuTemplate)[] { + const { + params: { + item: { availableVersions }, + selectedVersion, + onVersionChange, + }, + } = this.props; + const submenuPath = [ + ...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT, + 'other-versions', + ]; + return [ + { + submenuPath, + menuLabel: otherVersionsLabel, + options: { order: String(1) }, + }, + ...availableVersions + .filter((version) => version !== selectedVersion) + .map((version) => ({ + menuPath: submenuPath, + menuLabel: version, + handler: { + execute: () => onVersionChange(version), + }, + })), + ]; + } + + private get actions(): MenuActionTemplate[] { + const { + action, + params: { + item, + item: { availableVersions, installedVersion }, + install, + uninstall, + selectedVersion, + }, + } = this.props; + const removeAction = { + menuLabel: removeLabel, + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP, + handler: { + execute: () => uninstall(item), + }, + }; + const installAction = { + menuLabel: installVersionLabel( + selectedVersion ?? Installable.latest(availableVersions) ?? '' + ), + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP, + handler: { + execute: () => install(item), + }, + }; + const installLatestAction = { + menuLabel: installLatestLabel, + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP, + handler: { + execute: () => install(item), + }, + }; + const updateAction = { + menuLabel: updateLabel, + menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP, + handler: { + execute: () => install(item), + }, + }; + switch (action) { + case 'unknown': + return []; + case 'remove': { + return [removeAction]; + } + case 'update': { + return [removeAction, updateAction]; + } + case 'installLatest': + return [ + ...(Boolean(installedVersion) ? [removeAction] : []), + installLatestAction, + ]; + case 'installSelected': { + return [ + ...(Boolean(installedVersion) ? [removeAction] : []), + installAction, + ]; + } } - const onClickUninstall = () => uninstall(item); - const installedVersion = !!item.installedVersion && ( - <div className="version-info"> - <span className="version"> - {nls.localize( - 'arduino/component/version', - 'Version {0}', - item.installedVersion - )} - </span> - <span - className="installed uppercase" - onClick={onClickUninstall} - {...{ - install: nls.localize('arduino/component/installed', 'Installed'), - uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'), - }} - /> + } +} + +class Title<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + }> +> { + override render(): React.ReactNode { + const { name, author } = this.props.params.item; + const title = + name && author ? nameAuthorLabel(name, author) : name ? name : Unknown; + return ( + <div className="title" title={title}> + {name && author ? ( + <> + {<span className="name">{name}</span>}{' '} + {<span className="author">{`${byLabel} ${author}`}</span>} + </> + ) : name ? ( + <span className="name">{name}</span> + ) : ( + <span className="name">{Unknown}</span> + )} </div> ); + } +} - const summary = <div className="summary">{item.summary}</div>; - const description = <div className="summary">{item.description}</div>; +class InstalledVersion<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + }> +> { + private readonly onClick = (): void => { + this.props.params.uninstall(this.props.params.item); + }; - const moreInfo = !!item.moreInfoLink && ( - <a href={item.moreInfoLink} onClick={this.onMoreInfoClick}> - {nls.localize('arduino/component/moreInfo', 'More info')} - </a> + override render(): React.ReactNode { + const { installedVersion } = this.props.params.item; + return ( + installedVersion && ( + <div className="version"> + <span + className="installed-version" + onClick={this.onClick} + {...{ + version: installedLabel(installedVersion), + remove: removeLabel, + }} + /> + </div> + ) ); - const onClickInstall = () => install(item); - const installButton = item.installable && ( - <button - className="theia-button secondary install uppercase" - onClick={onClickInstall} - > - {nls.localize('arduino/component/install', 'Install')} - </button> + } +} + +class Content<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + onMoreInfo: (href: string | undefined) => void; + }> +> { + override render(): React.ReactNode { + const { + onMoreInfo, + params: { + item: { summary, description, moreInfoLink }, + }, + } = this.props; + const content = [summary, description].filter(Boolean).join(' '); + return ( + <div className="content" title={content}> + <p>{content}</p> + <MoreInfo onMoreInfo={onMoreInfo} href={moreInfoLink} /> + </div> ); + } +} - const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => { - const version = event.target.value; - if (version) { - onVersionChange(version); - } - }; +class MoreInfo extends React.Component< + Readonly<{ + onMoreInfo: (href: string | undefined) => void; + href?: string; + }> +> { + private readonly onClick = ( + event: React.SyntheticEvent<HTMLAnchorElement, Event> + ): void => { + const { target } = event.nativeEvent; + if (target instanceof HTMLAnchorElement) { + this.props.onMoreInfo(target.href); + event.nativeEvent.preventDefault(); + } + }; - const versions = (() => { - const { availableVersions } = item; - if (availableVersions.length === 0) { - return undefined; - } else if (availableVersions.length === 1) { - return <label>{availableVersions[0]}</label>; - } else { + override render(): React.ReactNode { + const { href } = this.props; + return ( + href && ( + <div className="info" title={clickToOpenInBrowserLabel(href)}> + <a href={href} onClick={this.onClick}> + {moreInfoLabel} + </a> + </div> + ) + ); + } +} + +class Footer<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + action: Installable.Action; + }> +> { + override render(): React.ReactNode { + const { action, params } = this.props; + return ( + <div className="footer"> + <SelectVersion params={params} action={action} /> + <Button params={params} action={action} /> + </div> + ); + } +} + +class SelectVersion<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + action: Installable.Action; + }> +> { + private readonly onChange = ( + event: React.ChangeEvent<HTMLSelectElement> + ): void => { + const version = event.target.value; + if (version) { + this.props.params.onVersionChange(version); + } + }; + + override render(): React.ReactNode { + const { + selectedVersion, + item: { availableVersions }, + } = this.props.params; + switch (this.props.action) { + case 'installLatest': // fall-through + case 'installSelected': // fall-through + case 'update': // fall-through + case 'remove': return ( <select className="theia-select" - value={input.selectedVersion} - onChange={onSelectChange} + value={selectedVersion} + onChange={this.onChange} > - {item.availableVersions - .filter((version) => version !== item.installedVersion) // Filter the version that is currently installed. - .map((version) => ( - <option value={version} key={version}> - {version} - </option> - ))} + {availableVersions.map((version) => ( + <option value={version} key={version}> + {version} + </option> + ))} </select> ); - } - })(); + case 'unknown': + return undefined; + } + } +} +class Button<T extends ArduinoComponent> extends React.Component< + Readonly<{ + params: ListItemRendererParams<T>; + action: Installable.Action; + }> +> { + override render(): React.ReactNode { + const { + params: { item, install, uninstall, state }, + } = this.props; + const classNames = ['theia-button install uppercase']; + let onClick; + let label; + switch (this.props.action) { + case 'unknown': + return undefined; + case 'installLatest': { + classNames.push('primary'); + label = installLabel; + onClick = () => install(item); + break; + } + case 'installSelected': { + classNames.push('secondary'); + label = installLabel; + onClick = () => install(item); + break; + } + case 'update': { + classNames.push('secondary'); + label = updateLabel; + onClick = () => install(item); + break; + } + case 'remove': { + classNames.push('secondary', 'no-border'); + label = removeLabel; + onClick = () => uninstall(item); + break; + } + } return ( - <div className="component-list-item noselect"> - <div className="header"> - {nameAndAuthor} - {installedVersion} - </div> - <div className="content"> - {summary} - {description} - </div> - <div className="info">{moreInfo}</div> - <div className="footer"> - {versions} - {installButton} - </div> - </div> + <button + className={classNames.join(' ')} + onClick={onClick} + disabled={Boolean(state)} + > + {label} + </button> ); } } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index a6cf5ffbf..5b730d5ef 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -29,29 +29,27 @@ export abstract class ListWidget< > extends ReactWidget { @inject(MessageService) protected readonly messageService: MessageService; - - @inject(CommandService) - protected readonly commandService: CommandService; - - @inject(ResponseServiceClient) - protected readonly responseService: ResponseServiceClient; - @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; + @inject(CommandService) + private readonly commandService: CommandService; + @inject(ResponseServiceClient) + private readonly responseService: ResponseServiceClient; /** * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. */ - protected focusNode: HTMLElement | undefined; + private focusNode: HTMLElement | undefined; private readonly didReceiveFirstFocus = new Deferred(); - protected readonly searchOptionsChangeEmitter = new Emitter< + private readonly searchOptionsChangeEmitter = new Emitter< Partial<S> | undefined >(); + private readonly onDidShowEmitter = new Emitter<void>(); /** * Instead of running an `update` from the `postConstruct` `init` method, * we use this variable to track first activate, then run. */ - protected firstActivate = true; + private firstUpdate = true; constructor(protected options: ListWidget.Options<T, S>) { super(); @@ -64,7 +62,10 @@ export abstract class ListWidget< this.addClass('arduino-list-widget'); this.node.tabIndex = 0; // To be able to set the focus on the widget. this.scrollOptions = undefined; - this.toDispose.push(this.searchOptionsChangeEmitter); + this.toDispose.pushAll([ + this.searchOptionsChangeEmitter, + this.onDidShowEmitter, + ]); } @postConstruct() @@ -81,12 +82,14 @@ export abstract class ListWidget< protected override onAfterShow(message: Message): void { this.maybeUpdateOnFirstRender(); super.onAfterShow(message); + this.onDidShowEmitter.fire(); } private maybeUpdateOnFirstRender() { - if (this.firstActivate) { - this.firstActivate = false; + if (this.firstUpdate) { + this.firstUpdate = false; this.update(); + this.didReceiveFirstFocus.promise.then(() => this.focusNode?.focus()); } } @@ -106,7 +109,9 @@ export abstract class ListWidget< this.updateScrollBar(); } - protected onFocusResolved = (element: HTMLElement | undefined): void => { + private readonly onFocusResolved = ( + element: HTMLElement | undefined + ): void => { this.focusNode = element; this.didReceiveFirstFocus.resolve(); }; @@ -133,7 +138,7 @@ export abstract class ListWidget< return this.options.installable.uninstall({ item, progressId }); } - render(): React.ReactNode { + override render(): React.ReactNode { return ( <FilterableListContainer<T, S> defaultSearchOptions={this.options.defaultSearchOptions} @@ -149,6 +154,7 @@ export abstract class ListWidget< messageService={this.messageService} commandService={this.commandService} responseService={this.responseService} + onDidShow={this.onDidShowEmitter.event} /> ); } diff --git a/arduino-ide-extension/src/common/protocol/arduino-component.ts b/arduino-ide-extension/src/common/protocol/arduino-component.ts index 20d49f9be..298708e12 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-component.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-component.ts @@ -1,34 +1,35 @@ -import { Installable } from './installable'; +import type { Installable } from './installable'; export interface ArduinoComponent { readonly name: string; - readonly deprecated?: boolean; readonly author: string; readonly summary: string; readonly description: string; - readonly moreInfoLink?: string; readonly availableVersions: Installable.Version[]; - readonly installable: boolean; readonly installedVersion?: Installable.Version; /** * This is the `Type` in IDE (1.x) UI. */ readonly types: string[]; + readonly deprecated?: boolean; + readonly moreInfoLink?: string; } export namespace ArduinoComponent { - export function is(arg: any): arg is ArduinoComponent { + export function is(arg: unknown): arg is ArduinoComponent { return ( - !!arg && - 'name' in arg && - typeof arg['name'] === 'string' && - 'author' in arg && - typeof arg['author'] === 'string' && - 'summary' in arg && - typeof arg['summary'] === 'string' && - 'description' in arg && - typeof arg['description'] === 'string' && - 'installable' in arg && - typeof arg['installable'] === 'boolean' + typeof arg === 'object' && + (<ArduinoComponent>arg).name !== undefined && + typeof (<ArduinoComponent>arg).name === 'string' && + (<ArduinoComponent>arg).author !== undefined && + typeof (<ArduinoComponent>arg).author === 'string' && + (<ArduinoComponent>arg).summary !== undefined && + typeof (<ArduinoComponent>arg).summary === 'string' && + (<ArduinoComponent>arg).description !== undefined && + typeof (<ArduinoComponent>arg).description === 'string' && + (<ArduinoComponent>arg).availableVersions !== undefined && + Array.isArray((<ArduinoComponent>arg).availableVersions) && + (<ArduinoComponent>arg).types !== undefined && + Array.isArray((<ArduinoComponent>arg).types) ); } } diff --git a/arduino-ide-extension/src/common/protocol/examples-service.ts b/arduino-ide-extension/src/common/protocol/examples-service.ts index 8194e57e4..db16a66f0 100644 --- a/arduino-ide-extension/src/common/protocol/examples-service.ts +++ b/arduino-ide-extension/src/common/protocol/examples-service.ts @@ -9,4 +9,8 @@ export interface ExamplesService { current: SketchContainer[]; any: SketchContainer[]; }>; + /** + * Finds example sketch containers for the installed library. + */ + find(options: { libraryName: string }): Promise<SketchContainer[]>; } diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index f311f8202..962f52aec 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -51,6 +51,46 @@ export namespace Installable { }; } + export const ActionLiterals = [ + 'installLatest', + 'installSelected', + 'update', + 'remove', + 'unknown', + ] as const; + export type Action = typeof ActionLiterals[number]; + + export function action(params: { + installed?: Version | undefined; + available: Version[]; + selected?: Version; + }): Action { + const { installed, available } = params; + const latest = Installable.latest(available); + if (!latest || (installed && !available.includes(installed))) { + return 'unknown'; + } + const selected = params.selected ?? latest; + if (installed === selected) { + return 'remove'; + } + if (installed) { + return selected === latest && installed !== latest + ? 'update' + : 'installSelected'; + } else { + return selected === latest ? 'installLatest' : 'installSelected'; + } + } + + export function latest(versions: Version[]): Version | undefined { + if (!versions.length) { + return undefined; + } + const ordered = versions.slice().sort(Installable.Version.COMPARATOR); + return ordered[ordered.length - 1]; + } + export const Installed = <T extends ArduinoComponent>({ installedVersion, }: T): boolean => { diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index 4a20aae21..e8a32d901 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -198,6 +198,10 @@ export namespace LibraryService { export namespace List { export interface Options { readonly fqbn?: string | undefined; + /** + * The name of the library to filter to. + */ + readonly libraryName?: string | undefined; } } } @@ -241,11 +245,15 @@ export interface LibraryPackage extends ArduinoComponent { readonly category: string; } export namespace LibraryPackage { - export function is(arg: any): arg is LibraryPackage { + export function is(arg: unknown): arg is LibraryPackage { return ( ArduinoComponent.is(arg) && - 'includes' in arg && - Array.isArray(arg['includes']) + (<LibraryPackage>arg).includes !== undefined && + Array.isArray((<LibraryPackage>arg).includes) && + (<LibraryPackage>arg).exampleUris !== undefined && + Array.isArray((<LibraryPackage>arg).exampleUris) && + (<LibraryPackage>arg).location !== undefined && + typeof (<LibraryPackage>arg).location === 'number' ); } diff --git a/arduino-ide-extension/src/node/examples-service-impl.ts b/arduino-ide-extension/src/node/examples-service-impl.ts index 63860c555..83cca1e1a 100644 --- a/arduino-ide-extension/src/node/examples-service-impl.ts +++ b/arduino-ide-extension/src/node/examples-service-impl.ts @@ -118,6 +118,16 @@ export class ExamplesServiceImpl implements ExamplesService { return { user, current, any }; } + async find(options: { libraryName: string }): Promise<SketchContainer[]> { + const { libraryName } = options; + const packages = await this.libraryService.list({ libraryName }); + return Promise.all( + packages + .filter(({ location }) => location === LibraryLocation.USER) + .map((pkg) => this.tryGroupExamples(pkg)) + ); + } + /** * The CLI provides direct FS paths to the examples so that menus and menu groups cannot be built for the UI by traversing the * folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index 9d345b2d6..bd5eb8cb1 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -103,7 +103,6 @@ export class LibraryServiceImpl return toLibrary( { name: item.getName(), - installable: true, installedVersion, }, item.getLatest()!, @@ -154,8 +153,10 @@ export class LibraryServiceImpl async list({ fqbn, + libraryName, }: { fqbn?: string | undefined; + libraryName?: string | undefined; }): Promise<LibraryPackage[]> { const coreClient = await this.coreClient; const { client, instance } = coreClient; @@ -166,6 +167,9 @@ export class LibraryServiceImpl req.setAll(true); // https://github.com/arduino/arduino-ide/pull/303#issuecomment-815556447 req.setFqbn(fqbn); } + if (libraryName) { + req.setName(libraryName); + } const resp = await new Promise<LibraryListResponse | undefined>( (resolve, reject) => { @@ -219,7 +223,6 @@ export class LibraryServiceImpl { name: library.getName(), installedVersion, - installable: true, description: library.getSentence(), summary: library.getParagraph(), moreInfoLink: library.getWebsite(), @@ -455,8 +458,7 @@ function toLibrary( return { name: '', exampleUris: [], - installable: false, - location: 0, + location: LibraryLocation.BUILTIN, ...pkg, author: lib.getAuthor(), diff --git a/arduino-ide-extension/src/test/browser/fixtures/boards.ts b/arduino-ide-extension/src/test/browser/fixtures/boards.ts index 0cedb5b77..16256f3ab 100644 --- a/arduino-ide-extension/src/test/browser/fixtures/boards.ts +++ b/arduino-ide-extension/src/test/browser/fixtures/boards.ts @@ -49,7 +49,6 @@ export const aPackage: BoardsPackage = { deprecated: false, description: 'Some Arduino Board, Some Other Arduino Board', id: 'some:arduinoCoreId', - installable: true, moreInfoLink: 'http://www.some-url.lol/', name: 'Some Arduino Package', summary: 'Boards included in this package:', diff --git a/arduino-ide-extension/src/test/common/installable.test.ts b/arduino-ide-extension/src/test/common/installable.test.ts index 4047f37f1..ed89e799a 100644 --- a/arduino-ide-extension/src/test/common/installable.test.ts +++ b/arduino-ide-extension/src/test/common/installable.test.ts @@ -2,6 +2,16 @@ import { expect } from 'chai'; import { Installable } from '../../common/protocol/installable'; describe('installable', () => { + const latest = '2.0.0'; + // shuffled versions + const available: Installable.Version[] = [ + '1.4.1', + '1.0.0', + latest, + '2.0.0-beta.1', + '1.5', + ]; + describe('compare', () => { const testMe = Installable.Version.COMPARATOR; @@ -39,4 +49,93 @@ describe('installable', () => { }); }); }); + + describe('latest', () => { + it('should get the latest version from a shuffled array', () => { + const copy = available.slice(); + expect(Installable.latest(copy)).to.be.equal(latest); + expect(available).to.be.deep.equal(copy); + }); + }); + + describe('action', () => { + const installLatest: Installable.Action = 'installLatest'; + const installSelected: Installable.Action = 'installSelected'; + const update: Installable.Action = 'update'; + const remove: Installable.Action = 'remove'; + const unknown: Installable.Action = 'unknown'; + const notAvailable = '0.0.0'; + + it("should result 'unknown' if available is empty", () => { + expect(Installable.action({ available: [] })).to.be.equal(unknown); + }); + it("should result 'unknown' if installed is not in available", () => { + expect( + Installable.action({ available, installed: notAvailable }) + ).to.be.equal(unknown); + }); + + it("should result 'installLatest' if not installed and not selected", () => { + expect(Installable.action({ available })).to.be.equal(installLatest); + }); + it("should result 'installLatest' if not installed and latest is selected", () => { + expect(Installable.action({ available, selected: latest })).to.be.equal( + installLatest + ); + }); + + it("should result 'installSelected' if not installed and not latest is selected", () => { + available + .filter((version) => version !== latest) + .forEach((selected) => + expect( + Installable.action({ + available, + selected, + }) + ).to.be.equal(installSelected) + ); + }); + it("should result 'installSelected' if installed and the selected is neither the latest nor the installed", () => { + available.forEach((installed) => + available + .filter((selected) => selected !== latest && selected !== installed) + .forEach((selected) => + expect( + Installable.action({ + installed, + available, + selected, + }) + ).to.be.equal(installSelected) + ) + ); + }); + + it("should result 'update' if the installed version is not the latest and the latest is selected", () => { + available + .filter((installed) => installed !== latest) + .forEach((installed) => + expect( + Installable.action({ + installed, + available, + selected: latest, + }) + ).to.be.equal(update) + ); + }); + + it("should result 'remove' if the selected version equals the installed version", () => { + available.forEach((version) => + expect( + Installable.action({ + installed: version, + available, + selected: version, + }) + ).to.be.equal(remove) + ); + }); + }); }); diff --git a/i18n/en.json b/i18n/en.json index e02f7eeb4..9accac660 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -160,13 +160,19 @@ "component": { "boardsIncluded": "Boards included in this package:", "by": "by", + "clickToOpen": "Click to open in browser: {0}", "filterSearch": "Filter your search...", "install": "Install", - "installed": "Installed", + "installLatest": "Install Latest", + "installVersion": "Install {0}", + "installed": "{0} installed", "moreInfo": "More info", + "otherVersions": "Other versions", + "remove": "Remove", + "title": "{0} by {1}", "uninstall": "Uninstall", "uninstallMsg": "Do you want to uninstall {0}?", - "version": "Version {0}" + "update": "Update" }, "configuration": { "cli": { diff --git a/package.json b/package.json index 8267afa70..266f0feac 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,6 @@ "typescript": "~4.5.5", "xhr2": "^0.2.1" }, - "resolutions": { - "@types/react": "18.0.0", - "@types/react-dom": "18.0.0" - }, "scripts": { "prepare": "lerna run prepare && yarn download:plugins", "cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js", diff --git a/yarn.lock b/yarn.lock index e77dd480d..3ed199799 100644 --- a/yarn.lock +++ b/yarn.lock @@ -898,13 +898,6 @@ dependencies: regenerator-runtime "^0.13.10" -"@babel/runtime@^7.7.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" - integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== - dependencies: - regenerator-runtime "^0.13.10" - "@babel/template@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" @@ -3535,10 +3528,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@18.0.0", "@types/react-dom@^18.0.6": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141" - integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg== +"@types/react-dom@^18.0.6": + version "18.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" + integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== dependencies: "@types/react" "*" @@ -3556,14 +3549,6 @@ dependencies: "@types/react" "*" -"@types/react-virtualized@^9.21.21": - version "9.21.21" - resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.21.tgz#65c96f25314f0fb3d40536929dc78112753b49e1" - integrity sha512-Exx6I7p4Qn+BBA1SRyj/UwQlZ0I0Pq7g7uhAp0QQ4JWzZunqEqNBGTmCmMmS/3N9wFgAGWuBD16ap7k8Y14VPA== - dependencies: - "@types/prop-types" "*" - "@types/react" "^17" - "@types/react-window@^1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" @@ -3571,7 +3556,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.0.0", "@types/react@^17", "@types/react@^18.0.15": +"@types/react@*": version "18.0.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.0.tgz#4be8aa3a2d04afc3ac2cc1ca43d39b0bd412890c" integrity sha512-7+K7zEQYu7NzOwQGLR91KwWXXDzmTFODRVizJyIALf6RfLv2GDpqpknX64pvRVILXCpXi7O/pua8NGk44dLvJw== @@ -3580,6 +3565,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.0.15": + version "18.0.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" + integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/request@^2.0.3": version "2.48.8" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" @@ -5542,7 +5536,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -6436,7 +6430,7 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-helpers@^5.0.1, dom-helpers@^5.1.3: +dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== @@ -12322,11 +12316,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - react-markdown@^8.0.0: version "8.0.3" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.3.tgz#e8aba0d2f5a1b2124d476ee1fff9448a2f57e4b3" @@ -12397,18 +12386,6 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-virtualized@^9.22.3: - version "9.22.3" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421" - integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw== - dependencies: - "@babel/runtime" "^7.7.2" - clsx "^1.0.4" - dom-helpers "^5.1.3" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-lifecycles-compat "^3.0.4" - react-virtuoso@^2.17.0: version "2.19.1" resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.19.1.tgz#a660a5c3cafcc7a84b59dfc356e1916e632c1e3a"