diff --git a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts index 96cfb0e56..ffb5d8680 100644 --- a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts +++ b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts @@ -4,6 +4,10 @@ import { AgentEntity } from '../agent.entity.js'; import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; export const customAgentRepositoryExtension = { + async saveNewAgent(agent: AgentEntity): Promise { + return await this.save(agent); + }, + async createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise { const newAgent = await this.createNewAgentForConnection(connection); return newAgent.token; @@ -59,14 +63,8 @@ export const customAgentRepositoryExtension = { return 'IBMDB2-TEST-AGENT-TOKEN'; case ConnectionTypeTestEnum.agent_mongodb: return 'MONGODB-TEST-AGENT-TOKEN'; - case ConnectionTypeTestEnum.elasticsearch: - return 'ELASTICSEARCH-TEST-AGENT-TOKEN'; - case ConnectionTypeTestEnum.agent_cassandra: - return 'CASSANDRA-TEST-AGENT-TOKEN'; - case ConnectionTypeTestEnum.agent_redis: - return 'REDIS-TEST-AGENT-TOKEN'; - case ConnectionTypeTestEnum.agent_clickhouse: - return 'CLICKHOUSE-TEST-AGENT-TOKEN'; + default: + throw new Error(`Unsupported connection type for test agent token: ${connectionType}`); } }, }; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html index d6f8fb042..dd2e0e29b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -34,7 +34,8 @@

label: tableWidgets[value.key].name || value.key, value: tableRowFieldsShown[value.key], widgetStructure: tableWidgets[value.key], - relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined + relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined, + autofocus: autofocusField === value.key }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateField, args: ['$event', value.key] } @@ -49,7 +50,8 @@

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

value: tableRowFieldsShown[value.key], readonly: tableRowFieldsComparator[value.key] === 'empty', structure: tableRowStructure[value.key], - relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined + relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined, + autofocus: autofocusField === value.key }" [ndcDynamicOutputs]="{ onFieldChange: { handler: updateField, args: ['$event', value.key] } 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 7bad098de..62ecfd772 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,4 +1,4 @@ -import { Component, OnInit, Inject, KeyValueDiffers, KeyValueDiffer } from '@angular/core'; +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'; @@ -16,7 +16,7 @@ import { filterTypes } from 'src/app/consts/filter-types'; import { ActivatedRoute } from '@angular/router'; import { getComparatorsFromUrl, getFiltersFromUrl } from 'src/app/lib/parse-filter-params'; import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; import { map, startWith } from 'rxjs/operators'; import { Observable, } from 'rxjs'; import { FormControl } from '@angular/forms'; @@ -48,7 +48,7 @@ import { Angulartics2OnModule } from 'angulartics2'; ContentLoaderComponent ] }) -export class DbTableFiltersDialogComponent implements OnInit { +export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { public tableFilters = []; public fieldSearchControl = new FormControl(''); @@ -67,6 +67,7 @@ export class DbTableFiltersDialogComponent implements OnInit { public tableWidgets: object; public tableWidgetsList: string[] = []; public UIwidgets = UIwidgets; + public autofocusField: string | null = null; constructor( @Inject(MAT_DIALOG_DATA) public data: any, @@ -91,6 +92,11 @@ export class DbTableFiltersDialogComponent implements OnInit { 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 @@ -122,12 +128,75 @@ export class DbTableFiltersDialogComponent implements OnInit { } } - this.data.structure.widgets.length && this.setWidgets(this.data.structure.widgets); + 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[] { diff --git a/frontend/src/app/components/dashboard/db-table-view/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 index 2e915eed2..55c64baa2 100644 --- a/frontend/src/app/components/dashboard/db-table-view/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 @@ -85,7 +85,6 @@

Preview

{{ displayName }}
+ class="db-table-active-filter-chip" + (removed)="removeFilter.emit(activeFilter.key)" + (click)="handleActiveFilterClick(activeFilter.key)"> {{ getFilter(activeFilter) }} - @@ -207,11 +209,12 @@

{{ displayName }}

(filterSelected)="onFilterSelected($event)" > +
-
+
{ + 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; @@ -745,7 +784,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { // Force Angular to detect changes and re-render the table immediately this.cdr.detectChanges(); - this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { +this._tables.updatePersonalTableViewSettings(this.connectionID, this.name, { list_fields: this.tableData.columns.map(col => col.title) }).subscribe({ next: () => { @@ -755,9 +794,10 @@ export class DbTableViewComponent implements OnInit, OnChanges { console.error('Error updating personal table view settings:', error); } }); - + console.log('Columns reordered in menu - table updated:', newDisplayedOrder); } + exportData() { const convertToCSVValue = (value: any): string => { // Handle null and undefined diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index a27ec9c1e..765543ebd 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -1,24 +1,198 @@ .filters-content { display: grid; - grid-template-columns: auto 228px 0 1fr 160px 32px; + grid-template-columns: 32px auto 228px 0 1fr 120px; grid-column-gap: 8px; align-content: flex-start; - align-items: flex-start; + align-items: center; padding-top: 4px !important; } +::ng-deep mat-dialog-content.filters-content { + padding-left: 24px !important; + padding-right: 24px !important; +} + .filters-select { - grid-column: 1 / span 6; + grid-column: 2 / span 5; margin-bottom: 16px; } .filter-line { - grid-column: 1 / span 4; + grid-column: 2 / span 4; } -.dynamic-column-radio { - grid-column: 5; +.empty-conditions-container { + grid-column: 1 / span 6; + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + margin-bottom: 16px; +} + +.add-condition-container { + grid-column: 1 / span 6; + display: flex; + align-items: center; + gap: 12px; margin-top: 8px; + margin-bottom: 16px; +} + +.where-label { + font-weight: 500; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; +} + +@media (prefers-color-scheme: dark) { + .where-label { + color: rgba(255, 255, 255, 0.87); + } +} + +.add-condition-button { + width: auto; + min-width: auto; + min-height: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + text-transform: none; + margin: 0 !important; + padding: 0 16px; + gap: 4px; +} + +.add-condition-button.accent-button { + color: #C177FC; +} + +.add-condition-button.accent-button:hover { + background-color: rgba(193, 119, 252, 0.04); +} + +.add-condition-button.accent-button .add-icon { + font-size: 20px; + width: 20px; + height: 20px; + line-height: 20px; +} + +.empty-conditions-container .column-name-icon { + font-size: 18px; + width: 18px; + height: 18px; + vertical-align: middle; +} + +.empty-condition-input { + width: auto; + min-width: 200px; + max-width: 300px; + margin: 0; + flex-shrink: 0; +} + +.empty-condition-input ::ng-deep .mat-mdc-form-field-infix { + min-height: 36px; + padding-top: 8px; + padding-bottom: 8px; + display: flex; + align-items: center; +} + +.empty-condition-input ::ng-deep .mat-mdc-form-field { + margin: 0; +} + +.empty-condition-input ::ng-deep .mat-mdc-text-field-wrapper { + padding-bottom: 0; + margin-bottom: 0; +} + +.empty-condition-input ::ng-deep .mat-mdc-form-field-subscript-wrapper { + margin-top: 0; + position: absolute; + top: 100%; +} + +.dynamic-column-radio { + grid-column: 6; + margin-top: -16px; + align-self: center; +} + +.comparator-select-field { + grid-column: 3; +} + +.add-condition-footer { + grid-column: 1 / span 6; + margin-top: 24px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 16px; +} + +.add-condition-footer button { + width: auto; + min-width: auto; + min-height: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + text-transform: none; + margin: 0 0 16px 0 !important; + padding: 0 16px; +} + +.add-condition-footer .filters-select { + width: auto; + min-width: 200px; + max-width: 300px; + margin: 0 !important; + min-height: 36px; + margin-top: 0 !important; + flex-shrink: 0; +} + +.add-condition-footer .filters-select ::ng-deep .mat-mdc-form-field-infix { + min-height: 36px; + padding-top: 8px; + padding-bottom: 8px; + display: flex; + align-items: center; +} + +.add-condition-footer .filters-select ::ng-deep .mat-mdc-text-field-wrapper { + padding-bottom: 0; + margin-bottom: 0; +} + +.add-condition-footer ::ng-deep .mat-mdc-form-field { + margin-top: 0 !important; + margin-bottom: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; +} + +.add-condition-footer ::ng-deep .mat-mdc-form-field-wrapper { + padding-bottom: 0; +} + +.dynamic-column-radio ::ng-deep .mdc-form-field { + display: flex; + align-items: center; + gap: 4px; +} + +.dynamic-column-radio ::ng-deep .mdc-form-field>label { + white-space: nowrap; } .filter-save-form { @@ -49,6 +223,7 @@ grid-column: 1 / span 6; margin: 0 0 16px 0; font-size: 14px; + white-space: nowrap; } @media (prefers-color-scheme: light) { @@ -67,6 +242,10 @@ grid-column: 1 / -1; } +.full-width.filter-name-field { + max-width: 400px; +} + .default-filter-checkbox { margin-bottom: 16px; } @@ -83,20 +262,72 @@ flex-direction: column; } +.filters-header-description { + margin: 0 24px 16px 24px; + font-size: 14px; + line-height: 1.5; +} + +@media (prefers-color-scheme: light) { + .filters-header-description { + color: rgba(0, 0, 0, 0.6); + } +} + +@media (prefers-color-scheme: dark) { + .filters-header-description { + color: rgba(255, 255, 255, 0.6); + } +} + ::ng-deep .mat-dialog-container { display: flex; } -.filters-content { - flex: 1 0 auto; +.column-name { + grid-column: 2; + margin-top: -8px; + align-self: center; } -.column-name { - margin-top: 12px; +.conditions-vertical-line { + grid-column: 1; + grid-row: 2 / -1; + width: 1px; + background-color: rgba(0, 0, 0, 0.12); + margin-left: 9px; + margin-top: 0; + margin-bottom: 0; + z-index: 0; +} + +@media (prefers-color-scheme: dark) { + .conditions-vertical-line { + background-color: rgba(255, 255, 255, 0.12); + } } .filter-delete-button { - margin-top: 4px; + grid-column: 1; + margin-top: -20px; + margin-right: 0; + margin-left: 0; + align-self: center; + justify-self: center; + position: relative; + z-index: 1; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; +} + +.filter-delete-button ::ng-deep .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; } .settings-form__reset-button { @@ -109,3 +340,81 @@ font-style: italic; margin: 16px 0; } + +.comparator-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin-right: 8px; + vertical-align: middle; +} + +::ng-deep .mat-mdc-select-panel .mat-mdc-option { + display: flex; + align-items: center; +} + +.comparator-select-field { + width: auto; + min-width: 140px; + max-width: none; +} + +.comparator-select-field ::ng-deep .mat-mdc-form-field-infix { + width: 100% !important; + min-width: fit-content; + padding-left: 0; + position: relative; +} + +.comparator-select-field ::ng-deep .mat-mdc-select-trigger { + width: 100%; + min-width: fit-content; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.comparator-select-field ::ng-deep .mat-mdc-select-value-text { + width: auto; + min-width: fit-content; + flex: 1; +} + +.comparator-select-field ::ng-deep .mat-mdc-select-arrow-wrapper { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} + +.comparator-select-field ::ng-deep .mat-mdc-select-panel { + min-width: max-content !important; + width: max-content !important; +} + +.conditions-error-message { + grid-column: 1 / span 6; + color: #f44336; + font-size: 12px; + margin: 8px 0 0 0; + padding-left: 16px; +} + +/* Add more spacing for multiline textarea inputs (more than 2 rows) */ +.filters-content ::ng-deep .filter-line mat-form-field:has(textarea[rows]):not(:has(textarea[rows="1"])):not(:has(textarea[rows="2"])), +.filters-content ::ng-deep .filter-line mat-form-field:has(textarea.long-textarea), +.filters-content ::ng-deep .filter-line mat-form-field:has(textarea.form-textarea) { + margin-top: 24px !important; + margin-bottom: 24px !important; +} + +/* Add more spacing for foreign key fields */ +.filters-content ::ng-deep .filter-line .foreign-key, +.filters-content ::ng-deep .filter-line app-edit-foreign-key, +.filters-content ::ng-deep .foreign-key { + margin-top: 24px !important; + margin-bottom: 24px !important; +} diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index 843615c44..431e6f0be 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -2,42 +2,62 @@

New fast filter for {{ data.displayTableName }}

+

+ Use conditions to quickly filter {{ data.displayTableName.toLowerCase() }} and optionally edit one column. +

- + Fast filter name - + [(ngModel)]="data.filtersSet.name" + (input)="showNameError = false"> + Fast filter name is required -

Conditions & main column

-

- Select which conditions to add and choose a main column. Main columns remain editable, while conditions are set. -

+ + + Click here to add condition + + + + {{field}} + + + + + - - - Click here to add condition - - - - {{field}} - - - +
+ subdirectory_arrow_right + where +
+
+
@@ -75,50 +95,63 @@

Conditions & main column

{{value.key}} + appearance="outline" + class="comparator-select-field"> - starts with + play_arrow + starts with - ends with + play_arrow + ends with - equal + drag_handle + equal - contains + search + contains - not contains + block + not contains - is empty + space_bar + is empty + appearance="outline" + class="comparator-select-field"> - equal + drag_handle + equal - greater than + keyboard_arrow_right + greater than - less than + keyboard_arrow_left + less than - greater than or equal + keyboard_double_arrow_right + greater than or equal - less than or equal + keyboard_double_arrow_left + less than or equal @@ -141,15 +174,47 @@

Conditions & main column

- Main column + Editable
- + + +
+ + + Click here to add condition + + + + {{field}} + + + + + At least one condition is required + + +

+ At least one condition is required +

+
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts index 1c658f9cf..dfe60c1e4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts @@ -1,11 +1,12 @@ import { FormControl, FormsModule, ReactiveFormsModule, } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { Component, Inject, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } 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 { MatRadioModule } from '@angular/material/radio'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -36,6 +37,7 @@ import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; MatIconModule, MatSelectModule, MatCheckboxModule, + MatRadioModule, DynamicModule, RouterModule, MatDialogModule, @@ -46,7 +48,7 @@ import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; templateUrl: './saved-filters-dialog.component.html', styleUrl: './saved-filters-dialog.component.css' }) -export class SavedFiltersDialogComponent implements OnInit { +export class SavedFiltersDialogComponent implements OnInit, AfterViewInit { // @Input() connectionID: string; // @Input() tableName: string; // @Input() displayTableName: string; @@ -68,6 +70,11 @@ export class SavedFiltersDialogComponent implements OnInit { public tableWidgetsList: string[] = []; public UIwidgets = UIwidgets; public dynamicColumn: string | null = null; + public showAddConditionField = false; + public showNameError = false; + public showConditionsError = false; + + @ViewChild('tableFiltersForm') tableFiltersForm: any; constructor( @Inject(MAT_DIALOG_DATA) public data: any, @@ -76,6 +83,7 @@ export class SavedFiltersDialogComponent implements OnInit { private dialogRef: MatDialogRef, private snackBar: MatSnackBar, private angulartics2: Angulartics2, + private elementRef: ElementRef, ) {} ngOnInit(): void { @@ -125,6 +133,35 @@ export class SavedFiltersDialogComponent implements OnInit { ); } + ngAfterViewInit(): void { + // If editing an existing filter (has id), remove focus from the filter name input + if (this.data.filtersSet && this.data.filtersSet.id) { + setTimeout(() => { + const nameInput = this.elementRef.nativeElement.querySelector('input[name="filters_set_name"]') as HTMLInputElement; + if (nameInput && document.activeElement === nameInput) { + nameInput.blur(); + } + }, 100); + } + } + + get hasSelectedFilters(): boolean { + return Object.keys(this.tableRowFieldsShown).length > 0; + } + + handleAddConditionButtonClick(): void { + this.showAddConditionField = true; + setTimeout(() => { + const input = this.elementRef.nativeElement.querySelector('input[name="filter_columns"]') as HTMLInputElement; + input?.focus(); + }, 0); + } + + cancelAddConditionInput(): void { + this.showAddConditionField = false; + this.fieldSearchControl.setValue(''); + } + private _filter(value: string): string[] { return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); } @@ -165,6 +202,10 @@ export class SavedFiltersDialogComponent implements OnInit { updateField = (updatedValue: any, field: string) => { this.tableRowFieldsShown[field] = updatedValue; this.updateFiltersCount(); + // Reset conditions error when a filter is added + if (this.showConditionsError && Object.keys(this.tableRowFieldsShown).length > 0) { + this.showConditionsError = false; + } } addFilter(e) { @@ -173,6 +214,22 @@ export class SavedFiltersDialogComponent implements OnInit { this.tableRowFieldsComparator = {...this.tableRowFieldsComparator, [key]: this.tableRowFieldsComparator[key] || 'eq'}; this.fieldSearchControl.setValue(''); this.updateFiltersCount(); + // Reset conditions error when a filter is added + this.showConditionsError = false; + if (this.hasSelectedFilters) { + this.showAddConditionField = false; + } + } + + handleInputBlur(): void { + // Hide the field if it's empty when it loses focus + if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { + setTimeout(() => { + if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { + this.cancelAddConditionInput(); + } + }, 200); + } } updateComparator(event, fieldName: string) { @@ -212,6 +269,22 @@ export class SavedFiltersDialogComponent implements OnInit { } } + getOperatorIcon(operator: string): string { + const iconMap: { [key: string]: string } = { + 'startswith': 'play_arrow', + 'endswith': 'play_arrow', + 'eq': 'drag_handle', + 'contains': 'search', + 'icontains': 'block', + 'empty': 'space_bar', + 'gt': 'keyboard_arrow_right', + 'lt': 'keyboard_arrow_left', + 'gte': 'keyboard_double_arrow_right', + 'lte': 'keyboard_double_arrow_left' + }; + return iconMap[operator] || 'drag_handle'; + } + removeFilter(field) { delete this.tableRowFieldsShown[field]; delete this.tableRowFieldsComparator[field]; @@ -219,6 +292,11 @@ export class SavedFiltersDialogComponent implements OnInit { this.dynamicColumn = null; } this.updateFiltersCount(); + // Reset conditions error when filters are removed (will be re-validated on save) + this.showConditionsError = false; + if (!this.hasSelectedFilters) { + this.showAddConditionField = false; + } } updateFiltersCount() { @@ -234,6 +312,58 @@ export class SavedFiltersDialogComponent implements OnInit { } handleSaveFilters() { + // Reset error flags + this.showNameError = false; + this.showConditionsError = false; + + // Validate filter name + if (!this.data.filtersSet.name || this.data.filtersSet.name.trim() === '') { + this.showNameError = true; + setTimeout(() => { + const nameInput = this.elementRef.nativeElement.querySelector('input[name="filters_set_name"]') as HTMLInputElement; + if (nameInput) { + nameInput.focus(); + nameInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 0); + return; + } + + // Validate conditions - check if there are any filters + // A valid filter must have a comparator defined + // Either regular filters OR dynamic column with comparator should exist + const hasRegularFilters = Object.keys(this.tableRowFieldsShown).some(key => { + // Skip dynamic column for regular filter check + if (key === this.dynamicColumn) { + return false; + } + // Check if comparator is defined (even if value is empty/null, comparator must exist) + return this.tableRowFieldsComparator[key] !== undefined && this.tableRowFieldsComparator[key] !== null; + }); + + // Check if dynamic column has a comparator (it counts as a valid filter condition) + const hasDynamicColumnFilter = this.dynamicColumn && + this.tableRowFieldsComparator[this.dynamicColumn] !== undefined && + this.tableRowFieldsComparator[this.dynamicColumn] !== null; + + if (!hasRegularFilters && !hasDynamicColumnFilter) { + this.showConditionsError = true; + setTimeout(() => { + const conditionInput = this.elementRef.nativeElement.querySelector('input[name="filter_columns"]') as HTMLInputElement; + if (conditionInput) { + conditionInput.focus(); + conditionInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + // If input is not visible, show the add condition button area + const addButton = this.elementRef.nativeElement.querySelector('.add-condition-footer button') as HTMLElement; + if (addButton) { + addButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, 0); + return; + } + let payload; if (Object.keys(this.tableRowFieldsShown).length) { let filters = {}; diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index f8adcd240..c16ea7a84 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -1,19 +1,22 @@ .saved-filters-container { display: flex; flex-direction: column; - gap: 8px; } .saved-filters-list { display: flex; align-items: center; gap: 16px; + margin-bottom: 12px; } .saved-filters-list__first-time-button { transition: background 0.3s ease; } +.create-filter-button { +} + @media (prefers-color-scheme: light) { .saved-filters-list__first-time-button { background: var(--color-accentedPalette-50); @@ -47,6 +50,100 @@ --mat-chip-elevated-selected-container-color: var(--color-accentedPalette-500) !important; --mat-chip-container-height: 32px; --mat-chip-label-text-color: rgba(0, 0, 0, 0.64); + --mdc-chip-container-shape-radius: 4px; + --mdc-chip-outline-width: 1px; + --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); + --mdc-chip-selected-container-shape-leading-width: 0px; + --mdc-chip-with-icon-graphic-selected-container-shape-leading-width: 0px; + --mdc-chip-with-icon-graphic-selected-container-shape-leading-start-width: 0px; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip.mdc-evolution-chip--selected { + --mdc-chip-selected-container-elevation-shadow: none; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip .mdc-evolution-chip__checkmark { + display: none !important; + width: 0 !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip .mdc-evolution-chip__checkmark-svg { + display: none !important; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip .mdc-evolution-chip__graphic { + display: none !important; + width: 0 !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip .mdc-evolution-chip__action { + padding-left: 16px !important; + padding-right: 8px !important; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip.mdc-evolution-chip--selected .mdc-evolution-chip__action { + padding-left: 16px !important; + padding-right: 8px !important; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-standard-chip.mdc-evolution-chip--selected .mdc-evolution-chip__action .mdc-evolution-chip__action__content { + margin-left: 0 !important; + padding-left: 0 !important; + transform: translateX(0) !important; +} + +.saved-filters-tabs ::ng-deep .filter-chip-wrapper { + display: flex; + align-items: center; + gap: 4px; +} + +.saved-filters-tabs ::ng-deep .mat-mdc-chip-action { + display: flex; + align-items: center; +} + +.saved-filters-tabs ::ng-deep .mdc-evolution-chip__action { + display: flex; + align-items: center; + justify-content: flex-start; +} + +.saved-filters-tabs ::ng-deep .mdc-evolution-chip__text-label { + display: flex; + align-items: center; + line-height: 1; +} + +.filter-chip-content { + display: inline-block; + vertical-align: middle; + line-height: 1; +} + +.filter-chip-menu-button { + opacity: 1; + width: 24px; + height: 24px; + line-height: 24px; + flex-shrink: 0; + margin-left: auto; + margin-right: -4px; +} + +.filter-chip-menu-button mat-icon { + font-size: 18px; + width: 18px; + height: 18px; } @media (prefers-color-scheme: light) { @@ -67,7 +164,9 @@ .filters-container { display: flex; + align-items: center; gap: 20px; + margin-top: -8px; transform: translateX(-10%) scale(0.8); } @@ -78,16 +177,71 @@ } } +.filters-where-label { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 4px; + margin-top: -2px !important; + white-space: nowrap; +} + +.filters-where-label .column-name-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; + vertical-align: middle; +} + +@media (prefers-color-scheme: light) { + .filters-where-label { + color: rgba(0, 0, 0, 0.87); + } +} + +@media (prefers-color-scheme: dark) { + .filters-where-label { + color: rgba(255, 255, 255, 0.87); + } +} + .dynamic-column-editor { display: flex; align-items: center; gap: 16px; + margin-left: 0; /* transform: translateX(-20%) scale(0.8); */ /* transform-origin: center right; */ } .column-name { - margin-top: -8px; + margin-top: -4px !important; + display: flex; + align-items: center; + gap: 4px; + font-weight: 500; +} + +.column-name-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; + vertical-align: middle; +} + +.comparator-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin-right: 8px; + vertical-align: middle; +} + +::ng-deep .mat-mdc-select-panel .mat-mdc-option { + display: flex; + align-items: center; } .comparator-text { @@ -137,6 +291,7 @@ gap: 8px; transform: translateX(5%) scale(1.1); padding-bottom: 16px; + margin-left: 0; } @media (width <= 600px) { @@ -147,6 +302,112 @@ .static-filter-chip { cursor: default; + pointer-events: none; +} + +::ng-deep .static-filter-chip .mat-mdc-chip { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + --mdc-chip-container-shape-radius: 16px !important; + background-color: #E8ECEE !important; + border-radius: 16px !important; + pointer-events: none !important; + cursor: default !important; +} + +::ng-deep .static-filter-chip .mat-mdc-chip:hover { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + cursor: default !important; +} + +::ng-deep .static-filter-chip .mat-mdc-chip:active { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + cursor: default !important; +} + +::ng-deep .static-filter-chip .mdc-evolution-chip__cell { + background-color: #E8ECEE !important; + border-radius: 16px !important; +} + +::ng-deep .static-filter-chip .mdc-evolution-chip__cell:hover { + background-color: #E8ECEE !important; +} + +::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + pointer-events: none !important; +} + +::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon:hover { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + cursor: default !important; +} + +::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon:active { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + cursor: default !important; +} + +::ng-deep .static-filter-chip .mdc-evolution-chip__action { + pointer-events: none !important; + cursor: default !important; +} + +@media (prefers-color-scheme: dark) { + ::ng-deep .static-filter-chip .mat-mdc-chip { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mat-mdc-chip:hover { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mat-mdc-chip:active { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mdc-evolution-chip__cell { + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mdc-evolution-chip__cell:hover { + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon:hover { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } + + ::ng-deep .static-filter-chip .mat-mdc-chip.mdc-evolution-chip--with-primary-icon:active { + --mdc-chip-container-color: #E8ECEE !important; + --mdc-chip-elevated-container-color: #E8ECEE !important; + background-color: #E8ECEE !important; + } } /* .dynamic-column-editor + .static-filters { diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html index 99b2f3bea..00f8b6716 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html @@ -13,41 +13,53 @@ - - - - - {{ filter.name }} + (click)="selectFiltersSet(filter.id)" + class="filter-chip-wrapper"> + {{ filter.name }} + + + + +
-
+
+ + subdirectory_arrow_right + where +
- where - - {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} - + + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} {{ {startswith: 'starts with', endswith: 'ends with', eq: 'equal', contains: 'contains', icontains: 'not contains', empty: 'is empty'}[savedFilterMap[selectedFilterSetId].dynamicColumn.operator] }} @@ -59,12 +71,30 @@ - starts with - ends with - equal - contains - not contains - is empty + + play_arrow + starts with + + + play_arrow + ends with + + + drag_handle + equal + + + search + contains + + + block + not contains + + + space_bar + is empty + @@ -72,11 +102,26 @@ - equal - greater than - less than - greater than or equal - less than or equal + + drag_handle + equal + + + keyboard_arrow_right + greater than + + + keyboard_arrow_left + less than + + + keyboard_double_arrow_right + greater than or equal + + + keyboard_double_arrow_left + less than or equal + @@ -133,7 +178,7 @@ >
-
+
{{ getFilter(filter) }} diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts index 2423f9224..60a5274c8 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -69,6 +69,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { public selectedFilterSetId: string | null = null; public selectedFilter: any = null; public shouldAutofocus: boolean = false; + public currentFilterForMenu: any = null; public tableStructure: any = null; public tableRowFieldsShown: { [key: string]: any } = {}; @@ -264,6 +265,29 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { // when 'filters set updated' is received } + setCurrentFilter(filter: any) { + this.currentFilterForMenu = filter; + } + + handleEditFilter(filter: any) { + if (filter) { + this.handleOpenSavedFiltersDialog(filter); + } + } + + handleDeleteFilter(filter: any) { + if (filter) { + this._tables.deleteSavedFilter(this.connectionID, this.selectedTableName, filter.id).subscribe({ + next: () => { + // The deletion will trigger 'delete saved filters' event which will refresh the list + }, + error: (error) => { + console.error('Error deleting filter:', error); + } + }); + } + } + getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { if (!filters) return []; @@ -371,6 +395,22 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { this.selectedFilter = entry; } + getOperatorIcon(operator: string): string { + const iconMap: { [key: string]: string } = { + 'startswith': 'play_arrow', + 'endswith': 'play_arrow', + 'eq': 'drag_handle', + 'contains': 'search', + 'icontains': 'block', + 'empty': 'space_bar', + 'gt': 'keyboard_arrow_right', + 'lt': 'keyboard_arrow_left', + 'gte': 'keyboard_double_arrow_right', + 'lte': 'keyboard_double_arrow_left' + }; + return iconMap[operator] || 'drag_handle'; + } + getFilter(activeFilter: {column: string, operator: string, value: any}) { const displayedName = normalizeTableName(activeFilter.column); const comparator = activeFilter.operator; diff --git a/frontend/src/app/components/dashboard/db-tables-data-source.ts b/frontend/src/app/components/dashboard/db-tables-data-source.ts index 0ff513fb5..87062b140 100644 --- a/frontend/src/app/components/dashboard/db-tables-data-source.ts +++ b/frontend/src/app/components/dashboard/db-tables-data-source.ts @@ -1,7 +1,7 @@ import { CollectionViewer } from '@angular/cdk/collections'; import { DataSource } from '@angular/cdk/table'; import { MatPaginator } from '@angular/material/paginator'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; import { filter } from 'lodash-es'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; @@ -194,7 +194,7 @@ export class TablesDataSource implements DataSource { try { parsedParams = JSON5.parse(widget.widget_params); - } catch { + } catch (error) { parsedParams = {}; } 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 b5e970b63..cff2fac9e 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 @@ -14,7 +14,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import JsonURL from '@jsonurl/jsonurl'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; import { DynamicModule } from 'ng-dynamic-component'; import { defaultTimestampValues, recordEditTypes, timestampTypes, UIwidgets } from 'src/app/consts/record-edit-types'; import { formatFieldValue } from 'src/app/lib/format-field-value'; diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts index 8ba3118d5..e53e04d01 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts @@ -44,6 +44,7 @@ export class RangeRecordViewComponent extends BaseRecordViewFieldComponent imple } private _parseWidgetParams(): void { + console.log('Parsing widget params:', this.widgetStructure?.widget_params); if (this.widgetStructure?.widget_params) { try { const params = this.widgetStructure.widget_params; @@ -60,6 +61,7 @@ export class RangeRecordViewComponent extends BaseRecordViewFieldComponent imple console.error('Failed to parse widget params:', error); } } + console.log('Parsed widget params:', { min: this.min, max: this.max, step: this.step }); } private _updateDisplayValue(): void {