diff --git a/backend/src/entities/table-settings/application/data-structures/create-table-settings.ds.ts b/backend/src/entities/table-settings/application/data-structures/create-table-settings.ds.ts index 54cc254af..71ec333a6 100644 --- a/backend/src/entities/table-settings/application/data-structures/create-table-settings.ds.ts +++ b/backend/src/entities/table-settings/application/data-structures/create-table-settings.ds.ts @@ -81,4 +81,10 @@ export class CreateTableSettingsDs { @ApiProperty() allow_csv_import: boolean; + + @ApiProperty() + save_ordering_by_default: boolean; + + @ApiProperty({ required: false }) + save_ordering_by_default_columns?: { [columnName: string]: boolean }; } diff --git a/backend/src/entities/table-settings/application/data-structures/found-table-settings.ds.ts b/backend/src/entities/table-settings/application/data-structures/found-table-settings.ds.ts index bfa89efff..3da748cee 100644 --- a/backend/src/entities/table-settings/application/data-structures/found-table-settings.ds.ts +++ b/backend/src/entities/table-settings/application/data-structures/found-table-settings.ds.ts @@ -82,4 +82,10 @@ export class FoundTableSettingsDs { @ApiProperty() allow_csv_import: boolean; + + @ApiProperty() + save_ordering_by_default: boolean; + + @ApiProperty({ required: false }) + save_ordering_by_default_columns?: { [columnName: string]: boolean }; } diff --git a/backend/src/entities/table-settings/table-settings.controller.ts b/backend/src/entities/table-settings/table-settings.controller.ts index 95471b3d7..1a82d7fa8 100644 --- a/backend/src/entities/table-settings/table-settings.controller.ts +++ b/backend/src/entities/table-settings/table-settings.controller.ts @@ -120,6 +120,8 @@ export class TableSettingsController { @Body('icon') icon: string, @Body('allow_csv_export') allow_csv_export: boolean, @Body('allow_csv_import') allow_csv_import: boolean, + @Body('save_ordering_by_default') save_ordering_by_default: boolean, + @Body('save_ordering_by_default_columns') save_ordering_by_default_columns: { [columnName: string]: boolean }, @UserId() userId: string, @MasterPassword() masterPwd: string, ): Promise { @@ -150,6 +152,8 @@ export class TableSettingsController { icon: icon, allow_csv_export: allow_csv_export, allow_csv_import: allow_csv_import, + save_ordering_by_default: save_ordering_by_default, + save_ordering_by_default_columns: save_ordering_by_default_columns, }; const errors = this.validateParameters(inputData); @@ -200,6 +204,8 @@ export class TableSettingsController { @Body('icon') icon: string, @Body('allow_csv_export') allow_csv_export: boolean, @Body('allow_csv_import') allow_csv_import: boolean, + @Body('save_ordering_by_default') save_ordering_by_default: boolean, + @Body('save_ordering_by_default_columns') save_ordering_by_default_columns: { [columnName: string]: boolean }, @UserId() userId: string, @MasterPassword() masterPwd: string, ): Promise { @@ -229,6 +235,8 @@ export class TableSettingsController { icon: icon, allow_csv_export: allow_csv_export, allow_csv_import: allow_csv_import, + save_ordering_by_default: save_ordering_by_default, + save_ordering_by_default_columns: save_ordering_by_default_columns, }; const errors = this.validateParameters(inputData); diff --git a/backend/src/entities/table-settings/table-settings.entity.ts b/backend/src/entities/table-settings/table-settings.entity.ts index d0147a3ca..8b63d64c9 100644 --- a/backend/src/entities/table-settings/table-settings.entity.ts +++ b/backend/src/entities/table-settings/table-settings.entity.ts @@ -73,6 +73,12 @@ export class TableSettingsEntity { @Column({ default: true, type: 'boolean' }) allow_csv_import: boolean; + @Column({ default: false, type: 'boolean' }) + save_ordering_by_default: boolean; + + @Column('jsonb', { default: null, nullable: true }) + save_ordering_by_default_columns: { [columnName: string]: boolean } | null; + @Column('varchar', { array: true, default: null }) sensitive_fields: string[]; diff --git a/backend/src/entities/table-settings/utils/build-found-table-settings-ds.ts b/backend/src/entities/table-settings/utils/build-found-table-settings-ds.ts index ae8ffb2eb..4fc1cee3f 100644 --- a/backend/src/entities/table-settings/utils/build-found-table-settings-ds.ts +++ b/backend/src/entities/table-settings/utils/build-found-table-settings-ds.ts @@ -29,6 +29,8 @@ export function buildFoundTableSettingsDs(tableSettings: TableSettingsEntity): F icon, allow_csv_export, allow_csv_import, + save_ordering_by_default, + save_ordering_by_default_columns, } = tableSettings; let connection_id = tableSettings.connection_id as unknown; if (connection_id instanceof ConnectionEntity) { @@ -61,5 +63,7 @@ export function buildFoundTableSettingsDs(tableSettings: TableSettingsEntity): F icon: icon, allow_csv_export: allow_csv_export, allow_csv_import: allow_csv_import, + save_ordering_by_default: save_ordering_by_default, + save_ordering_by_default_columns: save_ordering_by_default_columns || undefined, }; } diff --git a/backend/src/entities/table-settings/utils/build-new-table-settings-entity.ts b/backend/src/entities/table-settings/utils/build-new-table-settings-entity.ts index 1fdef0450..ad1072e93 100644 --- a/backend/src/entities/table-settings/utils/build-new-table-settings-entity.ts +++ b/backend/src/entities/table-settings/utils/build-new-table-settings-entity.ts @@ -32,6 +32,8 @@ export function buildNewTableSettingsEntity( icon, allow_csv_export, allow_csv_import, + save_ordering_by_default, + save_ordering_by_default_columns, } = settings; newSettings.connection_id = connection; newSettings.display_name = display_name; @@ -58,5 +60,7 @@ export function buildNewTableSettingsEntity( newSettings.icon = icon; newSettings.allow_csv_export = allow_csv_export; newSettings.allow_csv_import = allow_csv_import; + newSettings.save_ordering_by_default = save_ordering_by_default; + newSettings.save_ordering_by_default_columns = save_ordering_by_default_columns || null; return newSettings; } diff --git a/backend/src/migrations/1767703855307-AddSaveOrderingByDefaultInTableSettingsEntity.ts b/backend/src/migrations/1767703855307-AddSaveOrderingByDefaultInTableSettingsEntity.ts new file mode 100644 index 000000000..006107814 --- /dev/null +++ b/backend/src/migrations/1767703855307-AddSaveOrderingByDefaultInTableSettingsEntity.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSaveOrderingByDefaultInTableSettingsEntity1767703855307 implements MigrationInterface { + name = 'AddSaveOrderingByDefaultInTableSettingsEntity1767703855307'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tableSettings" ADD "save_ordering_by_default" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tableSettings" DROP COLUMN "save_ordering_by_default"`); + } +} + diff --git a/backend/src/migrations/1767704529763-AddSaveOrderingByDefaultColumnsInTableSettingsEntity.ts b/backend/src/migrations/1767704529763-AddSaveOrderingByDefaultColumnsInTableSettingsEntity.ts new file mode 100644 index 000000000..ef838c393 --- /dev/null +++ b/backend/src/migrations/1767704529763-AddSaveOrderingByDefaultColumnsInTableSettingsEntity.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSaveOrderingByDefaultColumnsInTableSettingsEntity1767704529763 implements MigrationInterface { + name = 'AddSaveOrderingByDefaultColumnsInTableSettingsEntity1767704529763'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tableSettings" ADD "save_ordering_by_default_columns" jsonb DEFAULT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tableSettings" DROP COLUMN "save_ordering_by_default_columns"`); + } +} + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 8a9a78ccb..e328a52a5 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -114,6 +114,9 @@ export class AppComponent { this.matIconRegistry.addSvgIcon("github", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/github.svg")); this.matIconRegistry.addSvgIcon("google", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/google.svg")); this.matIconRegistry.addSvgIcon("ai_rocket", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/ai-rocket.svg")); + this.matIconRegistry.addSvgIcon("sort_ascending", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/sort-ascending.svg")); + this.matIconRegistry.addSvgIcon("sort_descending", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/sort-descending.svg")); + this.matIconRegistry.addSvgIcon("sorting", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/sorting.svg")); angulartics2Amplitude.startTracking(); } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts index bb582fbe9..bc682fd8a 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts @@ -83,6 +83,7 @@ export class DbTableSettingsComponent implements OnInit { allow_csv_export: true, allow_csv_import: true, can_delete: true, + save_ordering_by_default: false, } public tableSettings: TableSettings = null; public defaultIcons = ['favorite', 'star', 'done', 'arrow_forward', 'key', 'lock', 'visibility', 'language', 'notifications', 'schedule']; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index 4a4673591..58d1d067d 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -427,6 +427,284 @@ th.mat-header-cell, td.mat-cell { text-align: left; } +.sort-header-cell { + padding: 0 !important; +} + +.sort-header-cell:hover .sort-header-button, +.sort-header-cell.sort-header-active .sort-header-button, +.sort-header-button[aria-expanded="true"], +.sort-header-button:focus { + opacity: 1; + visibility: visible; +} + +.sort-header-content { + display: flex; + align-items: center; + width: 100%; + padding: 0 16px 0 0; + text-align: left; +} + +.sort-header-button { + width: 32px; + height: 32px; + --mdc-icon-button-icon-size: 18px; + margin-left: 4px; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; +} + +.sort-header-button .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +.sort-header-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +.sort-menu { + min-width: 280px; +} + +.sort-menu .mat-mdc-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} + +.sort-menu-item-content { + display: flex; + align-items: center; + gap: 6px; +} + +.sort-menu-item-content.sort-menu-item-active-content { + background-color: rgba(0, 0, 0, 0.08); + padding: 4px 8px; + margin: -4px -8px; + border-radius: 4px; +} + +@media (prefers-color-scheme: dark) { + .sort-menu-item-content.sort-menu-item-active-content { + background-color: rgba(255, 255, 255, 0.08); + } +} + +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content, +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content { + background-color: rgba(0, 0, 0, 0.08) !important; + padding: 4px 8px !important; + margin: -4px -8px !important; + border-radius: 4px !important; + display: inline-flex !important; +} + +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content span, +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content span { + background-color: rgba(0, 0, 0, 0.08) !important; + padding: 2px 6px !important; + border-radius: 4px !important; + display: inline-block !important; +} + +@media (prefers-color-scheme: dark) { + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content, + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content span, + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active .sort-menu-item-content span { + background-color: rgba(255, 255, 255, 0.08) !important; + } +} + +.sort-menu-icon { + font-size: 16px; + width: 16px; + height: 16px; +} + +.sort-menu-icon-container { + display: flex; + align-items: center; + gap: 4px; + position: relative; + width: 28px; + height: 16px; +} + +.sort-menu-icon-lines { + display: flex; + flex-direction: column; + gap: 3px; + align-items: flex-start; + width: 12px; + height: 14px; +} + +.sort-menu-line { + display: inline-block; + height: 2px; + background-color: currentColor; + border-radius: 1px; +} + +.sort-menu-line-1 { + width: 6px; +} + +.sort-menu-line-2 { + width: 8px; +} + +.sort-menu-line-3 { + width: 12px; +} + + +.sort-menu-icon-arrow { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; +} + +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active, +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active { + background-color: rgba(0, 0, 0, 0.08) !important; +} + +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:hover, +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:hover { + background-color: rgba(0, 0, 0, 0.12) !important; +} + +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:focus, +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:focus { + background-color: rgba(0, 0, 0, 0.08) !important; +} + +:host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:focus-visible, +:host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:focus-visible { + background-color: rgba(0, 0, 0, 0.08) !important; +} + +@media (prefers-color-scheme: dark) { + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active, + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:hover, + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:hover { + background-color: rgba(255, 255, 255, 0.12) !important; + } + + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:focus, + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:focus { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + :host ::ng-deep .sort-menu button.mat-mdc-menu-item.sort-menu-item-active:focus-visible, + :host ::ng-deep .sort-menu .mat-mdc-menu-item.sort-menu-item-active:focus-visible { + background-color: rgba(255, 255, 255, 0.08) !important; + } +} + +.sort-icon-container { + position: relative; + width: 28px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 0; +} + +.sort-icon-custom { + position: relative; + width: 14px; + height: 14px; + flex-shrink: 0; +} + +.sort-icon-custom::before, +.sort-icon-custom::after { + content: ''; + position: absolute; + left: 0; + height: 2px; + background-color: currentColor; + border-radius: 1px; +} + +/* A-Z: короткая, длинная, короткая */ +.sort-icon-asc .sort-icon-custom { + background-color: currentColor; + width: 6px; + height: 2px; + border-radius: 1px; + top: 0; +} + +.sort-icon-asc .sort-icon-custom::before { + top: 5px; + width: 12px; +} + +.sort-icon-asc .sort-icon-custom::after { + top: 10px; + width: 6px; +} + +/* Z-A: короткая, длинная, короткая */ +.sort-icon-desc .sort-icon-custom { + background-color: currentColor; + width: 6px; + height: 2px; + border-radius: 1px; + top: 0; +} + +.sort-icon-desc .sort-icon-custom::before { + top: 5px; + width: 12px; +} + +.sort-icon-desc .sort-icon-custom::after { + top: 10px; + width: 6px; +} + +.sort-icon-arrow { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.sort-icon-arrow { + transform: rotate(180deg); +} + +.sort-icon-arrow-down { + transform: rotate(180deg); +} + .db-table-cell-checkbox { display: flex; align-items: center; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 5dd7b8391..bc8541711 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -235,7 +235,40 @@

{{ displayName }}

- {{ tableData.dataNormalizedColumns[column] }} + +
+ {{ tableData.dataNormalizedColumns[column] }} + + + + + +
+
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 84f8f9c01..4cd1ea982 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 @@ -40,6 +40,7 @@ import { SavedFiltersPanelComponent } from './saved-filters-panel/saved-filters- import { SelectionModel } from '@angular/cdk/collections'; 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 { formatFieldValue } from 'src/app/lib/format-field-value'; import { getTableTypes } from 'src/app/lib/setup-table-row-structure'; import { merge } from 'rxjs'; @@ -133,6 +134,8 @@ export class DbTableViewComponent implements OnInit { public tableRelatedRecords: any = null; public displayCellComponents; public UIwidgets = UIwidgets; + private tableSettings: any = null; + private isSettingsExist: boolean = false; // public tableTypes: object; @Input() set table(value){ @@ -147,6 +150,7 @@ export class DbTableViewComponent implements OnInit { private _notifications: NotificationsService, private _tableRow: TableRowService, private _connections: ConnectionsService, + private _tables: TablesService, private route: ActivatedRoute, public router: Router, public dialog: MatDialog, @@ -178,9 +182,110 @@ export class DbTableViewComponent implements OnInit { } }); this.loadRowsPage(); + + // Save ordering to table settings when sort changes + if (this.sort.active) { + this.saveOrderingToSettings(this.sort.active, this.sort.direction.toUpperCase()); + } else { + this.saveOrderingToSettings('', 'ASC'); + } }) ) .subscribe(); + + // Apply saved sorting after view is initialized + // Use setTimeout to ensure settings are loaded from ngOnInit + setTimeout(() => { + this.applySavedSorting(); + }, 100); + } + + applySavedSorting() { + if (!this.connectionID || !this.name || !this.sort || !this.paginator) return; + + // Check if there's a saved sort in URL params first + const urlSortActive = this.route.snapshot.queryParams.sort_active; + const urlSortDirection = this.route.snapshot.queryParams.sort_direction; + + if (urlSortActive && urlSortDirection) { + // URL params take precedence - apply them + const direction = urlSortDirection.toLowerCase() as 'asc' | 'desc'; + this.sort.sort({ + id: urlSortActive, + start: direction, + disableClear: true + }); + return; + } + + // If no URL params, load from table settings and apply + if (this.tableSettings && this.tableSettings.ordering_field && this.tableSettings.ordering) { + const direction = this.tableSettings.ordering.toLowerCase() as 'asc' | 'desc'; + this.sort.sort({ + id: this.tableSettings.ordering_field, + start: direction, + disableClear: true + }); + + // Update URL to reflect saved sorting + const filters = JsonURL.stringify(this.activeFilters); + const saved_filter = this.route.snapshot.queryParams.saved_filter; + const dynamic_column = this.route.snapshot.queryParams.dynamic_column; + + this.router.navigate([`/dashboard/${this.connectionID}/${this.name}`], { + queryParams: { + filters, + saved_filter, + dynamic_column, + sort_active: this.tableSettings.ordering_field, + sort_direction: this.tableSettings.ordering, + page_index: this.paginator.pageIndex, + page_size: this.paginator.pageSize + }, + replaceUrl: true + }); + this.loadRowsPage(); + } else { + // Load settings if not loaded yet + this._tables.fetchTableSettings(this.connectionID, this.name) + .subscribe(settings => { + if (settings && Object.keys(settings).length > 0) { + this.isSettingsExist = true; + this.tableSettings = settings; + + if (settings.ordering_field && settings.ordering) { + const direction = settings.ordering.toLowerCase() as 'asc' | 'desc'; + this.sort.sort({ + id: settings.ordering_field, + start: direction, + disableClear: true + }); + + // Update URL to reflect saved sorting + const filters = JsonURL.stringify(this.activeFilters); + const saved_filter = this.route.snapshot.queryParams.saved_filter; + const dynamic_column = this.route.snapshot.queryParams.dynamic_column; + + this.router.navigate([`/dashboard/${this.connectionID}/${this.name}`], { + queryParams: { + filters, + saved_filter, + dynamic_column, + sort_active: settings.ordering_field, + sort_direction: settings.ordering, + page_index: this.paginator.pageIndex, + page_size: this.paginator.pageSize + }, + replaceUrl: true + }); + this.loadRowsPage(); + } + } else { + this.isSettingsExist = false; + this.tableSettings = null; + } + }); + } } ngOnInit() { @@ -198,6 +303,20 @@ export class DbTableViewComponent implements OnInit { this.hasSavedFilterActive = !!params.saved_filter; if (this.hasSavedFilterActive ) this.searchString = ''; }); + + // Load table settings - will be applied in ngAfterViewInit + if (this.connectionID && this.name) { + this._tables.fetchTableSettings(this.connectionID, this.name) + .subscribe(settings => { + if (settings && Object.keys(settings).length > 0) { + this.isSettingsExist = true; + this.tableSettings = settings; + } else { + this.isSettingsExist = false; + this.tableSettings = null; + } + }); + } } onInput(searchValue: string) { @@ -234,6 +353,27 @@ export class DbTableViewComponent implements OnInit { if (changes.name?.currentValue && this.paginator) { this.paginator.pageIndex = 0; this.searchString = ''; + + // Reload table settings when table name changes + if (this.connectionID && this.name) { + this._tables.fetchTableSettings(this.connectionID, this.name) + .subscribe(settings => { + if (settings && Object.keys(settings).length > 0) { + this.isSettingsExist = true; + this.tableSettings = settings; + } else { + this.isSettingsExist = false; + this.tableSettings = null; + } + + // Apply saved sorting after a short delay to ensure sort is ready + if (this.sort && this.paginator) { + setTimeout(() => { + this.applySavedSorting(); + }, 100); + } + }); + } } } @@ -241,6 +381,123 @@ export class DbTableViewComponent implements OnInit { return this.tableData.sortByColumns.includes(column) || !this.tableData.sortByColumns.length; } + getSortIcon(column: string): string { + if (this.sort && this.sort.active === column) { + return this.sort.direction === 'asc' ? 'arrow_upward' : 'arrow_downward'; + } + return 'sync_alt'; + } + + getSortTooltip(column: string): string { + if (this.sort && this.sort.active === column) { + return this.sort.direction === 'asc' + ? 'Sort ascending (A-Z)' + : 'Sort descending (Z-A)'; + } + return 'Sort column'; + } + + applySort(column: string, direction: 'asc' | 'desc') { + if (!this.sort || !this.paginator) return; + + // Если колонка уже отсортирована в том же направлении, отменяем сортировку + if (this.sort.active === column && this.sort.direction === direction) { + this.clearSort(); + return; + } + + // Применяем сортировку программно через MatSort API + this.sort.sort({ + id: column, + start: direction, + disableClear: true + }); + + // Триггерим событие сортировки вручную, чтобы обновить URL и загрузить данные + const filters = JsonURL.stringify(this.activeFilters); + const saved_filter = this.route.snapshot.queryParams.saved_filter; + const dynamic_column = this.route.snapshot.queryParams.dynamic_column; + + this.router.navigate([`/dashboard/${this.connectionID}/${this.name}`], { + queryParams: { + filters, + saved_filter, + dynamic_column, + sort_active: column, + sort_direction: direction.toUpperCase(), + page_index: this.paginator.pageIndex, + page_size: this.paginator.pageSize + } + }); + this.loadRowsPage(); + + // Always save ordering to table settings + this.saveOrderingToSettings(column, direction.toUpperCase()); + } + + saveOrderingToSettings(column: string, direction: string) { + if (!this.connectionID || !this.name) return; + + if (!this.tableSettings) { + // Create new settings if they don't exist + this.tableSettings = { + connection_id: this.connectionID, + table_name: this.name, + icon: '', + display_name: '', + autocomplete_columns: [], + identity_column: '', + search_fields: [], + excluded_fields: [], + list_fields: [], + ordering: direction, + ordering_field: column, + readonly_fields: [], + sortable_by: [], + columns_view: [], + sensitive_fields: [], + allow_csv_export: true, + allow_csv_import: true, + can_delete: true, + }; + } else { + // Update existing settings - set ordering + this.tableSettings.ordering = direction; + this.tableSettings.ordering_field = column; + } + + this._tables.updateTableSettings(this.isSettingsExist, this.connectionID, this.name, this.tableSettings) + .subscribe(() => { + // Settings updated successfully + }); + } + + clearSort() { + if (!this.sort || !this.paginator) return; + + // Очищаем сортировку, вызывая sort с пустым id + this.sort.sort({ id: '', start: 'asc', disableClear: false }); + + const filters = JsonURL.stringify(this.activeFilters); + const saved_filter = this.route.snapshot.queryParams.saved_filter; + const dynamic_column = this.route.snapshot.queryParams.dynamic_column; + + // Навигация без параметров сортировки (они будут удалены из URL) + this.router.navigate([`/dashboard/${this.connectionID}/${this.name}`], { + queryParams: { + filters, + saved_filter, + dynamic_column, + page_index: this.paginator.pageIndex, + page_size: this.paginator.pageSize + } + }); + this.loadRowsPage(); + + // Save cleared sorting to table settings + this.saveOrderingToSettings('', 'ASC'); + } + isForeignKey(column: string) { return this.tableData.foreignKeysList.includes(column); } 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..4c86c836e 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 @@ -109,3 +109,98 @@ 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-right: 40px; + 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; +} + +.comparator-text { + grid-column: 3; + font-size: 14px; + font-weight: 400; + line-height: 20px; + padding: 8px 0; + align-self: center; + color: rgba(0, 0, 0, 0.87); +} + +@media (prefers-color-scheme: dark) { + .comparator-text { + color: rgba(255, 255, 255, 0.87); + } +} + +.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.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.ts index 1c658f9cf..03533dbc5 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 @@ -212,6 +212,38 @@ 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'; + } + + getOperatorText(operator: string): string { + const textMap: { [key: string]: string } = { + 'startswith': 'starts with', + 'endswith': 'ends with', + 'eq': 'equal', + 'contains': 'contains', + 'icontains': 'not contains', + 'empty': 'is empty', + 'gt': 'greater than', + 'lt': 'less than', + 'gte': 'greater than or equal', + 'lte': 'less than or equal' + }; + return textMap[operator] || 'equal'; + } + removeFilter(field) { delete this.tableRowFieldsShown[field]; delete this.tableRowFieldsComparator[field]; @@ -230,6 +262,10 @@ export class SavedFiltersDialogComponent implements OnInit { this.dynamicColumn = null; } else { this.dynamicColumn = field; + // Ensure comparator is set (default to 'eq' if not set) + if (!this.tableRowFieldsComparator[field]) { + this.tableRowFieldsComparator[field] = 'eq'; + } } } 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 4e50b5293..e35898227 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 @@ -80,7 +80,7 @@ } .column-name { - margin-top: -8px; + margin-top: 0; } @media (prefers-color-scheme: light) { @@ -98,6 +98,20 @@ margin-left: 8px; } +.comparator-text { + font-size: 14px; + font-weight: 400; + line-height: 20px; + margin-left: 8px; + color: rgba(0, 0, 0, 0.87); +} + +@media (prefers-color-scheme: dark) { + .comparator-text { + color: rgba(255, 255, 255, 0.87); + } +} + .dynamic-column-editor ::ng-deep .foreign-key { display: block; min-width: 280px; 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 3ea37f474..e82b65126 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 @@ -44,35 +44,13 @@
- where - - {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} - + + + {{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }} + + {{ getOperatorText(savedFilterMap[selectedFilterSetId]?.dynamicColumn.operator || 'eq') }} + - - - starts with - ends with - equal - contains - not contains - is empty - - - - - - equal - greater than - less than - greater than or equal - less than or equal - -
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..b4b3036f3 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 @@ -371,6 +371,38 @@ 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'; + } + + getOperatorText(operator: string): string { + const textMap: { [key: string]: string } = { + 'startswith': 'starts with', + 'endswith': 'ends with', + 'eq': 'equal', + 'contains': 'contains', + 'icontains': 'not contains', + 'empty': 'is empty', + 'gt': 'greater than', + 'lt': 'less than', + 'gte': 'greater than or equal', + 'lte': 'less than or equal' + }; + return textMap[operator] || 'equal'; + } + getFilter(activeFilter: {column: string, operator: string, value: any}) { const displayedName = normalizeTableName(activeFilter.column); const comparator = activeFilter.operator; diff --git a/frontend/src/app/models/table.ts b/frontend/src/app/models/table.ts index ab5c6233b..6635e857b 100644 --- a/frontend/src/app/models/table.ts +++ b/frontend/src/app/models/table.ts @@ -37,6 +37,8 @@ export interface TableSettings { allow_csv_export: boolean, allow_csv_import: boolean, can_delete: boolean, + save_ordering_by_default: boolean, + save_ordering_by_default_columns?: { [columnName: string]: boolean }, } export interface TableRow { diff --git a/frontend/src/assets/icons/sort-ascending.svg b/frontend/src/assets/icons/sort-ascending.svg new file mode 100644 index 000000000..0e7446a36 --- /dev/null +++ b/frontend/src/assets/icons/sort-ascending.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/sort-descending.svg b/frontend/src/assets/icons/sort-descending.svg new file mode 100644 index 000000000..7fc67db8e --- /dev/null +++ b/frontend/src/assets/icons/sort-descending.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/sorting.svg b/frontend/src/assets/icons/sorting.svg new file mode 100644 index 000000000..3e81159ff --- /dev/null +++ b/frontend/src/assets/icons/sorting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + +