Skip to content

Commit 4dfcb1e

Browse files
guguclaude
andcommitted
Make boolean and select filter widgets smart filters
Boolean filter: emit onComparatorChange, rename Unknown → Null, only show the Null option when the column allows nulls, and restore null/empty/'' values correctly on URL load. Select filter: convert to mat-select multiple, emit eq+scalar for a single pick, in+array for multi-pick, and undefined when cleared so the filter is dropped. Restore scalar, null, and array values on URL load. Both use the existing smart-filter pattern so the dialog manages no comparator dropdown for them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c5f27d6 commit 4dfcb1e

6 files changed

Lines changed: 284 additions & 69 deletions

File tree

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
1-
<div *ngIf="isRadiogroup; else checkboxElement" class="radio-line">
2-
<label id="">{{normalizedLabel()}}</label>
1+
<div class="radio-line">
2+
<label>{{normalizedLabel()}}</label>
33
<mat-button-toggle-group name="{{label()}}-{{key()}}" attr.data-testid="record-{{label()}}-boolean-radio-group"
44
[hideSingleSelectionIndicator]="true"
55
[disabled]="disabled()"
66
[(ngModel)]="booleanValue" (ngModelChange)="onBooleanChange()">
7-
<mat-button-toggle [value]=true>Yes</mat-button-toggle>
8-
<mat-button-toggle [value]=false>No</mat-button-toggle>
9-
<mat-button-toggle value="unknown">Unknown</mat-button-toggle>
7+
<mat-button-toggle [value]="true">Yes</mat-button-toggle>
8+
<mat-button-toggle [value]="false">No</mat-button-toggle>
9+
@if (isRadiogroup) {
10+
<mat-button-toggle value="unknown">Null</mat-button-toggle>
11+
}
1012
</mat-button-toggle-group>
1113
</div>
12-
13-
<ng-template #checkboxElement>
14-
<div class="radio-line">
15-
<label id="">{{normalizedLabel()}}</label>
16-
<mat-button-toggle-group name="{{label()}}-{{key()}}" attr.data-testid="record-{{label()}}-boolean-radio-group"
17-
[hideSingleSelectionIndicator]="true"
18-
[disabled]="disabled()"
19-
[(ngModel)]="booleanValue" (ngModelChange)="onBooleanChange()">
20-
<mat-button-toggle [value]=true>Yes</mat-button-toggle>
21-
<mat-button-toggle [value]=false>No</mat-button-toggle>
22-
</mat-button-toggle-group>
23-
</div>
24-
</ng-template>

frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.spec.ts

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('BooleanFilterComponent', () => {
99
let component: BooleanFilterComponent;
1010
let fixture: ComponentFixture<BooleanFilterComponent>;
1111

12-
const fakeStructure = {
12+
const fakeStructureNotNull = {
1313
column_name: 'banned',
1414
column_default: '0',
1515
data_type: 'tinyint',
@@ -20,6 +20,11 @@ describe('BooleanFilterComponent', () => {
2020
character_maximum_length: 1,
2121
};
2222

23+
const fakeStructureNullable = {
24+
...fakeStructureNotNull,
25+
allow_null: true,
26+
};
27+
2328
beforeEach(async () => {
2429
await TestBed.configureTestingModule({
2530
imports: [MatSnackBarModule, MatDialogModule, BooleanFilterComponent, BrowserAnimationsModule],
@@ -36,44 +41,104 @@ describe('BooleanFilterComponent', () => {
3641
expect(component).toBeTruthy();
3742
});
3843

39-
it('should set booleanValue in false when input value is 0', () => {
44+
it('should set booleanValue to false when input value is 0', () => {
4045
component.value = 0;
41-
fixture.componentRef.setInput('structure', fakeStructure);
46+
fixture.componentRef.setInput('structure', fakeStructureNotNull);
4247
component.ngOnInit();
4348

4449
expect(component.booleanValue).toEqual(false);
4550
});
4651

47-
it('should set booleanValue in unknown when input value is null', () => {
52+
it('should set booleanValue to unknown when input value is null', () => {
4853
component.value = null;
49-
fixture.componentRef.setInput('structure', fakeStructure);
54+
fixture.componentRef.setInput('structure', fakeStructureNullable);
55+
component.ngOnInit();
56+
57+
expect(component.booleanValue).toEqual('unknown');
58+
});
59+
60+
it('should set booleanValue to unknown when input value is empty string', () => {
61+
component.value = '';
62+
fixture.componentRef.setInput('structure', fakeStructureNullable);
5063
component.ngOnInit();
5164

5265
expect(component.booleanValue).toEqual('unknown');
5366
});
5467

55-
it('should set isRadiogroup in false if allow_null is false', () => {
68+
it('should set isRadiogroup to false if allow_null is false', () => {
5669
component.value = undefined;
57-
fixture.componentRef.setInput('structure', fakeStructure);
70+
fixture.componentRef.setInput('structure', fakeStructureNotNull);
5871
component.ngOnInit();
5972

6073
expect(component.isRadiogroup).toEqual(false);
6174
});
6275

63-
it('should set isRadiogroup in true if allow_null is true', () => {
76+
it('should set isRadiogroup to true if allow_null is true', () => {
6477
component.value = undefined;
65-
fixture.componentRef.setInput('structure', {
66-
column_name: 'banned',
67-
column_default: '0',
68-
data_type: 'tinyint',
69-
isExcluded: false,
70-
isSearched: false,
71-
auto_increment: false,
72-
allow_null: true,
73-
character_maximum_length: 1,
74-
});
78+
fixture.componentRef.setInput('structure', fakeStructureNullable);
7579
component.ngOnInit();
7680

7781
expect(component.isRadiogroup).toEqual(true);
7882
});
83+
84+
it('should emit eq comparator when Yes is toggled', () => {
85+
vi.spyOn(component.onFieldChange, 'emit');
86+
vi.spyOn(component.onComparatorChange, 'emit');
87+
fixture.componentRef.setInput('structure', fakeStructureNullable);
88+
component.ngOnInit();
89+
90+
component.booleanValue = true;
91+
component.onBooleanChange();
92+
93+
expect(component.onFieldChange.emit).toHaveBeenCalled();
94+
expect(component.onComparatorChange.emit).toHaveBeenCalledWith('eq');
95+
});
96+
97+
it('should emit eq comparator when No is toggled', () => {
98+
vi.spyOn(component.onFieldChange, 'emit');
99+
vi.spyOn(component.onComparatorChange, 'emit');
100+
fixture.componentRef.setInput('structure', fakeStructureNullable);
101+
component.ngOnInit();
102+
103+
component.booleanValue = false;
104+
component.onBooleanChange();
105+
106+
expect(component.onFieldChange.emit).toHaveBeenCalled();
107+
expect(component.onComparatorChange.emit).toHaveBeenCalledWith('eq');
108+
});
109+
110+
it('should emit eq comparator and null value when Null is toggled', () => {
111+
vi.spyOn(component.onFieldChange, 'emit');
112+
vi.spyOn(component.onComparatorChange, 'emit');
113+
fixture.componentRef.setInput('structure', fakeStructureNullable);
114+
component.ngOnInit();
115+
116+
component.booleanValue = 'unknown';
117+
component.onBooleanChange();
118+
119+
expect(component.onFieldChange.emit).toHaveBeenCalledWith(null);
120+
expect(component.onComparatorChange.emit).toHaveBeenCalledWith('eq');
121+
});
122+
123+
it('should render Null option when allow_null is true', () => {
124+
fixture.componentRef.setInput('structure', fakeStructureNullable);
125+
fixture.detectChanges();
126+
127+
const buttons = fixture.nativeElement.querySelectorAll('mat-button-toggle');
128+
const labels = Array.from(buttons).map((b: Element) => b.textContent?.trim());
129+
130+
expect(labels).toContain('Null');
131+
});
132+
133+
it('should not render Null option when allow_null is false', () => {
134+
fixture.componentRef.setInput('structure', fakeStructureNotNull);
135+
fixture.detectChanges();
136+
137+
const buttons = fixture.nativeElement.querySelectorAll('mat-button-toggle');
138+
const labels = Array.from(buttons).map((b: Element) => b.textContent?.trim());
139+
140+
expect(labels).not.toContain('Null');
141+
expect(labels).toContain('Yes');
142+
expect(labels).toContain('No');
143+
});
79144
});

frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CommonModule } from '@angular/common';
2-
import { Component, Input } from '@angular/core';
2+
import { AfterViewInit, Component, Input } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
44
import { MatButtonToggleModule } from '@angular/material/button-toggle';
55
import { DBtype } from 'src/app/models/connection';
@@ -12,12 +12,12 @@ import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field
1212
styleUrls: ['./boolean.component.css'],
1313
imports: [CommonModule, FormsModule, MatButtonToggleModule],
1414
})
15-
export class BooleanFilterComponent extends BaseFilterFieldComponent {
15+
export class BooleanFilterComponent extends BaseFilterFieldComponent implements AfterViewInit {
1616
@Input() value;
1717

1818
public isRadiogroup: boolean;
19-
private connectionType: DBtype;
2019
public booleanValue: boolean | 'unknown';
20+
private connectionType: DBtype;
2121

2222
constructor(private _connections: ConnectionsService) {
2323
super();
@@ -36,28 +36,36 @@ export class BooleanFilterComponent extends BaseFilterFieldComponent {
3636
this.isRadiogroup = this.structure()?.allow_null || !!parsedParams?.allow_null;
3737
}
3838

39+
ngAfterViewInit(): void {
40+
this.onComparatorChange.emit('eq');
41+
}
42+
3943
setBooleanValue() {
4044
if (typeof this.value === 'boolean') {
4145
this.booleanValue = this.value;
42-
} else if (this.value === null) {
46+
return;
47+
}
48+
49+
if (this.value === null || this.value === undefined || this.value === '') {
4350
this.booleanValue = 'unknown';
44-
} else {
45-
switch (this.value) {
46-
case 0:
47-
case '0':
48-
case 'F':
49-
case 'N':
50-
case 'false':
51-
this.booleanValue = false;
52-
break;
53-
case 1:
54-
case '1':
55-
case 'T':
56-
case 'Y':
57-
case 'true':
58-
this.booleanValue = true;
59-
break;
60-
}
51+
return;
52+
}
53+
54+
switch (this.value) {
55+
case 0:
56+
case '0':
57+
case 'F':
58+
case 'N':
59+
case 'false':
60+
this.booleanValue = false;
61+
break;
62+
case 1:
63+
case '1':
64+
case 'T':
65+
case 'Y':
66+
case 'true':
67+
this.booleanValue = true;
68+
break;
6169
}
6270
}
6371

@@ -74,5 +82,6 @@ export class BooleanFilterComponent extends BaseFilterFieldComponent {
7482
}
7583

7684
this.onFieldChange.emit(formattedValue);
85+
this.onComparatorChange.emit('eq');
7786
}
7887
}
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<mat-form-field class="select-form-field" appearance="outline">
22
<mat-label>{{normalizedLabel()}}</mat-label>
3-
<mat-select name="{{label()}}-{{key()}}"
4-
[required]="required()" [disabled]="disabled()" [readonly]="readonly()"
3+
<mat-select name="{{label()}}-{{key()}}" multiple
4+
[required]="required()" [disabled]="disabled()"
55
attr.data-testid="record-{{label()}}-select"
6-
[(ngModel)]="value" (ngModelChange)="onFieldChange.emit($event)">
7-
<mat-option *ngFor="let option of options"
8-
[value]="option.value">
9-
{{option.label}}
10-
</mat-option>
6+
[(ngModel)]="selectedValues" (ngModelChange)="onSelectionChange($event)">
7+
@for (option of options; track option.value) {
8+
<mat-option [value]="option.value">{{option.label}}</mat-option>
9+
}
1110
</mat-select>
1211
</mat-form-field>

0 commit comments

Comments
 (0)