Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/enums/widget-type.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export enum WidgetTypeEnum {
Range = 'Range',
Timezone = 'Timezone',
S3 = 'S3',
Email = 'Email',
}
18 changes: 13 additions & 5 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
"@sentry-internal/rrweb": "^2.31.0",
"@sentry/angular": "^10.33.0",
"@stripe/stripe-js": "^5.3.0",
"@types/google-one-tap": "^1.2.6",
"@types/lodash": "^4.17.13",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
"amplitude-js": "^8.21.9",
Expand All @@ -52,7 +50,6 @@
"date-fns": "^4.1.0",
"ipaddr.js": "^2.2.0",
"json5": "^2.2.3",
"knip": "^5.79.0",
"libphonenumber-js": "^1.12.9",
"lodash": "^4.17.21",
"lodash-es": "^4.17.23",
Expand All @@ -66,7 +63,6 @@
"pluralize": "^8.0.0",
"postgres-interval": "^4.0.2",
"posthog-js": "^1.341.0",
"puppeteer": "^24.29.1",
"rxjs": "^7.4.0",
"tslib": "^2.8.1",
"uuid": "^11.1.0",
Expand All @@ -81,10 +77,14 @@
"@angular/language-service": "~20.3.18",
"@sentry-internal/rrweb": "^2.16.0",
"@storybook/angular": "^10.2.14",
"@types/google-one-tap": "^1.2.6",
"@types/lodash": "^4.17.13",
"@types/node": "^22.10.2",
"@vitest/browser": "^3.1.1",
"jsdom": "^27.4.0",
"knip": "^5.79.0",
"playwright": "^1.57.0",
"puppeteer": "^24.29.1",
"storybook": "^10.2.14",
"ts-node": "~10.9.2",
"typescript": "~5.9.3",
Expand All @@ -93,7 +93,15 @@
"resolutions": {
"mermaid": "^11.10.0",
"webpack": "5.104.1",
"lodash-es": "4.17.23"
"lodash-es": "4.17.23",
"path-to-regexp": "8.4.0",
"serialize-javascript": "7.0.5",
"brace-expansion": "1.1.13",
"node-forge": "1.4.0",
"dompurify": "3.3.2",
"picomatch": "4.0.4",
"tar": "7.5.11",
"rollup": "4.59.0"
},
"packageManager": "yarn@1.22.22"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,27 @@

.filters-content {
display: grid;
grid-template-columns: auto 228px 0 1fr 32px;
grid-template-columns: auto 1fr 32px;
grid-column-gap: 8px;
align-content: flex-start;
align-items: flex-start;
}

.filters-select {
grid-column: 1 / span 5;
grid-column: 1 / -1;
}

.filter-line {
grid-column: 1 / span 4;
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
grid-column-gap: 8px;
align-items: flex-start;
}

.filter-line__field {
grid-column: 2;
min-width: 0;
}

::ng-deep .mat-dialog-container > .ng-star-inserted {
Expand Down Expand Up @@ -46,6 +55,7 @@
}

.filter-delete-button {
grid-column: 3;
margin-top: 4px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,122 +17,33 @@ <h1 mat-dialog-title class="filters-header">
[formControl]="fieldSearchControl">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="addFilter($event)">
<mat-option *ngFor="let field of foundFields | async"
[ngClass]="{'disabled': field === 'No matches'}"
[value]="field">
{{field}}
[value]="field.key"
[disabled]="isFieldAlreadyFiltered(field.key)">
{{ field.label }}
</mat-option>
</mat-autocomplete>
</mat-form-field>

<ng-container *ngFor="let value of tableRowFieldsShown | keyvalue; trackBy:trackByFn">
<div *ngIf="getComparatorType(getInputType(value.key)) === 'nonComparable'; else comparableFilter" class="filter-line">

<div *ngIf="isWidget(value.key); else defaultTableField">
<ndc-dynamic [ndcDynamicComponent]="tableWidgets[value.key].widget_type ? UIwidgets[tableWidgets[value.key].widget_type] : inputs[tableTypes[value.key]]"
[ndcDynamicInputs]="{
key: value.key,
label: tableWidgets[value.key].name || value.key,
value: tableRowFieldsShown[value.key],
widgetStructure: tableWidgets[value.key],
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
autofocus: autofocusField === value.key
}"
@for (value of tableRowFieldsShown | keyvalue; track value.key) {
<div class="filter-line">
<span class='mat-body-1 column-name'>{{ getFieldLabel(value.key) }}</span>
<div class="filter-line__field">
<ndc-dynamic
[ndcDynamicComponent]="getFilterComponent(value.key)"
[ndcDynamicInputs]="getFilterInputs(value.key)"
[ndcDynamicOutputs]="{
onFieldChange: { handler: updateField, args: ['$event', value.key] }
onFieldChange: { handler: updateField, args: ['$event', value.key] },
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
}"
></ndc-dynamic>
</div>

<ng-template #defaultTableField>
<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
[ndcDynamicInputs]="{
key: value.key,
label: value.key,
value: tableRowFieldsShown[value.key],
structure: tableRowStructure[value.key],
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
autofocus: autofocusField === value.key
}"
[ndcDynamicOutputs]="{
onFieldChange: { handler: updateField, args: ['$event', value.key] }
}"
></ndc-dynamic>
</ng-template>
<button mat-icon-button type="button" class="filter-delete-button"
matTooltip="Remove"
(click)="removeFilter(value.key)">
<mat-icon>close</mat-icon>
</button>
</div>

<ng-template #comparableFilter>
<span class='mat-body-1 column-name'>{{value.key}}</span>

<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'text'"
appearance="outline">
<mat-select name="textComparator-{{value.key}}"
[(ngModel)]="tableRowFieldsComparator[value.key]"
(ngModelChange)="updateComparator($event, value.key)">
<mat-option value="startswith">
starts with
</mat-option>
<mat-option value="endswith">
ends with
</mat-option>
<mat-option value="eq">
equal
</mat-option>
<mat-option value="contains">
contains
</mat-option>
<mat-option value="icontains">
not contains
</mat-option>
<mat-option value="empty">
is empty
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field *ngIf="getComparatorType(getInputType(value.key)) === 'number'"
appearance="outline">
<mat-select name="numberComparator-{{value.key}}"
[(ngModel)]="tableRowFieldsComparator[value.key]"
(ngModelChange)="updateComparator($event, value.key)">
<mat-option value="eq">
equal
</mat-option>
<mat-option value="gt">
greater than
</mat-option>
<mat-option value="lt">
less than
</mat-option>
<mat-option value="gte">
greater than or equal
</mat-option>
<mat-option value="lte">
less than or equal
</mat-option>
</mat-select>
</mat-form-field>

<ndc-dynamic [ndcDynamicComponent]="inputs[tableTypes[value.key]]"
[ndcDynamicInputs]="{
key: value.key,
label: value.key,
value: tableRowFieldsShown[value.key],
readonly: tableRowFieldsComparator[value.key] === 'empty',
structure: tableRowStructure[value.key],
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
autofocus: autofocusField === value.key
}"
[ndcDynamicOutputs]="{
onFieldChange: { handler: updateField, args: ['$event', value.key] }
}"
></ndc-dynamic>
</ng-template>
<button mat-icon-button type="button" class="filter-delete-button"
matTooltip="Remove"
(click)="removeFilter(value.key)">
<mat-icon>close</mat-icon>
</button>
</ng-container>
}
</mat-dialog-content>
</ng-template>

Expand All @@ -146,6 +57,7 @@ <h1 mat-dialog-title class="filters-header">
Reset
</button>
<button mat-flat-button mat-dialog-close
type="button"
angulartics2On="click"
angularticsAction="Filters: cancel is clicked"
(click)="posthog.capture('Filters: cancel is clicked')">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ describe('DbTableFiltersDialogComponent', () => {
await fixture.whenStable();

expect(component.tableForeignKeys).toEqual(mockStructureForFilterDialog.foreignKeys);
expect(component.fields).toEqual(['FirstName', 'Id', 'bool']);
expect(component.fields).toEqual([
{ key: 'FirstName', label: 'FirstName' },
{ key: 'Id', label: 'Id' },
{ key: 'bool', label: 'bool' },
]);
expect(component.tableRowStructure).toEqual({
FirstName: fakeFirstName,
Id: fakeId,
Expand Down Expand Up @@ -179,4 +183,67 @@ describe('DbTableFiltersDialogComponent', () => {
const comparatorType = component.getComparatorType(undefined);
expect(comparatorType).toEqual('nonComparable');
});

it('should receive comparator from EmailFilterComponent after it renders', async () => {
component.setWidgets([
{
field_name: 'FirstName',
widget_type: 'Email',
widget_params: '// No settings required',
name: '',
description: '',
},
]);

component.addFilter({ option: { value: 'FirstName' } });

// Default comparator is 'eq' from addFilter
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');

// Trigger change detection so ndc-dynamic renders EmailFilterComponent
fixture.detectChanges();
await fixture.whenStable();

// EmailFilterComponent emits 'eq' (default mode) in ngAfterViewInit,
// which updateComparatorFromComponent receives
Comment on lines +207 to +208
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says EmailFilterComponent emits 'eq' in ngAfterViewInit, but the implementation only emits when filterMode !== 'eq'. Updating/removing the comment would prevent misleading future readers of the test.

Suggested change
// EmailFilterComponent emits 'eq' (default mode) in ngAfterViewInit,
// which updateComparatorFromComponent receives
// Rendering the EmailFilterComponent should not change the comparator;
// it remains the 'eq' value that addFilter already set.

Copilot uses AI. Check for mistakes.
expect(component.tableRowFieldsComparator['FirstName']).toEqual('eq');
});

it('should recognize widgets when passed as object (real app format)', () => {
const widgetsObject = {
FirstName: {
field_name: 'FirstName',
widget_type: 'Email',
widget_params: {},
name: '',
description: '',
},
};

component.data.structure.widgets = widgetsObject;
component.ngOnInit();

expect(component.isWidget('FirstName')).toBe(true);
});

it('should report fields with active filters as already filtered', () => {
component.tableRowFieldsShown = { FirstName: 'John' };
expect(component.isFieldAlreadyFiltered('FirstName')).toBe(true);
expect(component.isFieldAlreadyFiltered('Id')).toBe(false);
});

it('should use widget name as field label when widget has a name', () => {
component.data.structure.widgets = {
FirstName: {
field_name: 'FirstName',
widget_type: 'Email',
widget_params: {},
name: 'Customer Email',
description: '',
},
};
component.ngOnInit();

expect(component.fields).toEqual(expect.arrayContaining([{ key: 'FirstName', label: 'Customer Email' }]));
});
});
Loading
Loading