Skip to content

Commit e0a70a0

Browse files
Merge pull request #1698 from rocket-admin/email-phone-filter-improvements
Email, phone, date filter improvements, migrate filters to signals
2 parents 6d0dcee + 7b34ebe commit e0a70a0

68 files changed

Lines changed: 1944 additions & 1034 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/enums/widget-type.enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export enum WidgetTypeEnum {
2222
Range = 'Range',
2323
Timezone = 'Timezone',
2424
S3 = 'S3',
25+
Email = 'Email',
2526
}

frontend/package.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
"@sentry-internal/rrweb": "^2.31.0",
3838
"@sentry/angular": "^10.33.0",
3939
"@stripe/stripe-js": "^5.3.0",
40-
"@types/google-one-tap": "^1.2.6",
41-
"@types/lodash": "^4.17.13",
4240
"@zxcvbn-ts/core": "^3.0.4",
4341
"@zxcvbn-ts/language-en": "^3.0.2",
4442
"amplitude-js": "^8.21.9",
@@ -52,7 +50,6 @@
5250
"date-fns": "^4.1.0",
5351
"ipaddr.js": "^2.2.0",
5452
"json5": "^2.2.3",
55-
"knip": "^5.79.0",
5653
"libphonenumber-js": "^1.12.9",
5754
"lodash": "^4.17.21",
5855
"lodash-es": "^4.17.23",
@@ -66,7 +63,6 @@
6663
"pluralize": "^8.0.0",
6764
"postgres-interval": "^4.0.2",
6865
"posthog-js": "^1.341.0",
69-
"puppeteer": "^24.29.1",
7066
"rxjs": "^7.4.0",
7167
"tslib": "^2.8.1",
7268
"uuid": "^11.1.0",
@@ -81,10 +77,14 @@
8177
"@angular/language-service": "~20.3.18",
8278
"@sentry-internal/rrweb": "^2.16.0",
8379
"@storybook/angular": "^10.2.14",
80+
"@types/google-one-tap": "^1.2.6",
81+
"@types/lodash": "^4.17.13",
8482
"@types/node": "^22.10.2",
8583
"@vitest/browser": "^3.1.1",
8684
"jsdom": "^27.4.0",
85+
"knip": "^5.79.0",
8786
"playwright": "^1.57.0",
87+
"puppeteer": "^24.29.1",
8888
"storybook": "^10.2.14",
8989
"ts-node": "~10.9.2",
9090
"typescript": "~5.9.3",
@@ -93,7 +93,15 @@
9393
"resolutions": {
9494
"mermaid": "^11.10.0",
9595
"webpack": "5.104.1",
96-
"lodash-es": "4.17.23"
96+
"lodash-es": "4.17.23",
97+
"path-to-regexp": "8.4.0",
98+
"serialize-javascript": "7.0.5",
99+
"brace-expansion": "1.1.13",
100+
"node-forge": "1.4.0",
101+
"dompurify": "3.3.2",
102+
"picomatch": "4.0.4",
103+
"tar": "7.5.11",
104+
"rollup": "4.59.0"
97105
},
98106
"packageManager": "yarn@1.22.22"
99107
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,27 @@
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;
21+
grid-column: 1 / -1;
22+
display: grid;
23+
grid-template-columns: subgrid;
24+
grid-column-gap: 8px;
25+
align-items: flex-start;
26+
}
27+
28+
.filter-line__field {
29+
grid-column: 2;
30+
min-width: 0;
2231
}
2332

2433
::ng-deep .mat-dialog-container > .ng-star-inserted {
@@ -46,6 +55,7 @@
4655
}
4756

4857
.filter-delete-button {
58+
grid-column: 3;
4959
margin-top: 4px;
5060
}
5161

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

Lines changed: 19 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -17,122 +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-
30-
<div *ngIf="isWidget(value.key); else defaultTableField">
31-
<ndc-dynamic [ndcDynamicComponent]="tableWidgets[value.key].widget_type ? UIwidgets[tableWidgets[value.key].widget_type] : inputs[tableTypes[value.key]]"
32-
[ndcDynamicInputs]="{
33-
key: value.key,
34-
label: tableWidgets[value.key].name || value.key,
35-
value: tableRowFieldsShown[value.key],
36-
widgetStructure: tableWidgets[value.key],
37-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
38-
autofocus: autofocusField === value.key
39-
}"
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)"
4034
[ndcDynamicOutputs]="{
41-
onFieldChange: { handler: updateField, args: ['$event', value.key] }
35+
onFieldChange: { handler: updateField, args: ['$event', value.key] },
36+
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
4237
}"
4338
></ndc-dynamic>
4439
</div>
45-
46-
<ng-template #defaultTableField>
47-
<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
48-
[ndcDynamicInputs]="{
49-
key: value.key,
50-
label: value.key,
51-
value: tableRowFieldsShown[value.key],
52-
structure: tableRowStructure[value.key],
53-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
54-
autofocus: autofocusField === value.key
55-
}"
56-
[ndcDynamicOutputs]="{
57-
onFieldChange: { handler: updateField, args: ['$event', value.key] }
58-
}"
59-
></ndc-dynamic>
60-
</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>
6145
</div>
62-
63-
<ng-template #comparableFilter>
64-
<span class='mat-body-1 column-name'>{{value.key}}</span>
65-
66-
<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'text'"
67-
appearance="outline">
68-
<mat-select name="textComparator-{{value.key}}"
69-
[(ngModel)]="tableRowFieldsComparator[value.key]"
70-
(ngModelChange)="updateComparator($event, value.key)">
71-
<mat-option value="startswith">
72-
starts with
73-
</mat-option>
74-
<mat-option value="endswith">
75-
ends with
76-
</mat-option>
77-
<mat-option value="eq">
78-
equal
79-
</mat-option>
80-
<mat-option value="contains">
81-
contains
82-
</mat-option>
83-
<mat-option value="icontains">
84-
not contains
85-
</mat-option>
86-
<mat-option value="empty">
87-
is empty
88-
</mat-option>
89-
</mat-select>
90-
</mat-form-field>
91-
92-
<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'number'"
93-
appearance="outline">
94-
<mat-select name="numberComparator-{{value.key}}"
95-
[(ngModel)]="tableRowFieldsComparator[value.key]"
96-
(ngModelChange)="updateComparator($event, value.key)">
97-
<mat-option value="eq">
98-
equal
99-
</mat-option>
100-
<mat-option value="gt">
101-
greater than
102-
</mat-option>
103-
<mat-option value="lt">
104-
less than
105-
</mat-option>
106-
<mat-option value="gte">
107-
greater than or equal
108-
</mat-option>
109-
<mat-option value="lte">
110-
less than or equal
111-
</mat-option>
112-
</mat-select>
113-
</mat-form-field>
114-
115-
<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
116-
[ndcDynamicInputs]="{
117-
key: value.key,
118-
label: value.key,
119-
value: tableRowFieldsShown[value.key],
120-
readonly: tableRowFieldsComparator[value.key] === 'empty',
121-
structure: tableRowStructure[value.key],
122-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
123-
autofocus: autofocusField === value.key
124-
}"
125-
[ndcDynamicOutputs]="{
126-
onFieldChange: { handler: updateField, args: ['$event', value.key] }
127-
}"
128-
></ndc-dynamic>
129-
</ng-template>
130-
<button mat-icon-button type="button" class="filter-delete-button"
131-
matTooltip="Remove"
132-
(click)="removeFilter(value.key)">
133-
<mat-icon>close</mat-icon>
134-
</button>
135-
</ng-container>
46+
}
13647
</mat-dialog-content>
13748
</ng-template>
13849

@@ -146,6 +57,7 @@ <h1 mat-dialog-title class="filters-header">
14657
Reset
14758
</button>
14859
<button mat-flat-button mat-dialog-close
60+
type="button"
14961
angulartics2On="click"
15062
angularticsAction="Filters: cancel is clicked"
15163
(click)="posthog.capture('Filters: cancel is clicked')">

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

Lines changed: 68 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,
@@ -179,4 +183,67 @@ describe('DbTableFiltersDialogComponent', () => {
179183
const comparatorType = component.getComparatorType(undefined);
180184
expect(comparatorType).toEqual('nonComparable');
181185
});
186+
187+
it('should receive comparator from EmailFilterComponent after it renders', async () => {
188+
component.setWidgets([
189+
{
190+
field_name: 'FirstName',
191+
widget_type: 'Email',
192+
widget_params: '// No settings required',
193+
name: '',
194+
description: '',
195+
},
196+
]);
197+
198+
component.addFilter({ option: { value: 'FirstName' } });
199+
200+
// Default comparator is 'eq' from addFilter
201+
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');
202+
203+
// Trigger change detection so ndc-dynamic renders EmailFilterComponent
204+
fixture.detectChanges();
205+
await fixture.whenStable();
206+
207+
// EmailFilterComponent emits 'eq' (default mode) in ngAfterViewInit,
208+
// which updateComparatorFromComponent receives
209+
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');
210+
});
211+
212+
it('should recognize widgets when passed as object (real app format)', () => {
213+
const widgetsObject = {
214+
FirstName: {
215+
field_name: 'FirstName',
216+
widget_type: 'Email',
217+
widget_params: {},
218+
name: '',
219+
description: '',
220+
},
221+
};
222+
223+
component.data.structure.widgets = widgetsObject;
224+
component.ngOnInit();
225+
226+
expect(component.isWidget('FirstName')).toBe(true);
227+
});
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+
});
182249
});

0 commit comments

Comments
 (0)