From 0d9322f3c6cc7d316f60307910b0bd0b9f8343ae Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 1 Apr 2026 14:04:33 +0000 Subject: [PATCH 01/10] Add email and phone filter components with comparator support Introduce dedicated Email and Phone filter field components for table filtering. Add onComparatorChange output to base filter/edit fields so filter components can dynamically update the comparator. Register Email widget type in backend enum and frontend display/edit/filter type maps. Also move @types packages to devDependencies and add yarn resolutions for vulnerable transitive dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/src/enums/widget-type.enum.ts | 1 + frontend/package.json | 18 +++-- .../db-table-filters-dialog.component.html | 7 +- .../db-table-filters-dialog.component.ts | 10 ++- .../saved-filters-dialog.component.html | 11 ++- .../saved-filters-dialog.component.ts | 12 ++- .../base-filter-field.component.ts | 1 + .../filter-fields/email/email.component.css | 3 + .../filter-fields/email/email.component.html | 9 +++ .../filter-fields/email/email.component.ts | 49 +++++++++++ .../filter-fields/phone/phone.component.css | 23 ++++++ .../filter-fields/phone/phone.component.html | 16 ++++ .../filter-fields/phone/phone.component.ts | 81 +++++++++++++++++++ .../base-row-field.component.ts | 1 + frontend/src/app/consts/filter-types.ts | 4 + frontend/src/app/consts/record-edit-types.ts | 1 + .../src/app/consts/table-display-types.ts | 1 + 17 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 frontend/src/app/components/ui-components/filter-fields/email/email.component.css create mode 100644 frontend/src/app/components/ui-components/filter-fields/email/email.component.html create mode 100644 frontend/src/app/components/ui-components/filter-fields/email/email.component.ts create mode 100644 frontend/src/app/components/ui-components/filter-fields/phone/phone.component.css create mode 100644 frontend/src/app/components/ui-components/filter-fields/phone/phone.component.html create mode 100644 frontend/src/app/components/ui-components/filter-fields/phone/phone.component.ts diff --git a/backend/src/enums/widget-type.enum.ts b/backend/src/enums/widget-type.enum.ts index 121da11c6..db1e037d9 100644 --- a/backend/src/enums/widget-type.enum.ts +++ b/backend/src/enums/widget-type.enum.ts @@ -22,4 +22,5 @@ export enum WidgetTypeEnum { Range = 'Range', Timezone = 'Timezone', S3 = 'S3', + Email = 'Email', } diff --git a/frontend/package.json b/frontend/package.json index e53fc7940..d58d0472c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", @@ -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", @@ -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", @@ -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", @@ -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" } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html index 3f2b51b2f..81269b647 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -38,7 +38,8 @@

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] } }" > @@ -54,7 +55,8 @@

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] } }" > @@ -146,6 +148,7 @@

Reset -
+ {{value.key}}
{ let params; - if (widget.widget_params !== '// No settings required') { + if (typeof widget.widget_params === 'string' && widget.widget_params !== '// No settings required') { try { params = JSON.parse(widget.widget_params); } catch (_e) { params = ''; } + } else if (typeof widget.widget_params !== 'string') { + params = widget.widget_params; } else { params = ''; } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html index 92fd2885b..e21786fda 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html @@ -39,11 +39,11 @@ - - @@ -58,7 +58,7 @@
- + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} {{ {startswith: 'starts with', endswith: 'ends with', eq: 'equal', contains: 'contains', icontains: 'not contains', empty: 'is empty'}[savedFilterMap[selectedFilterSetId].dynamicColumn.operator] }} @@ -138,7 +138,8 @@ autofocus: shouldAutofocus }" [ndcDynamicOutputs]="{ - onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] } + onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] }, + onComparatorChange: { handler: updateDynamicColumnComparator, args: ['$event'] } }" >
@@ -154,7 +155,8 @@ autofocus: shouldAutofocus }" [ndcDynamicOutputs]="{ - onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] } + onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] }, + onComparatorChange: { handler: updateDynamicColumnComparator, args: ['$event'] } }" > diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts index 6b38723f9..9dd9d22d7 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -17,8 +17,8 @@ import { Angulartics2OnModule } from 'angulartics2'; import { DynamicModule } from 'ng-dynamic-component'; import posthog from 'posthog-js'; import { PlaceholderSavedFiltersComponent } from 'src/app/components/skeletons/placeholder-saved-filters/placeholder-saved-filters.component'; -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 { normalizeTableName } from 'src/app/lib/normalize'; import { TableField, TableForeignKey } from 'src/app/models/table'; import { AccessLevel } from 'src/app/models/user'; @@ -76,7 +76,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { public tableRowFieldsShown: { [key: string]: any } = {}; public tableRowStructure: { [key: string]: any } = {}; public tableWidgetsList: string[] = []; - public UIwidgets = UIwidgets; + public UIwidgets = { ...EditUIwidgets, ...FilterUIwidgets }; public displayedComparators = { eq: '=', @@ -244,6 +244,9 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { if (this.resetSelection) { this.selectedFilterSetId = null; } + if (this.tableWidgets) { + this.tableWidgetsList = Object.keys(this.tableWidgets); + } } ngOnDestroy() { diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.css b/frontend/src/app/components/ui-components/filter-fields/email/email.component.css index 1a1932e02..b5c331633 100644 --- a/frontend/src/app/components/ui-components/filter-fields/email/email.component.css +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.css @@ -1,3 +1,15 @@ -.email-form-field { +.email-filter-row { + display: flex; + gap: 8px; + align-items: flex-start; width: 100%; } + +.comparator-field { + flex: 0 0 auto; + min-width: 150px; +} + +.value-field { + flex: 1; +} diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.html b/frontend/src/app/components/ui-components/filter-fields/email/email.component.html index f1fc4920b..549c6008d 100644 --- a/frontend/src/app/components/ui-components/filter-fields/email/email.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.html @@ -1,9 +1,32 @@ - + diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/email/email.component.spec.ts new file mode 100644 index 000000000..f4a4802b5 --- /dev/null +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { EmailFilterComponent } from './email.component'; + +describe('EmailFilterComponent', () => { + let component: EmailFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmailFilterComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EmailFilterComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should default to eq filter mode', () => { + fixture.detectChanges(); + expect(component.filterMode).toBe('eq'); + }); + + it('should emit eq comparator by default after view init', () => { + const emitted: string[] = []; + component.onComparatorChange.subscribe((v: string) => emitted.push(v)); + + component.ngOnInit(); + const emittedDuringInit = [...emitted]; + + component.ngAfterViewInit(); + + expect(emittedDuringInit).toEqual([]); + expect(emitted).toEqual(['eq']); + }); + + it('should prefix domain value with @ on domain change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + vi.spyOn(component.onComparatorChange, 'emit'); + fixture.detectChanges(); + + component.onDomainChange('gmail.com'); + + expect(component.value).toBe('@gmail.com'); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('@gmail.com'); + expect(component.onComparatorChange.emit).toHaveBeenCalledWith('endswith'); + }); + + it('should switch to eq mode and emit plain text', () => { + vi.spyOn(component.onFieldChange, 'emit'); + vi.spyOn(component.onComparatorChange, 'emit'); + fixture.detectChanges(); + + component.textValue = 'user@test.com'; + component.onFilterModeChange('eq'); + + expect(component.onComparatorChange.emit).toHaveBeenCalledWith('eq'); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('user@test.com'); + }); + + it('should switch to empty mode', () => { + vi.spyOn(component.onFieldChange, 'emit'); + vi.spyOn(component.onComparatorChange, 'emit'); + fixture.detectChanges(); + + component.onFilterModeChange('empty'); + + expect(component.value).toBe(''); + expect(component.onComparatorChange.emit).toHaveBeenCalledWith('empty'); + }); + + it('should parse existing @domain value on init', () => { + component.value = '@example.com'; + fixture.detectChanges(); + + expect(component.filterMode).toBe('domain'); + expect(component.domainValue).toBe('example.com'); + }); +}); diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.ts b/frontend/src/app/components/ui-components/filter-fields/email/email.component.ts index 479829892..18a919f41 100644 --- a/frontend/src/app/components/ui-components/filter-fields/email/email.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.ts @@ -3,32 +3,37 @@ import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from ' import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; @Component({ selector: 'app-filter-email', templateUrl: './email.component.html', styleUrls: ['./email.component.css'], - imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule], + imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule], }) export class EmailFilterComponent extends BaseFilterFieldComponent implements OnInit, AfterViewInit { @Input() value: string; @ViewChild('inputElement') inputElement: ElementRef; + public filterMode: string = 'eq'; public domainValue: string = ''; + public textValue: string = ''; ngOnInit(): void { super.ngOnInit(); - this.onComparatorChange.emit('endswith'); if (this.value?.startsWith('@')) { + this.filterMode = 'domain'; this.domainValue = this.value.substring(1); } else if (this.value) { - this.domainValue = this.value; + this.textValue = this.value; } } ngAfterViewInit(): void { + this.emitCurrentState(); + if (this.autofocus && this.inputElement) { setTimeout(() => { this.inputElement.nativeElement.focus(); @@ -36,14 +41,42 @@ export class EmailFilterComponent extends BaseFilterFieldComponent implements On } } - onDomainChange(domain: string): void { - this.domainValue = domain; - if (domain) { - this.value = `@${domain}`; - } else { + onFilterModeChange(mode: string): void { + this.filterMode = mode; + + if (mode === 'empty') { this.value = ''; + this.onFieldChange.emit(this.value); + this.onComparatorChange.emit('empty'); + return; } + + if (mode === 'domain') { + this.value = this.domainValue ? `@${this.domainValue}` : ''; + this.onComparatorChange.emit('endswith'); + } else { + this.value = this.textValue; + this.onComparatorChange.emit(mode); + } + + this.onFieldChange.emit(this.value); + } + + onDomainChange(domain: string): void { + this.domainValue = domain; + this.value = domain ? `@${domain}` : ''; this.onFieldChange.emit(this.value); this.onComparatorChange.emit('endswith'); } + + onTextChange(text: string): void { + this.textValue = text; + this.value = text; + this.onFieldChange.emit(this.value); + this.onComparatorChange.emit(this.filterMode); + } + + private emitCurrentState(): void { + this.onComparatorChange.emit(this.filterMode === 'domain' ? 'endswith' : this.filterMode); + } } diff --git a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.css b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.css index 54eca3454..c411395e7 100644 --- a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.css +++ b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.css @@ -1,7 +1,19 @@ -.phone-form-field { +.phone-filter-row { + display: flex; + gap: 8px; + align-items: flex-start; width: 100%; } +.comparator-field { + flex: 0 0 auto; + min-width: 150px; +} + +.value-field { + flex: 1; +} + .country-flag { margin-right: 8px; font-size: 16px; diff --git a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.html index 9bcef1e9c..4d139ce72 100644 --- a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.html @@ -1,16 +1,37 @@ - - {{normalizedLabel}} (country) - - - - {{country.flag}} - {{country.name}} - ({{country.dialCode}}) - - - +
+ + + equal + country code + starts with + contains + is empty + + + + @if (filterMode === 'country') { + + {{normalizedLabel}} (country) + + + @for (country of filteredCountries | async; track country.code) { + + {{country.flag}} + {{country.name}} + ({{country.dialCode}}) + + } + + + } @else if (filterMode !== 'empty') { + + {{normalizedLabel}} + + + } +
diff --git a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.ts index 6a42ca460..bbd6373bd 100644 --- a/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/phone/phone.component.ts @@ -1,9 +1,10 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { AfterViewInit, Component, Input, OnInit } from '@angular/core'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { COUNTRIES, Country, getCountryFlag } from '../../../../consts/countries'; @@ -17,14 +18,24 @@ interface CountryWithFlag extends Country { selector: 'app-filter-phone', templateUrl: './phone.component.html', styleUrls: ['./phone.component.css'], - imports: [CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatAutocompleteModule, MatInputModule], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatAutocompleteModule, + MatInputModule, + MatSelectModule, + ], }) -export class PhoneFilterComponent extends BaseFilterFieldComponent implements OnInit { +export class PhoneFilterComponent extends BaseFilterFieldComponent implements OnInit, AfterViewInit { @Input() value: string; + public filterMode: string = 'eq'; public countries: CountryWithFlag[] = []; public countryControl = new FormControl(''); public filteredCountries: Observable; + public textValue: string = ''; getCountryFlag = getCountryFlag; @@ -33,9 +44,51 @@ export class PhoneFilterComponent extends BaseFilterFieldComponent implements On this.loadCountries(); this.setupAutocomplete(); this.setInitialValue(); + } + + ngAfterViewInit(): void { + this.emitCurrentState(); + } + + onFilterModeChange(mode: string): void { + this.filterMode = mode; + + if (mode === 'empty') { + this.value = ''; + this.onFieldChange.emit(this.value); + this.onComparatorChange.emit('empty'); + return; + } + + if (mode === 'country') { + const selected = this.countryControl.value; + this.value = typeof selected === 'object' && selected ? selected.dialCode : ''; + this.onComparatorChange.emit('startswith'); + } else { + this.value = this.textValue; + this.onComparatorChange.emit(mode); + } + + this.onFieldChange.emit(this.value); + } + + onCountrySelected(selectedCountry: CountryWithFlag): void { + this.value = selectedCountry.dialCode; + this.onFieldChange.emit(this.value); this.onComparatorChange.emit('startswith'); } + onTextChange(text: string): void { + this.textValue = text; + this.value = text; + this.onFieldChange.emit(this.value); + this.onComparatorChange.emit(this.filterMode); + } + + displayFn(country: CountryWithFlag): string { + return country ? `${country.flag} ${country.name} (${country.dialCode})` : ''; + } + private setupAutocomplete(): void { this.filteredCountries = this.countryControl.valueChanges.pipe( startWith(''), @@ -47,7 +100,10 @@ export class PhoneFilterComponent extends BaseFilterFieldComponent implements On if (this.value) { const country = this.countries.find((c) => c.dialCode === this.value); if (country) { + this.filterMode = 'country'; this.countryControl.setValue(country); + } else { + this.textValue = this.value; } } } @@ -62,20 +118,14 @@ export class PhoneFilterComponent extends BaseFilterFieldComponent implements On ); } - onCountrySelected(selectedCountry: CountryWithFlag): void { - this.value = selectedCountry.dialCode; - this.onFieldChange.emit(this.value); - this.onComparatorChange.emit('startswith'); - } - - displayFn(country: CountryWithFlag): string { - return country ? `${country.flag} ${country.name} (${country.dialCode})` : ''; - } - private loadCountries(): void { this.countries = COUNTRIES.filter((country) => country.dialCode).map((country) => ({ ...country, flag: getCountryFlag(country.code), })); } + + private emitCurrentState(): void { + this.onComparatorChange.emit(this.filterMode === 'country' ? 'startswith' : this.filterMode); + } } From 89b3d1140825d90f4c369c28d231dff147da512f Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Thu, 2 Apr 2026 19:00:11 +0000 Subject: [PATCH 03/10] Clean up filter components: remove dead code, debug logs, and redundant state Remove dead setWidgets() from saved-filters-dialog and panel, no-op cast.subscribe() calls, spurious @Injectable() decorators, debug console.log statements, commented-out code, and redundant textValue state in email/phone filters. Cache country and timezone lists at module level. Replace O(n) isWidget array scan with O(1) object lookup. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../db-table-filters-dialog.component.ts | 20 +--- .../saved-filters-dialog.component.ts | 70 +------------- .../saved-filters-panel.component.ts | 51 +--------- .../boolean/boolean.component.ts | 2 - .../country/country.component.ts | 58 ++++++----- .../filter-fields/email/email.component.css | 2 +- .../filter-fields/email/email.component.html | 4 +- .../email/email.component.spec.ts | 18 +++- .../filter-fields/email/email.component.ts | 25 ++--- .../filter-fields/phone/phone.component.css | 2 +- .../filter-fields/phone/phone.component.html | 4 +- .../filter-fields/phone/phone.component.ts | 59 ++++-------- .../filter-fields/text/text.component.ts | 4 +- .../timezone/timezone.component.ts | 95 ++++++++----------- 14 files changed, 128 insertions(+), 286 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts index a5de81919..515d2f8f3 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts @@ -23,7 +23,6 @@ import { getComparatorsFromUrl, getFiltersFromUrl } from 'src/app/lib/parse-filt 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({ @@ -65,14 +64,12 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { public differ: KeyValueDiffer; public tableTypes: Object; public tableWidgets: object; - public tableWidgetsList: string[] = []; 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, ) { @@ -80,7 +77,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { } ngOnInit(): void { - this._tables.cast.subscribe(); this.tableForeignKeys = { ...this.data.structure.foreignKeys }; this.tableRowFields = Object.assign( {}, @@ -90,7 +86,6 @@ 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) => { @@ -98,28 +93,21 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { }), ); - // 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; @@ -146,7 +134,6 @@ export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { 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]) { @@ -164,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); @@ -173,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}"]`, @@ -228,7 +213,6 @@ 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) => { @@ -248,11 +232,11 @@ 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) => { diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts index 822cf3e04..a44481123 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts @@ -57,11 +57,6 @@ import { TablesService } from 'src/app/services/tables.service'; styleUrl: './saved-filters-dialog.component.css', }) export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { - // @Input() connectionID: string; - // @Input() tableName: string; - // @Input() displayTableName: string; - // @Input() filtersSet: any; - public tableFilters = []; public fieldSearchControl = new FormControl(''); public fields: string[]; @@ -71,11 +66,9 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { public tableRowStructure: Object; public tableRowFieldsShown: Object = {}; public tableRowFieldsComparator: Object = {}; - // public tableForeignKeys: {[key: string]: TableForeignKey}; public tableFiltersCount: number = 0; public tableTypes: Object; public tableWidgets: object; - public tableWidgetsList: string[] = []; public UIwidgets = { ...EditUIwidgets, ...FilterUIwidgets }; public dynamicColumn: string | null = null; public showAddConditionField = false; @@ -95,8 +88,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { ) {} ngOnInit(): void { - this._tables.cast.subscribe(); - if (this.data.filtersSet) { this.tableRowFieldsShown = Object.entries(this.data.filtersSet.filters).reduce((acc, [field, conditions]) => { const [_comparator, value] = Object.entries(conditions)[0]; @@ -113,7 +104,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { {}, ); - // Initialize dynamic column if it exists in the filters set if (this.data.filtersSet.dynamic_column?.column_name) { this.tableRowFieldsShown[this.data.filtersSet.dynamic_column.column_name] = null; this.tableRowFieldsComparator[this.data.filtersSet.dynamic_column.column_name] = @@ -122,7 +112,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } } - // this.tableForeignKeys = {...this.data.structure.foreignKeys}; this.tableRowFields = Object.assign( {}, ...this.data.structure.map((field: TableField) => ({ [field.column_name]: undefined })), @@ -140,10 +129,8 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { }), ); - // Setup widgets - data comes pre-processed as object { field_name: { widget_type, widget_params, ... } } const tableWidgets = this.data.tableWidgets; if (tableWidgets && Object.keys(tableWidgets).length) { - this.tableWidgetsList = Object.keys(tableWidgets); this.tableWidgets = tableWidgets; } @@ -154,7 +141,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } ngAfterViewInit(): void { - // If editing an existing filter (has id), remove focus from the filter name input if (this.data.filtersSet && this.data.filtersSet.id) { setTimeout(() => { const nameInput = this.elementRef.nativeElement.querySelector( @@ -192,36 +178,12 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { return filterTypes[this._connections.currentConnection.type]; } - setWidgets(widgets: any[]) { - this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); - this.tableWidgets = Object.assign( - {}, - ...widgets.map((widget: any) => { - let params; - if (typeof widget.widget_params === 'string' && widget.widget_params !== '// No settings required') { - try { - params = JSON.parse(widget.widget_params); - } catch (_e) { - params = ''; - } - } else if (typeof widget.widget_params !== 'string') { - params = widget.widget_params; - } else { - params = ''; - } - return { - [widget.field_name]: { ...widget, widget_params: params }, - }; - }), - ); - } - trackByFn(_index: number, item: any) { return item.key; } isWidget(columnName: string) { - return this.tableWidgetsList.includes(columnName); + return this.tableWidgets && columnName in this.tableWidgets; } updateComparatorFromComponent = (comparator: string, field: string) => { @@ -231,7 +193,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { updateField = (updatedValue: any, field: string) => { this.tableRowFieldsShown[field] = updatedValue; this.updateFiltersCount(); - // Reset conditions error when a filter is added if (this.showConditionsError && Object.keys(this.tableRowFieldsShown).length > 0) { this.showConditionsError = false; } @@ -246,7 +207,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { }; this.fieldSearchControl.setValue(''); this.updateFiltersCount(); - // Reset conditions error when a filter is added this.showConditionsError = false; if (this.hasSelectedFilters) { this.showAddConditionField = false; @@ -254,7 +214,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } handleInputBlur(): void { - // Hide the field if it's empty when it loses focus if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { setTimeout(() => { if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { @@ -265,7 +224,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } updateComparator(event, fieldName: string) { - console.log('Updating comparator for field:', fieldName, 'obj', this.tableRowFieldsComparator); if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; } @@ -324,7 +282,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { this.dynamicColumn = null; } this.updateFiltersCount(); - // Reset conditions error when filters are removed (will be re-validated on save) this.showConditionsError = false; if (!this.hasSelectedFilters) { this.showAddConditionField = false; @@ -344,11 +301,9 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } handleSaveFilters() { - // Reset error flags this.showNameError = false; this.showConditionsError = false; - // Validate filter name if (!this.data.filtersSet.name || this.data.filtersSet.name.trim() === '') { this.showNameError = true; setTimeout(() => { @@ -363,19 +318,13 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { return; } - // Validate conditions - check if there are any filters - // A valid filter must have a comparator defined - // Either regular filters OR dynamic column with comparator should exist const hasRegularFilters = Object.keys(this.tableRowFieldsShown).some((key) => { - // Skip dynamic column for regular filter check if (key === this.dynamicColumn) { return false; } - // Check if comparator is defined (even if value is empty/null, comparator must exist) return this.tableRowFieldsComparator[key] !== undefined && this.tableRowFieldsComparator[key] !== null; }); - // Check if dynamic column has a comparator (it counts as a valid filter condition) const hasDynamicColumnFilter = this.dynamicColumn && this.tableRowFieldsComparator[this.dynamicColumn] !== undefined && @@ -391,7 +340,6 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { conditionInput.focus(); conditionInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else { - // If input is not visible, show the add condition button area const addButton = this.elementRef.nativeElement.querySelector('.add-condition-footer button') as HTMLElement; if (addButton) { addButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -406,13 +354,11 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { let filters = {}; for (const key in this.tableRowFieldsShown) { - // Skip fields that are marked as dynamic column if (key === this.dynamicColumn) { continue; } if (this.tableRowFieldsComparator[key] !== undefined) { - // If value is empty or undefined, use null const value = this.tableRowFieldsShown[key] === '' || this.tableRowFieldsShown[key] === undefined ? null @@ -424,15 +370,12 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { } } - // const filters = JsonURL.stringify( this.filters ); payload = { name: this.data.filtersSet.name, filters, }; - // Only add dynamic_column if one is selected if (this.dynamicColumn) { - // Create object with column_name and comparator properties payload.dynamic_column = { column_name: this.dynamicColumn, comparator: this.tableRowFieldsComparator[this.dynamicColumn] || '', @@ -471,16 +414,5 @@ export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { ); } } - - // saveFilter() { - - // this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload) - // .subscribe(() => { - // this.dialogRef.close(true); - // }, (error) => { - // console.error('Error saving filter:', error); - // this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); - // }); - // } } } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts index 9dd9d22d7..63e874e57 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; @@ -56,7 +56,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { @Input() structure: any; @Input() tableForeignKeys: TableForeignKey[] = []; @Input() tableWidgets: any = {}; - // @Input() savedFilterData: any; @Output() filterSelected = new EventEmitter(); @Input() resetSelection: boolean = false; @@ -75,7 +74,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { public tableStructure: any = null; public tableRowFieldsShown: { [key: string]: any } = {}; public tableRowStructure: { [key: string]: any } = {}; - public tableWidgetsList: string[] = []; public UIwidgets = { ...EditUIwidgets, ...FilterUIwidgets }; public displayedComparators = { @@ -240,13 +238,10 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { }); } - ngOnChanges() { - if (this.resetSelection) { + ngOnChanges(changes: SimpleChanges) { + if (changes.resetSelection && this.resetSelection) { this.selectedFilterSetId = null; } - if (this.tableWidgets) { - this.tableWidgetsList = Object.keys(this.tableWidgets); - } } ngOnDestroy() { @@ -273,9 +268,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { }, }, }); - - // No need to handle URL updates here - it's now handled in the tables.cast subscription - // when 'filters set updated' is received } setCurrentFilter(filter: any) { @@ -291,9 +283,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { handleDeleteFilter(filter: any) { if (filter) { this._tables.deleteSavedFilter(this.connectionID, this.selectedTableName, filter.id).subscribe({ - next: () => { - // The deletion will trigger 'delete saved filters' event which will refresh the list - }, + next: () => {}, error: (error) => { console.error('Error deleting filter:', error); }, @@ -370,7 +360,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { } selectFiltersSet(selectedFilterSetId: string): void { - console.log('selectFiltersSet ID:', selectedFilterSetId); if (this.selectedFilterSetId === selectedFilterSetId) { this.selectedFilterSetId = null; this.filterSelected.emit(null); @@ -398,7 +387,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { const queryParams = this.buildQueryParams(additionalParams); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - // Reset autofocus after the component has been rendered setTimeout(() => { this.shouldAutofocus = false; }, 500); @@ -449,7 +437,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { } isWidget(columnName: string) { - return this.tableWidgetsList.includes(columnName); + return this.tableWidgets && columnName in this.tableWidgets; } getInputType(field: string) { @@ -472,28 +460,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { } } - setWidgets(widgets: any[]) { - this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); - this.tableWidgets = Object.assign( - {}, - ...widgets.map((widget: any) => { - let params; - if (widget.widget_params !== '// No settings required') { - try { - params = JSON.parse(widget.widget_params); - } catch (_e) { - params = ''; - } - } else { - params = ''; - } - return { - [widget.field_name]: { ...widget, widget_params: params }, - }; - }), - ); - } - trackByFn(_index: number, item: any) { return item.key; } @@ -549,8 +515,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { return; } - console.log(value, 'value in updateDynamicColumnValue'); - selectedFilter.dynamicColumn.value = value; if (this.dynamicColumnValueDebounceTimer) { @@ -567,8 +531,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; - console.log('Applying dynamic column changes for filter selectedFilter:', selectedFilter); - if (!selectedFilter || !selectedFilter.dynamicColumn) return; const dynamicColumn = { @@ -593,9 +555,6 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { } } - console.log('applyDynamicColumnChanges, filters:', filters); - - // Build filter-related params using the helper method const additionalParams: any = { filters: JsonURL.stringify(filters), dynamic_column: JsonURL.stringify(dynamicColumn), diff --git a/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts index 60f620646..f2f25d7e9 100644 --- a/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts @@ -46,7 +46,6 @@ export class BooleanFilterComponent extends BaseFilterFieldComponent { this.booleanValue = this.value; } else if (this.value === null) { this.booleanValue = 'unknown'; - console.log('i entered condition this.value === null'); } else { switch (this.value) { case 0: @@ -68,7 +67,6 @@ export class BooleanFilterComponent extends BaseFilterFieldComponent { } onBooleanChange() { - console.log(this.connectionType); let formattedValue; switch (this.connectionType) { case DBtype.MySQL: diff --git a/frontend/src/app/components/ui-components/filter-fields/country/country.component.ts b/frontend/src/app/components/ui-components/filter-fields/country/country.component.ts index c859c75a8..b25ad8f73 100644 --- a/frontend/src/app/components/ui-components/filter-fields/country/country.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/country/country.component.ts @@ -9,6 +9,18 @@ import { map, startWith } from 'rxjs/operators'; import { COUNTRIES, getCountryFlag } from '../../../../consts/countries'; import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; +interface CountryOption { + value: string | null; + label: string; + flag: string; +} + +const BASE_COUNTRIES: CountryOption[] = COUNTRIES.map((country) => ({ + value: country.code, + label: country.name, + flag: getCountryFlag(country.code), +})); + @Component({ selector: 'app-filter-country', templateUrl: './country.component.html', @@ -19,9 +31,9 @@ import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field export class CountryFilterComponent extends BaseFilterFieldComponent { @Input() value: string; - public countries: { value: string | null; label: string; flag: string }[] = []; - public countryControl = new FormControl<{ value: string | null; label: string; flag: string } | string>(''); - public filteredCountries: Observable<{ value: string | null; label: string; flag: string }[]>; + public countries: CountryOption[] = []; + public countryControl = new FormControl(''); + public filteredCountries: Observable; originalOrder = () => { return 0; @@ -31,11 +43,26 @@ export class CountryFilterComponent extends BaseFilterFieldComponent { ngOnInit(): void { super.ngOnInit(); - this.loadCountries(); + + if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) { + this.countries = [{ value: null, label: '', flag: '' }, ...BASE_COUNTRIES]; + } else { + this.countries = BASE_COUNTRIES; + } + this.setupAutocomplete(); this.setInitialValue(); } + onCountrySelected(selectedCountry: CountryOption): void { + this.value = selectedCountry.value; + this.onFieldChange.emit(this.value); + } + + displayFn(country: any): string { + return country ? country.label : ''; + } + private setupAutocomplete(): void { this.filteredCountries = this.countryControl.valueChanges.pipe( startWith(''), @@ -52,32 +79,11 @@ export class CountryFilterComponent extends BaseFilterFieldComponent { } } - private _filter(value: string): { value: string | null; label: string; flag: string }[] { + private _filter(value: string): CountryOption[] { const filterValue = value.toLowerCase(); return this.countries.filter( (country) => country.label?.toLowerCase().includes(filterValue) || country.value?.toLowerCase().includes(filterValue), ); } - - onCountrySelected(selectedCountry: { value: string | null; label: string; flag: string }): void { - this.value = selectedCountry.value; - this.onFieldChange.emit(this.value); - } - - displayFn(country: any): string { - return country ? country.label : ''; - } - - private loadCountries(): void { - this.countries = COUNTRIES.map((country) => ({ - value: country.code, - label: country.name, - flag: getCountryFlag(country.code), - })); - - if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) { - this.countries = [{ value: null, label: '', flag: '' }, ...this.countries]; - } - } } diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.css b/frontend/src/app/components/ui-components/filter-fields/email/email.component.css index b5c331633..3b1f34518 100644 --- a/frontend/src/app/components/ui-components/filter-fields/email/email.component.css +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.css @@ -1,4 +1,4 @@ -.email-filter-row { +.filter-row { display: flex; gap: 8px; align-items: flex-start; diff --git a/frontend/src/app/components/ui-components/filter-fields/email/email.component.html b/frontend/src/app/components/ui-components/filter-fields/email/email.component.html index 549c6008d..85626d811 100644 --- a/frontend/src/app/components/ui-components/filter-fields/email/email.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/email/email.component.html @@ -1,4 +1,4 @@ -