From f48e554229c326ae4051e78397a9048fe4ce56f0 Mon Sep 17 00:00:00 2001 From: Yves Peter Date: Tue, 27 Jan 2026 20:52:03 +0100 Subject: [PATCH 1/4] refactor(deployFilter): add new deploy filter component, fix issues setting/loading filter options --- .../src/app/deployment/deployment-filter.ts | 4 - .../deployment-filter.component.html | 62 ++++++++ .../deployment-filter.component.spec.ts | 137 ++++++++++++++++++ .../deployment-filter.component.ts | 50 +++++++ .../deployments/deployments.component.html | 78 ++-------- .../deployments/deployments.component.spec.ts | 75 ++++++---- .../app/deployments/deployments.component.ts | 73 ++++------ 7 files changed, 338 insertions(+), 141 deletions(-) create mode 100644 AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html create mode 100644 AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts create mode 100644 AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts diff --git a/AMW_angular/io/src/app/deployment/deployment-filter.ts b/AMW_angular/io/src/app/deployment/deployment-filter.ts index 7ea3a27bb..bbbf8af31 100644 --- a/AMW_angular/io/src/app/deployment/deployment-filter.ts +++ b/AMW_angular/io/src/app/deployment/deployment-filter.ts @@ -1,10 +1,6 @@ -import { ComparatorFilterOption } from './comparator-filter-option'; - export interface DeploymentFilter { name: string; comp: string; val: any; type: string; - compOptions: ComparatorFilterOption[]; - valOptions: string[]; } diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html new file mode 100644 index 000000000..f1ad14067 --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html @@ -0,0 +1,62 @@ +
+
+ +
+ @if (filter.type === 'SpecialFilterType') { +
+ } + @if (filter.type !== 'SpecialFilterType') { +
+ +
+ @if (filter.type !== 'DateType') { + @if (filter.type !== 'booleanType' && filter.type !== 'ENUM_TYPE') { +
+ + + @for (filterValueOption of valOptions(); track filterValueOption) { + + } + +
+ } + @if (filter.type === 'booleanType' || filter.type === 'ENUM_TYPE') { +
+ +
+ } + } + @if (filter.type === 'DateType') { +
+
+ +
+
+ } + } +
+ +
+
diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts new file mode 100644 index 000000000..c40b910a1 --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts @@ -0,0 +1,137 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { DeploymentFilterComponent } from './deployment-filter.component'; +import { DeploymentService } from '../../deployment/deployment.service'; +import { DeploymentFilter } from '../../deployment/deployment-filter'; +import { of } from 'rxjs'; + +describe('DeploymentFilterComponent', () => { + let component: DeploymentFilterComponent; + let fixture: ComponentFixture; + let deploymentService: DeploymentService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeploymentFilterComponent], + providers: [DeploymentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }).compileComponents(); + + fixture = TestBed.createComponent(DeploymentFilterComponent); + component = fixture.componentInstance; + deploymentService = TestBed.inject(DeploymentService); + }); + + it('should create', () => { + component.filter = { + name: 'State', + type: 'ENUM_TYPE', + comp: 'eq', + val: 'failed', + } as DeploymentFilter; + component.index = 0; + component.compOptions = [{ name: 'eq', displayName: 'is' }]; + expect(component).toBeTruthy(); + }); + + it('should load options for ENUM_TYPE filters and mark for check', () => { + const filter: DeploymentFilter = { + name: 'State', + type: 'ENUM_TYPE', + comp: 'eq', + val: 'failed', + } as DeploymentFilter; + + component.filter = filter; + component.index = 0; + component.compOptions = [{ name: 'eq', displayName: 'is' }]; + + const mockOptions = ['success', 'failed', 'canceled']; + vi.spyOn(deploymentService, 'getFilterOptionValues').mockImplementation(() => { + // Before async emission, the component should pre-seed valOptions with the current value + expect(component.valOptions()).toEqual(['failed']); + return of(mockOptions); + }); + + component.ngOnInit(); + + expect(deploymentService.getFilterOptionValues).toHaveBeenCalledWith('State'); + expect(component.valOptions()).toEqual(mockOptions); + }); + + it('should set boolean options for booleanType filters and mark for check', () => { + const filter: DeploymentFilter = { + name: 'Confirmed', + type: 'booleanType', + comp: 'eq', + val: 'true', + } as DeploymentFilter; + + component.filter = filter; + component.index = 0; + component.compOptions = [{ name: 'eq', displayName: 'is' }]; + + component.ngOnInit(); + + expect(component.valOptions()).toEqual(['true', 'false']); + }); + + it('should not load options for SpecialFilterType', () => { + const filter: DeploymentFilter = { + name: 'Special', + type: 'SpecialFilterType', + comp: 'eq', + val: '', + } as DeploymentFilter; + + component.filter = filter; + component.index = 0; + component.compOptions = []; + + const spy = vi.spyOn(deploymentService, 'getFilterOptionValues'); + + component.ngOnInit(); + + expect(spy).not.toHaveBeenCalled(); + expect(component.valOptions()).toEqual([]); + }); + + it('should not load options for DateType filters', () => { + const filter: DeploymentFilter = { + name: 'Date', + type: 'DateType', + comp: 'eq', + val: '', + } as DeploymentFilter; + + component.filter = filter; + component.index = 0; + component.compOptions = []; + + const spy = vi.spyOn(deploymentService, 'getFilterOptionValues'); + + component.ngOnInit(); + + expect(spy).not.toHaveBeenCalled(); + expect(component.valOptions()).toEqual([]); + }); + + it('should emit remove event', () => { + const filter: DeploymentFilter = { + name: 'State', + type: 'ENUM_TYPE', + comp: 'eq', + val: 'failed', + } as DeploymentFilter; + + component.filter = filter; + component.index = 0; + + let emittedFilter: DeploymentFilter | undefined; + component.remove.subscribe((f) => (emittedFilter = f)); + + component.onRemove(); + + expect(emittedFilter).toBe(filter); + }); +}); diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts new file mode 100644 index 000000000..58d2c725a --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, Input, OnInit, Output, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { DeploymentFilter } from '../../deployment/deployment-filter'; +import { ComparatorFilterOption } from '../../deployment/comparator-filter-option'; +import { DeploymentService } from '../../deployment/deployment.service'; +import { ButtonComponent } from '../../shared/button/button.component'; +import { IconComponent } from '../../shared/icon/icon.component'; +import { DateTimePickerComponent } from '../../shared/date-time-picker/date-time-picker.component'; + +@Component({ + selector: 'app-deployment-filter', + templateUrl: './deployment-filter.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, ButtonComponent, IconComponent, DateTimePickerComponent], +}) +export class DeploymentFilterComponent implements OnInit { + private deploymentService = inject(DeploymentService); + + @Input({ required: true }) filter!: DeploymentFilter; + @Input({ required: true }) index!: number; + @Input() compOptions: ComparatorFilterOption[] = []; + @Output() remove = new EventEmitter(); + + valOptions = signal([]); + + ngOnInit() { + // Pre-seed options with the current value so the select can render it before async options load + if (this.filter?.val) { + this.valOptions.set([String(this.filter.val)]); + } + this.loadOptions(); + console.log("init " + this.filter) + } + + private loadOptions() { + if (this.filter.type === 'booleanType') { + this.valOptions.set(['true', 'false']); + } else if (this.filter.type !== 'SpecialFilterType' && this.filter.type !== 'DateType') { + this.deploymentService.getFilterOptionValues(this.filter.name).subscribe({ + next: (options) => { + this.valOptions.set(options); + }, + }); + } + } + + onRemove() { + this.remove.emit(this.filter); + } +} diff --git a/AMW_angular/io/src/app/deployments/deployments.component.html b/AMW_angular/io/src/app/deployments/deployments.component.html index 11d52f47d..9be2aea70 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.html +++ b/AMW_angular/io/src/app/deployments/deployments.component.html @@ -21,77 +21,21 @@ [(ngModel)]="selectedFilterType" (change)="addFilter()" > - @for (filterType of filterTypes; track filterType.name) { + @for (filterType of filterTypes(); track filterType.name) { } - @if (filters.length > 0) { - @for (filter of filters; track $index; let i = $index) { -
-
- -
- @if (filter.type === 'SpecialFilterType') { -
- } - @if (filter.type !== 'SpecialFilterType') { -
- -
- @if (filter.type !== 'DateType') { - @if (filter.type !== 'booleanType' && filter.type !== 'ENUM_TYPE') { -
- - - @for (filterValueOption of filter.valOptions; track filterValueOption) { - - } - -
- } - @if (filter.type === 'booleanType' || filter.type === 'ENUM_TYPE') { -
- -
- } - } - @if (filter.type === 'DateType') { -
-
- -
-
- } - } -
- -
-
+ @if (filters().length > 0) { + @for (filter of filters(); track $index; let i = $index) { + + } } @@ -103,7 +47,7 @@ Clear filters Clipboard diff --git a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts index 2ceecaf1d..c8e50c09b 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts @@ -81,7 +81,7 @@ describe('DeploymentsComponent (with query params)', () => { expect(deploymentService.canRequestDeployments).toHaveBeenCalled(); }); - it('should enhance filters with the right comparator and comparator options on ngOnInit', () => { + it('should enhance filters with the right comparator on ngOnInit', () => { // given const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, @@ -101,13 +101,13 @@ describe('DeploymentsComponent (with query params)', () => { activatedRouteStub.queryParams.next({ filters: filter }); // then - expect(component.paramFilters[0].compOptions.length).toEqual(1); - expect(component.paramFilters[1].compOptions.length).toEqual(3); + expect(component.paramFilters[0].type).toEqual('StringType'); + expect(component.paramFilters[1].type).toEqual('DateType'); expect(component.paramFilters[0].comp).toEqual('eq'); expect(component.paramFilters[1].comp).toEqual('lt'); }); - it('should enhance filters with the right option values on ngOnInit', () => { + it('should enhance filters with correct filter types on ngOnInit', () => { // given const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, @@ -115,15 +115,6 @@ describe('DeploymentsComponent (with query params)', () => { ]; vi.spyOn(deploymentService, 'getAllDeploymentFilterTypes').mockReturnValue(of(deploymentFilters)); vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of([])); - vi.spyOn(deploymentService, 'getFilterOptionValues').mockImplementation((param: string) => { - const optionValues: { - [key: string]: string[]; - } = { - Application: ['app1', 'app2'], - 'Confirmed on': [], - }; - return of(optionValues[param]); - }); vi.spyOn(deploymentService, 'canRequestDeployments').mockReturnValue(of(true)); // when @@ -131,8 +122,36 @@ describe('DeploymentsComponent (with query params)', () => { activatedRouteStub.queryParams.next({ filters: filter }); // then - expect(component.paramFilters[0].valOptions.length).toEqual(2); - expect(component.paramFilters[1].valOptions.length).toEqual(0); + expect(component.paramFilters[0].name).toEqual('Application'); + expect(component.paramFilters[0].type).toEqual('StringType'); + expect(component.paramFilters[1].name).toEqual('Confirmed on'); + expect(component.paramFilters[1].type).toEqual('DateType'); + }); + + it('should set URL filter value immediately while component loads options asynchronously', () => { + // given + const stateFilter = JSON.stringify([{ name: 'State', comp: 'eq', val: 'failed' }]); + const deploymentFilters: DeploymentFilterType[] = [{ name: 'State', type: 'ENUM_TYPE' }]; + + vi.spyOn(deploymentService, 'getAllDeploymentFilterTypes').mockReturnValue(of(deploymentFilters)); + vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of([])); + vi.spyOn(deploymentService, 'canRequestDeployments').mockReturnValue(of(true)); + vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); + + // when + component.ngOnInit(); + activatedRouteStub.queryParams.next({ filters: stateFilter }); + + // then + expect(component.filters().length).toBe(1); + const filterResult = component.filters()[0]; + + // The filter value should be preserved as 'failed' immediately + expect(filterResult.val).toEqual('failed'); + // Filter should have the correct type + expect(filterResult.type).toEqual('ENUM_TYPE'); + // Component will load options asynchronously + expect(filterResult.name).toEqual('State'); }); it('should apply filters ngOnInit ', () => { @@ -378,14 +397,14 @@ describe('DeploymentsComponent (without query params)', () => { it('should remove filter and reset offset on removeFilter', () => { // given component.offset = 10; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'true', type: 'booleanType', } as DeploymentFilter, - ]; + ]); const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, { name: 'Confirmed on', type: 'DateType' }, @@ -399,24 +418,24 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of(comparatorOptions)); // when - component.removeFilter(component.filters[0]); + component.removeFilter(component.filters()[0]); // then - expect(component.filters.length).toEqual(0); + expect(component.filters().length).toEqual(0); expect(component.offset).toEqual(0); }); it('should reset offset on setMaxResultsPerPage', () => { // given component.offset = 10; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'true', type: 'booleanType', } as DeploymentFilter, - ]; + ]); // when component.setMaxResultsPerPage(20); @@ -498,7 +517,7 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); // given - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', @@ -511,7 +530,7 @@ describe('DeploymentsComponent (without query params)', () => { val: 'TestApp', type: 'StringType', } as DeploymentFilter, - ]; + ]); // when component.applyFilters(); @@ -550,7 +569,7 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of(comparatorOptions)); vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', @@ -575,13 +594,13 @@ describe('DeploymentsComponent (without query params)', () => { val: '', type: 'SpecialFilterType', } as DeploymentFilter, - ]; + ]); // when component.applyFilters(); // then - expect(component.filters.length).toEqual(3); + expect(component.filters().length).toEqual(3); expect(sessionStorage.getItem('deploymentFilters')).toEqual(JSON.stringify(expectedFilters)); expect(deploymentService.getFilteredDeployments).toHaveBeenCalledWith( JSON.stringify(expectedFilters), @@ -609,7 +628,7 @@ describe('DeploymentsComponent (without query params)', () => { { name: 'Confirmed', comp: 'eq', val: 'true' } as DeploymentFilter, { name: 'Application', comp: 'eq', val: 'TestApp' } as DeploymentFilter, ]; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', @@ -622,7 +641,7 @@ describe('DeploymentsComponent (without query params)', () => { val: 'TestApp', type: 'StringType', } as DeploymentFilter, - ]; + ]); const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, { name: 'Confirmed on', type: 'DateType' }, diff --git a/AMW_angular/io/src/app/deployments/deployments.component.ts b/AMW_angular/io/src/app/deployments/deployments.component.ts index 6f281502e..696325de8 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.ts @@ -16,12 +16,12 @@ import { DateTimeModel } from '../shared/date-time-picker/date-time.model'; import { PaginationComponent } from '../shared/pagination/pagination.component'; import { DeploymentsListComponent } from './deployments-list.component'; import { IconComponent } from '../shared/icon/icon.component'; -import { DateTimePickerComponent } from '../shared/date-time-picker/date-time-picker.component'; import { NotificationComponent } from '../shared/elements/notification/notification.component'; import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component'; import { PageComponent } from '../layout/page/page.component'; import { ToastService } from '../shared/elements/toast/toast.service'; import { ButtonComponent } from '../shared/button/button.component'; +import { DeploymentFilterComponent } from './deployment-filter/deployment-filter.component'; @Component({ selector: 'app-deployments', @@ -31,12 +31,12 @@ import { ButtonComponent } from '../shared/button/button.component'; LoadingIndicatorComponent, NotificationComponent, FormsModule, - DateTimePickerComponent, IconComponent, DeploymentsListComponent, PaginationComponent, PageComponent, ButtonComponent, + DeploymentFilterComponent, ], }) export class DeploymentsComponent implements OnInit { @@ -58,23 +58,21 @@ export class DeploymentsComponent implements OnInit { filtersForParam: DeploymentFilter[] = []; // valid for all, loaded once - filterTypes: DeploymentFilterType[] = []; + filterTypes = signal([]); comparatorOptions: ComparatorFilterOption[] = []; comparatorOptionsMap: { [key: string]: string } = {}; + singleComparatorOption: ComparatorFilterOption[] = [{ name: 'eq', displayName: 'is' }]; hasPermissionToRequestDeployments = false; csvSeparator = ''; // available edit actions deploymentDate: number; // for deployment date change - // available filterValues (if any) - filterValueOptions: { [key: string]: string[] } = {}; - // to be added selectedFilterType: DeploymentFilterType; // already set - filters: DeploymentFilter[] = []; + filters = signal([]); // filtered deployments deployments = signal([]); @@ -131,9 +129,7 @@ export class DeploymentsComponent implements OnInit { newFilter.comp = this.defaultComparator; newFilter.val = this.selectedFilterType.type === 'booleanType' ? 'true' : ''; newFilter.type = this.selectedFilterType.type; - newFilter.compOptions = this.comparatorOptionsForType(this.selectedFilterType.type); - this.setValueOptionsForFilter(newFilter); - this.filters.push(newFilter); + this.filters.update((filters) => [...filters, newFilter]); this.offset = 0; this.selectedFilterType = null; this.selectModel.reset(null); @@ -141,19 +137,23 @@ export class DeploymentsComponent implements OnInit { } removeFilter(filter: DeploymentFilter) { - const i: number = _.findIndex(this.filters, { + const i: number = _.findIndex(this.filters(), { name: filter.name, comp: filter.comp, val: filter.val, }); if (i !== -1) { - this.filters.splice(i, 1); + this.filters.update((filters) => { + const newFilters = [...filters]; + newFilters.splice(i, 1); + return newFilters; + }); } this.offset = 0; } clearFilters() { - this.filters = []; + this.filters.set([]); sessionStorage.setItem('deploymentFilters', null); this.updateFiltersInURL(null); } @@ -163,7 +163,7 @@ export class DeploymentsComponent implements OnInit { this.filtersForParam = []; const filtersToBeRemoved: DeploymentFilter[] = []; this.errorMessage = ''; - this.filters.forEach((filter) => { + this.filters().forEach((filter) => { if (filter.val || filter.type === 'SpecialFilterType') { this.filtersForParam.push({ name: filter.name, @@ -332,7 +332,7 @@ export class DeploymentsComponent implements OnInit { private canFilterBeAdded(): boolean { return ( this.selectedFilterType.name !== 'Latest deployment job for App Server and Env' || - _.findIndex(this.filters, { name: this.selectedFilterType.name }) === -1 + _.findIndex(this.filters(), { name: this.selectedFilterType.name }) === -1 ); } @@ -374,21 +374,14 @@ export class DeploymentsComponent implements OnInit { private comparatorOptionsForType(filterType: string) { if (filterType === 'booleanType' || filterType === 'StringType' || filterType === 'ENUM_TYPE') { - return [{ name: 'eq', displayName: 'is' }]; + return this.singleComparatorOption; } else { return this.comparatorOptions; } } - private setValueOptionsForFilter(filter: DeploymentFilter) { - if (!this.filterValueOptions[filter.name]) { - if (filter.type === 'booleanType') { - filter.valOptions = this.filterValueOptions[filter.name] = ['true', 'false']; - } else { - this.getAndSetFilterOptionValues(filter); - } - } - filter.valOptions = this.filterValueOptions[filter.name]; + comparatorOptionsForFilterType(filterType: string) { + return this.comparatorOptionsForType(filterType); } private mapStates() { @@ -422,7 +415,7 @@ export class DeploymentsComponent implements OnInit { private initTypeAndOptions() { this.isLoading.set(true); this.deploymentService.getAllDeploymentFilterTypes().subscribe({ - next: (r) => (this.filterTypes = _.sortBy(r, 'name')), + next: (r) => this.filterTypes.set(_.sortBy(r, 'name')), error: (e) => (this.errorMessage = e), complete: () => { this.getAllComparatorOptions(); @@ -441,14 +434,6 @@ export class DeploymentsComponent implements OnInit { }); } - private getAndSetFilterOptionValues(filter: DeploymentFilter) { - this.deploymentService.getFilterOptionValues(filter.name).subscribe({ - next: (r) => (this.filterValueOptions[filter.name] = r), - error: (e) => (this.errorMessage = e), - complete: () => (filter.valOptions = this.filterValueOptions[filter.name]), - }); - } - private getFilteredDeployments(filterString: string) { this.isLoading.set(true); this.deploymentService @@ -490,23 +475,27 @@ export class DeploymentsComponent implements OnInit { } private enhanceParamFilter() { - if (this.paramFilters) { + if (this.paramFilters && this.paramFilters.length > 0) { this.clearFilters(); + const enhancedFilters: DeploymentFilter[] = []; + this.paramFilters.forEach((filter) => { - const i: number = _.findIndex(this.filterTypes, ['name', filter.name]); + const i: number = _.findIndex(this.filterTypes(), ['name', filter.name]); if (i >= 0) { - filter.type = this.filterTypes[i].type; - filter.compOptions = this.comparatorOptionsForType(filter.type); + filter.type = this.filterTypes()[i].type; filter.comp = !filter.comp ? this.defaultComparator : filter.comp; this.parseDateTime(filter); - this.setValueOptionsForFilter(filter); - this.filters.push(filter); + enhancedFilters.push(filter); } else { this.errorMessage = 'Error parsing filter'; } }); - } - if (this.autoload) { + + this.filters.set(enhancedFilters); + if (this.autoload) { + this.applyFilters(); + } + } else if (this.autoload) { this.applyFilters(); } } From 3d9bcd5cd4bef2c2c614a110c251be0041ea65c9 Mon Sep 17 00:00:00 2001 From: Yves Peter Date: Wed, 28 Jan 2026 10:27:32 +0100 Subject: [PATCH 2/4] refactor(deployments): move type from interface to component, remove some redundant filters --- .../src/app/deployment/deployment-filter.ts | 1 - .../deployment-filter.component.html | 12 +-- .../deployment-filter.component.spec.ts | 12 +-- .../deployment-filter.component.ts | 6 +- .../deployments/deployments.component.html | 5 +- .../deployments/deployments.component.spec.ts | 27 +++---- .../app/deployments/deployments.component.ts | 73 ++++++++----------- 7 files changed, 58 insertions(+), 78 deletions(-) diff --git a/AMW_angular/io/src/app/deployment/deployment-filter.ts b/AMW_angular/io/src/app/deployment/deployment-filter.ts index bbbf8af31..1efd1211f 100644 --- a/AMW_angular/io/src/app/deployment/deployment-filter.ts +++ b/AMW_angular/io/src/app/deployment/deployment-filter.ts @@ -2,5 +2,4 @@ export interface DeploymentFilter { name: string; comp: string; val: any; - type: string; } diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html index f1ad14067..d1fffd10a 100644 --- a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html @@ -2,10 +2,10 @@
- @if (filter.type === 'SpecialFilterType') { + @if (type === 'SpecialFilterType') {
} - @if (filter.type !== 'SpecialFilterType') { + @if (type !== 'SpecialFilterType') {
@@ -31,7 +31,7 @@
} - @if (filter.type === 'booleanType' || filter.type === 'ENUM_TYPE') { + @if (type === 'booleanType' || type === 'ENUM_TYPE') {
+
- @if (type === 'SpecialFilterType') { -
- } - @if (type !== 'SpecialFilterType') { + @if (type() !== FilterType.SPECIAL) {
- @if (type !== 'DateType') { - @if (type !== 'booleanType' && type !== 'ENUM_TYPE') { + @if (type() !== FilterType.DATE) { + @if (type() !== FilterType.BOOLEAN && type() !== FilterType.ENUM) {
- - + + @for (filterValueOption of valOptions(); track filterValueOption) { }
} - @if (type === 'booleanType' || type === 'ENUM_TYPE') { + @if (type() === FilterType.BOOLEAN || type() === FilterType.ENUM) {
- @for (filterValueOption of valOptions(); track filterValueOption) { } @@ -41,11 +38,11 @@
} } - @if (type === 'DateType') { + @if (type() === FilterType.DATE) {
} } -
+
diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts index 45d37b504..cbc335024 100644 --- a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts @@ -20,17 +20,14 @@ describe('DeploymentFilterComponent', () => { fixture = TestBed.createComponent(DeploymentFilterComponent); component = fixture.componentInstance; deploymentService = TestBed.inject(DeploymentService); + + fixture.componentRef.setInput('filter', { name: 'Test Filter', comp: 'eq', val: 'test' } as DeploymentFilter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'StringType'); + fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]); }); it('should create', () => { - component.filter = { - name: 'State', - comp: 'eq', - val: 'failed', - } as DeploymentFilter; - component.index = 0; - component.type = 'ENUM_TYPE'; - component.compOptions = [{ name: 'eq', displayName: 'is' }]; expect(component).toBeTruthy(); }); @@ -41,10 +38,10 @@ describe('DeploymentFilterComponent', () => { val: 'failed', } as DeploymentFilter; - component.filter = filter; - component.index = 0; - component.type = 'ENUM_TYPE'; - component.compOptions = [{ name: 'eq', displayName: 'is' }]; + fixture.componentRef.setInput('filter', filter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'ENUM_TYPE'); + fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]); const mockOptions = ['success', 'failed', 'canceled']; vi.spyOn(deploymentService, 'getFilterOptionValues').mockImplementation(() => { @@ -66,10 +63,10 @@ describe('DeploymentFilterComponent', () => { val: 'true', } as DeploymentFilter; - component.filter = filter; - component.index = 0; - component.type = 'booleanType'; - component.compOptions = [{ name: 'eq', displayName: 'is' }]; + fixture.componentRef.setInput('filter', filter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'booleanType'); + fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]); component.ngOnInit(); @@ -83,10 +80,10 @@ describe('DeploymentFilterComponent', () => { val: '', } as DeploymentFilter; - component.filter = filter; - component.index = 0; - component.type = 'SpecialFilterType'; - component.compOptions = []; + fixture.componentRef.setInput('filter', filter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'SpecialFilterType'); + fixture.componentRef.setInput('compOptions', []); const spy = vi.spyOn(deploymentService, 'getFilterOptionValues'); @@ -103,10 +100,10 @@ describe('DeploymentFilterComponent', () => { val: '', } as DeploymentFilter; - component.filter = filter; - component.index = 0; - component.type = 'DateType'; - component.compOptions = []; + fixture.componentRef.setInput('filter', filter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'DateType'); + fixture.componentRef.setInput('compOptions', []); const spy = vi.spyOn(deploymentService, 'getFilterOptionValues'); @@ -123,9 +120,9 @@ describe('DeploymentFilterComponent', () => { val: 'failed', } as DeploymentFilter; - component.filter = filter; - component.index = 0; - component.type = 'ENUM_TYPE'; + fixture.componentRef.setInput('filter', filter); + fixture.componentRef.setInput('index', 0); + fixture.componentRef.setInput('type', 'ENUM_TYPE'); let emittedFilter: DeploymentFilter | undefined; component.remove.subscribe((f) => (emittedFilter = f)); diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts index 691bdcaa3..14e7139ad 100644 --- a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, - EventEmitter, inject, - Input, + input, OnInit, - Output, + output, signal, } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { DeploymentFilter } from '../../deployment/deployment-filter'; import { ComparatorFilterOption } from '../../deployment/comparator-filter-option'; import { DeploymentService } from '../../deployment/deployment.service'; +import { FilterType } from '../../deployment/filter-type.enum'; import { ButtonComponent } from '../../shared/button/button.component'; import { IconComponent } from '../../shared/icon/icon.component'; import { DateTimePickerComponent } from '../../shared/date-time-picker/date-time-picker.component'; @@ -21,32 +21,33 @@ import { DateTimePickerComponent } from '../../shared/date-time-picker/date-time selector: 'app-deployment-filter', templateUrl: './deployment-filter.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [FormsModule, ButtonComponent, IconComponent, DateTimePickerComponent], + imports: [CommonModule, FormsModule, ButtonComponent, IconComponent, DateTimePickerComponent], }) export class DeploymentFilterComponent implements OnInit { private deploymentService = inject(DeploymentService); + protected readonly FilterType = FilterType; - @Input({ required: true }) filter!: DeploymentFilter; - @Input({ required: true }) index!: number; - @Input({ required: true }) type!: string; - @Input() compOptions: ComparatorFilterOption[] = []; - @Output() remove = new EventEmitter(); + filter = input.required(); + index = input.required(); + type = input.required(); + compOptions = input([]); + remove = output(); valOptions = signal([]); ngOnInit() { // Pre-seed options with the current value so the select can render it before async options load - if (this.filter?.val) { - this.valOptions.set([String(this.filter.val)]); + if (this.filter()?.val) { + this.valOptions.set([String(this.filter().val)]); } this.loadOptions(); } private loadOptions() { - if (this.type === 'booleanType') { + if (this.type() === FilterType.BOOLEAN) { this.valOptions.set(['true', 'false']); - } else if (this.type !== 'SpecialFilterType' && this.type !== 'DateType') { - this.deploymentService.getFilterOptionValues(this.filter.name).subscribe({ + } else if (this.type() !== FilterType.SPECIAL && this.type() !== FilterType.DATE) { + this.deploymentService.getFilterOptionValues(this.filter().name).subscribe({ next: (options) => { this.valOptions.set(options); }, @@ -55,6 +56,6 @@ export class DeploymentFilterComponent implements OnInit { } onRemove() { - this.remove.emit(this.filter); + this.remove.emit(this.filter()); } } diff --git a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts index 68d530199..f64e0be20 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts @@ -9,6 +9,7 @@ import { DeploymentService } from '../deployment/deployment.service'; import { DeploymentsListComponent } from './deployments-list.component'; import { DeploymentsEditModalComponent } from './deployments-edit-modal.component'; import { DeploymentFilterType } from '../deployment/deployment-filter-type'; +import { FilterType } from '../deployment/filter-type.enum'; import { ComparatorFilterOption } from '../deployment/comparator-filter-option'; import { DeploymentFilter } from '../deployment/deployment-filter'; import { Deployment } from '../deployment/deployment'; @@ -123,9 +124,9 @@ describe('DeploymentsComponent (with query params)', () => { // then expect(component.paramFilters[0].name).toEqual('Application'); - expect(component.getFilterType('Application')).toEqual('StringType'); + expect(component.getFilterType('Application')).toEqual(FilterType.STRING); expect(component.paramFilters[1].name).toEqual('Confirmed on'); - expect(component.getFilterType('Confirmed on')).toEqual('DateType'); + expect(component.getFilterType('Confirmed on')).toEqual(FilterType.DATE); }); it('should set URL filter value immediately while component loads options asynchronously', () => { diff --git a/AMW_angular/io/src/app/deployments/deployments.component.ts b/AMW_angular/io/src/app/deployments/deployments.component.ts index 39b9e661c..f4f53a50d 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.ts @@ -1,11 +1,12 @@ import { Location } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnInit, ViewChild, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, ViewChild, computed, inject, signal } from '@angular/core'; import { FormsModule, NgModel } from '@angular/forms'; import { ActivatedRoute, Params } from '@angular/router'; import * as _ from 'lodash-es'; import * as datefns from 'date-fns'; import { Subscription, timer } from 'rxjs'; import { DeploymentFilter } from '../deployment/deployment-filter'; +import { FilterType } from '../deployment/filter-type.enum'; import { DeploymentFilterType } from '../deployment/deployment-filter-type'; import { ComparatorFilterOption } from '../deployment/comparator-filter-option'; import { Deployment } from '../deployment/deployment'; @@ -54,6 +55,16 @@ export class DeploymentsComponent implements OnInit { // valid for all, loaded once filterTypes = signal([]); + private filterTypeMap = computed(() => { + const map = new Map(); + this.filterTypes().forEach(filterType => { + const validFilterTypes = Object.values(FilterType); + if (validFilterTypes.includes(filterType.type as FilterType)) { + map.set(filterType.name, filterType.type as FilterType); + } + }); + return map; + }); comparatorOptions: ComparatorFilterOption[] = []; comparatorOptionsMap: { [key: string]: string } = {}; singleComparatorOption: ComparatorFilterOption[] = [{ name: 'eq', displayName: 'is' }]; @@ -122,7 +133,7 @@ export class DeploymentsComponent implements OnInit { const newFilter: DeploymentFilter = {} as DeploymentFilter; newFilter.name = this.selectedFilterType.name; newFilter.comp = this.defaultComparator; - newFilter.val = this.selectedFilterType.type === 'booleanType' ? 'true' : ''; + newFilter.val = this.selectedFilterType.type === FilterType.BOOLEAN ? 'true' : ''; this.filters.update((filters) => [...filters, newFilter]); this.offset = 0; this.selectedFilterType = null; @@ -160,7 +171,7 @@ export class DeploymentsComponent implements OnInit { const filterType = this.getFilterType(filter.name); if (!filter.val && filterType !== 'SpecialFilterType') { filtersToBeRemoved.push(filter); - } else if (filterType === 'DateType' && !filter.val) { + } else if (filterType === FilterType.DATE && !filter.val) { this.errorMessage = 'Invalid date'; } }); @@ -300,8 +311,8 @@ export class DeploymentsComponent implements OnInit { } } - getFilterType(filterName: string): string | undefined { - return this.filterTypes().find((ft) => ft.name === filterName)?.type; + getFilterType(filterName: string): FilterType | undefined { + return this.filterTypeMap().get(filterName); } private canFilterBeAdded(): boolean { @@ -348,7 +359,7 @@ export class DeploymentsComponent implements OnInit { } private comparatorOptionsForType(filterType: string) { - if (filterType === 'booleanType' || filterType === 'StringType' || filterType === 'ENUM_TYPE') { + if (filterType === FilterType.BOOLEAN || filterType === FilterType.STRING || filterType === FilterType.ENUM) { return this.singleComparatorOption; } else { return this.comparatorOptions; @@ -365,7 +376,8 @@ export class DeploymentsComponent implements OnInit { ({ name: filter.name, comp: filter.comp, - val: this.getFilterType(filter.name) === 'DateType' ? filter.val.toEpoch().toString() : filter.val, + val: this.getFilterType(filter.name) === FilterType.DATE + && filter.val instanceof DateTimeModel ? filter.val.toEpoch().toString() : filter.val, }) as DeploymentFilter, ); return JSON.stringify(filters); @@ -488,8 +500,8 @@ export class DeploymentsComponent implements OnInit { // parse string from json back to DateTimeModel private parseDateTime(filter: DeploymentFilter, filterType: string) { - if (filterType === 'DateType') { - filter.val = DateTimeModel.fromLocalString(filter.val); + if (filterType === FilterType.DATE) { + filter.val = DateTimeModel.fromLocalString(filter.val as string); } }