From fbca7363f4288b8f9d00550b082c85d19ed66e62 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 2 Aug 2025 17:34:33 +0300 Subject: [PATCH 01/29] admin panel: refactor table view --- frontend/src/app/app-routing.module.ts | 7 +- .../dashboard/dashboard.component.html | 4 +- .../dashboard/dashboard.component.ts | 14 ++-- .../db-action-link-dialog.component.css | 0 .../db-action-link-dialog.component.html | 0 .../db-action-link-dialog.component.spec.ts | 0 .../db-action-link-dialog.component.ts | 0 ...k-action-confirmation-dialog.component.css | 0 ...-action-confirmation-dialog.component.html | 0 ...tion-confirmation-dialog.component.spec.ts | 0 ...lk-action-confirmation-dialog.component.ts | 0 .../action-delete-dialog.component.css | 0 .../action-delete-dialog.component.html | 0 .../action-delete-dialog.component.spec.ts | 0 .../action-delete-dialog.component.ts | 0 .../db-table-actions.component.css | 0 .../db-table-actions.component.html | 0 .../db-table-actions.component.spec.ts | 0 .../db-table-actions.component.ts | 8 +-- .../db-table-ai-panel.component.css | 0 .../db-table-ai-panel.component.html | 0 .../db-table-ai-panel.component.spec.ts | 0 .../db-table-ai-panel.component.ts | 0 .../db-table-export-dialog.component.css | 0 .../db-table-export-dialog.component.html | 0 .../db-table-export-dialog.component.spec.ts | 0 .../db-table-export-dialog.component.ts | 0 .../db-table-filters-dialog.component.css | 0 .../db-table-filters-dialog.component.html | 0 .../db-table-filters-dialog.component.spec.ts | 0 .../db-table-filters-dialog.component.ts | 2 +- .../db-table-import-dialog.component.css | 0 .../db-table-import-dialog.component.html | 0 .../db-table-import-dialog.component.spec.ts | 0 .../db-table-import-dialog.component.ts | 0 .../db-table-row-view.component.css | 0 .../db-table-row-view.component.html | 0 .../db-table-row-view.component.spec.ts | 0 .../db-table-row-view.component.ts | 2 +- .../db-table-settings.component.css | 0 .../db-table-settings.component.html | 0 .../db-table-settings.component.spec.ts | 0 .../db-table-settings.component.ts | 14 ++-- .../db-table-view.component.css} | 0 .../db-table-view.component.html} | 0 .../db-table-view.component.spec.ts} | 16 ++--- .../db-table-view.component.ts} | 65 +++---------------- .../db-table-widgets.component.css | 0 .../db-table-widgets.component.html | 0 .../db-table-widgets.component.spec.ts | 5 +- .../db-table-widgets.component.ts | 20 +++--- .../widget-delete-dialog.component.css | 0 .../widget-delete-dialog.component.html | 0 .../widget-delete-dialog.component.spec.ts | 0 .../widget-delete-dialog.component.ts | 0 .../widget/widget.component.css | 0 .../widget/widget.component.html | 0 .../widget/widget.component.spec.ts | 0 .../widget/widget.component.ts | 0 .../db-table-row-edit.component.ts | 6 +- 60 files changed, 58 insertions(+), 105 deletions(-) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-action-link-dialog/db-action-link-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-action-link-dialog/db-action-link-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-action-link-dialog/db-action-link-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-action-link-dialog/db-action-link-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/action-delete-dialog/action-delete-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/action-delete-dialog/action-delete-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/action-delete-dialog/action-delete-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/action-delete-dialog/action-delete-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/db-table-actions.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/db-table-actions.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/db-table-actions.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-actions/db-table-actions.component.ts (97%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-ai-panel/db-table-ai-panel.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-ai-panel/db-table-ai-panel.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-ai-panel/db-table-ai-panel.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-ai-panel/db-table-ai-panel.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-export-dialog/db-table-export-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-export-dialog/db-table-export-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-export-dialog/db-table-export-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-export-dialog/db-table-export-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-filters-dialog/db-table-filters-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-filters-dialog/db-table-filters-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-filters-dialog/db-table-filters-dialog.component.ts (98%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-import-dialog/db-table-import-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-import-dialog/db-table-import-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-import-dialog/db-table-import-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-import-dialog/db-table-import-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-row-view/db-table-row-view.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-row-view/db-table-row-view.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-row-view/db-table-row-view.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-row-view/db-table-row-view.component.ts (98%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-settings/db-table-settings.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-settings/db-table-settings.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-settings/db-table-settings.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-settings/db-table-settings.component.ts (94%) rename frontend/src/app/components/dashboard/{db-table/db-table.component.css => db-table-view/db-table-view.component.css} (100%) rename frontend/src/app/components/dashboard/{db-table/db-table.component.html => db-table-view/db-table-view.component.html} (100%) rename frontend/src/app/components/dashboard/{db-table/db-table.component.spec.ts => db-table-view/db-table-view.component.spec.ts} (95%) rename frontend/src/app/components/dashboard/{db-table/db-table.component.ts => db-table-view/db-table-view.component.ts} (81%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/db-table-widgets.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/db-table-widgets.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/db-table-widgets.component.spec.ts (99%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/db-table-widgets.component.ts (91%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget/widget.component.css (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget/widget.component.html (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget/widget.component.spec.ts (100%) rename frontend/src/app/components/dashboard/{ => db-table-view}/db-table-widgets/widget/widget.component.ts (100%) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index e8d52750e..6f0dde1f8 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -8,11 +8,10 @@ import { ConnectDBComponent } from './components/connect-db/connect-db.component import { ConnectionSettingsComponent } from './components/connection-settings/connection-settings.component'; import { ConnectionsListComponent } from './components/connections-list/connections-list.component'; import { DashboardComponent } from './components/dashboard/dashboard.component' -import { DbTableActionsComponent } from './components/dashboard/db-table-actions/db-table-actions.component'; -import { DbTableComponent } from './components/dashboard/db-table/db-table.component'; +import { DbTableActionsComponent } from './components/dashboard/db-table-view/db-table-actions/db-table-actions.component'; import { DbTableRowEditComponent } from './components/db-table-row-edit/db-table-row-edit.component'; -import { DbTableSettingsComponent } from './components/dashboard/db-table-settings/db-table-settings.component'; -import { DbTableWidgetsComponent } from './components/dashboard/db-table-widgets/db-table-widgets.component'; +import { DbTableSettingsComponent } from './components/dashboard/db-table-view/db-table-settings/db-table-settings.component'; +import { DbTableWidgetsComponent } from './components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component'; import { EmailChangeComponent } from './components/email-change/email-change.component'; import { EmailVerificationComponent } from './components/email-verification/email-verification.component'; import { LoginComponent } from './components/login/login.component'; diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index 27a029e73..dc7f76158 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -74,7 +74,7 @@

Rocketadmin can not find any tables

- Rocketadmin can not find any tables (resetAllFilters)="clearAllFilters()" (search)="search($event)" (activateActions)="activateActions($event)"> - + { - let component: DbTableComponent; - let fixture: ComponentFixture; +describe('DbTableViewComponent', () => { + let component: DbTableViewComponent; + let fixture: ComponentFixture; const mockWidgets = { "Region": { @@ -81,14 +81,14 @@ describe('DbTableComponent', () => { FormsModule, MatDialogModule, Angulartics2Module.forRoot({}), - DbTableComponent + DbTableViewComponent ], providers: [provideHttpClient(), provideRouter([])] }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(DbTableComponent); + fixture = TestBed.createComponent(DbTableViewComponent); component = fixture.componentInstance; component.table = new TablesDataSource({} as any, {} as any, {} as any, {} as any); component.selection = new SelectionModel(true, []); diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts similarity index 81% rename from frontend/src/app/components/dashboard/db-table/db-table.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index b4a586bb5..275bcf37b 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -3,34 +3,20 @@ import * as JSON5 from 'json5'; import { ActivatedRoute, Router } from '@angular/router'; import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { CustomAction, TableForeignKey, TablePermissions, TableProperties, TableRow, Widget } from 'src/app/models/table'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { Observable, merge, of } from 'rxjs'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UIwidgets, tableDisplayTypes } from '../../../consts/table-display-types'; -import { map, startWith, tap } from 'rxjs/operators'; import { AccessLevel } from 'src/app/models/user'; import { Angulartics2OnModule } from 'angulartics2'; -// Import all display components -import { BaseTableDisplayFieldComponent } from '../../ui-components/table-display-fields/base-table-display-field/base-table-display-field.component'; -import { BooleanDisplayComponent } from '../../ui-components/table-display-fields/boolean/boolean.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CodeDisplayComponent } from '../../ui-components/table-display-fields/code/code.component'; import { CommonModule } from '@angular/common'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { CountryDisplayComponent } from '../../ui-components/table-display-fields/country/country.component'; -import { DateDisplayComponent } from '../../ui-components/table-display-fields/date/date.component'; -import { DateTimeDisplayComponent } from '../../ui-components/table-display-fields/date-time/date-time.component'; -import { DbTableExportDialogComponent } from '../db-table-export-dialog/db-table-export-dialog.component'; -import { DbTableImportDialogComponent } from '../db-table-import-dialog/db-table-import-dialog.component'; +import { DbTableExportDialogComponent } from './db-table-export-dialog/db-table-export-dialog.component'; +import { DbTableImportDialogComponent } from './db-table-import-dialog/db-table-import-dialog.component'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { DynamicModule } from 'ng-dynamic-component'; -import { FileDisplayComponent } from '../../ui-components/table-display-fields/file/file.component'; import { ForeignKeyDisplayComponent } from '../../ui-components/table-display-fields/foreign-key/foreign-key.component'; -import { IdDisplayComponent } from '../../ui-components/table-display-fields/id/id.component'; -import { ImageDisplayComponent } from '../../ui-components/table-display-fields/image/image.component'; -import { JsonEditorDisplayComponent } from '../../ui-components/table-display-fields/json-editor/json-editor.component'; import JsonURL from "@jsonurl/jsonurl"; -import { LongTextDisplayComponent } from '../../ui-components/table-display-fields/long-text/long-text.component'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -47,26 +33,16 @@ import { MatSort } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MoneyDisplayComponent } from '../../ui-components/table-display-fields/money/money.component'; import { NotificationsService } from 'src/app/services/notifications.service'; -import { NumberDisplayComponent } from '../../ui-components/table-display-fields/number/number.component'; -import { PasswordDisplayComponent } from '../../ui-components/table-display-fields/password/password.component'; -import { PhoneDisplayComponent } from '../../ui-components/table-display-fields/phone/phone.component'; import { PlaceholderTableDataComponent } from '../../skeletons/placeholder-table-data/placeholder-table-data.component'; -import { PointDisplayComponent } from '../../ui-components/table-display-fields/point/point.component'; import { RouterModule } from '@angular/router'; -import { SelectDisplayComponent } from '../../ui-components/table-display-fields/select/select.component'; import { SelectionModel } from '@angular/cdk/collections'; -import { StaticTextDisplayComponent } from '../../ui-components/table-display-fields/static-text/static-text.component'; import { TableRowService } from 'src/app/services/table-row.service'; import { TableStateService } from 'src/app/services/table-state.service'; -import { TextDisplayComponent } from '../../ui-components/table-display-fields/text/text.component'; -import { TimeDisplayComponent } from '../../ui-components/table-display-fields/time/time.component'; -import { TimeIntervalDisplayComponent } from '../../ui-components/table-display-fields/time-interval/time-interval.component'; -import { UrlDisplayComponent } from '../../ui-components/table-display-fields/url/url.component'; import { formatFieldValue } from 'src/app/lib/format-field-value'; +import { merge } from 'rxjs'; import { normalizeTableName } from '../../../lib/normalize' -import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; +import { tap } from 'rxjs/operators'; interface Column { title: string, @@ -74,9 +50,9 @@ interface Column { } @Component({ - selector: 'app-db-table', - templateUrl: './db-table.component.html', - styleUrls: ['./db-table.component.css'], + selector: 'app-db-table-view', + templateUrl: './db-table-view.component.html', + styleUrls: ['./db-table-view.component.css'], imports: [ CommonModule, FormsModule, @@ -100,34 +76,11 @@ interface Column { Angulartics2OnModule, PlaceholderTableDataComponent, DynamicModule, - // Display components for different field types - // BaseTableDisplayFieldComponent, - // TextDisplayComponent, - // LongTextDisplayComponent, - // IdDisplayComponent, - // BooleanDisplayComponent, ForeignKeyDisplayComponent, - // DateDisplayComponent, - // DateTimeDisplayComponent, - // TimeDisplayComponent, - // SelectDisplayComponent, - // CodeDisplayComponent, - // MoneyDisplayComponent, - // PasswordDisplayComponent, - // FileDisplayComponent, - // ImageDisplayComponent, - // UrlDisplayComponent, - // JsonEditorDisplayComponent, - // NumberDisplayComponent, - // StaticTextDisplayComponent, - // CountryDisplayComponent, - // PhoneDisplayComponent, - // PointDisplayComponent, - // TimeIntervalDisplayComponent ] }) -export class DbTableComponent implements OnInit { +export class DbTableViewComponent implements OnInit { @Input() name: string; @Input() displayName: string; diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.spec.ts similarity index 99% rename from frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.spec.ts index 5939ab836..807e90584 100644 --- a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.spec.ts @@ -1,15 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; + +import { Angulartics2Module } from 'angulartics2'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { DashboardComponent } from '../dashboard.component'; +import { DashboardComponent } from '../../dashboard.component'; import { DbTableWidgetsComponent } from './db-table-widgets.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { RouterTestingModule } from '@angular/router/testing'; import { TablesService } from 'src/app/services/tables.service'; import { WidgetDeleteDialogComponent } from './widget-delete-dialog/widget-delete-dialog.component'; import { of } from 'rxjs'; -import { Angulartics2Module } from 'angulartics2'; import { provideHttpClient } from '@angular/common/http'; describe('DbTableWidgetsComponent', () => { diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts similarity index 91% rename from frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts index 76308ef37..bb94c632c 100644 --- a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts @@ -2,33 +2,33 @@ import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; import { Component, OnInit } from '@angular/core'; import { TableField, Widget } from 'src/app/models/table'; -import { AlertComponent } from '../../ui-components/alert/alert.component'; -import { BreadcrumbsComponent } from '../../ui-components/breadcrumbs/breadcrumbs.component'; -import { CodeEditComponent } from '../../ui-components/record-edit-fields/code/code.component'; +import { AlertComponent } from '../../../ui-components/alert/alert.component'; +import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; +import { CodeEditComponent } from '../../../ui-components/record-edit-fields/code/code.component'; import { CommonModule } from '@angular/common'; import { CompanyService } from 'src/app/services/company.service'; import { ConnectionsService } from 'src/app/services/connections.service'; import { FormsModule } from '@angular/forms'; -import { ImageEditComponent } from '../../ui-components/record-edit-fields/image/image.component'; +import { ImageEditComponent } from '../../../ui-components/record-edit-fields/image/image.component'; import { Location } from '@angular/common'; -import { LongTextEditComponent } from '../../ui-components/record-edit-fields/long-text/long-text.component'; +import { LongTextEditComponent } from '../../../ui-components/record-edit-fields/long-text/long-text.component'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { PasswordEditComponent } from '../../ui-components/record-edit-fields/password/password.component'; -import { PlaceholderTableWidgetsComponent } from '../../skeletons/placeholder-table-widgets/placeholder-table-widgets.component'; +import { PasswordEditComponent } from '../../../ui-components/record-edit-fields/password/password.component'; +import { PlaceholderTableWidgetsComponent } from '../../../skeletons/placeholder-table-widgets/placeholder-table-widgets.component'; import { Router } from '@angular/router'; import { RouterModule } from '@angular/router'; -import { SelectEditComponent } from '../../ui-components/record-edit-fields/select/select.component'; +import { SelectEditComponent } from '../../../ui-components/record-edit-fields/select/select.component'; import { TablesService } from 'src/app/services/tables.service'; -import { TextEditComponent } from '../../ui-components/record-edit-fields/text/text.component'; +import { TextEditComponent } from '../../../ui-components/record-edit-fields/text/text.component'; import { Title } from '@angular/platform-browser'; import { UIwidgets } from "src/app/consts/record-edit-types"; import { UiSettingsService } from 'src/app/services/ui-settings.service'; -import { UrlEditComponent } from '../../ui-components/record-edit-fields/url/url.component'; +import { UrlEditComponent } from '../../../ui-components/record-edit-fields/url/url.component'; import { WidgetComponent } from './widget/widget.component'; import { WidgetDeleteDialogComponent } from './widget-delete-dialog/widget-delete-dialog.component'; import { difference } from "lodash"; diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget-delete-dialog/widget-delete-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.html diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-widgets/widget/widget.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.ts diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts index 366a08b38..f12184a7b 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts @@ -11,14 +11,14 @@ import { normalizeFieldName, normalizeTableName } from 'src/app/lib/normalize'; import { AlertComponent } from '../ui-components/alert/alert.component'; import { BannerComponent } from '../ui-components/banner/banner.component'; -import { BbBulkActionConfirmationDialogComponent } from '../dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component'; +import { BbBulkActionConfirmationDialogComponent } from '../dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component'; import { BreadcrumbsComponent } from '../ui-components/breadcrumbs/breadcrumbs.component'; import { CommonModule } from '@angular/common'; import { CompanyService } from 'src/app/services/company.service'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DBtype } from 'src/app/models/connection'; -import { DbActionLinkDialogComponent } from '../dashboard/db-action-link-dialog/db-action-link-dialog.component'; -import { DbTableRowViewComponent } from '../dashboard/db-table-row-view/db-table-row-view.component'; +import { DbActionLinkDialogComponent } from '../dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component'; +import { DbTableRowViewComponent } from '../dashboard/db-table-view/db-table-row-view/db-table-row-view.component'; import { DynamicModule } from 'ng-dynamic-component'; import JsonURL from "@jsonurl/jsonurl"; import { MatButtonModule } from '@angular/material/button'; From ad2ef97d4fa416344d94f9c4127fb823ef9b6947 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 2 Aug 2025 21:43:13 +0300 Subject: [PATCH 02/29] saved filters: - add saved filters panel; - add save filters set dialog and request. --- .../db-table-view.component.html | 8 + .../db-table-view/db-table-view.component.ts | 2 + .../saved-filters-dialog.component.css | 82 ++++++ .../saved-filters-dialog.component.html | 171 ++++++++++++ .../saved-filters-dialog.component.spec.ts | 23 ++ .../saved-filters-dialog.component.ts | 247 ++++++++++++++++++ .../saved-filters-panel.component.css | 0 .../saved-filters-panel.component.html | 5 + .../saved-filters-panel.component.spec.ts | 23 ++ .../saved-filters-panel.component.ts | 40 +++ frontend/src/app/consts/filter-types.ts | 3 +- frontend/src/app/services/tables.service.ts | 26 ++ 12 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 64474682d..80fb39730 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -179,6 +179,14 @@

{{ displayName }}

+ +
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 275bcf37b..19c588a99 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -36,6 +36,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { NotificationsService } from 'src/app/services/notifications.service'; import { PlaceholderTableDataComponent } from '../../skeletons/placeholder-table-data/placeholder-table-data.component'; import { RouterModule } from '@angular/router'; +import { SavedFiltersPanelComponent } from './saved-filters-panel/saved-filters-panel.component'; import { SelectionModel } from '@angular/cdk/collections'; import { TableRowService } from 'src/app/services/table-row.service'; import { TableStateService } from 'src/app/services/table-state.service'; @@ -77,6 +78,7 @@ interface Column { PlaceholderTableDataComponent, DynamicModule, ForeignKeyDisplayComponent, + SavedFiltersPanelComponent ] }) diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css new file mode 100644 index 000000000..611eed465 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -0,0 +1,82 @@ +.filters-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; +} + +.filters-content { + display: grid; + grid-template-columns: auto 228px 0 1fr 32px; + grid-column-gap: 8px; + align-content: flex-start; + align-items: flex-start; +} + +.filters-select { + grid-column: 1 / span 5; + margin-bottom: 16px; +} + +.filter-line { + grid-column: 1 / span 4; +} + +.filter-save-form { + grid-column: 1 / span 5; + margin-bottom: 24px; +} + +.section-title { + grid-column: 1 / span 5; + margin: 16px 0 8px 0; + color: rgba(0, 0, 0, 0.87); + font-weight: 500; +} + +.full-width { + width: 100%; +} + +.default-filter-checkbox { + margin-bottom: 16px; +} + +::ng-deep .mat-dialog-container > .ng-star-inserted { + display: flex; + flex-direction: column; + width: 100%; +} + +.filters-form { + flex: 1 0 auto; + display: flex; + flex-direction: column; +} + +::ng-deep .mat-dialog-container { + display: flex; +} + +.filters-content { + flex: 1 0 auto; +} + +.column-name { + margin-top: 18px; +} + +.filter-delete-button { + margin-top: 4px; +} + +.settings-form__reset-button { + margin-right: auto; +} + +.no-filters-message { + grid-column: 1 / span 5; + color: rgba(0, 0, 0, 0.6); + font-style: italic; + margin: 16px 0; +} diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html new file mode 100644 index 000000000..8da38d2d5 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -0,0 +1,171 @@ +
+

+ Save Filters for {{ data.displayTableName }} table + + settings + +

+ + + + + + Filter Name + + + Filter name is required + + + +

Define Filters

+ + + + Add filter by... + + + + {{field}} + + + + + + +
+ +
+ +
+ + + + +
+ + + {{value.key}} + + + + + starts with + + + ends with + + + equal + + + contains + + + not contains + + + is empty + + + + + + + + equal + + + greater than + + + less than + + + greater than or equal + + + less than or equal + + + + + + + +
+ + +
+ No filters added. Please add at least one filter. +
+
+
+ + + + + + +
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts new file mode 100644 index 000000000..0e9859d37 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SavedFiltersDialogComponent } from './saved-filters-dialog.component'; + +describe('SavedFiltersDialogComponent', () => { + let component: SavedFiltersDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SavedFiltersDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SavedFiltersDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..8606a495b --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts @@ -0,0 +1,247 @@ +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { CommonModule } from '@angular/common'; +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { DynamicModule } from 'ng-dynamic-component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { RouterModule } from '@angular/router'; +import { ContentLoaderComponent } from 'src/app/components/ui-components/content-loader/content-loader.component'; +import { Observable, map, startWith } from 'rxjs'; +import { TablesService } from 'src/app/services/tables.service'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { filterTypes } from 'src/app/consts/filter-types'; +import { UIwidgets } from 'src/app/consts/record-edit-types'; +import { TableField, TableForeignKey } from 'src/app/models/table'; +import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; +import { omitBy } from 'lodash'; + +@Component({ + selector: 'app-saved-filters-dialog', + imports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatSelectModule, + DynamicModule, + RouterModule, + MatDialogModule, + MatSnackBarModule, + ContentLoaderComponent + ], + templateUrl: './saved-filters-dialog.component.html', + styleUrl: './saved-filters-dialog.component.css' +}) +export class SavedFiltersDialogComponent implements OnInit { + @Input() connectionID: string; + @Input() tableName: string; + @Input() displayTableName: string; + @Input() savedFilterData: any; + + public tableFilters = []; + public fieldSearchControl = new FormControl(''); + public fields: string[]; + public foundFields: Observable; + + public tableRowFields: Object; + 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 = UIwidgets; + + // Form for saving filters + public filterForm = new FormGroup({ + filterName: new FormControl('', [Validators.required]), + filterDescription: new FormControl(''), + isDefault: new FormControl(false) + }); + + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _tables: TablesService, + private _connections: ConnectionsService, + private dialogRef: MatDialogRef, + private snackBar: MatSnackBar + ) {} + + ngOnInit(): void { + this._tables.cast.subscribe(); + + // Load table structure + this._tables.fetchTableStructure(this.data.connectionID, this.data.tableName).subscribe({ + next: (structure) => { + this.tableForeignKeys = {...structure.foreignKeys}; + this.tableRowFields = Object.assign({}, ...structure.structure.map((field: TableField) => ({[field.column_name]: undefined}))); + const foreignKeysList = structure.foreignKeys.map((fk: TableForeignKey) => fk.column_name); + this.tableTypes = getTableTypes(structure.structure, foreignKeysList); + this.fields = structure.structure + .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') + .map((field: TableField) => field.column_name); + + this.tableRowStructure = Object.assign({}, ...structure.structure.map((field: TableField) => { + return {[field.column_name]: field}; + })); + + // Setup widgets if available + if (structure.widgets && structure.widgets.length) { + this.setWidgets(structure.widgets); + } + + this.foundFields = this.fieldSearchControl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value || '')), + ); + }, + error: (err) => { + console.error('Error loading table structure:', err); + } + }); + } + + private _filter(value: string): string[] { + return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); + } + + get inputs() { + 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 (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; + } + + isWidget(columnName: string) { + return this.tableWidgetsList.includes(columnName); + } + + updateField = (updatedValue: any, field: string) => { + this.tableRowFieldsShown[field] = updatedValue; + this.updateFiltersCount(); + } + + addFilter(e) { + const key = e.option.value; + this.tableRowFieldsShown = {...this.tableRowFieldsShown, [key]: this.tableRowFields[key]}; + this.tableRowFieldsComparator = {...this.tableRowFieldsComparator, [key]: this.tableRowFieldsComparator[key] || 'eq'}; + this.fieldSearchControl.setValue(''); + this.updateFiltersCount(); + } + + updateComparator(event, fieldName: string) { + if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; + } + + resetFilters() { + this.tableFilters = []; + this.tableRowFieldsShown = {}; + this.tableRowFieldsComparator = {}; + this.updateFiltersCount(); + } + + getInputType(field: string) { + let widgetType; + if (this.isWidget(field)) { + widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; + } else { + widgetType = this.inputs[this.tableTypes[field]]?.type; + } + return widgetType; + } + + getComparatorType(typeOfComponent) { + if (typeOfComponent === 'text') { + return 'text'; + } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { + return 'number'; + } else { + return 'nonComparable'; + } + } + + removeFilter(field) { + delete this.tableRowFieldsShown[field]; + delete this.tableRowFieldsComparator[field]; + this.updateFiltersCount(); + } + + updateFiltersCount() { + this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; + } + + saveFilter() { + if (!this.filterForm.valid || this.tableFiltersCount === 0) { + return; + } + + // Prepare filter data to save + // const filterToSave = { + // name: this.filterForm.get('filterName').value, + // description: this.filterForm.get('filterDescription').value, + // isDefault: this.filterForm.get('isDefault').value, + // connectionId: this.data.connectionID, + // tableName: this.data.tableName, + // filters: this.tableRowFieldsShown, + // comparators: this.tableRowFieldsComparator + // }; + + const nonEmptyFilters = omitBy(this.tableRowFieldsShown, (value) => value === undefined); + + if (Object.keys(nonEmptyFilters).length) { + let filters = {}; + for (const key in nonEmptyFilters) { + if (this.tableRowFieldsComparator[key] !== undefined) { + filters[key] = { + [this.tableRowFieldsComparator[key]]: nonEmptyFilters[key] + }; + } + } + + // const filters = JsonURL.stringify( this.filters ); + + this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, {name: this.filterForm.get('filterName').value, filters}) + .subscribe(() => { + this.dialogRef.close(true); + }, (error) => { + console.error('Error saving filter:', error); + this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); + }); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..5a356cc60 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html @@ -0,0 +1,5 @@ + diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts new file mode 100644 index 000000000..6dba45b46 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SavedFiltersPanelComponent } from './saved-filters-panel.component'; + +describe('SavedFiltersPanelComponent', () => { + let component: SavedFiltersPanelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SavedFiltersPanelComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SavedFiltersPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..3f2a58812 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; + +@Component({ + selector: 'app-saved-filters-panel', + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + ], + templateUrl: './saved-filters-panel.component.html', + styleUrl: './saved-filters-panel.component.css' +}) +export class SavedFiltersPanelComponent { + @Input() connectionID: string; + @Input() selectedTableName: string; + @Input() selectedTableDisplayName: string; + @Input() savedFilterData: any; + + constructor(private dialog: MatDialog) {} + + + + handleOpenSavedFiltersDialog() { + this.dialog.open(SavedFiltersDialogComponent, { + width: '56em', + data: { + connectionID: this.connectionID, + tableName: this.selectedTableName, + displayTableName: this.selectedTableDisplayName, + savedFilterData: this.savedFilterData + } + }); + } +} diff --git a/frontend/src/app/consts/filter-types.ts b/frontend/src/app/consts/filter-types.ts index 42ceaa095..c78c04ed2 100644 --- a/frontend/src/app/consts/filter-types.ts +++ b/frontend/src/app/consts/filter-types.ts @@ -31,7 +31,8 @@ export const UIwidgets = { Select: SelectFilterComponent, Password: PasswordFilterComponent, File: FileFilterComponent, - Country: CountryFilterComponent + Country: CountryFilterComponent, + Foreign_key: ForeignKeyFilterComponent, } export const filterTypes = { diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index 85498185e..203a009ee 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -510,4 +510,30 @@ export class TablesService { }) ); } + + createSavedFilter(connectionID: string, tableName: string, filters: object) { + return this._http.post(`/table-filters/${connectionID}`, filters, { + params: { + tableName + } + }) + .pipe( + map(res => { + this.tables.next('saved filters'); + this._notifications.showSuccessSnackbar('Saved filters have been updated.') + return res + }), + catchError((err) => { + console.log(err); + this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (id: number) => this._notifications.dismissAlert() + } + ]); + return EMPTY; + }) + ) + } } From d84dc0fb307c2c347a7553499f9602c423e63bf6 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 2 Aug 2025 22:43:24 +0300 Subject: [PATCH 03/29] saved filters: map saved filters into tabs --- .../db-table-view.component.html | 1 - .../saved-filters-panel.component.css | 81 +++++++++++++++++ .../saved-filters-panel.component.html | 38 +++++++- .../saved-filters-panel.component.ts | 91 ++++++++++++++++++- frontend/src/app/services/tables.service.ts | 17 ++++ 5 files changed, 219 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 80fb39730..7dd94dc83 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -184,7 +184,6 @@

{{ displayName }}

[connectionID]="connectionID" [selectedTableName]="name" [selectedTableDisplayName]="displayName" - [savedFilterData]="tableData.savedFilters" >
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index e69de29bb..f97e8350d 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -0,0 +1,81 @@ +.saved-filters-container { + display: flex; + flex-direction: column; + gap: 16px; + margin: 8px 0; +} + +.saved-filters-tabs { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +} + +.filter-chips-container { + margin-top: 8px; + padding: 12px; + background-color: rgba(0, 0, 0, 0.04); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.filter-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-chip { + font-size: 13px; + transition: all 0.2s ease; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.filter-chip:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.column-name { + font-weight: 500; + margin-right: 4px; +} + +.operator { + margin-right: 4px; + font-style: italic; + color: #666; +} + +.value { + color: #3f51b5; + font-weight: 500; +} + +/* Style for the tabs as chips */ +:host ::ng-deep .mat-mdc-chip-option.mdc-evolution-chip--selected .mdc-evolution-chip__text-label { + color: white; +} + +:host ::ng-deep .mat-mdc-chip-option { + min-height: 36px; +} + +.no-filters { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + background-color: rgba(0, 0, 0, 0.04); + border-radius: 4px; + color: rgba(0, 0, 0, 0.6); +} + +.no-filters mat-icon { + color: #999; + font-size: 18px; + width: 18px; + height: 18px; +} \ No newline at end of file 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 5a356cc60..9471b39a4 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 @@ -1,5 +1,33 @@ - +
+ + +
+ info + No saved filters. Create one by clicking the button above. +
+ +
+ + + {{ filter.name }} + + + +
+
+ + {{ getFilter(entry) }} + +
+
+
+
\ No newline at end of file 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 3f2a58812..28231d4fe 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,10 +1,15 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; +import { TablesService } from 'src/app/services/tables.service'; +import { normalizeTableName } from 'src/app/lib/normalize'; @Component({ selector: 'app-saved-filters-panel', @@ -12,6 +17,9 @@ import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filter CommonModule, MatButtonModule, MatIconModule, + MatChipsModule, + MatTabsModule, + MatTooltipModule, ], templateUrl: './saved-filters-panel.component.html', styleUrl: './saved-filters-panel.component.css' @@ -20,11 +28,35 @@ export class SavedFiltersPanelComponent { @Input() connectionID: string; @Input() selectedTableName: string; @Input() selectedTableDisplayName: string; - @Input() savedFilterData: any; + // @Input() savedFilterData: any; + @Output() filterSelected = new EventEmitter(); - constructor(private dialog: MatDialog) {} + public savedFilterData: any[] = []; + public selectedFilterIndex: number = 0; + public displayedComparators = { + eq: "=", + gt: ">", + lt: "<", + gte: ">=", + lte: "<=" + } + constructor( + private dialog: MatDialog, + private _tables: TablesService, + ) {} + ngOnInit() { + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ + next: (data) => { + this.savedFilterData = data; + console.log('Saved filters data:', this.savedFilterData); + }, + error: (error) => { + console.error('Error fetching saved filters:', error); + } + }); + } handleOpenSavedFiltersDialog() { this.dialog.open(SavedFiltersDialogComponent, { @@ -37,4 +69,57 @@ export class SavedFiltersPanelComponent { } }); } + + /** + * Transform the filters object into an array of entries for display + * @param filters The filters object from saved filter data + * @returns Array of objects with column, operator, and value + */ + getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { + if (!filters) return []; + + const entries: { column: string; operator: string; value: string }[] = []; + + Object.keys(filters).forEach(column => { + const operations = filters[column]; + + Object.keys(operations).forEach(operator => { + entries.push({ + column, + operator, + value: operations[operator] + }); + }); + }); + + return entries; + } + + /** + * Select a filter and emit the selection event + * @param index Index of the selected filter + */ + selectFilter(index: number): void { + this.selectedFilterIndex = index; + this.filterSelected.emit(this.savedFilterData[index]); + } + + getFilter(activeFilter: {column: string, operator: string, value: any}) { + const displayedName = normalizeTableName(activeFilter.column); + const comparator = activeFilter.operator; + const filterValue = activeFilter.value; + if (comparator == 'startswith') { + return `${displayedName} = ${filterValue}...` + } else if (comparator == 'endswith') { + return `${displayedName} = ...${filterValue}` + } else if (comparator == 'contains') { + return `${displayedName} = ...${filterValue}...` + } else if (comparator == 'icontains') { + return `${displayedName} != ...${filterValue}...` + } else if (comparator == 'empty') { + return `${displayedName} = ' '` + } else { + return `${displayedName} ${this.displayedComparators[comparator]} ${filterValue}` + } + } } diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index 203a009ee..02ae0de3d 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -511,6 +511,23 @@ export class TablesService { ); } + getSavedFilters(connectionID: string, tableName: string) { + return this._http.get(`/table-filters/${connectionID}/all`, { + params: { + tableName + } + }) + .pipe( + map((res) => { + return res + }), + catchError((err) => { + console.log(err); + return throwError(() => new Error(err.error.message)); + }) + ); + } + createSavedFilter(connectionID: string, tableName: string, filters: object) { return this._http.post(`/table-filters/${connectionID}`, filters, { params: { From bc94f8ae036a337a45afe62e0ad053ddd6ffba13 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 2 Aug 2025 23:10:53 +0300 Subject: [PATCH 04/29] saved filters: apply filters when switch between saved filters --- .../dashboard/dashboard.component.html | 3 +- .../dashboard/dashboard.component.ts | 4 ++ .../db-table-view.component.html | 2 +- .../db-table-view/db-table-view.component.ts | 2 + .../saved-filters-panel.component.html | 4 +- .../saved-filters-panel.component.ts | 59 +++++++++++++++++-- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index dc7f76158..428a6ee2f 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -89,7 +89,8 @@

Rocketadmin can not find any tables

(removeFilter)="removeFilter($event)" (resetAllFilters)="clearAllFilters()" (search)="search($event)" - (activateActions)="activateActions($event)"> + (activateActions)="activateActions($event)" + (applyFilter)="applyFilter($event)">
{{ displayName }}
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 19c588a99..442ab1520 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -104,6 +104,8 @@ export class DbTableViewComponent implements OnInit { @Output() activateAction = new EventEmitter(); @Output() activateActions = new EventEmitter(); + @Output() applyFilter = new EventEmitter(); + // public tablesSwitchControl = new FormControl(''); public tableData: any; public filteredTables: TableProperties[]; 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 9471b39a4..335933f1e 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 @@ -20,10 +20,10 @@ -
+
{{ getFilter(entry) }} 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 28231d4fe..e27584b66 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,6 +1,8 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; +import JsonURL from '@jsonurl/jsonurl'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; @@ -24,7 +26,7 @@ import { normalizeTableName } from 'src/app/lib/normalize'; templateUrl: './saved-filters-panel.component.html', styleUrl: './saved-filters-panel.component.css' }) -export class SavedFiltersPanelComponent { +export class SavedFiltersPanelComponent implements OnInit { @Input() connectionID: string; @Input() selectedTableName: string; @Input() selectedTableDisplayName: string; @@ -32,7 +34,7 @@ export class SavedFiltersPanelComponent { @Output() filterSelected = new EventEmitter(); public savedFilterData: any[] = []; - public selectedFilterIndex: number = 0; + public selectedFilterIndex: number = -1; public displayedComparators = { eq: "=", gt: ">", @@ -44,6 +46,8 @@ export class SavedFiltersPanelComponent { constructor( private dialog: MatDialog, private _tables: TablesService, + private router: Router, + private route: ActivatedRoute ) {} ngOnInit() { @@ -51,6 +55,25 @@ export class SavedFiltersPanelComponent { next: (data) => { this.savedFilterData = data; console.log('Saved filters data:', this.savedFilterData); + + // Check if there's a saved_filter parameter in the URL + this.route.queryParams.subscribe(params => { + const savedFilterName = params['saved_filter']; + + if (savedFilterName && this.savedFilterData.length > 0) { + // Find the index of the saved filter with the matching name + const filterIndex = this.savedFilterData.findIndex(filter => filter.name === savedFilterName); + + // If found, select it + if (filterIndex !== -1) { + this.selectedFilterIndex = filterIndex; + this.filterSelected.emit(this.savedFilterData[filterIndex]); + } + } else { + // No filter specified in URL, keep selectedFilterIndex as -1 (none selected) + this.selectedFilterIndex = -1; + } + }); }, error: (error) => { console.error('Error fetching saved filters:', error); @@ -101,7 +124,35 @@ export class SavedFiltersPanelComponent { */ selectFilter(index: number): void { this.selectedFilterIndex = index; - this.filterSelected.emit(this.savedFilterData[index]); + const selectedFilter = this.savedFilterData[index]; + this.filterSelected.emit(selectedFilter); + + // Get the current query params + const currentParams = this.route.snapshot.queryParams; + + // Update the URL with the filter data and saved filter name + const filters = JsonURL.stringify(selectedFilter.filters); + + // Create query params object + const queryParams: any = { + filters, + page_index: 0, + page_size: currentParams['page_size'] || 30, // Use current page size or default + saved_filter: selectedFilter.name // Add the saved filter name to the URL + }; + + // Only add sorting params if they exist + if (currentParams['sort_active']) { + queryParams.sort_active = currentParams['sort_active']; + } + + if (currentParams['sort_direction']) { + queryParams.sort_direction = currentParams['sort_direction']; + } + + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { + queryParams + }); } getFilter(activeFilter: {column: string, operator: string, value: any}) { From e00bd7b99c9ccc9aa44859936d72424c2b0a2c7e Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sun, 3 Aug 2025 17:22:11 +0300 Subject: [PATCH 05/29] saved filters: refactor chips behavior, use id, show filter to edit --- .../saved-filters-panel.component.html | 32 ++++++++----------- .../saved-filters-panel.component.ts | 30 ++++++++++++++--- 2 files changed, 39 insertions(+), 23 deletions(-) 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 335933f1e..5c95fe780 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 @@ -9,25 +9,21 @@ No saved filters. Create one by clicking the button above.
-
- - - {{ filter.name }} - - + + + {{ filter.name }} + + -
-
- - {{ getFilter(entry) }} - -
+
+ + {{ getFilter(entry) }} + +
+ edit filter {{ selectedFilter | json }}
\ No newline at end of file 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 e27584b66..e65d56b38 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 @@ -7,6 +7,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; @@ -22,6 +23,7 @@ import { normalizeTableName } from 'src/app/lib/normalize'; MatChipsModule, MatTabsModule, MatTooltipModule, + MatMenuModule ], templateUrl: './saved-filters-panel.component.html', styleUrl: './saved-filters-panel.component.css' @@ -34,8 +36,13 @@ export class SavedFiltersPanelComponent implements OnInit { @Output() filterSelected = new EventEmitter(); public savedFilterData: any[] = []; + public savedFilterMap: { [key: string]: any } = {}; public selectedFilterIndex: number = -1; - public displayedComparators = { + + public selectedFilterSetId: string | null = null; + public selectedFilter: any = null; + + public displayedComparators = { eq: "=", gt: ">", lt: "<", @@ -54,7 +61,15 @@ export class SavedFiltersPanelComponent implements OnInit { this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ next: (data) => { this.savedFilterData = data; - console.log('Saved filters data:', this.savedFilterData); + this.savedFilterMap = Object.assign({}, ...data.map((filter, index) => { + // Create a copy of the filter with transformed filters array + const transformedFilter = { + ...filter, + filterEntries: this.getFilterEntries(filter.filters) + }; + return { [filter.id]: transformedFilter }; + })); + console.log('Saved filters map:', this.savedFilterMap); // Check if there's a saved_filter parameter in the URL this.route.queryParams.subscribe(params => { @@ -122,9 +137,10 @@ export class SavedFiltersPanelComponent implements OnInit { * Select a filter and emit the selection event * @param index Index of the selected filter */ - selectFilter(index: number): void { - this.selectedFilterIndex = index; - const selectedFilter = this.savedFilterData[index]; + + selectFiltersSet(selectedFilterSetId: string): void { + this.selectedFilterSetId = selectedFilterSetId; + const selectedFilter = this.savedFilterMap[selectedFilterSetId]; this.filterSelected.emit(selectedFilter); // Get the current query params @@ -155,6 +171,10 @@ export class SavedFiltersPanelComponent implements OnInit { }); } + selectFilter(entry) { + this.selectedFilter = entry; + } + getFilter(activeFilter: {column: string, operator: string, value: any}) { const displayedName = normalizeTableName(activeFilter.column); const comparator = activeFilter.operator; From d481bfce35e98557bf1c20bf310a021303f9c309 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 4 Aug 2025 01:53:57 +0300 Subject: [PATCH 06/29] saved filters: edit form for single filter --- .../db-table-view.component.html | 1 + .../db-table-view/db-table-view.component.ts | 2 +- .../saved-filters-panel.component.css | 56 +++++++++ .../saved-filters-panel.component.html | 101 +++++++++++++++- .../saved-filters-panel.component.ts | 113 ++++++++++++++++-- 5 files changed, 258 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 12de73c3f..ecbbc1bfe 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -183,6 +183,7 @@

{{ displayName }}

[connectionID]="connectionID" [selectedTableName]="name" [selectedTableDisplayName]="displayName" + [tableTypes]="tableData.tableTypes" (filterSelected)="applyFilter.emit($event)" > diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 442ab1520..4c436880c 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -130,7 +130,7 @@ export class DbTableViewComponent implements OnInit { public tableRelatedRecords: any = null; public displayCellComponents; public UIwidgets = UIwidgets; - public tableTypes: object; + // public tableTypes: object; @Input() set table(value){ if (value) this.tableData = value; diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index f97e8350d..db9fe7abe 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -78,4 +78,60 @@ font-size: 18px; width: 18px; height: 18px; +} + +/* Single filter editor styles */ +.single-filter-editor { + margin-top: 20px; + padding: 16px; + background-color: #f5f7fa; + border-radius: 8px; + border: 1px solid #e0e4e8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.filter-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #e0e4e8; +} + +.filter-editor-header h3 { + margin: 0; + font-size: 16px; + font-weight: 500; + color: #333; +} + +.filter-edit-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.filter-line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; +} + +.column-name { + min-width: 120px; + font-weight: 500; +} + +.filter-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; +} + +.value-input { + min-width: 200px; + flex-grow: 1; } \ No newline at end of file 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 5c95fe780..26618d008 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 @@ -22,8 +22,105 @@ (click)="selectFilter(entry)"> {{ getFilter(entry) }} -
- edit filter {{ selectedFilter | json }} + +
+
+

Edit Filter: {{ selectedFilter.column }}

+ +
+ +
+
+ {{ selectedFilter.column }} + + + + + starts with + ends with + equal + contains + not contains + is empty + + + + + + + equal + greater than + less than + greater than or equal + less than or equal + + + + +
+
+ +
+ + + + +
+ + +
+ +
+
+ +
+ + +
+
\ No newline at end of file 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 e65d56b38..7d9a9d214 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 @@ -2,25 +2,38 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { DynamicModule } from 'ng-dynamic-component'; +import { FormsModule } from '@angular/forms'; import JsonURL from '@jsonurl/jsonurl'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; +import { MatSelectModule } from '@angular/material/select'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; import { TablesService } from 'src/app/services/tables.service'; +import { UIwidgets } from 'src/app/consts/record-edit-types'; +import { filterTypes } from 'src/app/consts/filter-types'; import { normalizeTableName } from 'src/app/lib/normalize'; @Component({ selector: 'app-saved-filters-panel', imports: [ CommonModule, + FormsModule, + DynamicModule, MatButtonModule, MatIconModule, MatChipsModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, MatTabsModule, MatTooltipModule, MatMenuModule @@ -32,6 +45,7 @@ export class SavedFiltersPanelComponent implements OnInit { @Input() connectionID: string; @Input() selectedTableName: string; @Input() selectedTableDisplayName: string; + @Input() tableTypes: any; // @Input() savedFilterData: any; @Output() filterSelected = new EventEmitter(); @@ -42,17 +56,31 @@ export class SavedFiltersPanelComponent implements OnInit { public selectedFilterSetId: string | null = null; public selectedFilter: any = null; + public tableStructure: any = null; + public tableRowFieldsShown: { [key: string]: any } = {}; + public tableRowStructure: { [key: string]: any } = {}; + public tableForeignKeys: any[] = []; + public tableWidgets: { [key: string]: any } = {}; + public tableWidgetsList: string[] = []; + public UIwidgets = UIwidgets; + public displayedComparators = { eq: "=", gt: ">", lt: "<", gte: ">=", - lte: "<=" + lte: "<=", + startswith: "starts with", + endswith: "ends with", + contains: "contains", + icontains: "not contains", + empty: "is empty" } constructor( private dialog: MatDialog, private _tables: TablesService, + private _connections: ConnectionsService, private router: Router, private route: ActivatedRoute ) {} @@ -108,11 +136,6 @@ export class SavedFiltersPanelComponent implements OnInit { }); } - /** - * Transform the filters object into an array of entries for display - * @param filters The filters object from saved filter data - * @returns Array of objects with column, operator, and value - */ getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { if (!filters) return []; @@ -133,11 +156,6 @@ export class SavedFiltersPanelComponent implements OnInit { return entries; } - /** - * Select a filter and emit the selection event - * @param index Index of the selected filter - */ - selectFiltersSet(selectedFilterSetId: string): void { this.selectedFilterSetId = selectedFilterSetId; const selectedFilter = this.savedFilterMap[selectedFilterSetId]; @@ -171,7 +189,7 @@ export class SavedFiltersPanelComponent implements OnInit { }); } - selectFilter(entry) { + selectFilter(entry: { column: string; operator: string; value: any }) { this.selectedFilter = entry; } @@ -193,4 +211,75 @@ export class SavedFiltersPanelComponent implements OnInit { return `${displayedName} ${this.displayedComparators[comparator]} ${filterValue}` } } + + get inputs() { + return filterTypes[this._connections.currentConnection.type]; + } + + isWidget(columnName: string) { + return this.tableWidgetsList.includes(columnName); + } + + getInputType(field: string) { + let widgetType; + if (this.isWidget(field)) { + widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; + } else { + widgetType = this.inputs[this.tableTypes[field]]?.type; + } + return widgetType; + } + + getComparatorType(typeOfComponent) { + if (typeOfComponent === 'text') { + return 'text'; + } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { + return 'number'; + } else { + return 'nonComparable'; + } + } + + updateField = (updatedValue: any, field: string) => { + this.selectedFilter.value = updatedValue; + } + + updateComparator(event: string) { + this.selectedFilter.operator = event; + } + + cancelEditFilter() { + this.selectedFilter = null; + } + + 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} + }; + }) + ); + } + + // Helper method to track objects in ngFor + trackByFn(index: number, item: any) { + return item.key; + } + + // Save the edited filter + applyEditedFilter() { + console.log('Applying edited filter:', this.selectedFilter); + } } From 9efc57a0c6f80b3c88279de971fc19915627318c Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 4 Aug 2025 02:51:59 +0300 Subject: [PATCH 07/29] saved filters: - show filters menu; - display filters in edit popup; - remove filters. --- .../db-table-filters-dialog.component.ts | 2 + .../saved-filters-dialog.component.html | 15 +++--- .../saved-filters-dialog.component.ts | 52 ++++++++++++------- .../saved-filters-panel.component.html | 21 +++++++- .../saved-filters-panel.component.ts | 7 ++- frontend/src/app/services/tables.service.ts | 28 +++++++++- 6 files changed, 94 insertions(+), 31 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 fb9d016fb..0b6c04b0d 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 @@ -95,6 +95,8 @@ export class DbTableFiltersDialogComponent implements OnInit { 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; diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index 8da38d2d5..4a7525e34 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -13,8 +13,10 @@

Filter Name - - + + Filter name is required @@ -152,18 +154,17 @@

Define Filters

- 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 8606a495b..8e1956258 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 @@ -44,10 +44,10 @@ import { omitBy } from 'lodash'; styleUrl: './saved-filters-dialog.component.css' }) export class SavedFiltersDialogComponent implements OnInit { - @Input() connectionID: string; - @Input() tableName: string; - @Input() displayTableName: string; - @Input() savedFilterData: any; + // @Input() connectionID: string; + // @Input() tableName: string; + // @Input() displayTableName: string; + // @Input() filtersSet: any; public tableFilters = []; public fieldSearchControl = new FormControl(''); @@ -65,13 +65,6 @@ export class SavedFiltersDialogComponent implements OnInit { public tableWidgetsList: string[] = []; public UIwidgets = UIwidgets; - // Form for saving filters - public filterForm = new FormGroup({ - filterName: new FormControl('', [Validators.required]), - filterDescription: new FormControl(''), - isDefault: new FormControl(false) - }); - constructor( @Inject(MAT_DIALOG_DATA) public data: any, private _tables: TablesService, @@ -83,6 +76,20 @@ export class SavedFiltersDialogComponent implements OnInit { 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]; + acc[field] = value; + return acc; + }, {}); + + this.tableRowFieldsComparator = Object.entries(this.data.filtersSet.filters).reduce((acc, [field, conditions]) => { + const [comparator] = Object.keys(conditions); + acc[field] = comparator; + return acc; + }, {}); + } + // Load table structure this._tables.fetchTableStructure(this.data.connectionID, this.data.tableName).subscribe({ next: (structure) => { @@ -168,11 +175,16 @@ export class SavedFiltersDialogComponent implements OnInit { if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; } - resetFilters() { - this.tableFilters = []; - this.tableRowFieldsShown = {}; - this.tableRowFieldsComparator = {}; - this.updateFiltersCount(); + removeFilters() { + this._tables.deleteSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id).subscribe({ + next: () => { + this.dialogRef.close(true); + }, + error: (error) => { + console.error('Error removing filters:', error); + this.snackBar.open('Error removing filters', 'Close', { duration: 3000 }); + } + }); } getInputType(field: string) { @@ -206,9 +218,9 @@ export class SavedFiltersDialogComponent implements OnInit { } saveFilter() { - if (!this.filterForm.valid || this.tableFiltersCount === 0) { - return; - } + // if (!this.filterForm.valid || this.tableFiltersCount === 0) { + // return; + // } // Prepare filter data to save // const filterToSave = { @@ -235,7 +247,7 @@ export class SavedFiltersDialogComponent implements OnInit { // const filters = JsonURL.stringify( this.filters ); - this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, {name: this.filterForm.get('filterName').value, filters}) + this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, {name: this.data.filtersSet.name, filters}) .subscribe(() => { this.dialogRef.close(true); }, (error) => { 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 26618d008..3ec84f4d7 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 @@ -1,9 +1,28 @@
- + + + + + +
info No saved filters. Create one by clicking the button above. 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 7d9a9d214..9d216c1dc 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 @@ -124,14 +124,17 @@ export class SavedFiltersPanelComponent implements OnInit { }); } - handleOpenSavedFiltersDialog() { + handleOpenSavedFiltersDialog(filtersSet: any = null) { this.dialog.open(SavedFiltersDialogComponent, { width: '56em', data: { connectionID: this.connectionID, tableName: this.selectedTableName, displayTableName: this.selectedTableDisplayName, - savedFilterData: this.savedFilterData + filtersSet: filtersSet ? filtersSet : { + name: '', + filters: {} + } } }); } diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index 02ae0de3d..ee92408b8 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -552,5 +552,31 @@ export class TablesService { return EMPTY; }) ) - } + } + + deleteSavedFilter(connectionID: string, tableName: string, filterId: string) { + return this._http.delete(`/table-filters/${connectionID}/${filterId}`, { + params: { + tableName + } + }) + .pipe( + map(res => { + this.tables.next('delete saved filters'); + this._notifications.showSuccessSnackbar('Saved filter has been deleted.') + return res + }), + catchError((err) => { + console.log(err); + this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (id: number) => this._notifications.dismissAlert() + } + ]); + return EMPTY; + }) + ) + } } From 0d40c60eaaf623f67148244d84b38f52f168f29e Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 4 Aug 2025 17:18:14 +0300 Subject: [PATCH 08/29] saved filters, edit dialog: pass foreign keys and fix foreign key field. --- .../db-table-view.component.html | 3 + .../saved-filters-dialog.component.html | 6 +- .../saved-filters-dialog.component.ts | 67 +++++++++---------- .../saved-filters-panel.component.ts | 14 +++- 4 files changed, 48 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index ecbbc1bfe..6bb473391 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -183,6 +183,9 @@

{{ displayName }}

[connectionID]="connectionID" [selectedTableName]="name" [selectedTableDisplayName]="displayName" + [structure]="tableData.structure" + [tableWidgets]="tableData.widgets" + [tableForeignKeys]="tableData.foreignKeys" [tableTypes]="tableData.tableTypes" (filterSelected)="applyFilter.emit($event)" > diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index 4a7525e34..d1bde0380 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -49,7 +49,7 @@

Define Filters

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 + relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateField, args: ['$event', value.key] } @@ -64,7 +64,7 @@

Define Filters

label: value.key, value: tableRowFieldsShown[value.key], structure: tableRowStructure[value.key], - relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined + relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateField, args: ['$event', value.key] } @@ -132,7 +132,7 @@

Define Filters

value: tableRowFieldsShown[value.key], readonly: tableRowFieldsComparator[value.key] === 'empty', structure: tableRowStructure[value.key], - relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined + relations: tableTypes[value.key] === 'foreign key' ? data.tableForeignKeys[value.key] : undefined }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateField, args: ['$event', value.key] } 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 8e1956258..5ecf20b91 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 @@ -58,7 +58,7 @@ export class SavedFiltersDialogComponent implements OnInit { public tableRowStructure: Object; public tableRowFieldsShown: Object = {}; public tableRowFieldsComparator: Object = {}; - public tableForeignKeys: {[key: string]: TableForeignKey}; + // public tableForeignKeys: {[key: string]: TableForeignKey}; public tableFiltersCount: number = 0; public tableTypes: Object; public tableWidgets: object; @@ -90,35 +90,27 @@ export class SavedFiltersDialogComponent implements OnInit { }, {}); } - // Load table structure - this._tables.fetchTableStructure(this.data.connectionID, this.data.tableName).subscribe({ - next: (structure) => { - this.tableForeignKeys = {...structure.foreignKeys}; - this.tableRowFields = Object.assign({}, ...structure.structure.map((field: TableField) => ({[field.column_name]: undefined}))); - const foreignKeysList = structure.foreignKeys.map((fk: TableForeignKey) => fk.column_name); - this.tableTypes = getTableTypes(structure.structure, foreignKeysList); - this.fields = structure.structure - .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') - .map((field: TableField) => field.column_name); - - this.tableRowStructure = Object.assign({}, ...structure.structure.map((field: TableField) => { - return {[field.column_name]: field}; - })); - - // Setup widgets if available - if (structure.widgets && structure.widgets.length) { - this.setWidgets(structure.widgets); - } + // this.tableForeignKeys = {...this.data.structure.foreignKeys}; + this.tableRowFields = Object.assign({}, ...this.data.structure.map((field: TableField) => ({[field.column_name]: undefined}))); + const foreignKeysList = Object.keys(this.data.tableForeignKeys); + this.tableTypes = getTableTypes(this.data.structure, foreignKeysList); + this.fields = this.data.structure + .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') + .map((field: TableField) => field.column_name); + + this.tableRowStructure = Object.assign({}, ...this.data.structure.map((field: TableField) => { + return {[field.column_name]: field}; + })); + + // Setup widgets if available + if (this.data.tableWidgets && this.data.tableWidgets.length) { + this.setWidgets(this.data.tableWidgets); + } - this.foundFields = this.fieldSearchControl.valueChanges.pipe( - startWith(''), - map(value => this._filter(value || '')), - ); - }, - error: (err) => { - console.error('Error loading table structure:', err); - } - }); + this.foundFields = this.fieldSearchControl.valueChanges.pipe( + startWith(''), + map(value => this._filter(value || '')), + ); } private _filter(value: string): string[] { @@ -172,6 +164,7 @@ export class SavedFiltersDialogComponent implements OnInit { } updateComparator(event, fieldName: string) { + console.log('Updating comparator for field:', fieldName, 'obj', this.tableRowFieldsComparator); if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; } @@ -233,16 +226,16 @@ export class SavedFiltersDialogComponent implements OnInit { // comparators: this.tableRowFieldsComparator // }; - const nonEmptyFilters = omitBy(this.tableRowFieldsShown, (value) => value === undefined); + // const nonEmptyFilters = omitBy(this.tableRowFieldsShown, (value) => value === undefined); - if (Object.keys(nonEmptyFilters).length) { + if (Object.keys(this.tableRowFieldsShown).length) { let filters = {}; - for (const key in nonEmptyFilters) { - if (this.tableRowFieldsComparator[key] !== undefined) { - filters[key] = { - [this.tableRowFieldsComparator[key]]: nonEmptyFilters[key] - }; - } + for (const key in this.tableRowFieldsShown) { + if (this.tableRowFieldsComparator[key] !== undefined) { + filters[key] = { + [this.tableRowFieldsComparator[key]]: this.tableRowFieldsShown[key] + }; + } } // const filters = JsonURL.stringify( this.filters ); 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 9d216c1dc..b5aec8601 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,6 +17,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; +import { TableForeignKey } from 'src/app/models/table'; import { TablesService } from 'src/app/services/tables.service'; import { UIwidgets } from 'src/app/consts/record-edit-types'; import { filterTypes } from 'src/app/consts/filter-types'; @@ -46,6 +47,9 @@ export class SavedFiltersPanelComponent implements OnInit { @Input() selectedTableName: string; @Input() selectedTableDisplayName: string; @Input() tableTypes: any; + @Input() structure: any; + @Input() tableForeignKeys: TableForeignKey[] = []; + @Input() tableWidgets: any = {}; // @Input() savedFilterData: any; @Output() filterSelected = new EventEmitter(); @@ -59,8 +63,8 @@ export class SavedFiltersPanelComponent implements OnInit { public tableStructure: any = null; public tableRowFieldsShown: { [key: string]: any } = {}; public tableRowStructure: { [key: string]: any } = {}; - public tableForeignKeys: any[] = []; - public tableWidgets: { [key: string]: any } = {}; + // public tableForeignKeys: any[] = []; + // public tableWidgets: { [key: string]: any } = {}; public tableWidgetsList: string[] = []; public UIwidgets = UIwidgets; @@ -131,6 +135,9 @@ export class SavedFiltersPanelComponent implements OnInit { connectionID: this.connectionID, tableName: this.selectedTableName, displayTableName: this.selectedTableDisplayName, + structure: this.structure, + tableForeignKeys: this.tableForeignKeys, + tableWidgets: this.tableWidgets, filtersSet: filtersSet ? filtersSet : { name: '', filters: {} @@ -147,6 +154,8 @@ export class SavedFiltersPanelComponent implements OnInit { Object.keys(filters).forEach(column => { const operations = filters[column]; + console.log('Operations for column:', column, operations); + Object.keys(operations).forEach(operator => { entries.push({ column, @@ -156,6 +165,7 @@ export class SavedFiltersPanelComponent implements OnInit { }); }); + console.log('Filter entries:', entries); return entries; } From 3ea68b05b1aae6558877de832fb78f3fc63cf1a0 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 4 Aug 2025 21:39:31 +0300 Subject: [PATCH 09/29] saved filters: add dynamic_column --- .../edit-filter-dialog.component.ts | 48 ++++ .../saved-filters-dialog.component.css | 22 +- .../saved-filters-dialog.component.html | 10 +- .../saved-filters-dialog.component.spec.ts | 165 ++++++++++++- .../saved-filters-dialog.component.ts | 48 +++- .../saved-filters-panel.component.css | 63 +++++ .../saved-filters-panel.component.html | 106 ++++---- .../saved-filters-panel.component.spec.ts | 103 +++++++- .../saved-filters-panel.component.ts | 233 ++++++++++++++++-- .../test-menu.component.html | 18 ++ 10 files changed, 738 insertions(+), 78 deletions(-) create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts create mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts new file mode 100644 index 000000000..c467e6edc --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts @@ -0,0 +1,48 @@ +import { Component, Inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { normalizeTableName } from 'src/app/lib/normalize'; + +@Component({ + selector: 'app-edit-filter-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatDialogModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule + ], + template: ` +

Edit {{ normalizeTableName(data.entry.column) }} filter

+ + + Filter value + + + + + + + + `, + styles: [` + mat-dialog-content { + min-width: 300px; + padding-top: 10px; + } + `] +}) +export class EditFilterDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: {entry: {column: string, operator: string, value: any}} + ) {} + + normalizeTableName = normalizeTableName; +} diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index 611eed465..09df26ffb 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -7,14 +7,14 @@ .filters-content { display: grid; - grid-template-columns: auto 228px 0 1fr 32px; + grid-template-columns: auto 228px 0 1fr 160px 32px; grid-column-gap: 8px; align-content: flex-start; align-items: flex-start; } .filters-select { - grid-column: 1 / span 5; + grid-column: 1 / span 6; margin-bottom: 16px; } @@ -22,18 +22,30 @@ grid-column: 1 / span 4; } +.dynamic-column-radio { + grid-column: 5; + margin-top: 14px; +} + .filter-save-form { - grid-column: 1 / span 5; + grid-column: 1 / span 6; margin-bottom: 24px; } .section-title { - grid-column: 1 / span 5; + grid-column: 1 / span 6; margin: 16px 0 8px 0; color: rgba(0, 0, 0, 0.87); font-weight: 500; } +.section-description { + grid-column: 1 / span 6; + margin: 0 0 16px 0; + color: rgba(0, 0, 0, 0.6); + font-size: 14px; +} + .full-width { width: 100%; } @@ -75,7 +87,7 @@ } .no-filters-message { - grid-column: 1 / span 5; + grid-column: 1 / span 6; color: rgba(0, 0, 0, 0.6); font-style: italic; margin: 16px 0; diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index d1bde0380..51ea64c3f 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -21,7 +21,8 @@

-

Define Filters

+

Define Filters and Dynamic Column

+

Select filters to apply and optionally mark one column as dynamic

@@ -139,6 +140,13 @@

Define Filters

}" > +
+ + Dynamic column + +
-
+
+ +
+

Filters:

+ + {{ getFilter(filter) }} + +
-
+ +
+

Dynamic Column:

+
- {{ selectedFilter.column }} + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} - - + + (ngModelChange)="updateDynamicColumnComparator($event)"> starts with ends with equal @@ -69,10 +67,10 @@

Edit Filter: {{ selectedFilter.column }}

- - + + (ngModelChange)="updateDynamicColumnComparator($event)"> equal greater than less than @@ -82,64 +80,68 @@

Edit Filter: {{ selectedFilter.column }}

-
-
- +
+
- - +
-
- +
-
- +
- +
+
+ + +
+ No filters to display.
\ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts index 6dba45b46..1dffa9a12 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts @@ -1,23 +1,120 @@ +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; import { SavedFiltersPanelComponent } from './saved-filters-panel.component'; +import { TablesService } from 'src/app/services/tables.service'; +import { of } from 'rxjs'; describe('SavedFiltersPanelComponent', () => { let component: SavedFiltersPanelComponent; let fixture: ComponentFixture; + let tablesServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + + const mockFilter = { + id: 'filter1', + name: 'Test Filter', + filters: { + name: { eq: 'John' }, + age: { gt: 25 } + }, + dynamic_column: { + column_name: 'city', + comparator: 'eq' + } + }; beforeEach(async () => { + const tablesServiceMock = jasmine.createSpyObj('TablesService', ['getSavedFilters', 'createSavedFilter']); + tablesServiceMock.getSavedFilters.and.returnValue(of([mockFilter])); + tablesServiceMock.cast = of({}); + + const routerMock = jasmine.createSpyObj('Router', ['navigate']); + + const activatedRouteMock = { + queryParams: of({}), + snapshot: { + queryParams: {} + } + }; + + const matDialogMock = jasmine.createSpyObj('MatDialog', ['open']); + await TestBed.configureTestingModule({ - imports: [SavedFiltersPanelComponent] - }) - .compileComponents(); + imports: [SavedFiltersPanelComponent], + providers: [ + { provide: TablesService, useValue: tablesServiceMock }, + { provide: Router, useValue: routerMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: MatDialog, useValue: matDialogMock } + ] + }).compileComponents(); + tablesServiceSpy = TestBed.inject(TablesService) as jasmine.SpyObj; + routerSpy = TestBed.inject(Router) as jasmine.SpyObj; + fixture = TestBed.createComponent(SavedFiltersPanelComponent); component = fixture.componentInstance; + component.connectionID = 'conn1'; + component.selectedTableName = 'users'; + component.structure = []; + component.tableTypes = {}; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should process filters data to separate static filters and dynamic column', () => { + const result = component.processFiltersData(mockFilter); + + expect(result.dynamicColumn).toBeTruthy(); + expect(result.dynamicColumn?.column).toBe('city'); + expect(result.dynamicColumn?.operator).toBe('eq'); + + expect(result.staticFilters.length).toBe(2); + expect(result.staticFilters[0].column).toBe('name'); + expect(result.staticFilters[1].column).toBe('age'); + }); + + it('should update dynamic column comparator', () => { + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } + } + }; + + component.updateDynamicColumnComparator('contains'); + + expect(component.savedFilterMap.filter1.dynamicColumn.operator).toBe('contains'); + }); + + it('should set value to empty string when comparator is empty', () => { + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } + } + }; + + component.updateDynamicColumnComparator('empty'); + + expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe(''); + }); + + it('should update dynamic column value', () => { + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } + } + }; + + component.updateDynamicColumnValue('Chicago'); + + expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe('Chicago'); + }); }); 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 b5aec8601..3f93fb7b4 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 @@ -94,11 +94,8 @@ export class SavedFiltersPanelComponent implements OnInit { next: (data) => { this.savedFilterData = data; this.savedFilterMap = Object.assign({}, ...data.map((filter, index) => { - // Create a copy of the filter with transformed filters array - const transformedFilter = { - ...filter, - filterEntries: this.getFilterEntries(filter.filters) - }; + // Process the filter to separate static filters and dynamic column + const transformedFilter = this.processFiltersData(filter); return { [filter.id]: transformedFilter }; })); console.log('Saved filters map:', this.savedFilterMap); @@ -169,6 +166,42 @@ export class SavedFiltersPanelComponent implements OnInit { return entries; } + /** + * Process filters data to separate static filters and dynamic column + */ + processFiltersData(filter: any) { + // Create a transformed filter object with processed data + const transformedFilter = { + ...filter, + filterEntries: this.getFilterEntries(filter.filters), + staticFilters: [] as { column: string; operator: string; value: any }[], + dynamicColumn: null as { column: string; operator: string; value: any } | null + }; + + // Process filter entries to separate static filters and dynamic column + if (filter.dynamic_column && filter.dynamic_column.column_name) { + // If we have a dynamic column, create it from the dynamic_column property + transformedFilter.dynamicColumn = { + column: filter.dynamic_column.column_name, + operator: filter.dynamic_column.comparator, + value: null // The actual value will come from filters if available + }; + + // If there's a corresponding filter entry for the dynamic column, use its value + const dynamicColFilters = filter.filters && filter.filters[filter.dynamic_column.column_name]; + if (dynamicColFilters) { + const operator = filter.dynamic_column.comparator; + transformedFilter.dynamicColumn.value = dynamicColFilters[operator]; + } + } + + // Create the static filters array (excluding the dynamic column) + transformedFilter.staticFilters = this.getFilterEntries(filter.filters) + .filter(entry => !filter.dynamic_column || entry.column !== filter.dynamic_column.column_name); + + return transformedFilter; + } + selectFiltersSet(selectedFilterSetId: string): void { this.selectedFilterSetId = selectedFilterSetId; const selectedFilter = this.savedFilterMap[selectedFilterSetId]; @@ -188,6 +221,14 @@ export class SavedFiltersPanelComponent implements OnInit { saved_filter: selectedFilter.name // Add the saved filter name to the URL }; + // Add dynamic_column to query params if it exists + if (selectedFilter.dynamicColumn) { + queryParams.dynamic_column = JsonURL.stringify({ + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }); + } + // Only add sorting params if they exist if (currentParams['sort_active']) { queryParams.sort_active = currentParams['sort_active']; @@ -253,17 +294,17 @@ export class SavedFiltersPanelComponent implements OnInit { } } - updateField = (updatedValue: any, field: string) => { - this.selectedFilter.value = updatedValue; - } + // updateField = (updatedValue: any, field: string) => { + // this.selectedFilter.value = updatedValue; + // } - updateComparator(event: string) { - this.selectedFilter.operator = event; - } + // updateComparator(event: string) { + // this.selectedFilter.operator = event; + // } - cancelEditFilter() { - this.selectedFilter = null; - } + // cancelEditFilter() { + // this.selectedFilter = null; + // } setWidgets(widgets: any[]) { this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); @@ -295,4 +336,168 @@ export class SavedFiltersPanelComponent implements OnInit { applyEditedFilter() { console.log('Applying edited filter:', this.selectedFilter); } + + /** + * Update dynamic column comparator + * This is defined as an arrow function to ensure 'this' is bound correctly when called from template + */ + updateDynamicColumnComparator = (comparator: string) => { + console.log('Updating comparator:', comparator); + console.log('Current savedFilterMap:', this.savedFilterMap); + console.log('Current selectedFilterSetId:', this.selectedFilterSetId); + + // Check for all potential undefined/null values in the chain + if (!this.savedFilterMap) { + console.error('savedFilterMap is undefined'); + return; + } + + if (!this.selectedFilterSetId) { + console.error('selectedFilterSetId is null or undefined'); + return; + } + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + if (!selectedFilter) { + console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); + return; + } + + if (!selectedFilter.dynamicColumn) { + console.error('dynamicColumn is null or undefined for the selected filter'); + return; + } + + // If we get here, it's safe to update the comparator + selectedFilter.dynamicColumn.operator = comparator; + + // If comparator is 'empty', set value to empty string + if (comparator === 'empty') { + selectedFilter.dynamicColumn.value = ''; + } + + console.log('Updated dynamic column comparator. New state:', selectedFilter.dynamicColumn); + } + + /** + * Update dynamic column value + * This is defined as an arrow function to ensure 'this' is bound correctly when called from ndc-dynamic + */ + updateDynamicColumnValue = (value: any) => { + console.log('Updating dynamic column value:', value); + console.log('Current savedFilterMap:', this.savedFilterMap); + console.log('Current selectedFilterSetId:', this.selectedFilterSetId); + + // Check for all potential undefined/null values in the chain + if (!this.savedFilterMap) { + console.error('savedFilterMap is undefined'); + return; + } + + if (!this.selectedFilterSetId) { + console.error('selectedFilterSetId is null or undefined'); + return; + } + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + if (!selectedFilter) { + console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); + return; + } + + if (!selectedFilter.dynamicColumn) { + console.error('dynamicColumn is null or undefined for the selected filter'); + return; + } + + // If we get here, it's safe to update the value + selectedFilter.dynamicColumn.value = value; + console.log('Updated dynamic column value. New state:', selectedFilter.dynamicColumn); + } + + /** + * Apply changes to the dynamic column and rerender the table + * This doesn't affect saved filter settings + */ + applyDynamicColumnChanges() { + if (!this.selectedFilterSetId) return; + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + + console.log('Applying dynamic column changes for filter selectedFilter:', selectedFilter); + + if (!selectedFilter || !selectedFilter.dynamicColumn) return; + + // Get the current query params + const currentParams = this.route.snapshot.queryParams; + + // Create dynamic column object for the query params + const dynamicColumn = { + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }; + + // Create filter value for the dynamic column + const filterValue = selectedFilter.dynamicColumn.value === '' || + selectedFilter.dynamicColumn.value === undefined ? + null : selectedFilter.dynamicColumn.value; + + // Create a copy of the original filters + const filters = { ...selectedFilter.filters }; + + console.log('dynamicColumn:', selectedFilter.dynamicColumn); + + // Only add the dynamic column to filters if it has a non-null value + if (selectedFilter.dynamicColumn.column && + selectedFilter.dynamicColumn.operator && + filterValue !== null) { + // Add or update the filter for the dynamic column + filters[selectedFilter.dynamicColumn.column] = { + [selectedFilter.dynamicColumn.operator]: filterValue + }; + } else { + // If the dynamic column exists in filters, remove it + if (filters[selectedFilter.dynamicColumn.column]) { + delete filters[selectedFilter.dynamicColumn.column]; + } + } + + console.log('Applying dynamic column changes, add dynamicColumn to filters:', filters); + + // Update the URL with the updated filter data + const queryParams: any = { + filters: JsonURL.stringify(filters), + dynamic_column: JsonURL.stringify(dynamicColumn), + page_index: 0, // Reset to first page + page_size: currentParams['page_size'] || 30, + saved_filter: selectedFilter.name + }; + + // Only add sorting params if they exist + if (currentParams['sort_active']) { + queryParams.sort_active = currentParams['sort_active']; + } + + if (currentParams['sort_direction']) { + queryParams.sort_direction = currentParams['sort_direction']; + } + + // Create updated filter data for rendering the table with dynamic column merged into filters + const updatedFilterData = { + ...selectedFilter, + filters: filters, // Filters already include the dynamic column from previous logic + dynamic_column: dynamicColumn + }; + + // Also ensure we keep the original filter entries format updated + updatedFilterData.filterEntries = this.getFilterEntries(filters); + + // Emit the updated filter to update the table view + this.filterSelected.emit(updatedFilterData); + + // Navigate with updated query params to rerender the table + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { + queryParams + }); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html new file mode 100644 index 000000000..926f6e198 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html @@ -0,0 +1,18 @@ +
+

Test Menu Component

+ + + +
+ + + +
+ + +
+
+
+
From 612a5ed76f5e1ecf42ca88fba07fcc8014f97289 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 5 Aug 2025 18:15:45 +0300 Subject: [PATCH 10/29] remove commented lines and consolelogs --- .../saved-filters-panel.component.ts | 101 ++---------------- 1 file changed, 6 insertions(+), 95 deletions(-) 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 3f93fb7b4..6db079238 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 @@ -63,8 +63,6 @@ export class SavedFiltersPanelComponent implements OnInit { public tableStructure: any = null; public tableRowFieldsShown: { [key: string]: any } = {}; public tableRowStructure: { [key: string]: any } = {}; - // public tableForeignKeys: any[] = []; - // public tableWidgets: { [key: string]: any } = {}; public tableWidgetsList: string[] = []; public UIwidgets = UIwidgets; @@ -93,28 +91,22 @@ export class SavedFiltersPanelComponent implements OnInit { this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ next: (data) => { this.savedFilterData = data; - this.savedFilterMap = Object.assign({}, ...data.map((filter, index) => { - // Process the filter to separate static filters and dynamic column + this.savedFilterMap = Object.assign({}, ...data.map((filter) => { const transformedFilter = this.processFiltersData(filter); return { [filter.id]: transformedFilter }; })); - console.log('Saved filters map:', this.savedFilterMap); - // Check if there's a saved_filter parameter in the URL this.route.queryParams.subscribe(params => { const savedFilterName = params['saved_filter']; if (savedFilterName && this.savedFilterData.length > 0) { - // Find the index of the saved filter with the matching name const filterIndex = this.savedFilterData.findIndex(filter => filter.name === savedFilterName); - // If found, select it if (filterIndex !== -1) { this.selectedFilterIndex = filterIndex; this.filterSelected.emit(this.savedFilterData[filterIndex]); } } else { - // No filter specified in URL, keep selectedFilterIndex as -1 (none selected) this.selectedFilterIndex = -1; } }); @@ -150,9 +142,6 @@ export class SavedFiltersPanelComponent implements OnInit { Object.keys(filters).forEach(column => { const operations = filters[column]; - - console.log('Operations for column:', column, operations); - Object.keys(operations).forEach(operator => { entries.push({ column, @@ -166,11 +155,7 @@ export class SavedFiltersPanelComponent implements OnInit { return entries; } - /** - * Process filters data to separate static filters and dynamic column - */ processFiltersData(filter: any) { - // Create a transformed filter object with processed data const transformedFilter = { ...filter, filterEntries: this.getFilterEntries(filter.filters), @@ -178,16 +163,13 @@ export class SavedFiltersPanelComponent implements OnInit { dynamicColumn: null as { column: string; operator: string; value: any } | null }; - // Process filter entries to separate static filters and dynamic column if (filter.dynamic_column && filter.dynamic_column.column_name) { - // If we have a dynamic column, create it from the dynamic_column property transformedFilter.dynamicColumn = { column: filter.dynamic_column.column_name, operator: filter.dynamic_column.comparator, - value: null // The actual value will come from filters if available + value: null }; - // If there's a corresponding filter entry for the dynamic column, use its value const dynamicColFilters = filter.filters && filter.filters[filter.dynamic_column.column_name]; if (dynamicColFilters) { const operator = filter.dynamic_column.comparator; @@ -195,7 +177,6 @@ export class SavedFiltersPanelComponent implements OnInit { } } - // Create the static filters array (excluding the dynamic column) transformedFilter.staticFilters = this.getFilterEntries(filter.filters) .filter(entry => !filter.dynamic_column || entry.column !== filter.dynamic_column.column_name); @@ -207,21 +188,17 @@ export class SavedFiltersPanelComponent implements OnInit { const selectedFilter = this.savedFilterMap[selectedFilterSetId]; this.filterSelected.emit(selectedFilter); - // Get the current query params const currentParams = this.route.snapshot.queryParams; - // Update the URL with the filter data and saved filter name const filters = JsonURL.stringify(selectedFilter.filters); - // Create query params object const queryParams: any = { filters, page_index: 0, - page_size: currentParams['page_size'] || 30, // Use current page size or default - saved_filter: selectedFilter.name // Add the saved filter name to the URL + page_size: currentParams['page_size'] || 30, + saved_filter: selectedFilter.name }; - // Add dynamic_column to query params if it exists if (selectedFilter.dynamicColumn) { queryParams.dynamic_column = JsonURL.stringify({ column_name: selectedFilter.dynamicColumn.column, @@ -229,7 +206,6 @@ export class SavedFiltersPanelComponent implements OnInit { }); } - // Only add sorting params if they exist if (currentParams['sort_active']) { queryParams.sort_active = currentParams['sort_active']; } @@ -294,18 +270,6 @@ export class SavedFiltersPanelComponent implements OnInit { } } - // updateField = (updatedValue: any, field: string) => { - // this.selectedFilter.value = updatedValue; - // } - - // updateComparator(event: string) { - // this.selectedFilter.operator = event; - // } - - // cancelEditFilter() { - // this.selectedFilter = null; - // } - setWidgets(widgets: any[]) { this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); this.tableWidgets = Object.assign({}, ...widgets @@ -327,26 +291,11 @@ export class SavedFiltersPanelComponent implements OnInit { ); } - // Helper method to track objects in ngFor trackByFn(index: number, item: any) { return item.key; } - // Save the edited filter - applyEditedFilter() { - console.log('Applying edited filter:', this.selectedFilter); - } - - /** - * Update dynamic column comparator - * This is defined as an arrow function to ensure 'this' is bound correctly when called from template - */ updateDynamicColumnComparator = (comparator: string) => { - console.log('Updating comparator:', comparator); - console.log('Current savedFilterMap:', this.savedFilterMap); - console.log('Current selectedFilterSetId:', this.selectedFilterSetId); - - // Check for all potential undefined/null values in the chain if (!this.savedFilterMap) { console.error('savedFilterMap is undefined'); return; @@ -368,27 +317,14 @@ export class SavedFiltersPanelComponent implements OnInit { return; } - // If we get here, it's safe to update the comparator selectedFilter.dynamicColumn.operator = comparator; - // If comparator is 'empty', set value to empty string if (comparator === 'empty') { selectedFilter.dynamicColumn.value = ''; } - - console.log('Updated dynamic column comparator. New state:', selectedFilter.dynamicColumn); } - /** - * Update dynamic column value - * This is defined as an arrow function to ensure 'this' is bound correctly when called from ndc-dynamic - */ updateDynamicColumnValue = (value: any) => { - console.log('Updating dynamic column value:', value); - console.log('Current savedFilterMap:', this.savedFilterMap); - console.log('Current selectedFilterSetId:', this.selectedFilterSetId); - - // Check for all potential undefined/null values in the chain if (!this.savedFilterMap) { console.error('savedFilterMap is undefined'); return; @@ -410,15 +346,9 @@ export class SavedFiltersPanelComponent implements OnInit { return; } - // If we get here, it's safe to update the value selectedFilter.dynamicColumn.value = value; - console.log('Updated dynamic column value. New state:', selectedFilter.dynamicColumn); } - /** - * Apply changes to the dynamic column and rerender the table - * This doesn't affect saved filter settings - */ applyDynamicColumnChanges() { if (!this.selectedFilterSetId) return; @@ -428,43 +358,29 @@ export class SavedFiltersPanelComponent implements OnInit { if (!selectedFilter || !selectedFilter.dynamicColumn) return; - // Get the current query params const currentParams = this.route.snapshot.queryParams; - // Create dynamic column object for the query params const dynamicColumn = { column_name: selectedFilter.dynamicColumn.column, comparator: selectedFilter.dynamicColumn.operator }; - // Create filter value for the dynamic column - const filterValue = selectedFilter.dynamicColumn.value === '' || - selectedFilter.dynamicColumn.value === undefined ? - null : selectedFilter.dynamicColumn.value; + const filterValue = selectedFilter.dynamicColumn.value === '' || selectedFilter.dynamicColumn.value === undefined ? null : selectedFilter.dynamicColumn.value; - // Create a copy of the original filters const filters = { ...selectedFilter.filters }; - console.log('dynamicColumn:', selectedFilter.dynamicColumn); - - // Only add the dynamic column to filters if it has a non-null value if (selectedFilter.dynamicColumn.column && selectedFilter.dynamicColumn.operator && filterValue !== null) { - // Add or update the filter for the dynamic column filters[selectedFilter.dynamicColumn.column] = { [selectedFilter.dynamicColumn.operator]: filterValue }; } else { - // If the dynamic column exists in filters, remove it if (filters[selectedFilter.dynamicColumn.column]) { delete filters[selectedFilter.dynamicColumn.column]; } } - console.log('Applying dynamic column changes, add dynamicColumn to filters:', filters); - - // Update the URL with the updated filter data const queryParams: any = { filters: JsonURL.stringify(filters), dynamic_column: JsonURL.stringify(dynamicColumn), @@ -473,7 +389,6 @@ export class SavedFiltersPanelComponent implements OnInit { saved_filter: selectedFilter.name }; - // Only add sorting params if they exist if (currentParams['sort_active']) { queryParams.sort_active = currentParams['sort_active']; } @@ -482,20 +397,16 @@ export class SavedFiltersPanelComponent implements OnInit { queryParams.sort_direction = currentParams['sort_direction']; } - // Create updated filter data for rendering the table with dynamic column merged into filters const updatedFilterData = { ...selectedFilter, - filters: filters, // Filters already include the dynamic column from previous logic + filters: filters, dynamic_column: dynamicColumn }; - // Also ensure we keep the original filter entries format updated updatedFilterData.filterEntries = this.getFilterEntries(filters); - // Emit the updated filter to update the table view this.filterSelected.emit(updatedFilterData); - // Navigate with updated query params to rerender the table this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); From dbfc183f659cce3a872025460f53b6f3209b5844 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 6 Aug 2025 18:38:35 +0300 Subject: [PATCH 11/29] saved filters: - remove filter selection through index; - clear filters on selecting already active filter --- .../saved-filters-panel.component.ts | 100 ++++++++++-------- 1 file changed, 58 insertions(+), 42 deletions(-) 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 6db079238..1fffecb57 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 @@ -52,10 +52,10 @@ export class SavedFiltersPanelComponent implements OnInit { @Input() tableWidgets: any = {}; // @Input() savedFilterData: any; @Output() filterSelected = new EventEmitter(); + @Input() resetSelection: boolean = false; public savedFilterData: any[] = []; public savedFilterMap: { [key: string]: any } = {}; - public selectedFilterIndex: number = -1; public selectedFilterSetId: string | null = null; public selectedFilter: any = null; @@ -97,17 +97,15 @@ export class SavedFiltersPanelComponent implements OnInit { })); this.route.queryParams.subscribe(params => { - const savedFilterName = params['saved_filter']; + const savedFilterId = params['saved_filter']; - if (savedFilterName && this.savedFilterData.length > 0) { - const filterIndex = this.savedFilterData.findIndex(filter => filter.name === savedFilterName); - - if (filterIndex !== -1) { - this.selectedFilterIndex = filterIndex; - this.filterSelected.emit(this.savedFilterData[filterIndex]); + if (savedFilterId && this.savedFilterData.length > 0) { + if (savedFilterId) { + this.selectedFilterSetId = savedFilterId; + this.filterSelected.emit(this.savedFilterMap[savedFilterId]); } } else { - this.selectedFilterIndex = -1; + this.selectedFilterSetId = null; } }); }, @@ -117,6 +115,12 @@ export class SavedFiltersPanelComponent implements OnInit { }); } + ngOnChanges() { + if (this.resetSelection) { + this.selectedFilterSetId = null; + } + } + handleOpenSavedFiltersDialog(filtersSet: any = null) { this.dialog.open(SavedFiltersDialogComponent, { width: '56em', @@ -182,30 +186,17 @@ export class SavedFiltersPanelComponent implements OnInit { return transformedFilter; } - - selectFiltersSet(selectedFilterSetId: string): void { - this.selectedFilterSetId = selectedFilterSetId; - const selectedFilter = this.savedFilterMap[selectedFilterSetId]; - this.filterSelected.emit(selectedFilter); - + private buildQueryParams(additionalParams: any = {}): any { const currentParams = this.route.snapshot.queryParams; - const filters = JsonURL.stringify(selectedFilter.filters); - + // Start with pagination parameters const queryParams: any = { - filters, page_index: 0, page_size: currentParams['page_size'] || 30, - saved_filter: selectedFilter.name + ...additionalParams }; - if (selectedFilter.dynamicColumn) { - queryParams.dynamic_column = JsonURL.stringify({ - column_name: selectedFilter.dynamicColumn.column, - comparator: selectedFilter.dynamicColumn.operator - }); - } - + // Preserve sort parameters if present if (currentParams['sort_active']) { queryParams.sort_active = currentParams['sort_active']; } @@ -214,9 +205,43 @@ export class SavedFiltersPanelComponent implements OnInit { queryParams.sort_direction = currentParams['sort_direction']; } - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { - queryParams - }); + return queryParams; + } + + selectFiltersSet(selectedFilterSetId: string): void { + if (this.selectedFilterSetId === selectedFilterSetId) { + // If the same filter is selected, clear the selection + this.selectedFilterSetId = null; + this.filterSelected.emit(null); + + // Navigate without filters and saved_filter parameters + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + return; + } + + // Apply new filter selection + this.selectedFilterSetId = selectedFilterSetId; + const selectedFilter = this.savedFilterMap[selectedFilterSetId]; + this.filterSelected.emit(selectedFilter); + + // Build filter-related params + const additionalParams: any = { + filters: JsonURL.stringify(selectedFilter.filters), + saved_filter: selectedFilterSetId + }; + + // Add dynamic column if present + if (selectedFilter.dynamicColumn) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }); + } + + // Navigate with all parameters + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } selectFilter(entry: { column: string; operator: string; value: any }) { @@ -358,8 +383,6 @@ export class SavedFiltersPanelComponent implements OnInit { if (!selectedFilter || !selectedFilter.dynamicColumn) return; - const currentParams = this.route.snapshot.queryParams; - const dynamicColumn = { column_name: selectedFilter.dynamicColumn.column, comparator: selectedFilter.dynamicColumn.operator @@ -381,21 +404,14 @@ export class SavedFiltersPanelComponent implements OnInit { } } - const queryParams: any = { + // Build filter-related params using the helper method + const additionalParams: any = { filters: JsonURL.stringify(filters), dynamic_column: JsonURL.stringify(dynamicColumn), - page_index: 0, // Reset to first page - page_size: currentParams['page_size'] || 30, - saved_filter: selectedFilter.name + saved_filter: this.selectedFilterSetId }; - if (currentParams['sort_active']) { - queryParams.sort_active = currentParams['sort_active']; - } - - if (currentParams['sort_direction']) { - queryParams.sort_direction = currentParams['sort_direction']; - } + const queryParams = this.buildQueryParams(additionalParams); const updatedFilterData = { ...selectedFilter, From 16ab60845ee93cf5abff61eba8a8bc6468f1b31a Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 6 Aug 2025 18:48:52 +0300 Subject: [PATCH 12/29] saved filters: hide active filters chips when saved filter is activated --- .../db-table-view/db-table-view.component.html | 2 +- .../db-table-view/db-table-view.component.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 6bb473391..3c62211b0 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -169,7 +169,7 @@

{{ displayName }}

-
+
{{ getFilter(activeFilter) }} diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 4c436880c..ddc4d7af4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -101,6 +101,8 @@ export class DbTableViewComponent implements OnInit { @Output() removeFilter = new EventEmitter(); @Output() resetAllFilters = new EventEmitter(); // @Output() viewRow = new EventEmitter(); + + public hasSavedFilterActive: boolean = false; @Output() activateAction = new EventEmitter(); @Output() activateActions = new EventEmitter(); @@ -177,6 +179,7 @@ export class DbTableViewComponent implements OnInit { ngOnInit() { this.searchString = this.route.snapshot.queryParams.search; + this.hasSavedFilterActive = !!this.route.snapshot.queryParams.saved_filter; const connectionType = this._connections.currentConnection.type; this.displayCellComponents = tableDisplayTypes[connectionType]; @@ -184,6 +187,20 @@ export class DbTableViewComponent implements OnInit { this._tableState.cast.subscribe(row => { this.selectedRow = row; }); + + // Subscribe to route changes to update hasSavedFilterActive when URL parameters change + let previousHasSavedFilter = this.hasSavedFilterActive; + this.route.queryParams.subscribe(params => { + const currentHasSavedFilter = !!params.saved_filter; + this.hasSavedFilterActive = currentHasSavedFilter; + + // If a saved filter was active but now it's not, reset all active filters + if (previousHasSavedFilter && !currentHasSavedFilter) { + this.resetAllFilters.emit(); + } + + previousHasSavedFilter = currentHasSavedFilter; + }); } onInput(searchValue: string) { From e254b3ece27198dd84b1811026a46ea2fb476c72 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 9 Aug 2025 13:12:27 +0300 Subject: [PATCH 13/29] fix switching between filters, saved filters and search --- .../dashboard/dashboard.component.ts | 18 ++++++- .../db-table-filters-dialog.component.ts | 38 ++++++++----- .../db-table-view.component.html | 2 +- .../db-table-view/db-table-view.component.ts | 22 ++++---- .../edit-filter-dialog.component.ts | 48 ----------------- .../saved-filters-panel.component.ts | 54 ++++++++++--------- .../test-menu.component.html | 18 ------- 7 files changed, 81 insertions(+), 119 deletions(-) delete mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts delete mode 100644 frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index 41277a2ac..1ddcdc0dc 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -253,6 +253,7 @@ export class DashboardComponent implements OnInit, OnDestroy { const search = queryParams.search; this.getRows(search); + console.log('getRows from setTable'); }) const selectedTableProperties = this.tablesList.find( (table: any) => table.table == this.selectedTableName); @@ -279,6 +280,8 @@ export class DashboardComponent implements OnInit, OnDestroy { if (action === 'filter') { const filtersFromDialog = {...filterDialodRef.componentInstance.tableRowFieldsShown}; + console.log('Filters from dialog:', filtersFromDialog); + const nonEmptyFilters = omitBy(filtersFromDialog, (value) => value === undefined); this.comparators = filterDialodRef.componentInstance.tableRowFieldsComparator; @@ -292,8 +295,12 @@ export class DashboardComponent implements OnInit, OnDestroy { } } + console.log('Filters to apply:', this.filters); + const filters = JsonURL.stringify( this.filters ); + console.log('Filters to navigate:', filters); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams: { filters, @@ -302,6 +309,8 @@ export class DashboardComponent implements OnInit, OnDestroy { } }); this.getRows(); + console.log('getRows from afterClosed'); + this.angulartics2.eventTrack.next({ action: 'Dashboard: filter is applied', @@ -310,6 +319,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } else if (action === 'reset') { this.filters = {}; this.getRows(); + console.log('getRows from reset filters afterClosed'); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`]); } }) @@ -324,6 +334,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.selection.clear(); this.getRows(); + console.log('getRows from removeFilter'); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams: { filters, @@ -337,6 +348,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.filters = {}; this.comparators = {}; this.getRows(); + console.log('getRows from clearAllFilters'); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams: { page_index: 0, @@ -347,6 +359,7 @@ export class DashboardComponent implements OnInit, OnDestroy { search(value: string) { this.getRows(value); + console.log('getRows from search'); this.filters = {}; this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams: { @@ -358,6 +371,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } getRows(search?: string) { + console.log('getRows, filters:', this.filters); this._uiSettings.getUiSettings() .subscribe ((settings: UiSettings) => { this.uiSettings = settings?.connections[this.connectionID]; @@ -379,8 +393,10 @@ export class DashboardComponent implements OnInit, OnDestroy { } applyFilter(filters: any) { - this.filters = filters.filters; + console.log('applyFilter with filters:', filters); + this.filters = filters?.filters; this.getRows(); + console.log('getRows from applyFilter'); } openIntercome() { 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 0b6c04b0d..177217d3a 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 @@ -92,21 +92,33 @@ export class DbTableFiltersDialogComponent implements OnInit { })); const queryParams = this.route.snapshot.queryParams; - 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; - this.tableRowFieldsComparator = getComparatorsFromUrl(filters); + // 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 { - const fieldsToSearch = this.data.structure.structure.filter((field: TableField) => field.isSearched); - if (fieldsToSearch.length) { - this.tableFilters = fieldsToSearch.map((field:TableField) => field.column_name); - this.tableRowFieldsShown = Object.assign({}, ...fieldsToSearch.map((field: TableField) => ({[field.column_name]: undefined}))); - this.tableRowFieldsComparator = Object.assign({}, ...fieldsToSearch.map((field: TableField) => ({[field.column_name]: 'eq'}))); + // 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; + this.tableRowFieldsComparator = getComparatorsFromUrl(filters); + } else { + const fieldsToSearch = this.data.structure.structure.filter((field: TableField) => field.isSearched); + if (fieldsToSearch.length) { + this.tableFilters = fieldsToSearch.map((field:TableField) => field.column_name); + this.tableRowFieldsShown = Object.assign({}, ...fieldsToSearch.map((field: TableField) => ({[field.column_name]: undefined}))); + this.tableRowFieldsComparator = Object.assign({}, ...fieldsToSearch.map((field: TableField) => ({[field.column_name]: 'eq'}))); + } } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 3c62211b0..d87e3173a 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -187,7 +187,7 @@

{{ displayName }}

[tableWidgets]="tableData.widgets" [tableForeignKeys]="tableData.foreignKeys" [tableTypes]="tableData.tableTypes" - (filterSelected)="applyFilter.emit($event)" + (filterSelected)="onFilterSelected($event)" >
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index ddc4d7af4..e3a8fe2c2 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -179,7 +179,7 @@ export class DbTableViewComponent implements OnInit { ngOnInit() { this.searchString = this.route.snapshot.queryParams.search; - this.hasSavedFilterActive = !!this.route.snapshot.queryParams.saved_filter; + // this.hasSavedFilterActive = !!this.route.snapshot.queryParams.saved_filter; const connectionType = this._connections.currentConnection.type; this.displayCellComponents = tableDisplayTypes[connectionType]; @@ -188,18 +188,9 @@ export class DbTableViewComponent implements OnInit { this.selectedRow = row; }); - // Subscribe to route changes to update hasSavedFilterActive when URL parameters change - let previousHasSavedFilter = this.hasSavedFilterActive; this.route.queryParams.subscribe(params => { - const currentHasSavedFilter = !!params.saved_filter; - this.hasSavedFilterActive = currentHasSavedFilter; - - // If a saved filter was active but now it's not, reset all active filters - if (previousHasSavedFilter && !currentHasSavedFilter) { - this.resetAllFilters.emit(); - } - - previousHasSavedFilter = currentHasSavedFilter; + this.hasSavedFilterActive = !!params.saved_filter; + if (this.hasSavedFilterActive ) this.searchString = ''; }); } @@ -268,7 +259,7 @@ export class DbTableViewComponent implements OnInit { } getFiltersCount(activeFilters: object) { - if (activeFilters) return Object.keys(activeFilters).length; + if (activeFilters && !this.hasSavedFilterActive) return Object.keys(activeFilters).length; return 0; } @@ -519,4 +510,9 @@ export class DbTableViewComponent implements OnInit { switchTable(e) { } + + onFilterSelected($event) { + console.log('table view fiers filterSelected:', $event) + this.applyFilter.emit($event); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts deleted file mode 100644 index c467e6edc..000000000 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/edit-filter-dialog.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { normalizeTableName } from 'src/app/lib/normalize'; - -@Component({ - selector: 'app-edit-filter-dialog', - standalone: true, - imports: [ - CommonModule, - FormsModule, - MatDialogModule, - MatButtonModule, - MatFormFieldModule, - MatInputModule - ], - template: ` -

Edit {{ normalizeTableName(data.entry.column) }} filter

- - - Filter value - - - - - - - - `, - styles: [` - mat-dialog-content { - min-width: 300px; - padding-top: 10px; - } - `] -}) -export class EditFilterDialogComponent { - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: {entry: {column: string, operator: string, value: any}} - ) {} - - normalizeTableName = normalizeTableName; -} 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 1fffecb57..4063526fc 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 @@ -56,6 +56,7 @@ export class SavedFiltersPanelComponent implements OnInit { public savedFilterData: any[] = []; public savedFilterMap: { [key: string]: any } = {}; + public initialFilterApplied: boolean = false; public selectedFilterSetId: string | null = null; public selectedFilter: any = null; @@ -102,7 +103,11 @@ export class SavedFiltersPanelComponent implements OnInit { if (savedFilterId && this.savedFilterData.length > 0) { if (savedFilterId) { this.selectedFilterSetId = savedFilterId; - this.filterSelected.emit(this.savedFilterMap[savedFilterId]); + // Only emit when the filter is first loaded from URL, not when selectFiltersSet changes the URL + if (!this.initialFilterApplied) { + this.initialFilterApplied = true; + this.filterSelected.emit(this.savedFilterMap[savedFilterId]); + } } } else { this.selectedFilterSetId = null; @@ -154,8 +159,6 @@ export class SavedFiltersPanelComponent implements OnInit { }); }); }); - - console.log('Filter entries:', entries); return entries; } @@ -209,39 +212,40 @@ export class SavedFiltersPanelComponent implements OnInit { } selectFiltersSet(selectedFilterSetId: string): void { + console.log('selectFiltersSet ID:', selectedFilterSetId); if (this.selectedFilterSetId === selectedFilterSetId) { // If the same filter is selected, clear the selection this.selectedFilterSetId = null; this.filterSelected.emit(null); + this.initialFilterApplied = false; // Reset flag when clearing filter // Navigate without filters and saved_filter parameters const queryParams = this.buildQueryParams(); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - return; - } - - // Apply new filter selection - this.selectedFilterSetId = selectedFilterSetId; - const selectedFilter = this.savedFilterMap[selectedFilterSetId]; - this.filterSelected.emit(selectedFilter); + } else { + // Apply new filter selection + this.selectedFilterSetId = selectedFilterSetId; + const selectedFilter = this.savedFilterMap[selectedFilterSetId]; + this.filterSelected.emit(selectedFilter); + + // Build filter-related params + const additionalParams: any = { + filters: JsonURL.stringify(selectedFilter.filters), + saved_filter: selectedFilterSetId + }; - // Build filter-related params - const additionalParams: any = { - filters: JsonURL.stringify(selectedFilter.filters), - saved_filter: selectedFilterSetId - }; + // Add dynamic column if present + if (selectedFilter.dynamicColumn) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }); + } - // Add dynamic column if present - if (selectedFilter.dynamicColumn) { - additionalParams.dynamic_column = JsonURL.stringify({ - column_name: selectedFilter.dynamicColumn.column, - comparator: selectedFilter.dynamicColumn.operator - }); + // Navigate with all parameters + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } - - // Navigate with all parameters - const queryParams = this.buildQueryParams(additionalParams); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } selectFilter(entry: { column: string; operator: string; value: any }) { diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html deleted file mode 100644 index 926f6e198..000000000 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/test-menu.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
-

Test Menu Component

- - - -
- - - -
- - -
-
-
-
From 88736e2620ff926b3e63efa2e2aa779c909a7c27 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 9 Aug 2025 14:00:18 +0300 Subject: [PATCH 14/29] saved filters: read dynamic field from url params correctly --- .../saved-filters-panel.component.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) 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 4063526fc..504564127 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 @@ -56,7 +56,6 @@ export class SavedFiltersPanelComponent implements OnInit { public savedFilterData: any[] = []; public savedFilterMap: { [key: string]: any } = {}; - public initialFilterApplied: boolean = false; public selectedFilterSetId: string | null = null; public selectedFilter: any = null; @@ -99,19 +98,27 @@ export class SavedFiltersPanelComponent implements OnInit { this.route.queryParams.subscribe(params => { const savedFilterId = params['saved_filter']; + const dynamicColumn = JsonURL.parse(params['dynamic_column']); if (savedFilterId && this.savedFilterData.length > 0) { if (savedFilterId) { this.selectedFilterSetId = savedFilterId; - // Only emit when the filter is first loaded from URL, not when selectFiltersSet changes the URL - if (!this.initialFilterApplied) { - this.initialFilterApplied = true; - this.filterSelected.emit(this.savedFilterMap[savedFilterId]); + + if (dynamicColumn) { + const filters = JsonURL.parse(params['filters']); + + this.savedFilterMap[savedFilterId].dynamicColumn = { + column: dynamicColumn.column_name, + operator: dynamicColumn.comparator, + value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null + }; } } } else { this.selectedFilterSetId = null; } + + console.log('savedFilterMap after dynamicColumn assignment:', this.savedFilterMap); }); }, error: (error) => { @@ -214,27 +221,20 @@ export class SavedFiltersPanelComponent implements OnInit { selectFiltersSet(selectedFilterSetId: string): void { console.log('selectFiltersSet ID:', selectedFilterSetId); if (this.selectedFilterSetId === selectedFilterSetId) { - // If the same filter is selected, clear the selection this.selectedFilterSetId = null; this.filterSelected.emit(null); - this.initialFilterApplied = false; // Reset flag when clearing filter - - // Navigate without filters and saved_filter parameters const queryParams = this.buildQueryParams(); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } else { - // Apply new filter selection this.selectedFilterSetId = selectedFilterSetId; const selectedFilter = this.savedFilterMap[selectedFilterSetId]; this.filterSelected.emit(selectedFilter); - // Build filter-related params const additionalParams: any = { filters: JsonURL.stringify(selectedFilter.filters), saved_filter: selectedFilterSetId }; - // Add dynamic column if present if (selectedFilter.dynamicColumn) { additionalParams.dynamic_column = JsonURL.stringify({ column_name: selectedFilter.dynamicColumn.column, @@ -242,7 +242,6 @@ export class SavedFiltersPanelComponent implements OnInit { }); } - // Navigate with all parameters const queryParams = this.buildQueryParams(additionalParams); this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); } @@ -408,6 +407,8 @@ export class SavedFiltersPanelComponent implements OnInit { } } + console.log('applyDynamicColumnChanges, filters:', filters); + // Build filter-related params using the helper method const additionalParams: any = { filters: JsonURL.stringify(filters), From 6b8e9c2822eafa34b54981e216448fe048542a24 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 9 Aug 2025 15:01:33 +0300 Subject: [PATCH 15/29] table view: use route.snapshot in setTable to avoid subscription --- .../dashboard/dashboard.component.ts | 29 ++++++---- .../saved-filters-panel.component.ts | 56 +++++++++---------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index 1ddcdc0dc..6f2571995 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -151,6 +151,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.shownTableTitles = settings?.connections[this.connectionID]?.shownTableTitles ?? true; this.getData(); + console.log('getData from ngOnInit'); }); } @@ -159,6 +160,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } async getData() { + console.log('getData'); let tables; try { tables = await this.getTables(); @@ -185,6 +187,7 @@ export class DashboardComponent implements OnInit, OnDestroy { if (tableName) { this.selectedTableName = tableName; this.setTable(tableName); + console.log('setTable from getData paramMap'); this.title.setTitle(`${this.selectedTableDisplayName} table | ${this._company.companyTabTitle || 'Rocketadmin'}`); this.selection.clear(); } else { @@ -201,12 +204,14 @@ export class DashboardComponent implements OnInit, OnDestroy { this._tableRow.cast.subscribe((arg) => { if (arg === 'delete row' && this.selectedTableName) { this.setTable(this.selectedTableName); + console.log('setTable from getData _tableRow cast'); this.selection.clear(); }; }); this._tables.cast.subscribe((arg) => { if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) { this.setTable(this.selectedTableName); + console.log('setTable from getData _tables cast'); this.selection.clear(); }; if (arg === 'activate actions') { @@ -217,6 +222,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } getTables() { + console.log('getTables'); return this._tables.fetchTables(this.connectionID).toPromise(); } @@ -243,18 +249,17 @@ export class DashboardComponent implements OnInit, OnDestroy { setTable(tableName: string) { this.selectedTableName = tableName; - this.route.queryParams.pipe(first()).subscribe((queryParams) => { - this.filters = JsonURL.parse( queryParams.filters ); - this.comparators = getComparatorsFromUrl(this.filters); - this.pageIndex = parseInt(queryParams.page_index) || 0; - this.pageSize = parseInt(queryParams.page_size) || 30; - this.sortColumn = queryParams.sort_active; - this.sortOrder = queryParams.sort_direction; - - const search = queryParams.search; - this.getRows(search); - console.log('getRows from setTable'); - }) + const queryParams = this.route.snapshot.queryParams; + this.filters = JsonURL.parse(queryParams.filters); + this.comparators = getComparatorsFromUrl(this.filters); + this.pageIndex = parseInt(queryParams.page_index) || 0; + this.pageSize = parseInt(queryParams.page_size) || 30; + this.sortColumn = queryParams.sort_active; + this.sortOrder = queryParams.sort_direction; + + const search = queryParams.search; + this.getRows(search); + console.log('getRows from setTable'); const selectedTableProperties = this.tablesList.find( (table: any) => table.table == this.selectedTableName); if (selectedTableProperties) { 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 504564127..932b65772 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 @@ -90,36 +90,36 @@ export class SavedFiltersPanelComponent implements OnInit { ngOnInit() { this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ next: (data) => { - this.savedFilterData = data; - this.savedFilterMap = Object.assign({}, ...data.map((filter) => { - const transformedFilter = this.processFiltersData(filter); - return { [filter.id]: transformedFilter }; - })); - - this.route.queryParams.subscribe(params => { - const savedFilterId = params['saved_filter']; - const dynamicColumn = JsonURL.parse(params['dynamic_column']); - - if (savedFilterId && this.savedFilterData.length > 0) { - if (savedFilterId) { - this.selectedFilterSetId = savedFilterId; - - if (dynamicColumn) { - const filters = JsonURL.parse(params['filters']); - - this.savedFilterMap[savedFilterId].dynamicColumn = { - column: dynamicColumn.column_name, - operator: dynamicColumn.comparator, - value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null - }; + if (data) { + this.savedFilterData = data; + this.savedFilterMap = Object.assign({}, ...data.map((filter) => { + const transformedFilter = this.processFiltersData(filter); + return { [filter.id]: transformedFilter }; + })); + + this.route.queryParams.subscribe(params => { + const savedFilterId = params['saved_filter']; + const dynamicColumn = JsonURL.parse(params['dynamic_column']); + + if (savedFilterId && this.savedFilterData.length > 0) { + if (savedFilterId) { + this.selectedFilterSetId = savedFilterId; + + if (dynamicColumn) { + const filters = JsonURL.parse(params['filters']); + + this.savedFilterMap[savedFilterId].dynamicColumn = { + column: dynamicColumn.column_name, + operator: dynamicColumn.comparator, + value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null + }; + } } + } else { + this.selectedFilterSetId = null; } - } else { - this.selectedFilterSetId = null; - } - - console.log('savedFilterMap after dynamicColumn assignment:', this.savedFilterMap); - }); + }); + } }, error: (error) => { console.error('Error fetching saved filters:', error); From 95f930945194528fe27b5563cab625595c62b347 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 9 Aug 2025 16:24:33 +0300 Subject: [PATCH 16/29] saved filters panel: add paramMap subscription to update saved filtes list of a table --- .../saved-filters-panel.component.ts | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) 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 932b65772..d0f7c2159 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 @@ -88,6 +88,20 @@ export class SavedFiltersPanelComponent implements OnInit { ) {} ngOnInit() { + this.route.paramMap.subscribe(params => { + const tableNameFromUrl = params.get('table-name'); + if (tableNameFromUrl) { + this.selectedTableName = tableNameFromUrl; + this.loadSavedFilters(); + } + }); + } + + loadSavedFilters() { + if (!this.connectionID || !this.selectedTableName) { + return; + } + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ next: (data) => { if (data) { @@ -97,28 +111,25 @@ export class SavedFiltersPanelComponent implements OnInit { return { [filter.id]: transformedFilter }; })); - this.route.queryParams.subscribe(params => { - const savedFilterId = params['saved_filter']; - const dynamicColumn = JsonURL.parse(params['dynamic_column']); - - if (savedFilterId && this.savedFilterData.length > 0) { - if (savedFilterId) { - this.selectedFilterSetId = savedFilterId; - - if (dynamicColumn) { - const filters = JsonURL.parse(params['filters']); - - this.savedFilterMap[savedFilterId].dynamicColumn = { - column: dynamicColumn.column_name, - operator: dynamicColumn.comparator, - value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null - }; - } - } - } else { - this.selectedFilterSetId = null; + const params = this.route.snapshot.queryParams; + const savedFilterId = params['saved_filter']; + const dynamicColumn = params['dynamic_column'] ? JsonURL.parse(params['dynamic_column']) : null; + + if (savedFilterId && this.savedFilterData.length > 0) { + this.selectedFilterSetId = savedFilterId; + + if (dynamicColumn && this.savedFilterMap[savedFilterId]) { + const filters = params['filters'] ? JsonURL.parse(params['filters']) : {}; + + this.savedFilterMap[savedFilterId].dynamicColumn = { + column: dynamicColumn.column_name, + operator: dynamicColumn.comparator, + value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null + }; } - }); + } else { + this.selectedFilterSetId = null; + } } }, error: (error) => { From ebc0cd261f0c64ae0216b26293d45cac2b84511e Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sat, 9 Aug 2025 23:22:29 +0300 Subject: [PATCH 17/29] saved filter: - add update saved filters; - update saved filters layout. --- .../saved-filters-dialog.component.html | 5 +- .../saved-filters-dialog.component.ts | 48 ++-- .../saved-filters-panel.component.css | 37 +-- .../saved-filters-panel.component.html | 215 ++++++++---------- frontend/src/app/services/tables.service.ts | 26 +++ 5 files changed, 162 insertions(+), 169 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index 51ea64c3f..c2cf21a33 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -1,4 +1,4 @@ -
+

Save Filters for {{ data.displayTableName }} table Define Filters and Dynamic Column

- 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 282fefc8b..ee1ba6552 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 @@ -231,24 +231,8 @@ export class SavedFiltersDialogComponent implements OnInit { } } - saveFilter() { - // if (!this.filterForm.valid || this.tableFiltersCount === 0) { - // return; - // } - - // Prepare filter data to save - // const filterToSave = { - // name: this.filterForm.get('filterName').value, - // description: this.filterForm.get('filterDescription').value, - // isDefault: this.filterForm.get('isDefault').value, - // connectionId: this.data.connectionID, - // tableName: this.data.tableName, - // filters: this.tableRowFieldsShown, - // comparators: this.tableRowFieldsComparator - // }; - - // const nonEmptyFilters = omitBy(this.tableRowFieldsShown, (value) => value === undefined); - + handleSaveFilters() { + let payload; if (Object.keys(this.tableRowFieldsShown).length) { let filters = {}; @@ -270,7 +254,7 @@ export class SavedFiltersDialogComponent implements OnInit { } // const filters = JsonURL.stringify( this.filters ); - const payload = { + payload = { name: this.data.filtersSet.name, filters }; @@ -284,13 +268,35 @@ export class SavedFiltersDialogComponent implements OnInit { }; } - this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload) + if (this.data.filtersSet.id) { + this._tables.updateSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id, payload) + .subscribe(() => { + this.dialogRef.close(true); + }, (error) => { + console.error('Error updating filter:', error); + this.snackBar.open('Error updating filter', 'Close', { duration: 3000 }); + }); + } else { + 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 }); }); - } + } + } + + // 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 }); + // }); + // } } } \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index eaa9428ad..5456ee1df 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -1,8 +1,14 @@ .saved-filters-container { display: flex; flex-direction: column; + gap: 8px; + margin: 20px 0; +} + +.saved-filters-list { + display: flex; + align-items: center; gap: 16px; - margin: 8px 0; } .saved-filters-tabs { @@ -63,24 +69,6 @@ min-height: 36px; } -.no-filters { - display: flex; - align-items: center; - gap: 8px; - padding: 12px; - background-color: rgba(0, 0, 0, 0.04); - border-radius: 4px; - color: rgba(0, 0, 0, 0.6); -} - -.no-filters mat-icon { - color: #999; - font-size: 18px; - width: 18px; - height: 18px; -} - -/* Single filter editor styles */ .single-filter-editor { margin-top: 20px; padding: 16px; @@ -141,7 +129,6 @@ display: flex; flex-direction: column; gap: 16px; - margin-top: 16px; } .static-filters { @@ -179,7 +166,6 @@ .dynamic-column-editor { display: flex; - flex-direction: column; gap: 16px; } @@ -189,12 +175,3 @@ gap: 8px; margin-top: 8px; } - -.no-filters-message { - padding: 16px; - color: rgba(0, 0, 0, 0.6); - font-style: italic; - background-color: #f5f7fa; - border-radius: 8px; - text-align: center; -} \ No newline at end of file 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 41795b232..461c45eb2 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 @@ -1,125 +1,93 @@
- - - - - - - + + + + -
- info - No saved filters. Create one by clicking the button above. + + + {{ filter.name }} + +
- - - {{ filter.name }} - - -
- -
-

Filters:

- - {{ getFilter(filter) }} - -
- - -
-

Dynamic Column:

-
-
- {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} +
+
+ {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} - - - - starts with - ends with - equal - contains - not contains - is empty - - + + + + starts with + ends with + equal + contains + not contains + is empty + + - - - - equal - greater than - less than - greater than or equal - less than or equal - - + + + + equal + greater than + less than + greater than or equal + less than or equal + + - -
-
- -
- - - - + +
+
+
- -
+ Dynamic Column: onFieldChange: { handler: updateDynamicColumnValue, args: ['$event'] } }" > -
+
-
- + +
+
-
- -
- No filters to display. +
+ +
+
+
+ + {{ getFilter(filter) }} +
\ No newline at end of file diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index ee92408b8..18785a56a 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -554,6 +554,32 @@ export class TablesService { ) } + updateSavedFilter(connectionID: string, tableName: string, filtersId: string, filters: object) { + return this._http.put(`/table-filters/${connectionID}/${filtersId}`, filters, { + params: { + tableName + } + }) + .pipe( + map(res => { + this.tables.next('update filters'); + this._notifications.showSuccessSnackbar('Saved filter has been updated.') + return res + }), + catchError((err) => { + console.log(err); + this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (id: number) => this._notifications.dismissAlert() + } + ]); + return EMPTY; + }) + ); + } + deleteSavedFilter(connectionID: string, tableName: string, filterId: string) { return this._http.delete(`/table-filters/${connectionID}/${filterId}`, { params: { From fec3e7e8d2ee5f14f4c1db0c20bdaf2874c27aeb Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sun, 10 Aug 2025 00:12:26 +0300 Subject: [PATCH 18/29] saved filters: update list on add and delete --- .../saved-filters-panel.component.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 d0f7c2159..b643998c4 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 @@ -95,6 +95,20 @@ export class SavedFiltersPanelComponent implements OnInit { this.loadSavedFilters(); } }); + + this._tables.cast.subscribe((arg) => { + if (arg === 'saved filters') { + this.loadSavedFilters(); + } + + if (arg === 'delete saved filters') { + this.loadSavedFilters(); + this.selectedFilterSetId = null; + this.filterSelected.emit(null); + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + }); } loadSavedFilters() { From 949da92fdc5a23af709be23049916c3eee7f105f Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Sun, 10 Aug 2025 00:34:06 +0300 Subject: [PATCH 19/29] saved filters: element visibility diponding on access level --- .../dashboard/db-table-view/db-table-view.component.html | 1 + .../saved-filters-panel/saved-filters-panel.component.html | 4 ++-- .../saved-filters-panel/saved-filters-panel.component.ts | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index d87e3173a..ddfc0ed61 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -187,6 +187,7 @@

{{ displayName }}

[tableWidgets]="tableData.widgets" [tableForeignKeys]="tableData.foreignKeys" [tableTypes]="tableData.tableTypes" + [accessLevel]="accessLevel" (filterSelected)="onFilterSelected($event)" > 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 461c45eb2..5693c6026 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 @@ -1,13 +1,13 @@
- - - + @@ -34,78 +34,58 @@
-
-
- {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + + where {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + + + starts with + ends with + equal + contains + not contains + is empty + + - - - - starts with - ends with - equal - contains - not contains - is empty - - + + + equal + greater than + less than + greater than or equal + less than or equal + + - - - - equal - greater than - less than - greater than or equal - less than or equal - - - - -
-
- -
- - - - + +
+
+
- -
+ -
+
-
- + +
+
-
+
{{ getFilter(filter) }} From 5b0b08b437347e354f24c2b69107b678e54aee25 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 13 Aug 2025 20:01:23 +0300 Subject: [PATCH 25/29] saved filters: debounce and update table on dynamic filter value change --- .../saved-filters-panel.component.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 92fa9bb7a..f04862027 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,6 +1,7 @@ import { ActivatedRoute, Router } from '@angular/router'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { AccessLevel } from 'src/app/models/user'; import { CommonModule } from '@angular/common'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DynamicModule } from 'ng-dynamic-component'; @@ -22,7 +23,6 @@ import { TablesService } from 'src/app/services/tables.service'; import { UIwidgets } from 'src/app/consts/record-edit-types'; import { filterTypes } from 'src/app/consts/filter-types'; import { normalizeTableName } from 'src/app/lib/normalize'; -import { AccessLevel } from 'src/app/models/user'; @Component({ selector: 'app-saved-filters-panel', @@ -43,7 +43,7 @@ import { AccessLevel } from 'src/app/models/user'; templateUrl: './saved-filters-panel.component.html', styleUrl: './saved-filters-panel.component.css' }) -export class SavedFiltersPanelComponent implements OnInit { +export class SavedFiltersPanelComponent implements OnInit, OnDestroy { @Input() connectionID: string; @Input() selectedTableName: string; @Input() selectedTableDisplayName: string; @@ -57,6 +57,8 @@ export class SavedFiltersPanelComponent implements OnInit { @Input() accessLevel: AccessLevel; + private dynamicColumnValueDebounceTimer: any = null; + public savedFilterData: any[] = []; public savedFilterMap: { [key: string]: any } = {}; @@ -167,6 +169,12 @@ export class SavedFiltersPanelComponent implements OnInit { } } + ngOnDestroy() { + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + } + handleOpenSavedFiltersDialog(filtersSet: any = null) { this.dialog.open(SavedFiltersDialogComponent, { width: '56em', @@ -408,7 +416,17 @@ export class SavedFiltersPanelComponent implements OnInit { return; } + console.log(value, 'value in updateDynamicColumnValue'); + selectedFilter.dynamicColumn.value = value; + + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + + this.dynamicColumnValueDebounceTimer = setTimeout(() => { + this.applyDynamicColumnChanges(); + }, 800); } applyDynamicColumnChanges() { From bc96ab0a4f221d6dee40a760dbfd8e2e8e31d52a Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 13 Aug 2025 20:23:13 +0300 Subject: [PATCH 26/29] saved filters: fix structure passing --- .../dashboard/db-table-view/db-table-view.component.html | 2 +- .../saved-filters-panel/saved-filters-panel.component.css | 4 ++++ .../saved-filters-panel/saved-filters-panel.component.html | 6 +++++- .../saved-filters-panel/saved-filters-panel.component.ts | 6 +++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index ddfc0ed61..dac863d53 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -179,7 +179,7 @@

{{ displayName }}

- - where {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + where + + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + + { + return {[field.column_name]: field}; + })); } loadSavedFilters() { From 4fa278ec0a5b4d01df06a5db957122c96a9c1023 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 13 Aug 2025 20:54:13 +0300 Subject: [PATCH 27/29] saved filter: fix styles --- .../saved-filters-dialog/saved-filters-dialog.component.css | 6 +++--- .../saved-filters-dialog.component.html | 5 ----- .../saved-filters-panel/saved-filters-panel.component.css | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index 09df26ffb..6deec4ede 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -24,7 +24,7 @@ .dynamic-column-radio { grid-column: 5; - margin-top: 14px; + margin-top: 8px; } .filter-save-form { @@ -47,7 +47,7 @@ } .full-width { - width: 100%; + grid-column: 1 / -1; } .default-filter-checkbox { @@ -75,7 +75,7 @@ } .column-name { - margin-top: 18px; + margin-top: 12px; } .filter-delete-button { diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index c2cf21a33..f44bab341 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -153,11 +153,6 @@

Define Filters and Dynamic Column

close - - -
- No filters added. Please add at least one filter. -
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index b6b87a337..6863fb37e 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -53,7 +53,7 @@ flex-wrap: wrap; align-items: center; gap: 8px; - transform: scale(1.1); + transform: translateX(5%) scale(1.1); padding-bottom: 16px; } From b9428a559ba88448a21921bf502ceb235482134a Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 13 Aug 2025 21:18:33 +0300 Subject: [PATCH 28/29] saved filters: update url if editing currently applied filter --- .../saved-filters-panel.component.ts | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) 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 caecf4f24..4106ef5a6 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 @@ -111,10 +111,66 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { }); this._tables.cast.subscribe((arg) => { - if (arg === 'filters set saved' || arg === 'filters set updated') { + if (arg === 'filters set saved') { this.loadSavedFilters(); } + if (arg === 'filters set updated') { + // Get the current saved filter ID from URL + const savedFilterIdFromUrl = this.route.snapshot.queryParams['saved_filter']; + + if (savedFilterIdFromUrl) { + // If we have a filter selected in URL, get latest data and update URL + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ + next: (data) => { + if (data) { + // Find the updated filter in the response + const updatedFilter = data.find(filter => filter.id === savedFilterIdFromUrl); + if (updatedFilter) { + const processedFilter = this.processFiltersData(updatedFilter); + + // Update local state + this.selectedFilterSetId = savedFilterIdFromUrl; + this.filterSelected.emit(processedFilter); + + // Update URL with the refreshed filter data + const additionalParams: any = { + filters: JsonURL.stringify(updatedFilter.filters), + saved_filter: savedFilterIdFromUrl + }; + + if (updatedFilter.dynamic_column) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: updatedFilter.dynamic_column.column_name, + comparator: updatedFilter.dynamic_column.comparator + }); + } + + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + + // Update the full filters map + this.savedFilterData = data.sort((a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + this.savedFilterMap = Object.assign({}, ...data.map((filter) => { + const transformedFilter = this.processFiltersData(filter); + return { [filter.id]: transformedFilter }; + })); + } + }, + error: (error) => { + console.error('Error fetching updated filters:', error); + this.loadSavedFilters(); + } + }); + } else { + // Just refresh filters if no filter selected in URL + this.loadSavedFilters(); + } + } + if (arg === 'delete saved filters') { this.loadSavedFilters(); this.selectedFilterSetId = null; @@ -180,7 +236,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { } handleOpenSavedFiltersDialog(filtersSet: any = null) { - this.dialog.open(SavedFiltersDialogComponent, { + const dialogRef = this.dialog.open(SavedFiltersDialogComponent, { width: '56em', data: { connectionID: this.connectionID, @@ -195,6 +251,9 @@ 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 } getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { From 2b0c41d4383d6c5640bcbcdbe263a5459166d65e Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 13 Aug 2025 21:49:53 +0300 Subject: [PATCH 29/29] saved filters: fix unit tests --- .../saved-filters-dialog.component.spec.ts | 126 +++++++++++++----- .../saved-filters-panel.component.spec.ts | 102 +++++++++++--- 2 files changed, 178 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts index 2d4f82837..0e9e1c3c4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts @@ -1,8 +1,10 @@ +import { ActivatedRoute, RouterModule } from '@angular/router'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ConnectionsService } from 'src/app/services/connections.service'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { RouterTestingModule } from '@angular/router/testing'; import { SavedFiltersDialogComponent } from './saved-filters-dialog.component'; import { TablesService } from 'src/app/services/tables.service'; import { of } from 'rxjs'; @@ -14,25 +16,37 @@ describe('SavedFiltersDialogComponent', () => { let connectionsServiceMock: jasmine.SpyObj; beforeEach(async () => { - const tableSpy = jasmine.createSpyObj('TablesService', ['cast', 'createSavedFilter', 'deleteSavedFilter']); + const tableSpy = jasmine.createSpyObj('TablesService', ['cast', 'createSavedFilter', 'deleteSavedFilter', 'updateSavedFilter']); tableSpy.cast = jasmine.createSpyObj('BehaviorSubject', ['subscribe']); - + const connectionSpy = jasmine.createSpyObj('ConnectionsService', [], { currentConnection: { type: 'postgres' } }); await TestBed.configureTestingModule({ - imports: [SavedFiltersDialogComponent], + imports: [ + SavedFiltersDialogComponent, + RouterTestingModule + ], providers: [ { provide: TablesService, useValue: tableSpy }, { provide: ConnectionsService, useValue: connectionSpy }, { provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } }, { provide: MatSnackBar, useValue: { open: jasmine.createSpy('open') } }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: { get: () => {} }, + queryParamMap: { get: () => {} } + } + } + }, { provide: MAT_DIALOG_DATA, useValue: { connectionID: '123', tableName: 'test_table', displayTableName: 'Test Table', - filtersSet: { + filtersSet: { name: 'Test Filter', filters: {} }, @@ -55,22 +69,22 @@ describe('SavedFiltersDialogComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - + it('should toggle dynamic column', () => { const fieldName = 'test_field'; - + // Initially null expect(component.dynamicColumn).toBeNull(); - + // First toggle sets to fieldName component.toggleDynamicColumn(fieldName); expect(component.dynamicColumn).toBe(fieldName); - + // Second toggle sets back to null component.toggleDynamicColumn(fieldName); expect(component.dynamicColumn).toBeNull(); }); - + it('should exclude dynamic column from filters and include it with column_name and comparator', () => { // Setup const fieldName = 'test_field'; @@ -78,10 +92,10 @@ describe('SavedFiltersDialogComponent', () => { component.tableRowFieldsComparator = { [fieldName]: 'eq' }; component.dynamicColumn = fieldName; tablesServiceMock.createSavedFilter.and.returnValue(of({})); - - // Call saveFilter - component.saveFilter(); - + + // Call handleSaveFilters + component.handleSaveFilters(); + // Verify - filters should be empty, and dynamic_column should have column_name and comparator expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( component.data.connectionID, @@ -96,25 +110,25 @@ describe('SavedFiltersDialogComponent', () => { } ); }); - + it('should include dynamic column with column_name and comparator and exclude it from filters', () => { // Setup const fieldName = 'test_field'; const dynamicFieldName = 'dynamic_field'; - component.tableRowFieldsShown = { + component.tableRowFieldsShown = { [fieldName]: 'test_value', [dynamicFieldName]: 'dynamic_value' }; - component.tableRowFieldsComparator = { + component.tableRowFieldsComparator = { [fieldName]: 'eq', [dynamicFieldName]: 'contains' }; component.dynamicColumn = dynamicFieldName; // Different field from the one with comparator tablesServiceMock.createSavedFilter.and.returnValue(of({})); - - // Call saveFilter - component.saveFilter(); - + + // Call handleSaveFilters + component.handleSaveFilters(); + // Verify - only fieldName in filters, dynamicFieldName in dynamic_column expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( component.data.connectionID, @@ -129,17 +143,17 @@ describe('SavedFiltersDialogComponent', () => { } ); }); - + it('should handle empty values in filters as null', () => { // Setup const fieldName = 'test_field'; component.tableRowFieldsShown = { [fieldName]: '' }; // Empty value component.tableRowFieldsComparator = { [fieldName]: 'eq' }; tablesServiceMock.createSavedFilter.and.returnValue(of({})); - - // Call saveFilter - component.saveFilter(); - + + // Call handleSaveFilters + component.handleSaveFilters(); + // Verify - value should be null instead of empty string expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( component.data.connectionID, @@ -150,25 +164,25 @@ describe('SavedFiltersDialogComponent', () => { } ); }); - + it('should handle dynamic column with no comparator', () => { // Setup const fieldName = 'test_field'; const dynamicFieldName = 'dynamic_field_no_comparator'; - component.tableRowFieldsShown = { + component.tableRowFieldsShown = { [fieldName]: 'test_value', [dynamicFieldName]: 'dynamic_value' }; - component.tableRowFieldsComparator = { + component.tableRowFieldsComparator = { [fieldName]: 'eq' // No comparator for dynamicFieldName }; component.dynamicColumn = dynamicFieldName; tablesServiceMock.createSavedFilter.and.returnValue(of({})); - - // Call saveFilter - component.saveFilter(); - + + // Call handleSaveFilters + component.handleSaveFilters(); + // Verify - dynamic_column should have empty comparator expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( component.data.connectionID, @@ -183,4 +197,52 @@ describe('SavedFiltersDialogComponent', () => { } ); }); + + it('should update existing filter when id is present', () => { + // Setup + const fieldName = 'test_field'; + const filterId = '123'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.data.filtersSet.id = filterId; + tablesServiceMock.updateSavedFilter.and.returnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify updateSavedFilter is called instead of createSavedFilter + expect(tablesServiceMock.updateSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + filterId, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } } + } + ); + expect(tablesServiceMock.createSavedFilter).not.toHaveBeenCalled(); + }); + + it('should create new filter when id is not present', () => { + // Setup + const fieldName = 'test_field'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.data.filtersSet.id = undefined; // No ID means creating a new filter + tablesServiceMock.createSavedFilter.and.returnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify createSavedFilter is called + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } } + } + ); + expect(tablesServiceMock.updateSavedFilter).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts index 1dffa9a12..38fd06149 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts @@ -1,11 +1,32 @@ import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { MatDialog } from '@angular/material/dialog'; import { SavedFiltersPanelComponent } from './saved-filters-panel.component'; import { TablesService } from 'src/app/services/tables.service'; import { of } from 'rxjs'; +// We need to mock the JsonURL import used in the component +jasmine.getEnv().allowRespy(true); // Allow respying on the same object +class JsonURLMock { + static stringify(obj: any): string { + return JSON.stringify(obj); + } + + static parse(str: string): any { + try { + return JSON.parse(str); + } catch (e) { + return {}; + } + } +} + +// Add to global scope to be used by the component +(window as any).JsonURL = JsonURLMock; + describe('SavedFiltersPanelComponent', () => { let component: SavedFiltersPanelComponent; let fixture: ComponentFixture; @@ -29,22 +50,38 @@ describe('SavedFiltersPanelComponent', () => { const tablesServiceMock = jasmine.createSpyObj('TablesService', ['getSavedFilters', 'createSavedFilter']); tablesServiceMock.getSavedFilters.and.returnValue(of([mockFilter])); tablesServiceMock.cast = of({}); - + const routerMock = jasmine.createSpyObj('Router', ['navigate']); - + const activatedRouteMock = { queryParams: of({}), + paramMap: of(convertToParamMap({})), + queryParamMap: of(convertToParamMap({})), snapshot: { - queryParams: {} + queryParams: {}, + paramMap: { + get: (key: string) => null + }, + queryParamMap: { + get: (key: string) => null + } } }; const matDialogMock = jasmine.createSpyObj('MatDialog', ['open']); - + + const connectionsServiceMock = jasmine.createSpyObj('ConnectionsService', [], { + currentConnection: { type: 'postgres' } + }); + await TestBed.configureTestingModule({ - imports: [SavedFiltersPanelComponent], + imports: [ + SavedFiltersPanelComponent, + HttpClientTestingModule + ], providers: [ { provide: TablesService, useValue: tablesServiceMock }, + { provide: ConnectionsService, useValue: connectionsServiceMock }, { provide: Router, useValue: routerMock }, { provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: MatDialog, useValue: matDialogMock } @@ -53,32 +90,38 @@ describe('SavedFiltersPanelComponent', () => { tablesServiceSpy = TestBed.inject(TablesService) as jasmine.SpyObj; routerSpy = TestBed.inject(Router) as jasmine.SpyObj; - + fixture = TestBed.createComponent(SavedFiltersPanelComponent); component = fixture.componentInstance; component.connectionID = 'conn1'; component.selectedTableName = 'users'; component.structure = []; component.tableTypes = {}; + component.selectedTableDisplayName = 'Users'; + component.tableForeignKeys = []; + + // Mock filterSelected event emitter + spyOn(component.filterSelected, 'emit'); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); - + it('should process filters data to separate static filters and dynamic column', () => { const result = component.processFiltersData(mockFilter); - + expect(result.dynamicColumn).toBeTruthy(); expect(result.dynamicColumn?.column).toBe('city'); expect(result.dynamicColumn?.operator).toBe('eq'); - + expect(result.staticFilters.length).toBe(2); expect(result.staticFilters[0].column).toBe('name'); expect(result.staticFilters[1].column).toBe('age'); }); - + it('should update dynamic column comparator', () => { component.selectedFilterSetId = 'filter1'; component.savedFilterMap = { @@ -86,35 +129,58 @@ describe('SavedFiltersPanelComponent', () => { dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } } }; - + component.updateDynamicColumnComparator('contains'); - + expect(component.savedFilterMap.filter1.dynamicColumn.operator).toBe('contains'); }); - + it('should set value to empty string when comparator is empty', () => { + // Setup component with the minimal required properties component.selectedFilterSetId = 'filter1'; component.savedFilterMap = { filter1: { - dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, + filters: { city: { eq: 'New York' } } } }; - + + // Spy on applyDynamicColumnChanges to prevent it from executing + spyOn(component, 'applyDynamicColumnChanges'); + + // Call the method under test component.updateDynamicColumnComparator('empty'); - + + // Verify the value was set to empty string expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe(''); }); - + it('should update dynamic column value', () => { + // Setup component with the minimal required properties component.selectedFilterSetId = 'filter1'; component.savedFilterMap = { filter1: { - dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, + filters: { city: { eq: 'New York' } } } }; + // Spy on applyDynamicColumnChanges to prevent it from executing + spyOn(component, 'applyDynamicColumnChanges'); + + // Replace setTimeout with a function that executes immediately + spyOn(window, 'setTimeout').and.callFake((fn) => { + // Execute function immediately instead of waiting + fn(); + // Return a fake timer ID + return 999; + }); + + // Call the method under test component.updateDynamicColumnValue('Chicago'); + // Verify the value was updated expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe('Chicago'); + expect(component.applyDynamicColumnChanges).toHaveBeenCalled(); }); });