Add match list tab
So the user can easily see what teams are playing in what matches
Signed-off-by: Ravago Jones <ravagojones@gmail.com>
Change-Id: Ie5efefbc890d02e8aec6b991dbc5b10249f41370
diff --git a/scouting/scouting_test.ts b/scouting/scouting_test.ts
index fda3834..84824ff 100644
--- a/scouting/scouting_test.ts
+++ b/scouting/scouting_test.ts
@@ -15,8 +15,9 @@
// Protractor since they're not angular elements. We achieve this by checking
// an invisible checkbox that's off-screen.
async function disableAlerts() {
- await browser.executeAsyncScript(function (callback) {
- let block_alerts = document.getElementById('block_alerts') as HTMLInputElement;
+ await browser.executeAsyncScript(function(callback) {
+ let block_alerts =
+ document.getElementById('block_alerts') as HTMLInputElement;
block_alerts.checked = true;
callback();
});
@@ -47,8 +48,11 @@
// Asserts that the n'th instance of a field on the "Submit and Review"
// screen has a specific value.
-async function expectNthReviewFieldToBe(fieldName: string, n: number, expectedValue: string) {
- expect(await element.all(by.cssContainingText('li', `${fieldName}:`)).get(n).getText())
+async function expectNthReviewFieldToBe(
+ fieldName: string, n: number, expectedValue: string) {
+ expect(await element.all(by.cssContainingText('li', `${fieldName}:`))
+ .get(n)
+ .getText())
.toEqual(`${fieldName}: ${expectedValue}`);
}
@@ -58,8 +62,7 @@
// overwrite the text that is there. If we didn't hit CTRL-A to select all
// the text, we'd be appending to whatever is there already.
return element(by.id(id)).sendKeys(
- protractor.Key.CONTROL, 'a', protractor.Key.NULL,
- value);
+ protractor.Key.CONTROL, 'a', protractor.Key.NULL, value);
}
describe('The scouting web page', () => {
@@ -71,19 +74,23 @@
// Import the match list before running any tests. Ideally this should be
// run in beforeEach(), but it's not worth doing that at this time. Our
// tests are basic enough not to require this.
- await element(by.cssContainingText('.nav-link', 'Import Match List')).click();
+ await element(by.cssContainingText('.nav-link', 'Import Match List'))
+ .click();
expect(await getHeadingText()).toEqual('Import Match List');
await setTextboxByIdTo('year', '2016');
await setTextboxByIdTo('event_code', 'nytr');
await element(by.buttonText('Import')).click();
await browser.wait(EC.textToBePresentInElement(
- element(by.css('.progress_message')), 'Successfully imported match list.'));
+ element(by.css('.progress_message')),
+ 'Successfully imported match list.'));
});
it('should: error on unknown match.', async () => {
await loadPage();
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
// Pick a match that doesn't exist in the 2016nytr match list.
await setTextboxByIdTo('match_number', '3');
await setTextboxByIdTo('team_number', '971');
@@ -96,14 +103,16 @@
// Attempt to submit and validate the error.
await element(by.buttonText('Submit')).click();
- expect(await getErrorMessage()).toContain(
- 'Failed to find team 971 in match 3 in the schedule.');
+ expect(await getErrorMessage())
+ .toContain('Failed to find team 971 in match 3 in the schedule.');
});
it('should: review and submit correct data.', async () => {
await loadPage();
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
// Submit scouting data for a random team that attended 2016nytr.
expect(await getHeadingText()).toEqual('Team Selection');
await setTextboxByIdTo('match_number', '2');
@@ -156,8 +165,8 @@
await expectReviewFieldToBe('Broke (mechanically)', 'true');
await element(by.buttonText('Submit')).click();
- await browser.wait(EC.textToBePresentInElement(
- element(by.css('.header')), 'Success'));
+ await browser.wait(
+ EC.textToBePresentInElement(element(by.css('.header')), 'Success'));
// TODO(phil): Make sure the data made its way to the database correctly.
});
@@ -165,23 +174,27 @@
it('should: load all images successfully.', async () => {
await loadPage();
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
// Get to the Auto display with the field pictures.
expect(await getHeadingText()).toEqual('Team Selection');
await element(by.buttonText('Next')).click();
expect(await getHeadingText()).toEqual('Auto');
// We expect 2 fully loaded images.
- browser.executeAsyncScript(function (callback) {
- let images = document.getElementsByTagName('img');
- let numLoaded = 0;
- for (let i = 0; i < images.length; i += 1) {
- if (images[i].naturalWidth > 0) {
- numLoaded += 1;
- }
- }
- callback(numLoaded);
- }).then(function (numLoaded) {
- expect(numLoaded).toBe(2);
- });
+ browser
+ .executeAsyncScript(function(callback) {
+ let images = document.getElementsByTagName('img');
+ let numLoaded = 0;
+ for (let i = 0; i < images.length; i += 1) {
+ if (images[i].naturalWidth > 0) {
+ numLoaded += 1;
+ }
+ }
+ callback(numLoaded);
+ })
+ .then(function(numLoaded) {
+ expect(numLoaded).toBe(2);
+ });
});
});
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index f0b91f6..f386f06 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
deps = [
"//scouting/www/entry",
"//scouting/www/import_match_list",
+ "//scouting/www/match_list",
"@npm//@angular/animations",
"@npm//@angular/common",
"@npm//@angular/core",
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index d4ee5f4..fc21164 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -5,14 +5,18 @@
<ul class="nav nav-tabs">
<li class="nav-item">
- <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabTo('Entry')">Data Entry</a>
+ <a class="nav-link" [class.active]="tabIs('MatchList')" (click)="switchTabToGuarded('MatchList')">Match List</a>
</li>
<li class="nav-item">
- <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabTo('ImportMatchList')">Import Match List</a>
+ <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabToGuarded('Entry')">Data Entry</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabToGuarded('ImportMatchList')">Import Match List</a>
</li>
</ul>
<ng-container [ngSwitch]="tab">
- <app-entry *ngSwitchCase="'Entry'"></app-entry>
+ <app-match-list (selectedTeamEvent)="selectTeamInMatch($event)" *ngSwitchCase="'MatchList'"></app-match-list>
+ <app-entry (switchTabsEvent)="switchTabTo($event)" [teamNumber]="selectedTeamInMatch.teamNumber" [matchNumber]="selectedTeamInMatch.matchNumber" *ngSwitchCase="'Entry'"></app-entry>
<app-import-match-list *ngSwitchCase="'ImportMatchList'"></app-import-match-list>
</ng-container>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index e34283a..fbde74f 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -1,6 +1,11 @@
import {Component, ElementRef, ViewChild} from '@angular/core';
-type Tab = 'Entry'|'ImportMatchList';
+type Tab = 'MatchList'|'Entry'|'ImportMatchList';
+type TeamInMatch = {
+ teamNumber: number,
+ matchNumber: number,
+ compLevel: string
+};
@Component({
selector: 'my-app',
@@ -8,14 +13,17 @@
styleUrls: ['./common.css']
})
export class App {
- tab: Tab = 'Entry';
+ selectedTeamInMatch:
+ TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+ tab: Tab = 'MatchList';
- @ViewChild("block_alerts") block_alerts: ElementRef;
+ @ViewChild('block_alerts') block_alerts: ElementRef;
constructor() {
window.addEventListener('beforeunload', (e) => {
if (!this.block_alerts.nativeElement.checked) {
- // Based on https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
+ // Based on
+ // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
// This combination ensures a dialog will be shown on most browsers.
e.preventDefault();
e.returnValue = '';
@@ -27,7 +35,12 @@
return this.tab == tab;
}
- switchTabTo(tab: Tab) {
+ selectTeamInMatch(teamInMatch: TeamInMatch) {
+ this.selectedTeamInMatch = teamInMatch;
+ this.switchTabTo('Entry');
+ }
+
+ switchTabToGuarded(tab: Tab) {
let shouldSwitch = true;
if (this.tab !== tab) {
if (!this.block_alerts.nativeElement.checked) {
@@ -35,7 +48,11 @@
}
}
if (shouldSwitch) {
- this.tab = tab;
+ this.switchTabTo(tab);
}
}
+
+ private switchTabTo(tab: Tab) {
+ this.tab = tab;
+ }
}
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 2a514f2..e560bfc 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -3,6 +3,7 @@
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {EntryModule} from './entry/entry.module';
import {ImportMatchListModule} from './import_match_list/import_match_list.module';
+import {MatchListModule} from './match_list/match_list.module';
import {App} from './app';
@@ -13,6 +14,7 @@
BrowserAnimationsModule,
EntryModule,
ImportMatchListModule,
+ MatchListModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 7461aad..d850949 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -1,134 +1,141 @@
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-
+import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
-import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
import * as submit_data_scouting from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
+import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
+
import SubmitDataScouting = submit_data_scouting.scouting.webserver.requests.SubmitDataScouting;
import SubmitDataScoutingResponse = submit_data_scouting_response.scouting.webserver.requests.SubmitDataScoutingResponse;
import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
-type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|'Review and Submit'|'Success'
-type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|'High'|'Transversal'
+type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|
+ 'Review and Submit'|'Success'
+type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|
+ 'High'|'Transversal'
-@Component({
- selector: 'app-entry',
- templateUrl: './entry.ng.html',
- styleUrls: ['../common.css', './entry.component.css']
-})
-export class EntryComponent {
- section: Section = 'Team Selection';
- matchNumber: number = 1
- teamNumber: number = 1
- autoUpperShotsMade: number = 0;
- autoLowerShotsMade: number = 0;
- autoShotsMissed: number = 0;
- teleUpperShotsMade: number = 0;
- teleLowerShotsMade: number = 0;
- teleShotsMissed: number = 0;
- defensePlayedOnScore: number = 0;
- defensePlayedScore: number = 0;
- level: Level = 'NoAttempt';
- ball1: boolean = false;
- ball2: boolean = false;
- ball3: boolean = false;
- ball4: boolean = false;
- ball5: boolean = false;
- quadrant: number = 1;
- errorMessage: string = '';
- noShow: boolean = false;
- neverMoved: boolean = false;
- batteryDied: boolean = false;
- mechanicallyBroke: boolean = false;
- lostComs: boolean = false;
+ @Component({
+ selector: 'app-entry',
+ templateUrl: './entry.ng.html',
+ styleUrls: ['../common.css', './entry.component.css']
+ }) export class EntryComponent {
+ section: Section = 'Team Selection';
+ @Output() switchTabsEvent = new EventEmitter<string>();
+ @Input() matchNumber: number = 1;
+ @Input() teamNumber: number = 1;
+ autoUpperShotsMade: number = 0;
+ autoLowerShotsMade: number = 0;
+ autoShotsMissed: number = 0;
+ teleUpperShotsMade: number = 0;
+ teleLowerShotsMade: number = 0;
+ teleShotsMissed: number = 0;
+ defensePlayedOnScore: number = 0;
+ defensePlayedScore: number = 0;
+ level: Level = 'NoAttempt';
+ ball1: boolean = false;
+ ball2: boolean = false;
+ ball3: boolean = false;
+ ball4: boolean = false;
+ ball5: boolean = false;
+ quadrant: number = 1;
+ errorMessage: string = '';
+ noShow: boolean = false;
+ neverMoved: boolean = false;
+ batteryDied: boolean = false;
+ mechanicallyBroke: boolean = false;
+ lostComs: boolean = false;
- @ViewChild("header") header: ElementRef;
+ @ViewChild('header') header: ElementRef;
- nextSection() {
- if (this.section === 'Team Selection') {
- this.section = 'Auto';
- } else if (this.section === 'Auto') {
- this.section = 'TeleOp';
- } else if (this.section === 'TeleOp') {
- this.section = 'Climb';
- } else if (this.section === 'Climb') {
- this.section = 'Other';
- } else if (this.section === 'Other') {
- this.section = 'Review and Submit';
- } else if (this.section === 'Review and Submit') {
- this.submitDataScouting();
- return;
- }
- // Scroll back to the top so that we can be sure the user sees the
- // entire next screen. Otherwise it's easy to overlook input fields.
- this.scrollToTop();
+ nextSection() {
+ if (this.section === 'Team Selection') {
+ this.section = 'Auto';
+ } else if (this.section === 'Auto') {
+ this.section = 'TeleOp';
+ } else if (this.section === 'TeleOp') {
+ this.section = 'Climb';
+ } else if (this.section === 'Climb') {
+ this.section = 'Other';
+ } else if (this.section === 'Other') {
+ this.section = 'Review and Submit';
+ } else if (this.section === 'Review and Submit') {
+ this.submitDataScouting();
+ return;
+ } else if (this.section === 'Success') {
+ this.switchTabsEvent.emit('MatchList');
+ return;
}
+ // Scroll back to the top so that we can be sure the user sees the
+ // entire next screen. Otherwise it's easy to overlook input fields.
+ this.scrollToTop();
+ }
- prevSection() {
- if (this.section === 'Auto') {
- this.section = 'Team Selection';
- } else if (this.section === 'TeleOp') {
- this.section = 'Auto';
- } else if (this.section === 'Climb') {
- this.section = 'TeleOp';
- } else if (this.section === 'Other') {
- this.section = 'Climb';
- } else if (this.section === 'Review and Submit') {
- this.section = 'Other';
- }
- // Scroll back to the top so that we can be sure the user sees the
- // entire previous screen. Otherwise it's easy to overlook input
- // fields.
- this.scrollToTop();
+ prevSection() {
+ if (this.section === 'Auto') {
+ this.section = 'Team Selection';
+ } else if (this.section === 'TeleOp') {
+ this.section = 'Auto';
+ } else if (this.section === 'Climb') {
+ this.section = 'TeleOp';
+ } else if (this.section === 'Other') {
+ this.section = 'Climb';
+ } else if (this.section === 'Review and Submit') {
+ this.section = 'Other';
}
+ // Scroll back to the top so that we can be sure the user sees the
+ // entire previous screen. Otherwise it's easy to overlook input
+ // fields.
+ this.scrollToTop();
+ }
- private scrollToTop() {
- this.header.nativeElement.scrollIntoView();
+ private scrollToTop() {
+ this.header.nativeElement.scrollIntoView();
+ }
+
+ async submitDataScouting() {
+ this.errorMessage = '';
+
+ const builder =
+ new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+ SubmitDataScouting.startSubmitDataScouting(builder);
+ SubmitDataScouting.addTeam(builder, this.teamNumber);
+ SubmitDataScouting.addMatch(builder, this.matchNumber);
+ SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
+ SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
+ SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
+ SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
+ SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
+ SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
+ SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
+ SubmitDataScouting.addAutoBall1(builder, this.ball1);
+ SubmitDataScouting.addAutoBall2(builder, this.ball2);
+ SubmitDataScouting.addAutoBall3(builder, this.ball3);
+ SubmitDataScouting.addAutoBall4(builder, this.ball4);
+ SubmitDataScouting.addAutoBall5(builder, this.ball5);
+ SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
+
+ // TODO(phil): Add support for defensePlayedOnScore.
+ // TODO(phil): Fix the Climbing score.
+ SubmitDataScouting.addClimbing(builder, 1);
+ builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch(
+ '/requests/submit/data_scouting', {method: 'POST', body: buffer});
+
+ if (res.ok) {
+ // We successfully submitted the data. Report success.
+ this.section = 'Success';
+ } else {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage =
+ `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
}
-
- async submitDataScouting() {
- this.errorMessage = '';
-
- const builder = new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
- SubmitDataScouting.startSubmitDataScouting(builder);
- SubmitDataScouting.addTeam(builder, this.teamNumber);
- SubmitDataScouting.addMatch(builder, this.matchNumber);
- SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
- SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
- SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
- SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
- SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
- SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
- SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
- SubmitDataScouting.addAutoBall1(builder, this.ball1);
- SubmitDataScouting.addAutoBall2(builder, this.ball2);
- SubmitDataScouting.addAutoBall3(builder, this.ball3);
- SubmitDataScouting.addAutoBall4(builder, this.ball4);
- SubmitDataScouting.addAutoBall5(builder, this.ball5);
- SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
-
- // TODO(phil): Add support for defensePlayedOnScore.
- // TODO(phil): Fix the Climbing score.
- SubmitDataScouting.addClimbing(builder, 1);
- builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
-
- const buffer = builder.asUint8Array();
- const res = await fetch(
- '/requests/submit/data_scouting', {method: 'POST', body: buffer});
-
- if (res.ok) {
- // We successfully submitted the data. Report success.
- this.section = 'Success';
- } else {
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(
- fbBuffer as unknown as flatbuffers.ByteBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
- }
- }
+ }
}
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index a39db40..5849f3b 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -193,14 +193,16 @@
<span class="error_message">{{ errorMessage }}</span>
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
+ <div class="buttons justify-content-end">
<button class="btn btn-primary" (click)="nextSection()">Submit</button>
</div>
</div>
<div *ngSwitchCase="'Success'" id="success" class="container-fluid">
- <h4>Success</h4>
- <div>Please reload the page to submit more data.</div>
+ <span>Successfully submitted scouting data.</span>
+ <div class="buttons justify-content-end">
+ <button class="btn btn-primary" (click)="nextSection()">Continue</button>
+ </div>
+
</div>
</ng-container>
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
new file mode 100644
index 0000000..a33caf8
--- /dev/null
+++ b/scouting/www/match_list/BUILD
@@ -0,0 +1,27 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+ name = "match_list",
+ srcs = [
+ "match_list.component.ts",
+ "match_list.module.ts",
+ ],
+ angular_assets = [
+ "match_list.component.css",
+ "match_list.ng.html",
+ "//scouting/www:common_css",
+ ],
+ compiler = "//tools:tsc_wrapped_with_angular",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ use_angular_plugin = True,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@angular/forms",
+ ],
+)
diff --git a/scouting/www/match_list/match_list.component.css b/scouting/www/match_list/match_list.component.css
new file mode 100644
index 0000000..a2c1676
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.css
@@ -0,0 +1,11 @@
+* {
+ padding: 10px;
+}
+
+.red {
+ background-color: #dc3545;
+}
+
+.blue {
+ background-color: #0d6efd;
+}
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
new file mode 100644
index 0000000..0085b7b
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.ts
@@ -0,0 +1,122 @@
+import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+import * as request_all_matches from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_generated';
+import * as request_all_matches_response from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
+
+import RequestAllMatches = request_all_matches.scouting.webserver.requests.RequestAllMatches;
+import RequestAllMatchesResponse = request_all_matches_response.scouting.webserver.requests.RequestAllMatchesResponse;
+import Match = request_all_matches_response.scouting.webserver.requests.Match;
+import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
+
+type TeamInMatch = {
+ teamNumber: number,
+ matchNumber: number,
+ compLevel: string
+};
+
+@Component({
+ selector: 'app-match-list',
+ templateUrl: './match_list.ng.html',
+ styleUrls: ['../common.css', './match_list.component.css']
+})
+export class MatchListComponent implements OnInit {
+ @Output() selectedTeamEvent = new EventEmitter<TeamInMatch>();
+ teamInMatch: TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+ progressMessage: string = '';
+ errorMessage: string = '';
+ matchList: Match[] = [];
+
+ setTeamInMatch(teamInMatch: TeamInMatch) {
+ this.teamInMatch = teamInMatch;
+ this.selectedTeamEvent.emit(teamInMatch);
+ }
+
+ teamsInMatch(match: Match): {number: number, color: 'red'|'blue'}[] {
+ return [
+ {number: match.r1(), color: 'red'},
+ {number: match.r2(), color: 'red'},
+ {number: match.r3(), color: 'red'},
+ {number: match.b1(), color: 'blue'},
+ {number: match.b2(), color: 'blue'},
+ {number: match.b3(), color: 'blue'},
+ ];
+ }
+
+ matchType(match: Match): string|null {
+ switch (match.compLevel()) {
+ case 'qm':
+ return 'Quals';
+ case 'ef':
+ return 'Eighth Final';
+ case 'qf':
+ return 'Quarter Final';
+ case 'sf':
+ return 'Semi Final';
+ case 'f':
+ return 'Final';
+ default:
+ return null;
+ }
+ }
+
+ displayMatchNumber(match: Match): string {
+ return `${this.matchType(match)} ${match.matchNumber()}`;
+ }
+
+ ngOnInit() {
+ this.importMatchList();
+ }
+
+ async importMatchList() {
+ this.errorMessage = '';
+
+ const builder =
+ new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+ RequestAllMatches.startRequestAllMatches(builder);
+ builder.finish(RequestAllMatches.endRequestAllMatches(builder));
+
+ this.progressMessage = 'Fetching match list. Please be patient.';
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch(
+ '/requests/request/all_matches', {method: 'POST', body: buffer});
+
+ if (res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse =
+ RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ this.matchList = [];
+ for (let i = 0; i < parsedResponse.matchListLength(); i++) {
+ this.matchList.push(parsedResponse.matchList(i));
+ }
+ this.matchList.sort((a, b) => {
+ let aString = this.displayMatchNumber(a);
+ let bString = this.displayMatchNumber(b);
+ if (aString < bString) {
+ return -1;
+ }
+ if (aString > bString) {
+ return 1;
+ }
+ return 0;
+ });
+
+ this.progressMessage = 'Successfully fetched match list.';
+ } else {
+ this.progressMessage = '';
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage =
+ `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ }
+}
diff --git a/scouting/www/match_list/match_list.module.ts b/scouting/www/match_list/match_list.module.ts
new file mode 100644
index 0000000..dbec410
--- /dev/null
+++ b/scouting/www/match_list/match_list.module.ts
@@ -0,0 +1,13 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+
+import {MatchListComponent} from './match_list.component';
+
+@NgModule({
+ declarations: [MatchListComponent],
+ exports: [MatchListComponent],
+ imports: [CommonModule, FormsModule],
+})
+export class MatchListModule {
+}
diff --git a/scouting/www/match_list/match_list.ng.html b/scouting/www/match_list/match_list.ng.html
new file mode 100644
index 0000000..39d4578
--- /dev/null
+++ b/scouting/www/match_list/match_list.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+ <h2>Matches</h2>
+</div>
+
+<div class="container-fluid">
+
+ <div class="row">
+
+ <div *ngFor="let match of matchList; index as i">
+ <span class="badge bg-secondary rounded-left">{{ displayMatchNumber(match) }}</span>
+ <div class="list-group list-group-horizontal-sm">
+ <button
+ *ngFor="let team of teamsInMatch(match);"
+ (click)="setTeamInMatch({
+ teamNumber: team.number,
+ matchNumber: match.matchNumber(),
+ compLevel: match.compLevel()
+ })"
+ class="text-center text-white fw-bold
+ list-group-item list-group-item-action"
+ [ngClass]="team.color">
+ {{ team.number }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <span class="progress_message" role="alert">{{ progressMessage }}</span>
+ <span class="error_message" role="alert">{{ errorMessage }}</span>
+</div>