diff --git a/web/src/app/pages/create-survey/create-survey.component.html b/web/src/app/pages/create-survey/create-survey.component.html index 8652f3620..eeb7b2e8f 100644 --- a/web/src/app/pages/create-survey/create-survey.component.html +++ b/web/src/app/pages/create-survey/create-survey.component.html @@ -77,6 +77,18 @@ (onValidationChange)="onValidationChange($event)" > + + + { job001: job, }), /* acl= */ Map(), - {type: DataSharingType.PRIVATE} + {type: DataSharingType.CUSTOM, customText: 'Good day, sir'} ); const jobWithTask = new Job( jobId, @@ -141,12 +142,16 @@ describe('CreateSurveyComponent', () => { 'updateTitleAndDescription', 'createSurvey', 'getActiveSurvey', + 'updateDataSharingTerms', ]); surveyServiceSpy.createSurvey.and.returnValue( new Promise(resolve => resolve(newSurveyId)) ); activeSurvey$ = new Subject(); surveyServiceSpy.getActiveSurvey$.and.returnValue(activeSurvey$); + surveyServiceSpy.updateDataSharingTerms.and.returnValue( + new Promise(resolve => resolve(undefined)) + ); jobServiceSpy = jasmine.createSpyObj('JobService', [ 'addOrUpdateJob', @@ -178,6 +183,7 @@ describe('CreateSurveyComponent', () => { CreateSurveyComponent, SurveyDetailsComponent, JobDetailsComponent, + DataSharingTermsComponent, SurveyReviewComponent, ], providers: [ @@ -437,6 +443,33 @@ describe('CreateSurveyComponent', () => { }); }); + describe('Data Sharing Terms', () => { + beforeEach(fakeAsync(() => { + surveyId$.next(surveyId); + activeSurvey$.next(surveyWithJob); + tick(); + // Forcibly set phase to DEFINE_DATA_SHARING_TERMS + component.setupPhase = SetupPhase.DEFINE_DATA_SHARING_TERMS; + fixture.detectChanges(); + })); + + it('updates data sharing agreement after clicking continue', () => { + clickContinueButton(fixture); + + expect(surveyServiceSpy.updateDataSharingTerms).toHaveBeenCalledOnceWith( + surveyId, + DataSharingType.CUSTOM, + 'Good day, sir' + ); + }); + + it('goes back to task definition component after back button is clicked', () => { + clickBackButton(fixture); + + expect(component.setupPhase).toBe(SetupPhase.DEFINE_TASKS); + }); + }); + describe('Review', () => { beforeEach(fakeAsync(() => { surveyId$.next(surveyId); @@ -447,10 +480,10 @@ describe('CreateSurveyComponent', () => { fixture.detectChanges(); })); - it('goes back to task definition component after back button is clicked', () => { + it('goes back to data sharing component after back button is clicked', () => { clickBackButton(fixture); - expect(component.setupPhase).toBe(SetupPhase.DEFINE_TASKS); + expect(component.setupPhase).toBe(SetupPhase.DEFINE_DATA_SHARING_TERMS); }); }); }); 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 318974bc3..1a1e152a5 100644 --- a/web/src/app/pages/create-survey/create-survey.component.ts +++ b/web/src/app/pages/create-survey/create-survey.component.ts @@ -21,6 +21,7 @@ import {filter, first, firstValueFrom} from 'rxjs'; import {Job} from 'app/models/job.model'; import {LocationOfInterest} from 'app/models/loi.model'; import {Survey} from 'app/models/survey.model'; +import {DataSharingTermsComponent} from 'app/pages/create-survey/data-sharing-terms/data-sharing-terms.component'; import {JobDetailsComponent} from 'app/pages/create-survey/job-details/job-details.component'; import {SurveyDetailsComponent} from 'app/pages/create-survey/survey-details/survey-details.component'; import {TaskDetailsComponent} from 'app/pages/create-survey/task-details/task-details.component'; @@ -107,6 +108,9 @@ export class CreateSurveyComponent implements OnInit { survey: Survey, lois: Immutable.List ): SetupPhase { + if (this.hasTask(survey)) { + return SetupPhase.DEFINE_DATA_SHARING_TERMS; + } if (!lois.isEmpty()) { return SetupPhase.DEFINE_TASKS; } @@ -136,6 +140,7 @@ export class CreateSurveyComponent implements OnInit { [SetupPhase.JOB_DETAILS, 'Add a job'], [SetupPhase.DEFINE_LOIS, 'Data collection strategy'], [SetupPhase.DEFINE_TASKS, 'Define data collection tasks'], + [SetupPhase.DEFINE_DATA_SHARING_TERMS, 'Define data sharing terms'], [SetupPhase.REVIEW, 'Review and share survey'], ]); @@ -179,9 +184,12 @@ export class CreateSurveyComponent implements OnInit { this.canContinue = true; this.setupPhase = SetupPhase.DEFINE_LOIS; break; - case SetupPhase.REVIEW: + case SetupPhase.DEFINE_DATA_SHARING_TERMS: this.setupPhase = SetupPhase.DEFINE_TASKS; break; + case SetupPhase.REVIEW: + this.setupPhase = SetupPhase.DEFINE_DATA_SHARING_TERMS; + break; default: break; } @@ -207,6 +215,10 @@ export class CreateSurveyComponent implements OnInit { break; case SetupPhase.DEFINE_TASKS: await this.saveTasks(); + this.setupPhase = SetupPhase.DEFINE_DATA_SHARING_TERMS; + break; + case SetupPhase.DEFINE_DATA_SHARING_TERMS: + await this.saveDataSharingTerms(); this.setupPhase = SetupPhase.REVIEW; break; case SetupPhase.REVIEW: @@ -273,11 +285,25 @@ export class CreateSurveyComponent implements OnInit { ); } + private async saveDataSharingTerms() { + const type = this.dataSharingTerms?.formGroup.controls.type.value; + const customText = + this.dataSharingTerms?.formGroup.controls.customText.value ?? undefined; + await this.surveyService.updateDataSharingTerms( + this.survey!.id, + type, + customText + ); + } + @ViewChild('surveyLoi') surveyLoi?: SurveyLoiComponent; @ViewChild('taskDetails') taskDetails?: TaskDetailsComponent; + + @ViewChild('dataSharingTerms') + dataSharingTerms?: DataSharingTermsComponent; } export enum SetupPhase { @@ -285,5 +311,6 @@ export enum SetupPhase { JOB_DETAILS, DEFINE_LOIS, DEFINE_TASKS, + DEFINE_DATA_SHARING_TERMS, REVIEW, } diff --git a/web/src/app/pages/create-survey/create-survey.module.ts b/web/src/app/pages/create-survey/create-survey.module.ts index 2516c8e12..e8bb4691d 100644 --- a/web/src/app/pages/create-survey/create-survey.module.ts +++ b/web/src/app/pages/create-survey/create-survey.module.ts @@ -23,6 +23,7 @@ import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {HeaderModule} from 'app/components/header/header.module'; import {CreateSurveyComponent} from 'app/pages/create-survey/create-survey.component'; +import {DataSharingTermsModule} from 'app/pages/create-survey/data-sharing-terms/data-sharing-terms.module'; import {TaskDetailsModule} from 'app/pages/create-survey/task-details/task-details.module'; import {JobDetailsModule} from './job-details/job-details.module'; @@ -34,6 +35,7 @@ import {SurveyReviewModule} from './survey-review/survey-review.module'; @NgModule({ declarations: [CreateSurveyComponent], imports: [ + DataSharingTermsModule, JobDetailsModule, TaskDetailsModule, SurveyDetailsModule, diff --git a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.html b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.html new file mode 100644 index 000000000..2d85bc712 --- /dev/null +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.html @@ -0,0 +1,55 @@ + + +
+ + + + + {{ option.label }} +

{{ option.description }}

+
+
+

+ Customize agreement +

+

+ Create a custom agreement which will be shown to data collectors before they can + collect data. Data collectors must agree to the terms of this agreement. +

+ + + + + Required + + + +
+
+
+
+
diff --git a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.scss b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.scss new file mode 100644 index 000000000..8d7ec9c2c --- /dev/null +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright 2024 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. + */ + +.data-sharing-terms { + display: flex; + flex-direction: column; + gap: 12px; + + .data-sharing-terms-card { + color: #202124; + font-size: 14px; + font-weight: 500; + + .data-sharing-label { + font-size: 16px; + font-weight: 700; + } + } + + .custom-terms { + display: block; + } +} 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 new file mode 100644 index 000000000..6d87b0b11 --- /dev/null +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.spec.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2024 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 {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DataSharingType} from 'app/models/survey.model'; +import {DataSharingTermsComponent} from 'app/pages/create-survey/data-sharing-terms/data-sharing-terms.component'; + +describe('DataSharingTermsComponent', () => { + let component: DataSharingTermsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DataSharingTermsComponent], + imports: [CommonModule], + }).compileComponents(); + + fixture = TestBed.createComponent(DataSharingTermsComponent); + fixture.componentInstance.type = DataSharingType.PUBLIC; + fixture.componentInstance.customText = 'Hey there here is an agreement'; + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('displays mat-cards for each of the three data sharing types', () => { + const matCards: HTMLElement[] = + fixture.debugElement.nativeElement.querySelectorAll( + '.data-sharing-terms-card' + ); + const contentValues = Array.from(matCards).map((card: HTMLElement) => ({ + label: card + .querySelector('.option-radio-button .data-sharing-label')! + .textContent!.trim(), + description: card + .querySelector('.option-radio-button p')! + .textContent!.trim(), + })); + expect(contentValues).toEqual([ + { + label: 'Private', + description: 'Data will be shared with survey organizers only', + }, + { + label: 'Public', + description: + 'Survey organizers may share and use data publicly with no constraints', + }, + { + label: 'Custom agreement', + description: + 'Survey organizers create terms which must be accepted by data collectors before collecting data', + }, + ]); + }); +}); diff --git a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.ts b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.ts new file mode 100644 index 000000000..009afecef --- /dev/null +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.component.ts @@ -0,0 +1,108 @@ +/** + * Copyright 2024 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, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; + +import {DataSharingType} from 'app/models/survey.model'; + +type DataSharingTermsOption = { + value: DataSharingType; + label: string; + description: string; +}; + +@Component({ + selector: 'data-sharing-terms', + templateUrl: './data-sharing-terms.component.html', + styleUrls: ['./data-sharing-terms.component.scss'], +}) +export class DataSharingTermsComponent implements OnInit { + @Input() type!: DataSharingType; + @Input() customText?: string; + @Output() onValidationChange: EventEmitter = + new EventEmitter(); + + readonly typeControlKey = 'type'; + readonly customTextControlKey = 'customText'; + formGroup!: FormGroup; + + dataSharingTermsOptions: DataSharingTermsOption[] = [ + { + value: DataSharingType.PRIVATE, + label: 'Private', + description: 'Data will be shared with survey organizers only', + }, + { + value: DataSharingType.PUBLIC, + label: 'Public', + description: + 'Survey organizers may share and use data publicly with no constraints', + }, + { + value: DataSharingType.CUSTOM, + label: 'Custom agreement', + description: + 'Survey organizers create terms which must be accepted by data collectors before collecting data', + }, + ]; + + constructor(private formBuilder: FormBuilder) { + this.formGroup = this.formBuilder.group({ + [this.typeControlKey]: DataSharingType.PRIVATE, + [this.customTextControlKey]: [ + {value: '', disabled: true}, + Validators.required, + ], + }); + + this.formGroup.statusChanges.subscribe(_ => { + this.onValidationChange.emit(this.formGroup?.valid); + }); + + this.typeControl.valueChanges.subscribe(type => { + if (type === DataSharingType.CUSTOM) { + this.customTextControl.enable(); + } else { + this.customTextControl.disable(); + } + }); + } + + get typeControl() { + return this.formGroup.controls[this.typeControlKey]; + } + + get customTextControl() { + return this.formGroup.controls[this.customTextControlKey]; + } + + ngOnInit(): void { + this.formGroup.controls[this.typeControlKey].setValue(this.type); + if (this.customText) { + this.formGroup.controls[this.customTextControlKey].setValue( + this.customText + ); + } + } + + shouldShowCustomizeAgreementSection(type: DataSharingType): boolean { + return ( + type === DataSharingType.CUSTOM && + this.typeControl.value === DataSharingType.CUSTOM + ); + } +} diff --git a/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.module.ts b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.module.ts new file mode 100644 index 000000000..005cbc2cb --- /dev/null +++ b/web/src/app/pages/create-survey/data-sharing-terms/data-sharing-terms.module.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2024 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 {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatRadioModule} from '@angular/material/radio'; + +import {DataSharingTermsComponent} from 'app/pages/create-survey/data-sharing-terms/data-sharing-terms.component'; + +@NgModule({ + declarations: [DataSharingTermsComponent], + imports: [ + CommonModule, + MatButtonModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatRadioModule, + ReactiveFormsModule, + ], + exports: [DataSharingTermsComponent], +}) +export class DataSharingTermsModule {} diff --git a/web/src/app/pages/create-survey/step-card/_step-card.component-theme.scss b/web/src/app/pages/create-survey/step-card/_step-card.component-theme.scss index 6dc692f52..0886ef60e 100644 --- a/web/src/app/pages/create-survey/step-card/_step-card.component-theme.scss +++ b/web/src/app/pages/create-survey/step-card/_step-card.component-theme.scss @@ -22,13 +22,23 @@ .title div { color: mat.get-theme-color($theme, outline); } - + .description { color: mat.get-theme-color($theme, on-surface-variant); } } } +@mixin typography($theme) { + .field-header { + font: mat.get-theme-typography($theme, title-medium, font); + } + .field-description { + font: mat.get-theme-typography($theme, body-medium, font); + } + } + @mixin theme($theme) { @include color($theme); + @include typography($theme); }