Skip to content

Show user fields dialog again if upload fails #1415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
import { UserFields } from './contributions/user-fields';

const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
Expand Down Expand Up @@ -761,6 +762,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch);

bindContributionProvider(bind, StartupTaskProvider);
Expand Down
155 changes: 28 additions & 127 deletions arduino-ide-extension/src/browser/contributions/upload-sketch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
Expand All @@ -11,96 +11,36 @@ import {
TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';

@injectable()
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;

@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;

private boardRequiresUserFields = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();

private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;

protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}

private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) {
return '';
}
return fqbn + '|' + address;
}
@inject(UserFields)
private readonly userFields: UserFields;

override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (
this.boardRequiresUserFields &&
key &&
!this.cachedUserFields.has(key)
) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceProvider.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
if (await this.userFields.checkUserFieldsDialog()) {
this.uploadSketch();
}
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress,
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}

const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
).map((f) => ({ ...f }));

const result = await this.userFieldsDialog.open();
if (!result) {
return;
if (await this.userFields.checkUserFieldsDialog(true)) {
this.uploadSketch();
}
this.cachedUserFields.set(key, result);
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
});
registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
Expand All @@ -120,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
}

override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
})
);
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
})
);
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
});

registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
});
}

override registerKeybindings(registry: KeybindingRegistry): void {
Expand Down Expand Up @@ -215,18 +130,7 @@ export class UploadSketch extends CoreServiceContribution {
return;
}

// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (
uploadOptions.userFields.length === 0 &&
this.boardRequiresUserFields
) {
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
if (!this.userFields.checkUserFieldsForUpload()) {
return;
}

Expand All @@ -242,6 +146,7 @@ export class UploadSketch extends CoreServiceContribution {
{ timeout: 3000 }
);
} catch (e) {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
this.uploadInProgress = false;
Expand All @@ -258,7 +163,7 @@ export class UploadSketch extends CoreServiceContribution {
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const userFields = this.userFields();
const userFields = this.userFields.getUserFields();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not pass the fqbn from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason since I can easily get them from the boardsServiceProvider. Passing it here would just add a parameter, with no benefit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly this; why do you want to calculate it twice? verify, and upload will require the fqbn anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO how the user fields are stored is not related to the upload. I may want to open that dialog even if I'm not uploading.

const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
Expand Down Expand Up @@ -301,10 +206,6 @@ export class UploadSketch extends CoreServiceContribution {
return port;
}

private userFields(): BoardUserField[] {
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
}

/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
Expand Down
150 changes: 150 additions & 0 deletions arduino-ide-extension/src/browser/contributions/user-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution';
import { UploadSketch } from './upload-sketch';

@injectable()
export class UserFields extends Contribution {
private boardRequiresUserFields = false;
private userFieldsSet = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not simplify this and if there are user fields for an fqbn in the map, it means true. Otherwise, false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that we would lose the value of the user field. As far as I know, we don't want that, we want to preserve the value, but give the user the possibility to change it.

private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();

@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;

@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;

@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;

protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}

override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
}

private selectedFqbnAddress(): string | undefined {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return undefined;
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) {
return undefined;
}
return fqbn + '|' + address;
}

private async showUserFieldsDialog(
key: string
): Promise<BoardUserField[] | undefined> {
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = cached ? cached.slice() : await this.boardsServiceProvider.selectedBoardUserFields();
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}

this.userFieldsSet = true;
this.cachedUserFields.set(key, result);
return result;
}

async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
const key = this.selectedFqbnAddress();
if (!key) {
return false;
}
/*
If the board requires to be configured with user fields, we want
to show the user fields dialog, but only if they weren't already
filled in or if they were filled in, but the previous upload failed.
*/
if (
!forceOpen &&
(!this.boardRequiresUserFields ||
(this.cachedUserFields.has(key) && this.userFieldsSet))
) {
return true;
}
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
return userFieldsFilledIn;
}

checkUserFieldsForUpload(): boolean {
// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
this.userFieldsSet = true;
return true;
}
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
this.userFieldsSet = false;
return false;
}

getUserFields(): BoardUserField[] {
const fqbnAddress = this.selectedFqbnAddress();
if (!fqbnAddress) {
return [];
}
return this.cachedUserFields.get(fqbnAddress) ?? [];
}

isRequired(): boolean {
return this.boardRequiresUserFields;
}

notifyFailedWithError(e: Error): void {
if (
this.boardRequiresUserFields &&
CoreError.UploadFailed.is(e)
) {
this.userFieldsSet = false;
}
}
}