From 70d4fadb31e38bfb958b3621e8c7b52c8b2b3faa Mon Sep 17 00:00:00 2001 From: Jason McCollum Date: Tue, 30 Jan 2024 13:48:54 -0800 Subject: [PATCH] Rearrange welcome page Update cloud button Update welcome page entry points Update text Update text Update style Create scenario-solution help dialog Display help in upload dialog Update welcome page styling Add zip icon Change upload dialog size Update upload dialog layout Add CSV layout Add CSV help dialog Fix test Update branding Change JSON upload layout Update place ids flow Go directly into application upload file upload Open upload dialog when CSV upload is closed on landing page Update cloud upload icon Add license info Use custom ErrorStateMatcher on file upload input Add close buttons to dialogs Add close buttons to dialogs Style close buttons Update tests Fix style Adjust margin on logo Add icons Update app logo --- application/frontend/src/app/app.module.ts | 2 + .../src/app/core/actions/upload.actions.ts | 7 +- .../core/components/logo/logo.component.html | 8 +- .../core/components/logo/logo.component.scss | 23 +-- .../app/core/containers/app/app.component.ts | 5 + .../csv-help-dialog.component.html | 16 ++ .../csv-help-dialog.component.scss | 0 .../csv-help-dialog.component.spec.ts | 33 +++ .../csv-help-dialog.component.ts | 25 +++ .../main-actions/main-actions.component.ts | 2 +- ...enario-solution-help-dialog.component.html | 20 ++ ...enario-solution-help-dialog.component.scss | 0 ...rio-solution-help-dialog.component.spec.ts | 33 +++ ...scenario-solution-help-dialog.component.ts | 25 +++ .../upload-dialog/upload-dialog.component.css | 23 --- .../upload-dialog.component.html | 192 +++++++++++------- .../upload-dialog.component.scss | 55 +++++ .../upload-dialog/upload-dialog.component.ts | 64 +++++- .../frontend/src/app/core/core.module.ts | 2 + .../app/core/effects/upload.effects.spec.ts | 2 +- .../src/app/core/effects/upload.effects.ts | 14 +- .../src/app/core/reducers/ui.reducer.ts | 11 +- .../src/app/core/selectors/ui.selectors.ts | 5 + .../welcome-page/welcome-page.component.html | 85 ++++---- ...ponent.css => welcome-page.component.scss} | 63 +++--- .../welcome-page/welcome-page.component.ts | 8 +- .../frontend/src/assets/images/bookmark.svg | 4 + .../frontend/src/assets/images/csv2.svg | 10 + .../src/assets/images/help-filled.svg | 4 + .../frontend/src/assets/images/input.svg | 1 + .../frontend/src/assets/images/maps.svg | 2 +- .../frontend/src/assets/images/zip.svg | 3 + application/frontend/src/styles/main.scss | 24 +++ 33 files changed, 576 insertions(+), 195 deletions(-) create mode 100644 application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.html create mode 100644 application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.scss create mode 100644 application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.spec.ts create mode 100644 application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.ts create mode 100644 application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.html create mode 100644 application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.scss create mode 100644 application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.spec.ts create mode 100644 application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.ts delete mode 100644 application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.css create mode 100644 application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.scss rename application/frontend/src/app/welcome/containers/welcome-page/{welcome-page.component.css => welcome-page.component.scss} (57%) create mode 100644 application/frontend/src/assets/images/bookmark.svg create mode 100644 application/frontend/src/assets/images/csv2.svg create mode 100644 application/frontend/src/assets/images/help-filled.svg create mode 100644 application/frontend/src/assets/images/input.svg create mode 100644 application/frontend/src/assets/images/zip.svg diff --git a/application/frontend/src/app/app.module.ts b/application/frontend/src/app/app.module.ts index 6423e530..f0f78dd4 100644 --- a/application/frontend/src/app/app.module.ts +++ b/application/frontend/src/app/app.module.ts @@ -48,6 +48,7 @@ import { } from './core/effects'; import { HttpClientModule } from '@angular/common/http'; import { initApp } from './app-initializer'; +import { ScenarioSolutionHelpDialogComponent } from './core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component'; @NgModule({ imports: [ @@ -105,5 +106,6 @@ import { initApp } from './app-initializer'; }, ], bootstrap: [AppComponent], + declarations: [ScenarioSolutionHelpDialogComponent], }) export class AppModule {} diff --git a/application/frontend/src/app/core/actions/upload.actions.ts b/application/frontend/src/app/core/actions/upload.actions.ts index b6781b49..e05c0613 100644 --- a/application/frontend/src/app/core/actions/upload.actions.ts +++ b/application/frontend/src/app/core/actions/upload.actions.ts @@ -7,11 +7,14 @@ * https://opensource.org/licenses/MIT. */ -import { createAction } from '@ngrx/store'; +import { createAction, props } from '@ngrx/store'; export const openDialog = createAction('[Upload] Open Dialog'); export const closeDialog = createAction('[Upload] Close Dialog'); -export const openCsvDialog = createAction('[Upload] Open CSV Dialog'); +export const openCsvDialog = createAction( + '[Upload] Open CSV Dialog', + props<{ openUploadDialogOnClose?: boolean }>() +); export const closeCsvDialog = createAction('[Upload] Close CSV Dialog'); diff --git a/application/frontend/src/app/core/components/logo/logo.component.html b/application/frontend/src/app/core/components/logo/logo.component.html index b70f1566..71dfaeb7 100644 --- a/application/frontend/src/app/core/components/logo/logo.component.html +++ b/application/frontend/src/app/core/components/logo/logo.component.html @@ -1,8 +1,8 @@ - - Cloud
Optimization AI
+ title="Google Maps Platform" + aria-label="Google Maps Platform"> + + Google
Maps Platform
diff --git a/application/frontend/src/app/core/components/logo/logo.component.scss b/application/frontend/src/app/core/components/logo/logo.component.scss index f9e7f393..918073f2 100644 --- a/application/frontend/src/app/core/components/logo/logo.component.scss +++ b/application/frontend/src/app/core/components/logo/logo.component.scss @@ -1,6 +1,6 @@ :host { display: flex; - margin: 1rem 0 0 0; + margin: 1rem; } .branding, @@ -10,7 +10,7 @@ align-items: center; position: relative; top: 2px; - margin: 4px 12px; + margin: 0; user-select: none; white-space: nowrap; } @@ -19,19 +19,16 @@ position: relative; top: -1.5px; color: #5f6368; - font-family: 'Product Sans', Arial, sans-serif; - font-size: 20px; + font-family: 'Google Sans', 'Roboto', Arial, sans-serif; + font-style: normal; + font-size: 22px; font-weight: 400; - line-height: 24px; - padding-left: 4px; + line-height: 28px; text-rendering: optimizeLegibility; text-align: left; } -// a.branding:focus { -// text-decoration: none; -// } - -// a.branding:focus .branding__name { -// text-decoration: underline; -// } +.branding__logo { + position: relative; + top: -4px; +} diff --git a/application/frontend/src/app/core/containers/app/app.component.ts b/application/frontend/src/app/core/containers/app/app.component.ts index 329c2a52..6004b133 100644 --- a/application/frontend/src/app/core/containers/app/app.component.ts +++ b/application/frontend/src/app/core/containers/app/app.component.ts @@ -45,17 +45,21 @@ export class AppComponent { 'arrow_downward', 'arrow_drop_down', 'arrow_upward', + 'bookmark', 'calendar', 'clear', 'clock', 'cloud_download', 'cloud_upload', 'csv', + 'csv2', 'delete', 'depot_outline', 'dropoff', 'edit', 'help', + 'help-filled', + 'input', 'map', 'maps', 'more_vert', @@ -79,6 +83,7 @@ export class AppComponent { 'select_bbox_active', 'select_polygon', 'select_polygon_active', + 'zip', ]; icons.forEach((icon) => matIconRegistry.addSvgIcon( diff --git a/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.html b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.html new file mode 100644 index 00000000..904ad61e --- /dev/null +++ b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.html @@ -0,0 +1,16 @@ +

+ +
What data can I upload via CSV?
+

+

+ If you have data for vehicles, shipments, or vehicle operators in individual .csv files, you can + use this option to upload your data. You can also use this option get sample files to see how your + data should be structured. +

diff --git a/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.scss b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.spec.ts b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.spec.ts new file mode 100644 index 00000000..f2500c72 --- /dev/null +++ b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.spec.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CsvHelpDialogComponent } from './csv-help-dialog.component'; +import { MatDialogRef } from '@angular/material/dialog'; + +describe('CsvHelpDialogComponent', () => { + let component: CsvHelpDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CsvHelpDialogComponent], + providers: [{ provide: MatDialogRef, useValue: {} }], + }).compileComponents(); + + fixture = TestBed.createComponent(CsvHelpDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.ts b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.ts new file mode 100644 index 00000000..d13ea9cc --- /dev/null +++ b/application/frontend/src/app/core/containers/csv-help-dialog/csv-help-dialog.component.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-csv-help-dialog', + templateUrl: './csv-help-dialog.component.html', + styleUrls: ['./csv-help-dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CsvHelpDialogComponent { + constructor(private dialogRef: MatDialogRef) {} + + cancel(): void { + this.dialogRef.close(); + } +} diff --git a/application/frontend/src/app/core/containers/main-actions/main-actions.component.ts b/application/frontend/src/app/core/containers/main-actions/main-actions.component.ts index 0c607844..1a3084d8 100644 --- a/application/frontend/src/app/core/containers/main-actions/main-actions.component.ts +++ b/application/frontend/src/app/core/containers/main-actions/main-actions.component.ts @@ -59,7 +59,7 @@ export class MainActionsComponent implements OnInit { } onCsvUpload(): void { - this.store.dispatch(UploadActions.openCsvDialog()); + this.store.dispatch(UploadActions.openCsvDialog({})); } onCSVDownload(): void { diff --git a/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.html b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.html new file mode 100644 index 00000000..c7ecbb63 --- /dev/null +++ b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.html @@ -0,0 +1,20 @@ +

+ +
What's the difference between a scenario and a solution?
+

+

+ A scenario is a vehicle routing problem (VRP) request that Fleet Routing can solve. It consists of + one or more vehicles and one or more shipments to be delivered. +

+ +

+ A solution is what the Fleet Routing API produces in response to a request to solve a scenario. + Solutions are visible in FleetRouting App via the Gantt chart, on a map, or in the Metadata view. +

diff --git a/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.scss b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.spec.ts b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.spec.ts new file mode 100644 index 00000000..64820ea7 --- /dev/null +++ b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.spec.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScenarioSolutionHelpDialogComponent } from './scenario-solution-help-dialog.component'; +import { MatDialogRef } from '@angular/material/dialog'; + +describe('ScenarioSolutionHelpDialogComponent', () => { + let component: ScenarioSolutionHelpDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ScenarioSolutionHelpDialogComponent], + providers: [{ provide: MatDialogRef, useValue: {} }], + }).compileComponents(); + + fixture = TestBed.createComponent(ScenarioSolutionHelpDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.ts b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.ts new file mode 100644 index 00000000..c01e008b --- /dev/null +++ b/application/frontend/src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-scenario-solution-help-dialog', + templateUrl: './scenario-solution-help-dialog.component.html', + styleUrls: ['./scenario-solution-help-dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ScenarioSolutionHelpDialogComponent { + constructor(private dialogRef: MatDialogRef) {} + + cancel(): void { + this.dialogRef.close(); + } +} diff --git a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.css b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.css deleted file mode 100644 index 10dcee72..00000000 --- a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.css +++ /dev/null @@ -1,23 +0,0 @@ -.file-input__form-field { - display: flex; - align-items: center; -} - -.file-input__form-field mat-form-field.mat-form-field { - flex-grow: 1; - width: auto; -} - -.file-input__form-field :last-child { - flex-grow: 0; - margin-left: 16px; -} - -.file-input input[type='file'] { - opacity: 0; - height: 0; -} - -.mat-dialog-content { - overflow-x: hidden; -} diff --git a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.html b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.html index a4b27e7f..3c3b84ab 100644 --- a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.html +++ b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.html @@ -1,31 +1,66 @@
-

Upload an existing scenario or solution

+

+ + Upload a scenario or solution +

-

- Have an existing vehicle routing problem you want to solve? You can upload a JSON description - of the problem to see the routes that Cloud Fleet Routing recommends, and then download the - generated routes if you wish. -

-

- You may also upload a zip file containing a scenario and its matching solution. In order to do - this, please adhere to the following requirements: -

-
    -
  • The zip file contains two files named scenario.json and solution.json
  • -
  • The provided solution is for the provided scenario
  • -
-
-
- + +

What file types can I use?

+

+ Load scenario and solution files + + + +

+
+
+
+
+
+ +
+ .json file +
+

+ Use this option when you have a .json file that contains a VRP scenario you want to + solve. +

+
+
+
+
+ +
+ .zip file +
+

+ Use this option when you have a .zip file that contains both the .json and VRP scenario + files. The .zip file must contain the scenario.json and + solution.json +

+
+
+
+ {{ (messages$ | async)?.formFileRequired || 'File is required' }} @@ -38,69 +73,84 @@

Upload an existing scenario or solution

{{ (messages$ | async)?.formInvalidRequestFormat || 'Invalid request format' }} + .json or .zip
-
-

Validating upload

- -
-
-

- ⚠️ This scenario contains Place ID waypoints. Some features of the application require - lat/lng locations. Click the button below to request the lat/lng location of all Place - IDs in the scenario (it will take a few minutes). You may continue with just the Place - IDs, but some parts of the application may not work properly. -

- -
+ +
+
+

+ ⚠️ This scenario contains Place ID waypoints. Some features of the application require + lat/lng locations. Click the button below to request the lat/lng location of all Place + IDs in the scenario (it will take a few minutes). You may continue with just the Place + IDs, but some parts of the application may not work properly. +

+ +
-
- - Requesting Place ID locations... ({{ placeIdProgress | number }} of - {{ placeIdCount | number }}) -
+
+ + Requesting Place ID locations... ({{ placeIdProgress | number }} of + {{ placeIdCount | number }}) +
- - {{ placeIdError }} - + + {{ placeIdError }} + +
+ + +
+
+

+ Load shipment, vehicle, and operator data individually + + + +

+
+
+ + .csv files +
+

+ Use this option when you have a CSV files that contain shipment, vehicle and/or vehicle + operator data. +

+
+
- -
-
-
- Cancel -
diff --git a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.scss b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.scss new file mode 100644 index 00000000..1ed6e4ab --- /dev/null +++ b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.scss @@ -0,0 +1,55 @@ +@import '../../../../styles/variables.scss'; + +.file-input input[type='file'] { + opacity: 0; + height: 0; +} + +.mat-dialog-content { + overflow-x: hidden; +} + +.scenario-solution-help-icon { + display: inline-flex; + + mat-icon { + width: 20px; + height: 20px; + } +} + +.upload-container { + border-radius: 0.625rem; + padding: 0.9375rem; + gap: 0.5rem; +} + +.json-upload-container { + background: rgba(66, 133, 244, 0.05); +} + +.csv-upload-container { + background: rgba(189, 189, 189, 0.15); +} + +.json-description-container { + gap: 1rem; +} + +.json-description { + flex-grow: 1; + width: 0; +} + +.description-text { + color: $gray; +} + +.white-button { + border: 1px solid #4285f480; + background: #fff; +} + +.json-icon { + color: $blue; +} diff --git a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.ts b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.ts index 63dba81c..2cb8b878 100644 --- a/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.ts +++ b/application/frontend/src/app/core/containers/upload-dialog/upload-dialog.component.ts @@ -9,12 +9,14 @@ import { ChangeDetectionStrategy, Component, ElementRef, ViewChild } from '@angular/core'; import { + FormGroupDirective, + NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, } from '@angular/forms'; -import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef, MatDialogState } from '@angular/material/dialog'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { merge } from 'lodash'; @@ -26,16 +28,37 @@ import { IWaypoint } from '../../models/dispatcher.model'; import * as fromConfig from '../../selectors/config.selectors'; import { DispatcherService, FileService, PlacesService, UploadService } from '../../services'; import { toDispatcherLatLng } from 'src/app/util'; +import { ScenarioSolutionHelpDialogComponent } from 'src/app/core/containers/scenario-solution-help-dialog/scenario-solution-help-dialog.component'; +import { UploadActions } from '../../actions'; +import { CsvHelpDialogComponent } from '../csv-help-dialog/csv-help-dialog.component'; +import { ErrorStateMatcher } from '@angular/material/core'; + +class FileUploadErrorStateMatcher implements ErrorStateMatcher { + isErrorState( + control: UntypedFormControl | null, + ngForm: FormGroupDirective | NgForm | null + ): boolean { + const invalid = + ngForm?.errors?.required || + ngForm.errors?.zipContents || + ngForm.errors?.fileFormat || + ngForm.errors?.requestFormat || + control?.invalid; + const show = ngForm?.touched && ngForm?.dirty; + return !!(invalid && show); + } +} @Component({ selector: 'app-upload-dialog', templateUrl: './upload-dialog.component.html', - styleUrls: ['./upload-dialog.component.css'], + styleUrls: ['./upload-dialog.component.scss'], changeDetection: ChangeDetectionStrategy.Default, }) export class UploadDialogComponent { @ViewChild('fileInput', { static: true }) fileInput: ElementRef; + readonly fileUploadErrorStateMatcher = new FileUploadErrorStateMatcher(); readonly form: UntypedFormGroup; readonly fileName: UntypedFormControl; fileInvalid: boolean; @@ -63,6 +86,7 @@ export class UploadDialogComponent { constructor( private store: Store, private dialogRef: MatDialogRef, + private dialog: MatDialog, private dispatcherService: DispatcherService, private fileService: FileService, private placesService: PlacesService, @@ -75,6 +99,23 @@ export class UploadDialogComponent { this.messages$ = this.store.pipe(select(fromConfig.selectMessagesConfig)); } + openScenarioSolutionHelp(): void { + this.dialog.open(ScenarioSolutionHelpDialogComponent, { + maxWidth: '600px', + }); + } + + openCsvHelp(): void { + this.dialog.open(CsvHelpDialogComponent, { + maxWidth: '600px', + }); + } + + loadFromCsv(): void { + this.dialogRef.close(); + this.store.dispatch(UploadActions.openCsvDialog({ openUploadDialogOnClose: true })); + } + cancel(): void { this.dialogRef.close(); } @@ -90,7 +131,6 @@ export class UploadDialogComponent { if (!this.fileInput) { return; } - this.fileInput.nativeElement.click(); this.fileName.markAsTouched(); } @@ -99,7 +139,12 @@ export class UploadDialogComponent { (e.target as HTMLInputElement).value = null; } + onCancelSelectFile(): void { + this.fileName.markAsDirty(); + } + async fileSelected(e: Event): Promise { + this.fileName.markAsDirty(); const target = e.target as HTMLInputElement; const file = target && target.files && target.files[0]; if (!file) { @@ -125,6 +170,11 @@ export class UploadDialogComponent { this.scenario = null; this.fileName.setValue(file.name); this.validatingUpload = false; + + if (!this.scenarioHasPlaceIds) { + this.solve(); + } + return; } @@ -150,6 +200,10 @@ export class UploadDialogComponent { } this.validatingUpload = false; this.fileName.setValue(file.name); + + if (!this.zipContentsInvalid && !this.fileInvalid && !this.scenarioHasPlaceIds) { + this.solve(); + } } get scenarioHasPlaceIds(): boolean { @@ -222,6 +276,10 @@ export class UploadDialogComponent { } this.resolvingPlaceIds = false; + + if (!this.placeIdError) { + this.solve(); + } } } diff --git a/application/frontend/src/app/core/core.module.ts b/application/frontend/src/app/core/core.module.ts index c04f4b09..28694d24 100644 --- a/application/frontend/src/app/core/core.module.ts +++ b/application/frontend/src/app/core/core.module.ts @@ -89,6 +89,7 @@ import { ConfirmBulkEditDialogComponent } from './components/confirm-bulk-edit-d import { VehicleOperatorsControlBarComponent } from './containers/vehicle-operators-control-bar/vehicle-operators-control-bar.component'; import { BaseEditVehicleOperatorDialogComponent } from './components/base-edit-vehicle-operator-dialog/base-edit-vehicle-operator-dialog.component'; import { PreSolveEditVehicleOperatorDialogComponent } from './containers/pre-solve-edit-vehicle-operator-dialog/pre-solve-edit-vehicle-operator-dialog.component'; +import { CsvHelpDialogComponent } from './containers/csv-help-dialog/csv-help-dialog.component'; export const COMPONENTS = [ BaseDocumentationDialogComponent, @@ -180,6 +181,7 @@ export const CONTAINERS = [ VehicleOperatorsControlBarComponent, PreSolveEditVehicleOperatorDialogComponent, BaseEditVehicleOperatorDialogComponent, + CsvHelpDialogComponent, ], exports: [COMPONENTS, CONTAINERS, CsvUploadDialogComponent], }) diff --git a/application/frontend/src/app/core/effects/upload.effects.spec.ts b/application/frontend/src/app/core/effects/upload.effects.spec.ts index 6ca6330d..a0fc9a36 100644 --- a/application/frontend/src/app/core/effects/upload.effects.spec.ts +++ b/application/frontend/src/app/core/effects/upload.effects.spec.ts @@ -86,7 +86,7 @@ describe('UploadEffects', () => { expect(matDialog.getDialogById).toHaveBeenCalledWith(Modal.Upload); expect(matDialog.open).toHaveBeenCalledWith(UploadDialogComponent, { id: Modal.Upload, - maxWidth: jasmine.any(String), + width: jasmine.any(String), }); }); }); diff --git a/application/frontend/src/app/core/effects/upload.effects.ts b/application/frontend/src/app/core/effects/upload.effects.ts index ecdbe658..d2026f44 100644 --- a/application/frontend/src/app/core/effects/upload.effects.ts +++ b/application/frontend/src/app/core/effects/upload.effects.ts @@ -27,6 +27,7 @@ import { Modal } from '../models'; import { UploadType } from '../models/upload'; import * as fromUI from '../selectors/ui.selectors'; import { MessageService, NormalizationService } from '../services'; +import { forkJoin, of } from 'rxjs'; @Injectable() export class UploadEffects { @@ -46,8 +47,17 @@ export class UploadEffects { }) .afterClosed() ), - mergeMap((dialogResult) => { + mergeMap((dialogResult) => + forkJoin([ + of(dialogResult), + this.store.pipe(select(fromUI.selectOpenUploadDialogOnClose), first()), + ]) + ), + mergeMap(([dialogResult, openUploadDialog]) => { if (!dialogResult) { + if (openUploadDialog) { + return [UploadActions.openDialog()]; + } return []; } const actions: Action[] = [UploadActions.closeCsvDialog()]; @@ -67,7 +77,7 @@ export class UploadEffects { this.dialog .open(UploadDialogComponent, { id: Modal.Upload, - maxWidth: '420px', + width: '700px', }) .afterClosed() ), diff --git a/application/frontend/src/app/core/reducers/ui.reducer.ts b/application/frontend/src/app/core/reducers/ui.reducer.ts index b7b3b18a..b34b82d0 100644 --- a/application/frontend/src/app/core/reducers/ui.reducer.ts +++ b/application/frontend/src/app/core/reducers/ui.reducer.ts @@ -38,6 +38,7 @@ export interface State { mouseOverId?: number; page: Page; splitSizes?: number[]; + openUploadDialogOnClose?: boolean; } export const initialState: State = { @@ -48,6 +49,7 @@ export const initialState: State = { mouseOverId: null, page: Page.Welcome, splitSizes: [50, 50], + openUploadDialogOnClose: false, }; export const reducer = createReducer( @@ -58,7 +60,11 @@ export const reducer = createReducer( mouseOverId: null, })), on(WelcomePageActions.initialize, () => ({ ...initialState })), - on(UploadActions.openCsvDialog, (state) => ({ ...state, modal: Modal.CsvUpload })), + on(UploadActions.openCsvDialog, (state, action) => ({ + ...state, + modal: Modal.CsvUpload, + openUploadDialogOnClose: action.openUploadDialogOnClose, + })), on(DownloadActions.downloadPDF, (state) => ({ ...state, modal: Modal.DownloadPDF })), on(UploadActions.openDialog, WelcomePageActions.openUploadDialog, (state) => ({ ...state, @@ -128,3 +134,6 @@ export const selectHasMap = (state: State): boolean => state.hasMap; export const selectSplitSizes = (state: State): number[] => state.splitSizes; export const selectMouseOverId = (state: State): number => state.mouseOverId; + +export const selectOpenUploadDialogOnClose = (state: State): boolean => + state.openUploadDialogOnClose; diff --git a/application/frontend/src/app/core/selectors/ui.selectors.ts b/application/frontend/src/app/core/selectors/ui.selectors.ts index 5a0cb212..09f35b61 100644 --- a/application/frontend/src/app/core/selectors/ui.selectors.ts +++ b/application/frontend/src/app/core/selectors/ui.selectors.ts @@ -34,3 +34,8 @@ export const selectStarted = createSelector( ); export const selectMouseOverId = createSelector(selectUIState, fromUI.selectMouseOverId); + +export const selectOpenUploadDialogOnClose = createSelector( + selectUIState, + fromUI.selectOpenUploadDialogOnClose +); diff --git a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.html b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.html index 8518fba2..ed354919 100644 --- a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.html +++ b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.html @@ -1,57 +1,66 @@
- - Cloud Fleet Routing API + What is Fleet Routing App?

- Cloud Fleet Routing solves your vehicle routing problems rapidly and at massive scale. It - supports capacities, costs, time windows, breaks, delays, traffic, heterogeneous fleets, and - different modalities (driving, bicycling, or walking). More information is available in the - app help docs or on our - documentation site. + Fleet Routing App is an open source web application intended to showcase the functionality of + GMP Route Optimization API (formerly Cloud Fleet Routing / CFR), and help developers better + evaluate and onboard to the product. It also serves as a sample UI implementation that + demonstrates how a dispatcher planning app integrates with the Route Optimization API.

- For questions, reach out at - cloud-fleet-routing-support@google.com. Try advanced features. + GMP Route Optimization API solves Vehicle Routing Problems (VRPs) for developers from across + different industries such as Logistics, On Demand Deliveries, Field Services and more.

- By using this demo app you agree to the terms and conditions of the following license: - Google Maps Platform + Learn more

-
-
- -
+

Select one of the options below to start.

+
+ +
-
-
-
- -
-
- -
+
-
- - - +
+

More information

+

+ App help docs +

+

For questions, reach out at gmpro-support@google.com.

+

+ By using this demo app you agree to the terms and conditions of the following license: + Google Maps Platform +

diff --git a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.css b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.scss similarity index 57% rename from application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.css rename to application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.scss index 42643267..2af61340 100644 --- a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.css +++ b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.scss @@ -1,6 +1,8 @@ +@import '../../../../styles/variables.scss'; + :host { display: block; - text-align: center; + text-align: justify; } .page-content { @@ -30,6 +32,10 @@ section:last-child { margin-top: 8px; } +.intro p { + margin-bottom: 1.6rem; +} + .intro > :last-child { margin-bottom: 0; } @@ -52,17 +58,15 @@ section:last-child { } .entry-points { - flex-wrap: wrap; - justify-content: space-around; -} + border-radius: 0.625rem; + background: rgba(66, 133, 244, 0.05); -.entry-points > .col { display: flex; -} - -.entry-points button { - margin: 16px 8px 0; - flex-shrink: 0; + padding: 0.9375rem; + flex-direction: column; + align-items: flex-start; + gap: 1.5625rem; + align-self: stretch; } .cloud-invisible { @@ -73,29 +77,30 @@ section:last-child { visibility: visible; } -@media only screen and (max-width: 1024px) { - .intro, - .guide { - margin-bottom: 0; - } - - .intro .mat-title, - .intro p { - margin-bottom: 4px; - } +#page-title { + font-size: 2rem; + font-weight: 500; + line-height: 160%; +} - .metric svg { - margin-right: 10px; - } +.cloud-button { + display: flex; + justify-content: center; + align-items: center; - .entry-points button { - margin-top: 8px; + svg { + fill: $blue !important; } } -#page-title { +.main-entry-points { display: flex; - flex-direction: row; - justify-content: center; - align-items: center; + align-items: flex-start; + gap: 1rem; + border-radius: var(--none, 0rem); +} + +.entry-point-button { + border: 1px solid #4285f480; + background: #fff; } diff --git a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.ts b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.ts index 39f73b6a..3900fb7b 100644 --- a/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.ts +++ b/application/frontend/src/app/welcome/containers/welcome-page/welcome-page.component.ts @@ -10,7 +10,7 @@ import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { DocumentationActions, StorageApiActions, UploadActions } from 'src/app/core/actions'; +import { DocumentationActions, StorageApiActions } from 'src/app/core/actions'; import { selectHasStorageApiRoot } from 'src/app/core/selectors/config.selectors'; import { State } from 'src/app/reducers'; import { WelcomePageActions } from '../../actions'; @@ -19,7 +19,7 @@ import * as fromConfig from 'src/app/core/selectors/config.selectors'; @Component({ selector: 'app-welcome-page', templateUrl: './welcome-page.component.html', - styleUrls: ['./welcome-page.component.css'], + styleUrls: ['./welcome-page.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class WelcomePageComponent implements OnInit { @@ -47,10 +47,6 @@ export class WelcomePageComponent implements OnInit { this.store.dispatch(StorageApiActions.openLoadDialog()); } - loadFromCsv(): void { - this.store.dispatch(UploadActions.openCsvDialog()); - } - onHelp(): void { this.store.dispatch(DocumentationActions.open()); } diff --git a/application/frontend/src/assets/images/bookmark.svg b/application/frontend/src/assets/images/bookmark.svg new file mode 100644 index 00000000..d78257e3 --- /dev/null +++ b/application/frontend/src/assets/images/bookmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/application/frontend/src/assets/images/csv2.svg b/application/frontend/src/assets/images/csv2.svg new file mode 100644 index 00000000..cd6aea40 --- /dev/null +++ b/application/frontend/src/assets/images/csv2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/application/frontend/src/assets/images/help-filled.svg b/application/frontend/src/assets/images/help-filled.svg new file mode 100644 index 00000000..6d81ed39 --- /dev/null +++ b/application/frontend/src/assets/images/help-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/application/frontend/src/assets/images/input.svg b/application/frontend/src/assets/images/input.svg new file mode 100644 index 00000000..4f74c9af --- /dev/null +++ b/application/frontend/src/assets/images/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/application/frontend/src/assets/images/maps.svg b/application/frontend/src/assets/images/maps.svg index e21c2c68..7a884da3 100644 --- a/application/frontend/src/assets/images/maps.svg +++ b/application/frontend/src/assets/images/maps.svg @@ -1 +1 @@ - \ No newline at end of file +Maps_Pin_FullColor \ No newline at end of file diff --git a/application/frontend/src/assets/images/zip.svg b/application/frontend/src/assets/images/zip.svg new file mode 100644 index 00000000..dfe842fa --- /dev/null +++ b/application/frontend/src/assets/images/zip.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/application/frontend/src/styles/main.scss b/application/frontend/src/styles/main.scss index 961ff799..8664c0f5 100644 --- a/application/frontend/src/styles/main.scss +++ b/application/frontend/src/styles/main.scss @@ -882,3 +882,27 @@ app-csv-upload-dialog app-bulk-edit-unset { color: #f44336; } + +.strong { + font-weight: 500 !important; +} + +.blue-svg-icon mat-icon svg path { + fill: $blue; +} + +.small-close-button { + margin-left: auto !important; + width: 24px !important; + height: 24px !important; + line-height: 24px !important; + + margin-right: -0.5rem !important; + margin-top: -0.5rem !important; + + .mat-button-wrapper .mat-icon { + width: 20px !important; + height: 20px !important; + line-height: 20px !important; + } +}