mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-04-16 22:24:49 +00:00
AAE-36660 Update flags component to use signals
This commit is contained in:
@@ -1,66 +1,68 @@
|
||||
<mat-toolbar class="adf-feature-flags-overrides-header">
|
||||
<div class="adf-feature-flags-overrides-header-text" tabindex="0">
|
||||
<adf-feature-flags-override-indicator
|
||||
class="adf-activity-indicator"
|
||||
size='large' />
|
||||
<span>{{ "CORE.FEATURE-FLAGS.OVERRIDES" | translate }}</span>
|
||||
</div>
|
||||
<mat-slide-toggle
|
||||
[checked]="isEnabled"
|
||||
(change)="onEnable($event.checked)" />
|
||||
<button class="adf-feature-flags-overrides-header-close" mat-icon-button mat-dialog-close>
|
||||
<mat-icon adf-icon="close" />
|
||||
</button>
|
||||
<div class="adf-feature-flags-overrides-header-text" tabindex="0">
|
||||
<adf-feature-flags-override-indicator class="adf-activity-indicator" size='large' />
|
||||
<span>{{ "CORE.FEATURE-FLAGS.OVERRIDES" | translate }}</span>
|
||||
</div>
|
||||
<mat-slide-toggle [checked]="isEnabled" (change)="onEnable($event.checked)" />
|
||||
<button class="adf-feature-flags-overrides-header-close" mat-icon-button mat-dialog-close>
|
||||
<mat-icon adf-icon="close" />
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
||||
<ng-container *ngIf="flags$ | async as flags">
|
||||
<table mat-table [dataSource]="flags" class="adf-feature-flags-overrides-table mat-elevation-z0">
|
||||
<ng-container matColumnDef="icon">
|
||||
<th mat-header-cell class="adf-icon-col adf-header-cell" *matHeaderCellDef>
|
||||
<mat-icon class="material-icons-outlined adf-search-icon" adf-icon="search" />
|
||||
</th>
|
||||
<td mat-cell class="adf-icon-col" *matCellDef="let element">
|
||||
<button mat-icon-button *ngIf="element.fictive; else flagFromApi" class="adf-fictive-flag-button" (click)="onDelete(element.flag)">
|
||||
<mat-icon class="material-icons-outlined adf-custom-flag-icon" adf-icon="memory" />
|
||||
<mat-icon class="material-icons-outlined adf-trash-icon" adf-icon="delete" />
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="flags()">
|
||||
<table mat-table [dataSource]="flags()"
|
||||
class="adf-feature-flags-overrides-table mat-elevation-z0">
|
||||
<ng-container matColumnDef="icon">
|
||||
<th mat-header-cell class="adf-icon-col adf-header-cell" *matHeaderCellDef>
|
||||
<mat-icon class="material-icons-outlined adf-search-icon" adf-icon="search" />
|
||||
</th>
|
||||
<td mat-cell class="adf-icon-col" *matCellDef="let element">
|
||||
<button mat-icon-button *ngIf="element.fictive; else flagFromApi"
|
||||
class="adf-fictive-flag-button" (click)="onDelete(element.flag)">
|
||||
<mat-icon class="material-icons-outlined adf-custom-flag-icon"
|
||||
adf-icon="memory" />
|
||||
<mat-icon class="material-icons-outlined adf-trash-icon" adf-icon="delete" />
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="flag">
|
||||
<th mat-header-cell class="flag-col header-cell" *matHeaderCellDef>
|
||||
<mat-form-field class="adf-flag-form-field" appearance="fill" floatLabel="auto">
|
||||
<input class="flag-input" [placeholder]="(isEnabled ? 'CORE.FEATURE-FLAGS.FILTER_OR_ADD_NEW' : 'CORE.FEATURE-FLAGS.FILTER') | translate" matInput type="text" [(ngModel)]="inputValue" (keyup)="onInputChange(inputValue)" (keypress)="onAdd($event)">
|
||||
</mat-form-field>
|
||||
</th>
|
||||
<td mat-cell class="flag-col" *matCellDef="let element">{{ element.flag }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="flag">
|
||||
<th mat-header-cell class="flag-col header-cell" *matHeaderCellDef>
|
||||
<mat-form-field class="adf-flag-form-field" appearance="fill" floatLabel="auto">
|
||||
<input class="flag-input"
|
||||
[placeholder]="(isEnabled ? 'CORE.FEATURE-FLAGS.FILTER_OR_ADD_NEW' : 'CORE.FEATURE-FLAGS.FILTER') | translate"
|
||||
matInput type="text" [(ngModel)]="inputValue"
|
||||
(keyup)="onInputChange(inputValue)" (keypress)="onAdd($event)">
|
||||
</mat-form-field>
|
||||
</th>
|
||||
<td mat-cell class="flag-col" *matCellDef="let element">{{ element.flag }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell class="adf-val-col header-cell" *matHeaderCellDef>
|
||||
<div class="adf-input-field-buttons-container">
|
||||
<button *ngIf="showPlusButton$ | async" mat-icon-button title="{{'CORE.FEATURE-FLAGS.ADD_NEW' | translate}}" color="accent" (click)="onAddButtonClick()">
|
||||
<mat-icon class="material-icons-outlined" adf-icon="add_circle" />
|
||||
</button>
|
||||
<button *ngIf="inputValue" matSuffix mat-icon-button aria-label="Clear" (click)="onClearInput()" class="adf-clear-button">
|
||||
<mat-icon adf-icon="cancel" />
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell class="adf-val-col" *matCellDef="let element">
|
||||
<mat-slide-toggle
|
||||
[checked]="element.value"
|
||||
(change)="onChange(element.flag, $event.checked)"
|
||||
[disabled]="!isEnabled" />
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell class="adf-val-col header-cell" *matHeaderCellDef>
|
||||
<div class="adf-input-field-buttons-container">
|
||||
<button *ngIf="showPlusButton()" mat-icon-button
|
||||
title="{{'CORE.FEATURE-FLAGS.ADD_NEW' | translate}}" color="accent"
|
||||
(click)="onAddButtonClick()">
|
||||
<mat-icon class="material-icons-outlined" adf-icon="add_circle" />
|
||||
</button>
|
||||
<button *ngIf="inputValue" matSuffix mat-icon-button aria-label="Clear"
|
||||
(click)="onClearInput()" class="adf-clear-button">
|
||||
<mat-icon adf-icon="cancel" />
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell class="adf-val-col" *matCellDef="let element">
|
||||
<mat-slide-toggle [checked]="element.value"
|
||||
(change)="onChange(element.flag, $event.checked)" [disabled]="!isEnabled" />
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #flagFromApi>
|
||||
<mat-icon class="material-icons-outlined" adf-icon="cloud" />
|
||||
<mat-icon class="material-icons-outlined" adf-icon="cloud" />
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<IWritableFeaturesService>(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<boolean>;
|
||||
|
||||
inputValue = '';
|
||||
inputValue$ = new BehaviorSubject<string>('');
|
||||
showPlusButton$!: Observable<boolean>;
|
||||
showPlusButton: Signal<boolean>;
|
||||
writableFlagChangeset: WritableFlagChangeset = {};
|
||||
constructor() {
|
||||
if (this.featuresService.isEnabled$) {
|
||||
this.featuresService
|
||||
.isEnabled$()
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((isEnabled) => {
|
||||
this.isEnabled = isEnabled;
|
||||
});
|
||||
}
|
||||
constructor(
|
||||
@Inject(FeaturesServiceToken)
|
||||
private featuresService: IDebugFeaturesService & IFeaturesService<WritableFlagChangeset>,
|
||||
@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() {
|
||||
|
||||
@@ -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<IDebugFeaturesService> => {
|
||||
const isEnabled$ = new BehaviorSubject<boolean>(false);
|
||||
const flags$ = new BehaviorSubject<WritableFlagChangeset>(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 = {};
|
||||
|
||||
Reference in New Issue
Block a user