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..428a6ee2f 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 (removeFilter)="removeFilter($event)" (resetAllFilters)="clearAllFilters()" (search)="search($event)" - (activateActions)="activateActions($event)"> - + (activateActions)="activateActions($event)" + (applyFilter)="applyFilter($event)"> + { 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,17 +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); - }) + 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) { @@ -279,21 +285,27 @@ 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; if (Object.keys(nonEmptyFilters).length) { this.filters = {}; for (const key in nonEmptyFilters) { - if (this.comparators[key] !== undefined) { - this.filters[key] = { - [this.comparators[key]]: nonEmptyFilters[key] - }; - } + if (this.comparators[key] !== undefined) { + this.filters[key] = { + [this.comparators[key]]: nonEmptyFilters[key] + }; + } } + 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 +314,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 +324,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 +339,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 +353,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 +364,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 +376,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]; @@ -376,7 +395,13 @@ export class DashboardComponent implements OnInit, OnDestroy { shownColumns }); }); + } + applyFilter(filters: any) { + 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-action-link-dialog/db-action-link-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-action-link-dialog/db-action-link-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/action-delete-dialog/action-delete-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/action-delete-dialog/action-delete-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.css diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.html diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts similarity index 97% rename from frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts index 0a9d33da1..8a2d9c08d 100644 --- a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts @@ -4,18 +4,18 @@ import { Component, OnInit } from '@angular/core'; import { CustomAction, CustomActionMethod, CustomActionType, CustomEvent, EventType, Rule } from 'src/app/models/table'; import { ActionDeleteDialogComponent } from './action-delete-dialog/action-delete-dialog.component'; -import { AlertComponent } from '../../ui-components/alert/alert.component'; -import { BreadcrumbsComponent } from '../../ui-components/breadcrumbs/breadcrumbs.component'; +import { AlertComponent } from '../../../ui-components/alert/alert.component'; +import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { CodeEditorModule } from '@ngstack/code-editor'; import { CommonModule } from '@angular/common'; import { CompanyMember } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { ContentLoaderComponent } from '../../ui-components/content-loader/content-loader.component'; +import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; -import { IconPickerComponent } from '../../ui-components/icon-picker/icon-picker.component'; +import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; diff --git a/frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css diff --git a/frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html diff --git a/frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-ai-panel/db-table-ai-panel.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-export-dialog/db-table-export-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/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 similarity index 81% rename from frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts index 897e3bb01..177217d3a 100644 --- a/frontend/src/app/components/dashboard/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 @@ -24,7 +24,7 @@ import JsonURL from "@jsonurl/jsonurl"; import { DynamicModule } from 'ng-dynamic-component'; import { RouterModule } from '@angular/router'; import { MatDialogModule } from '@angular/material/dialog'; -import { ContentLoaderComponent } from '../../ui-components/content-loader/content-loader.component'; +import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component'; import { Angulartics2OnModule } from 'angulartics2'; @Component({ @@ -92,19 +92,33 @@ export class DbTableFiltersDialogComponent implements OnInit { })); const queryParams = this.route.snapshot.queryParams; - const filters = JsonURL.parse(queryParams.filters); - const filtersValues = getFiltersFromUrl(filters); - 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-import-dialog/db-table-import-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css diff --git a/frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.html diff --git a/frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-import-dialog/db-table-import-dialog.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts similarity index 97% rename from frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts index cc96f5e29..1ab7e6d2f 100644 --- a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts @@ -14,13 +14,13 @@ import { MatListModule } from '@angular/material/list'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { NotificationsService } from 'src/app/services/notifications.service'; -import { PlaceholderRecordViewComponent } from '../../skeletons/placeholder-record-view/placeholder-record-view.component'; +import { PlaceholderRecordViewComponent } from '../../../skeletons/placeholder-record-view/placeholder-record-view.component'; import { TableStateService } from 'src/app/services/table-state.service'; import { TablesService } from 'src/app/services/tables.service'; import { formatFieldValue } from 'src/app/lib/format-field-value'; -import { ForeignKeyRecordViewComponent } from '../../ui-components/record-view-fields/foreign-key/foreign-key.component'; import { UIwidgets, recordViewFieldTypes } from 'src/app/consts/record-view-types'; import { DynamicModule } from 'ng-dynamic-component'; +import { ForeignKeyRecordViewComponent } from 'src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component'; @Component({ selector: 'app-db-table-row-view', diff --git a/frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.css diff --git a/frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.html similarity index 100% rename from frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.html diff --git a/frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts similarity index 100% rename from frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts diff --git a/frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts similarity index 94% rename from frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts index af278a4ff..88a4dd17d 100644 --- a/frontend/src/app/components/dashboard/db-table-settings/db-table-settings.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts @@ -2,31 +2,31 @@ import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; import { Component, Inject, OnInit } from '@angular/core'; import { TableField, TableOrdering, TableSettings } from 'src/app/models/table'; -import { AlertComponent } from '../../ui-components/alert/alert.component'; +import { AlertComponent } from '../../../ui-components/alert/alert.component'; import { Angulartics2 } from 'angulartics2'; -import { BreadcrumbsComponent } from '../../ui-components/breadcrumbs/breadcrumbs.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 { DragDropModule } from '@angular/cdk/drag-drop'; import { FormsModule } from '@angular/forms'; -import { IconPickerComponent } from '../../ui-components/icon-picker/icon-picker.component'; +import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; import { Location } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { NgForm } from '@angular/forms'; -import { PlaceholderTableSettingsComponent } from '../../skeletons/placeholder-table-settings/placeholder-table-settings.component'; +import { PlaceholderTableSettingsComponent } from '../../../skeletons/placeholder-table-settings/placeholder-table-settings.component'; import { Router } from '@angular/router'; import { RouterModule } from '@angular/router'; import { TablesService } from 'src/app/services/tables.service'; import { Title } from '@angular/platform-browser'; import { normalizeTableName } from 'src/app/lib/normalize'; -import { MatButtonModule } from '@angular/material/button'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { CompanyService } from 'src/app/services/company.service'; @Component({ selector: 'app-db-table-settings', diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css similarity index 100% rename from frontend/src/app/components/dashboard/db-table/db-table.component.css rename to frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html similarity index 97% rename from frontend/src/app/components/dashboard/db-table/db-table.component.html rename to frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 64474682d..dac863d53 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -169,7 +169,7 @@

{{ displayName }}

-
+
{{ getFilter(activeFilter) }} @@ -179,6 +179,18 @@

{{ displayName }}

+ +
diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts similarity index 95% rename from frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts rename to frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts index 7af381b95..5dbea58e1 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts @@ -1,22 +1,22 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Angulartics2Module } from 'angulartics2'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DbTableComponent } from './db-table.component'; +import { DbTableViewComponent } from './db-table-view.component'; import { FormsModule } from '@angular/forms'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSortModule } from '@angular/material/sort'; import { SelectionModel } from '@angular/cdk/collections'; import { TablesDataSource } from '../db-tables-data-source'; -import { MatDialogModule } from '@angular/material/dialog'; import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; -import { Angulartics2Module } from 'angulartics2'; -describe('DbTableComponent', () => { - 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 1610f42ab..9770a1c93 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,25 +33,17 @@ 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 { SavedFiltersPanelComponent } from './saved-filters-panel/saved-filters-panel.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 { tap } from 'rxjs/operators'; import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; interface Column { @@ -74,9 +52,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 +78,12 @@ 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 + SavedFiltersPanelComponent ] }) -export class DbTableComponent implements OnInit { +export class DbTableViewComponent implements OnInit { @Input() name: string; @Input() displayName: string; @@ -146,9 +102,13 @@ export class DbTableComponent 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(); + @Output() applyFilter = new EventEmitter(); + // public tablesSwitchControl = new FormControl(''); public tableData: any; public filteredTables: TableProperties[]; @@ -173,7 +133,7 @@ export class DbTableComponent 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; @@ -220,6 +180,7 @@ export class DbTableComponent 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]; @@ -227,6 +188,11 @@ export class DbTableComponent implements OnInit { this._tableState.cast.subscribe(row => { this.selectedRow = row; }); + + this.route.queryParams.subscribe(params => { + this.hasSavedFilterActive = !!params.saved_filter; + if (this.hasSavedFilterActive ) this.searchString = ''; + }); } onInput(searchValue: string) { @@ -294,7 +260,7 @@ export class DbTableComponent implements OnInit { } getFiltersCount(activeFilters: object) { - if (activeFilters) return Object.keys(activeFilters).length; + if (activeFilters && !this.hasSavedFilterActive) return Object.keys(activeFilters).length; return 0; } @@ -546,4 +512,9 @@ export class DbTableComponent 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-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 c80672a8c..c1e7671cd 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/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..6deec4ede --- /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,94 @@ +.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 160px 32px; + grid-column-gap: 8px; + align-content: flex-start; + align-items: flex-start; +} + +.filters-select { + grid-column: 1 / span 6; + margin-bottom: 16px; +} + +.filter-line { + grid-column: 1 / span 4; +} + +.dynamic-column-radio { + grid-column: 5; + margin-top: 8px; +} + +.filter-save-form { + grid-column: 1 / span 6; + margin-bottom: 24px; +} + +.section-title { + 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 { + grid-column: 1 / -1; +} + +.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: 12px; +} + +.filter-delete-button { + margin-top: 4px; +} + +.settings-form__reset-button { + margin-right: auto; +} + +.no-filters-message { + 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 new file mode 100644 index 000000000..f44bab341 --- /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,174 @@ +
+

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

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

Define Filters and Dynamic Column

+

Select filters to apply and optionally mark one column as dynamic

+ + + + 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 + + + + + + +
+ + Dynamic column + +
+ +
+
+
+ + + + + + +
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..0e9e1c3c4 --- /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,248 @@ +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'; + +describe('SavedFiltersDialogComponent', () => { + let component: SavedFiltersDialogComponent; + let fixture: ComponentFixture; + let tablesServiceMock: jasmine.SpyObj; + let connectionsServiceMock: jasmine.SpyObj; + + beforeEach(async () => { + 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, + 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: { + name: 'Test Filter', + filters: {} + }, + structure: [], + tableForeignKeys: {}, + tableWidgets: [] + }} + ] + }) + .compileComponents(); + + tablesServiceMock = TestBed.inject(TablesService) as jasmine.SpyObj; + connectionsServiceMock = TestBed.inject(ConnectionsService) as jasmine.SpyObj; + + fixture = TestBed.createComponent(SavedFiltersDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + 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'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.dynamicColumn = fieldName; + tablesServiceMock.createSavedFilter.and.returnValue(of({})); + + // 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, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { }, + dynamic_column: { + column_name: fieldName, + comparator: 'eq' + } + } + ); + }); + + 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 = { + [fieldName]: 'test_value', + [dynamicFieldName]: 'dynamic_value' + }; + component.tableRowFieldsComparator = { + [fieldName]: 'eq', + [dynamicFieldName]: 'contains' + }; + component.dynamicColumn = dynamicFieldName; // Different field from the one with comparator + tablesServiceMock.createSavedFilter.and.returnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - only fieldName in filters, dynamicFieldName in dynamic_column + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + dynamic_column: { + column_name: dynamicFieldName, + comparator: 'contains' + } + } + ); + }); + + 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 handleSaveFilters + component.handleSaveFilters(); + + // Verify - value should be null instead of empty string + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: null } } + } + ); + }); + + it('should handle dynamic column with no comparator', () => { + // Setup + const fieldName = 'test_field'; + const dynamicFieldName = 'dynamic_field_no_comparator'; + component.tableRowFieldsShown = { + [fieldName]: 'test_value', + [dynamicFieldName]: 'dynamic_value' + }; + component.tableRowFieldsComparator = { + [fieldName]: 'eq' + // No comparator for dynamicFieldName + }; + component.dynamicColumn = dynamicFieldName; + tablesServiceMock.createSavedFilter.and.returnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - dynamic_column should have empty comparator + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + dynamic_column: { + column_name: dynamicFieldName, + comparator: '' + } + } + ); + }); + + 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-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..ee1ba6552 --- /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,302 @@ +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 { MatCheckboxModule } from '@angular/material/checkbox'; +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, + MatCheckboxModule, + 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() filtersSet: 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; + public dynamicColumn: string | null = null; + + 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(); + + 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; + }, {}); + + // Initialize dynamic column if it exists in the filters set + if (this.data.filtersSet.dynamic_column && this.data.filtersSet.dynamic_column.column_name) { + this.tableRowFieldsShown[this.data.filtersSet.dynamic_column.column_name] = null; + this.tableRowFieldsComparator[this.data.filtersSet.dynamic_column.column_name] = this.data.filtersSet.dynamic_column.comparator || ''; + this.dynamicColumn = this.data.filtersSet.dynamic_column.column_name; + } + } + + // 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 || '')), + ); + } + + 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) { + console.log('Updating comparator for field:', fieldName, 'obj', this.tableRowFieldsComparator); + if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; + } + + 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) { + 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]; + if (this.dynamicColumn === field) { + this.dynamicColumn = null; + } + this.updateFiltersCount(); + } + + updateFiltersCount() { + this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; + } + + toggleDynamicColumn(field: string) { + if (this.dynamicColumn === field) { + this.dynamicColumn = null; + } else { + this.dynamicColumn = field; + } + } + + handleSaveFilters() { + let payload; + if (Object.keys(this.tableRowFieldsShown).length) { + let filters = {}; + + for (const key in this.tableRowFieldsShown) { + // Skip fields that are marked as dynamic column + if (key === this.dynamicColumn) { + continue; + } + + if (this.tableRowFieldsComparator[key] !== undefined) { + // If value is empty or undefined, use null + const value = this.tableRowFieldsShown[key] === '' || this.tableRowFieldsShown[key] === undefined ? + null : this.tableRowFieldsShown[key]; + + filters[key] = { + [this.tableRowFieldsComparator[key]]: value + }; + } + } + + // const filters = JsonURL.stringify( this.filters ); + payload = { + name: this.data.filtersSet.name, + filters + }; + + // Only add dynamic_column if one is selected + if (this.dynamicColumn) { + // Create object with column_name and comparator properties + payload['dynamic_column'] = { + column_name: this.dynamicColumn, + comparator: this.tableRowFieldsComparator[this.dynamicColumn] || '' + }; + } + + 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 new file mode 100644 index 000000000..6863fb37e --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -0,0 +1,66 @@ +.saved-filters-container { + display: flex; + flex-direction: column; + gap: 8px; + margin: 20px 0 4px; +} + +.saved-filters-list { + display: flex; + align-items: center; + gap: 16px; +} + +/* .saved-filters-tabs { + --mdc-chip-container-shape-radius: 4px; +} */ + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip { + --mdc-chip-container-shape-radius: 4px; + --mdc-chip-outline-width: 1px; + --mdc-chip-outline-color: rgba(0,0,0,0.24); + --mdc-chip-elevated-container-color: transparent !important; + --mdc-chip-elevated-selected-container-color: var(--color-accentedPalette-500) !important; + --mdc-chip-container-height: 32px; + --mdc-chip-label-text-color: rgba(0,0,0,0.64); +} + +.filters-container { + display: flex; + gap: 20px; + transform: translateX(-10%) scale(0.8); +} + +.dynamic-column-editor { + display: flex; + align-items: center; + gap: 16px; + /* transform: translateX(-20%) scale(0.8); */ + /* transform-origin: center right; */ +} + +.column-name { + color: rgba(0,0,0,0.64); + margin-top: -8px; +} + +.column-name strong { + margin-left: 8px; +} + +.static-filters { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + transform: translateX(5%) scale(1.1); + padding-bottom: 16px; +} + +.static-filter-chip { + cursor: default; +} + +/* .dynamic-column-editor + .static-filters { + margin-top: -16px; +} */ 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..7384e9123 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html @@ -0,0 +1,126 @@ +
+
+ + + + + + + + + + + {{ filter.name }} + + +
+ +
+
+ where + + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + + + + + starts with + ends with + equal + contains + not contains + is empty + + + + + + equal + greater than + less than + greater than or equal + less than or equal + + + + +
+
+ +
+ + + + +
+ + +
+ +
+
+
+ + {{ getFilter(filter) }} + +
+
+
\ 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 new file mode 100644 index 000000000..38fd06149 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts @@ -0,0 +1,186 @@ +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; + 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({}), + paramMap: of(convertToParamMap({})), + queryParamMap: of(convertToParamMap({})), + snapshot: { + 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, + HttpClientTestingModule + ], + providers: [ + { provide: TablesService, useValue: tablesServiceMock }, + { provide: ConnectionsService, useValue: connectionsServiceMock }, + { 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 = {}; + 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 = { + 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', () => { + // Setup component with the minimal required properties + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + 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' }, + 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(); + }); +}); 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..4106ef5a6 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -0,0 +1,550 @@ +import { ActivatedRoute, Router } from '@angular/router'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { TableField, TableForeignKey } from 'src/app/models/table'; + +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'; +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 + ], + templateUrl: './saved-filters-panel.component.html', + styleUrl: './saved-filters-panel.component.css' +}) +export class SavedFiltersPanelComponent implements OnInit, OnDestroy { + @Input() connectionID: string; + @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(); + @Input() resetSelection: boolean = false; + + @Input() accessLevel: AccessLevel; + + private dynamicColumnValueDebounceTimer: any = null; + + public savedFilterData: any[] = []; + public savedFilterMap: { [key: string]: any } = {}; + + public selectedFilterSetId: string | null = null; + public selectedFilter: any = null; + + public tableStructure: any = null; + public tableRowFieldsShown: { [key: string]: any } = {}; + public tableRowStructure: { [key: string]: any } = {}; + public tableWidgetsList: string[] = []; + public UIwidgets = UIwidgets; + + public displayedComparators = { + eq: "=", + gt: ">", + lt: "<", + gte: ">=", + 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 + ) {} + + ngOnInit() { + this.route.paramMap.subscribe(params => { + const tableNameFromUrl = params.get('table-name'); + if (tableNameFromUrl) { + this.selectedTableName = tableNameFromUrl; + this.loadSavedFilters(); + } + }); + + this.route.queryParamMap.subscribe(params => { + const savedFilterId = params.get('saved_filter'); + if (savedFilterId) { + this.selectedFilterSetId = savedFilterId; + } else { + this.selectedFilterSetId = null; + } + }); + + this._tables.cast.subscribe((arg) => { + 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; + this.filterSelected.emit(null); + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + }); + + this.tableRowStructure = Object.assign({}, ...this.structure.map((field: TableField) => { + return {[field.column_name]: field}; + })); + } + + loadSavedFilters() { + if (!this.connectionID || !this.selectedTableName) { + return; + } + + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ + next: (data) => { + if (data) { + 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 }; + })); + + const params = this.route.snapshot.queryParams; + const dynamicColumn = params['dynamic_column'] ? JsonURL.parse(params['dynamic_column']) : null; + + if (this.selectedFilterSetId && this.savedFilterData.length > 0) { + if (dynamicColumn && this.savedFilterMap[this.selectedFilterSetId]) { + const filters = params['filters'] ? JsonURL.parse(params['filters']) : {}; + + this.savedFilterMap[this.selectedFilterSetId].dynamicColumn = { + column: dynamicColumn.column_name, + operator: dynamicColumn.comparator, + value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null + }; + } + } + } + }, + error: (error) => { + console.error('Error fetching saved filters:', error); + } + }); + } + + ngOnChanges() { + if (this.resetSelection) { + this.selectedFilterSetId = null; + } + } + + ngOnDestroy() { + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + } + + handleOpenSavedFiltersDialog(filtersSet: any = null) { + const dialogRef = this.dialog.open(SavedFiltersDialogComponent, { + width: '56em', + data: { + connectionID: this.connectionID, + tableName: this.selectedTableName, + displayTableName: this.selectedTableDisplayName, + structure: this.structure, + tableForeignKeys: this.tableForeignKeys, + tableWidgets: this.tableWidgets, + filtersSet: filtersSet ? filtersSet : { + name: '', + filters: {} + } + } + }); + + // 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 }[] { + 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; + } + + processFiltersData(filter: any) { + 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 + }; + + if (filter.dynamic_column && filter.dynamic_column.column_name) { + transformedFilter.dynamicColumn = { + column: filter.dynamic_column.column_name, + operator: filter.dynamic_column.comparator, + value: null + }; + + const dynamicColFilters = filter.filters && filter.filters[filter.dynamic_column.column_name]; + if (dynamicColFilters) { + const operator = filter.dynamic_column.comparator; + transformedFilter.dynamicColumn.value = dynamicColFilters[operator]; + } + } + + transformedFilter.staticFilters = this.getFilterEntries(filter.filters) + .filter(entry => !filter.dynamic_column || entry.column !== filter.dynamic_column.column_name); + + return transformedFilter; + } + private buildQueryParams(additionalParams: any = {}): any { + const currentParams = this.route.snapshot.queryParams; + + // Start with pagination parameters + const queryParams: any = { + page_index: 0, + page_size: currentParams['page_size'] || 30, + ...additionalParams + }; + + // Preserve sort parameters if present + if (currentParams['sort_active']) { + queryParams.sort_active = currentParams['sort_active']; + } + + if (currentParams['sort_direction']) { + queryParams.sort_direction = currentParams['sort_direction']; + } + + return queryParams; + } + + selectFiltersSet(selectedFilterSetId: string): void { + console.log('selectFiltersSet ID:', selectedFilterSetId); + if (this.selectedFilterSetId === selectedFilterSetId) { + this.selectedFilterSetId = null; + this.filterSelected.emit(null); + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } else { + this.selectedFilterSetId = selectedFilterSetId; + const selectedFilter = this.savedFilterMap[selectedFilterSetId]; + this.filterSelected.emit(selectedFilter); + + const additionalParams: any = { + filters: JsonURL.stringify(selectedFilter.filters), + saved_filter: selectedFilterSetId + }; + + if (selectedFilter.dynamicColumn) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }); + } + + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + } + + selectFilter(entry: { column: string; operator: string; value: any }) { + this.selectedFilter = entry; + } + + 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}` + } + } + + 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'; + } + } + + 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; + } + + updateDynamicColumnComparator = (comparator: string) => { + 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; + } + + selectedFilter.dynamicColumn.operator = comparator; + + if (comparator === 'empty') { + selectedFilter.dynamicColumn.value = ''; + } + } + + updateDynamicColumnValue = (value: any) => { + 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; + } + + console.log(value, 'value in updateDynamicColumnValue'); + + selectedFilter.dynamicColumn.value = value; + + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + + this.dynamicColumnValueDebounceTimer = setTimeout(() => { + this.applyDynamicColumnChanges(); + }, 800); + } + + 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; + + const dynamicColumn = { + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator + }; + + const filterValue = selectedFilter.dynamicColumn.value === '' || selectedFilter.dynamicColumn.value === undefined ? null : selectedFilter.dynamicColumn.value; + + const filters = { ...selectedFilter.filters }; + + if (selectedFilter.dynamicColumn.column && + selectedFilter.dynamicColumn.operator && + filterValue !== null) { + filters[selectedFilter.dynamicColumn.column] = { + [selectedFilter.dynamicColumn.operator]: filterValue + }; + } else { + if (filters[selectedFilter.dynamicColumn.column]) { + delete filters[selectedFilter.dynamicColumn.column]; + } + } + + console.log('applyDynamicColumnChanges, filters:', filters); + + // Build filter-related params using the helper method + const additionalParams: any = { + filters: JsonURL.stringify(filters), + dynamic_column: JsonURL.stringify(dynamicColumn), + saved_filter: this.selectedFilterSetId + }; + + const queryParams = this.buildQueryParams(additionalParams); + + const updatedFilterData = { + ...selectedFilter, + filters: filters, + dynamic_column: dynamicColumn + }; + + updatedFilterData.filterEntries = this.getFilterEntries(filters); + + this.filterSelected.emit(updatedFilterData); + + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { + queryParams + }); + } +} 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 5f71c3e27..f7c9986c4 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'; diff --git a/frontend/src/app/consts/filter-types.ts b/frontend/src/app/consts/filter-types.ts index 6e31ede37..4da90fce5 100644 --- a/frontend/src/app/consts/filter-types.ts +++ b/frontend/src/app/consts/filter-types.ts @@ -24,6 +24,7 @@ export const UIwidgets = { Date: DateFilterComponent, DateTime: DateTimeFilterComponent, File: FileFilterComponent, + Foreign_key: ForeignKeyFilterComponent, JSON: JsonEditorFilterComponent, Number: NumberFilterComponent, Password: PasswordFilterComponent, @@ -32,6 +33,9 @@ export const UIwidgets = { String: TextFilterComponent, Textarea: LongTextFilterComponent, Time: TimeFilterComponent, + TimeInterval: TimeIntervalFilterComponent, + Point: PointFilterComponent, + ID: IdFilterComponent, } export const filterTypes = { diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index 85498185e..049c9dc40 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -510,4 +510,81 @@ 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: { + tableName + } + }) + .pipe( + map(res => { + this.tables.next('filters set saved'); + this._notifications.showSuccessSnackbar('Saved filters have been updated.') + return res + }), + catchError((err) => { + console.log(err); + this._notifications.showErrorSnackbar(err.error.message); + return EMPTY; + }) + ) + } + + 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('filters set updated'); + this._notifications.showSuccessSnackbar('Saved filter has been updated.') + return res + }), + catchError((err) => { + console.log(err); + this._notifications.showErrorSnackbar(err.error.message); + 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.showErrorSnackbar(err.error.message); + return EMPTY; + }) + ) + } }