Skip to content

Commit c5f27d6

Browse files
committed
Refactor table filters dialog: extract DefaultFilterComponent
Why: simplify the dialog template, align all filter rows on a consistent grid regardless of widget type, surface widget display names, and prevent users from re-adding the same column twice. - Add DefaultFilterComponent that wraps simple value editors with their comparator dropdown, mirroring the smart-filter pattern (Email/Phone) - Dialog now resolves one component per row via getFilterComponent / getFilterInputs, eliminating the comparable / nonComparable branching - Show widget.name as the field label in both the "Add filter by" dropdown and the active filter rows - Disable already-filtered fields in the autocomplete options - Replace flex layout with a 3-column grid + subgrid so every filter row renders on a single line with name / value-area / delete aligned
1 parent 0971a23 commit c5f27d6

8 files changed

Lines changed: 311 additions & 141 deletions

File tree

frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,29 @@
77

88
.filters-content {
99
display: grid;
10-
grid-template-columns: auto 228px 0 1fr 32px;
10+
grid-template-columns: auto 1fr 32px;
1111
grid-column-gap: 8px;
1212
align-content: flex-start;
1313
align-items: flex-start;
1414
}
1515

1616
.filters-select {
17-
grid-column: 1 / span 5;
17+
grid-column: 1 / -1;
1818
}
1919

2020
.filter-line {
21-
grid-column: 1 / span 4;
22-
display: flex;
21+
grid-column: 1 / -1;
22+
display: grid;
23+
grid-template-columns: subgrid;
24+
grid-column-gap: 8px;
2325
align-items: flex-start;
24-
gap: 8px;
2526
}
2627

27-
.filter-line > div,
28-
.filter-line > ng-template,
29-
.filter-line > ndc-dynamic {
30-
flex: 1;
28+
.filter-line__field {
29+
grid-column: 2;
3130
min-width: 0;
3231
}
3332

34-
.filter-line > .column-name {
35-
flex: 0 0 auto;
36-
white-space: nowrap;
37-
}
38-
3933
::ng-deep .mat-dialog-container > .ng-star-inserted {
4034
display: flex;
4135
flex-direction: column;
@@ -61,6 +55,7 @@
6155
}
6256

6357
.filter-delete-button {
58+
grid-column: 3;
6459
margin-top: 4px;
6560
}
6661

frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html

Lines changed: 16 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -17,125 +17,33 @@ <h1 mat-dialog-title class="filters-header">
1717
[formControl]="fieldSearchControl">
1818
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="addFilter($event)">
1919
<mat-option *ngFor="let field of foundFields | async"
20-
[ngClass]="{'disabled': field === 'No matches'}"
21-
[value]="field">
22-
{{field}}
20+
[value]="field.key"
21+
[disabled]="isFieldAlreadyFiltered(field.key)">
22+
{{ field.label }}
2323
</mat-option>
2424
</mat-autocomplete>
2525
</mat-form-field>
2626

27-
<ng-container *ngFor="let value of tableRowFieldsShown | keyvalue; trackBy:trackByFn">
28-
<div *ngIf="getComparatorType(getInputType(value.key)) === 'nonComparable'; else comparableFilter" class="filter-line">
29-
<span class='mat-body-1 column-name'>{{value.key}}</span>
30-
31-
<div *ngIf="isWidget(value.key); else defaultTableField">
32-
<ndc-dynamic [ndcDynamicComponent]="tableWidgets[value.key].widget_type ? UIwidgets[tableWidgets[value.key].widget_type] : inputs[tableTypes[value.key]]"
33-
[ndcDynamicInputs]="{
34-
key: value.key,
35-
label: tableWidgets[value.key].name || value.key,
36-
value: tableRowFieldsShown[value.key],
37-
widgetStructure: tableWidgets[value.key],
38-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
39-
autofocus: autofocusField === value.key
40-
}"
27+
@for (value of tableRowFieldsShown | keyvalue; track value.key) {
28+
<div class="filter-line">
29+
<span class='mat-body-1 column-name'>{{ getFieldLabel(value.key) }}</span>
30+
<div class="filter-line__field">
31+
<ndc-dynamic
32+
[ndcDynamicComponent]="getFilterComponent(value.key)"
33+
[ndcDynamicInputs]="getFilterInputs(value.key)"
4134
[ndcDynamicOutputs]="{
4235
onFieldChange: { handler: updateField, args: ['$event', value.key] },
4336
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
4437
}"
4538
></ndc-dynamic>
4639
</div>
47-
48-
<ng-template #defaultTableField>
49-
<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
50-
[ndcDynamicInputs]="{
51-
key: value.key,
52-
label: value.key,
53-
value: tableRowFieldsShown[value.key],
54-
structure: tableRowStructure[value.key],
55-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
56-
autofocus: autofocusField === value.key
57-
}"
58-
[ndcDynamicOutputs]="{
59-
onFieldChange: { handler: updateField, args: ['$event', value.key] },
60-
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
61-
}"
62-
></ndc-dynamic>
63-
</ng-template>
40+
<button mat-icon-button type="button" class="filter-delete-button"
41+
matTooltip="Remove"
42+
(click)="removeFilter(value.key)">
43+
<mat-icon>close</mat-icon>
44+
</button>
6445
</div>
65-
66-
<ng-template #comparableFilter>
67-
<span class='mat-body-1 column-name'>{{value.key}}</span>
68-
69-
<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'text'"
70-
appearance="outline">
71-
<mat-select name="textComparator-{{value.key}}"
72-
[(ngModel)]="tableRowFieldsComparator[value.key]"
73-
(ngModelChange)="updateComparator($event, value.key)">
74-
<mat-option value="startswith">
75-
starts with
76-
</mat-option>
77-
<mat-option value="endswith">
78-
ends with
79-
</mat-option>
80-
<mat-option value="eq">
81-
equal
82-
</mat-option>
83-
<mat-option value="contains">
84-
contains
85-
</mat-option>
86-
<mat-option value="icontains">
87-
not contains
88-
</mat-option>
89-
<mat-option value="empty">
90-
is empty
91-
</mat-option>
92-
</mat-select>
93-
</mat-form-field>
94-
95-
<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'number'"
96-
appearance="outline">
97-
<mat-select name="numberComparator-{{value.key}}"
98-
[(ngModel)]="tableRowFieldsComparator[value.key]"
99-
(ngModelChange)="updateComparator($event, value.key)">
100-
<mat-option value="eq">
101-
equal
102-
</mat-option>
103-
<mat-option value="gt">
104-
greater than
105-
</mat-option>
106-
<mat-option value="lt">
107-
less than
108-
</mat-option>
109-
<mat-option value="gte">
110-
greater than or equal
111-
</mat-option>
112-
<mat-option value="lte">
113-
less than or equal
114-
</mat-option>
115-
</mat-select>
116-
</mat-form-field>
117-
118-
<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
119-
[ndcDynamicInputs]="{
120-
key: value.key,
121-
label: value.key,
122-
value: tableRowFieldsShown[value.key],
123-
readonly: tableRowFieldsComparator[value.key] === 'empty',
124-
structure: tableRowStructure[value.key],
125-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
126-
autofocus: autofocusField === value.key
127-
}"
128-
[ndcDynamicOutputs]="{
129-
onFieldChange: { handler: updateField, args: ['$event', value.key] }
130-
}"
131-
></ndc-dynamic>
132-
</ng-template>
133-
<button mat-icon-button type="button" class="filter-delete-button"
134-
matTooltip="Remove"
135-
(click)="removeFilter(value.key)">
136-
<mat-icon>close</mat-icon>
137-
</button>
138-
</ng-container>
46+
}
13947
</mat-dialog-content>
14048
</ng-template>
14149

frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ describe('DbTableFiltersDialogComponent', () => {
111111
await fixture.whenStable();
112112

113113
expect(component.tableForeignKeys).toEqual(mockStructureForFilterDialog.foreignKeys);
114-
expect(component.fields).toEqual(['FirstName', 'Id', 'bool']);
114+
expect(component.fields).toEqual([
115+
{ key: 'FirstName', label: 'FirstName' },
116+
{ key: 'Id', label: 'Id' },
117+
{ key: 'bool', label: 'bool' },
118+
]);
115119
expect(component.tableRowStructure).toEqual({
116120
FirstName: fakeFirstName,
117121
Id: fakeId,
@@ -221,4 +225,25 @@ describe('DbTableFiltersDialogComponent', () => {
221225

222226
expect(component.isWidget('FirstName')).toBe(true);
223227
});
228+
229+
it('should report fields with active filters as already filtered', () => {
230+
component.tableRowFieldsShown = { FirstName: 'John' };
231+
expect(component.isFieldAlreadyFiltered('FirstName')).toBe(true);
232+
expect(component.isFieldAlreadyFiltered('Id')).toBe(false);
233+
});
234+
235+
it('should use widget name as field label when widget has a name', () => {
236+
component.data.structure.widgets = {
237+
FirstName: {
238+
field_name: 'FirstName',
239+
widget_type: 'Email',
240+
widget_params: {},
241+
name: 'Customer Email',
242+
description: '',
243+
},
244+
};
245+
component.ngOnInit();
246+
247+
expect(component.fields).toEqual(expect.arrayContaining([{ key: 'FirstName', label: 'Customer Email' }]));
248+
});
224249
});

frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { CommonModule } from '@angular/common';
2-
import { AfterViewInit, Component, Inject, KeyValueDiffer, KeyValueDiffers, OnInit } from '@angular/core';
2+
import { AfterViewInit, Component, Inject, KeyValueDiffer, KeyValueDiffers, OnInit, Type } from '@angular/core';
33
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
44
import { MatAutocompleteModule } from '@angular/material/autocomplete';
55
import { MatButtonModule } from '@angular/material/button';
66
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
77
import { MatFormFieldModule } from '@angular/material/form-field';
88
import { MatIconModule } from '@angular/material/icon';
99
import { MatInputModule } from '@angular/material/input';
10-
import { MatSelectModule } from '@angular/material/select';
1110
import { ActivatedRoute, RouterModule } from '@angular/router';
1211
import JsonURL from '@jsonurl/jsonurl';
1312
import { Angulartics2OnModule } from 'angulartics2';
@@ -24,6 +23,7 @@ import { getTableTypes } from 'src/app/lib/setup-table-row-structure';
2423
import { TableField, TableForeignKey, Widget } from 'src/app/models/table';
2524
import { ConnectionsService } from 'src/app/services/connections.service';
2625
import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component';
26+
import { DefaultFilterComponent } from '../../../ui-components/filter-fields/default-filter/default-filter.component';
2727

2828
@Component({
2929
selector: 'app-db-table-filters-dialog',
@@ -38,7 +38,6 @@ import { ContentLoaderComponent } from '../../../ui-components/content-loader/co
3838
MatInputModule,
3939
MatButtonModule,
4040
MatIconModule,
41-
MatSelectModule,
4241
DynamicModule,
4342
SignalComponentIoModule,
4443
RouterModule,
@@ -52,8 +51,8 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
5251
public tableFilters = [];
5352
public fieldSearchControl = new FormControl('');
5453

55-
public fields: string[];
56-
public foundFields: Observable<string[]>;
54+
public fields: { key: string; label: string }[];
55+
public foundFields: Observable<{ key: string; label: string }[]>;
5756

5857
public tableRowFields: Object;
5958
public tableRowStructure: Object;
@@ -83,9 +82,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
8382
...this.data.structure.structure.map((field: TableField) => ({ [field.column_name]: undefined })),
8483
);
8584
this.tableTypes = getTableTypes(this.data.structure.structure, this.data.structure.foreignKeysList);
86-
this.fields = this.data.structure.structure
87-
.filter((field: TableField) => this.getInputType(field.column_name) !== 'file')
88-
.map((field: TableField) => field.column_name);
8985
this.tableRowStructure = Object.assign(
9086
{},
9187
...this.data.structure.structure.map((field: TableField) => {
@@ -134,6 +130,13 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
134130
this.setWidgets(widgetsArray);
135131
}
136132

133+
this.fields = this.data.structure.structure
134+
.filter((field: TableField) => this.getInputType(field.column_name) !== 'file')
135+
.map((field: TableField) => ({
136+
key: field.column_name,
137+
label: this.getFieldLabel(field.column_name),
138+
}));
139+
137140
if (this.autofocusField && this.tableFilters && !this.tableFilters.includes(this.autofocusField)) {
138141
this.tableFilters.push(this.autofocusField);
139142
if (!this.tableRowFieldsShown[this.autofocusField]) {
@@ -197,8 +200,53 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
197200
}
198201
}
199202

200-
private _filter(value: string): string[] {
201-
return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase()));
203+
isFieldAlreadyFiltered(key: string): boolean {
204+
return Object.hasOwn(this.tableRowFieldsShown, key);
205+
}
206+
207+
getFieldLabel(key: string): string {
208+
return this.tableWidgets?.[key]?.name || key;
209+
}
210+
211+
getFilterComponent(field: string): Type<any> {
212+
const valueComponent = this.resolveValueComponent(field);
213+
const innerType = (valueComponent as { type?: string })?.type;
214+
if (innerType === 'text' || innerType === 'number' || innerType === 'datetime') {
215+
return DefaultFilterComponent;
216+
}
217+
return valueComponent;
218+
}
219+
220+
getFilterInputs(field: string): Record<string, any> {
221+
const valueComponent = this.resolveValueComponent(field);
222+
const innerType = (valueComponent as { type?: string })?.type;
223+
const baseInputs: Record<string, any> = {
224+
key: field,
225+
label: this.getFieldLabel(field),
226+
value: this.tableRowFieldsShown[field],
227+
structure: this.tableRowStructure[field],
228+
relations: this.tableTypes[field] === 'foreign key' ? this.tableForeignKeys[field] : undefined,
229+
autofocus: this.autofocusField === field,
230+
};
231+
232+
if (innerType === 'text' || innerType === 'number' || innerType === 'datetime') {
233+
return {
234+
...baseInputs,
235+
valueComponent,
236+
comparator: this.tableRowFieldsComparator[field] || 'eq',
237+
};
238+
}
239+
240+
if (this.isWidget(field)) {
241+
return { ...baseInputs, widgetStructure: this.tableWidgets[field] };
242+
}
243+
244+
return baseInputs;
245+
}
246+
247+
private _filter(value: string): { key: string; label: string }[] {
248+
const v = value.toLowerCase();
249+
return this.fields.filter((field) => field.key.toLowerCase().includes(v) || field.label.toLowerCase().includes(v));
202250
}
203251

204252
ngDoCheck() {
@@ -231,10 +279,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
231279
);
232280
}
233281

234-
trackByFn(_index: number, item: any) {
235-
return item.key;
236-
}
237-
238282
isWidget(columnName: string) {
239283
return this.tableWidgets && columnName in this.tableWidgets;
240284
}
@@ -257,10 +301,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
257301
this.fieldSearchControl.setValue('');
258302
}
259303

260-
updateComparator(event, fieldName: string) {
261-
if (event === 'empty') this.tableRowFieldsShown[fieldName] = '';
262-
}
263-
264304
resetFilters() {
265305
this.tableFilters = [];
266306
this.tableRowFieldsShown = {};
@@ -290,4 +330,14 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
290330
delete this.tableRowFieldsShown[field];
291331
delete this.tableRowFieldsComparator[field];
292332
}
333+
334+
private resolveValueComponent(field: string): Type<any> {
335+
if (this.isWidget(field)) {
336+
const widgetType = this.tableWidgets[field].widget_type;
337+
if (widgetType && this.UIwidgets[widgetType]) {
338+
return this.UIwidgets[widgetType];
339+
}
340+
}
341+
return this.inputs[this.tableTypes[field]];
342+
}
293343
}

0 commit comments

Comments
 (0)