diff --git a/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.html b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.html index 7f239ac27..4125e2479 100644 --- a/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.html +++ b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.html @@ -17,7 +17,7 @@
diff --git a/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.spec.ts b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.spec.ts new file mode 100644 index 000000000..14dd9a58e --- /dev/null +++ b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.spec.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2026 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 } from '@angular/core/testing'; +import { MatSlideToggleChange, MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { Map } from 'immutable'; + +import { DataSharingType, Survey, SurveyDataVisibility } from 'app/models/survey.model'; +import { AuthService } from 'app/services/auth/auth.service'; +import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; + +import { DataVisibilityControlComponent } from './data-visibility-control.component'; + +describe('DataVisibilityControlComponent', () => { + let component: DataVisibilityControlComponent; + let fixture: ComponentFixture; + + let draftSurveyService: jasmine.SpyObj; + + const mockSurveyBase = new Survey( + 'survey1', + 'Test Survey', + 'A test survey', + Map(), + Map(), + 'owner1', + { type: DataSharingType.PRIVATE } + ); + + beforeEach(async () => { + draftSurveyService = jasmine.createSpyObj('DraftSurveyService', [ + 'updateDataVisibility', + ]); + + await TestBed.configureTestingModule({ + declarations: [DataVisibilityControlComponent], + imports: [MatSlideToggleModule], + providers: [ + { provide: AuthService, useValue: {} }, + { provide: DraftSurveyService, useValue: draftSurveyService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DataVisibilityControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('defaults to CONTRIBUTOR_AND_ORGANIZERS when survey has no dataVisibility', () => { + fixture.componentRef.setInput('survey', mockSurveyBase); + fixture.detectChanges(); + + expect(component.selectedDataVisibility).toBe( + SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS + ); + }); + + it('reflects ALL_SURVEY_PARTICIPANTS when survey has that visibility set', () => { + const survey = mockSurveyBase.copyWith({ + dataVisibility: SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS, + }); + fixture.componentRef.setInput('survey', survey); + fixture.detectChanges(); + + expect(component.selectedDataVisibility).toBe( + SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS + ); + }); + + it('reflects CONTRIBUTOR_AND_ORGANIZERS when survey has that visibility set', () => { + const survey = mockSurveyBase.copyWith({ + dataVisibility: SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS, + }); + fixture.componentRef.setInput('survey', survey); + fixture.detectChanges(); + + expect(component.selectedDataVisibility).toBe( + SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS + ); + }); + + it('updates to ALL_SURVEY_PARTICIPANTS and calls service when toggle is turned on', () => { + fixture.componentRef.setInput('survey', mockSurveyBase); + fixture.detectChanges(); + + component.onDataVisibilityChange({ checked: true } as MatSlideToggleChange); + + expect(component.selectedDataVisibility).toBe( + SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS + ); + expect(draftSurveyService.updateDataVisibility).toHaveBeenCalledWith( + SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS + ); + }); + + it('updates to CONTRIBUTOR_AND_ORGANIZERS and calls service when toggle is turned off', () => { + const survey = mockSurveyBase.copyWith({ + dataVisibility: SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS, + }); + fixture.componentRef.setInput('survey', survey); + fixture.detectChanges(); + + component.onDataVisibilityChange({ checked: false } as MatSlideToggleChange); + + expect(component.selectedDataVisibility).toBe( + SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS + ); + expect(draftSurveyService.updateDataVisibility).toHaveBeenCalledWith( + SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS + ); + }); +}); diff --git a/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.ts b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.ts index c48425f3d..b66a5c9e6 100644 --- a/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.ts +++ b/web/src/app/components/shared/data-visibility-control/data-visibility-control.component.ts @@ -47,7 +47,7 @@ export class DataVisibilityControlComponent { }); } - changeDataVisibility(event: MatSlideToggleChange) { + onDataVisibilityChange(event: MatSlideToggleChange) { const dataVisibility = event.checked ? SurveyDataVisibility.ALL_SURVEY_PARTICIPANTS : SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS; diff --git a/web/src/app/components/shared/job-integration-control/job-integration-control.component.html b/web/src/app/components/shared/job-integration-control/job-integration-control.component.html new file mode 100644 index 000000000..00b9a6355 --- /dev/null +++ b/web/src/app/components/shared/job-integration-control/job-integration-control.component.html @@ -0,0 +1,22 @@ + + +
+ +
diff --git a/web/src/app/components/shared/job-integration-control/job-integration-control.component.spec.ts b/web/src/app/components/shared/job-integration-control/job-integration-control.component.spec.ts new file mode 100644 index 000000000..ddbebe83b --- /dev/null +++ b/web/src/app/components/shared/job-integration-control/job-integration-control.component.spec.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2026 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 } from '@angular/core/testing'; +import { + MatSlideToggleChange, + MatSlideToggleModule, +} from '@angular/material/slide-toggle'; +import { Map } from 'immutable'; + +import { Job } from 'app/models/job.model'; +import { AuthService } from 'app/services/auth/auth.service'; +import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; + +import { JobIntegrationControlComponent } from './job-integration-control.component'; + +describe('JobIntegrationControlComponent', () => { + let component: JobIntegrationControlComponent; + let fixture: ComponentFixture; + + let draftSurveyService: jasmine.SpyObj; + + const integrationId = 'integration1'; + + const mockJobBase = new Job('job1', 0, '#000', 'Test Job'); + + beforeEach(async () => { + draftSurveyService = jasmine.createSpyObj('DraftSurveyService', [ + 'addOrUpdateJob', + ]); + + await TestBed.configureTestingModule({ + declarations: [JobIntegrationControlComponent], + imports: [MatSlideToggleModule], + providers: [ + { provide: AuthService, useValue: {} }, + { provide: DraftSurveyService, useValue: draftSurveyService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(JobIntegrationControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('reflects false when job has no enabled integrations', () => { + fixture.componentRef.setInput('surveyId', 'survey1'); + fixture.componentRef.setInput('integrationId', integrationId); + fixture.componentRef.setInput('job', mockJobBase); + fixture.detectChanges(); + + expect(component.integrationEnabled).toBeFalse(); + }); + + it('reflects true when job has the integration enabled', () => { + const job = mockJobBase.copyWith({ + enabledIntegrations: Map([[integrationId, { id: integrationId }]]), + }); + fixture.componentRef.setInput('surveyId', 'survey1'); + fixture.componentRef.setInput('integrationId', integrationId); + fixture.componentRef.setInput('job', job); + fixture.detectChanges(); + + expect(component.integrationEnabled).toBeTrue(); + }); + + it('adds integration and calls service when toggle is turned on', () => { + fixture.componentRef.setInput('surveyId', 'survey1'); + fixture.componentRef.setInput('integrationId', integrationId); + fixture.componentRef.setInput('job', mockJobBase); + fixture.detectChanges(); + + component.onIntegrationToggle({ checked: true } as MatSlideToggleChange); + + const updatedJob: Job = + draftSurveyService.addOrUpdateJob.calls.mostRecent().args[0]; + expect(updatedJob.enabledIntegrations.has(integrationId)).toBeTrue(); + expect(updatedJob.enabledIntegrations.get(integrationId)).toEqual({ + id: integrationId, + }); + }); + + it('removes integration and calls service when toggle is turned off', () => { + const job = mockJobBase.copyWith({ + enabledIntegrations: Map([[integrationId, { id: integrationId }]]), + }); + fixture.componentRef.setInput('surveyId', 'survey1'); + fixture.componentRef.setInput('integrationId', integrationId); + fixture.componentRef.setInput('job', job); + fixture.detectChanges(); + + component.onIntegrationToggle({ checked: false } as MatSlideToggleChange); + + const updatedJob: Job = + draftSurveyService.addOrUpdateJob.calls.mostRecent().args[0]; + expect(updatedJob.enabledIntegrations.has(integrationId)).toBeFalse(); + }); +}); diff --git a/web/src/app/components/shared/job-integration-control/job-integration-control.component.ts b/web/src/app/components/shared/job-integration-control/job-integration-control.component.ts new file mode 100644 index 000000000..b362925a1 --- /dev/null +++ b/web/src/app/components/shared/job-integration-control/job-integration-control.component.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2026 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 { Component, effect, input } from '@angular/core'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { Integration, Job } from 'app/models/job.model'; + +import { AuthService } from 'app/services/auth/auth.service'; +import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; + +@Component({ + selector: 'ground-job-integration-control', + templateUrl: './job-integration-control.component.html', + standalone: false, +}) +export class JobIntegrationControlComponent { + surveyId = input(); + integrationId = input(); + job = input(); + + integrationEnabled!: boolean; + + constructor( + readonly authService: AuthService, + readonly draftSurveyService: DraftSurveyService + ) { + effect(() => { + const surveyId = this.surveyId(); + const integrationId = this.integrationId(); + const job = this.job(); + if (surveyId && integrationId && job) { + this.integrationEnabled = job.enabledIntegrations.has(integrationId) || false; + } + }); + } + + onIntegrationToggle(event: MatSlideToggleChange) { + const integrationId = this.integrationId(); + const job = this.job(); + if (integrationId && job) { + const updatedIntegrations = event.checked + ? job.enabledIntegrations.set(integrationId, { + id: integrationId, + } as Integration) + : job.enabledIntegrations.delete(integrationId); + + this.draftSurveyService.addOrUpdateJob( + job.copyWith({ enabledIntegrations: updatedIntegrations }) + ); + } + } +} diff --git a/web/src/app/components/shared/job-integration-control/job-integration-control.module.ts b/web/src/app/components/shared/job-integration-control/job-integration-control.module.ts new file mode 100644 index 000000000..3b0a662b0 --- /dev/null +++ b/web/src/app/components/shared/job-integration-control/job-integration-control.module.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2026 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 { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; + +import { JobIntegrationControlComponent } from './job-integration-control.component'; + +@NgModule({ + declarations: [JobIntegrationControlComponent], + imports: [CommonModule, MatSlideToggleModule], + exports: [JobIntegrationControlComponent], +}) +export class JobIntegrationControlModule {}