mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
feat: new UX for the boards/library manager widgets
Closes #19 Closes #781 Closes #1591 Closes #1607 Closes #1697 Closes #1707 Closes #1924 Closes #1941 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
58aac236bf
commit
2aad0e3b16
@ -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",
|
||||
@ -96,7 +95,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",
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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];
|
||||
|
@ -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<unknown>;
|
||||
onDidFailOpen?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
sketch: Sketch
|
||||
) => MaybePromise<unknown>;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
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 forceUpdate = () =>
|
||||
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;
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
onDidFailOpen: (err) => {
|
||||
this.messageService.error(err.message);
|
||||
forceUpdate();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
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()
|
||||
|
@ -12,7 +12,10 @@ import {
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
} from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import {
|
||||
ListWidget,
|
||||
UserAbortError,
|
||||
} from '../widgets/component-list/list-widget';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
@ -141,6 +144,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
// All
|
||||
installDependencies = true;
|
||||
}
|
||||
} else {
|
||||
throw new UserAbortError();
|
||||
}
|
||||
} else {
|
||||
// The lib does not have any dependencies.
|
||||
@ -235,6 +240,21 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
this.response = 0;
|
||||
super.handleEnter(event);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
let buttonToFocus: HTMLButtonElement | undefined = undefined;
|
||||
for (const child of Array.from(this.controlPanel.children)) {
|
||||
if (child instanceof HTMLButtonElement) {
|
||||
if (child.classList.contains('main')) {
|
||||
buttonToFocus = child;
|
||||
break;
|
||||
}
|
||||
buttonToFocus = child;
|
||||
}
|
||||
}
|
||||
buttonToFocus?.focus();
|
||||
}
|
||||
}
|
||||
export namespace MessageBoxDialog {
|
||||
export interface Options extends DialogProps {
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('fonts/OpenSans-Regular-webfont.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans Bold';
|
||||
src: url('fonts/OpenSans-Bold-webfont.woff') format('woff');
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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. */
|
||||
@ -74,9 +78,9 @@ button.theia-button,
|
||||
.theia-button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: 'Open Sans',sans-serif;
|
||||
font-family: 'Open Sans Bold',sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
@ -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,
|
||||
|
@ -44,102 +44,152 @@
|
||||
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);
|
||||
}
|
||||
|
||||
.component-list-item:hover {
|
||||
cursor: pointer;
|
||||
padding: 20px 15px 25px;
|
||||
}
|
||||
|
||||
.component-list-item .header {
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: var(--theia-statusBar-height);
|
||||
}
|
||||
|
||||
.component-list-item .header .version-info {
|
||||
.component-list-item .header > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.component-list-item .header > div .p-TabBar-toolbar {
|
||||
align-self: start;
|
||||
padding: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
.component-list-item:hover .header > div .p-TabBar-toolbar > div {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.component-list-item .header > div .p-TabBar-toolbar > div {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.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 Bold';
|
||||
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);
|
||||
}
|
||||
|
||||
.component-list-item:hover .header .author {
|
||||
color: var(--theia-foreground);
|
||||
}
|
||||
|
||||
.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-arduino-toolbar-dropdown-option-backgroundHover);
|
||||
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);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:hover:before {
|
||||
background-color: var(--theia-button-foreground);
|
||||
color: var(--theia-button-background);
|
||||
content: attr(uninstall);
|
||||
content: attr(version);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.component-list-item[min-width~="170px"] .footer {
|
||||
padding: 5px 5px 0px 0px;
|
||||
min-height: 35px;
|
||||
.component-list-item .header .installed-version:hover:before {
|
||||
content: attr(remove);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.component-list-item .content {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-direction: column;
|
||||
padding-top: 4px;
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.component-list-item .content > p {
|
||||
margin-block-start: unset;
|
||||
margin-block-end: unset;
|
||||
|
||||
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 +201,33 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
background-color: transparent;
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
.component-list-item .theia-button.secondary.no-border {
|
||||
border: 2px solid var(--theia-button-foreground)
|
||||
}
|
||||
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
|
||||
.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-version:hover:before {
|
||||
background-color: transparent;
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -1,60 +1,76 @@
|
||||
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';
|
||||
import { UserAbortError } from './list-widget';
|
||||
|
||||
export class ComponentListItem<
|
||||
T extends ArduinoComponent
|
||||
> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
|
||||
constructor(props: ComponentListItem.Props<T>) {
|
||||
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,
|
||||
inProgress: this.state.inProgress,
|
||||
install: (item) => this.install(item),
|
||||
uninstall: (item) => this.uninstall(item),
|
||||
onVersionChange: (version) => this.onVersionChange(version),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private async install(item: T): Promise<void> {
|
||||
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<void> {
|
||||
await this.props.uninstall(item);
|
||||
await this.withState('uninstalling', () => this.props.uninstall(item));
|
||||
}
|
||||
|
||||
private async withState(
|
||||
inProgress: 'installing' | 'uninstalling',
|
||||
task: () => Promise<unknown>
|
||||
): Promise<void> {
|
||||
this.setState({ inProgress });
|
||||
try {
|
||||
await task();
|
||||
} catch (err) {
|
||||
if (err instanceof UserAbortError) {
|
||||
// No state update when user cancels the task
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
this.setState({ inProgress: 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 +79,18 @@ export namespace ComponentListItem {
|
||||
readonly item: T;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
readonly onItemEdit: (
|
||||
item: T,
|
||||
selectedVersion: Installable.Version
|
||||
) => void;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
selectedVersion?: Installable.Version;
|
||||
inProgress?: 'installing' | 'uninstalling' | undefined;
|
||||
}
|
||||
}
|
||||
|
@ -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<T>(
|
||||
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<T extends ArduinoComponent> extends React.Component<
|
||||
ComponentList.Props<T>
|
||||
> {
|
||||
private readonly cache: CellMeasurerCache;
|
||||
private resizeAllFlag: boolean;
|
||||
private list: List | undefined;
|
||||
private mostRecentWidth: number | undefined;
|
||||
|
||||
constructor(props: ComponentList.Props<T>) {
|
||||
super(props);
|
||||
this.cache = new CellMeasurerCache({
|
||||
defaultHeight: 140,
|
||||
fixedWidth: true,
|
||||
});
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
|
||||
this.resizeAllFlag = true;
|
||||
setTimeout(() => this.clearAll(), 0);
|
||||
}
|
||||
this.mostRecentWidth = width;
|
||||
return (
|
||||
<List
|
||||
className={'items-container'}
|
||||
rowRenderer={this.createItem}
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={this.props.items.length}
|
||||
rowHeight={this.cache.rowHeight}
|
||||
deferredMeasurementCache={this.cache}
|
||||
ref={this.setListRef}
|
||||
estimatedRowSize={140}
|
||||
// If default value, then `react-virtualized` will optimize and list item will not receive a `:hover` event.
|
||||
// Hence, install and version `<select>` won't be visible even if the mouse cursor is over the `<div>`.
|
||||
// See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42
|
||||
scrollingResetTimeInterval={0}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: ComponentList.Props<T>): 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 (
|
||||
<CellMeasurer
|
||||
cache={this.cache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
rowIndex={index}
|
||||
parent={parent}
|
||||
>
|
||||
{({ registerChild }) => (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<div ref={registerChild} style={style}>
|
||||
<ComponentListItem<T>
|
||||
key={this.props.itemLabel(item)}
|
||||
item={item}
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall}
|
||||
/>
|
||||
</div>
|
||||
<Virtuoso
|
||||
data={this.props.items}
|
||||
itemContent={(_: number, item: T) => (
|
||||
<ComponentListItem<T>
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ComponentList {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly items: T[];
|
||||
@ -150,5 +34,13 @@ export namespace ComponentList {
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
readonly onItemEdit: (
|
||||
item: T,
|
||||
selectedVersion: Installable.Version
|
||||
) => void;
|
||||
}
|
||||
}
|
||||
|
@ -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<T, S>,
|
||||
FilterableListContainer.State<T, S>
|
||||
> {
|
||||
private readonly toDispose: DisposableCollection;
|
||||
|
||||
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
||||
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 (
|
||||
<div className={'filterable-list-container'}>
|
||||
@ -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<void> {
|
||||
private async install(item: T, version: Installable.Version): Promise<void> {
|
||||
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<void> {
|
||||
private async uninstall(item: T): Promise<void> {
|
||||
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<Partial<S> | undefined>;
|
||||
readonly messageService: MessageService;
|
||||
readonly responseService: ResponseServiceClient;
|
||||
readonly onDidShow: Event<void>;
|
||||
readonly install: ({
|
||||
item,
|
||||
progressId,
|
||||
@ -193,5 +213,9 @@ export namespace FilterableListContainer {
|
||||
export interface State<T, S extends Searchable.Options> {
|
||||
searchOptions: S;
|
||||
items: T[];
|
||||
edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,137 +1,783 @@
|
||||
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<void> {
|
||||
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<T extends ArduinoComponent> {
|
||||
readonly item: T;
|
||||
readonly selectedVersion: Installable.Version | undefined;
|
||||
readonly inProgress?: 'installing' | 'uninstalling' | undefined;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
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<T extends ArduinoComponent> {
|
||||
@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<HTMLAnchorElement, Event>
|
||||
): 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<void>,
|
||||
uninstall: (item: T) => Promise<void>,
|
||||
onVersionChange: (version: Installable.Version) => void
|
||||
): React.ReactNode {
|
||||
const { item } = input;
|
||||
let nameAndAuthor: JSX.Element;
|
||||
if (item.name && item.author) {
|
||||
const name = <span className="name">{item.name}</span>;
|
||||
const author = <span className="author">{item.author}</span>;
|
||||
nameAndAuthor = (
|
||||
<span>
|
||||
{name} {nls.localize('arduino/component/by', 'by')} {author}
|
||||
</span>
|
||||
);
|
||||
} 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>;
|
||||
}
|
||||
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'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const summary = <div className="summary">{item.summary}</div>;
|
||||
const description = <div className="summary">{item.description}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && (
|
||||
<a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>
|
||||
{nls.localize('arduino/component/moreInfo', 'More info')}
|
||||
</a>
|
||||
);
|
||||
const onClickInstall = () => install(item);
|
||||
const installButton = item.installable && (
|
||||
<button
|
||||
className="theia-button secondary install uppercase"
|
||||
onClick={onClickInstall}
|
||||
>
|
||||
{nls.localize('arduino/component/install', 'Install')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const version = event.target.value;
|
||||
if (version) {
|
||||
onVersionChange(version);
|
||||
}
|
||||
};
|
||||
|
||||
const versions = (() => {
|
||||
const { availableVersions } = item;
|
||||
if (availableVersions.length === 0) {
|
||||
return undefined;
|
||||
} else if (availableVersions.length === 1) {
|
||||
return <label>{availableVersions[0]}</label>;
|
||||
} else {
|
||||
return (
|
||||
<select
|
||||
className="theia-select"
|
||||
value={input.selectedVersion}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
{item.availableVersions
|
||||
.filter((version) => version !== item.installedVersion) // Filter the version that is currently installed.
|
||||
.map((version) => (
|
||||
<option value={version} key={version}>
|
||||
{version}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
|
||||
const action = this.action(params);
|
||||
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}
|
||||
<>
|
||||
<Separator />
|
||||
<div className="component-list-item noselect">
|
||||
<Header
|
||||
params={params}
|
||||
action={action}
|
||||
services={this.services}
|
||||
onMoreInfo={this.onMoreInfo}
|
||||
/>
|
||||
<Content params={params} onMoreInfo={this.onMoreInfo} />
|
||||
<Footer params={params} action={action} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private action(params: ListItemRendererParams<T>): 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 (
|
||||
<div className="separator">
|
||||
<div />
|
||||
<div className="line" />
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Header<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
services: ListItemRendererServices;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="header">
|
||||
<div>
|
||||
<Title {...this.props} />
|
||||
<Toolbar {...this.props} />
|
||||
</div>
|
||||
<InstalledVersion {...this.props} />
|
||||
</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 + itemBox.height / 2,
|
||||
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), [])
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledVersion<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
}>
|
||||
> {
|
||||
private readonly onClick = (): void => {
|
||||
this.props.params.uninstall(this.props.params.item);
|
||||
};
|
||||
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Content<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
params: {
|
||||
item: { summary, description },
|
||||
},
|
||||
} = this.props;
|
||||
const content = [summary, description].filter(Boolean).join(' ');
|
||||
return (
|
||||
<div className="content" title={content}>
|
||||
<p>{content}</p>
|
||||
<MoreInfo {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MoreInfo<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
params: {
|
||||
item: { moreInfoLink: 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 {
|
||||
return (
|
||||
<div className="footer">
|
||||
<SelectVersion {...this.props} />
|
||||
<Button {...this.props} />
|
||||
</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={selectedVersion}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
{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, inProgress: 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 (
|
||||
<button
|
||||
className={classNames.join(' ')}
|
||||
onClick={onClick}
|
||||
disabled={Boolean(state)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -186,3 +192,10 @@ export namespace ListWidget {
|
||||
readonly defaultSearchOptions: S;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserAbortError extends Error {
|
||||
constructor(message = 'User abort') {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, UserAbortError.prototype);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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:',
|
||||
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
10
i18n/en.json
10
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": {
|
||||
|
@ -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",
|
||||
|
55
yarn.lock
55
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user