Table list ordering#1617
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces table list ordering functionality, allowing users to reorder tables within folders using drag-and-drop. The changes refactor the table data structure from a flat list to a hierarchical folder-based system with ordering support.
Changes:
- Moves table folder management methods from ConnectionsService to TablesService, introducing a new v2 API endpoint for fetching table folders
- Adds CDK drag-drop functionality to enable table reordering within folders, with order persistence via UI settings for "All Tables" and connection settings for custom folders
- Refactors components to use
tableFolders(hierarchical structure) instead of separatetablesandfoldersinputs, simplifying data flow
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/app/services/tables.service.ts | Adds fetchTablesFolders and updateTablesFolders methods to handle table folder operations |
| frontend/src/app/services/connections.service.ts | Removes getTablesFolders and updateTablesFolders methods, consolidating folder operations in TablesService |
| frontend/src/app/models/ui-settings.ts | Adds allTablesOrder property to persist table order for the "All Tables" folder |
| frontend/src/app/models/table.ts | Adds initials and icon properties to TableProperties for improved UI display |
| frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts | Refactors to use tableFolders input, implements CDK drag-drop for table reordering, adds commented-out legacy code |
| frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html | Integrates CDK drag-drop directives for reordering, maintains native drag-drop for folder operations |
| frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css | Adds styling for drag-drop indicators and animations |
| frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts | Refactors to use single tableFolders input, adds computed properties for table organization |
| frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html | Simplifies table switcher dropdown to use folder categories from tableFolders |
| frontend/src/app/components/dashboard/dashboard.component.ts | Refactors getData from async/await to reactive subscribe pattern, introduces tableFolders data structure |
| frontend/src/app/components/dashboard/dashboard.component.html | Updates component bindings to pass tableFolders instead of separate tables/folders |
| frontend/src/app/app.component.html | Updates copyright year to 2026 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [draggable]="true" | ||
| (dragstart)="onTableDragStart($event, table)" | ||
| (dragend)="onTableDragEnd($event)" |
There was a problem hiding this comment.
Potential conflict between two drag-and-drop mechanisms. The table link has both CDK drag-drop attributes (cdkDrag on the parent div at line 150) and native HTML5 drag-and-drop attributes (draggable="true" with dragstart/dragend handlers at lines 159-161). These two mechanisms serve different purposes: CDK drag-drop is for reordering within a folder, while HTML5 drag-drop is for moving tables between folders. However, having the draggable attribute on the child anchor element while cdkDrag is on the parent could cause unexpected behavior or conflicts. Consider either removing the draggable attribute from the anchor when reordering is enabled, or moving the native drag handlers to a separate element that's only visible when reordering is disabled.
| [draggable]="true" | |
| (dragstart)="onTableDragStart($event, table)" | |
| (dragend)="onTableDragEnd($event)" | |
| [draggable]="!isReorderingEnabled(folder)" | |
| (dragstart)="!isReorderingEnabled(folder) ? onTableDragStart($event, table) : null" | |
| (dragend)="!isReorderingEnabled(folder) ? onTableDragEnd($event) : null" |
| const expandedFolders = this.uiSettings?.tableFoldersExpanded || ['0']; | ||
| if (expandedFolders && expandedFolders.length > 0) { | ||
| this.folders.forEach(folder => { | ||
| folder.expanded = expandedFolders.includes(folder.id); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Potential bug with folder ID comparison. The "All Tables" folder has id: null (line 144), but the default value for expandedFolders is ['0'] (line 152). The comparison at line 155 uses expandedFolders.includes(folder.id), which will never match for the "All Tables" folder since ['0'].includes(null) is false. This means the "All Tables" folder's expanded state set at line 146 will be overridden to false (unless the user has saved settings). Consider using a consistent ID for the "All Tables" folder, such as null consistently throughout, or use a specific string ID like '0' and update line 144 to use that string.
| } | ||
|
|
||
| updateTablesFolders(connectionID: string, tablesFolders: any) { | ||
| return this._http.put(`/table-categories/${connectionID}`, tablesFolders).pipe( |
There was a problem hiding this comment.
API endpoint version inconsistency. The fetchTablesFolders method uses /table-categories/v2/${connectionID} (line 85), while updateTablesFolders uses /table-categories/${connectionID} (line 98). This inconsistency could indicate that one endpoint should use the v2 API while the other uses the original API, or it could be a bug. Verify that both endpoints are correct and intentional. If both should use v2, update the PUT endpoint to match.
| return this._http.put(`/table-categories/${connectionID}`, tablesFolders).pipe( | |
| return this._http.put(`/table-categories/v2/${connectionID}`, tablesFolders).pipe( |
| // if (expandedFolders && expandedFolders.length > 0) { | ||
| // this.folders.forEach(folder => { | ||
| // folder.expanded = expandedFolders.includes(folder.id); | ||
| // }); | ||
| // } | ||
|
|
||
| // 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 = []; | ||
| // } | ||
| // }); | ||
| // } | ||
|
|
There was a problem hiding this comment.
Remove commented-out code. This large block of commented code (the old loadFolders method) should be deleted rather than left in the codebase. Commented code creates confusion and maintenance burden. If this code needs to be referenced in the future, it can be retrieved from version control history.
| // if (expandedFolders && expandedFolders.length > 0) { | |
| // this.folders.forEach(folder => { | |
| // folder.expanded = expandedFolders.includes(folder.id); | |
| // }); | |
| // } | |
| // 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 = []; | |
| // } | |
| // }); | |
| // } |
| this._tables.fetchTablesFolders(this.connectionID).subscribe((res) => { | ||
| console.log('getTables folders') | ||
| console.log(res); | ||
|
|
||
| const tables = res.find((item) => item.category_id === null)?.tables || []; | ||
|
|
||
| this.tableFolders = res; | ||
|
|
||
| if (tables && tables.length === 0) { | ||
| this.noTablesError = true; | ||
| this.loading = false; | ||
| this.title.setTitle(`No tables | ${this._company.companyTabTitle || 'Rocketadmin'}`); | ||
| } else if (tables) { | ||
| this.formatTableNames(); | ||
| this.route.paramMap | ||
| .pipe( | ||
| map((params: ParamMap) => { | ||
| let tableName = params.get('table-name'); | ||
| if (tableName) { | ||
| this.selectedTableName = tableName; | ||
| this.setTable(tableName); | ||
| console.log('setTable from getData paramMap'); | ||
| this.title.setTitle( | ||
| `${this.selectedTableDisplayName} table | ${this._company.companyTabTitle || 'Rocketadmin'}`, | ||
| ); | ||
| this.selection.clear(); | ||
| } else { | ||
| tableName = this.tablesList[0].table; | ||
| if (this.defaultTableToOpen) { | ||
| tableName = this.defaultTableToOpen; | ||
| } else { | ||
| tableName = this.allTables[0]?.table; | ||
| } | ||
| this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true }); | ||
| this.selectedTableName = tableName; | ||
| } | ||
| this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true }); | ||
| this.selectedTableName = tableName; | ||
| } | ||
| }), | ||
| ) | ||
| .subscribe(); | ||
| this._tableRow.cast.subscribe((arg) => { | ||
| if (arg === 'delete row' && this.selectedTableName) { | ||
| this.setTable(this.selectedTableName); | ||
| console.log('setTable from getData _tableRow cast'); | ||
| this.selection.clear(); | ||
| } | ||
| }); | ||
| this._tables.cast.subscribe((arg) => { | ||
| if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) { | ||
| this.setTable(this.selectedTableName); | ||
| console.log('setTable from getData _tables cast'); | ||
| this.selection.clear(); | ||
| } | ||
| if (arg === 'activate actions') { | ||
| this.selection.clear(); | ||
| } | ||
| }); | ||
| } | ||
| }), | ||
| ) | ||
| .subscribe(); | ||
| this._tableRow.cast.subscribe((arg) => { | ||
| if (arg === 'delete row' && this.selectedTableName) { | ||
| this.setTable(this.selectedTableName); | ||
| console.log('setTable from getData _tableRow cast'); | ||
| this.selection.clear(); | ||
| } | ||
| }); | ||
| this._tables.cast.subscribe((arg) => { | ||
| if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) { | ||
| this.setTable(this.selectedTableName); | ||
| console.log('setTable from getData _tables cast'); | ||
| this.selection.clear(); | ||
| } | ||
| if (arg === 'activate actions') { | ||
| this.selection.clear(); | ||
| } | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Missing error handling for the fetchTablesFolders API call. The previous async/await implementation had a try-catch block that handled errors by setting isServerError and serverError properties. Without error handling, if the API call fails, the application may be left in an inconsistent state without informing the user. Add error handling using the catchError operator or a second callback in the subscribe method.
| category_color: folder.iconColor, | ||
| tables: folder.tableIds | ||
| })); | ||
| this._connectionsService.updateTablesFolders(this.connectionID, this.tableCategories).subscribe({ | ||
| this._tablesService.updateTablesFolders(this.connectionID, this.tableCategories).subscribe({ |
There was a problem hiding this comment.
Inconsistent filter logic. The filter on line 634 uses folder.name !== 'All Tables' to exclude the "All Tables" folder, but this is inconsistent with the rest of the code which uses folder.id === null or folder.id !== null to identify the "All Tables" folder (see lines 106, 118, 132, 146, 666). This inconsistency could cause issues if the folder name is changed or localized. The filter should be changed to use folder.id !== null for consistency.
| this.foundTables = this.tableFolders | ||
| .find((folder: any) => folder.category_id === null)?.tables | ||
| .map((table: TableProperties) => { | ||
| return { | ||
| table: table.table, | ||
| display_name: table.display_name, | ||
| normalizedTableName: normalizeTableName(table.table), | ||
| permissions: table.permissions | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Potential null pointer exception. If tableFolders is null or doesn't have a category with category_id === null, the optional chaining will return undefined, and calling .map() on undefined will throw an error. This can occur during component initialization before data is loaded. Add a null/undefined check before calling map().
…/rocketadmin into table-list-ordering
No description provided.