Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -36,8 +36,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 @@ -51,7 +49,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 @@ -65,7 +62,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 @@ -80,10 +76,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 @@ -92,7 +92,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 @@ -19,6 +19,21 @@

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

.filter-line > div,
.filter-line > ng-template,
.filter-line > ndc-dynamic {
flex: 1;
min-width: 0;
}

.filter-line > .column-name {
flex: 0 0 auto;
white-space: nowrap;
}

::ng-deep .mat-dialog-container > .ng-star-inserted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h1 mat-dialog-title class="filters-header">

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

<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]]"
Expand All @@ -38,7 +39,8 @@ <h1 mat-dialog-title class="filters-header">
autofocus: autofocusField === 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>
Expand All @@ -54,7 +56,8 @@ <h1 mat-dialog-title class="filters-header">
autofocus: autofocusField === 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>
</ng-template>
Expand Down Expand Up @@ -146,6 +149,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 @@ -179,4 +179,46 @@ 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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-i
import posthog from 'posthog-js';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { filterTypes } from 'src/app/consts/filter-types';
import { UIwidgets } from 'src/app/consts/record-edit-types';
import { UIwidgets as FilterUIwidgets, filterTypes } from 'src/app/consts/filter-types';
import { UIwidgets as EditUIwidgets } from 'src/app/consts/record-edit-types';
import { getComparatorsFromUrl, getFiltersFromUrl } from 'src/app/lib/parse-filter-params';
import { getTableTypes } from 'src/app/lib/setup-table-row-structure';
import { TableField, TableForeignKey, Widget } from 'src/app/models/table';
import { ConnectionsService } from 'src/app/services/connections.service';
import { TablesService } from 'src/app/services/tables.service';
import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component';

@Component({
Expand Down Expand Up @@ -65,22 +64,19 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
public differ: KeyValueDiffer<string, any>;
public tableTypes: Object;
public tableWidgets: object;
public tableWidgetsList: string[] = [];
public UIwidgets = UIwidgets;
public UIwidgets = { ...EditUIwidgets, ...FilterUIwidgets };
public autofocusField: string | null = null;

constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
private _connections: ConnectionsService,
private _tables: TablesService,
public route: ActivatedRoute,
private differs: KeyValueDiffers,
) {
this.differ = this.differs.find({}).create();
}

ngOnInit(): void {
this._tables.cast.subscribe();
this.tableForeignKeys = { ...this.data.structure.foreignKeys };
this.tableRowFields = Object.assign(
{},
Expand All @@ -90,36 +86,28 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
this.fields = this.data.structure.structure
.filter((field: TableField) => this.getInputType(field.column_name) !== 'file')
.map((field: TableField) => field.column_name);
// this.foundFields = [...this.fields];
this.tableRowStructure = Object.assign(
{},
...this.data.structure.structure.map((field: TableField) => {
return { [field.column_name]: field };
}),
);

// Set autofocus field if provided
if (this.data.autofocusField) {
this.autofocusField = this.data.autofocusField;
}

const queryParams = this.route.snapshot.queryParams;

// If saved_filter is present in queryParams, show empty form without applying filters
if (queryParams.saved_filter) {
// Show empty form without filters
this.tableFilters = [];
this.tableRowFieldsShown = {};
this.tableRowFieldsComparator = {};
} else {
// Original behavior - parse and apply filters from URL
let filters = {};
if (queryParams.filters) filters = JsonURL.parse(queryParams.filters);
// const filters = JsonURL.parse(queryParams.filters || '{}');
const filtersValues = getFiltersFromUrl(filters);

console.log('Parsed filters from URL:', filtersValues);

if (Object.keys(filtersValues).length) {
this.tableFilters = Object.keys(filtersValues).map((key) => key);
this.tableRowFieldsShown = filtersValues;
Expand All @@ -140,11 +128,12 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
}
}

if (this.data.structure.widgets && this.data.structure.widgets.length) {
this.setWidgets(this.data.structure.widgets);
const widgets = this.data.structure.widgets;
const widgetsArray = Array.isArray(widgets) ? widgets : Object.values(widgets || {});
if (widgetsArray.length) {
this.setWidgets(widgetsArray);
}

// If autofocusField is provided, ensure it's in the filters list
if (this.autofocusField && this.tableFilters && !this.tableFilters.includes(this.autofocusField)) {
this.tableFilters.push(this.autofocusField);
if (!this.tableRowFieldsShown[this.autofocusField]) {
Expand All @@ -162,7 +151,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
}

ngAfterViewInit(): void {
// Set focus on the autofocus field after view is initialized
if (this.autofocusField) {
setTimeout(() => {
this.focusOnField(this.autofocusField);
Expand All @@ -171,7 +159,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
}

focusOnField(fieldName: string) {
// Try multiple selectors to find the input field
const selectors = [
`input[name*="${fieldName}"]`,
`textarea[name*="${fieldName}"]`,
Expand Down Expand Up @@ -226,13 +213,14 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
}

setWidgets(widgets: Widget[]) {
this.tableWidgetsList = widgets.map((widget: Widget) => widget.field_name);
this.tableWidgets = Object.assign(
{},
...widgets.map((widget: Widget) => {
let params;
if (widget.widget_params !== '// No settings required') {
if (typeof widget.widget_params === 'string' && widget.widget_params !== '// No settings required') {
params = JSON5.parse(widget.widget_params);
} else if (typeof widget.widget_params !== 'string') {
params = widget.widget_params;
} else {
params = '';
}
Expand All @@ -244,17 +232,21 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit {
}

trackByFn(_index: number, item: any) {
return item.key; // or item.id
return item.key;
}

isWidget(columnName: string) {
return this.tableWidgetsList.includes(columnName);
return this.tableWidgets && columnName in this.tableWidgets;
}

updateField = (updatedValue: any, field: string) => {
this.tableRowFieldsShown[field] = updatedValue;
};

updateComparatorFromComponent = (comparator: string, field: string) => {
this.tableRowFieldsComparator[field] = comparator;
};

addFilter(e) {
const key = e.option.value;
this.tableRowFieldsShown = { ...this.tableRowFieldsShown, [key]: this.tableRowFields[key] };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export class DbTableWidgetsComponent implements OnInit {
"name": ""
}
`,
Email: `// No settings required`,
S3: `// Configure AWS S3 widget for file storage
// bucket: S3 bucket name (required)
// prefix: Optional path prefix for uploaded files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@

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

.filter-line > div,
.filter-line > ng-template,
.filter-line > ndc-dynamic {
flex: 1;
min-width: 0;
}

.filter-line > .column-name {
flex: 0 0 auto;
white-space: nowrap;
margin-top: 18px;
}

.empty-conditions-container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ <h1 mat-dialog-title class="filters-header">
<mat-icon>close</mat-icon>
</button>
<div *ngIf="getComparatorType(getInputType(value.key)) === 'nonComparable'; else comparableFilter" class="filter-line">
<span class='mat-body-1 column-name'>{{value.key}}</span>

<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]]"
Expand All @@ -101,7 +102,8 @@ <h1 mat-dialog-title class="filters-header">
relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined
}"
[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>
Expand All @@ -116,7 +118,8 @@ <h1 mat-dialog-title class="filters-header">
relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined
}"
[ndcDynamicOutputs]="{
onFieldChange: { handler: updateField, args: ['$event', value.key] }
onFieldChange: { handler: updateField, args: ['$event', value.key] },
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
}"
></ndc-dynamic>
</ng-template>
Expand Down Expand Up @@ -197,7 +200,8 @@ <h1 mat-dialog-title class="filters-header">
relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined
}"
[ndcDynamicOutputs]="{
onFieldChange: { handler: updateField, args: ['$event', value.key] }
onFieldChange: { handler: updateField, args: ['$event', value.key] },
onComparatorChange: { handler: updateComparatorFromComponent, args: ['$event', value.key] }
}"
></ndc-dynamic>
</ng-template>
Expand Down Expand Up @@ -259,7 +263,7 @@ <h1 mat-dialog-title class="filters-header">
(click)="removeFilters()">
Remove
</button>
<button mat-flat-button mat-dialog-close>
<button mat-flat-button mat-dialog-close type="button">
Cancel
</button>
<button mat-flat-button color="primary" type="submit">
Expand Down
Loading
Loading