0; else noTablesInCollection"
- class="selected-tables-list">
+ class="selected-tables-list"
+ cdkDropList
+ [cdkDropListDisabled]="!isReorderingEnabled(folder)"
+ (cdkDropListDropped)="onTableReorder($event, folder)">
+ cdkDrag
+ [cdkDragDisabled]="!isReorderingEnabled(folder)">
+
drag_indicator
{{getTableName(table)}}
diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.spec.ts b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.spec.ts
index 18ce44c89..da9612430 100644
--- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.spec.ts
+++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.spec.ts
@@ -21,6 +21,13 @@ describe('DbTablesListComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(DbTablesListComponent);
component = fixture.componentInstance;
+ component.tableFolders = [
+ {
+ category_id: 'all-tables-kitten',
+ category_name: 'All Tables',
+ tables: [],
+ },
+ ];
fixture.detectChanges();
});
diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts
index ac7da4bbb..df2cb1818 100644
--- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts
+++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts
@@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
+import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { DbFolderDeleteDialogComponent, DbFolderDeleteDialogData } from './db-folder-delete-dialog/db-folder-delete-dialog.component';
import { DbFolderEditDialogComponent, DbFolderEditDialogData } from './db-folder-edit-dialog/db-folder-edit-dialog.component';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
@@ -6,7 +7,6 @@ import { TableProperties, } from 'src/app/models/table';
import { AccessLevel } from 'src/app/models/user';
import { CommonModule } from '@angular/common';
-import { ConnectionsService } from 'src/app/services/connections.service';
import { ContentLoaderComponent } from '../../ui-components/content-loader/content-loader.component';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -22,6 +22,7 @@ import { TableCategory } from 'src/app/models/connection';
import { TableStateService } from 'src/app/services/table-state.service';
import { TablesService } from 'src/app/services/tables.service';
import { UiSettingsService } from 'src/app/services/ui-settings.service';
+import { normalizeTableName } from 'src/app/lib/normalize';
export interface Folder {
id: string;
@@ -32,6 +33,13 @@ export interface Folder {
isEmpty?: boolean; // Flag to indicate if folder is newly created and empty
}
+interface Category {
+ category_id: string;
+ category_name: string;
+ category_color?: string;
+ tables: TableProperties[];
+}
+
@Component({
selector: 'app-db-tables-list',
templateUrl: './db-tables-list.component.html',
@@ -39,6 +47,7 @@ export interface Folder {
imports: [
CommonModule,
FormsModule,
+ DragDropModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
@@ -55,7 +64,7 @@ export interface Folder {
export class DbTablesListComponent implements OnInit, OnChanges {
@Input() connectionID: string;
@Input() connectionTitle: string;
- @Input() tables: TableProperties[];
+ @Input() tableFolders: Category[];
@Input() selectedTable: string;
@Input() collapsed: boolean;
@Input() uiSettings: any;
@@ -83,6 +92,9 @@ export class DbTablesListComponent implements OnInit, OnChanges {
private preservedFolderStates: { [key: string]: boolean } = {};
private preservedActiveFolder: string | null = null;
+ // Search state preservation
+ private preSearchFolderStates: { [key: string]: boolean } | null = null;
+
// Folder icon colors
public folderIconColors = [
{ name: 'Default', value: '#212121' },
@@ -96,15 +108,41 @@ export class DbTablesListComponent implements OnInit, OnChanges {
];
constructor(
- private _tableState: TableStateService,_tablesService: TablesService,
- private _connectionsService: ConnectionsService,
+ private _tableState: TableStateService,
+ private _tablesService: TablesService,
private _uiSettingsService: UiSettingsService,
private dialog: MatDialog
) { }
ngOnInit() {
- this.foundTables = this.tables;
- this.loadFolders();
+ this.foundTables = this.tableFolders
+ .find((folder: any) => folder.category_id === 'all-tables-kitten')?.tables
+ .map((table: TableProperties) => {
+ return {
+ table: table.table,
+ display_name: table.display_name,
+ normalizedTableName: normalizeTableName(table.table),
+ permissions: table.permissions
+ };
+ });
+ this.folders = this.tableFolders.map(category => {
+ const tableIds = category.tables.map((table: TableProperties) => table.table);
+ return {
+ id: category.category_id,
+ name: category.category_name,
+ expanded: category.category_id === 'all-tables-kitten' ? true : false,
+ tableIds,
+ iconColor: category.category_color
+ };
+ });
+
+ const expandedFolders = this.uiSettings?.tableFoldersExpanded || ['0'];
+ if (expandedFolders && expandedFolders.length > 0) {
+ this.folders.forEach(folder => {
+ folder.expanded = expandedFolders.includes(folder.id);
+ });
+ }
+
console.log('ngOnInit - showCollapsedTableList initialized to:', this.showCollapsedTableList);
}
@@ -122,42 +160,36 @@ export class DbTablesListComponent implements OnInit, OnChanges {
searchSubstring() {
if (!this.substringToSearch || this.substringToSearch.trim() === '') {
- this.foundTables = this.tables;
- // Collapse all folders when search is cleared
+ this.foundTables = this.allTables;
+ // Restore folder expanded states from before the search
+ if (this.preSearchFolderStates) {
+ this.folders.forEach(folder => {
+ if (Object.hasOwn(this.preSearchFolderStates, folder.id)) {
+ folder.expanded = this.preSearchFolderStates[folder.id];
+ }
+ });
+ this.preSearchFolderStates = null;
+ }
+ return;
+ }
+
+ // Save folder states before the first search modification
+ if (!this.preSearchFolderStates) {
+ this.preSearchFolderStates = {};
this.folders.forEach(folder => {
- folder.expanded = false;
+ this.preSearchFolderStates[folder.id] = folder.expanded;
});
- return;
}
const searchTerm = this.substringToSearch.toLowerCase();
- // Get all tables that match the search (including those in folders)
- const allTables = [...this.tables];
-
- // Add tables from folders that might not be in the main tables list
- this.folders.forEach(folder => {
- folder.tableIds.forEach(tableId => {
- // Find the table object by ID
- const tableInFolder = this.tables.find(t => t.table === tableId);
- if (tableInFolder && !allTables.find(t => t.table === tableId)) {
- allTables.push(tableInFolder);
- }
- });
- });
-
// Filter all tables by search term
- this.foundTables = allTables.filter(tableItem =>
+ this.foundTables = this.allTables.filter(tableItem =>
tableItem.table.toLowerCase().includes(searchTerm) ||
(tableItem.display_name?.toLowerCase().includes(searchTerm)) ||
(tableItem.normalizedTableName?.toLowerCase().includes(searchTerm))
);
- // Remove duplicates
- this.foundTables = this.foundTables.filter((table, index, self) =>
- index === self.findIndex(t => t.table === table.table)
- );
-
// Expand all folders that contain matching tables
this.folders.forEach(folder => {
const folderTables = this.getFolderTables(folder);
@@ -177,20 +209,6 @@ export class DbTablesListComponent implements OnInit, OnChanges {
return table.display_name || table.normalizedTableName || table.table
}
- // saveCollapsedMenuState() {
- // const state = {
- // showCollapsedTableList: this.showCollapsedTableList,
- // currentCollapsedFolderId: this.currentCollapsedFolder?.id || null
- // };
- // const key = `collapsedMenuState_${this.connectionID}`;
- // localStorage.setItem(key, JSON.stringify(state));
- // console.log('Collapsed menu state saved:', state);
- // }
-
- // loadAndSetExpandedFolders() {
-
- // }
-
getTableNameLength(tableName: string) {
return tableName.length;
}
@@ -234,12 +252,6 @@ export class DbTablesListComponent implements OnInit, OnChanges {
this.showCollapsedTableList = true;
this.currentCollapsedFolder = folder;
}
-
- // Save the collapsed menu state
- // this.saveCollapsedMenuState();
-
- console.log('showCollapsedTableList is now:', this.showCollapsedTableList);
- console.log('currentCollapsedFolder is now:', this.currentCollapsedFolder?.name);
}
getCollapsedTableList(): TableProperties[] {
@@ -297,7 +309,11 @@ export class DbTablesListComponent implements OnInit, OnChanges {
}
getFolderTables(folder: Folder): TableProperties[] {
- const folderTables = this.tables.filter(table => folder.tableIds.includes(table.table));
+ // Preserve folder.tableIds order by mapping through tableIds
+ const allTablesMap = new Map(this.allTables.map(t => [t.table, t]));
+ const folderTables = folder.tableIds
+ .map(tableId => allTablesMap.get(tableId))
+ .filter((table): table is TableProperties => !!table);
// If there's a search term, filter the collection tables too
if (this.substringToSearch && this.substringToSearch.trim() !== '') {
@@ -314,7 +330,7 @@ export class DbTablesListComponent implements OnInit, OnChanges {
getAvailableTables(folder: Folder | null): TableProperties[] {
if (!folder) return [];
- return this.tables.filter(table => !folder.tableIds.includes(table.table));
+ return this.allTables.filter(table => !folder.tableIds.includes(table.table));
}
isTableInAnyFolder(table: TableProperties): boolean {
@@ -338,7 +354,7 @@ export class DbTablesListComponent implements OnInit, OnChanges {
const dialogData: DbFolderEditDialogData = {
folder: folder,
- tables: this.tables,
+ tables: this.allTables,
folderIconColors: this.folderIconColors
};
@@ -378,7 +394,8 @@ export class DbTablesListComponent implements OnInit, OnChanges {
get allTables(): TableProperties[] {
- return this.tables;
+ return this.tableFolders
+ ?.find((folder: any) => folder.category_id === 'all-tables-kitten')?.tables || [];
}
isTableInFolder(table: TableProperties): boolean {
@@ -451,10 +468,6 @@ export class DbTablesListComponent implements OnInit, OnChanges {
const activeFolder = this.findActiveFolder();
this.preservedActiveFolder = activeFolder ? activeFolder.id : null;
}
-
- console.log('Preserved folder states:', this.preservedFolderStates);
- console.log('Preserved active folder:', this.preservedActiveFolder);
- console.log('Expanded folders count:', Object.values(this.preservedFolderStates).filter(expanded => expanded).length);
}
private restoreFolderStates() {
@@ -468,9 +481,6 @@ export class DbTablesListComponent implements OnInit, OnChanges {
allTablesFolder.expanded = true;
this.currentCollapsedFolder = allTablesFolder;
this.showCollapsedTableList = true;
- console.log('No custom folders - keeping All Tables expanded');
- // Save the collapsed menu state after restoration
- // this.saveCollapsedMenuState();
return;
}
}
@@ -502,13 +512,6 @@ export class DbTablesListComponent implements OnInit, OnChanges {
this.showCollapsedTableList = true;
}
}
-
- console.log('Restored folder states:', this.preservedFolderStates);
- console.log('Restored active folder:', this.preservedActiveFolder);
- console.log('Has expanded folders:', hasExpandedFolders);
-
- // Save the collapsed menu state after restoration
- // this.saveCollapsedMenuState();
}
private findActiveFolder(): Folder | null {
@@ -523,57 +526,31 @@ export class DbTablesListComponent implements OnInit, OnChanges {
return Date.now().toString();
}
- private loadFolders() {
- this._connectionsService.getTablesFolders(this.connectionID).subscribe({
- next: (categories: TableCategory[]) => {
- if (categories && categories.length > 0) {
- this.tableCategories = categories;
- this.folders = categories.map(cat => ({
- id: cat.category_id,
- name: cat.category_name,
- expanded: false,
- tableIds: cat.tables,
- iconColor: cat.category_color
- }));
- console.log('Folders loaded from connection settings:', this.folders.map(c => ({ name: c.name, expanded: c.expanded })));
- } else {
- console.log('No folders found in connection settings.');
- this.folders = [];
- }
-
- const expandedFolders = this.uiSettings?.tableFoldersExpanded || ['0'];
- if (expandedFolders && expandedFolders.length > 0) {
- this.folders.forEach(folder => {
- folder.expanded = expandedFolders.includes(folder.id);
- });
- }
+ private getDropIndex(event: DragEvent, folder: Folder): number {
+ const folderEl = (event.currentTarget as HTMLElement);
+ const tableItems = folderEl.querySelectorAll('.selected-table-item');
+ const y = event.clientY;
- const allTablesFolder: Folder = {
- id: '0',
- name: 'All Tables',
- expanded: expandedFolders && expandedFolders.length === 0 ? false : expandedFolders.includes('0'),
- tableIds: this.tables.map(table => table.table)
- };
- this.folders.unshift(allTablesFolder);
- },
- error: (error) => {
- console.error('Error fetching folders from connection settings:', error);
- this.folders = [];
+ for (let i = 0; i < tableItems.length; i++) {
+ const rect = tableItems[i].getBoundingClientRect();
+ const midY = rect.top + rect.height / 2;
+ if (y < midY) {
+ return i;
}
- });
+ }
+ return folder.tableIds.length;
}
private saveFolders() {
try {
this.tableCategories = this.folders
- .filter(folder => folder.name !== 'All Tables') // Exclude "All Tables" from saving
.map(folder => ({
category_id: folder.id,
category_name: folder.name,
category_color: folder.iconColor,
tables: folder.tableIds
}));
- this._connectionsService.updateTablesFolders(this.connectionID, this.tableCategories).subscribe({
+ this._tablesService.updateTablesFolders(this.connectionID, this.tableCategories).subscribe({
next: () => {
console.log('Connection settings updated with folders.');
},
@@ -586,6 +563,21 @@ export class DbTablesListComponent implements OnInit, OnChanges {
}
}
+ // Table reordering within folders
+ isReorderingEnabled(folder: Folder): boolean {
+ return this.accessLevel === 'edit'
+ && !this.collapsed
+ && (!this.substringToSearch || this.substringToSearch.trim() === '');
+ }
+
+ onTableReorder(event: CdkDragDrop
, folder: Folder) {
+ if (event.previousIndex === event.currentIndex) {
+ return;
+ }
+ moveItemInArray(folder.tableIds, event.previousIndex, event.currentIndex);
+ this.saveFolders();
+ }
+
// Drag and drop methods
onTableDragStart(event: DragEvent, table: TableProperties) {
this.draggedTable = table;
@@ -628,13 +620,13 @@ export class DbTablesListComponent implements OnInit, OnChanges {
// Table already exists in this folder, do nothing (no notification)
return;
} else {
- // Simply add table to the target folder (don't remove from other folders)
- folder.tableIds.push(this.draggedTable.table);
+ // Determine insertion index based on drop position
+ const insertIndex = this.getDropIndex(event, folder);
+ folder.tableIds.splice(insertIndex, 0, this.draggedTable.table);
// Remove empty flag when adding tables via drag and drop
if (folder.isEmpty) {
folder.isEmpty = false;
}
- console.log('onFolderDrop');
this.saveFolders();
}
}
diff --git a/frontend/src/app/components/secrets/secrets.component.spec.ts b/frontend/src/app/components/secrets/secrets.component.spec.ts
index 84a24ea63..e9647c114 100644
--- a/frontend/src/app/components/secrets/secrets.component.spec.ts
+++ b/frontend/src/app/components/secrets/secrets.component.spec.ts
@@ -1,5 +1,6 @@
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator';
@@ -11,6 +12,7 @@ import { BehaviorSubject, of } from 'rxjs';
import { Secret } from 'src/app/models/secret';
import { CompanyService } from 'src/app/services/company.service';
import { SecretsService } from 'src/app/services/secrets.service';
+import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component';
import { AuditLogDialogComponent } from './audit-log-dialog/audit-log-dialog.component';
import { CreateSecretDialogComponent } from './create-secret-dialog/create-secret-dialog.component';
import { DeleteSecretDialogComponent } from './delete-secret-dialog/delete-secret-dialog.component';
@@ -64,7 +66,12 @@ describe('SecretsComponent', () => {
{ provide: CompanyService, useValue: mockCompanyService },
{ provide: MatDialog, useValue: mockDialog },
],
- }).compileComponents();
+ })
+ .overrideComponent(SecretsComponent, {
+ remove: { imports: [ProfileSidebarComponent] },
+ add: { schemas: [NO_ERRORS_SCHEMA] },
+ })
+ .compileComponents();
fixture = TestBed.createComponent(SecretsComponent);
component = fixture.componentInstance;
diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts
index fb6ab7bdc..4302368bb 100644
--- a/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts
+++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts
@@ -125,9 +125,17 @@ describe('TurnstileComponent', () => {
});
fixture.detectChanges();
- // MAX_POLL_ATTEMPTS (50) * POLL_INTERVAL_MS (100) = 5000ms
- await delay(5200);
+
+ // Poll until the error is emitted instead of relying on a fixed delay,
+ // since zone.js and browser overhead make timers unreliable.
+ const maxWait = 12000;
+ const pollInterval = 200;
+ let waited = 0;
+ while (!errorEmitted && waited < maxWait) {
+ await delay(pollInterval);
+ waited += pollInterval;
+ }
expect(errorEmitted).toBe(true);
- }, 10000);
+ }, 20000);
});
diff --git a/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts b/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts
index 322eff0ce..54c1bf3d7 100644
--- a/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts
+++ b/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts
@@ -118,6 +118,7 @@ describe('PermissionsAddDialogComponent', () => {
fixture = TestBed.createComponent(PermissionsAddDialogComponent);
component = fixture.componentInstance;
usersService = TestBed.inject(UsersService);
+ vi.spyOn(usersService, 'fetchPermission').mockReturnValue(of(fakePermissionsResponse));
fixture.detectChanges();
});
diff --git a/frontend/src/app/models/table.ts b/frontend/src/app/models/table.ts
index 3dfb071da..248f193f0 100644
--- a/frontend/src/app/models/table.ts
+++ b/frontend/src/app/models/table.ts
@@ -10,6 +10,8 @@ export interface TableProperties {
table: string,
display_name?: string,
normalizedTableName?: string,
+ initials?: string,
+ icon?: string,
permissions: TablePermissions,
}
diff --git a/frontend/src/app/services/connections.service.ts b/frontend/src/app/services/connections.service.ts
index fa6a88f73..f88872747 100644
--- a/frontend/src/app/services/connections.service.ts
+++ b/frontend/src/app/services/connections.service.ts
@@ -503,33 +503,4 @@ export class ConnectionsService {
}),
);
}
-
- getTablesFolders(connectionID: string) {
- return this._http.get(`/table-categories/${connectionID}`).pipe(
- map((res) => {
- return res;
- }),
- catchError((err) => {
- console.log(err);
- const errorMessage = err.error?.message || 'Unknown error';
- this._notifications.showErrorSnackbar(`${errorMessage}.`);
- return EMPTY;
- }),
- );
- }
-
- updateTablesFolders(connectionID: string, tablesFolders: any) {
- return this._http.put(`/table-categories/${connectionID}`, tablesFolders).pipe(
- map((res) => {
- // this._notifications.showSuccessSnackbar('Connection settings has been updated successfully.');
- return res;
- }),
- catchError((err) => {
- console.log(err);
- const errorMessage = err.error?.message || 'Unknown error';
- this._notifications.showErrorSnackbar(`${errorMessage}.`);
- return EMPTY;
- }),
- );
- }
}
diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts
index 72b2db20e..2df9e2598 100644
--- a/frontend/src/app/services/tables.service.ts
+++ b/frontend/src/app/services/tables.service.ts
@@ -79,6 +79,36 @@ export class TablesService {
);
}
+ fetchTablesFolders(connectionID: string, hidden?: boolean) {
+ console.log('fetchTablesFolders service')
+ return this._http
+ .get(`/table-categories/v2/${connectionID}`, {
+ params: {
+ ...(hidden ? { hidden } : {}),
+ },
+ })
+ .pipe(
+ map((res) => {
+ return res;
+ }),
+ );
+ }
+
+ updateTablesFolders(connectionID: string, tablesFolders: any) {
+ return this._http.put(`/table-categories/${connectionID}`, tablesFolders).pipe(
+ map((res) => {
+ // this._notifications.showSuccessSnackbar('Connection settings has been updated successfully.');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ const errorMessage = err.error?.message || 'Unknown error';
+ this._notifications.showErrorSnackbar(`${errorMessage}.`);
+ return EMPTY;
+ }),
+ );
+ }
+
fetchTable({
connectionID,
tableName,