Skip to content

Commit 45c4514

Browse files
guguclaude
andcommitted
Fix widget filter component loading and add comparator selectors
- Fix dynamic component loading for widget-type filters in all three filter contexts (main dialog, saved-filters dialog, saved-filters panel). Widget data was passed as an object but guards checked .length which is undefined on objects, so setWidgets() was never called and widget filter components never loaded. - Add built-in comparator selectors to Email and Phone filter components (domain equal, equal, contains, starts with, etc.) so each component manages its own filter logic independently. - Move comparator emission from ngOnInit to ngAfterViewInit so it fires after ndc-dynamic subscribes to outputs. - Show field name label for nonComparable filter fields. - Add Email to widget defaultParams so it can be configured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d9322f commit 45c4514

15 files changed

Lines changed: 357 additions & 63 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ <h1 mat-dialog-title class="filters-header">
2626

2727
<ng-container *ngFor="let value of tableRowFieldsShown | keyvalue; trackBy:trackByFn">
2828
<div *ngIf="getComparatorType(getInputType(value.key)) === 'nonComparable'; else comparableFilter" class="filter-line">
29+
<span class='mat-body-1 column-name'>{{value.key}}</span>
2930

3031
<div *ngIf="isWidget(value.key); else defaultTableField">
3132
<ndc-dynamic [ndcDynamicComponent]="tableWidgets[value.key].widget_type ? UIwidgets[tableWidgets[value.key].widget_type] : inputs[tableTypes[value.key]]"

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,46 @@ describe('DbTableFiltersDialogComponent', () => {
179179
const comparatorType = component.getComparatorType(undefined);
180180
expect(comparatorType).toEqual('nonComparable');
181181
});
182+
183+
it('should receive comparator from EmailFilterComponent after it renders', async () => {
184+
component.setWidgets([
185+
{
186+
field_name: 'FirstName',
187+
widget_type: 'Email',
188+
widget_params: '// No settings required',
189+
name: '',
190+
description: '',
191+
},
192+
]);
193+
194+
component.addFilter({ option: { value: 'FirstName' } });
195+
196+
// Default comparator is 'eq' from addFilter
197+
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');
198+
199+
// Trigger change detection so ndc-dynamic renders EmailFilterComponent
200+
fixture.detectChanges();
201+
await fixture.whenStable();
202+
203+
// EmailFilterComponent emits 'eq' (default mode) in ngAfterViewInit,
204+
// which updateComparatorFromComponent receives
205+
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');
206+
});
207+
208+
it('should recognize widgets when passed as object (real app format)', () => {
209+
const widgetsObject = {
210+
FirstName: {
211+
field_name: 'FirstName',
212+
widget_type: 'Email',
213+
widget_params: {},
214+
name: '',
215+
description: '',
216+
},
217+
};
218+
219+
component.data.structure.widgets = widgetsObject;
220+
component.ngOnInit();
221+
222+
expect(component.isWidget('FirstName')).toBe(true);
223+
});
182224
});

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,10 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
140140
}
141141
}
142142

143-
if (this.data.structure.widgets && this.data.structure.widgets.length) {
144-
this.setWidgets(this.data.structure.widgets);
143+
const widgets = this.data.structure.widgets;
144+
const widgetsArray = Array.isArray(widgets) ? widgets : Object.values(widgets || {});
145+
if (widgetsArray.length) {
146+
this.setWidgets(widgetsArray);
145147
}
146148

147149
// If autofocusField is provided, ensure it's in the filters list
@@ -231,8 +233,10 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
231233
{},
232234
...widgets.map((widget: Widget) => {
233235
let params;
234-
if (widget.widget_params !== '// No settings required') {
236+
if (typeof widget.widget_params === 'string' && widget.widget_params !== '// No settings required') {
235237
params = JSON5.parse(widget.widget_params);
238+
} else if (typeof widget.widget_params !== 'string') {
239+
params = widget.widget_params;
236240
} else {
237241
params = '';
238242
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export class DbTableWidgetsComponent implements OnInit {
254254
"name": ""
255255
}
256256
`,
257+
Email: `// No settings required`,
257258
S3: `// Configure AWS S3 widget for file storage
258259
// bucket: S3 bucket name (required)
259260
// prefix: Optional path prefix for uploaded files

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ <h1 mat-dialog-title class="filters-header">
9090
<mat-icon>close</mat-icon>
9191
</button>
9292
<div *ngIf="getComparatorType(getInputType(value.key)) === 'nonComparable'; else comparableFilter" class="filter-line">
93+
<span class='mat-body-1 column-name'>{{value.key}}</span>
9394

9495
<div *ngIf="isWidget(value.key); else defaultTableField">
9596
<ndc-dynamic [ndcDynamicComponent]="tableWidgets[value.key].widget_type ? UIwidgets[tableWidgets[value.key].widget_type] : inputs[tableTypes[value.key]]"

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,11 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit {
140140
}),
141141
);
142142

143-
// Setup widgets if available
144-
if (this.data.tableWidgets?.length) {
145-
this.setWidgets(this.data.tableWidgets);
143+
// Setup widgets - data comes pre-processed as object { field_name: { widget_type, widget_params, ... } }
144+
const tableWidgets = this.data.tableWidgets;
145+
if (tableWidgets && Object.keys(tableWidgets).length) {
146+
this.tableWidgetsList = Object.keys(tableWidgets);
147+
this.tableWidgets = tableWidgets;
146148
}
147149

148150
this.foundFields = this.fieldSearchControl.valueChanges.pipe(
@@ -196,12 +198,14 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit {
196198
{},
197199
...widgets.map((widget: any) => {
198200
let params;
199-
if (widget.widget_params !== '// No settings required') {
201+
if (typeof widget.widget_params === 'string' && widget.widget_params !== '// No settings required') {
200202
try {
201203
params = JSON.parse(widget.widget_params);
202204
} catch (_e) {
203205
params = '';
204206
}
207+
} else if (typeof widget.widget_params !== 'string') {
208+
params = widget.widget_params;
205209
} else {
206210
params = '';
207211
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
</mat-chip-option>
4040
</mat-chip-listbox>
4141
<mat-menu #filterMenu="matMenu">
42-
<button mat-menu-item (click)="handleEditFilter(currentFilterForMenu)">
42+
<button mat-menu-item type="button" (click)="handleEditFilter(currentFilterForMenu)">
4343
<mat-icon fontSet="material-symbols-outlined">create</mat-icon>
4444
<span>Edit</span>
4545
</button>
46-
<button mat-menu-item (click)="handleDeleteFilter(currentFilterForMenu)">
46+
<button mat-menu-item type="button" (click)="handleDeleteFilter(currentFilterForMenu)">
4747
<mat-icon fontSet="material-symbols-outlined">delete</mat-icon>
4848
<span>Delete</span>
4949
</button>
@@ -58,7 +58,7 @@
5858
<form *ngIf="savedFilterMap[selectedFilterSetId]?.dynamicColumn"
5959
class="dynamic-column-editor"
6060
(ngSubmit)="applyDynamicColumnChanges()">
61-
<span class="column-name mat-body-1" *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) !== 'nonComparable'">
61+
<span class="column-name mat-body-1">
6262
<strong>{{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }}</strong>
6363
<span *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'text'" class="comparator-text">
6464
{{ {startswith: 'starts with', endswith: 'ends with', eq: 'equal', contains: 'contains', icontains: 'not contains', empty: 'is empty'}[savedFilterMap[selectedFilterSetId].dynamicColumn.operator] }}
@@ -138,7 +138,8 @@
138138
autofocus: shouldAutofocus
139139
}"
140140
[ndcDynamicOutputs]="{
141-
onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] }
141+
onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] },
142+
onComparatorChange: { handler: updateDynamicColumnComparator, args: ['$event'] }
142143
}"
143144
></ndc-dynamic>
144145
</div>
@@ -154,7 +155,8 @@
154155
autofocus: shouldAutofocus
155156
}"
156157
[ndcDynamicOutputs]="{
157-
onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] }
158+
onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] },
159+
onComparatorChange: { handler: updateDynamicColumnComparator, args: ['$event'] }
158160
}"
159161
></ndc-dynamic>
160162
</ng-template>

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { Angulartics2OnModule } from 'angulartics2';
1717
import { DynamicModule } from 'ng-dynamic-component';
1818
import posthog from 'posthog-js';
1919
import { PlaceholderSavedFiltersComponent } from 'src/app/components/skeletons/placeholder-saved-filters/placeholder-saved-filters.component';
20-
import { filterTypes } from 'src/app/consts/filter-types';
21-
import { UIwidgets } from 'src/app/consts/record-edit-types';
20+
import { UIwidgets as FilterUIwidgets, filterTypes } from 'src/app/consts/filter-types';
21+
import { UIwidgets as EditUIwidgets } from 'src/app/consts/record-edit-types';
2222
import { normalizeTableName } from 'src/app/lib/normalize';
2323
import { TableField, TableForeignKey } from 'src/app/models/table';
2424
import { AccessLevel } from 'src/app/models/user';
@@ -76,7 +76,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy {
7676
public tableRowFieldsShown: { [key: string]: any } = {};
7777
public tableRowStructure: { [key: string]: any } = {};
7878
public tableWidgetsList: string[] = [];
79-
public UIwidgets = UIwidgets;
79+
public UIwidgets = { ...EditUIwidgets, ...FilterUIwidgets };
8080

8181
public displayedComparators = {
8282
eq: '=',
@@ -244,6 +244,9 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy {
244244
if (this.resetSelection) {
245245
this.selectedFilterSetId = null;
246246
}
247+
if (this.tableWidgets) {
248+
this.tableWidgetsList = Object.keys(this.tableWidgets);
249+
}
247250
}
248251

249252
ngOnDestroy() {
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1-
.email-form-field {
1+
.email-filter-row {
2+
display: flex;
3+
gap: 8px;
4+
align-items: flex-start;
25
width: 100%;
36
}
7+
8+
.comparator-field {
9+
flex: 0 0 auto;
10+
min-width: 150px;
11+
}
12+
13+
.value-field {
14+
flex: 1;
15+
}
Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1-
<mat-form-field class="email-form-field" appearance="outline">
2-
<mat-label>{{normalizedLabel}} (domain)</mat-label>
3-
<span matTextPrefix>@&nbsp;</span>
4-
<input matInput type="text" name="{{label}}-{{key}}"
5-
#inputElement
6-
[required]="required" [disabled]="disabled" [readonly]="readonly"
7-
placeholder="gmail.com"
8-
[(ngModel)]="domainValue" (ngModelChange)="onDomainChange($event)">
9-
</mat-form-field>
1+
<div class="email-filter-row">
2+
<mat-form-field class="comparator-field" appearance="outline">
3+
<mat-select [(ngModel)]="filterMode" (ngModelChange)="onFilterModeChange($event)">
4+
<mat-option value="eq">equal</mat-option>
5+
<mat-option value="domain">domain equal</mat-option>
6+
<mat-option value="contains">contains</mat-option>
7+
<mat-option value="startswith">starts with</mat-option>
8+
<mat-option value="endswith">ends with</mat-option>
9+
<mat-option value="empty">is empty</mat-option>
10+
</mat-select>
11+
</mat-form-field>
12+
13+
@if (filterMode === 'domain') {
14+
<mat-form-field class="value-field" appearance="outline">
15+
<mat-label>{{normalizedLabel}} (domain)</mat-label>
16+
<span matTextPrefix>@&nbsp;</span>
17+
<input matInput type="text" name="{{label}}-{{key}}"
18+
#inputElement
19+
[required]="required" [disabled]="disabled" [readonly]="readonly"
20+
placeholder="gmail.com"
21+
[(ngModel)]="domainValue" (ngModelChange)="onDomainChange($event)">
22+
</mat-form-field>
23+
} @else if (filterMode !== 'empty') {
24+
<mat-form-field class="value-field" appearance="outline">
25+
<mat-label>{{normalizedLabel}}</mat-label>
26+
<input matInput type="text" name="{{label}}-{{key}}"
27+
#inputElement
28+
[required]="required" [disabled]="disabled" [readonly]="readonly"
29+
[(ngModel)]="textValue" (ngModelChange)="onTextChange($event)">
30+
</mat-form-field>
31+
}
32+
</div>

0 commit comments

Comments
 (0)