[Scouting App] Note scout multiple robots and add keywords
Note scouts often scout multiple robots so the scouting app should support it and
checkboxes were added to select keywords/tags. These tags will likely have to be modified
according to 2023's game.
Mobile UI Preview: https://ibb.co/QdFxwGj
Change-Id: If4fcb3ee97da5f52e428cb0a4b0a8401b4700a02
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
diff --git a/scouting/www/notes/notes.component.css b/scouting/www/notes/notes.component.css
index 869bdab..67b2351 100644
--- a/scouting/www/notes/notes.component.css
+++ b/scouting/www/notes/notes.component.css
@@ -6,7 +6,6 @@
width: calc(100% - 20px);
}
-.buttons {
- display: flex;
- justify-content: space-between;
+.container-main {
+ padding-left: 20px;
}
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 0f0eb82..86df425 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -9,88 +9,146 @@
import {SubmitNotes} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_generated';
import {SubmitNotesResponse} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_response_generated';
+/*
+For new games, the keywords being used will likely need to be updated.
+To update the keywords complete the following:
+ 1) Update the Keywords Interface and KEYWORD_CHECKBOX_LABELS in notes.component.ts
+ The keys of Keywords and KEYWORD_CHECKBOX_LABELS should match.
+ 2) In notes.component.ts, update the setTeamNumber() method with the new keywords.
+ 3) Add/Edit the new keywords in /scouting/webserver/requests/messages/submit_notes.fbs.
+ 4) In notes.component.ts, update the submitData() method with the newKeywords
+ so that it matches the updated flatbuffer
+ 5) In db.go, update the NotesData struct and the
+ AddNotes method with the new keywords
+ 6) In db_test.go update the TestNotes method so the test uses the keywords
+ 7) Update the submitNoteScoutingHandler in requests.go with the new keywords
+ 8) Finally, update the corresponding test in requests_test.go (TestSubmitNotes)
+
+ Note: If you change the number of keywords you might need to
+ update how they are displayed in notes.ng.html
+*/
+
+// TeamSelection: Display form to add a team to the teams being scouted.
+// Data: Display the note textbox and keyword selection form
+// for all the teams being scouted.
type Section = 'TeamSelection' | 'Data';
-interface Note {
- readonly data: string;
+// Every keyword checkbox corresponds to a boolean.
+// If the boolean is True, the checkbox is selected
+// and the note scout saw that the robot being scouted
+// displayed said property (ex. Driving really well -> goodDriving)
+interface Keywords {
+ goodDriving: boolean;
+ badDriving: boolean;
+ sketchyClimb: boolean;
+ solidClimb: boolean;
+ goodDefense: boolean;
+ badDefense: boolean;
}
+interface Input {
+ teamNumber: number;
+ notesData: string;
+ keywordsData: Keywords;
+}
+
+const KEYWORD_CHECKBOX_LABELS = {
+ goodDriving: 'Good Driving',
+ badDriving: 'Bad Driving',
+ solidClimb: 'Solid Climb',
+ sketchyClimb: 'Sketchy Climb',
+ goodDefense: 'Good Defense',
+ badDefense: 'Bad Defense',
+} as const;
+
@Component({
selector: 'frc971-notes',
templateUrl: './notes.ng.html',
styleUrls: ['../common.css', './notes.component.css'],
})
export class Notes {
+ // Re-export KEYWORD_CHECKBOX_LABELS so that we can
+ // use it in the checkbox properties.
+ readonly KEYWORD_CHECKBOX_LABELS = KEYWORD_CHECKBOX_LABELS;
+
+ // Necessary in order to iterate the keys of KEYWORD_CHECKBOX_LABELS.
+ Object = Object;
+
section: Section = 'TeamSelection';
- notes: Note[] = [];
errorMessage = '';
+ teamNumberSelection: number = 971;
- teamNumber: number = 971;
- newData = '';
+ // Data inputted by user is stored in this array.
+ // Includes the team number, notes, and keyword selection.
+ newData: Input[] = [];
- async setTeamNumber() {
- const builder = new Builder();
- RequestNotesForTeam.startRequestNotesForTeam(builder);
- RequestNotesForTeam.addTeam(builder, this.teamNumber);
- builder.finish(RequestNotesForTeam.endRequestNotesForTeam(builder));
+ setTeamNumber() {
+ let data: Input = {
+ teamNumber: this.teamNumberSelection,
+ notesData: '',
+ keywordsData: {
+ goodDriving: false,
+ badDriving: false,
+ solidClimb: false,
+ sketchyClimb: false,
+ goodDefense: false,
+ badDefense: false,
+ },
+ };
- const buffer = builder.asUint8Array();
- const res = await fetch('/requests/request/notes_for_team', {
- method: 'POST',
- body: buffer,
- });
+ this.newData.push(data);
+ this.section = 'Data';
+ }
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-
- if (res.ok) {
- this.notes = [];
- const parsedResponse =
- RequestNotesForTeamResponse.getRootAsRequestNotesForTeamResponse(
- fbBuffer
- );
- for (let i = 0; i < parsedResponse.notesLength(); i++) {
- const fbNote = parsedResponse.notes(i);
- this.notes.push({data: fbNote.data()});
- }
- this.section = 'Data';
+ removeTeam(index: number) {
+ this.newData.splice(index, 1);
+ if (this.newData.length == 0) {
+ this.section = 'TeamSelection';
} else {
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ this.section = 'Data';
}
}
- changeTeam() {
+ addTeam() {
this.section = 'TeamSelection';
}
async submitData() {
- const builder = new Builder();
- const dataFb = builder.createString(this.newData);
- builder.finish(
- SubmitNotes.createSubmitNotes(builder, this.teamNumber, dataFb)
- );
+ for (let i = 0; i < this.newData.length; i++) {
+ const builder = new Builder();
+ const dataFb = builder.createString(this.newData[i].notesData);
+ builder.finish(
+ SubmitNotes.createSubmitNotes(
+ builder,
+ this.newData[i].teamNumber,
+ dataFb,
+ this.newData[i].keywordsData.goodDriving,
+ this.newData[i].keywordsData.badDriving,
+ this.newData[i].keywordsData.sketchyClimb,
+ this.newData[i].keywordsData.solidClimb,
+ this.newData[i].keywordsData.goodDefense,
+ this.newData[i].keywordsData.badDefense
+ )
+ );
- const buffer = builder.asUint8Array();
- const res = await fetch('/requests/submit/submit_notes', {
- method: 'POST',
- body: buffer,
- });
+ const buffer = builder.asUint8Array();
+ const res = await fetch('/requests/submit/submit_notes', {
+ method: 'POST',
+ body: buffer,
+ });
- if (res.ok) {
- this.newData = '';
- this.errorMessage = '';
- await this.setTeamNumber();
- } else {
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ if (!res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
}
+
+ this.newData = [];
+ this.errorMessage = '';
+ this.section = 'TeamSelection';
}
}
diff --git a/scouting/www/notes/notes.ng.html b/scouting/www/notes/notes.ng.html
index a69ba88..af48fd9 100644
--- a/scouting/www/notes/notes.ng.html
+++ b/scouting/www/notes/notes.ng.html
@@ -2,9 +2,9 @@
<ng-container [ngSwitch]="section">
<div *ngSwitchCase="'TeamSelection'">
- <label for="team_number_notes">Team Number</label>
+ <label class="label" for="team_number_notes">Team Number</label>
<input
- [(ngModel)]="teamNumber"
+ [(ngModel)]="teamNumberSelection"
type="number"
id="team_number_notes"
min="1"
@@ -14,17 +14,85 @@
</div>
<div *ngSwitchCase="'Data'">
- <h3>Scouting team: {{teamNumber}}</h3>
- <ul *ngFor="let note of notes">
- <li class="note">{{ note.data }}</li>
- </ul>
- <textarea class="text-input" [(ngModel)]="newData"></textarea>
- <div class="buttons">
- <button class="btn btn-primary" (click)="changeTeam()">
- Change team
- </button>
- <button class="btn btn-primary" (click)="submitData()">Submit</button>
+ <div class="container-main" *ngFor="let team of newData; let i = index">
+ <div class="pt-2 pb-2">
+ <div class="d-flex flex-row">
+ <div>
+ <button
+ class="btn bg-transparent ml-10 md-5"
+ (click)="removeTeam(i)"
+ >
+ ✖
+ <!--X Symbol-->
+ </button>
+ </div>
+ <div><h3>{{team.teamNumber}}</h3></div>
+ </div>
+ <div class="">
+ <textarea
+ class="text-input"
+ [(ngModel)]="newData[i].notesData"
+ ></textarea>
+ </div>
+ <!--Key Word Checkboxes-->
+ <!--Row 1 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+ <!--Slice KEYWORD_CHECKBOX_LABELS using https://angular.io/api/common/SlicePipe-->
+ <div class="d-flex flex-row justify-content-around">
+ <div
+ *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:0:((Object.keys(KEYWORD_CHECKBOX_LABELS).length)/2); let k = index"
+ >
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ [(ngModel)]="newData[i]['keywordsData'][key]"
+ type="checkbox"
+ id="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+ name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ />
+ <label
+ class="form-check-label"
+ for="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+ >
+ {{KEYWORD_CHECKBOX_LABELS[key]}}
+ </label>
+ <br />
+ </div>
+ </div>
+ </div>
+ <!--Row 2 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+ <div class="d-flex flex-row justify-content-around">
+ <div
+ *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:3:(Object.keys(KEYWORD_CHECKBOX_LABELS).length); let k = index"
+ >
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ [(ngModel)]="newData[i]['keywordsData'][key]"
+ type="checkbox"
+ id="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ />
+ <label
+ class="form-check-label"
+ for="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+ >
+ {{KEYWORD_CHECKBOX_LABELS[key]}}
+ </label>
+ <br />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="d-flex flex-row justify-content-center pt-2">
+ <div>
+ <button class="btn btn-secondary" (click)="addTeam()">Add team</button>
+ </div>
+ <div>
+ <button class="btn btn-success" (click)="submitData()">Submit</button>
+ </div>
</div>
</div>
+
<div class="error">{{errorMessage}}</div>
</ng-container>