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 3c24c1717..c48425f3d 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 @@ -14,9 +14,8 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, effect, input } from '@angular/core'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; -import { Subscription } from 'rxjs'; import { Survey, SurveyDataVisibility } from 'app/models/survey.model'; import { AuthService } from 'app/services/auth/auth.service'; @@ -28,7 +27,7 @@ import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.servi standalone: false, }) export class DataVisibilityControlComponent { - private subscription = new Subscription(); + survey = input(); selectedDataVisibility!: SurveyDataVisibility; @@ -38,16 +37,14 @@ export class DataVisibilityControlComponent { readonly authService: AuthService, readonly draftSurveyService: DraftSurveyService ) { - this.subscription.add( - this.draftSurveyService - .getSurvey$() - .subscribe(survey => this.onSurveyLoaded(survey)) - ); - } - - private async onSurveyLoaded(survey: Survey): Promise { - this.selectedDataVisibility = - survey.dataVisibility || SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS; + effect(() => { + const survey = this.survey(); + if (survey) { + this.selectedDataVisibility = + survey.dataVisibility || + SurveyDataVisibility.CONTRIBUTOR_AND_ORGANIZERS; + } + }); } changeDataVisibility(event: MatSlideToggleChange) { @@ -59,8 +56,4 @@ export class DataVisibilityControlComponent { this.draftSurveyService.updateDataVisibility(dataVisibility); } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } } diff --git a/web/src/app/components/shared/general-access-control/general-access-control.component.ts b/web/src/app/components/shared/general-access-control/general-access-control.component.ts index 56d022797..ad4eced74 100644 --- a/web/src/app/components/shared/general-access-control/general-access-control.component.ts +++ b/web/src/app/components/shared/general-access-control/general-access-control.component.ts @@ -16,9 +16,8 @@ import '@angular/localize/init'; -import { Component } from '@angular/core'; +import { Component, effect, input } from '@angular/core'; import { Map } from 'immutable'; -import { Subscription } from 'rxjs'; import { Survey, SurveyGeneralAccess } from 'app/models/survey.model'; import { AuthService } from 'app/services/auth/auth.service'; @@ -53,7 +52,7 @@ const generalAccessLabels = Map< standalone: false, }) export class GeneralAccessControlComponent { - private subscription = new Subscription(); + survey = input(); selectedGeneralAccess!: SurveyGeneralAccess; @@ -65,31 +64,24 @@ export class GeneralAccessControlComponent { readonly authService: AuthService, readonly draftSurveyService: DraftSurveyService ) { - this.subscription.add( - this.draftSurveyService - .getSurvey$() - .subscribe(survey => this.onSurveyLoaded(survey)) - ); + effect(() => { + const survey = this.survey(); + if (survey) { + // Default to RESTRICTED for general access if not explicitly set. + // This is present for backward-compatibility with older surveys. + this.selectedGeneralAccess = + survey.generalAccess || SurveyGeneralAccess.RESTRICTED; + } + }); } get generalAccessKeys(): SurveyGeneralAccess[] { return Array.from(this.generalAccessLabels.keys()); } - private async onSurveyLoaded(survey: Survey): Promise { - // Default to RESTRICTED for general access if not explicitly set. - // This is present for backward-compatibility with older surveys. - this.selectedGeneralAccess = - survey.generalAccess || SurveyGeneralAccess.RESTRICTED; - } - changeGeneralAccess(generalAccess: SurveyGeneralAccess) { this.selectedGeneralAccess = generalAccess; this.draftSurveyService.updateGeneralAccess(generalAccess); } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } } diff --git a/web/src/app/components/shared/share-dialog/share-dialog.component.spec.ts b/web/src/app/components/shared/share-dialog/share-dialog.component.spec.ts index 937aa2b14..ae96997ea 100644 --- a/web/src/app/components/shared/share-dialog/share-dialog.component.spec.ts +++ b/web/src/app/components/shared/share-dialog/share-dialog.component.spec.ts @@ -17,15 +17,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; import { MatSelectModule } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Map } from 'immutable'; -import { of } from 'rxjs'; +import { DataSharingType, Survey } from 'app/models/survey.model'; import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; import { ShareDialogComponent } from './share-dialog.component'; @@ -52,9 +52,20 @@ describe('ShareDialogComponent', () => { { provide: MatDialogRef, useValue: {} }, { provide: DraftSurveyService, + useValue: { updateAcl: () => null }, + }, + { + provide: MAT_DIALOG_DATA, useValue: { - getSurvey$: () => of({ acl: Map(), getAclEntriesSorted: () => [] }), - updateAcl: () => null, + survey: new Survey( + 'id', + 'title', + 'description', + Map(), + Map(), + 'owner@example.com', + { type: DataSharingType.PRIVATE } + ), }, }, ], diff --git a/web/src/app/components/shared/share-dialog/share-dialog.component.ts b/web/src/app/components/shared/share-dialog/share-dialog.component.ts index baefba2b0..e14c1a6d9 100644 --- a/web/src/app/components/shared/share-dialog/share-dialog.component.ts +++ b/web/src/app/components/shared/share-dialog/share-dialog.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { AbstractControl, FormControl, @@ -23,10 +23,9 @@ import { ValidatorFn, Validators, } from '@angular/forms'; -import { MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSelectChange } from '@angular/material/select'; import { Map } from 'immutable'; -import { Subscription } from 'rxjs'; import { AclEntry } from 'app/models/acl-entry.model'; import { Role } from 'app/models/role.model'; @@ -65,17 +64,12 @@ export class ShareDialogComponent { /** The active survey. */ private survey?: Survey; - private subscription = new Subscription(); - constructor( private dialogRef: MatDialogRef, - private draftSurveyService: DraftSurveyService + private draftSurveyService: DraftSurveyService, + @Inject(MAT_DIALOG_DATA) data: { survey: Survey } ) { - this.subscription.add( - this.draftSurveyService - .getSurvey$() - .subscribe(survey => this.onSurveyLoaded(survey)) - ); + this.onSurveyLoaded(data.survey); } /** @@ -128,13 +122,6 @@ export class ShareDialogComponent { this.dialogRef.close(); } - /** - * Clean up Rx subscription when cleaning up the component. - */ - ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - /** * Update ACL and surveyId when survey is loaded. */ diff --git a/web/src/app/components/shared/share-list/share-list.component.spec.ts b/web/src/app/components/shared/share-list/share-list.component.spec.ts index 427a58f41..3c817a1d1 100644 --- a/web/src/app/components/shared/share-list/share-list.component.spec.ts +++ b/web/src/app/components/shared/share-list/share-list.component.spec.ts @@ -18,7 +18,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatListModule } from '@angular/material/list'; import { MatSelectChange, MatSelectModule } from '@angular/material/select'; import { Map } from 'immutable'; -import { Subject } from 'rxjs'; import { Role } from 'app/models/role.model'; import { DataSharingType, Survey } from 'app/models/survey.model'; @@ -34,23 +33,19 @@ describe('ShareListComponent', () => { let draftSurveyServiceSpy: jasmine.SpyObj; let authServiceSpy: jasmine.SpyObj; - let activeSurvey$: Subject; const user = new User('user1', 'user1@gmail.com', true); beforeEach(async () => { draftSurveyServiceSpy = jasmine.createSpyObj( 'DraftSurveyService', - ['getSurvey$', 'updateAcl'] + ['updateAcl'] ); authServiceSpy = jasmine.createSpyObj('AuthService', [ 'getUser', ]); - activeSurvey$ = new Subject(); - - draftSurveyServiceSpy.getSurvey$.and.returnValue(activeSurvey$); authServiceSpy.getUser.and.callFake(email => { if (email === 'owner-email') { return Promise.resolve(new User('owner', 'owner@gmail.com', true)); @@ -79,7 +74,8 @@ describe('ShareListComponent', () => { }); it('updates itself when acl changes', async () => { - activeSurvey$.next( + fixture.componentRef.setInput( + 'survey', new Survey( 'id', 'title', @@ -90,6 +86,7 @@ describe('ShareListComponent', () => { { type: DataSharingType.PRIVATE } ) ); + fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); diff --git a/web/src/app/components/shared/share-list/share-list.component.ts b/web/src/app/components/shared/share-list/share-list.component.ts index 0b1326f26..7e3851cef 100644 --- a/web/src/app/components/shared/share-list/share-list.component.ts +++ b/web/src/app/components/shared/share-list/share-list.component.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, effect, input } from '@angular/core'; import { MatSelectChange } from '@angular/material/select'; import { Map } from 'immutable'; -import { Subscription } from 'rxjs'; import { AclEntry } from 'app/models/acl-entry.model'; import { Role } from 'app/models/role.model'; @@ -32,12 +31,11 @@ import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.servi standalone: false, }) export class ShareListComponent { + survey = input(); + acl: Array = []; - survey?: Survey; surveyOwnerEmail = ''; - private subscription = new Subscription(); - readonly roleOptions = ROLE_OPTIONS; roles = Role; @@ -46,16 +44,15 @@ export class ShareListComponent { readonly authService: AuthService, readonly draftSurveyService: DraftSurveyService ) { - this.subscription.add( - this.draftSurveyService - .getSurvey$() - .subscribe(survey => this.onSurveyLoaded(survey)) - ); + effect(() => { + const survey = this.survey(); + if (survey) { + this.onSurveyLoaded(survey); + } + }); } private async onSurveyLoaded(survey: Survey): Promise { - this.survey = survey; - const owner = await this.authService.getUser(survey.ownerId); this.surveyOwnerEmail = owner?.email || ''; @@ -88,8 +85,4 @@ export class ShareListComponent { Map(aclUpdate.map(entry => [entry.email, entry.role])) ); } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } } diff --git a/web/src/app/components/shared/share-survey/share-survey.component.html b/web/src/app/components/shared/share-survey/share-survey.component.html index f7892ab88..dda7860cf 100644 --- a/web/src/app/components/shared/share-survey/share-survey.component.html +++ b/web/src/app/components/shared/share-survey/share-survey.component.html @@ -27,7 +27,7 @@ - + @@ -37,12 +37,12 @@ - + - + Data visibility diff --git a/web/src/app/components/shared/share-survey/share-survey.component.spec.ts b/web/src/app/components/shared/share-survey/share-survey.component.spec.ts index 17bc61152..ea3d8ddba 100644 --- a/web/src/app/components/shared/share-survey/share-survey.component.spec.ts +++ b/web/src/app/components/shared/share-survey/share-survey.component.spec.ts @@ -3,6 +3,9 @@ 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'; +import { NEVER } from 'rxjs'; + +import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; import { ShareSurveyComponent } from './share-survey.component'; @@ -15,6 +18,12 @@ describe('ShareSurveyComponent', () => { imports: [MatIconModule, MatDialogModule, MatCardModule], declarations: [ShareSurveyComponent], schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: DraftSurveyService, + useValue: { getSurvey$: () => NEVER }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ShareSurveyComponent); diff --git a/web/src/app/components/shared/share-survey/share-survey.component.ts b/web/src/app/components/shared/share-survey/share-survey.component.ts index ae7051a33..9c1d1bedf 100644 --- a/web/src/app/components/shared/share-survey/share-survey.component.ts +++ b/web/src/app/components/shared/share-survey/share-survey.component.ts @@ -1,7 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { ShareDialogComponent } from 'app/components/shared/share-dialog/share-dialog.component'; +import { DraftSurveyService } from 'app/services/draft-survey/draft-survey.service'; @Component({ selector: 'share-survey', @@ -10,12 +12,16 @@ import { ShareDialogComponent } from 'app/components/shared/share-dialog/share-d standalone: false, }) export class ShareSurveyComponent { - constructor(private dialog: MatDialog) {} + private draftSurveyService = inject(DraftSurveyService); + private dialog = inject(MatDialog); + + survey = toSignal(this.draftSurveyService.getSurvey$()); openShareDialog(): void { this.dialog.open(ShareDialogComponent, { width: '580px', autoFocus: false, + data: { survey: this.survey() }, }); } }