diff --git a/frontend/package.json b/frontend/package.json index 2cb9d381d..8c4996130 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,7 +49,7 @@ "lodash-es": "^4.17.21", "mermaid": "^11.12.1", "monaco-editor": "0.55.1", - "ng-dynamic-component": "^10.7.0", + "ng-dynamic-component": "^10.8.0", "ngx-cookie-service": "^19.0.0", "ngx-markdown": "^19.1.1", "ngx-stripe": "^19.0.0", diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts index 62ecfd772..8c922838b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts @@ -1,286 +1,299 @@ -import { Component, OnInit, AfterViewInit, Inject, KeyValueDiffers, KeyValueDiffer } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { AfterViewInit, Component, Inject, KeyValueDiffer, KeyValueDiffers, OnInit } from '@angular/core'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule } 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 { TablesService } from 'src/app/services/tables.service'; -import { TableField, TableForeignKey, Widget } from 'src/app/models/table'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { UIwidgets } from 'src/app/consts/record-edit-types'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import JsonURL from '@jsonurl/jsonurl'; +import { Angulartics2OnModule } from 'angulartics2'; +import JSON5 from 'json5'; +import { DynamicModule } from 'ng-dynamic-component'; +import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; import { filterTypes } from 'src/app/consts/filter-types'; -import { ActivatedRoute } from '@angular/router'; +import { UIwidgets } from 'src/app/consts/record-edit-types'; import { getComparatorsFromUrl, getFiltersFromUrl } from 'src/app/lib/parse-filter-params'; import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; -import JSON5 from 'json5'; -import { map, startWith } from 'rxjs/operators'; -import { Observable, } from 'rxjs'; -import { FormControl } from '@angular/forms'; -import JsonURL from "@jsonurl/jsonurl"; -import { DynamicModule } from 'ng-dynamic-component'; -import { RouterModule } from '@angular/router'; -import { MatDialogModule } from '@angular/material/dialog'; +import { TableField, TableForeignKey, Widget } from 'src/app/models/table'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component'; -import { Angulartics2OnModule } from 'angulartics2'; @Component({ - selector: 'app-db-table-filters-dialog', - templateUrl: './db-table-filters-dialog.component.html', - styleUrls: ['./db-table-filters-dialog.component.css'], - imports: [ - CommonModule, - ReactiveFormsModule, - FormsModule, - MatAutocompleteModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatSelectModule, - DynamicModule, - RouterModule, - MatDialogModule, - Angulartics2OnModule, - ContentLoaderComponent - ] + selector: 'app-db-table-filters-dialog', + templateUrl: './db-table-filters-dialog.component.html', + styleUrls: ['./db-table-filters-dialog.component.css'], + imports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatSelectModule, + DynamicModule, + SignalComponentIoModule, + RouterModule, + MatDialogModule, + Angulartics2OnModule, + ContentLoaderComponent, + ], }) export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { - - public tableFilters = []; - public fieldSearchControl = new FormControl(''); - - public fields: string[]; - public foundFields: Observable; - - public tableRowFields: Object; - public tableRowStructure: Object; - public tableRowFieldsShown: Record = {}; - public tableRowFieldsComparator: Object = {}; - public tableForeignKeys: {[key: string]: TableForeignKey}; - public tableFiltersCount: number; - public differ: KeyValueDiffer; - public tableTypes: Object; - public tableWidgets: object; - public tableWidgetsList: string[] = []; - public UIwidgets = UIwidgets; - public autofocusField: string | null = null; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _connections: ConnectionsService, - private _tables: TablesService, - public route: ActivatedRoute, - private differs: KeyValueDiffers - ) { - this.differ = this.differs.find({}).create(); - } - - ngOnInit(): void { - this._tables.cast.subscribe(); - this.tableForeignKeys = {...this.data.structure.foreignKeys}; - this.tableRowFields = Object.assign({}, ...this.data.structure.structure.map((field: TableField) => ({[field.column_name]: undefined}))); - this.tableTypes = getTableTypes(this.data.structure.structure, this.data.structure.foreignKeysList); - this.fields = this.data.structure.structure - .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') - .map((field: TableField) => field.column_name); - // this.foundFields = [...this.fields]; - this.tableRowStructure = Object.assign({}, ...this.data.structure.structure.map((field: TableField) => { - return {[field.column_name]: field}; - })); - - // Set autofocus field if provided - if (this.data.autofocusField) { - this.autofocusField = this.data.autofocusField; - } - - const queryParams = this.route.snapshot.queryParams; - - // If saved_filter is present in queryParams, show empty form without applying filters - if (queryParams.saved_filter) { - // Show empty form without filters - this.tableFilters = []; - this.tableRowFieldsShown = {}; - this.tableRowFieldsComparator = {}; - } else { - // Original behavior - parse and apply filters from URL - let filters = {}; - if (queryParams.filters) filters = JsonURL.parse(queryParams.filters); - // const filters = JsonURL.parse(queryParams.filters || '{}'); - const filtersValues = getFiltersFromUrl(filters); - - console.log('Parsed filters from URL:', filtersValues); - - if (Object.keys(filtersValues).length) { - this.tableFilters = Object.keys(filtersValues).map(key => key); - this.tableRowFieldsShown = filtersValues; - 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'}))); - } - } - } - - if (this.data.structure.widgets && this.data.structure.widgets.length) { - this.setWidgets(this.data.structure.widgets); - } - - // If autofocusField is provided, ensure it's in the filters list - if (this.autofocusField && this.tableFilters && !this.tableFilters.includes(this.autofocusField)) { - this.tableFilters.push(this.autofocusField); - if (!this.tableRowFieldsShown[this.autofocusField]) { - this.tableRowFieldsShown[this.autofocusField] = undefined; - } - if (!this.tableRowFieldsComparator[this.autofocusField]) { - this.tableRowFieldsComparator[this.autofocusField] = 'eq'; - } - } - - this.foundFields = this.fieldSearchControl.valueChanges.pipe( - startWith(''), - map(value => this._filter(value || '')), - ); - - } - - ngAfterViewInit(): void { - // Set focus on the autofocus field after view is initialized - if (this.autofocusField) { - setTimeout(() => { - this.focusOnField(this.autofocusField); - }, 200); - } - } - - focusOnField(fieldName: string) { - // Try multiple selectors to find the input field - const selectors = [ - `input[name*="${fieldName}"]`, - `textarea[name*="${fieldName}"]`, - `[data-field="${fieldName}"] input`, - `[data-field="${fieldName}"] textarea`, - `mat-form-field:has([name*="${fieldName}"]) input`, - `mat-form-field:has([name*="${fieldName}"]) textarea` - ]; - - for (const selector of selectors) { - try { - const element = document.querySelector(selector) as HTMLElement; - if (element) { - element.focus(); - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - return; - } - } catch (e) { - // Continue to next selector if this one fails - } - } - - // Fallback: try to find by key attribute in ndc-dynamic components - const allInputs = document.querySelectorAll('input, textarea'); - for (let i = 0; i < allInputs.length; i++) { - const input = allInputs[i] as HTMLElement; - const formField = input.closest('mat-form-field'); - if (formField) { - const label = formField.querySelector('mat-label'); - if (label && label.textContent && label.textContent.trim() === fieldName) { - input.focus(); - input.scrollIntoView({ behavior: 'smooth', block: 'center' }); - return; - } - } - } - } - - private _filter(value: string): string[] { - return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); - } - - ngDoCheck() { - const change = this.differ.diff(this); - if (change) { - this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; - } - } - - get inputs() { - return filterTypes[this._connections.currentConnection.type] - } - - setWidgets(widgets: Widget[]) { - this.tableWidgetsList = widgets.map((widget: Widget) => widget.field_name); - this.tableWidgets = Object.assign({}, ...widgets - .map((widget: Widget) => { - let params; - if (widget.widget_params !== '// No settings required') { - params = JSON5.parse(widget.widget_params); - } else { - params = ''; - }; - return { - [widget.field_name]: {...widget, widget_params: params} - } - }) - ); - } - - trackByFn(_index: number, item: any) { - return item.key; // or item.id - } - - isWidget(columnName: string) { - return this.tableWidgetsList.includes(columnName); - } - - updateField = (updatedValue: any, field: string) => { - this.tableRowFieldsShown[field] = updatedValue; - } - - 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(''); - } - - updateComparator(event, fieldName: string) { - if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; - } - - resetFilters() { - this.tableFilters = []; - this.tableRowFieldsShown = {}; - } - - 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]; - } + public tableFilters = []; + public fieldSearchControl = new FormControl(''); + + public fields: string[]; + public foundFields: Observable; + + public tableRowFields: Object; + public tableRowStructure: Object; + public tableRowFieldsShown: Record = {}; + public tableRowFieldsComparator: Object = {}; + public tableForeignKeys: { [key: string]: TableForeignKey }; + public tableFiltersCount: number; + public differ: KeyValueDiffer; + public tableTypes: Object; + public tableWidgets: object; + public tableWidgetsList: string[] = []; + public UIwidgets = UIwidgets; + public autofocusField: string | null = null; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _connections: ConnectionsService, + private _tables: TablesService, + public route: ActivatedRoute, + private differs: KeyValueDiffers, + ) { + this.differ = this.differs.find({}).create(); + } + + ngOnInit(): void { + this._tables.cast.subscribe(); + this.tableForeignKeys = { ...this.data.structure.foreignKeys }; + this.tableRowFields = Object.assign( + {}, + ...this.data.structure.structure.map((field: TableField) => ({ [field.column_name]: undefined })), + ); + this.tableTypes = getTableTypes(this.data.structure.structure, this.data.structure.foreignKeysList); + this.fields = this.data.structure.structure + .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') + .map((field: TableField) => field.column_name); + // this.foundFields = [...this.fields]; + this.tableRowStructure = Object.assign( + {}, + ...this.data.structure.structure.map((field: TableField) => { + return { [field.column_name]: field }; + }), + ); + + // Set autofocus field if provided + if (this.data.autofocusField) { + this.autofocusField = this.data.autofocusField; + } + + const queryParams = this.route.snapshot.queryParams; + + // If saved_filter is present in queryParams, show empty form without applying filters + if (queryParams.saved_filter) { + // Show empty form without filters + this.tableFilters = []; + this.tableRowFieldsShown = {}; + this.tableRowFieldsComparator = {}; + } else { + // Original behavior - parse and apply filters from URL + let filters = {}; + if (queryParams.filters) filters = JsonURL.parse(queryParams.filters); + // const filters = JsonURL.parse(queryParams.filters || '{}'); + const filtersValues = getFiltersFromUrl(filters); + + console.log('Parsed filters from URL:', filtersValues); + + if (Object.keys(filtersValues).length) { + this.tableFilters = Object.keys(filtersValues).map((key) => key); + this.tableRowFieldsShown = filtersValues; + 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' })), + ); + } + } + } + + if (this.data.structure.widgets && this.data.structure.widgets.length) { + this.setWidgets(this.data.structure.widgets); + } + + // If autofocusField is provided, ensure it's in the filters list + if (this.autofocusField && this.tableFilters && !this.tableFilters.includes(this.autofocusField)) { + this.tableFilters.push(this.autofocusField); + if (!this.tableRowFieldsShown[this.autofocusField]) { + this.tableRowFieldsShown[this.autofocusField] = undefined; + } + if (!this.tableRowFieldsComparator[this.autofocusField]) { + this.tableRowFieldsComparator[this.autofocusField] = 'eq'; + } + } + + this.foundFields = this.fieldSearchControl.valueChanges.pipe( + startWith(''), + map((value) => this._filter(value || '')), + ); + } + + ngAfterViewInit(): void { + // Set focus on the autofocus field after view is initialized + if (this.autofocusField) { + setTimeout(() => { + this.focusOnField(this.autofocusField); + }, 200); + } + } + + focusOnField(fieldName: string) { + // Try multiple selectors to find the input field + const selectors = [ + `input[name*="${fieldName}"]`, + `textarea[name*="${fieldName}"]`, + `[data-field="${fieldName}"] input`, + `[data-field="${fieldName}"] textarea`, + `mat-form-field:has([name*="${fieldName}"]) input`, + `mat-form-field:has([name*="${fieldName}"]) textarea`, + ]; + + for (const selector of selectors) { + try { + const element = document.querySelector(selector) as HTMLElement; + if (element) { + element.focus(); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + return; + } + } catch (e) { + // Continue to next selector if this one fails + } + } + + // Fallback: try to find by key attribute in ndc-dynamic components + const allInputs = document.querySelectorAll('input, textarea'); + for (let i = 0; i < allInputs.length; i++) { + const input = allInputs[i] as HTMLElement; + const formField = input.closest('mat-form-field'); + if (formField) { + const label = formField.querySelector('mat-label'); + if (label && label.textContent && label.textContent.trim() === fieldName) { + input.focus(); + input.scrollIntoView({ behavior: 'smooth', block: 'center' }); + return; + } + } + } + } + + private _filter(value: string): string[] { + return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); + } + + ngDoCheck() { + const change = this.differ.diff(this); + if (change) { + this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; + } + } + + get inputs() { + return filterTypes[this._connections.currentConnection.type]; + } + + setWidgets(widgets: Widget[]) { + this.tableWidgetsList = widgets.map((widget: Widget) => widget.field_name); + this.tableWidgets = Object.assign( + {}, + ...widgets.map((widget: Widget) => { + let params; + if (widget.widget_params !== '// No settings required') { + params = JSON5.parse(widget.widget_params); + } else { + params = ''; + } + return { + [widget.field_name]: { ...widget, widget_params: params }, + }; + }), + ); + } + + trackByFn(_index: number, item: any) { + return item.key; // or item.id + } + + isWidget(columnName: string) { + return this.tableWidgetsList.includes(columnName); + } + + updateField = (updatedValue: any, field: string) => { + this.tableRowFieldsShown[field] = updatedValue; + }; + + 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(''); + } + + updateComparator(event, fieldName: string) { + if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; + } + + resetFilters() { + this.tableFilters = []; + this.tableRowFieldsShown = {}; + } + + 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]; + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/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 index 3e729f0ba..d89a69f08 100644 --- a/frontend/src/app/components/dashboard/db-table-view/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 @@ -1,329 +1,348 @@ -import { ActivatedRoute, RouterModule } from '@angular/router'; -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { TableField, TableRow, Widget } from 'src/app/models/table'; -import { UIwidgets, recordViewFieldTypes } from 'src/app/consts/record-view-types'; -import { normalizeFieldName, normalizeTableName } from 'src/app/lib/normalize'; - import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { DynamicModule } from 'ng-dynamic-component'; -import { ForeignKeyRecordViewComponent } from 'src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component'; -import JsonURL from "@jsonurl/jsonurl"; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import JsonURL from '@jsonurl/jsonurl'; +import { DynamicModule } from 'ng-dynamic-component'; +import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; +import { ForeignKeyRecordViewComponent } from 'src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component'; +import { recordViewFieldTypes, UIwidgets } from 'src/app/consts/record-view-types'; +import { formatFieldValue } from 'src/app/lib/format-field-value'; +import { normalizeFieldName, normalizeTableName } from 'src/app/lib/normalize'; +import { TableField, TableRow, Widget } from 'src/app/models/table'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { NotificationsService } from 'src/app/services/notifications.service'; -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 { PlaceholderRecordViewComponent } from '../../../skeletons/placeholder-record-view/placeholder-record-view.component'; @Component({ - selector: 'app-db-table-row-view', - templateUrl: './db-table-row-view.component.html', - styleUrls: ['./db-table-row-view.component.css'], - imports: [ - MatIconModule, - MatButtonModule, - ClipboardModule, - MatTooltipModule, - MatExpansionModule, - MatProgressSpinnerModule, - MatListModule, - RouterModule, - CommonModule, - PlaceholderRecordViewComponent, - ForeignKeyRecordViewComponent, - DynamicModule - ] + selector: 'app-db-table-row-view', + templateUrl: './db-table-row-view.component.html', + styleUrls: ['./db-table-row-view.component.css'], + imports: [ + MatIconModule, + MatButtonModule, + ClipboardModule, + MatTooltipModule, + MatExpansionModule, + MatProgressSpinnerModule, + MatListModule, + RouterModule, + CommonModule, + PlaceholderRecordViewComponent, + ForeignKeyRecordViewComponent, + DynamicModule, + SignalComponentIoModule, + ], }) export class DbTableRowViewComponent implements OnInit, OnDestroy { - @Input() activeFilters: object; - - public selectedRowCast: any; - public selectedRow: TableRow; - public columns: { - title: string; - normalizedTitle: string; - }[] = []; - public referencedTables: { table_name: string; displayTableName: string; columns: string[] }[] = []; - public referencedTablesURLParams: any; - public referencedRecords: {} = {}; - public recordViewComponents; - public widgetsMap: { [key: string]: any } = {}; - public UIwidgets = UIwidgets; - - constructor( - private _tables: TablesService, - private _tableState: TableStateService, - private _notifications: NotificationsService, - private _connections: ConnectionsService, - private route: ActivatedRoute, - ) { } - - ngOnInit(): void { - // this.connectionID = this._connections.connectionID; - - const connectionType = this._connections.currentConnection.type; - this.recordViewComponents = recordViewFieldTypes[connectionType]; - - this.selectedRowCast = this._tableState.cast.subscribe((row) => { - this.selectedRow = row; - - console.log('Selected row:', this.selectedRow); - - if (row?.columnsOrder) { - const columnsOrder = this.selectedRow.columnsOrder.length ? this.selectedRow.columnsOrder : Object.keys(this.selectedRow.record); - - this.columns = columnsOrder.map(column => { - return { - title: column, - normalizedTitle: row.widgets[column]?.name || normalizeFieldName(column) - } - }) - - if (row.relatedRecords?.referenced_by?.length) { - this.referencedRecords = {}; - - this.referencedTables = row.relatedRecords.referenced_by - .map((table: any) => { return {...table, displayTableName: table.display_name || normalizeTableName(table.table_name)}}); - - this.referencedTablesURLParams = row.relatedRecords.referenced_by - .map((table: any) => { - const params = {[table.column_name]: { - eq: row.record[row.relatedRecords.referenced_on_column_name] - }}; - return { - filters: JsonURL.stringify(params), - page_index: 0 - }}); - - row.relatedRecords.referenced_by.forEach((table: any) => { - const filters = {[table.column_name]: { - eq: row.record[row.relatedRecords.referenced_on_column_name] - }}; - - this._tables.fetchTable({ - connectionID: row.connectionID, - tableName: table.table_name, - requstedPage: 1, - chunkSize: 30, - filters - }).subscribe((res) => { - let identityColumn = res.identity_column; - let fieldsOrder = []; - - const foreignKeyMap = {}; - for (const fk of res.foreignKeys) { - foreignKeyMap[fk.column_name] = fk.referenced_column_name; - } - - const tableWidgetsNameMap = res.widgets.reduce((acc, widget: Widget) => { - acc[widget.field_name] = widget.name; - return acc; - }, {}); - - // Format each row - const formattedRows = res.rows.map(row => { - const formattedRow = {}; - - for (const key in row) { - if (foreignKeyMap[key] && typeof row[key] === 'object' && row[key] !== null) { - const preferredKey = Object.keys(row[key]).find(k => k !== foreignKeyMap[key]); - formattedRow[key] = preferredKey ? row[key][preferredKey] : row[key][foreignKeyMap[key]]; - } else { - formattedRow[key] = formatFieldValue(row[key], res.structure.find((field: TableField) => field.column_name === key)?.data_type || 'text'); - } - } - return formattedRow; - }) - - if (res.identity_column && res.list_fields.length) { - identityColumn = { - isSet: true, - fieldName: res.identity_column, - displayName: tableWidgetsNameMap[res.identity_column] || normalizeFieldName(res.identity_column) - }; - fieldsOrder = res.list_fields - .filter((field: string) => field !== res.identity_column) - .slice(0, 3) - .map((field: string) => { - return { - fieldName: field, - displayName: tableWidgetsNameMap[field] || normalizeFieldName(field) - }; - }); - } - - if (res.identity_column && !res.list_fields.length) { - identityColumn = { - isSet: true, - fieldName: res.identity_column, - displayName: tableWidgetsNameMap[res.identity_column] || normalizeFieldName(res.identity_column) - }; - fieldsOrder = res.structure - .filter((field: TableField) => field.column_name !== res.identity_column) - .slice(0, 3) - .map((field: TableField) => { - return { - fieldName: field.column_name, - displayName: tableWidgetsNameMap[field.column_name] || normalizeFieldName(field.column_name) - }; - }); - } - - if (!res.identity_column && res.list_fields.length) { - identityColumn = { - isSet: false, - fieldName: res.list_fields[0], - displayName: tableWidgetsNameMap[res.list_fields[0]] || normalizeFieldName(res.list_fields[0]) - }; - fieldsOrder = res.list_fields - .slice(1, 4) - .map((field: string) => { - return { - fieldName: field, - displayName: tableWidgetsNameMap[field] || normalizeFieldName(field) - }; - }); - } - - if (!res.identity_column && !res.list_fields.length) { - identityColumn = { - isSet: false, - fieldName: res.structure[0].column_name, - displayName: tableWidgetsNameMap[res.structure[0].column_name] || normalizeFieldName(res.structure[0].column_name) - }; - console.log(identityColumn); - fieldsOrder = res.structure - .slice(1, 4) - .map((field: TableField) => { - return { - fieldName: field.column_name, - displayName: tableWidgetsNameMap[field.column_name] || normalizeFieldName(field.column_name) - }; - }); - } - - const tableRecords = { - rows: formattedRows, - links: res.rows.map(row => { - return res.primaryColumns.reduce((keys, column) => { - if (res.foreignKeys.map(foreignKey => foreignKey.column_name).includes(column.column_name)) { - const referencedColumnNameOfForeignKey = foreignKeyMap[column.column_name]; - keys[column.column_name] = row[column.column_name][referencedColumnNameOfForeignKey]; - } else { - keys[column.column_name] = row[column.column_name]; - } - return keys; - }, {mode: 'view'}) - }), - identityColumn, - fieldsOrder - } - - this.referencedRecords[table.table_name] = tableRecords; - }); - }); - } - } - }); - } - - ngOnDestroy() { - this.selectedRowCast.unsubscribe(); - } - - isForeignKey(columnName: string) { - return this.selectedRow.foreignKeysList.includes(columnName); - } - - getForeignKeyValue(field: string) { - if (this.selectedRow && typeof this.selectedRow.record[field] === 'object') { - const identityColumnName = Object.keys(this.selectedRow.record[field]).find(key => key !== this.selectedRow.foreignKeys[field].referenced_column_name); - const referencedColumnName = this.selectedRow.foreignKeys[field].referenced_column_name; - if (identityColumnName) { - return this.selectedRow.record[field][identityColumnName]; - } - if (referencedColumnName) { - return this.selectedRow.record[field][referencedColumnName]; - } - return this.selectedRow.record[field] || ''; - }; - return this.selectedRow.record[field] || ''; - } - - getForeignKeyQueryParams(field: string) { - if (this.selectedRow) { - const referencedColumnName = this.selectedRow.foreignKeys[field]?.referenced_column_name; - - if (typeof this.selectedRow.record[field] === 'object') { - return {[referencedColumnName]: this.selectedRow.record[field][referencedColumnName]} - } else { - return {[referencedColumnName]: this.selectedRow.record[field]}; - } - }; - return {}; - } - - isWidget(columnName: string) { - return this.selectedRow.widgetsList.includes(columnName); - } - - getDedicatedPageLinkParams() { - if (this.selectedRow) { - const params = {}; - for (const key in this.selectedRow.primaryKeys) { - if (Object.hasOwn(this.selectedRow.primaryKeys, key)) { - if (this.selectedRow.foreignKeysList.includes(key)) { - const referencedColumnName = this.selectedRow.foreignKeys[key].referenced_column_name; - params[key] = this.selectedRow.record[key][referencedColumnName]; - } - else { - params[key] = this.selectedRow.primaryKeys[key]; - } - - } - } - return params; - }; - return {}; - } - - getDedicatedPageLink() { - if (this.selectedRow) { - const paramsObj = this.getDedicatedPageLinkParams(); - const params = new URLSearchParams(); - for (const key in paramsObj) { - if (Object.hasOwn(paramsObj, key)) { - params.set(key, paramsObj[key]); - } - } - return `${location.origin}${this.selectedRow.link}?${params.toString()}`; - }; - return ''; - } - - showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); - } - - stashUrlParams() { - this._tableState.setBackUrlParams(this.route.snapshot.queryParams.page_index, this.route.snapshot.queryParams.page_size, this.route.snapshot.queryParams.sort_active, this.route.snapshot.queryParams.sort_direction); - if (this.activeFilters && Object.keys(this.activeFilters).length > 0) { - this._tableState.setBackUrlFilters(this.activeFilters); - } else { - this._tableState.setBackUrlFilters(null); - } - - this._tableState.clearSelection(); - } - - handleClose() { - this._tableState.clearSelection(); - } + @Input() activeFilters: object; + + public selectedRowCast: any; + public selectedRow: TableRow; + public columns: { + title: string; + normalizedTitle: string; + }[] = []; + public referencedTables: { table_name: string; displayTableName: string; columns: string[] }[] = []; + public referencedTablesURLParams: any; + public referencedRecords: {} = {}; + public recordViewComponents; + public widgetsMap: { [key: string]: any } = {}; + public UIwidgets = UIwidgets; + + constructor( + private _tables: TablesService, + private _tableState: TableStateService, + private _notifications: NotificationsService, + private _connections: ConnectionsService, + private route: ActivatedRoute, + ) {} + + ngOnInit(): void { + // this.connectionID = this._connections.connectionID; + + const connectionType = this._connections.currentConnection.type; + this.recordViewComponents = recordViewFieldTypes[connectionType]; + + this.selectedRowCast = this._tableState.cast.subscribe((row) => { + this.selectedRow = row; + + console.log('Selected row:', this.selectedRow); + + if (row?.columnsOrder) { + const columnsOrder = this.selectedRow.columnsOrder.length + ? this.selectedRow.columnsOrder + : Object.keys(this.selectedRow.record); + + this.columns = columnsOrder.map((column) => { + return { + title: column, + normalizedTitle: row.widgets[column]?.name || normalizeFieldName(column), + }; + }); + + if (row.relatedRecords?.referenced_by?.length) { + this.referencedRecords = {}; + + this.referencedTables = row.relatedRecords.referenced_by.map((table: any) => { + return { ...table, displayTableName: table.display_name || normalizeTableName(table.table_name) }; + }); + + this.referencedTablesURLParams = row.relatedRecords.referenced_by.map((table: any) => { + const params = { + [table.column_name]: { + eq: row.record[row.relatedRecords.referenced_on_column_name], + }, + }; + return { + filters: JsonURL.stringify(params), + page_index: 0, + }; + }); + + row.relatedRecords.referenced_by.forEach((table: any) => { + const filters = { + [table.column_name]: { + eq: row.record[row.relatedRecords.referenced_on_column_name], + }, + }; + + this._tables + .fetchTable({ + connectionID: row.connectionID, + tableName: table.table_name, + requstedPage: 1, + chunkSize: 30, + filters, + }) + .subscribe((res) => { + let identityColumn = res.identity_column; + let fieldsOrder = []; + + const foreignKeyMap = {}; + for (const fk of res.foreignKeys) { + foreignKeyMap[fk.column_name] = fk.referenced_column_name; + } + + const tableWidgetsNameMap = res.widgets.reduce((acc, widget: Widget) => { + acc[widget.field_name] = widget.name; + return acc; + }, {}); + + // Format each row + const formattedRows = res.rows.map((row) => { + const formattedRow = {}; + + for (const key in row) { + if (foreignKeyMap[key] && typeof row[key] === 'object' && row[key] !== null) { + const preferredKey = Object.keys(row[key]).find((k) => k !== foreignKeyMap[key]); + formattedRow[key] = preferredKey ? row[key][preferredKey] : row[key][foreignKeyMap[key]]; + } else { + formattedRow[key] = formatFieldValue( + row[key], + res.structure.find((field: TableField) => field.column_name === key)?.data_type || 'text', + ); + } + } + return formattedRow; + }); + + if (res.identity_column && res.list_fields.length) { + identityColumn = { + isSet: true, + fieldName: res.identity_column, + displayName: tableWidgetsNameMap[res.identity_column] || normalizeFieldName(res.identity_column), + }; + fieldsOrder = res.list_fields + .filter((field: string) => field !== res.identity_column) + .slice(0, 3) + .map((field: string) => { + return { + fieldName: field, + displayName: tableWidgetsNameMap[field] || normalizeFieldName(field), + }; + }); + } + + if (res.identity_column && !res.list_fields.length) { + identityColumn = { + isSet: true, + fieldName: res.identity_column, + displayName: tableWidgetsNameMap[res.identity_column] || normalizeFieldName(res.identity_column), + }; + fieldsOrder = res.structure + .filter((field: TableField) => field.column_name !== res.identity_column) + .slice(0, 3) + .map((field: TableField) => { + return { + fieldName: field.column_name, + displayName: tableWidgetsNameMap[field.column_name] || normalizeFieldName(field.column_name), + }; + }); + } + + if (!res.identity_column && res.list_fields.length) { + identityColumn = { + isSet: false, + fieldName: res.list_fields[0], + displayName: tableWidgetsNameMap[res.list_fields[0]] || normalizeFieldName(res.list_fields[0]), + }; + fieldsOrder = res.list_fields.slice(1, 4).map((field: string) => { + return { + fieldName: field, + displayName: tableWidgetsNameMap[field] || normalizeFieldName(field), + }; + }); + } + + if (!res.identity_column && !res.list_fields.length) { + identityColumn = { + isSet: false, + fieldName: res.structure[0].column_name, + displayName: + tableWidgetsNameMap[res.structure[0].column_name] || + normalizeFieldName(res.structure[0].column_name), + }; + console.log(identityColumn); + fieldsOrder = res.structure.slice(1, 4).map((field: TableField) => { + return { + fieldName: field.column_name, + displayName: tableWidgetsNameMap[field.column_name] || normalizeFieldName(field.column_name), + }; + }); + } + + const tableRecords = { + rows: formattedRows, + links: res.rows.map((row) => { + return res.primaryColumns.reduce( + (keys, column) => { + if (res.foreignKeys.map((foreignKey) => foreignKey.column_name).includes(column.column_name)) { + const referencedColumnNameOfForeignKey = foreignKeyMap[column.column_name]; + keys[column.column_name] = row[column.column_name][referencedColumnNameOfForeignKey]; + } else { + keys[column.column_name] = row[column.column_name]; + } + return keys; + }, + { mode: 'view' }, + ); + }), + identityColumn, + fieldsOrder, + }; + + this.referencedRecords[table.table_name] = tableRecords; + }); + }); + } + } + }); + } + + ngOnDestroy() { + this.selectedRowCast.unsubscribe(); + } + + isForeignKey(columnName: string) { + return this.selectedRow.foreignKeysList.includes(columnName); + } + + getForeignKeyValue(field: string) { + if (this.selectedRow && typeof this.selectedRow.record[field] === 'object') { + const identityColumnName = Object.keys(this.selectedRow.record[field]).find( + (key) => key !== this.selectedRow.foreignKeys[field].referenced_column_name, + ); + const referencedColumnName = this.selectedRow.foreignKeys[field].referenced_column_name; + if (identityColumnName) { + return this.selectedRow.record[field][identityColumnName]; + } + if (referencedColumnName) { + return this.selectedRow.record[field][referencedColumnName]; + } + return this.selectedRow.record[field] || ''; + } + return this.selectedRow.record[field] || ''; + } + + getForeignKeyQueryParams(field: string) { + if (this.selectedRow) { + const referencedColumnName = this.selectedRow.foreignKeys[field]?.referenced_column_name; + + if (typeof this.selectedRow.record[field] === 'object') { + return { [referencedColumnName]: this.selectedRow.record[field][referencedColumnName] }; + } else { + return { [referencedColumnName]: this.selectedRow.record[field] }; + } + } + return {}; + } + + isWidget(columnName: string) { + return this.selectedRow.widgetsList.includes(columnName); + } + + getDedicatedPageLinkParams() { + if (this.selectedRow) { + const params = {}; + for (const key in this.selectedRow.primaryKeys) { + if (Object.hasOwn(this.selectedRow.primaryKeys, key)) { + if (this.selectedRow.foreignKeysList.includes(key)) { + const referencedColumnName = this.selectedRow.foreignKeys[key].referenced_column_name; + params[key] = this.selectedRow.record[key][referencedColumnName]; + } else { + params[key] = this.selectedRow.primaryKeys[key]; + } + } + } + return params; + } + return {}; + } + + getDedicatedPageLink() { + if (this.selectedRow) { + const paramsObj = this.getDedicatedPageLinkParams(); + const params = new URLSearchParams(); + for (const key in paramsObj) { + if (Object.hasOwn(paramsObj, key)) { + params.set(key, paramsObj[key]); + } + } + return `${location.origin}${this.selectedRow.link}?${params.toString()}`; + } + return ''; + } + + showCopyNotification(message: string) { + this._notifications.showSuccessSnackbar(message); + } + + stashUrlParams() { + this._tableState.setBackUrlParams( + this.route.snapshot.queryParams.page_index, + this.route.snapshot.queryParams.page_size, + this.route.snapshot.queryParams.sort_active, + this.route.snapshot.queryParams.sort_direction, + ); + if (this.activeFilters && Object.keys(this.activeFilters).length > 0) { + this._tableState.setBackUrlFilters(this.activeFilters); + } else { + this._tableState.setBackUrlFilters(null); + } + + this._tableState.clearSelection(); + } + + handleClose() { + this._tableState.clearSelection(); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 6aa95eb51..217f1462e 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -1,14 +1,19 @@ - - -import { SelectionModel } from '@angular/cdk/collections'; import { ClipboardModule } from '@angular/cdk/clipboard'; +import { SelectionModel } from '@angular/cdk/collections'; +import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { DynamicModule } from 'ng-dynamic-component'; - import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -24,18 +29,16 @@ import { MatSelectModule } from '@angular/material/select'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; - -import { Angulartics2OnModule } from 'angulartics2'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import JsonURL from '@jsonurl/jsonurl'; +import { Angulartics2OnModule } from 'angulartics2'; import JSON5 from 'json5'; +import { DynamicModule } from 'ng-dynamic-component'; +import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; import { merge } from 'rxjs'; import { tap } from 'rxjs/operators'; - -import { tableDisplayTypes, UIwidgets } from '../../../consts/table-display-types'; import { formatFieldValue } from 'src/app/lib/format-field-value'; -import { normalizeTableName } from '../../../lib/normalize'; import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; -import { AccessLevel } from 'src/app/models/user'; import { CustomAction, TableForeignKey, @@ -45,19 +48,20 @@ import { TableRow, Widget, } from 'src/app/models/table'; +import { AccessLevel } from 'src/app/models/user'; import { ConnectionsService } from 'src/app/services/connections.service'; import { NotificationsService } from 'src/app/services/notifications.service'; import { TableRowService } from 'src/app/services/table-row.service'; import { TableStateService } from 'src/app/services/table-state.service'; import { TablesService } from 'src/app/services/tables.service'; - +import { tableDisplayTypes, UIwidgets } from '../../../consts/table-display-types'; +import { normalizeTableName } from '../../../lib/normalize'; +import { PlaceholderTableDataComponent } from '../../skeletons/placeholder-table-data/placeholder-table-data.component'; +import { ForeignKeyDisplayComponent } from '../../ui-components/table-display-fields/foreign-key/foreign-key.component'; import { DbTableExportDialogComponent } from './db-table-export-dialog/db-table-export-dialog.component'; import { DbTableFiltersDialogComponent } from './db-table-filters-dialog/db-table-filters-dialog.component'; import { DbTableImportDialogComponent } from './db-table-import-dialog/db-table-import-dialog.component'; import { SavedFiltersPanelComponent } from './saved-filters-panel/saved-filters-panel.component'; -import { ForeignKeyDisplayComponent } from '../../ui-components/table-display-fields/foreign-key/foreign-key.component'; -import { PlaceholderTableDataComponent } from '../../skeletons/placeholder-table-data/placeholder-table-data.component'; - interface Column { title: string; @@ -100,6 +104,7 @@ export interface Folder { Angulartics2OnModule, PlaceholderTableDataComponent, DynamicModule, + SignalComponentIoModule, ForeignKeyDisplayComponent, SavedFiltersPanelComponent, ], @@ -160,7 +165,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { if (value) this.tableData = value; } -@ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; public defaultSort: { column: string; direction: 'asc' | 'desc' } | null = null; @@ -175,7 +180,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { private route: ActivatedRoute, public router: Router, public dialog: MatDialog, - private cdr: ChangeDetectorRef, + private cdr: ChangeDetectorRef, ) {} ngAfterViewInit() { @@ -187,7 +192,14 @@ export class DbTableViewComponent implements OnInit, OnChanges { // Subscribe to loading state to initialize default sort after data is loaded this.tableData.loading$.subscribe((loading: boolean) => { - console.log('loading$ changed:', loading, 'sort.active:', this.sort?.active, 'sort.direction:', this.sort?.direction); + console.log( + 'loading$ changed:', + loading, + 'sort.active:', + this.sort?.active, + 'sort.direction:', + this.sort?.direction, + ); if (!loading && this.tableData.defaultSort !== undefined) { // Update defaultSort reference whenever data loads @@ -212,7 +224,12 @@ export class DbTableViewComponent implements OnInit, OnChanges { } } - console.log('After loading complete - sort.active:', this.sort?.active, 'sort.direction:', this.sort?.direction); + console.log( + 'After loading complete - sort.active:', + this.sort?.active, + 'sort.direction:', + this.sort?.direction, + ); } }); @@ -332,32 +349,36 @@ export class DbTableViewComponent implements OnInit, OnChanges { if (this.isDefaultSort(column)) { // Remove default sort this.defaultSort = null; - this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { - ordering: null, - ordering_field: null, - }).subscribe({ - next: () => { - console.log('Personal table view settings updated - default sort removed'); - }, - error: (error) => { - console.error('Error updating personal table view settings:', error); - } - }); + this._tables + .updatePersonalTableViewSettings(this.connectionID, this.name, { + ordering: null, + ordering_field: null, + }) + .subscribe({ + next: () => { + console.log('Personal table view settings updated - default sort removed'); + }, + error: (error) => { + console.error('Error updating personal table view settings:', error); + }, + }); } else { // Set current sort as default const direction = this.sort.active === column ? this.sort.direction : 'asc'; this.defaultSort = { column, direction: direction as 'asc' | 'desc' }; - this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { - ordering: this.sort.direction === 'asc' ? TableOrdering.Ascending : TableOrdering.Descending, - ordering_field: column, - }).subscribe({ - next: () => { - console.log('Personal table view settings updated - default sort removed'); - }, - error: (error) => { - console.error('Error updating personal table view settings:', error); - } - }); + this._tables + .updatePersonalTableViewSettings(this.connectionID, this.name, { + ordering: this.sort.direction === 'asc' ? TableOrdering.Ascending : TableOrdering.Descending, + ordering_field: column, + }) + .subscribe({ + next: () => { + console.log('Personal table view settings updated - default sort removed'); + }, + error: (error) => { + console.error('Error updating personal table view settings:', error); + }, + }); } } @@ -683,120 +704,122 @@ export class DbTableViewComponent implements OnInit, OnChanges { this.applyFilter.emit($event); } - get sortedColumns() { - if (!this.tableData || !this.tableData.columns) { - return []; - } - // Sort columns: visible (selected=true) first, then hidden (selected=false) - return [...this.tableData.columns].sort((a, b) => { - if (a.selected === b.selected) return 0; - return a.selected ? -1 : 1; - }); - } - -onColumnVisibilityChange() { - this.tableData.changleColumnList(this.connectionID, this.name); - this.cdr.detectChanges(); - this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { - columns_view: this.tableData.displayedDataColumns - }).subscribe({ - next: () => { - console.log('Personal table view settings updated with custom ordering'); - }, - error: (error) => { - console.error('Error updating personal table view settings:', error); - } - }); - } - - handleActiveFilterClick(filterKey: string) { - const dialogRef = this.dialog.open(DbTableFiltersDialogComponent, { - width: '56em', - data: { - connectionID: this.connectionID, - tableName: this.name, - displayTableName: this.displayName, - structure: { - structure: this.tableData.structure, - foreignKeys: this.tableData.foreignKeys, - foreignKeysList: this.tableData.foreignKeysList, - widgets: this.tableData.widgets || [] - }, - autofocusField: filterKey - } - }); - - dialogRef.afterClosed().subscribe(action => { - if (action === 'filter' && dialogRef.componentInstance) { - const filtersFromDialog = {...dialogRef.componentInstance.tableRowFieldsShown}; - const comparators = dialogRef.componentInstance.tableRowFieldsComparator; - this.openFilters.emit({ - structure: this.tableData.structure, - foreignKeysList: this.tableData.foreignKeysList, - foreignKeys: this.tableData.foreignKeys, - widgets: this.tableData.widgets, - filters: filtersFromDialog, - comparators: comparators - }); - } - }); - } - - onColumnsMenuDrop(event: CdkDragDrop) { - if (event.previousIndex === event.currentIndex) { - return; - } - - // The drag indices are based on sortedColumns (visible first, then hidden) - // We need to map these to the actual indices in this.tableData.columns - const sorted = this.sortedColumns; - const draggedColumn = sorted[event.previousIndex]; - const targetColumn = sorted[event.currentIndex]; - - // Find actual indices in the original columns array - const actualPreviousIndex = this.tableData.columns.findIndex(col => col.title === draggedColumn.title); - const actualCurrentIndex = this.tableData.columns.findIndex(col => col.title === targetColumn.title); - - if (actualPreviousIndex === -1 || actualCurrentIndex === -1) { - return; - } - - // Reorder columns array in the menu - moveItemInArray(this.tableData.columns, actualPreviousIndex, actualCurrentIndex); - - // Update dataColumns array - this.tableData.dataColumns = this.tableData.columns.map(column => column.title); - - // Update displayedDataColumns to match the new order (only visible columns) - const newDisplayedOrder = this.tableData.columns - .filter(col => col.selected) - .map(col => col.title); - - this.tableData.displayedDataColumns = newDisplayedOrder; - - // Update full displayed columns list - THIS UPDATES THE TABLE IMMEDIATELY - if (this.tableData.keyAttributes && this.tableData.keyAttributes.length) { - this.tableData.displayedColumns = ['select', ...newDisplayedOrder, 'actions']; - } else { - this.tableData.displayedColumns = [...newDisplayedOrder]; - } - - // Force Angular to detect changes and re-render the table immediately - this.cdr.detectChanges(); - -this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { - list_fields: this.tableData.columns.map(col => col.title) - }).subscribe({ - next: () => { - console.log('Personal table view settings updated with custom ordering'); - }, - error: (error) => { - console.error('Error updating personal table view settings:', error); - } - }); - - console.log('Columns reordered in menu - table updated:', newDisplayedOrder); - } + get sortedColumns() { + if (!this.tableData || !this.tableData.columns) { + return []; + } + // Sort columns: visible (selected=true) first, then hidden (selected=false) + return [...this.tableData.columns].sort((a, b) => { + if (a.selected === b.selected) return 0; + return a.selected ? -1 : 1; + }); + } + + onColumnVisibilityChange() { + this.tableData.changleColumnList(this.connectionID, this.name); + this.cdr.detectChanges(); + this._tables + .updatePersonalTableViewSettings(this.connectionID, this.name, { + columns_view: this.tableData.displayedDataColumns, + }) + .subscribe({ + next: () => { + console.log('Personal table view settings updated with custom ordering'); + }, + error: (error) => { + console.error('Error updating personal table view settings:', error); + }, + }); + } + + handleActiveFilterClick(filterKey: string) { + const dialogRef = this.dialog.open(DbTableFiltersDialogComponent, { + width: '56em', + data: { + connectionID: this.connectionID, + tableName: this.name, + displayTableName: this.displayName, + structure: { + structure: this.tableData.structure, + foreignKeys: this.tableData.foreignKeys, + foreignKeysList: this.tableData.foreignKeysList, + widgets: this.tableData.widgets || [], + }, + autofocusField: filterKey, + }, + }); + + dialogRef.afterClosed().subscribe((action) => { + if (action === 'filter' && dialogRef.componentInstance) { + const filtersFromDialog = { ...dialogRef.componentInstance.tableRowFieldsShown }; + const comparators = dialogRef.componentInstance.tableRowFieldsComparator; + this.openFilters.emit({ + structure: this.tableData.structure, + foreignKeysList: this.tableData.foreignKeysList, + foreignKeys: this.tableData.foreignKeys, + widgets: this.tableData.widgets, + filters: filtersFromDialog, + comparators: comparators, + }); + } + }); + } + + onColumnsMenuDrop(event: CdkDragDrop) { + if (event.previousIndex === event.currentIndex) { + return; + } + + // The drag indices are based on sortedColumns (visible first, then hidden) + // We need to map these to the actual indices in this.tableData.columns + const sorted = this.sortedColumns; + const draggedColumn = sorted[event.previousIndex]; + const targetColumn = sorted[event.currentIndex]; + + // Find actual indices in the original columns array + const actualPreviousIndex = this.tableData.columns.findIndex((col) => col.title === draggedColumn.title); + const actualCurrentIndex = this.tableData.columns.findIndex((col) => col.title === targetColumn.title); + + if (actualPreviousIndex === -1 || actualCurrentIndex === -1) { + return; + } + + // Reorder columns array in the menu + moveItemInArray(this.tableData.columns, actualPreviousIndex, actualCurrentIndex); + + // Update dataColumns array + this.tableData.dataColumns = this.tableData.columns.map((column) => column.title); + + // Update displayedDataColumns to match the new order (only visible columns) + const newDisplayedOrder = this.tableData.columns.filter((col) => col.selected).map((col) => col.title); + + this.tableData.displayedDataColumns = newDisplayedOrder; + + // Update full displayed columns list - THIS UPDATES THE TABLE IMMEDIATELY + if (this.tableData.keyAttributes && this.tableData.keyAttributes.length) { + this.tableData.displayedColumns = ['select', ...newDisplayedOrder, 'actions']; + } else { + this.tableData.displayedColumns = [...newDisplayedOrder]; + } + + // Force Angular to detect changes and re-render the table immediately + this.cdr.detectChanges(); + + this._tables + .updatePersonalTableViewSettings(this.connectionID, this.name, { + list_fields: this.tableData.columns.map((col) => col.title), + }) + .subscribe({ + next: () => { + console.log('Personal table view settings updated with custom ordering'); + }, + error: (error) => { + console.error('Error updating personal table view settings:', error); + }, + }); + + console.log('Columns reordered in menu - table updated:', newDisplayedOrder); + } exportData() { const convertToCSVValue = (value: any): string => { 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 cff2fac9e..5dea77264 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 @@ -16,6 +16,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import JsonURL from '@jsonurl/jsonurl'; import JSON5 from 'json5'; import { DynamicModule } from 'ng-dynamic-component'; +import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; import { defaultTimestampValues, recordEditTypes, timestampTypes, UIwidgets } from 'src/app/consts/record-edit-types'; import { formatFieldValue } from 'src/app/lib/format-field-value'; import { normalizeFieldName, normalizeTableName } from 'src/app/lib/normalize'; @@ -57,6 +58,7 @@ import { BreadcrumbsComponent } from '../ui-components/breadcrumbs/breadcrumbs.c MatExpansionModule, MatChipsModule, DynamicModule, + SignalComponentIoModule, AlertComponent, PlaceholderRowEditComponent, BannerComponent, diff --git a/frontend/src/main.ts b/frontend/src/main.ts index b7d358a21..81950ef16 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -11,6 +11,7 @@ import { provideCodeEditor } from '@ngstack/code-editor'; import * as Sentry from '@sentry/angular'; import { Angulartics2Module } from 'angulartics2'; import { DynamicModule } from 'ng-dynamic-component'; +import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; import { CookieService } from 'ngx-cookie-service'; import { MarkdownModule, provideMarkdown } from 'ngx-markdown'; import { NgxStripeModule } from 'ngx-stripe'; @@ -90,6 +91,7 @@ bootstrapApplication(AppComponent, { ReactiveFormsModule, RouterModule, DynamicModule, + SignalComponentIoModule, Angulartics2Module.forRoot(), ClipboardModule, DragDropModule, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 40f4224be..843472ae7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -10696,16 +10696,16 @@ __metadata: languageName: node linkType: hard -"ng-dynamic-component@npm:^10.7.0": - version: 10.7.0 - resolution: "ng-dynamic-component@npm:10.7.0" +"ng-dynamic-component@npm:^10.8.0": + version: 10.8.2 + resolution: "ng-dynamic-component@npm:10.8.2" dependencies: tslib: ^2.0.0 peerDependencies: "@angular/common": ">=14.1.3" "@angular/core": ">=14.1.3" rxjs: ">=6.0.0" - checksum: 863fb60ff14c5afc540b5791ff07b9a90897b3a9ab0cf29705a35bf9ef3cd23c208333172bfaba8b994760913b69145e5a7c8f633b998486a3c7cb0c5b35ae2c + checksum: 60e28a58f4f78d9c18ad17b8c05741ad377b64a5b93c22cd89def47075a1b83405e587b47f855a1c7b6398c73209cd4770a24092448da4e163e159a482a9930a languageName: node linkType: hard @@ -12346,7 +12346,7 @@ __metadata: lodash-es: ^4.17.21 mermaid: ^11.12.1 monaco-editor: 0.55.1 - ng-dynamic-component: ^10.7.0 + ng-dynamic-component: ^10.8.0 ngx-cookie-service: ^19.0.0 ngx-markdown: ^19.1.1 ngx-stripe: ^19.0.0