diff --git a/web/package.json b/web/package.json index d66fdbcd0..58cbf7fa3 100644 --- a/web/package.json +++ b/web/package.json @@ -8,8 +8,9 @@ "build": "ng build -c $npm_config_config", "postbuild": "npm run --prefix scripts copy-asset-links", "build-all": "npm run build-local-deps && npm run build", - "build-and-test": "npm run build && npm run test", - "build-all-and-test": "npm run build-all && npm run test", + "build-and-test": "npm run build --config=local && npm run test", + "build-all-and-test": "npm run build-all --config=local && npm run test", + "build-all-and-test-headless": "npm run build-all --config=local && npm run test-headless", "watch": "npm run build -- --watch", "start": "ng serve -c $npm_config_config", "build-and-start": "npm run build && npm run start", diff --git a/web/scripts/copy-asset-links.js b/web/scripts/copy-asset-links.js index 61e93c2ed..7b113076c 100644 --- a/web/scripts/copy-asset-links.js +++ b/web/scripts/copy-asset-links.js @@ -38,5 +38,5 @@ if (existsSync(assertLinksFilepath)) { writeFileSync(`${wellKnownDir}/assetLinks.json`, assertLinks); } else { - console.warn('Missing asserLinks.json file'); + console.warn('Warning: Missing assetLinks.json file'); } diff --git a/web/src/app/components/header/header.component.spec.ts b/web/src/app/components/header/header.component.spec.ts index 8179f1285..9701842cc 100644 --- a/web/src/app/components/header/header.component.spec.ts +++ b/web/src/app/components/header/header.component.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatDialog} from '@angular/material/dialog'; import {MatMenuModule} from '@angular/material/menu'; @@ -46,6 +47,7 @@ describe('HeaderComponent', () => { {provide: Router, useValue: {events: of()}}, {provide: SurveyService, useValue: {canManageSurvey: () => false}}, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); diff --git a/web/src/app/components/loi-editor/loi-editor.component.spec.ts b/web/src/app/components/loi-editor/loi-editor.component.spec.ts index 96d1c335c..b60ac801c 100644 --- a/web/src/app/components/loi-editor/loi-editor.component.spec.ts +++ b/web/src/app/components/loi-editor/loi-editor.component.spec.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {GoogleMapsModule} from '@angular/google-maps'; import {MatDialog} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; import {List, Map} from 'immutable'; import {ImportDialogComponent} from 'app/components/import-dialog/import-dialog.component'; @@ -79,8 +82,9 @@ describe('LoiEditorComponent', () => { matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']); TestBed.configureTestingModule({ - imports: [GoogleMapsModule], + imports: [GoogleMapsModule, MatSlideToggleModule, MatIconModule], declarations: [LoiEditorComponent], + schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: DataStoreService, diff --git a/web/src/app/components/loi-selection/loi-selection.component.spec.ts b/web/src/app/components/loi-selection/loi-selection.component.spec.ts index fd777a614..00b4679ea 100644 --- a/web/src/app/components/loi-selection/loi-selection.component.spec.ts +++ b/web/src/app/components/loi-selection/loi-selection.component.spec.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {GoogleMapsModule} from '@angular/google-maps'; import {MatDialog} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; +import {MatListModule} from '@angular/material/list'; import {List, Map} from 'immutable'; import {Coordinate} from 'app/models/geometry/coordinate'; @@ -75,7 +78,12 @@ describe('LoiSelectionComponent', () => { matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']); TestBed.configureTestingModule({ - imports: [GoogleMapsModule, GroundIconModule], + imports: [ + GoogleMapsModule, + GroundIconModule, + MatListModule, + MatIconModule, + ], declarations: [LoiSelectionComponent], providers: [ { @@ -87,6 +95,7 @@ describe('LoiSelectionComponent', () => { useValue: matDialogSpy, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(LoiSelectionComponent); diff --git a/web/src/app/components/share-list/share-list.component.spec.ts b/web/src/app/components/share-list/share-list.component.spec.ts index a482bd8fd..7fcd021f8 100644 --- a/web/src/app/components/share-list/share-list.component.spec.ts +++ b/web/src/app/components/share-list/share-list.component.spec.ts @@ -19,6 +19,7 @@ import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {MatListModule} from '@angular/material/list'; import {MatListHarness} from '@angular/material/list/testing'; +import {MatSelectModule} from '@angular/material/select'; import {Map} from 'immutable'; import {Subject, firstValueFrom, of} from 'rxjs'; @@ -55,7 +56,7 @@ describe('ShareListComponent', () => { {type: DataSharingType.PRIVATE} ); - const user = new User('', '', true); + const user = new User('user1', 'user1@gmail.com', true); beforeEach(waitForAsync(() => { draftSurveyServiceSpy = jasmine.createSpyObj( @@ -74,7 +75,7 @@ describe('ShareListComponent', () => { TestBed.configureTestingModule({ declarations: [ShareListComponent], - imports: [MatListModule], + imports: [MatListModule, MatSelectModule], providers: [ {provide: DraftSurveyService, useValue: draftSurveyServiceSpy}, {provide: AuthService, useValue: authServiceSpy}, @@ -95,10 +96,10 @@ describe('ShareListComponent', () => { it('updates itself when acl changes', async () => { activeSurvey$.next(survey); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.whenStable().then(async () => { - expect(component.acl?.length).toBe(0); - }); + expect(component.acl?.length).toBe(0); activeSurvey$.next( new Survey( @@ -107,18 +108,18 @@ describe('ShareListComponent', () => { surveyDescription, /* jobs= */ Map(), /* acl= */ Map({a: Role.OWNER, b: Role.OWNER}), - /* ownerId= */ '', + /* ownerId= */ 'user1', {type: DataSharingType.PRIVATE} ) ); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.whenStable().then(async () => { - expect(component.acl?.length).toBe(2); + expect(component.acl?.length).toBe(2); - const aclList = await loader.getHarness(MatListHarness); - const aclListItems = await aclList.getItems(); + const aclList = await loader.getHarness(MatListHarness); + const aclListItems = await aclList.getItems(); - expect(aclListItems.length).toBe(2); - }); + expect(aclListItems.length).toBe(3); }); }); diff --git a/web/src/app/components/share-survey/share-survey.component.spec.ts b/web/src/app/components/share-survey/share-survey.component.spec.ts index 14037f6a7..e45cefcda 100644 --- a/web/src/app/components/share-survey/share-survey.component.spec.ts +++ b/web/src/app/components/share-survey/share-survey.component.spec.ts @@ -1,4 +1,6 @@ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatCardModule} from '@angular/material/card'; import {MatDialogModule} from '@angular/material/dialog'; import {MatIconModule} from '@angular/material/icon'; @@ -10,8 +12,9 @@ describe('ShareSurveyComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MatIconModule, MatDialogModule], + imports: [MatIconModule, MatDialogModule, MatCardModule], declarations: [ShareSurveyComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ShareSurveyComponent); diff --git a/web/src/app/components/sign-in-page/sign-in-page.component.spec.ts b/web/src/app/components/sign-in-page/sign-in-page.component.spec.ts index 9b3b4c50c..521fc5220 100644 --- a/web/src/app/components/sign-in-page/sign-in-page.component.spec.ts +++ b/web/src/app/components/sign-in-page/sign-in-page.component.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {NavigationEnd, Router} from '@angular/router'; import {BehaviorSubject, NEVER, of} from 'rxjs'; @@ -41,6 +42,7 @@ describe('SignInPageComponent', () => { useValue: {getUser$: () => NEVER, isAuthenticated$: () => NEVER}, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); diff --git a/web/src/app/components/survey-list/survey-list.component.spec.ts b/web/src/app/components/survey-list/survey-list.component.spec.ts index d9c3f25bd..0a561122d 100644 --- a/web/src/app/components/survey-list/survey-list.component.spec.ts +++ b/web/src/app/components/survey-list/survey-list.component.spec.ts @@ -26,6 +26,7 @@ import {AngularFireAuth} from '@angular/fire/compat/auth'; import {AngularFirestore} from '@angular/fire/compat/firestore'; import {MatButtonModule} from '@angular/material/button'; import {MatCardModule} from '@angular/material/card'; +import {MatChipsModule} from '@angular/material/chips'; import {MatDialog, MatDialogRef} from '@angular/material/dialog'; import {MatGridListModule} from '@angular/material/grid-list'; import {MatIconModule} from '@angular/material/icon'; @@ -176,6 +177,7 @@ describe('SurveyListComponent', () => { MatCardModule, MatGridListModule, MatIconModule, + MatChipsModule, ], declarations: [SurveyListComponent, HeaderComponent], providers: [ diff --git a/web/src/app/components/tasks-editor/add-task-button/add-task-button.component.spec.ts b/web/src/app/components/tasks-editor/add-task-button/add-task-button.component.spec.ts index d65b48054..8a4869160 100644 --- a/web/src/app/components/tasks-editor/add-task-button/add-task-button.component.spec.ts +++ b/web/src/app/components/tasks-editor/add-task-button/add-task-button.component.spec.ts @@ -15,6 +15,7 @@ */ import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatIconModule} from '@angular/material/icon'; import {AddTaskButtonComponent} from './add-task-button.component'; @@ -24,6 +25,7 @@ describe('TaskButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [MatIconModule], declarations: [AddTaskButtonComponent], }).compileComponents(); diff --git a/web/src/app/pages/create-survey/create-survey.component.spec.ts b/web/src/app/pages/create-survey/create-survey.component.spec.ts index fcb3b896f..87ca040b5 100644 --- a/web/src/app/pages/create-survey/create-survey.component.spec.ts +++ b/web/src/app/pages/create-survey/create-survey.component.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import { ComponentFixture, TestBed, @@ -23,6 +24,7 @@ import { waitForAsync, } from '@angular/core/testing'; import {MatDialogModule} from '@angular/material/dialog'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {By} from '@angular/platform-browser'; import {ActivatedRoute} from '@angular/router'; import {List, Map} from 'immutable'; @@ -43,6 +45,7 @@ import {SurveyDetailsComponent} from 'app/pages/create-survey/survey-details/sur import {DraftSurveyService} from 'app/services/draft-survey/draft-survey.service'; import {JobService} from 'app/services/job/job.service'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; +import {SURVEY_ID_NEW} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; import {TaskService} from 'app/services/task/task.service'; @@ -198,7 +201,7 @@ describe('CreateSurveyComponent', () => { ]); TestBed.configureTestingModule({ - imports: [MatDialogModule], + imports: [MatDialogModule, MatProgressSpinnerModule], declarations: [ CreateSurveyComponent, SurveyDetailsComponent, @@ -206,6 +209,7 @@ describe('CreateSurveyComponent', () => { DataSharingTermsComponent, ShareSurveyComponent, ], + schemas: [NO_ERRORS_SCHEMA], providers: [ {provide: NavigationService, useValue: navigationServiceSpy}, {provide: SurveyService, useValue: surveyServiceSpy}, @@ -246,7 +250,7 @@ describe('CreateSurveyComponent', () => { describe('when no survey', () => { beforeEach(fakeAsync(() => { - surveyId$.next(NavigationService.SURVEY_ID_NEW); + surveyId$.next(SURVEY_ID_NEW); activeSurvey$.next(Survey.UNSAVED_NEW); tick(); fixture.detectChanges(); @@ -314,7 +318,7 @@ describe('CreateSurveyComponent', () => { describe('Survey Details', () => { describe('when no survey', () => { beforeEach(fakeAsync(() => { - surveyId$.next(NavigationService.SURVEY_ID_NEW); + surveyId$.next(SURVEY_ID_NEW); activeSurvey$.next(Survey.UNSAVED_NEW); tick(); fixture.detectChanges(); diff --git a/web/src/app/pages/create-survey/create-survey.component.ts b/web/src/app/pages/create-survey/create-survey.component.ts index 2b6fcf34f..052c4789c 100644 --- a/web/src/app/pages/create-survey/create-survey.component.ts +++ b/web/src/app/pages/create-survey/create-survey.component.ts @@ -31,6 +31,7 @@ import {TaskDetailsComponent} from 'app/pages/create-survey/task-details/task-de import {DraftSurveyService} from 'app/services/draft-survey/draft-survey.service'; import {JobService} from 'app/services/job/job.service'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; +import {SURVEY_ID_NEW} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; import {TaskService} from 'app/services/task/task.service'; @@ -155,7 +156,7 @@ export class CreateSurveyComponent implements OnInit { ngOnInit(): void { this.subscription.add( this.navigationService.getSurveyId$().subscribe(async surveyId => { - this.surveyId = surveyId ? surveyId : NavigationService.SURVEY_ID_NEW; + this.surveyId = surveyId ? surveyId : SURVEY_ID_NEW; this.surveyService.activateSurvey(this.surveyId); await this.draftSurveyService.init(this.surveyId); this.draftSurveyService @@ -172,8 +173,7 @@ export class CreateSurveyComponent implements OnInit { .pipe( filter( ([survey]) => - this.surveyId === NavigationService.SURVEY_ID_NEW || - survey.id === this.surveyId + this.surveyId === SURVEY_ID_NEW || survey.id === this.surveyId ) ) .subscribe(([survey, lois]) => { @@ -332,7 +332,7 @@ export class CreateSurveyComponent implements OnInit { private async saveSurveyTitleAndDescription(): Promise { const [name, description] = this.surveyDetails!.toTitleAndDescription(); - if (this.surveyId === NavigationService.SURVEY_ID_NEW) { + if (this.surveyId === SURVEY_ID_NEW) { return await this.surveyService.createSurvey(name, description); } diff --git a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.spec.ts b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.spec.ts index 3e22ca1b4..53baddf19 100644 --- a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.spec.ts +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.spec.ts @@ -15,7 +15,11 @@ */ import {CommonModule} from '@angular/common'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatRadioModule} from '@angular/material/radio'; import { DATA_SHARING_TYPE_DESCRIPTION, @@ -30,7 +34,13 @@ describe('DataSharingTermsComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [DataSharingTermsComponent], - imports: [CommonModule], + imports: [ + CommonModule, + MatCardModule, + MatRadioModule, + ReactiveFormsModule, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(DataSharingTermsComponent); diff --git a/web/src/app/pages/create-survey/survey-details/survey-details.component.spec.ts b/web/src/app/pages/create-survey/survey-details/survey-details.component.spec.ts index 90192fe42..37ee05b04 100644 --- a/web/src/app/pages/create-survey/survey-details/survey-details.component.spec.ts +++ b/web/src/app/pages/create-survey/survey-details/survey-details.component.spec.ts @@ -14,7 +14,12 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {SurveyDetailsComponent} from 'app/pages/create-survey/survey-details/survey-details.component'; @@ -26,6 +31,17 @@ describe('SurveyDetailsComponent', () => { const description = 'description'; beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SurveyDetailsComponent], + imports: [ + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + NoopAnimationsModule, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + fixture = TestBed.createComponent(SurveyDetailsComponent); component = fixture.componentInstance; component.title = title; diff --git a/web/src/app/pages/create-survey/task-details/task-details.component.spec.ts b/web/src/app/pages/create-survey/task-details/task-details.component.spec.ts index 9777ee345..d42f4f5b0 100644 --- a/web/src/app/pages/create-survey/task-details/task-details.component.spec.ts +++ b/web/src/app/pages/create-survey/task-details/task-details.component.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatDialogModule} from '@angular/material/dialog'; import {Map} from 'immutable'; @@ -55,6 +56,7 @@ describe('TaskDetailsComponent', () => { }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(TaskDetailsComponent); diff --git a/web/src/app/pages/edit-survey/edit-details/edit-details.component.spec.ts b/web/src/app/pages/edit-survey/edit-details/edit-details.component.spec.ts index 03292bd47..85d99f4e2 100644 --- a/web/src/app/pages/edit-survey/edit-details/edit-details.component.spec.ts +++ b/web/src/app/pages/edit-survey/edit-details/edit-details.component.spec.ts @@ -15,17 +15,20 @@ */ import {CommonModule} from '@angular/common'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks, } from '@angular/core/testing'; +import {MatCardModule} from '@angular/material/card'; import { MatDialog, MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; import {By} from '@angular/platform-browser'; import {Map} from 'immutable'; import {of} from 'rxjs'; @@ -82,7 +85,8 @@ describe('EditDetailsComponent', () => { await TestBed.configureTestingModule({ declarations: [EditDetailsComponent], - imports: [CommonModule, MatDialogModule], + imports: [CommonModule, MatDialogModule, MatCardModule, MatIconModule], + schemas: [NO_ERRORS_SCHEMA], providers: [ {provide: MatDialog, useValue: dialogSpy}, { diff --git a/web/src/app/pages/edit-survey/edit-job/edit-job.component.spec.ts b/web/src/app/pages/edit-survey/edit-job/edit-job.component.spec.ts index 39bff6d61..b96d31af8 100644 --- a/web/src/app/pages/edit-survey/edit-job/edit-job.component.spec.ts +++ b/web/src/app/pages/edit-survey/edit-job/edit-job.component.spec.ts @@ -14,20 +14,24 @@ * limitations under the License. */ +import {Component, Input} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import { MatButtonToggle, MatButtonToggleGroup, } from '@angular/material/button-toggle'; import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; import {By} from '@angular/platform-browser'; import {ActivatedRoute} from '@angular/router'; import {User} from 'firebase/auth'; -import {Map} from 'immutable'; +import {List, Map} from 'immutable'; import {Subject, from, of} from 'rxjs'; +import {LoiEditorComponent} from 'app/components/loi-editor/loi-editor.component'; import {TasksEditorModule} from 'app/components/tasks-editor/tasks-editor.module'; -import {Job} from 'app/models/job.model'; +import {DataCollectionStrategy, Job} from 'app/models/job.model'; +import {LocationOfInterest} from 'app/models/loi.model'; import {Role} from 'app/models/role.model'; import {DataSharingType, Survey} from 'app/models/survey.model'; import {EditJobComponent} from 'app/pages/edit-survey/edit-job/edit-job.component'; @@ -38,6 +42,23 @@ import {DraftSurveyService} from 'app/services/draft-survey/draft-survey.service import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; +@Component({ + selector: 'loi-editor', + template: '', + providers: [ + { + provide: LoiEditorComponent, + useExisting: MockLoiEditorComponent, + }, + ], +}) +class MockLoiEditorComponent { + @Input() canImport!: boolean; + @Input() survey!: Survey; + @Input() job!: Job; + @Input() lois!: List; +} + describe('EditJobComponent', () => { let component: EditJobComponent; let fixture: ComponentFixture; @@ -55,11 +76,12 @@ describe('EditJobComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EditJobComponent], + declarations: [EditJobComponent, MockLoiEditorComponent], imports: [ MatButtonToggleGroup, MatButtonToggle, MatDialogModule, + MatIconModule, TasksEditorModule, ], providers: [ diff --git a/web/src/app/pages/edit-survey/edit-survey.component.spec.ts b/web/src/app/pages/edit-survey/edit-survey.component.spec.ts index 63d97713d..f469b612a 100644 --- a/web/src/app/pages/edit-survey/edit-survey.component.spec.ts +++ b/web/src/app/pages/edit-survey/edit-survey.component.spec.ts @@ -14,16 +14,22 @@ * limitations under the License. */ -import {WritableSignal, signal} from '@angular/core'; +import {NO_ERRORS_SCHEMA, WritableSignal, signal} from '@angular/core'; import { ComponentFixture, TestBed, + discardPeriodicTasks, fakeAsync, + flush, tick, waitForAsync, } from '@angular/core/testing'; import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {By} from '@angular/platform-browser'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {ActivatedRoute} from '@angular/router'; import {RouterTestingModule} from '@angular/router/testing'; import {Map} from 'immutable'; @@ -106,10 +112,13 @@ describe('EditSurveyComponent', () => { 'getEditSurveyPageSignal', 'getSurveyId$', 'getSurveyId', + 'navigateToEditJob', + 'navigateToEditSurvey', ] ); navigationServiceSpy.getSurveyId$.and.returnValue(surveyId$); navigationServiceSpy.getSurveyId.and.returnValue(surveyIdSignal); + navigationServiceSpy.getEditSurveyPageSignal.and.returnValue(signal('')); route = new ActivatedRouteStub(); surveyServiceSpy = jasmine.createSpyObj('SurveyService', [ @@ -121,11 +130,12 @@ describe('EditSurveyComponent', () => { draftSurveyServiceSpy = jasmine.createSpyObj( 'DraftSurveyService', - ['init', 'getSurvey$', 'addOrUpdateJob', 'deleteJob'] + ['init', 'getSurvey$', 'addOrUpdateJob', 'deleteJob', 'getSurvey'] ); draftSurveyServiceSpy.getSurvey$.and.returnValue( new BehaviorSubject(survey) ); + draftSurveyServiceSpy.getSurvey.and.returnValue(survey); jobServiceSpy = jasmine.createSpyObj('JobService', [ 'createNewJob', @@ -133,6 +143,7 @@ describe('EditSurveyComponent', () => { 'getNextColor', ]); jobServiceSpy.createNewJob.and.returnValue(newJob); + jobServiceSpy.duplicateJob.and.returnValue(newJob); jobServiceSpy.getNextColor.and.returnValue(undefined); dataStoreServiceSpy = jasmine.createSpyObj( @@ -148,8 +159,15 @@ describe('EditSurveyComponent', () => { dialogSpy.open.and.returnValue(dialogRefSpy); TestBed.configureTestingModule({ - imports: [RouterTestingModule], + imports: [ + RouterTestingModule, + MatDividerModule, + MatMenuModule, + MatProgressSpinnerModule, + NoopAnimationsModule, + ], declarations: [EditSurveyComponent], + schemas: [NO_ERRORS_SCHEMA], providers: [ {provide: NavigationService, useValue: navigationServiceSpy}, {provide: SurveyService, useValue: surveyServiceSpy}, @@ -193,6 +211,9 @@ describe('EditSurveyComponent', () => { surveyIdSignal.set(surveyId); surveyId$.next(surveyId); activeSurvey$.next(survey); + // Manually set survey to ensure it's available for template rendering + // bypassing potential async effect timing issues in tests. + fixture.componentInstance.survey = survey; tick(); fixture.detectChanges(); })); @@ -233,7 +254,7 @@ describe('EditSurveyComponent', () => { }); describe('add/rename/duplicate/delete a job', () => { - it('add a job', () => { + it('add a job', fakeAsync(() => { const addButton = fixture.debugElement.query(By.css('#add-button')) .nativeElement as HTMLElement; const newJobName = 'new job name'; @@ -246,35 +267,45 @@ describe('EditSurveyComponent', () => { expect(draftSurveyServiceSpy.addOrUpdateJob).toHaveBeenCalledOnceWith( newJob.copyWith({name: newJobName}) ); - }); + flush(); + discardPeriodicTasks(); + })); - it('rename a job', () => { + it('rename a job', fakeAsync(() => { const menuButton = fixture.debugElement.query(By.css('#menu-button-0')) .nativeElement as HTMLElement; - const renameButton = fixture.debugElement.query( - By.css('#rename-button-0') - ).nativeElement as HTMLElement; const newJobName = 'new job name'; dialogRefSpy.afterClosed.and.returnValue( of({dialogType: DialogType.RenameJob, jobName: newJobName}) ); menuButton.click(); + fixture.detectChanges(); + tick(); + + const renameButton = document.querySelector( + '#rename-button-0' + ) as HTMLElement; renameButton.click(); expect(draftSurveyServiceSpy.addOrUpdateJob).toHaveBeenCalledOnceWith( job1.copyWith({name: newJobName}) ); - }); + flush(); + discardPeriodicTasks(); + })); - it('duplicate a job', () => { + it('duplicate a job', fakeAsync(() => { const menuButton = fixture.debugElement.query(By.css('#menu-button-0')) .nativeElement as HTMLElement; - const duplicateButton = fixture.debugElement.query( - By.css('#duplicate-button-0') - ).nativeElement as HTMLElement; menuButton.click(); + fixture.detectChanges(); + tick(); + + const duplicateButton = document.querySelector( + '#duplicate-button-0' + ) as HTMLElement; duplicateButton.click(); expect(draftSurveyServiceSpy.addOrUpdateJob).toHaveBeenCalledOnceWith( @@ -284,23 +315,30 @@ describe('EditSurveyComponent', () => { ), true ); - }); + flush(); + discardPeriodicTasks(); + })); - it('delete a job', () => { + it('delete a job', fakeAsync(() => { const menuButton = fixture.debugElement.query(By.css('#menu-button-0')) .nativeElement as HTMLElement; - const deleteButton = fixture.debugElement.query( - By.css('#delete-button-0') - ).nativeElement as HTMLElement; dialogRefSpy.afterClosed.and.returnValue( of({dialogType: DialogType.DeleteJob, jobName: ''}) ); menuButton.click(); + fixture.detectChanges(); + tick(); + + const deleteButton = document.querySelector( + '#delete-button-0' + ) as HTMLElement; deleteButton.click(); expect(draftSurveyServiceSpy.deleteJob).toHaveBeenCalledOnceWith(job1); - }); + flush(); + discardPeriodicTasks(); + })); }); }); }); diff --git a/web/src/app/pages/edit-survey/edit-survey.component.ts b/web/src/app/pages/edit-survey/edit-survey.component.ts index b5bc2c819..0266c951e 100644 --- a/web/src/app/pages/edit-survey/edit-survey.component.ts +++ b/web/src/app/pages/edit-survey/edit-survey.component.ts @@ -24,6 +24,10 @@ import {Job} from 'app/models/job.model'; import {Survey} from 'app/models/survey.model'; import {DraftSurveyService} from 'app/services/draft-survey/draft-survey.service'; import {JobService} from 'app/services/job/job.service'; +import { + SURVEYS_SHARE, + SURVEY_SEGMENT, +} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; import {environment} from 'environments/environment'; @@ -74,10 +78,10 @@ export class EditSurveyComponent { const section = this.editSurveyPageSignal(); switch (section) { - case NavigationService.SURVEY_SEGMENT: + case SURVEY_SEGMENT: this.sectionTitle = $localize`:@@app.editSurvey.surveyDetails.title:Survey details`; break; - case NavigationService.SURVEYS_SHARE: + case SURVEYS_SHARE: this.sectionTitle = $localize`:@@app.editSurvey.sharing.title:Sharing`; break; default: diff --git a/web/src/app/pages/main-page-container/main-page-container.component.html b/web/src/app/pages/main-page-container/main-page-container.component.html index f3ca683f4..106617312 100644 --- a/web/src/app/pages/main-page-container/main-page-container.component.html +++ b/web/src/app/pages/main-page-container/main-page-container.component.html @@ -14,10 +14,8 @@ limitations under the License. --> - + - -
- -
-
+
+ +
diff --git a/web/src/app/pages/main-page-container/main-page-container.component.ts b/web/src/app/pages/main-page-container/main-page-container.component.ts index 4c5c0b025..b65156a1b 100644 --- a/web/src/app/pages/main-page-container/main-page-container.component.ts +++ b/web/src/app/pages/main-page-container/main-page-container.component.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import {Component, effect} from '@angular/core'; -import {Observable} from 'rxjs'; +import {Component, effect, input} from '@angular/core'; +import {toObservable, toSignal} from '@angular/core/rxjs-interop'; +import {switchMap} from 'rxjs/operators'; -import {Survey} from 'app/models/survey.model'; -import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; @Component({ @@ -27,20 +26,17 @@ import {SurveyService} from 'app/services/survey/survey.service'; styleUrls: ['./main-page-container.component.css'], }) export class MainPageContainerComponent { - private surveyIdSignal = this.navigationService.getSurveyId(); - - protected activeSurvey$: Observable; - - constructor( - private navigationService: NavigationService, - private surveyService: SurveyService - ) { - this.activeSurvey$ = surveyService.getActiveSurvey$(); + surveyId = input(); + survey = toSignal( + toObservable(this.surveyId).pipe( + switchMap(id => (id ? this.surveyService.loadSurvey$(id) : [])) + ) + ); + constructor(private surveyService: SurveyService) { effect(() => { - const surveyId = this.surveyIdSignal(); - - if (surveyId) this.surveyService.activateSurvey(surveyId); + const id = this.surveyId(); + if (id) this.surveyService.activateSurvey(id); }); } } diff --git a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.html b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.html index 9af015ad2..647530096 100644 --- a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.html +++ b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.html @@ -21,7 +21,7 @@ [(value)]="selectedValue" > diff --git a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.spec.ts b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.spec.ts index 3b956cf62..d1d634b3f 100644 --- a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.spec.ts @@ -36,7 +36,6 @@ import { } from 'app/services/drawing-tools/drawing-tools.service'; import {GroundPinService} from 'app/services/ground-pin/ground-pin.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; -import {SurveyService} from 'app/services/survey/survey.service'; import {DrawingToolsComponent} from './drawing-tools.component'; import {DrawingToolsModule} from './drawing-tools.module'; @@ -50,7 +49,6 @@ describe('DrawingToolsComponent', () => { let drawingToolsServiceSpy: jasmine.SpyObj; let mockSubmissionId$: BehaviorSubject; let navigationServiceSpy: jasmine.SpyObj; - let surveyServiceSpy: jasmine.SpyObj; const jobId1 = 'job001'; const jobId2 = 'job002'; @@ -105,11 +103,6 @@ describe('DrawingToolsComponent', () => { mockSubmissionId$ = new BehaviorSubject(null); navigationServiceSpy.getSubmissionId$.and.returnValue(mockSubmissionId$); - surveyServiceSpy = jasmine.createSpyObj('SurveyService', [ - 'getActiveSurvey$', - ]); - surveyServiceSpy.getActiveSurvey$.and.returnValue(of(mockSurvey)); - TestBed.configureTestingModule({ imports: [DrawingToolsModule, BrowserAnimationsModule], declarations: [DrawingToolsComponent], @@ -117,13 +110,16 @@ describe('DrawingToolsComponent', () => { {provide: AuthService, useValue: authServiceSpy}, {provide: DrawingToolsService, useValue: drawingToolsServiceSpy}, {provide: NavigationService, useValue: navigationServiceSpy}, - {provide: SurveyService, useValue: surveyServiceSpy}, ], }).compileComponents(); })); function resetFixture() { + if (fixture) { + fixture.destroy(); + } fixture = TestBed.createComponent(DrawingToolsComponent); + fixture.componentRef.setInput('survey', mockSurvey); fixture.detectChanges(); } @@ -188,6 +184,14 @@ describe('DrawingToolsComponent', () => { authServiceSpy.canUserAddPointsToJob.and.returnValue(false); resetFixture(); + // Verify spy behavior + expect( + authServiceSpy.canUserAddPointsToJob( + mockSurvey, + mockSurvey.jobs.first() + ) + ).toBe(false); + const addPointButton = fixture.debugElement.query( By.css('#add-point-button') ); diff --git a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.ts b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.ts index d9100465c..cfab9c26a 100644 --- a/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.ts +++ b/web/src/app/pages/main-page-container/main-page/drawing-tools/drawing-tools.component.ts @@ -20,12 +20,14 @@ import { Component, OnDestroy, OnInit, + computed, + effect, + input, } from '@angular/core'; import {DomSanitizer, SafeUrl} from '@angular/platform-browser'; import {List} from 'immutable'; import {Observable, Subscription} from 'rxjs'; -import {map} from 'rxjs/internal/operators/map'; -import {tap} from 'rxjs/operators'; +import {map} from 'rxjs/operators'; import {Job} from 'app/models/job.model'; import {Survey} from 'app/models/survey.model'; @@ -36,7 +38,6 @@ import { } from 'app/services/drawing-tools/drawing-tools.service'; import {GroundPinService} from 'app/services/ground-pin/ground-pin.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; -import {SurveyService} from 'app/services/survey/survey.service'; @Component({ selector: 'ground-drawing-tools', @@ -46,13 +47,21 @@ import {SurveyService} from 'app/services/survey/survey.service'; }) export class DrawingToolsComponent implements OnInit, OnDestroy { private subscription: Subscription = new Subscription(); + survey = input(); pointValue = 'point'; polygonValue = 'polygon'; selectedValue = ''; private lastSelectedValue = ''; selectedJobId = ''; - private activeSurvey!: Survey; - readonly jobs$: Observable>; + + readonly jobs = computed(() => { + const survey = this.survey(); + if (!survey) return List(); + return List(survey.jobs.valueSeq().toArray()) + .sortBy(l => l.index) + .filter(l => this.authService.canUserAddPointsToJob(survey, l)); + }); + readonly black = '#202225'; readonly addPointIconBlack = this.sanitizer.bypassSecurityTrustUrl( this.groundPinService.getPinImageSource(this.black) @@ -71,25 +80,20 @@ export class DrawingToolsComponent implements OnInit, OnDestroy { private sanitizer: DomSanitizer, private navigationService: NavigationService, private groundPinService: GroundPinService, - surveyService: SurveyService, - authService: AuthService + private authService: AuthService ) { this.isSubmissionSelected$ = this.navigationService .getSubmissionId$() .pipe(map(obs => !!obs)); this.disabled$ = drawingToolsService.getDisabled$(); - this.jobs$ = surveyService.getActiveSurvey$().pipe( - tap(survey => { - this.activeSurvey = survey; + + effect(() => { + const survey = this.survey(); + if (survey) { this.selectedJobId = survey.jobs.keySeq().first(); this.drawingToolsService.setSelectedJobId(this.selectedJobId); - }), - map(survey => - List(survey.jobs.valueSeq().toArray()) - .sortBy(l => l.index) - .filter(l => authService.canUserAddPointsToJob(this.activeSurvey, l)) - ) - ); + } + }); } ngOnInit() { diff --git a/web/src/app/pages/main-page-container/main-page/main-page.component.html b/web/src/app/pages/main-page-container/main-page/main-page.component.html index 8deff3a2d..dc43361ea 100644 --- a/web/src/app/pages/main-page-container/main-page/main-page.component.html +++ b/web/src/app/pages/main-page-container/main-page/main-page.component.html @@ -15,13 +15,10 @@ -->
- +
- - -
- -
- + + +
diff --git a/web/src/app/pages/main-page-container/main-page/main-page.component.spec.ts b/web/src/app/pages/main-page-container/main-page/main-page.component.spec.ts index c21dec11f..413fb0bc2 100644 --- a/web/src/app/pages/main-page-container/main-page/main-page.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/main-page.component.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import {Component, NO_ERRORS_SCHEMA, signal} from '@angular/core'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {AngularFireAuth} from '@angular/fire/compat/auth'; import {AngularFirestore} from '@angular/fire/compat/firestore'; @@ -66,7 +66,7 @@ describe('MainPageComponent', () => { getSurveyId$: () => NEVER, getLocationOfInterestId$: () => NEVER, getSubmissionId$: () => NEVER, - getUrlParams: () => NEVER, + getUrlParams: () => signal({}), }; TestBed.configureTestingModule({ @@ -93,6 +93,19 @@ describe('MainPageComponent', () => { }).compileComponents(); fixture = TestBed.createComponent(MainPageComponent); + + // Create a minimal mock survey for the input + const mockSurvey = { + id: 'survey1', + title: 'Survey Title', + description: 'Description', + jobs: {}, + acl: {}, + ownerId: 'owner1', + dataSharingTerms: {}, + } as any; + + fixture.componentRef.setInput('activeSurvey', mockSurvey); component = fixture.componentInstance; fixture.detectChanges(); })); diff --git a/web/src/app/pages/main-page-container/main-page/main-page.component.ts b/web/src/app/pages/main-page-container/main-page/main-page.component.ts index 42a49c53d..3facbe06c 100644 --- a/web/src/app/pages/main-page-container/main-page/main-page.component.ts +++ b/web/src/app/pages/main-page-container/main-page/main-page.component.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import {Component, OnInit, effect} from '@angular/core'; +import {Component, OnInit, effect, input} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; -import {Observable, Subscription} from 'rxjs'; +import {Subscription} from 'rxjs'; import {Survey} from 'app/models/survey.model'; import {AuthService} from 'app/services/auth/auth.service'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; +import {JOB_ID_NEW} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SubmissionService} from 'app/services/submission/submission.service'; import {SurveyService} from 'app/services/survey/survey.service'; @@ -39,11 +40,10 @@ import {TitleDialogComponent} from './title-dialog/title-dialog.component'; styleUrls: ['./main-page.component.scss'], }) export class MainPageComponent implements OnInit { + activeSurvey = input.required(); private urlParamsSignal = this.navigationService.getUrlParams(); - activeSurvey$: Observable; subscription: Subscription = new Subscription(); - shouldEnableDrawingTools = false; showSubmissionPanel: Boolean = false; constructor( @@ -54,8 +54,6 @@ export class MainPageComponent implements OnInit { private authService: AuthService, private dialog: MatDialog ) { - this.activeSurvey$ = this.surveyService.getActiveSurvey$(); - effect(() => { const {loiId, submissionId} = this.urlParamsSignal(); if (loiId) this.loiService.selectLocationOfInterest(loiId); @@ -68,9 +66,7 @@ export class MainPageComponent implements OnInit { this.subscription.add( this.navigationService .getSurveyId$() - .subscribe( - id => id === NavigationService.JOB_ID_NEW && this.showTitleDialog() - ) + .subscribe(id => id === JOB_ID_NEW && this.showTitleDialog()) ); // Redirect to sign in page if user is not authenticated. this.subscription.add( diff --git a/web/src/app/pages/main-page-container/main-page/map/map.component.spec.ts b/web/src/app/pages/main-page-container/main-page/map/map.component.spec.ts index 699b9ab2c..1f54bc2fe 100644 --- a/web/src/app/pages/main-page-container/main-page/map/map.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/map/map.component.spec.ts @@ -41,7 +41,6 @@ import { import {LocationOfInterestService} from 'app/services/loi/loi.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SubmissionService} from 'app/services/submission/submission.service'; -import {SurveyService} from 'app/services/survey/survey.service'; import {polygonShellCoordsToPolygon} from 'testing/helpers'; import {MapComponent} from './map.component'; @@ -49,7 +48,6 @@ import {MapComponent} from './map.component'; describe('MapComponent', () => { let component: MapComponent; let fixture: ComponentFixture; - let surveyServiceSpy: jasmine.SpyObj; let mockLois$: BehaviorSubject>; let loiServiceSpy: jasmine.SpyObj; let mockLocationOfInterestId$: BehaviorSubject; @@ -160,11 +158,6 @@ describe('MapComponent', () => { ); beforeEach(waitForAsync(() => { - surveyServiceSpy = jasmine.createSpyObj('SurveyService', [ - 'getActiveSurvey$', - ]); - surveyServiceSpy.getActiveSurvey$.and.returnValue(of(mockSurvey)); - loiServiceSpy = jasmine.createSpyObj( 'LocationOfInterestService', ['getLocationsOfInterest$', 'updatePoint', 'addPoint'] @@ -212,7 +205,6 @@ describe('MapComponent', () => { imports: [GoogleMapsModule], declarations: [MapComponent], providers: [ - {provide: SurveyService, useValue: surveyServiceSpy}, { provide: LocationOfInterestService, useValue: loiServiceSpy, @@ -238,6 +230,7 @@ describe('MapComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MapComponent); + fixture.componentRef.setInput('activeSurvey', mockSurvey); component = fixture.componentInstance; component.shouldEnableDrawingTools = true; fixture.detectChanges(); @@ -315,12 +308,14 @@ describe('MapComponent', () => { describe('when selected job id is given', () => { beforeEach(() => { fixture = TestBed.createComponent(MapComponent); + fixture.componentRef.setInput('activeSurvey', mockSurvey); component = fixture.componentInstance; component.selectedJob = job1; fixture.detectChanges(); }); it('should render only lois under the job', fakeAsync(() => { + tick(); component.ngOnChanges(); expect(component.markers.size).toEqual(1); @@ -331,9 +326,11 @@ describe('MapComponent', () => { })); it('should fit the map when survey changed', fakeAsync(() => { + tick(); spyOn(component.map, 'fitBounds'); component.selectedJob = job2; component.ngOnChanges(); + tick(); expect(component.map.fitBounds).toHaveBeenCalledOnceWith( new google.maps.LatLngBounds(new google.maps.LatLng(45.6, 12.3)) diff --git a/web/src/app/pages/main-page-container/main-page/map/map.component.ts b/web/src/app/pages/main-page-container/main-page/map/map.component.ts index 2b00060df..f8d0ecd9b 100644 --- a/web/src/app/pages/main-page-container/main-page/map/map.component.ts +++ b/web/src/app/pages/main-page-container/main-page/map/map.component.ts @@ -23,10 +23,13 @@ import { OnChanges, OnDestroy, ViewChild, + input, } from '@angular/core'; +import {toObservable} from '@angular/core/rxjs-interop'; import {GoogleMap} from '@angular/google-maps'; import {Map as ImmutableMap, List} from 'immutable'; import {BehaviorSubject, Observable, Subscription, combineLatest} from 'rxjs'; +import {filter, map} from 'rxjs/operators'; import {Coordinate} from 'app/models/geometry/coordinate'; import {Geometry, GeometryType} from 'app/models/geometry/geometry'; @@ -46,7 +49,6 @@ import {GroundPinService} from 'app/services/ground-pin/ground-pin.service'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SubmissionService} from 'app/services/submission/submission.service'; -import {SurveyService} from 'app/services/survey/survey.service'; // To make ESLint happy: /*global google*/ @@ -63,6 +65,7 @@ const enlargedPolygonStrokeWeight = 6; }) export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { private subscription: Subscription = new Subscription(); + activeSurvey = input(); private selectedJob$: BehaviorSubject = new BehaviorSubject< Job | undefined >(undefined); @@ -116,7 +119,6 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { constructor( private drawingToolsService: DrawingToolsService, - private surveyService: SurveyService, private loiService: LocationOfInterestService, private navigationService: NavigationService, private groundPinService: GroundPinService, @@ -125,7 +127,10 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { private changeDetectorRef: ChangeDetectorRef ) { this.lois$ = this.loiService.getLocationsOfInterest$(); - this.activeSurvey$ = this.surveyService.getActiveSurvey$(); + this.activeSurvey$ = toObservable(this.activeSurvey).pipe( + filter(s => !!s), + map(s => s as Survey) + ); } ngOnChanges() { diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.spec.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.spec.ts new file mode 100644 index 000000000..6ee296248 --- /dev/null +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.spec.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2025 The Ground Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {MatDialog} from '@angular/material/dialog'; +import {List, Map} from 'immutable'; +import {of} from 'rxjs'; + +import {LocationOfInterest} from 'app/models/loi.model'; +import {Submission} from 'app/models/submission/submission.model'; +import {DataSharingType, Survey} from 'app/models/survey.model'; +import {LocationOfInterestService} from 'app/services/loi/loi.service'; +import {NavigationService} from 'app/services/navigation/navigation.service'; +import {SubmissionService} from 'app/services/submission/submission.service'; + +import {LocationOfInterestPanelComponent} from './loi-panel.component'; + +describe('LocationOfInterestPanelComponent', () => { + let component: LocationOfInterestPanelComponent; + let fixture: ComponentFixture; + let loiServiceSpy: jasmine.SpyObj; + let submissionServiceSpy: jasmine.SpyObj; + let navigationServiceSpy: jasmine.SpyObj; + let dialogSpy: jasmine.SpyObj; + + const mockSurvey = new Survey( + 'survey1', + 'Survey Title', + 'Description', + Map(), + Map(), + 'owner1', + {type: DataSharingType.PRIVATE} + ); + + const mockLoi = new LocationOfInterest( + 'loi1', + 'job1', + {chainId: 'point1'} as any, + Map() + ); + + beforeEach(waitForAsync(() => { + loiServiceSpy = jasmine.createSpyObj('LocationOfInterestService', [ + 'getSelectedLocationOfInterest$', + ]); + submissionServiceSpy = jasmine.createSpyObj('SubmissionService', [ + 'getSubmissions$', + ]); + navigationServiceSpy = jasmine.createSpyObj('NavigationService', [ + 'showSubmissionDetail', + 'clearLocationOfInterestId', + ]); + dialogSpy = jasmine.createSpyObj('MatDialog', ['open']); + + loiServiceSpy.getSelectedLocationOfInterest$.and.returnValue(of(mockLoi)); + submissionServiceSpy.getSubmissions$.and.returnValue( + of(List()) + ); + + TestBed.configureTestingModule({ + declarations: [LocationOfInterestPanelComponent], + providers: [ + {provide: LocationOfInterestService, useValue: loiServiceSpy}, + {provide: SubmissionService, useValue: submissionServiceSpy}, + {provide: NavigationService, useValue: navigationServiceSpy}, + {provide: MatDialog, useValue: dialogSpy}, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LocationOfInterestPanelComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('activeSurvey', mockSurvey); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should navigate to submission detail on selection', () => { + component.loi = mockLoi; + const submissionId = 'sub1'; + component.onSelectSubmission(submissionId); + + expect(navigationServiceSpy.showSubmissionDetail).toHaveBeenCalledWith( + mockSurvey.id, + mockLoi.id, + submissionId + ); + }); + + it('should clear LOI on close', () => { + component.onClosePanel(); + expect(navigationServiceSpy.clearLocationOfInterestId).toHaveBeenCalled(); + }); +}); diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.ts index cf3db4d2c..38503b8e9 100644 --- a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.ts +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/loi-panel/loi-panel.component.ts @@ -14,18 +14,19 @@ * limitations under the License. */ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit, input} from '@angular/core'; +import {toObservable} from '@angular/core/rxjs-interop'; import {MatDialog} from '@angular/material/dialog'; import {List} from 'immutable'; -import {Subscription, switchMap} from 'rxjs'; +import {Subscription, combineLatest, switchMap} from 'rxjs'; import {LoiPropertiesDialogComponent} from 'app/components/loi-properties-dialog/loi-properties-dialog.component'; import {LocationOfInterest} from 'app/models/loi.model'; import {Submission} from 'app/models/submission/submission.model'; +import {Survey} from 'app/models/survey.model'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SubmissionService} from 'app/services/submission/submission.service'; -import {SurveyService} from 'app/services/survey/survey.service'; import {getLoiIcon} from 'app/utils/utils'; @Component({ @@ -35,40 +36,40 @@ import {getLoiIcon} from 'app/utils/utils'; }) export class LocationOfInterestPanelComponent implements OnInit, OnDestroy { subscription: Subscription = new Subscription(); + activeSurvey = input(); + + activeSurvey$ = toObservable(this.activeSurvey); loi!: LocationOfInterest; name!: string | null; icon!: string; iconColor!: string; - surveyId!: string; submissions!: List; isLoading = true; constructor( private dialog: MatDialog, private loiService: LocationOfInterestService, - private surveyService: SurveyService, private submissionService: SubmissionService, private navigationService: NavigationService ) {} ngOnInit() { this.subscription.add( - this.surveyService - .getActiveSurvey$() + combineLatest([ + this.activeSurvey$, + this.loiService.getSelectedLocationOfInterest$(), + ]) .pipe( - switchMap(survey => { - this.surveyId = survey.id; - return this.loiService.getSelectedLocationOfInterest$().pipe( - switchMap(loi => { - this.iconColor = survey.getJob(loi.jobId)!.color!; - this.loi = loi; - this.name = LocationOfInterestService.getDisplayName(loi); - this.icon = getLoiIcon(loi); + switchMap(([survey, loi]) => { + if (survey) { + this.iconColor = survey.getJob(loi.jobId)!.color!; + } + this.loi = loi; + this.name = LocationOfInterestService.getDisplayName(loi); + this.icon = getLoiIcon(loi); - return this.submissionService.getSubmissions$(); - }) - ); + return this.submissionService.getSubmissions$(); }) ) .subscribe(submissions => { @@ -79,8 +80,12 @@ export class LocationOfInterestPanelComponent implements OnInit, OnDestroy { } onSelectSubmission(submissionId: string) { + if (!this.activeSurvey()) { + console.error('No active survey'); + return; + } this.navigationService.showSubmissionDetail( - this.surveyId, + this.activeSurvey()!.id, this.loi.id, submissionId ); diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.html b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.html index 7e6598f29..0f833f1b3 100644 --- a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.html +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.html @@ -17,16 +17,12 @@
- + - +
-
+ \ No newline at end of file diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.spec.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.spec.ts new file mode 100644 index 000000000..061302c24 --- /dev/null +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.spec.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2025 The Ground Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {Map} from 'immutable'; +import {of} from 'rxjs'; + +import {DataSharingType, Survey} from 'app/models/survey.model'; +import { + NavigationService, + SideNavMode, +} from 'app/services/navigation/navigation.service'; + +import {SecondarySidePanelComponent} from './secondary-side-panel.component'; + +describe('SecondarySidePanelComponent', () => { + let component: SecondarySidePanelComponent; + let fixture: ComponentFixture; + let navigationServiceSpy: jasmine.SpyObj; + + const mockSurvey = new Survey( + 'survey1', + 'Survey Title', + 'Description', + Map(), + Map(), + 'owner1', + {type: DataSharingType.PRIVATE} + ); + + beforeEach(waitForAsync(() => { + navigationServiceSpy = jasmine.createSpyObj('NavigationService', [ + 'getSideNavMode$', + 'getLoiId', + 'getSubmissionId', + 'getSideNavMode', + ]); + + navigationServiceSpy.getSideNavMode$.and.returnValue( + of(SideNavMode.JOB_LIST) + ); + // Mock signal functions + (navigationServiceSpy.getLoiId as jasmine.Spy).and.returnValue(() => null); + (navigationServiceSpy.getSubmissionId as jasmine.Spy).and.returnValue( + () => null + ); + (navigationServiceSpy.getSideNavMode as jasmine.Spy).and.returnValue( + () => SideNavMode.JOB_LIST + ); + + TestBed.configureTestingModule({ + declarations: [SecondarySidePanelComponent], + providers: [{provide: NavigationService, useValue: navigationServiceSpy}], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SecondarySidePanelComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('activeSurvey', mockSurvey); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.ts index 7f7008e75..391673bb0 100644 --- a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.ts +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/secondary-side-panel.component.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import {Component} from '@angular/core'; +import {Component, input} from '@angular/core'; +import {Survey} from 'app/models/survey.model'; import { NavigationService, SideNavMode, @@ -27,6 +28,7 @@ import { styleUrls: ['./secondary-side-panel.component.css'], }) export class SecondarySidePanelComponent { + activeSurvey = input(); loiIdSignal = this.navigationService.getLoiId(); submissionIdSignal = this.navigationService.getSubmissionId(); sideNavModeSignal = this.navigationService.getSideNavMode(); diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.spec.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.spec.ts new file mode 100644 index 000000000..1557f9450 --- /dev/null +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.spec.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2025 The Ground Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AngularFireStorage} from '@angular/fire/compat/storage'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {Map} from 'immutable'; +import {of} from 'rxjs'; + +import {AuditInfo} from 'app/models/audit-info.model'; +import {Submission} from 'app/models/submission/submission.model'; +import {DataSharingType, Survey} from 'app/models/survey.model'; +import {NavigationService} from 'app/services/navigation/navigation.service'; +import {SubmissionService} from 'app/services/submission/submission.service'; + +import {SubmissionPanelComponent} from './submission-panel.component'; + +describe('SubmissionPanelComponent', () => { + let component: SubmissionPanelComponent; + let fixture: ComponentFixture; + let submissionServiceSpy: jasmine.SpyObj; + let navigationServiceSpy: jasmine.SpyObj; + let storageSpy: jasmine.SpyObj; + + const mockSurvey = new Survey( + 'survey1', + 'Survey Title', + 'Description', + Map(), + Map(), + 'owner1', + {type: DataSharingType.PRIVATE} + ); + + const mockUser = { + id: 'user001', + email: 'email@gmail.com', + displayName: 'User 1', + isAuthenticated: true, + }; + + const mockAuditInfo = new AuditInfo(mockUser, new Date(), new Date()); + + const mockSubmission = new Submission( + 'sub1', + 'loi1', + {id: 'job1'} as any, + mockAuditInfo, + mockAuditInfo, + Map() + ); + + beforeEach(waitForAsync(() => { + submissionServiceSpy = jasmine.createSpyObj('SubmissionService', [ + 'getSelectedSubmission$', + ]); + navigationServiceSpy = jasmine.createSpyObj('NavigationService', [ + 'getTaskId$', + 'selectLocationOfInterest', + 'showSubmissionDetailWithHighlightedTask', + ]); + storageSpy = jasmine.createSpyObj('AngularFireStorage', ['ref']); + + submissionServiceSpy.getSelectedSubmission$.and.returnValue( + of(mockSubmission) + ); + navigationServiceSpy.getTaskId$.and.returnValue(of(null)); + + TestBed.configureTestingModule({ + declarations: [SubmissionPanelComponent], + imports: [MatProgressSpinnerModule], + providers: [ + {provide: SubmissionService, useValue: submissionServiceSpy}, + {provide: NavigationService, useValue: navigationServiceSpy}, + {provide: AngularFireStorage, useValue: storageSpy}, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionPanelComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('activeSurvey', mockSurvey); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should navigate back to submission list', () => { + component.submission = mockSubmission; + component.navigateToSubmissionList(); + + expect(navigationServiceSpy.selectLocationOfInterest).toHaveBeenCalledWith( + mockSurvey.id, + mockSubmission.loiId + ); + }); +}); diff --git a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.ts b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.ts index 5c15e4051..c0f3af01d 100644 --- a/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.ts +++ b/web/src/app/pages/main-page-container/main-page/secondary-side-panel/submission-panel/submission-panel.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, Input, OnDestroy, OnInit, input} from '@angular/core'; import {AngularFireStorage} from '@angular/fire/compat/storage'; import {List} from 'immutable'; import {Subscription, firstValueFrom} from 'rxjs'; @@ -23,6 +23,7 @@ import {Point} from 'app/models/geometry/point'; import {MultipleSelection} from 'app/models/submission/multiple-selection'; import {Result} from 'app/models/submission/result.model'; import {Submission} from 'app/models/submission/submission.model'; +import {Survey} from 'app/models/survey.model'; import {Option} from 'app/models/task/option.model'; import {Task, TaskType} from 'app/models/task/task.model'; import {NavigationService} from 'app/services/navigation/navigation.service'; @@ -37,10 +38,10 @@ export class SubmissionPanelComponent implements OnInit, OnDestroy { subscription: Subscription = new Subscription(); @Input() submissionId!: string; + activeSurvey = input(); submission: Submission | null = null; tasks?: List; selectedTaskId: string | null = null; - surveyId: string | null = null; firebaseURLs = new Map(); isLoading = true; @@ -53,11 +54,6 @@ export class SubmissionPanelComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.subscription.add( - this.navigationService.getSurveyId$().subscribe(surveyId => { - this.surveyId = surveyId; - }) - ); this.subscription.add( this.submissionService.getSelectedSubmission$().subscribe(submission => { if (submission instanceof Submission) { @@ -108,9 +104,18 @@ export class SubmissionPanelComponent implements OnInit, OnDestroy { } navigateToSubmissionList() { + const survey = this.activeSurvey(); + if (!survey) { + console.error("No active survey - can't navigate to submission list"); + return; + } + if (!this.submission) { + console.error("No submission - can't navigate to submission list"); + return; + } this.navigationService.selectLocationOfInterest( - this.surveyId!, - this.submission!.loiId + survey.id, + this.submission.loiId ); } @@ -166,10 +171,20 @@ export class SubmissionPanelComponent implements OnInit, OnDestroy { } selectGeometry(task: Task): void { + const survey = this.activeSurvey(); + if (!survey) { + console.error("No active survey - can't select geometry"); + return; + } + if (!this.submission) { + console.error("No submission - can't select geometry"); + return; + } + this.navigationService.showSubmissionDetailWithHighlightedTask( - this.surveyId!, - this.submission!.loiId!, - this.submission!.id!, + survey.id, + this.submission.loiId, + this.submission.id, task.id ); } diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.html b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.html index ffa7df906..9d692d209 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.html +++ b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.html @@ -15,13 +15,12 @@ --> diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.spec.ts b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.spec.ts index f43fe4667..5464d6431 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.spec.ts @@ -59,17 +59,6 @@ const mockAngularFireAuth = { authState: of(authState), }; -class MockSurveyService { - getActiveSurvey$() { - return of(mockSurvey); - } - getCurrentSurvey() {} - getCurrentSurveyAcl() {} - canManageSurvey() {} -} - -const surveyService = new MockSurveyService(); - describe('JobListComponent', () => { let component: JobListComponent; let fixture: ComponentFixture; @@ -85,7 +74,6 @@ describe('JobListComponent', () => { declarations: [JobListComponent], imports: [MatListModule], providers: [ - {provide: SurveyService, useValue: surveyService}, { provide: Router, useValue: routerSpy, @@ -104,6 +92,7 @@ describe('JobListComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(JobListComponent); + fixture.componentRef.setInput('activeSurvey', mockSurvey); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.ts b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.ts index 13a59a5c8..c165cfd55 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.ts +++ b/web/src/app/pages/main-page-container/main-page/side-panel/job-list/job-list.component.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import {Component} from '@angular/core'; +import {Component, computed, input} from '@angular/core'; import {List} from 'immutable'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/internal/operators/map'; import {Job} from 'app/models/job.model'; +import {Survey} from 'app/models/survey.model'; import {NavigationService} from 'app/services/navigation/navigation.service'; -import {SurveyService} from 'app/services/survey/survey.service'; @Component({ selector: 'ground-job-list', @@ -29,19 +27,18 @@ import {SurveyService} from 'app/services/survey/survey.service'; styleUrls: ['./job-list.component.scss'], }) export class JobListComponent { - readonly jobs$: Observable>; + activeSurvey = input(); + readonly jobs = computed(() => { + const survey = this.activeSurvey(); + return survey + ? List(survey.jobs.valueSeq().toArray()).sortBy(l => l.index) + : List(); + }); - constructor( - readonly surveyService: SurveyService, - readonly navigationService: NavigationService - ) { - this.jobs$ = surveyService - .getActiveSurvey$() - .pipe( - map(survey => - List(survey.jobs.valueSeq().toArray()).sortBy(l => l.index) - ) - ); + constructor(readonly navigationService: NavigationService) {} + + trackById(index: number, job: Job): string { + return job.id; } isSidePanelExpanded() { diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.html b/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.html index de4ec9062..74c2cdeed 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.html +++ b/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.html @@ -16,6 +16,6 @@
- +
diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.ts b/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.ts index 9a5424adc..44943b021 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.ts +++ b/web/src/app/pages/main-page-container/main-page/side-panel/side-panel.component.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import {Component} from '@angular/core'; +import {Component, input} from '@angular/core'; import {Observable} from 'rxjs'; +import {Survey} from 'app/models/survey.model'; import { NavigationService, SideNavMode, @@ -28,6 +29,7 @@ import { styleUrls: ['./side-panel.component.css'], }) export class SidePanelComponent { + activeSurvey = input(); readonly sideNavMode = SideNavMode; readonly sideNavMode$: Observable; diff --git a/web/src/app/pages/main-page-container/main-page/side-panel/submission-form/submission-form.component.spec.ts b/web/src/app/pages/main-page-container/main-page/side-panel/submission-form/submission-form.component.spec.ts index 94daab063..753759c51 100644 --- a/web/src/app/pages/main-page-container/main-page/side-panel/submission-form/submission-form.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/side-panel/submission-form/submission-form.component.spec.ts @@ -50,6 +50,7 @@ import {AuthService} from 'app/services/auth/auth.service'; import {DataStoreService} from 'app/services/data-store/data-store.service'; import {LocationOfInterestService} from 'app/services/loi/loi.service'; import {NavigationService} from 'app/services/navigation/navigation.service'; +import {UrlParams} from 'app/services/navigation/url-params'; import {SubmissionService} from 'app/services/submission/submission.service'; import {SurveyService} from 'app/services/survey/survey.service'; @@ -172,7 +173,7 @@ describe('SubmissionFormComponent', () => { beforeEach(waitForAsync(() => { const navigationService = { getSurveyId$: () => of(''), - getUrlParams: () => NEVER, + getUrlParams: () => signal(new UrlParams(null, null, null, null)), getLocationOfInterestId$: () => NEVER, getSidePanelExpanded: () => false, }; diff --git a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.html b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.html index 9e0bfca69..4d04dc5cf 100644 --- a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.html +++ b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.html @@ -26,6 +26,8 @@
-
{{ title ?? 'Untitled survey' }}
+
+ {{ activeSurvey()?.title ?? 'Untitled survey' }} +
diff --git a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.spec.ts b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.spec.ts index 8a3f340e2..83c52a6ac 100644 --- a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.spec.ts +++ b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {MatDialogModule} from '@angular/material/dialog'; import {MatIconModule} from '@angular/material/icon'; @@ -54,6 +55,7 @@ describe('SurveyHeaderComponent', () => { }, }, ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); diff --git a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.ts b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.ts index a2d3170d5..96f601263 100644 --- a/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.ts +++ b/web/src/app/pages/main-page-container/main-page/survey-header/survey-header.component.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import {Component, OnDestroy} from '@angular/core'; +import {Component, input} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {Subscription} from 'rxjs'; import {ShareDialogComponent} from 'app/components/share-dialog/share-dialog.component'; +import {Survey} from 'app/models/survey.model'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {SurveyService} from 'app/services/survey/survey.service'; @@ -27,25 +28,14 @@ import {SurveyService} from 'app/services/survey/survey.service'; templateUrl: './survey-header.component.html', styleUrls: ['./survey-header.component.scss'], }) -export class SurveyHeaderComponent implements OnDestroy { - title: string; - surveyId!: string; +export class SurveyHeaderComponent { + activeSurvey = input(); - subscription: Subscription = new Subscription(); constructor( public navigationService: NavigationService, public surveyService: SurveyService, private dialog: MatDialog - ) { - this.title = ''; - const activeSurvey$ = this.surveyService.getActiveSurvey$(); - this.subscription.add( - activeSurvey$.subscribe(survey => { - this.title = survey.title || ''; - this.surveyId = survey.id; - }) - ); - } + ) {} /** * Updates the survey title with input element value. @@ -53,8 +43,9 @@ export class SurveyHeaderComponent implements OnDestroy { * @param evt the event emitted from the input element on blur. */ updateSurveyTitle(value: string): Promise { - if (value === this.title) return Promise.resolve(); - return this.surveyService.updateTitle(this.surveyId, value); + const survey = this.activeSurvey(); + if (!survey || value === survey.title) return Promise.resolve(); + return this.surveyService.updateTitle(survey.id, value); } onSurveysButtonClick(): void { @@ -68,15 +59,15 @@ export class SurveyHeaderComponent implements OnDestroy { }); } - ngOnDestroy() { - this.subscription.unsubscribe(); - } - onClickSidePanelButtonEvent() { this.navigationService.onClickSidePanelButton(); } isEditSurveyPage() { - return this.navigationService.isEditSurveyPage(this.surveyId); + if (!this.activeSurvey()) { + console.error('No active survey'); + return; + } + return this.navigationService.isEditSurveyPage(this.activeSurvey()!.id); } } diff --git a/web/src/app/routing.module.ts b/web/src/app/routing.module.ts index 7f2e13f0b..f421073b5 100644 --- a/web/src/app/routing.module.ts +++ b/web/src/app/routing.module.ts @@ -27,6 +27,24 @@ import {MainPageContainerComponent} from 'app/pages/main-page-container/main-pag import {MainPageContainerModule} from 'app/pages/main-page-container/main-page-container.module'; import {AuthGuard} from 'app/services/auth/auth.guard'; import {passlistGuard} from 'app/services/auth/passlist.guard'; +import { + ABOUT, + ANDROID_SEGMENT, + ERROR, + LOI_ID, + LOI_SEGMENT, + SIGN_IN_SEGMENT, + SUBMISSION_ID, + SUBMISSION_SEGMENT, + SURVEYS_CREATE, + SURVEYS_EDIT, + SURVEYS_SEGMENT, + SURVEY_ID, + SURVEY_SEGMENT, + TASK_ID, + TASK_SEGMENT, + TERMS, +} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {ShareSurveyComponent} from './components/share-survey/share-survey.component'; @@ -41,20 +59,6 @@ import {ErrorComponent} from './pages/error/error.component'; import {ErrorModule} from './pages/error/error.module'; import {TermsComponent} from './pages/terms/terms.component'; -const { - LOI_ID, - LOI_SEGMENT, - SIGN_IN_SEGMENT, - SUBMISSION_ID, - SUBMISSION_SEGMENT, - SURVEY_ID, - SURVEYS_CREATE, - SURVEYS_EDIT, - SURVEYS_SEGMENT, - TASK_ID, - TASK_SEGMENT, -} = NavigationService; - const routes: Routes = [ { path: '', @@ -82,7 +86,7 @@ const routes: Routes = [ canActivate: [AuthGuard, passlistGuard], }, { - path: `${NavigationService.SURVEY_SEGMENT}/:${SURVEY_ID}/${SURVEYS_EDIT}`, + path: `${SURVEY_SEGMENT}/:${SURVEY_ID}/${SURVEYS_EDIT}`, component: EditSurveyComponent, canActivate: [AuthGuard], children: [ @@ -93,7 +97,7 @@ const routes: Routes = [ ], }, { - path: `${NavigationService.SURVEY_SEGMENT}/:${SURVEY_ID}`, + path: `${SURVEY_SEGMENT}/:${SURVEY_ID}`, component: MainPageContainerComponent, canActivate: [AuthGuard], children: [ @@ -116,26 +120,26 @@ const routes: Routes = [ ], }, { - path: NavigationService.ERROR, + path: ERROR, component: ErrorComponent, canActivate: [AuthGuard], }, { - path: NavigationService.ABOUT, + path: ABOUT, component: AboutComponent, }, { - path: `${NavigationService.ANDROID_SEGMENT}`, + path: `${ANDROID_SEGMENT}`, component: AndroidIntentLandingPageComponent, children: [{path: '**', component: AndroidIntentLandingPageComponent}], }, { - path: NavigationService.TERMS, + path: TERMS, component: TermsComponent, canActivate: [AuthGuard], }, ]; -const config = RouterModule.forRoot(routes, {}); +const config = RouterModule.forRoot(routes, {bindToComponentInputs: true}); @NgModule({ imports: [config], diff --git a/web/src/app/services/auth/auth.guard.ts b/web/src/app/services/auth/auth.guard.ts index 0339f77e6..e6efed373 100644 --- a/web/src/app/services/auth/auth.guard.ts +++ b/web/src/app/services/auth/auth.guard.ts @@ -20,6 +20,10 @@ import {catchError, map} from 'rxjs/operators'; import {User} from 'app/models/user.model'; import {AuthService} from 'app/services/auth/auth.service'; +import { + SIGN_IN_SEGMENT, + TERMS, +} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; import {environment} from 'environments/environment'; @@ -49,7 +53,7 @@ export class AuthGuard { if (environment.useEmulators) { return true; } - if (url.includes(NavigationService.SIGN_IN_SEGMENT)) { + if (url.includes(SIGN_IN_SEGMENT)) { if (!user.isAuthenticated) { return true; } @@ -57,7 +61,7 @@ export class AuthGuard { return false; } - if (url.includes(NavigationService.TERMS)) { + if (url.includes(TERMS)) { if (user.isAuthenticated) { return true; } diff --git a/web/src/app/services/navigation/navigation.constants.ts b/web/src/app/services/navigation/navigation.constants.ts new file mode 100644 index 000000000..2eb540915 --- /dev/null +++ b/web/src/app/services/navigation/navigation.constants.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2025 The Ground Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum SideNavMode { + JOB_LIST = 1, + SUBMISSION = 2, +} + +export const LOI_SEGMENT = 'site'; +export const LOI_ID = 'siteId'; +export const JOB_ID_NEW = 'new'; +export const SUBMISSION_SEGMENT = 'submission'; +export const SUBMISSION_ID = 'submissionId'; +export const SUBMISSION_ID_NEW = 'new'; +export const SURVEY_ID_NEW = 'new'; +export const SURVEY_ID = 'surveyId'; +export const SURVEY_SEGMENT = 'survey'; +export const SIGN_IN_SEGMENT = 'signin'; +export const SURVEYS_SEGMENT = 'surveys'; +export const SURVEYS_CREATE = 'create'; +export const SURVEYS_EDIT = 'edit'; +export const SURVEYS_SHARE = 'share'; +export const TASK_SEGMENT = 'task'; +export const TASK_ID = 'taskId'; +export const JOB_SEGMENT = 'job'; +export const ERROR = 'error'; +export const ABOUT = 'about'; +export const TERMS = 'terms'; +export const ANDROID_SEGMENT = 'android'; diff --git a/web/src/app/services/navigation/navigation.service.ts b/web/src/app/services/navigation/navigation.service.ts index 93d340ec1..16081e9ba 100644 --- a/web/src/app/services/navigation/navigation.service.ts +++ b/web/src/app/services/navigation/navigation.service.ts @@ -33,9 +33,35 @@ import { import {BehaviorSubject, Observable, Subscription} from 'rxjs'; import {filter} from 'rxjs/operators'; +import { + ABOUT, + ANDROID_SEGMENT, + ERROR, + JOB_ID_NEW, + JOB_SEGMENT, + LOI_ID, + LOI_SEGMENT, + SIGN_IN_SEGMENT, + SUBMISSION_ID, + SUBMISSION_ID_NEW, + SUBMISSION_SEGMENT, + SURVEYS_CREATE, + SURVEYS_EDIT, + SURVEYS_SEGMENT, + SURVEYS_SHARE, + SURVEY_ID, + SURVEY_ID_NEW, + SURVEY_SEGMENT, + SideNavMode, + TASK_ID, + TASK_SEGMENT, + TERMS, +} from './navigation.constants'; import {UrlParams} from './url-params'; import {DataStoreService} from '../data-store/data-store.service'; +export {SideNavMode} from './navigation.constants'; + /** * Exposes application state in the URL as streams to other services * and components, and provides methods for altering said state. @@ -44,28 +70,6 @@ import {DataStoreService} from '../data-store/data-store.service'; providedIn: 'root', }) export class NavigationService implements OnDestroy { - static readonly LOI_SEGMENT = 'site'; - static readonly LOI_ID = 'siteId'; - static readonly JOB_ID_NEW = 'new'; - static readonly SUBMISSION_SEGMENT = 'submission'; - static readonly SUBMISSION_ID = 'submissionId'; - static readonly SUBMISSION_ID_NEW = 'new'; - static readonly SURVEY_ID_NEW = 'new'; - static readonly SURVEY_ID = 'surveyId'; - static readonly SURVEY_SEGMENT = 'survey'; - static readonly SIGN_IN_SEGMENT = 'signin'; - static readonly SURVEYS_SEGMENT = 'surveys'; - static readonly SURVEYS_CREATE = 'create'; - static readonly SURVEYS_EDIT = 'edit'; - static readonly SURVEYS_SHARE = 'share'; - static readonly TASK_SEGMENT = 'task'; - static readonly TASK_ID = 'taskId'; - static readonly JOB_SEGMENT = 'job'; - static readonly ERROR = 'error'; - static readonly ABOUT = 'about'; - static readonly TERMS = 'terms'; - static readonly ANDROID_SEGMENT = 'android'; - private sidePanelExpanded = true; private urlSignal = signal(''); @@ -371,30 +375,3 @@ export class NavigationService implements OnDestroy { this.subscription.unsubscribe(); } } - -export enum SideNavMode { - JOB_LIST = 1, - SUBMISSION = 2, -} - -const { - ABOUT, - ERROR, - LOI_ID, - LOI_SEGMENT, - JOB_SEGMENT, - SIGN_IN_SEGMENT, - SUBMISSION_ID, - SUBMISSION_SEGMENT, - SURVEY_ID, - SURVEY_ID_NEW, - SURVEY_SEGMENT, - SURVEYS_CREATE, - SURVEYS_EDIT, - SURVEYS_SHARE, - SURVEYS_SEGMENT, - TASK_ID, - TASK_SEGMENT, - TERMS, - ANDROID_SEGMENT, -} = NavigationService; diff --git a/web/src/app/services/navigation/url-params.ts b/web/src/app/services/navigation/url-params.ts index acb157661..30710d73d 100644 --- a/web/src/app/services/navigation/url-params.ts +++ b/web/src/app/services/navigation/url-params.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {SideNavMode} from './navigation.service'; +import {SideNavMode} from './navigation.constants'; export class UrlParams { public sideNavMode: SideNavMode | null; diff --git a/web/src/app/services/survey/survey.service.ts b/web/src/app/services/survey/survey.service.ts index 9d28517ad..307b680f7 100644 --- a/web/src/app/services/survey/survey.service.ts +++ b/web/src/app/services/survey/survey.service.ts @@ -23,6 +23,7 @@ import {Role} from 'app/models/role.model'; import {DataSharingType, Survey, SurveyState} from 'app/models/survey.model'; import {AuthService} from 'app/services/auth/auth.service'; import {DataStoreService} from 'app/services/data-store/data-store.service'; +import {SURVEY_ID_NEW} from 'app/services/navigation/navigation.constants'; import {NavigationService} from 'app/services/navigation/navigation.service'; @Injectable({ @@ -45,7 +46,7 @@ export class SurveyService { // Asynchronously load survey. switchMap() internally disposes // of previous subscription if present. switchMap(id => { - if (id === NavigationService.SURVEY_ID_NEW) { + if (id === SURVEY_ID_NEW) { return of(Survey.UNSAVED_NEW); } return this.dataStore.loadSurvey$(id); @@ -71,6 +72,10 @@ export class SurveyService { return this.activeSurvey$; } + loadSurvey$(id: string): Observable { + return this.dataStore.loadSurvey$(id); + } + getAccessibleSurveys$(): Observable> { const user = this.authService.getCurrentUser(); if (!user) {