Add dialog to insert user fields for board that require them to upload (#550)

* Rebuild gRPC protocol interfaces

* Implement methods to get user fields for board/port combination

* Implement dialog to input board user fields

* Add configure and upload step when uploading to board requiring user fields

* Disable Sketch > Configure and Upload menu if board doesn't support user fields

* Fix serial upload not working with all boards

* Update i18n source file

* fix user fields UI

* regenerate cli protocol

* fix localisation

* check if user fields are empty

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
Silvano Cerza
2021-11-25 18:22:51 +01:00
committed by GitHub
parent 74bfdc4c56
commit a090dfe99c
19 changed files with 568 additions and 679 deletions

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { BoardUserField } from '../../../common/protocol';
import { nls } from '@theia/core/lib/browser/nls';
export const UserFieldsComponent = ({
initialBoardUserFields,
updateUserFields,
cancel,
accept,
}: {
initialBoardUserFields: BoardUserField[];
updateUserFields: (userFields: BoardUserField[]) => void;
cancel: () => void;
accept: () => Promise<void>;
}): React.ReactElement => {
const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[]
>(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true);
React.useEffect(() => {
setBoardUserFields(initialBoardUserFields);
}, [initialBoardUserFields]);
const updateUserField =
(index: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const newBoardUserFields = [...boardUserFields];
newBoardUserFields[index].value = e.target.value;
setBoardUserFields(newBoardUserFields);
};
const allFieldsHaveValues = (userFields: BoardUserField[]): boolean => {
return (
userFields &&
userFields.length > 0 &&
userFields
.map<boolean>((field: BoardUserField): boolean => {
return field.value.length > 0;
})
.reduce((previous: boolean, current: boolean): boolean => {
return previous && current;
})
);
};
React.useEffect(() => {
updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
}, [boardUserFields]);
return (
<div>
<div className="user-fields-container">
<div className="user-fields-list">
{boardUserFields.map((field, index) => {
return (
<div className="dialogSection" key={index}>
<div className="dialogRow">
<label className="field-label">{field.label}</label>
</div>
<div className="dialogRow">
<input
type={field.secret ? 'password' : 'text'}
value={field.value}
className="theia-input"
placeholder={'Enter ' + field.label}
onChange={updateUserField(index)}
/>
</div>
</div>
);
})}
</div>
</div>
<div className="dialogSection">
<div className="dialogRow button-container">
<button
type="button"
className="theia-button secondary install-cert-btn"
onClick={cancel}
>
{nls.localize('arduino/userFields/cancel', 'Cancel')}
</button>
<button
type="button"
className="theia-button primary install-cert-btn"
disabled={uploadButtonDisabled}
onClick={accept}
>
{nls.localize('arduino/userFields/upload', 'Upload')}
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
protected _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
}
set currentUserFields(userFields: BoardUserField[]) {
this.setUserFields(userFields);
}
get currentUserFields(): BoardUserField[] {
return this._currentUserFields;
}
resetUserFieldsValue(): void {
this._currentUserFields = this._currentUserFields.map((field) => {
field.value = '';
return field;
});
}
protected setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}
protected render(): React.ReactNode {
return (
<form>
<UserFieldsComponent
initialBoardUserFields={this._currentUserFields}
updateUserFields={this.setUserFields.bind(this)}
cancel={this.cancel}
accept={this.accept}
/>
</form>
);
}
}
@injectable()
export class UserFieldsDialogProps extends DialogProps {}
@injectable()
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
protected readonly widget: UserFieldsDialogWidget;
constructor(
@inject(UserFieldsDialogProps)
protected readonly props: UserFieldsDialogProps
) {
super({
title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '',
});
this.titleNode.classList.add('user-fields-dialog-title');
this.contentNode.classList.add('user-fields-dialog-content');
this.acceptButton = undefined;
this.widget = new UserFieldsDialogWidget(
this.close.bind(this),
this.accept.bind(this)
);
}
set value(userFields: BoardUserField[]) {
this.widget.currentUserFields = userFields;
}
get value(): BoardUserField[] {
return this.widget.currentUserFields;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected async accept(): Promise<void> {
// If the user presses enter and at least
// a field is empty don't accept the input
for (const field of this.value) {
if (field.value.length === 0) {
return;
}
}
return super.accept();
}
close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
super.close();
}
}