From 4ad40afc080f813ef5d31d54c4b6d0ae96fab61a Mon Sep 17 00:00:00 2001 From: Wojciech Duda Date: Mon, 3 Nov 2025 14:57:30 +0100 Subject: [PATCH] AAE-36660 Update flags component to use signals --- .../lib/components/flags/flags.component.html | 114 +++++++++--------- .../components/flags/flags.component.spec.ts | 45 ++++--- .../lib/components/flags/flags.component.ts | 59 +++++---- .../mocks/features-service-mock.factory.ts | 60 +++++---- 4 files changed, 149 insertions(+), 129 deletions(-) diff --git a/lib/core/feature-flags/src/lib/components/flags/flags.component.html b/lib/core/feature-flags/src/lib/components/flags/flags.component.html index 3279ebe037..57998bad77 100644 --- a/lib/core/feature-flags/src/lib/components/flags/flags.component.html +++ b/lib/core/feature-flags/src/lib/components/flags/flags.component.html @@ -1,66 +1,68 @@ -
- - {{ "CORE.FEATURE-FLAGS.OVERRIDES" | translate }} -
- - +
+ + {{ "CORE.FEATURE-FLAGS.OVERRIDES" | translate }} +
+ +
- - - - - - + +
- - - -
+ + + + - - - - + + + + - - - - + + + + - - -
+ + + + - - - - {{ element.flag }} + + + + {{ element.flag }} -
- - -
-
- - +
+ + +
+
+ +
+ + +
- + - diff --git a/lib/core/feature-flags/src/lib/components/flags/flags.component.spec.ts b/lib/core/feature-flags/src/lib/components/flags/flags.component.spec.ts index d83eb2250e..0cf33f7d38 100644 --- a/lib/core/feature-flags/src/lib/components/flags/flags.component.spec.ts +++ b/lib/core/feature-flags/src/lib/components/flags/flags.component.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FlagsComponent } from './flags.component'; import { FeaturesDirective } from '../../directives/features.directive'; import { WritableFeaturesServiceToken } from '../../interfaces/features.interface'; @@ -41,23 +41,25 @@ describe('FlagsComponent', () => { const storageFeaturesService = TestBed.inject(WritableFeaturesServiceToken); storageFeaturesService.init(); + }); + + it('should initialize flags signal', fakeAsync(() => { fixture = TestBed.createComponent(FlagsComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); - - it('should initialize flags$', (done) => { - component.flags$.subscribe((flags) => { - expect(flags).toEqual([ - { fictive: false, flag: 'feature1', value: true }, - { fictive: false, flag: 'feature2', value: false }, - { fictive: false, flag: 'feature3', value: true } - ]); - done(); - }); - }); + tick(100); + const flags = component.flags(); + expect(flags).toEqual([ + { fictive: false, flag: 'feature1', value: true }, + { fictive: false, flag: 'feature2', value: false }, + { fictive: false, flag: 'feature3', value: true } + ]); + })); it('should update inputValue$ when onInputChange is called', (done) => { + fixture = TestBed.createComponent(FlagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); component.onInputChange('test'); component.inputValue$.subscribe((value) => { expect(value).toBe('test'); @@ -66,18 +68,23 @@ describe('FlagsComponent', () => { }); it('should clear inputValue when onClearInput is called', () => { + fixture = TestBed.createComponent(FlagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); component.inputValue = 'test'; component.onClearInput(); expect(component.inputValue).toBe(''); }); - it('should filter flags when when onClearInput is called', (done) => { + it('should filter flags when input value changes', fakeAsync(() => { + fixture = TestBed.createComponent(FlagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); component.onInputChange('feature1'); - component.flags$.subscribe((flags) => { - expect(flags).toEqual([{ fictive: false, flag: 'feature1', value: true }]); - done(); - }); - }); + tick(150); + const flags = component.flags(); + expect(flags).toEqual([{ fictive: false, flag: 'feature1', value: true }]); + })); afterEach(() => { fixture.destroy(); diff --git a/lib/core/feature-flags/src/lib/components/flags/flags.component.ts b/lib/core/feature-flags/src/lib/components/flags/flags.component.ts index 0a20c88a46..025f7e311a 100644 --- a/lib/core/feature-flags/src/lib/components/flags/flags.component.ts +++ b/lib/core/feature-flags/src/lib/components/flags/flags.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, ViewEncapsulation, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, signal, Signal, ViewEncapsulation, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FeaturesServiceToken, @@ -24,8 +24,8 @@ import { WritableFeaturesServiceToken, WritableFlagChangeset } from '../../interfaces/features.interface'; -import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; -import { debounceTime, map, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { debounceTime, map, tap } from 'rxjs/operators'; import { MatTableModule } from '@angular/material/table'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -35,7 +35,7 @@ import { FormsModule } from '@angular/forms'; import { FlagsOverrideComponent } from '../feature-override-indicator.component'; import { MatDialogModule } from '@angular/material/dialog'; import { TranslatePipe } from '@ngx-translate/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; import { IconModule } from '@alfresco/adf-core'; @Component({ @@ -63,22 +63,20 @@ export class FlagsComponent { private readonly writableFeaturesService = inject(WritableFeaturesServiceToken); displayedColumns: string[] = ['icon', 'flag', 'value']; - flags$: Observable<{ fictive: boolean; flag: string; value: any }[]>; - isEnabled = false; + flags: Signal<{ fictive: boolean; flag: string; value: any }[]>; + isEnabled: Signal; inputValue = ''; inputValue$ = new BehaviorSubject(''); - showPlusButton$!: Observable; + showPlusButton: Signal; writableFlagChangeset: WritableFlagChangeset = {}; - constructor() { - if (this.featuresService.isEnabled$) { - this.featuresService - .isEnabled$() - .pipe(takeUntilDestroyed()) - .subscribe((isEnabled) => { - this.isEnabled = isEnabled; - }); - } + constructor( + @Inject(FeaturesServiceToken) + private featuresService: IDebugFeaturesService & IFeaturesService, + @Inject(WritableFeaturesServiceToken) + private writableFeaturesService: IWritableFeaturesService + ) { + this.isEnabled = this.featuresService.isEnabled$() ? toSignal(this.featuresService.isEnabled$()) : signal(false); const flags$ = this.featuresService.getFlags$().pipe( tap((flags) => (this.writableFlagChangeset = flags)), @@ -93,19 +91,20 @@ export class FlagsComponent { const debouncedInputValue$ = this.inputValue$.pipe(debounceTime(100)); - this.flags$ = combineLatest([flags$, debouncedInputValue$]).pipe( - map(([flags, inputValue]) => { - if (!inputValue) { - return flags; - } + this.flags = toSignal( + combineLatest([flags$, debouncedInputValue$]).pipe( + map(([flags, inputValue]) => { + if (!inputValue) { + return flags; + } - return flags.filter((flag) => flag.flag.includes(inputValue)); - }) + return flags.filter((flag) => flag.flag.includes(inputValue)); + }) + ), + { initialValue: [] } ); - this.showPlusButton$ = this.flags$.pipe( - map((filteredFlags) => this.isEnabled && filteredFlags.length === 0 && this.inputValue.trim().length > 0) - ); + this.showPlusButton = computed(() => this.isEnabled() && this.flags()?.length === 0 && this.inputValue.trim().length > 0); } protected onChange(flag: string, value: boolean) { @@ -130,11 +129,9 @@ export class FlagsComponent { } protected onAdd(event: KeyboardEvent) { - this.showPlusButton$.pipe(take(1)).subscribe((showPlusButton) => { - if (showPlusButton && event.key === 'Enter' && event.shiftKey) { - this.writableFeaturesService.setFlag(this.inputValue, false); - } - }); + if (this.showPlusButton() && event.key === 'Enter' && event.shiftKey) { + this.writableFeaturesService.setFlag(this.inputValue, false); + } } protected onAddButtonClick() { diff --git a/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts index c51a8cc4d7..d695cb8d0e 100644 --- a/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts +++ b/lib/core/feature-flags/src/lib/mocks/features-service-mock.factory.ts @@ -15,8 +15,14 @@ * limitations under the License. */ -import { of } from 'rxjs'; -import { FeaturesServiceToken, FlagChangeset, IFeaturesService } from '../interfaces/features.interface'; +import { BehaviorSubject, of } from 'rxjs'; +import { + FeaturesServiceToken, + FlagChangeset, + IFeaturesService, + IDebugFeaturesService, + WritableFlagChangeset +} from '../interfaces/features.interface'; import { signal } from '@angular/core'; export interface MockFeatureFlags { @@ -33,27 +39,35 @@ const assertFeatureFlag = (flagChangeset: FlagChangeset, key: string): void => { } }; -const mockFeaturesService = (flagChangeset: FlagChangeset): IFeaturesService => ({ - init: () => of(flagChangeset), - isOn: (key) => { - assertFeatureFlag(flagChangeset, key); - return signal(flagChangeset[key].current); - }, - isOff: (key) => { - assertFeatureFlag(flagChangeset, key); - return signal(!flagChangeset[key].current); - }, - isOn$: (key) => { - assertFeatureFlag(flagChangeset, key); - return of(flagChangeset[key].current); - }, - isOff$: (key) => { - assertFeatureFlag(flagChangeset, key); - return of(!flagChangeset[key].current); - }, - getFlags: () => signal(flagChangeset), - getFlags$: () => of(flagChangeset) -}); +const mockFeaturesService = (flagChangeset: FlagChangeset): IFeaturesService & Partial => { + const isEnabled$ = new BehaviorSubject(false); + const flags$ = new BehaviorSubject(flagChangeset as WritableFlagChangeset); + + return { + init: () => of(flagChangeset), + isOn: (key) => { + assertFeatureFlag(flagChangeset, key); + return signal(flagChangeset[key].current); + }, + isOff: (key) => { + assertFeatureFlag(flagChangeset, key); + return signal(!flagChangeset[key].current); + }, + isOn$: (key) => { + assertFeatureFlag(flagChangeset, key); + return of(flagChangeset[key].current); + }, + isOff$: (key) => { + assertFeatureFlag(flagChangeset, key); + return of(!flagChangeset[key].current); + }, + getFlags: () => signal(flagChangeset as WritableFlagChangeset), + getFlags$: () => flags$.asObservable(), + isEnabled$: () => isEnabled$.asObservable(), + enable: (on: boolean) => isEnabled$.next(on), + resetFlags: () => {} + }; +}; const arrayToFlagChangeset = (featureFlags: string[]): FlagChangeset => { const flagChangeset: FlagChangeset = {};