mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-15 16:46:32 +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/p-queue": "^2.3.1",
|
||||||
"@types/ps-tree": "^1.1.0",
|
"@types/ps-tree": "^1.1.0",
|
||||||
"@types/react-tabs": "^2.3.2",
|
"@types/react-tabs": "^2.3.2",
|
||||||
"@types/react-virtualized": "^9.21.21",
|
|
||||||
"@types/temp": "^0.8.34",
|
"@types/temp": "^0.8.34",
|
||||||
"@types/which": "^1.3.1",
|
"@types/which": "^1.3.1",
|
||||||
"@vscode/debugprotocol": "^1.51.0",
|
"@vscode/debugprotocol": "^1.51.0",
|
||||||
@ -96,7 +95,6 @@
|
|||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
"react-select": "^5.6.0",
|
"react-select": "^5.6.0",
|
||||||
"react-tabs": "^3.1.2",
|
"react-tabs": "^3.1.2",
|
||||||
"react-virtualized": "^9.22.3",
|
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"string-natural-compare": "^2.0.3",
|
"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 { ProblemManager } from './theia/markers/problem-manager';
|
||||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
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 { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -1021,4 +1024,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
bind(SidebarBottomMenuWidget).toSelf();
|
bind(SidebarBottomMenuWidget).toSelf();
|
||||||
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
|
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
|
// 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
|
// in order to ensure the new ones are preferred
|
||||||
const candidates = packagesForBoard.filter(
|
const candidates = packagesForBoard.filter(
|
||||||
({ installable, installedVersion }) => installable && !installedVersion
|
({ installedVersion }) => !installedVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
return candidates[0];
|
return candidates[0];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as PQueue from 'p-queue';
|
import * as PQueue from 'p-queue';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
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 {
|
import {
|
||||||
MenuPath,
|
MenuPath,
|
||||||
CompositeMenuNode,
|
CompositeMenuNode,
|
||||||
@ -11,7 +11,11 @@ import {
|
|||||||
DisposableCollection,
|
DisposableCollection,
|
||||||
} from '@theia/core/lib/common/disposable';
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { OpenSketch } from './open-sketch';
|
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 { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||||
import {
|
import {
|
||||||
@ -25,11 +29,73 @@ import {
|
|||||||
SketchRef,
|
SketchRef,
|
||||||
SketchContainer,
|
SketchContainer,
|
||||||
SketchesError,
|
SketchesError,
|
||||||
Sketch,
|
|
||||||
CoreService,
|
CoreService,
|
||||||
|
SketchesService,
|
||||||
|
Sketch,
|
||||||
} from '../../common/protocol';
|
} 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 { 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()
|
@injectable()
|
||||||
export abstract class Examples extends SketchContribution {
|
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
|
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||||
registry.registerSubmenu(
|
registry.registerSubmenu(
|
||||||
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||||
nls.localize('arduino/examples/menu', 'Examples'),
|
examplesLabel,
|
||||||
{
|
{
|
||||||
order: '4',
|
order: '4',
|
||||||
}
|
}
|
||||||
@ -174,47 +240,33 @@ export abstract class Examples extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected createHandler(uri: string): CommandHandler {
|
protected createHandler(uri: string): CommandHandler {
|
||||||
|
const forceUpdate = () =>
|
||||||
|
this.update({
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const sketch = await this.clone(uri);
|
await openClonedExample(
|
||||||
if (sketch) {
|
uri,
|
||||||
try {
|
{
|
||||||
return this.commandService.executeCommand(
|
sketchesService: this.sketchesService,
|
||||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
commandService: this.commandRegistry,
|
||||||
sketch
|
},
|
||||||
);
|
{
|
||||||
} catch (err) {
|
onDidFailClone: () => {
|
||||||
if (SketchesError.NotFound.is(err)) {
|
|
||||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||||
this.update({
|
forceUpdate();
|
||||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
},
|
||||||
forceRefresh: true,
|
onDidFailOpen: (err) => {
|
||||||
});
|
this.messageService.error(err.message);
|
||||||
} else {
|
forceUpdate();
|
||||||
throw err;
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
@injectable()
|
||||||
|
@ -12,7 +12,10 @@ import {
|
|||||||
LibrarySearch,
|
LibrarySearch,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
} from '../../common/protocol/library-service';
|
} 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 { Installable } from '../../common/protocol';
|
||||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
@ -141,6 +144,8 @@ export class LibraryListWidget extends ListWidget<
|
|||||||
// All
|
// All
|
||||||
installDependencies = true;
|
installDependencies = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new UserAbortError();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The lib does not have any dependencies.
|
// The lib does not have any dependencies.
|
||||||
@ -235,6 +240,21 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
|||||||
this.response = 0;
|
this.response = 0;
|
||||||
super.handleEnter(event);
|
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 namespace MessageBoxDialog {
|
||||||
export interface Options extends DialogProps {
|
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 { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import {
|
import {
|
||||||
MAIN_MENU_BAR,
|
MAIN_MENU_BAR,
|
||||||
@ -7,6 +6,8 @@ import {
|
|||||||
MenuPath,
|
MenuPath,
|
||||||
SubMenuOptions,
|
SubMenuOptions,
|
||||||
} from '@theia/core/lib/common/menu';
|
} 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 {
|
export namespace ArduinoMenus {
|
||||||
// Main menu
|
// Main menu
|
||||||
@ -173,6 +174,17 @@ export namespace ArduinoMenus {
|
|||||||
'3_sign_out',
|
'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
|
// -- ROOT SSL CERTIFICATES
|
||||||
export const ROOT_CERTIFICATES__CONTEXT = [
|
export const ROOT_CERTIFICATES__CONTEXT = [
|
||||||
'arduino-root-certificates--context',
|
'arduino-root-certificates--context',
|
||||||
@ -230,3 +242,5 @@ export class PlaceholderMenuNode implements MenuNode {
|
|||||||
return [...this.menuPath, 'placeholder'].join('-');
|
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);
|
border: 1px solid var(--theia-arduino-toolbar-dropdown-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 28px;
|
height: var(--arduino-button-height);
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
min-width: 424px;
|
min-width: 424px;
|
||||||
max-height: 560px;
|
max-height: 560px;
|
||||||
padding: 0 28px;
|
padding: 0 var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
|
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
|
||||||
@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent > input {
|
.p-Widget.dialogOverlay .dialogBlock .dialogContent > input {
|
||||||
margin-bottom: 28px;
|
margin-bottom: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
|
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
|
||||||
@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
|
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
|
||||||
margin-top: 28px;
|
margin-top: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
|
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
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');
|
src: url('fonts/OpenSans-Bold-webfont.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ide-updater-dialog--logo-container {
|
.ide-updater-dialog--logo-container {
|
||||||
margin-right: 28px;
|
margin-right: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-updater-dialog--logo {
|
.ide-updater-dialog--logo {
|
||||||
@ -76,7 +76,7 @@
|
|||||||
.ide-updater-dialog .buttons-container {
|
.ide-updater-dialog .buttons-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 28px;
|
margin-top: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-updater-dialog .buttons-container a.theia-button {
|
.ide-updater-dialog .buttons-container a.theia-button {
|
||||||
|
@ -20,6 +20,10 @@
|
|||||||
@import './progress-bar.css';
|
@import './progress-bar.css';
|
||||||
@import './settings-step-input.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. */
|
/* 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) */
|
/* The SVG icons are still part of Theia (1.31.1) */
|
||||||
/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */
|
/* 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 */
|
/* Makes the sidepanel a bit wider when opening the widget */
|
||||||
.p-DockPanel-widget {
|
.p-DockPanel-widget {
|
||||||
min-width: 200px;
|
min-width: 220px;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
height: 200px;
|
height: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrule the default Theia CSS button styles. */
|
/* Overrule the default Theia CSS button styles. */
|
||||||
@ -74,9 +78,9 @@ button.theia-button,
|
|||||||
.theia-button {
|
.theia-button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: 'Open Sans',sans-serif;
|
font-family: 'Open Sans Bold',sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -95,7 +99,7 @@ button.theia-button,
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.theia-button {
|
button.theia-button {
|
||||||
height: 28px;
|
height: var(--arduino-button-height);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,10 +158,6 @@ button.theia-button.message-box-dialog-button {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uppercase {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* High Contrast Theme rules */
|
/* High Contrast Theme rules */
|
||||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
/* 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,
|
.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`. */
|
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 {
|
.component-list-item {
|
||||||
padding: 10px 10px 10px 15px;
|
padding: 20px 15px 25px;
|
||||||
font-size: var(--theia-ui-font-size1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-list-item:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .header {
|
.component-list-item .header {
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
display: flex;
|
min-height: var(--theia-statusBar-height);
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .header .name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-list-item .header .author {
|
.component-list-item .header .author {
|
||||||
font-weight: bold;
|
|
||||||
color: var(--theia-panelTitle-inactiveForeground);
|
color: var(--theia-panelTitle-inactiveForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item:hover .header .author {
|
|
||||||
color: var(--theia-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-list-item .header .version {
|
.component-list-item .header .version {
|
||||||
color: var(--theia-panelTitle-inactiveForeground);
|
color: var(--theia-panelTitle-inactiveForeground);
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .footer .theia-button.install {
|
.component-list-item .footer .theia-button.install {
|
||||||
height: auto; /* resets the default Theia button height in the filterable list widget */
|
height: auto; /* resets the default Theia button height in the filterable list widget */
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .header .installed:before {
|
.component-list-item .header .installed-version:before {
|
||||||
margin-left: 4px;
|
min-width: 79px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
justify-self: end;
|
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;
|
padding: 2px 4px 2px 4px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
|
||||||
max-height: calc(1em + 4px);
|
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);
|
color: var(--theia-button-background);
|
||||||
content: attr(uninstall);
|
content: attr(version);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item[min-width~="170px"] .footer {
|
.component-list-item .header .installed-version:hover:before {
|
||||||
padding: 5px 5px 0px 0px;
|
content: attr(remove);
|
||||||
min-height: 35px;
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-list-item .content {
|
||||||
display: flex;
|
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 {
|
.component-list-item .footer {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .footer > * {
|
.component-list-item .footer > * {
|
||||||
display: inline-block;
|
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 {
|
.component-list-item:hover .footer > label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 5px 0px 0px 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list-item .info a {
|
.component-list-item .info a {
|
||||||
@ -151,13 +201,33 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* High Contrast Theme rules */
|
.component-list-item .theia-button.secondary.no-border {
|
||||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
border: 2px solid var(--theia-button-foreground)
|
||||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:hover:before {
|
|
||||||
background-color: transparent;
|
|
||||||
outline: 1px dashed var(--theia-focusBorder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
border: 1px solid var(--theia-button-border);
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 28px;
|
height: var(--arduino-button-height);
|
||||||
width: 28px;
|
width: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-TabBar-toolbar .item.arduino-tool-item .arduino-upload-sketch--toolbar,
|
.p-TabBar-toolbar .item.arduino-tool-item .arduino-upload-sketch--toolbar,
|
||||||
@ -66,8 +66,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arduino-tool-icon {
|
.arduino-tool-icon {
|
||||||
height: 28px;
|
height: var(--arduino-button-height);
|
||||||
width: 28px;
|
width: var(--arduino-button-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arduino-verify-sketch--toolbar-icon {
|
.arduino-verify-sketch--toolbar-icon {
|
||||||
|
@ -1,60 +1,76 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
|
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||||
import { Installable } from '../../../common/protocol/installable';
|
import { Installable } from '../../../common/protocol/installable';
|
||||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
import type { ListItemRenderer } from './list-item-renderer';
|
||||||
import { ListItemRenderer } from './list-item-renderer';
|
import { UserAbortError } from './list-widget';
|
||||||
|
|
||||||
export class ComponentListItem<
|
export class ComponentListItem<
|
||||||
T extends ArduinoComponent
|
T extends ArduinoComponent
|
||||||
> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
|
> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
|
||||||
constructor(props: ComponentListItem.Props<T>) {
|
constructor(props: ComponentListItem.Props<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
if (props.item.installable) {
|
this.state = {};
|
||||||
const version = props.item.availableVersions.filter(
|
|
||||||
(version) => version !== props.item.installedVersion
|
|
||||||
)[0];
|
|
||||||
this.state = {
|
|
||||||
selectedVersion: version,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
const { item, itemRenderer } = this.props;
|
const { item, itemRenderer } = this.props;
|
||||||
|
const selectedVersion =
|
||||||
|
this.props.edited?.item.name === item.name
|
||||||
|
? this.props.edited.selectedVersion
|
||||||
|
: this.latestVersion;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{itemRenderer.renderItem(
|
{itemRenderer.renderItem({
|
||||||
Object.assign(this.state, { item }),
|
item,
|
||||||
this.install.bind(this),
|
selectedVersion,
|
||||||
this.uninstall.bind(this),
|
inProgress: this.state.inProgress,
|
||||||
this.onVersionChange.bind(this)
|
install: (item) => this.install(item),
|
||||||
)}
|
uninstall: (item) => this.uninstall(item),
|
||||||
|
onVersionChange: (version) => this.onVersionChange(version),
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async install(item: T): Promise<void> {
|
private async install(item: T): Promise<void> {
|
||||||
const toInstall = this.state.selectedVersion;
|
await this.withState('installing', () =>
|
||||||
const version = this.props.item.availableVersions.filter(
|
this.props.install(
|
||||||
(version) => version !== this.state.selectedVersion
|
item,
|
||||||
)[0];
|
this.props.edited?.item.name === item.name
|
||||||
this.setState({
|
? this.props.edited.selectedVersion
|
||||||
selectedVersion: version,
|
: Installable.latest(this.props.item.availableVersions)
|
||||||
});
|
)
|
||||||
try {
|
);
|
||||||
await this.props.install(item, toInstall);
|
|
||||||
} catch {
|
|
||||||
this.setState({
|
|
||||||
selectedVersion: toInstall,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uninstall(item: T): Promise<void> {
|
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 {
|
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 item: T;
|
||||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||||
readonly uninstall: (item: T) => 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>;
|
readonly itemRenderer: ListItemRenderer<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
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 * as React from '@theia/core/shared/react';
|
||||||
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
|
import { Virtuoso } from '@theia/core/shared/react-virtuoso';
|
||||||
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 { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||||
import { Installable } from '../../../common/protocol/installable';
|
import { Installable } from '../../../common/protocol/installable';
|
||||||
import { ComponentListItem } from './component-list-item';
|
import { ComponentListItem } from './component-list-item';
|
||||||
import { ListItemRenderer } from './list-item-renderer';
|
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<
|
export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||||
ComponentList.Props<T>
|
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 {
|
override render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ width, height }) => {
|
data={this.props.items}
|
||||||
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
|
itemContent={(_: number, item: T) => (
|
||||||
this.resizeAllFlag = true;
|
<ComponentListItem<T>
|
||||||
setTimeout(() => this.clearAll(), 0);
|
key={this.props.itemLabel(item)}
|
||||||
}
|
item={item}
|
||||||
this.mostRecentWidth = width;
|
itemRenderer={this.props.itemRenderer}
|
||||||
return (
|
install={this.props.install}
|
||||||
<List
|
uninstall={this.props.uninstall}
|
||||||
className={'items-container'}
|
edited={this.props.edited}
|
||||||
rowRenderer={this.createItem}
|
onItemEdit={this.props.onItemEdit}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</CellMeasurer>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace ComponentList {
|
export namespace ComponentList {
|
||||||
export interface Props<T extends ArduinoComponent> {
|
export interface Props<T extends ArduinoComponent> {
|
||||||
readonly items: T[];
|
readonly items: T[];
|
||||||
@ -150,5 +34,13 @@ export namespace ComponentList {
|
|||||||
readonly itemRenderer: ListItemRenderer<T>;
|
readonly itemRenderer: ListItemRenderer<T>;
|
||||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||||
readonly uninstall: (item: T) => 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 { ResponseServiceClient } from '../../../common/protocol';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { FilterRenderer } from './filter-renderer';
|
import { FilterRenderer } from './filter-renderer';
|
||||||
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
|
||||||
export class FilterableListContainer<
|
export class FilterableListContainer<
|
||||||
T extends ArduinoComponent,
|
T extends ArduinoComponent,
|
||||||
@ -23,21 +24,30 @@ export class FilterableListContainer<
|
|||||||
FilterableListContainer.Props<T, S>,
|
FilterableListContainer.Props<T, S>,
|
||||||
FilterableListContainer.State<T, S>
|
FilterableListContainer.State<T, S>
|
||||||
> {
|
> {
|
||||||
|
private readonly toDispose: DisposableCollection;
|
||||||
|
|
||||||
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
searchOptions: props.defaultSearchOptions,
|
searchOptions: props.defaultSearchOptions,
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
this.toDispose = new DisposableCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
this.search = debounce(this.search, 500, { trailing: true });
|
this.search = debounce(this.search, 500, { trailing: true });
|
||||||
this.search(this.state.searchOptions);
|
this.search(this.state.searchOptions);
|
||||||
this.props.searchOptionsDidChange((newSearchOptions) => {
|
this.toDispose.pushAll([
|
||||||
const { searchOptions } = this.state;
|
this.props.searchOptionsDidChange((newSearchOptions) => {
|
||||||
this.setSearchOptionsAndUpdate({ ...searchOptions, ...newSearchOptions });
|
const { searchOptions } = this.state;
|
||||||
});
|
this.setSearchOptionsAndUpdate({
|
||||||
|
...searchOptions,
|
||||||
|
...newSearchOptions,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
this.props.onDidShow(() => this.setState({ edited: undefined })),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentDidUpdate(): void {
|
override componentDidUpdate(): void {
|
||||||
@ -46,6 +56,10 @@ export class FilterableListContainer<
|
|||||||
this.props.container.updateScrollBar();
|
this.props.container.updateScrollBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override componentWillUnmount(): void {
|
||||||
|
this.toDispose.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
override render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className={'filterable-list-container'}>
|
<div className={'filterable-list-container'}>
|
||||||
@ -90,11 +104,13 @@ export class FilterableListContainer<
|
|||||||
itemRenderer={itemRenderer}
|
itemRenderer={itemRenderer}
|
||||||
install={this.install.bind(this)}
|
install={this.install.bind(this)}
|
||||||
uninstall={this.uninstall.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 = {
|
const searchOptions = {
|
||||||
...this.state.searchOptions,
|
...this.state.searchOptions,
|
||||||
[prop]: value,
|
[prop]: value,
|
||||||
@ -106,15 +122,14 @@ export class FilterableListContainer<
|
|||||||
this.setState({ searchOptions }, () => this.search(searchOptions));
|
this.setState({ searchOptions }, () => this.search(searchOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected search(searchOptions: S): void {
|
private search(searchOptions: S): void {
|
||||||
const { searchable } = this.props;
|
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(
|
private async install(item: T, version: Installable.Version): Promise<void> {
|
||||||
item: T,
|
|
||||||
version: Installable.Version
|
|
||||||
): Promise<void> {
|
|
||||||
const { install, searchable } = this.props;
|
const { install, searchable } = this.props;
|
||||||
await ExecuteWithProgress.doWithProgress({
|
await ExecuteWithProgress.doWithProgress({
|
||||||
...this.props,
|
...this.props,
|
||||||
@ -124,10 +139,10 @@ export class FilterableListContainer<
|
|||||||
run: ({ progressId }) => install({ item, progressId, version }),
|
run: ({ progressId }) => install({ item, progressId, version }),
|
||||||
});
|
});
|
||||||
const items = await searchable.search(this.state.searchOptions);
|
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({
|
const ok = await new ConfirmDialog({
|
||||||
title: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
title: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
||||||
msg: nls.localize(
|
msg: nls.localize(
|
||||||
@ -152,7 +167,11 @@ export class FilterableListContainer<
|
|||||||
run: ({ progressId }) => uninstall({ item, progressId }),
|
run: ({ progressId }) => uninstall({ item, progressId }),
|
||||||
});
|
});
|
||||||
const items = await searchable.search(this.state.searchOptions);
|
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 searchOptionsDidChange: Event<Partial<S> | undefined>;
|
||||||
readonly messageService: MessageService;
|
readonly messageService: MessageService;
|
||||||
readonly responseService: ResponseServiceClient;
|
readonly responseService: ResponseServiceClient;
|
||||||
|
readonly onDidShow: Event<void>;
|
||||||
readonly install: ({
|
readonly install: ({
|
||||||
item,
|
item,
|
||||||
progressId,
|
progressId,
|
||||||
@ -193,5 +213,9 @@ export namespace FilterableListContainer {
|
|||||||
export interface State<T, S extends Searchable.Options> {
|
export interface State<T, S extends Searchable.Options> {
|
||||||
searchOptions: S;
|
searchOptions: S;
|
||||||
items: T[];
|
items: T[];
|
||||||
|
edited?: {
|
||||||
|
item: T;
|
||||||
|
selectedVersion: Installable.Version;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,137 +1,783 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import { ApplicationError } from '@theia/core';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
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 { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
import { Installable } from '../../../common/protocol/installable';
|
import {
|
||||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
CommandHandler,
|
||||||
import { ComponentListItem } from './component-list-item';
|
CommandRegistry,
|
||||||
import { nls } from '@theia/core/lib/common';
|
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 { 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()
|
@injectable()
|
||||||
export class ListItemRenderer<T extends ArduinoComponent> {
|
export class ListItemRenderer<T extends ArduinoComponent> {
|
||||||
@inject(WindowService)
|
@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 = (
|
private readonly onMoreInfo = (href: string | undefined): void => {
|
||||||
event: React.SyntheticEvent<HTMLAnchorElement, Event>
|
if (href) {
|
||||||
): void => {
|
this.windowService.openNewWindow(href, { external: true });
|
||||||
const { target } = event.nativeEvent;
|
|
||||||
if (target instanceof HTMLAnchorElement) {
|
|
||||||
this.windowService.openNewWindow(target.href, { external: true });
|
|
||||||
event.nativeEvent.preventDefault();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderItem(
|
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
|
||||||
input: ComponentListItem.State & { item: T },
|
const action = this.action(params);
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component-list-item noselect">
|
<>
|
||||||
<div className="header">
|
<Separator />
|
||||||
{nameAndAuthor}
|
<div className="component-list-item noselect">
|
||||||
{installedVersion}
|
<Header
|
||||||
</div>
|
params={params}
|
||||||
<div className="content">
|
action={action}
|
||||||
{summary}
|
services={this.services}
|
||||||
{description}
|
onMoreInfo={this.onMoreInfo}
|
||||||
</div>
|
/>
|
||||||
<div className="info">{moreInfo}</div>
|
<Content params={params} onMoreInfo={this.onMoreInfo} />
|
||||||
<div className="footer">
|
<Footer params={params} action={action} />
|
||||||
{versions}
|
|
||||||
{installButton}
|
|
||||||
</div>
|
</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>
|
</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 {
|
> extends ReactWidget {
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected readonly messageService: MessageService;
|
protected readonly messageService: MessageService;
|
||||||
|
|
||||||
@inject(CommandService)
|
|
||||||
protected readonly commandService: CommandService;
|
|
||||||
|
|
||||||
@inject(ResponseServiceClient)
|
|
||||||
protected readonly responseService: ResponseServiceClient;
|
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: 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.
|
* 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();
|
private readonly didReceiveFirstFocus = new Deferred();
|
||||||
protected readonly searchOptionsChangeEmitter = new Emitter<
|
private readonly searchOptionsChangeEmitter = new Emitter<
|
||||||
Partial<S> | undefined
|
Partial<S> | undefined
|
||||||
>();
|
>();
|
||||||
|
private readonly onDidShowEmitter = new Emitter<void>();
|
||||||
/**
|
/**
|
||||||
* Instead of running an `update` from the `postConstruct` `init` method,
|
* Instead of running an `update` from the `postConstruct` `init` method,
|
||||||
* we use this variable to track first activate, then run.
|
* we use this variable to track first activate, then run.
|
||||||
*/
|
*/
|
||||||
protected firstActivate = true;
|
private firstUpdate = true;
|
||||||
|
|
||||||
constructor(protected options: ListWidget.Options<T, S>) {
|
constructor(protected options: ListWidget.Options<T, S>) {
|
||||||
super();
|
super();
|
||||||
@ -64,7 +62,10 @@ export abstract class ListWidget<
|
|||||||
this.addClass('arduino-list-widget');
|
this.addClass('arduino-list-widget');
|
||||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||||
this.scrollOptions = undefined;
|
this.scrollOptions = undefined;
|
||||||
this.toDispose.push(this.searchOptionsChangeEmitter);
|
this.toDispose.pushAll([
|
||||||
|
this.searchOptionsChangeEmitter,
|
||||||
|
this.onDidShowEmitter,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
@ -81,12 +82,14 @@ export abstract class ListWidget<
|
|||||||
protected override onAfterShow(message: Message): void {
|
protected override onAfterShow(message: Message): void {
|
||||||
this.maybeUpdateOnFirstRender();
|
this.maybeUpdateOnFirstRender();
|
||||||
super.onAfterShow(message);
|
super.onAfterShow(message);
|
||||||
|
this.onDidShowEmitter.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
private maybeUpdateOnFirstRender() {
|
private maybeUpdateOnFirstRender() {
|
||||||
if (this.firstActivate) {
|
if (this.firstUpdate) {
|
||||||
this.firstActivate = false;
|
this.firstUpdate = false;
|
||||||
this.update();
|
this.update();
|
||||||
|
this.didReceiveFirstFocus.promise.then(() => this.focusNode?.focus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +109,9 @@ export abstract class ListWidget<
|
|||||||
this.updateScrollBar();
|
this.updateScrollBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onFocusResolved = (element: HTMLElement | undefined): void => {
|
private readonly onFocusResolved = (
|
||||||
|
element: HTMLElement | undefined
|
||||||
|
): void => {
|
||||||
this.focusNode = element;
|
this.focusNode = element;
|
||||||
this.didReceiveFirstFocus.resolve();
|
this.didReceiveFirstFocus.resolve();
|
||||||
};
|
};
|
||||||
@ -133,7 +138,7 @@ export abstract class ListWidget<
|
|||||||
return this.options.installable.uninstall({ item, progressId });
|
return this.options.installable.uninstall({ item, progressId });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<FilterableListContainer<T, S>
|
<FilterableListContainer<T, S>
|
||||||
defaultSearchOptions={this.options.defaultSearchOptions}
|
defaultSearchOptions={this.options.defaultSearchOptions}
|
||||||
@ -149,6 +154,7 @@ export abstract class ListWidget<
|
|||||||
messageService={this.messageService}
|
messageService={this.messageService}
|
||||||
commandService={this.commandService}
|
commandService={this.commandService}
|
||||||
responseService={this.responseService}
|
responseService={this.responseService}
|
||||||
|
onDidShow={this.onDidShowEmitter.event}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -186,3 +192,10 @@ export namespace ListWidget {
|
|||||||
readonly defaultSearchOptions: S;
|
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 {
|
export interface ArduinoComponent {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly deprecated?: boolean;
|
|
||||||
readonly author: string;
|
readonly author: string;
|
||||||
readonly summary: string;
|
readonly summary: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly moreInfoLink?: string;
|
|
||||||
readonly availableVersions: Installable.Version[];
|
readonly availableVersions: Installable.Version[];
|
||||||
readonly installable: boolean;
|
|
||||||
readonly installedVersion?: Installable.Version;
|
readonly installedVersion?: Installable.Version;
|
||||||
/**
|
/**
|
||||||
* This is the `Type` in IDE (1.x) UI.
|
* This is the `Type` in IDE (1.x) UI.
|
||||||
*/
|
*/
|
||||||
readonly types: string[];
|
readonly types: string[];
|
||||||
|
readonly deprecated?: boolean;
|
||||||
|
readonly moreInfoLink?: string;
|
||||||
}
|
}
|
||||||
export namespace ArduinoComponent {
|
export namespace ArduinoComponent {
|
||||||
export function is(arg: any): arg is ArduinoComponent {
|
export function is(arg: unknown): arg is ArduinoComponent {
|
||||||
return (
|
return (
|
||||||
!!arg &&
|
typeof arg === 'object' &&
|
||||||
'name' in arg &&
|
(<ArduinoComponent>arg).name !== undefined &&
|
||||||
typeof arg['name'] === 'string' &&
|
typeof (<ArduinoComponent>arg).name === 'string' &&
|
||||||
'author' in arg &&
|
(<ArduinoComponent>arg).author !== undefined &&
|
||||||
typeof arg['author'] === 'string' &&
|
typeof (<ArduinoComponent>arg).author === 'string' &&
|
||||||
'summary' in arg &&
|
(<ArduinoComponent>arg).summary !== undefined &&
|
||||||
typeof arg['summary'] === 'string' &&
|
typeof (<ArduinoComponent>arg).summary === 'string' &&
|
||||||
'description' in arg &&
|
(<ArduinoComponent>arg).description !== undefined &&
|
||||||
typeof arg['description'] === 'string' &&
|
typeof (<ArduinoComponent>arg).description === 'string' &&
|
||||||
'installable' in arg &&
|
(<ArduinoComponent>arg).availableVersions !== undefined &&
|
||||||
typeof arg['installable'] === 'boolean'
|
Array.isArray((<ArduinoComponent>arg).availableVersions) &&
|
||||||
|
(<ArduinoComponent>arg).types !== undefined &&
|
||||||
|
Array.isArray((<ArduinoComponent>arg).types)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,8 @@ export interface ExamplesService {
|
|||||||
current: SketchContainer[];
|
current: SketchContainer[];
|
||||||
any: 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>({
|
export const Installed = <T extends ArduinoComponent>({
|
||||||
installedVersion,
|
installedVersion,
|
||||||
}: T): boolean => {
|
}: T): boolean => {
|
||||||
|
@ -198,6 +198,10 @@ export namespace LibraryService {
|
|||||||
export namespace List {
|
export namespace List {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
readonly fqbn?: string | undefined;
|
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;
|
readonly category: string;
|
||||||
}
|
}
|
||||||
export namespace LibraryPackage {
|
export namespace LibraryPackage {
|
||||||
export function is(arg: any): arg is LibraryPackage {
|
export function is(arg: unknown): arg is LibraryPackage {
|
||||||
return (
|
return (
|
||||||
ArduinoComponent.is(arg) &&
|
ArduinoComponent.is(arg) &&
|
||||||
'includes' in arg &&
|
(<LibraryPackage>arg).includes !== undefined &&
|
||||||
Array.isArray(arg['includes'])
|
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 };
|
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
|
* 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
|
* 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(
|
return toLibrary(
|
||||||
{
|
{
|
||||||
name: item.getName(),
|
name: item.getName(),
|
||||||
installable: true,
|
|
||||||
installedVersion,
|
installedVersion,
|
||||||
},
|
},
|
||||||
item.getLatest()!,
|
item.getLatest()!,
|
||||||
@ -154,8 +153,10 @@ export class LibraryServiceImpl
|
|||||||
|
|
||||||
async list({
|
async list({
|
||||||
fqbn,
|
fqbn,
|
||||||
|
libraryName,
|
||||||
}: {
|
}: {
|
||||||
fqbn?: string | undefined;
|
fqbn?: string | undefined;
|
||||||
|
libraryName?: string | undefined;
|
||||||
}): Promise<LibraryPackage[]> {
|
}): Promise<LibraryPackage[]> {
|
||||||
const coreClient = await this.coreClient;
|
const coreClient = await this.coreClient;
|
||||||
const { client, instance } = 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.setAll(true); // https://github.com/arduino/arduino-ide/pull/303#issuecomment-815556447
|
||||||
req.setFqbn(fqbn);
|
req.setFqbn(fqbn);
|
||||||
}
|
}
|
||||||
|
if (libraryName) {
|
||||||
|
req.setName(libraryName);
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await new Promise<LibraryListResponse | undefined>(
|
const resp = await new Promise<LibraryListResponse | undefined>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
@ -219,7 +223,6 @@ export class LibraryServiceImpl
|
|||||||
{
|
{
|
||||||
name: library.getName(),
|
name: library.getName(),
|
||||||
installedVersion,
|
installedVersion,
|
||||||
installable: true,
|
|
||||||
description: library.getSentence(),
|
description: library.getSentence(),
|
||||||
summary: library.getParagraph(),
|
summary: library.getParagraph(),
|
||||||
moreInfoLink: library.getWebsite(),
|
moreInfoLink: library.getWebsite(),
|
||||||
@ -455,8 +458,7 @@ function toLibrary(
|
|||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
exampleUris: [],
|
exampleUris: [],
|
||||||
installable: false,
|
location: LibraryLocation.BUILTIN,
|
||||||
location: 0,
|
|
||||||
...pkg,
|
...pkg,
|
||||||
|
|
||||||
author: lib.getAuthor(),
|
author: lib.getAuthor(),
|
||||||
|
@ -49,7 +49,6 @@ export const aPackage: BoardsPackage = {
|
|||||||
deprecated: false,
|
deprecated: false,
|
||||||
description: 'Some Arduino Board, Some Other Arduino Board',
|
description: 'Some Arduino Board, Some Other Arduino Board',
|
||||||
id: 'some:arduinoCoreId',
|
id: 'some:arduinoCoreId',
|
||||||
installable: true,
|
|
||||||
moreInfoLink: 'http://www.some-url.lol/',
|
moreInfoLink: 'http://www.some-url.lol/',
|
||||||
name: 'Some Arduino Package',
|
name: 'Some Arduino Package',
|
||||||
summary: 'Boards included in this package:',
|
summary: 'Boards included in this package:',
|
||||||
|
@ -2,6 +2,16 @@ import { expect } from 'chai';
|
|||||||
import { Installable } from '../../common/protocol/installable';
|
import { Installable } from '../../common/protocol/installable';
|
||||||
|
|
||||||
describe('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', () => {
|
describe('compare', () => {
|
||||||
const testMe = Installable.Version.COMPARATOR;
|
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": {
|
"component": {
|
||||||
"boardsIncluded": "Boards included in this package:",
|
"boardsIncluded": "Boards included in this package:",
|
||||||
"by": "by",
|
"by": "by",
|
||||||
|
"clickToOpen": "Click to open in browser: {0}",
|
||||||
"filterSearch": "Filter your search...",
|
"filterSearch": "Filter your search...",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
"installed": "Installed",
|
"installLatest": "Install Latest",
|
||||||
|
"installVersion": "Install {0}",
|
||||||
|
"installed": "{0} installed",
|
||||||
"moreInfo": "More info",
|
"moreInfo": "More info",
|
||||||
|
"otherVersions": "Other Versions",
|
||||||
|
"remove": "Remove",
|
||||||
|
"title": "{0} by {1}",
|
||||||
"uninstall": "Uninstall",
|
"uninstall": "Uninstall",
|
||||||
"uninstallMsg": "Do you want to uninstall {0}?",
|
"uninstallMsg": "Do you want to uninstall {0}?",
|
||||||
"version": "Version {0}"
|
"update": "Update"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"cli": {
|
"cli": {
|
||||||
|
@ -36,10 +36,6 @@
|
|||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"xhr2": "^0.2.1"
|
"xhr2": "^0.2.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"@types/react": "18.0.0",
|
|
||||||
"@types/react-dom": "18.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "lerna run prepare && yarn download:plugins",
|
"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",
|
"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:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.10"
|
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":
|
"@babel/template@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
"@types/react-dom@18.0.0", "@types/react-dom@^18.0.6":
|
"@types/react-dom@^18.0.6":
|
||||||
version "18.0.0"
|
version "18.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
|
||||||
integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==
|
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
@ -3556,14 +3549,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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":
|
"@types/react-window@^1.8.5":
|
||||||
version "1.8.5"
|
version "1.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
||||||
@ -3571,7 +3556,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@18.0.0", "@types/react@^17", "@types/react@^18.0.15":
|
"@types/react@*":
|
||||||
version "18.0.0"
|
version "18.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.0.tgz#4be8aa3a2d04afc3ac2cc1ca43d39b0bd412890c"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.0.tgz#4be8aa3a2d04afc3ac2cc1ca43d39b0bd412890c"
|
||||||
integrity sha512-7+K7zEQYu7NzOwQGLR91KwWXXDzmTFODRVizJyIALf6RfLv2GDpqpknX64pvRVILXCpXi7O/pua8NGk44dLvJw==
|
integrity sha512-7+K7zEQYu7NzOwQGLR91KwWXXDzmTFODRVizJyIALf6RfLv2GDpqpknX64pvRVILXCpXi7O/pua8NGk44dLvJw==
|
||||||
@ -3580,6 +3565,15 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
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":
|
"@types/request@^2.0.3":
|
||||||
version "2.48.8"
|
version "2.48.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c"
|
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"
|
process-nextick-args "^2.0.0"
|
||||||
readable-stream "^2.3.5"
|
readable-stream "^2.3.5"
|
||||||
|
|
||||||
clsx@^1.0.4, clsx@^1.1.0:
|
clsx@^1.1.0:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||||
@ -6436,7 +6430,7 @@ doctrine@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
dom-helpers@^5.0.1, dom-helpers@^5.1.3:
|
dom-helpers@^5.0.1:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
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:
|
react-markdown@^8.0.0:
|
||||||
version "8.0.3"
|
version "8.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.3.tgz#e8aba0d2f5a1b2124d476ee1fff9448a2f57e4b3"
|
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"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
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:
|
react-virtuoso@^2.17.0:
|
||||||
version "2.19.1"
|
version "2.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.19.1.tgz#a660a5c3cafcc7a84b59dfc356e1916e632c1e3a"
|
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.19.1.tgz#a660a5c3cafcc7a84b59dfc356e1916e632c1e3a"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user