From 6b33b01555cda7bddb2c8c62b47be435a0135d8d Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Thu, 9 Oct 2025 11:20:21 +0000 Subject: [PATCH 1/3] autofocus in fast filters --- frontend/package.json | 1 + .../saved-filters-panel.component.html | 9 ++- .../saved-filters-panel.component.ts | 8 +++ .../base-filter-field.component.ts | 1 + .../date-time/date-time.component.html | 1 + .../date-time/date-time.component.ts | 72 ++++++++++++++++--- .../filter-fields/date/date.component.html | 1 + .../filter-fields/date/date.component.ts | 13 +++- .../long-text/long-text.component.html | 1 + .../long-text/long-text.component.ts | 13 +++- .../number/number.component.html | 3 +- .../filter-fields/number/number.component.ts | 14 +++- .../password/password.component.html | 1 + .../password/password.component.ts | 13 +++- .../filter-fields/text/text.component.html | 1 + .../filter-fields/text/text.component.ts | 14 +++- .../date-time/date-time.component.css | 6 +- .../date-time/date-time.component.html | 5 +- .../date-time/date-time.component.ts | 47 ++++++++++-- .../date-time/date-time.component.css | 6 +- .../date-time/date-time.component.html | 5 +- .../date-time/date-time.component.ts | 53 ++++++++++---- frontend/yarn.lock | 10 +++ 23 files changed, 250 insertions(+), 48 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 663693054..548ced25f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "color-string": "^2.0.1", "convert": "^5.12.0", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", "libphonenumber-js": "^1.12.9", 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 0f8426c48..3ea37f474 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 @@ -83,7 +83,8 @@ label: tableWidgets[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column].name || savedFilterMap[selectedFilterSetId]?.dynamicColumn.column, value: savedFilterMap[selectedFilterSetId]?.dynamicColumn.value, widgetStructure: tableWidgets[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column], - relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined + relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined, + autofocus: shouldAutofocus }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] } @@ -98,7 +99,8 @@ label: savedFilterMap[selectedFilterSetId]?.dynamicColumn.column, value: savedFilterMap[selectedFilterSetId]?.dynamicColumn.value, structure: tableRowStructure[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column], - relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined + relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined, + autofocus: shouldAutofocus }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] } @@ -116,7 +118,8 @@ value: savedFilterMap[selectedFilterSetId]?.dynamicColumn.value, readonly: savedFilterMap[selectedFilterSetId]?.dynamicColumn.operator === 'empty', structure: tableRowStructure[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column], - relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined + relations: tableTypes[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] === 'foreign key' ? tableForeignKeys[savedFilterMap[selectedFilterSetId]?.dynamicColumn.column] : undefined, + autofocus: shouldAutofocus }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateDynamicColumnValue, 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 02b511e50..f2eea469e 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 @@ -68,6 +68,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { public selectedFilterSetId: string | null = null; public selectedFilter: any = null; + public shouldAutofocus: boolean = false; public tableStructure: any = null; public tableRowFieldsShown: { [key: string]: any } = {}; @@ -335,10 +336,12 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { if (this.selectedFilterSetId === selectedFilterSetId) { this.selectedFilterSetId = null; this.filterSelected.emit(null); + this.shouldAutofocus = false; const queryParams = this.buildQueryParams(); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } else { this.selectedFilterSetId = selectedFilterSetId; + this.shouldAutofocus = true; const selectedFilter = this.savedFilterMap[selectedFilterSetId]; this.filterSelected.emit(selectedFilter); @@ -356,6 +359,11 @@ 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); } } diff --git a/frontend/src/app/components/ui-components/filter-fields/base-filter-field/base-filter-field.component.ts b/frontend/src/app/components/ui-components/filter-fields/base-filter-field/base-filter-field.component.ts index 8614f4e14..774c159cd 100644 --- a/frontend/src/app/components/ui-components/filter-fields/base-filter-field/base-filter-field.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/base-filter-field/base-filter-field.component.ts @@ -17,6 +17,7 @@ export class BaseFilterFieldComponent implements OnInit { @Input() disabled: boolean; @Input() widgetStructure: WidgetStructure; @Input() relations: TableForeignKey; + @Input() autofocus: boolean = false; @Output() onFieldChange = new EventEmitter(); diff --git a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.html b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.html index 8996f45d6..6def8da23 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.html @@ -2,6 +2,7 @@ {{normalizedLabel}} (date) diff --git a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts index 002a50cd6..1ed23c41d 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts @@ -1,11 +1,12 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { format } from 'date-fns' +import { format, parseISO } from 'date-fns'; +import { fromZonedTime, toZonedTime, formatInTimeZone } from 'date-fns-tz'; @Component({ selector: 'app-filter-date-time', @@ -13,32 +14,81 @@ import { format } from 'date-fns' styleUrls: ['./date-time.component.css'], imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule] }) -export class DateTimeFilterComponent extends BaseFilterFieldComponent { +export class DateTimeFilterComponent extends BaseFilterFieldComponent implements AfterViewInit { @Input() value: string; + @ViewChild('dateInputElement') dateInputElement: ElementRef; @Output() onFieldChange = new EventEmitter(); static type = 'datetime'; public date: string; public time: string; + public timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; ngOnInit(): void { super.ngOnInit(); + this.parseWidgetParams(); + if (this.value) { - const datetime = new Date(this.value); - this.date = format(datetime, 'yyyy-MM-dd'); - this.time = format(datetime, 'HH:mm:ss'); + // Parse the UTC date from server + const utcDate = typeof this.value === 'string' ? parseISO(this.value) : new Date(this.value); + // Convert UTC to user's timezone for display + const zonedDate = toZonedTime(utcDate, this.timezone); + this.date = format(zonedDate, 'yyyy-MM-dd'); + this.time = format(zonedDate, 'HH:mm:ss'); } } onDateChange() { - if (!this.time) this.time = '00:00'; - const datetime = `${this.date}T${this.time}Z`; - this.onFieldChange.emit(datetime); + if (!this.time) this.time = '00:00:00'; + this.emitUtcDateTime(); } onTimeChange() { - const datetime = `${this.date}T${this.time}Z`; - this.onFieldChange.emit(datetime); + if (this.date) { + this.emitUtcDateTime(); + } + } + + private emitUtcDateTime() { + try { + // Create a date in the user's timezone + const localDateTimeString = `${this.date}T${this.time}`; + const localDate = new Date(localDateTimeString); + + // Convert from user's timezone to UTC + const utcDate = fromZonedTime(localDate, this.timezone); + // Format as ISO string without milliseconds for backward compatibility + const utcIsoString = utcDate.toISOString().replace(/\.\d{3}Z$/, 'Z'); + + this.onFieldChange.emit(utcIsoString); + } catch (error) { + console.error('Error converting datetime to UTC:', error); + } + } + + ngAfterViewInit(): void { + if (this.autofocus && this.dateInputElement) { + setTimeout(() => { + this.dateInputElement.nativeElement.focus(); + }, 100); + } + } + + private parseWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + try { + const params = typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + // Parse timezone parameter, default to user's local timezone + if (params.timezone) { + this.timezone = params.timezone; + } + } catch (e) { + console.error('Error parsing datetime widget params:', e); + } + } } } diff --git a/frontend/src/app/components/ui-components/filter-fields/date/date.component.html b/frontend/src/app/components/ui-components/filter-fields/date/date.component.html index 5a63de4c8..2b87fc46e 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date/date.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/date/date.component.html @@ -1,6 +1,7 @@ {{normalizedLabel}} diff --git a/frontend/src/app/components/ui-components/filter-fields/date/date.component.ts b/frontend/src/app/components/ui-components/filter-fields/date/date.component.ts index 0ffc2b311..c583e71f5 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date/date.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/date/date.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; import { CommonModule } from '@angular/common'; @@ -13,8 +13,9 @@ import { format } from 'date-fns' styleUrls: ['./date.component.css'], imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule] }) -export class DateFilterComponent extends BaseFilterFieldComponent { +export class DateFilterComponent extends BaseFilterFieldComponent implements AfterViewInit { @Input() value: string; + @ViewChild('inputElement') inputElement: ElementRef; static type = 'datetime'; public date: string; @@ -27,6 +28,14 @@ export class DateFilterComponent extends BaseFilterFieldComponent { } } + ngAfterViewInit(): void { + if (this.autofocus && this.inputElement) { + setTimeout(() => { + this.inputElement.nativeElement.focus(); + }, 100); + } + } + onDateChange() { this.onFieldChange.emit(this.date); } diff --git a/frontend/src/app/components/ui-components/filter-fields/long-text/long-text.component.html b/frontend/src/app/components/ui-components/filter-fields/long-text/long-text.component.html index 480299d16..41a02b8d8 100644 --- a/frontend/src/app/components/ui-components/filter-fields/long-text/long-text.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/long-text/long-text.component.html @@ -1,6 +1,7 @@ {{normalizedLabel}}