diff --git a/frontend/src/app/components/dashboard/dashboard.component.css b/frontend/src/app/components/dashboard/dashboard.component.css index 4db64c67b..bede3f5b8 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.css +++ b/frontend/src/app/components/dashboard/dashboard.component.css @@ -53,6 +53,22 @@ margin-left: 12px; } +.toggle-button-position button { + border-radius: 4px; +} + +.toggle-button-position button mat-icon { + border-radius: 4px; +} + +.toggle-button-position button:hover { + border-radius: 4px; +} + +.toggle-button-position button:hover mat-icon { + border-radius: 4px; +} + .dashboard { display: flex; flex-direction: column; diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index 87911c778..47ac5ff4b 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -69,9 +69,13 @@

Rocketadmin can not find any tables

+ [selectedTable]="selectedTableName" + [uiSettings]="uiSettings" + [accessLevel]="currentConnectionAccessLevel" + (expandSidebar)="toggleSideBar()"> diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index e4ed2e74d..efd97516d 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -124,6 +124,10 @@ export class DashboardComponent implements OnInit, OnDestroy { return this._connections.currentConnection.isTestConnection } + get connectionTitle() { + return this._connections.currentConnection?.title || 'Database'; + } + get defaultTableToOpen () { return this._connections.defaultTableToOpen; } diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.css b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.css new file mode 100644 index 000000000..cc617fcbd --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.css @@ -0,0 +1,124 @@ +.confirmation-message { + margin: 0 0 20px 0; + font-size: 16px; + line-height: 1.5; + color: #424242; +} + +.confirmation-message strong { + color: #212121; + font-weight: 600; +} + +.folder-info { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background-color: #e3f2fd; + border-radius: 4px; + margin-bottom: 20px; +} + +.info-icon { + color: #1976d2; + font-size: 20px; + width: 20px; + height: 20px; + margin-top: 2px; +} + +.info-text { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: #424242; + flex: 1; +} + +.warning-note { + padding: 12px 16px; + background-color: #fff3e0; + border-radius: 4px; + border-left: 4px solid #ff9800; +} + +.warning-note p { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: #424242; +} + +.warning-note strong { + color: #e65100; + font-weight: 600; +} + +.cancel-button { + color: #666; +} + +.delete-button { + display: flex; + align-items: center; + gap: 8px; +} + +.delete-button mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .dialog-title { + border-bottom-color: #424242; + } + + .dialog-content { + background-color: #303030; + } + + .confirmation-message { + color: #e0e0e0; + } + + .confirmation-message strong { + color: #fff; + } + + .folder-info { + background-color: #1a3a52; + } + + .info-icon { + color: #64b5f6; + } + + .info-text { + color: #e0e0e0; + } + + .warning-note { + background-color: #4a3c28; + border-left-color: #ffb74d; + } + + .warning-note p { + color: #e0e0e0; + } + + .warning-note strong { + color: #ffb74d; + } + + .dialog-actions { + border-top-color: #424242; + } + + .cancel-button { + color: #aaa; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.html b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.html new file mode 100644 index 000000000..5d5704819 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.html @@ -0,0 +1,35 @@ +

+ Delete Folder +

+ + +

+ Are you sure you want to delete the folder "{{data.folderName}}"? +

+ +
+ info +

+ This folder contains {{data.tableCount}} table{{data.tableCount > 1 ? 's' : ''}}. + The tables will not be deleted, only removed from this folder. +

+
+ +
+

Note: This action cannot be undone.

+
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.ts b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.ts new file mode 100644 index 000000000..127251b8c --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-delete-dialog/db-folder-delete-dialog.component.ts @@ -0,0 +1,37 @@ +import { Component, Inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; + +export interface DbFolderDeleteDialogData { + folderName: string; + tableCount: number; +} + +@Component({ + selector: 'app-db-folder-delete-dialog', + templateUrl: './db-folder-delete-dialog.component.html', + styleUrls: ['./db-folder-delete-dialog.component.css'], + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatButtonModule, + MatIconModule + ] +}) +export class DbFolderDeleteDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DbFolderDeleteDialogData + ) {} + + onCancel(): void { + this.dialogRef.close(false); + } + + onDelete(): void { + this.dialogRef.close(true); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.css b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.css new file mode 100644 index 000000000..ff3c148d5 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.css @@ -0,0 +1,208 @@ +.dialog-body { + min-width: 400px; + max-height: 500px; + overflow-y: auto; +} + +.folder-name-section { + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #e0e0e0; +} + +.folder-name-section h4 { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.folder-name-field { + width: 100%; +} + +.folder-color-section { + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #e0e0e0; +} + +.folder-color-section h4 { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.color-radio-group { + width: 100%; +} + +.color-row { + display: flex; + gap: 12px; + flex-wrap: wrap; + padding: 8px 0; +} + +.color-radio-label { + display: inline-block; + position: relative; + cursor: pointer; +} + +.color-radio-input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.color-swatch { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.color-radio-label:hover .color-swatch { + transform: scale(1.1); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2); +} + +.color-radio-label.selected .color-swatch { + /* border: 2px solid currentColor; */ + box-shadow: 0 0 0 3px rgba(33, 33, 33, 0.1), 0 3px 8px rgba(0, 0, 0, 0.2); + transform: scale(1.05); +} + +.color-radio-label.selected .color-swatch::after { + content: '✓'; + position: absolute; + /* font-size: 20px; */ + color: white; + /* text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); */ + /* font-weight: bold; */ +} + +.tables-section h4 { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.table-list { + margin-top: 8px; +} + +.table-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 8px; + border-bottom: 1px solid #e0e0e0; + transition: background-color 0.2s ease; +} + +.table-item:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +.table-item:last-child { + border-bottom: none; +} + +.table-name { + flex: 1; + font-size: 14px; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 12px; +} + +.action-button { + min-width: 70px; + height: 32px; + font-size: 12px; + font-weight: 500; + border-radius: 4px; + transition: all 0.2s ease; +} + +.add-button { + color: #4caf50; + background-color: transparent; +} + +.add-button:hover { + background-color: rgba(76, 175, 80, 0.1); +} + +.remove-button { + color: #f44336; + background-color: transparent; +} + +.remove-button:hover { + background-color: rgba(244, 67, 54, 0.1); +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .folder-name-section { + border-bottom-color: #424242; + } + + .folder-name-section h4 { + color: #999; + } + + .folder-color-section { + border-bottom-color: #424242; + } + + .folder-color-section h4, + .tables-section h4 { + color: #999; + } + + .color-swatch { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + .color-radio-label:hover .color-swatch { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4); + } + + .color-radio-label.selected .color-swatch { + border-color: currentColor; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1), 0 3px 8px rgba(0, 0, 0, 0.4); + } + + .color-radio-label.selected .color-swatch::before { + border-color: currentColor; + opacity: 0.2; + } + + .table-item { + border-bottom-color: #424242; + } + + .table-item:hover { + background-color: rgba(255, 255, 255, 0.04); + } + + .table-name { + color: #ccc; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.html b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.html new file mode 100644 index 000000000..bf926b7d7 --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.html @@ -0,0 +1,68 @@ +

+ Edit Folder +

+ + + +
+

Folder Name

+ + + The "All Tables" folder cannot be renamed + + Folder name is required + + +
+ + +
+

Folder Icon Color

+
+
+ +
+
+
+ +
+

Manage tables in this folder

+
+
+ {{getTableName(table)}} + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.ts b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.ts new file mode 100644 index 000000000..c1bc8370c --- /dev/null +++ b/frontend/src/app/components/dashboard/db-tables-list/db-folder-edit-dialog/db-folder-edit-dialog.component.ts @@ -0,0 +1,116 @@ +import { Component, Inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { FormsModule } from '@angular/forms'; +import { TableProperties } from 'src/app/models/table'; + +export interface Folder { + id: string; + name: string; + expanded: boolean; + tableIds: string[]; + iconColor?: string; + isEmpty?: boolean; +} + +export interface DbFolderEditDialogData { + folder: Folder; + tables: TableProperties[]; + folderIconColors: { name: string; value: string }[]; +} + +@Component({ + selector: 'app-db-folder-edit-dialog', + templateUrl: './db-folder-edit-dialog.component.html', + styleUrls: ['./db-folder-edit-dialog.component.css'], + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatButtonModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + FormsModule + ] +}) +export class DbFolderEditDialogComponent { + public folder: Folder; + public tables: TableProperties[]; + public folderIconColors: { name: string; value: string }[]; + private originalFolderName: string; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DbFolderEditDialogData + ) { + // Create a copy of the folder to work with + this.folder = { ...data.folder, tableIds: [...data.folder.tableIds] }; + // Store original name for validation + this.originalFolderName = data.folder.name; + // Set default color if not set + if (!this.folder.iconColor) { + this.folder.iconColor = '#757575'; + } + this.tables = data.tables; + this.folderIconColors = data.folderIconColors; + } + + getFolderIconColor(folder: Folder): string { + return folder.iconColor || '#757575'; + } + + onColorChange(color: string) { + console.log('Folder color changed to:', color); + } + + isTableInFolder(table: TableProperties): boolean { + return this.folder.tableIds.includes(table.table); + } + + getTableName(table: TableProperties): string { + return table.display_name || table.table; + } + + toggleTableInFolder(table: TableProperties) { + const tableId = table.table; + const isInFolder = this.folder.tableIds.includes(tableId); + + if (isInFolder) { + // Remove table from folder + this.folder.tableIds = this.folder.tableIds.filter(id => id !== tableId); + } else { + // Add table to folder + this.folder.tableIds.push(tableId); + // If this was an empty folder, mark it as no longer empty + if (this.folder.isEmpty) { + this.folder.isEmpty = false; + } + } + + console.log('toggleTableInFolder'); + } + + isAllTablesFolder(): boolean { + return this.folder.id === '0' || this.folder.name === 'All Tables'; + } + + onSave() { + // Validate folder name + if (!this.folder.name || this.folder.name.trim() === '') { + return; // Don't save if name is empty + } + + // Trim the folder name before saving + this.folder.name = this.folder.name.trim(); + this.dialogRef.close(this.folder); + } + + onCancel() { + this.dialogRef.close(); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css index e12d3204d..d1fc2efe5 100644 --- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css +++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css @@ -1,24 +1,416 @@ -.sidenav-header { +:host { + width: 240px; + max-width: 240px; + min-width: 240px; + display: block; + overflow-x: hidden; + overflow-y: auto; + position: relative; +} + +:host-context(.side-bar_collapsed) { + width: 65px; + max-width: 65px; + min-width: 65px; + overflow-x: hidden; + overflow-y: auto; +} + +/* Prevent horizontal scroll in sidebar */ +:host * { + max-width: 100%; + box-sizing: border-box; +} + + +.collapsed-content { display: flex; + flex-direction: column; align-items: center; - justify-content: space-between; - padding: 20px 16px 4px; + height: 100%; + min-height: calc(100vh - 120px); /* Adjust based on header height */ } -.connection-title { - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - line-height: 22px; - margin: 0 !important; - overflow: hidden; - padding-right: 36px; - padding-bottom: 4px; - text-overflow: ellipsis; - word-wrap: anywhere; +.collapsed-top-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; + justify-content: flex-start; + padding-top: 8px; } +.collapsed-bottom-section { + display: flex; + flex-direction: column; + align-items: center; + margin-top: auto; + padding-bottom: 8px; +} + +.collapsed-folders { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +:host-context(.side-bar_collapsed) .collapsed-folders { + width: 100%; + justify-content: center; + align-items: center; +} + +.collapsed-folder-item { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; /* Restored to 32px for square shape */ + border-radius: 4px; + background-color: transparent; + color: var(--color-primaryPalette-50-contrast); + cursor: pointer; + transition: background-color 0.2s ease; +} + +.collapsed-folder-item:hover { + background-color: var(--color-primaryPalette-100); +} + +.collapsed-folder-item_active { + background-color: rgba(33, 33, 33, 0.1); + color: #212121; +} + +.collapsed-folder-item_active:hover { + background-color: rgba(33, 33, 33, 0.15); +} + +.collapsed-folder-icon { + font-size: 20px; + height: 20px; + width: 20px; +} + +.collapsed-folder-icon.fallback { + font-size: 20px; + height: 20px; + width: 20px; +} + +/* Collapsed action buttons */ +.collapsed-search-button, +.collapsed-add-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + margin-bottom: 4px; +} + +.collapsed-search-button { + background-color: transparent; + color: var(--color-primaryPalette-500); +} + +.collapsed-add-button { + background-color: var(--color-primaryPalette-50); + color: var(--color-primaryPalette-50-contrast); +} + +.collapsed-search-button:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +.collapsed-add-button:hover { + background-color: var(--color-primaryPalette-100); +} + +.collapsed-action-icon { + font-size: 18px; + height: 18px; + width: 18px; +} + + +/* Collapsed table list styles */ +.collapsed-tables-list { + position: relative; + background: white; + border: 1px solid #e0e0e0; + border-top: none; + overflow: visible; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px; + margin-top: 8px; + border-radius: 4px; + width: 48px; + min-width: 48px; +} + + +:host-context(.side-bar_collapsed) .collapsed-tables-list { + margin-left: auto; + margin-right: auto; +} + +.collapsed-table-item { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; /* Restored to 32px for square shape */ + text-decoration: none; + color: inherit; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 6px; + flex-shrink: 0; + position: relative; +} + +.collapsed-table-item:hover { + transform: translateY(-1px); +} + +.collapsed-table-item:hover .collapsed-table-icon { + transform: scale(1.02); +} + +.collapsed-table-item:hover .collapsed-table-initials { + background-color: var(--color-primaryPalette-100); + transform: scale(1.02); +} + +.collapsed-table-item_active { + background-color: rgba(33, 33, 33, 0.1); + color: #212121; +} + +.collapsed-table-item_active .collapsed-table-initials { + background-color: #212121; + color: white; + transform: scale(1.05); +} + +.collapsed-table-item_active:hover .collapsed-table-initials { + background-color: #000000; +} + +/* Dark theme adaptations */ +@media (prefers-color-scheme: dark) { + .collapsed-folder-item { + background-color: transparent; + color: #ffffff; + } + + .collapsed-folder-item:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + + .collapsed-folder-item_active { + background-color: #212121 !important; + color: #ffffff !important; + } + + /* Более специфичный селектор для гарантии */ + .collapsed-folder-item.collapsed-folder-item_active { + background-color: #212121 !important; + } + + .collapsed-folder-item_active .collapsed-folder-icon { + color: #ffffff !important; + } + + .collapsed-folder-item_active:hover, + .collapsed-folder-item_active:focus, + .collapsed-folder-item_active:active { + background-color: #000000 !important; + } + + /* Гарантируем фон для активной папки во всех состояниях */ + .collapsed-folder-item_active:not(:hover):not(:focus):not(:active) { + background-color: #212121 !important; + } + + .collapsed-table-item { + color: #e0e0e0; + } + + .collapsed-table-item:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + .collapsed-table-item_active { + background-color: #212121; + color: #ffffff; + } + + .collapsed-table-item_active:hover { + background-color: #000000; + } + + .collapsed-table-icon-container { + background-color: #424242; + } + + .collapsed-table-item:hover .collapsed-table-icon-container { + background-color: rgba(255, 255, 255, 0.1); + } + + .collapsed-table-item_active .collapsed-table-icon-container { + background-color: #ffffff; + color: #000000; + } + + .collapsed-table-item_active:hover .collapsed-table-icon-container { + background-color: #f5f5f5; + } + + .collapsed-table-icon-container .collapsed-table-icon { + color: #e0e0e0; + } + + .collapsed-table-item_active .collapsed-table-icon-container .collapsed-table-icon { + color: #000000; + } + + .collapsed-table-initials { + background-color: #424242; + color: #e0e0e0; + } + + .collapsed-table-item:hover .collapsed-table-initials { + background-color: rgba(255, 255, 255, 0.1); + } + + .collapsed-table-item_active .collapsed-table-initials { + background-color: #ffffff; + color: #000000; + } + + .collapsed-table-item_active:hover .collapsed-table-initials { + background-color: #f5f5f5; + } + + .collapsed-search-button { + color: #ffffff !important; + } + + .collapsed-search-button:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .collapsed-add-button { + background-color: #424242; + color: #ffffff; + } + + .collapsed-add-button:hover { + background-color: #555555; + } +} + +.collapsed-table-item_active:hover { + background-color: rgba(33, 33, 33, 0.25); +} + +.collapsed-table-icon-container { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background-color: var(--color-primaryPalette-50); + border-radius: 6px; + transition: all 0.2s ease; +} + +.collapsed-table-item:hover .collapsed-table-icon-container { + background-color: var(--color-primaryPalette-100); + transform: scale(1.02); +} + +.collapsed-table-item_active .collapsed-table-icon-container { + background-color: #212121; + color: white; + transform: scale(1.05); +} + +.collapsed-table-item_active:hover .collapsed-table-icon-container { + background-color: #000000; +} + +.collapsed-table-icon-container .collapsed-table-icon { + font-size: 16px; + color: var(--color-primaryPalette-50-contrast); +} + +.collapsed-table-item_active .collapsed-table-icon-container .collapsed-table-icon { + color: white; +} + + +.collapsed-table-initials { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--color-primaryPalette-50); + color: var(--color-primaryPalette-50-contrast); + border-radius: 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + transition: all 0.2s ease; +} + +.collapsed-table-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: inherit; +} + +.collapsed-table-initials { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + background-color: var(--color-primaryPalette-50); + color: var(--color-primaryPalette-50-contrast); + transition: all 0.2s ease; +} + +.collapsed-table-text { + font-size: 11px; + font-weight: 600; + text-align: center; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.collapsed-table-icon { + font-size: 16px; + width: 16px; + height: 16px; +} + + .mat-icon-button { margin-right: -8px; } @@ -27,19 +419,39 @@ line-height: 48px; } +.tables-list { + width: 100%; + box-sizing: border-box; + min-height: 0; +} + .tables-list ::ng-deep .table-list-item .mat-list-item-content { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.table-list-item { + cursor: pointer; + transition: opacity 0.2s ease; +} + +.table-list-item[draggable="true"]:hover { + opacity: 0.8; +} + +.table-list-item[draggable="true"]:active { + opacity: 0.6; +} + + .table-list-initials { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; - border-radius: 50%; + border-radius: 4px; background-color: var(--color-primaryPalette-50); color: var(--color-primaryPalette-50-contrast); } @@ -49,6 +461,19 @@ background-color: var(--color-whitePalette-900); color: var(--color-whitePalette-900-contrast); } + + .folder-header.expanded { + background-color: #212121; + color: #ffffff; + } + + .folder-header.expanded .folder-icon { + color: #ffffff; + } + + .folder-header.expanded .folder-expand-icon { + color: #ffffff; + } } .table-list-initials:hover { @@ -89,8 +514,10 @@ background-color: transparent; margin-top: 4px; margin-left: 16px; - margin-bottom: -24px; + /* margin-bottom: -24px; */ width: calc(100% - 32px); + max-width: calc(240px - 32px); + box-sizing: border-box; } .search-input ::ng-deep * { @@ -109,3 +536,604 @@ padding-top: 8px; padding-bottom: 8px; } + +.add-folder-button-container { + margin-left: 16px; + margin-right: 16px; + margin-top: -12px; + margin-bottom: 12px; + width: calc(100% - 32px); + box-sizing: border-box; +} + +.add-folder-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 500; + text-transform: none; + letter-spacing: 0.5px; + border-color: #e0e0e0 !important; + color: #616161 !important; +} + +.add-folder-button:hover { + border-color: #757575 !important; + background-color: rgba(0, 0, 0, 0.04) !important; +} + +.add-folder-button mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +.folders-section { + margin: 0 16px; + margin-bottom: 8px; + width: calc(100% - 32px); + box-sizing: border-box; +} + +.folder-item { + margin-bottom: 4px; + border: none; + border-radius: 4px; + background: transparent; + width: 100%; + box-sizing: border-box; + position: relative; + transition: background-color 0.2s ease; +} + +.folder-item.drag-over { + background-color: rgba(33, 150, 243, 0.1); + border: 2px dashed #2196f3; +} + +.folder-item.expanded::after { + content: ''; + position: absolute; + bottom: 0; + left: 12px; + right: 12px; + height: 1px; + background-color: #e0e0e0; +} + +.folder-header { + display: flex; + align-items: center; + padding: 8px 4px 8px 0; + cursor: pointer; + user-select: none; + height: 40px; +} + +.folder-header:hover { + background: #f5f5f5; +} + +.folder-header.expanded { + background-color: var(--color-primaryPalette-50); + color: var(--color-primaryPalette-50-contrast); + border-radius: 4px; +} + +.folder-header.expanded .folder-icon { + color: var(--color-primaryPalette-50-contrast); +} + +.folder-header.expanded .folder-expand-icon { + color: var(--color-primaryPalette-50-contrast); +} + +.folder-actions { + display: flex; + align-items: center; + opacity: 0; + transition: opacity 0.2s ease; +} + +.folder-header:hover .folder-actions { + opacity: 1; +} + + +.more-folder-button { + width: 16px; + height: 16px; + line-height: 16px; + border: none; + border-radius: 0; + background-color: transparent !important; + color: #bdbdbd !important; + margin-left: 4px; +} + +.more-folder-button:hover { + border: none !important; + background-color: rgba(0, 0, 0, 0.02) !important; + color: #9e9e9e !important; +} + +.more-folder-button mat-icon { + font-size: 12px; + width: 12px; + height: 12px; +} + +.folder-name { + flex: 1; + font-weight: 500; + color: #424242; +} + +.folder-name-edit-container { + display: flex; + align-items: center; + flex: 1; + max-width: calc(100% - 40px); +} + +.folder-name.editing { + background: white; + border: 1px solid #2196f3; + border-radius: 2px; + padding: 2px 6px; + margin: 0 4px; + flex: 1; + max-width: 100%; +} + +.enter-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #9e9e9e; + cursor: pointer; + margin-left: 4px; +} + +.enter-icon:hover { + color: #757575; +} + +.folder-icon { + font-size: 20px; + width: 20px; + height: 20px; + color: #666; + margin-right: 8px; /* Уменьшаем отступ до названия папки до 8px */ + margin-left: 8px; /* Сдвигаем иконку вправо на 8px */ + flex-shrink: 0; +} + +.folder-expand-icon { + transition: transform 0.2s ease; + font-size: 20px; + width: 20px; + height: 20px; + color: #666; + margin-left: auto; +} + +.folder-expand-icon.expanded { + transform: rotate(90deg); +} + +.folder-tables { + padding: 0 12px 8px; + border-top: none; + width: 100%; + max-width: 240px; + box-sizing: border-box; + min-height: 40px; +} + +.folder-tables-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + margin-bottom: 4px; +} + +.tables-count { + font-size: 12px; + color: #666; + font-weight: 500; +} + +.add-table-button { + width: 24px; + height: 24px; + line-height: 24px; + border: none; + border-radius: 0; + background-color: transparent !important; + color: #9e9e9e !important; +} + +.add-table-button:hover { + border: none !important; + background-color: rgba(0, 0, 0, 0.02) !important; + color: #757575 !important; +} + +.add-table-button mat-icon { + font-size: 16px; + width: 16px; + height: 16px; +} + +.selected-tables-list { + max-height: none; + overflow-y: visible; +} + +.selected-table-item { + display: flex; + align-items: center; + padding: 2px 0 2px 8px; + margin-bottom: 0; + background: transparent; + border-radius: 0; + font-size: 14px; + width: 100%; + box-sizing: border-box; + overflow: hidden; +} + + +.table-link { + flex: 1; + display: block; + text-decoration: none; + color: inherit; + overflow: hidden; + min-width: 0; +} + +.table-link:hover { + background-color: rgba(0, 0, 0, 0.04); + border-radius: 0; + margin: 0 -12px; + padding: 0 12px; +} + +.table-link_active .table-name { + color: var(--color-accentedPalette-500); +} + +.table-name { + flex: 1; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + display: block; + padding: 4px 0 4px 0; +} + +.remove-table-button { + width: 20px; + height: 20px; + line-height: 20px; + opacity: 0.4; + flex-shrink: 0; + border: none; + border-radius: 0; + background-color: transparent !important; + color: #9e9e9e !important; + margin-left: auto; + margin-right: 0; +} + +.remove-table-button:hover { + opacity: 0.7; + border: none !important; + background-color: transparent !important; + color: #757575 !important; +} + +.remove-table-button mat-icon { + font-size: 14px; + width: 14px; + height: 14px; +} + +.no-tables-message { + text-align: center; + padding: 16px 8px; + color: #666; + height: 200px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.no-tables-message p { + margin: 0 0 12px 0; + font-size: 14px; +} + +.no-tables-message button { + font-size: 12px; + border-color: #e0e0e0 !important; + color: #9e9e9e !important; + background-color: transparent !important; +} + +.no-tables-message button:hover { + border-color: #bdbdbd !important; + background-color: rgba(0, 0, 0, 0.02) !important; + color: #757575 !important; +} + +/* Styles for empty folders */ +.no-tables-message.empty-folder-message { + padding: 12px 20px; + height: auto; + min-height: auto; +} + + +.empty-folder-button { + border-style: dashed !important; + border-width: 2px !important; + border-color: #ccc !important; + background-color: transparent !important; + color: #666 !important; + font-size: 14px !important; + min-width: 200px !important; + padding: 20px 16px !important; +} + +.empty-folder-button:hover { + border-color: #2196F3 !important; + color: #2196F3 !important; + background-color: rgba(33, 150, 243, 0.04) !important; +} + +/* Folder menu alignment */ +::ng-deep .mat-menu-panel .mat-menu-item { + display: flex !important; + align-items: center !important; + gap: 12px !important; + min-height: 48px !important; +} + +::ng-deep .mat-menu-panel .mat-menu-item .material-icons-outlined { + font-size: 20px !important; + width: 20px !important; + height: 20px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + line-height: 1 !important; +} + +::ng-deep .mat-menu-panel .mat-menu-item span { + display: flex !important; + align-items: center !important; +} + +/* Add Table Dialog Styles */ +.edit-tables-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 1; + visibility: visible; + transition: opacity 0.2s ease, visibility 0.2s ease; + outline: none; +} + +.dialog-content { + background: white; + border-radius: 8px; + max-width: 400px; + width: 90%; + max-height: 500px; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + transform: scale(1); + transition: transform 0.2s ease; + position: relative; +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e0e0e0; +} + +.dialog-header h3 { + margin: 0; + font-size: 18px; + font-weight: 500; +} + +.dialog-header .close-button { + color: #666; + width: 32px; + height: 32px; + line-height: 32px; +} + +.dialog-header .close-button:hover { + color: #333; + background-color: rgba(0, 0, 0, 0.04); +} + +.dialog-header .close-button mat-icon { + font-size: 20px; + width: 20px; + height: 20px; +} + +.dialog-body { + padding: 16px 20px; + max-height: 300px; + overflow-y: auto; +} + +.available-table-item { + padding: 8px 0; + border-bottom: 1px solid #f0f0f0; +} + +.available-table-item:last-child { + border-bottom: none; +} + +/* Edit Tables Dialog specific styles */ +.edit-tables-dialog .dialog-content { + max-width: 450px; + width: 90%; +} + +.edit-tables-dialog .dialog-body { + max-height: 400px; + overflow-y: auto; +} + +.edit-tables-dialog .table-list { + margin-top: 16px; +} + +.edit-tables-dialog .table-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #e0e0e0; + transition: background-color 0.2s ease; + padding-left: 4px; + padding-right: 4px; +} + +.edit-tables-dialog .table-item:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +.edit-tables-dialog .table-item:last-child { + border-bottom: none; +} + +.edit-tables-dialog .table-name { + flex: 1; + font-size: 14px; + color: #333; +} + +.edit-tables-dialog .action-button { + min-width: 60px; + height: 32px; + font-size: 12px; + font-weight: 500; + border-radius: 4px; + border: none; +} + +.edit-tables-dialog .add-button { + color: #4caf50; + background-color: transparent; +} + +.edit-tables-dialog .add-button:hover { + background-color: rgba(76, 175, 80, 0.1); +} + +.edit-tables-dialog .remove-button { + color: #f44336; + background-color: transparent; +} + +.edit-tables-dialog .remove-button:hover { + background-color: rgba(244, 67, 54, 0.1); +} + +.edit-tables-dialog .folder-color-section { + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #e0e0e0; +} + +.edit-tables-dialog .folder-color-section h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.edit-tables-dialog .color-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.edit-tables-dialog .tables-section h4 { + margin: 0 0 2px 0; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.edit-tables-dialog .color-option { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid #e0e0e0; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.edit-tables-dialog .color-option:hover { + transform: scale(1.1); + border-color: #999; +} + +.edit-tables-dialog .color-option.selected { + border-color: #333; + border-width: 3px; + transform: scale(1.15); +} + +.edit-tables-dialog .color-option.selected::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-weight: bold; + font-size: 12px; + text-shadow: 0 0 2px rgba(0, 0, 0, 0.8); +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px 20px; + border-top: 1px solid #e0e0e0; +} + + diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html index 8069fd56c..eda0e5dc1 100644 --- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html +++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html @@ -1,7 +1,76 @@ +
+ + +
+ +
+ +
+ search +
+ + +
+
+ dehaze + folder +
+
+ + dehaze + + + +
+ + +
+ +
+ add +
+
+
+ Nothing found. - - - @@ -47,4 +182,5 @@ Rocketadmin do not provide adding and deleting database tables, only editing ones. You can add a table with SQL editor.

- + + 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 5d2c7d535..18ce44c89 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 @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Angulartics2Module } from 'angulartics2'; import { DbTablesListComponent } from './db-tables-list.component'; +import { provideHttpClient } from '@angular/common/http'; describe('DbTablesListComponent', () => { let component: DbTablesListComponent; @@ -8,7 +10,11 @@ describe('DbTablesListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DbTablesListComponent] + providers: [provideHttpClient()], + imports: [ + Angulartics2Module.forRoot(), + DbTablesListComponent + ] }).compileComponents(); }); 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 538c1e450..c076bbec7 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,16 +1,36 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +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'; +import { TableProperties, TableSettings } 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'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; +import { MatMenuModule } from '@angular/material/menu'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { TableProperties } from 'src/app/models/table'; +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'; + +export interface Folder { + id: string; + name: string; + expanded: boolean; + tableIds: string[]; + iconColor?: string; // Optional color for folder icon + isEmpty?: boolean; // Flag to indicate if folder is newly created and empty +} @Component({ selector: 'app-db-tables-list', @@ -19,40 +39,162 @@ import { TableStateService } from 'src/app/services/table-state.service'; imports: [ CommonModule, FormsModule, + MatButtonModule, + MatCheckboxModule, MatIconModule, MatFormFieldModule, MatInputModule, MatListModule, MatTooltipModule, + MatMenuModule, + MatDialogModule, RouterModule, ContentLoaderComponent ] }) -export class DbTablesListComponent { +export class DbTablesListComponent implements OnInit, OnChanges { @Input() connectionID: string; + @Input() connectionTitle: string; @Input() tables: TableProperties[]; @Input() selectedTable: string; @Input() collapsed: boolean; + @Input() uiSettings: any; + @Input() accessLevel: AccessLevel; + @Output() expandSidebar = new EventEmitter(); + + public tableCategories: TableCategory[] = []; public substringToSearch: string; public foundTables: TableProperties[]; + public folders: Folder[] = []; + + // Dialog state + public currentFolder: Folder | null = null; + + // Drag and drop state + public draggedTable: TableProperties | null = null; + public dragOverFolder: string | null = null; + + // Collapsed state + public showCollapsedTableList: boolean = false; + public currentCollapsedFolder: Folder | null = null; + + // State preservation + private preservedFolderStates: { [key: string]: boolean } = {}; + private preservedActiveFolder: string | null = null; + + // Table icons cache + private tableIcons: { [key: string]: string } = {}; + + // Folder icon colors + public folderIconColors = [ + { name: 'Default', value: '#212121' }, + { name: 'Blue', value: '#2196F3' }, + { name: 'Green', value: '#4CAF50' }, + { name: 'Orange', value: '#FF9800' }, + { name: 'Red', value: '#F44336' }, + { name: 'Purple', value: '#9C27B0' }, + { name: 'Teal', value: '#009688' }, + { name: 'Pink', value: '#E91E63' } + ]; + constructor( private _tableState: TableStateService, + private _tablesService: TablesService, + private _connectionsService: ConnectionsService, + private _uiSettingsService: UiSettingsService, + private dialog: MatDialog ) { } ngOnInit() { this.foundTables = this.tables; + this.loadFolders(); + console.log('ngOnInit - showCollapsedTableList initialized to:', this.showCollapsedTableList); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['collapsed']) { + if (changes['collapsed'].currentValue === true) { + // Sidebar is being collapsed - preserve current state + this.preserveFolderStates(); + } else if (changes['collapsed'].currentValue === false) { + // Sidebar is being expanded - restore preserved state + this.restoreFolderStates(); + } + } } searchSubstring() { - this.foundTables = this.tables - .filter(tableItem => tableItem.table.toLowerCase().includes(this.substringToSearch?.toLowerCase()) || (tableItem.display_name && tableItem.display_name.toLowerCase().includes(this.substringToSearch.toLowerCase()))); + if (!this.substringToSearch || this.substringToSearch.trim() === '') { + this.foundTables = this.tables; + // Collapse all folders when search is cleared + this.folders.forEach(folder => { + folder.expanded = false; + }); + 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 => + tableItem.table.toLowerCase().includes(searchTerm) || + (tableItem.display_name && tableItem.display_name.toLowerCase().includes(searchTerm)) || + (tableItem.normalizedTableName && 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); + const hasMatchingTables = folderTables.some(table => + this.foundTables.some(foundTable => foundTable.table === table.table) + ); + + if (hasMatchingTables) { + folder.expanded = true; + } else { + folder.expanded = false; + } + }); } getTableName(table: TableProperties) { 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; } @@ -61,4 +203,456 @@ export class DbTablesListComponent { this._tableState.clearSelection(); this._tableState.closeAIpanel(); } + + onAddFolder() { + const newFolder: Folder = { + id: this.generateFolderId(), + name: `Folder ${this.folders.length}`, + expanded: true, // Разворачиваем папку сразу после создания + tableIds: [], + isEmpty: true // Mark as empty for special styling + }; + this.folders.push(newFolder); + console.log('onAddFolder'); + this.saveFolders(); + } + + toggleFolder(folderId: string) { + const folder = this.folders.find (f => f.id === folderId); + if (folder) { + folder.expanded = !folder.expanded; + const expandedFolders = this.folders.filter(f => f.expanded).map(f => f.id); + this._uiSettingsService.updateConnectionSetting(this.connectionID, 'tableFoldersExpanded', expandedFolders); + } + } + + onCollapsedFolderClick(folder: Folder) { + console.log('Clicked on folder:', folder.name); + + // If clicking on the same folder that's already open, close it + if (this.currentCollapsedFolder?.id === folder.id) { + this.showCollapsedTableList = false; + this.currentCollapsedFolder = null; + } else { + // If clicking on a different folder, open it immediately + 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[] { + if (!this.currentCollapsedFolder) { + console.log('No current collapsed folder'); + return []; + } + + const tables = this.getFolderTables(this.currentCollapsedFolder); + console.log('getCollapsedTableList - tables:', tables); + return tables; + } + + navigateToTable(table: TableProperties) { + // This method is called when clicking on a table in collapsed mode + // The actual navigation is handled by routerLink in the template + // We just need to close the sidebar after navigation + this.closeSidebar(); + } + + toggleCollapsedSearch() { + // Open the sidebar and activate search + this.expandSidebar.emit(); + // Focus on search input after sidebar expands + setTimeout(() => { + const input = document.querySelector('.search-input input') as HTMLInputElement; + if (input) { + input.focus(); + input.select(); + } + }, 300); // Wait for sidebar animation to complete + } + + deleteFolder(folder: Folder) { + const dialogData: DbFolderDeleteDialogData = { + folderName: folder.name, + tableCount: folder.tableIds.length + }; + + const dialogRef = this.dialog.open(DbFolderDeleteDialogComponent, { + width: '24em', + data: dialogData, + panelClass: 'db-folder-delete-dialog' + }); + + dialogRef.afterClosed().subscribe((confirmed: boolean) => { + if (confirmed) { + const index = this.folders.findIndex(f => f.id === folder.id); + if (index > -1) { + this.folders.splice(index, 1); + this.saveFolders(); + } + } + }); + } + + + onMenuClosed() { + // Menu closed, no action needed + } + + closeMenu(menu: any) { + if (menu && menu.close) { + menu.close(); + } + } + + + + + getFolderTables(folder: Folder): TableProperties[] { + const folderTables = this.tables.filter(table => folder.tableIds.includes(table.table)); + + // If there's a search term, filter the collection tables too + if (this.substringToSearch && this.substringToSearch.trim() !== '') { + const searchTerm = this.substringToSearch.toLowerCase(); + return folderTables.filter(table => + table.table.toLowerCase().includes(searchTerm) || + (table.display_name && table.display_name.toLowerCase().includes(searchTerm)) || + (table.normalizedTableName && table.normalizedTableName.toLowerCase().includes(searchTerm)) + ); + } + + return folderTables; + } + + getAvailableTables(folder: Folder | null): TableProperties[] { + if (!folder) return []; + return this.tables.filter(table => !folder.tableIds.includes(table.table)); + } + + isTableInAnyFolder(table: TableProperties): boolean { + return this.folders.some(folder => folder.tableIds.includes(table.table)); + } + + shouldShowFolder(folder: Folder): boolean { + // If no search term, show all folders + if (!this.substringToSearch || this.substringToSearch.trim() === '') { + return true; + } + + // Show collection if it has tables matching the search + const matchingTables = this.getFolderTables(folder); + return matchingTables.length > 0; + } + + showEditTablesDialog(folder: Folder) { + // Expand the folder if it's collapsed + folder.expanded = true; + + const dialogData: DbFolderEditDialogData = { + folder: folder, + tables: this.tables, + folderIconColors: this.folderIconColors + }; + + const dialogRef = this.dialog.open(DbFolderEditDialogComponent, { + width: '32em', + data: dialogData, + panelClass: 'db-folder-edit-dialog' + }); + + dialogRef.afterClosed().subscribe((result: Folder | undefined) => { + if (result) { + // Update the folder with the result from dialog + const index = this.folders.findIndex(f => f.id === result.id); + if (index !== -1) { + this.folders[index] = result; + this.saveFolders(); + } + } + }); + } + + + getFolderIconColor(folder: Folder, isActive?: boolean): string { + if (isActive) { + return '#212121'; // Black for active folders + } + + // Check if we're in dark theme + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + // In dark theme, use #212121 for folders without custom color + return folder.iconColor || '#212121'; + } + + // In light theme, use default color + return folder.iconColor || '#212121'; + } + + + get allTables(): TableProperties[] { + return this.tables; + } + + isTableInFolder(table: TableProperties): boolean { + return this.currentFolder ? this.currentFolder.tableIds.includes(table.table) : false; + } + + isTableInCurrentDraggedFolder(table: TableProperties, folder: Folder): boolean { + return this.draggedTable && + this.draggedTable.table === table.table && + folder.tableIds.includes(table.table); + } + + toggleTableInFolder(table: TableProperties) { + if (!this.currentFolder) return; + + const tableId = table.table; + const isInFolder = this.currentFolder.tableIds.includes(tableId); + + if (isInFolder) { + // Remove from folder + this.currentFolder.tableIds = this.currentFolder.tableIds.filter(id => id !== tableId); + } else { + // Add to folder + this.currentFolder.tableIds.push(tableId); + // Remove empty flag when adding tables + if (this.currentFolder.isEmpty) { + this.currentFolder.isEmpty = false; + } + } + + console.log('toggleTableInFolder'); + this.saveFolders(); + } + + + removeTableFromFolder(folder: Folder, table: TableProperties, event: Event) { + event.stopPropagation(); + folder.tableIds = folder.tableIds.filter(id => id !== table.table); + console.log('removeTableFromFolder'); + this.saveFolders(); + } + + trackByFolderId(index: number, folder: Folder): string { + return folder.id; + } + + private preserveFolderStates() { + // Save expanded states of all folders + this.folders.forEach(folder => { + this.preservedFolderStates[folder.id] = folder.expanded; + }); + + // Check if there are only "All Tables" folder (no custom folders) + const hasCustomFolders = this.folders.some(folder => folder.name !== 'All Tables'); + + // If no custom folders exist, ensure "All Tables" is always expanded + if (!hasCustomFolders) { + const allTablesFolder = this.folders.find(folder => folder.name === 'All Tables'); + if (allTablesFolder) { + this.preservedFolderStates[allTablesFolder.id] = true; + this.preservedActiveFolder = allTablesFolder.id; + } + } else { + // Find and save the currently active folder (the one that contains selected table) + 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() { + // Check if there are only "All Tables" folder (no custom folders) + const hasCustomFolders = this.folders.some(folder => folder.name !== 'All Tables'); + + // If no custom folders exist, always expand "All Tables" + if (!hasCustomFolders) { + const allTablesFolder = this.folders.find(folder => folder.name === 'All Tables'); + if (allTablesFolder) { + 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; + } + } + + // Restore expanded states of all folders + this.folders.forEach(folder => { + if (this.preservedFolderStates.hasOwnProperty(folder.id)) { + folder.expanded = this.preservedFolderStates[folder.id]; + } + }); + + // In collapsed view, show the table list if any folder was expanded + const hasExpandedFolders = Object.values(this.preservedFolderStates).some(expanded => expanded); + if (hasExpandedFolders) { + // If there was an active folder, use it; otherwise use the first expanded folder + let targetFolder = null; + + if (this.preservedActiveFolder) { + targetFolder = this.folders.find(f => f.id === this.preservedActiveFolder); + } + + if (!targetFolder) { + // Find the first expanded folder + targetFolder = this.folders.find(f => this.preservedFolderStates[f.id]); + } + + if (targetFolder) { + this.currentCollapsedFolder = targetFolder; + 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 { + if (!this.selectedTable) return null; + + return this.folders.find(folder => + folder.tableIds.includes(this.selectedTable) + ) || null; + } + + private generateFolderId(): string { + 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; + 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 = []; + } + }); + } + + 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({ + next: () => { + console.log('Connection settings updated with folders.'); + }, + error: (error) => { + console.error('Error updating connection settings with folders:', error); + } + }); + } catch (e) { + console.error('Error saving folders:', e); + } + } + + // Drag and drop methods + onTableDragStart(event: DragEvent, table: TableProperties) { + this.draggedTable = table; + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', table.table); + } + } + + onTableDragEnd(event: DragEvent) { + this.draggedTable = null; + this.dragOverFolder = null; + } + + onFolderDragOver(event: DragEvent, folderId: string) { + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + this.dragOverFolder = folderId; + } + + onFolderDragLeave(event: DragEvent, folderId: string) { + // Only clear if we're actually leaving the collection (not moving to a child element) + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const x = event.clientX; + const y = event.clientY; + + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { + this.dragOverFolder = null; + } + } + + onFolderDrop(event: DragEvent, folder: Folder) { + event.preventDefault(); + + if (this.draggedTable) { + // Check if table is already in this specific folder + if (folder.tableIds.includes(this.draggedTable.table)) { + // 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); + // Remove empty flag when adding tables via drag and drop + if (folder.isEmpty) { + folder.isEmpty = false; + } + console.log('onFolderDrop'); + this.saveFolders(); + } + } + + this.draggedTable = null; + this.dragOverFolder = null; + } } diff --git a/frontend/src/app/models/connection.ts b/frontend/src/app/models/connection.ts index 4a022b1aa..dba124abb 100644 --- a/frontend/src/app/models/connection.ts +++ b/frontend/src/app/models/connection.ts @@ -55,12 +55,20 @@ export interface TestConnection { message: string } +export interface TableCategory { + category_id: string, + category_name: string, + tables: string[], + category_color?: string +} + export interface ConnectionSettings { hidden_tables?: string[], - default_showing_table: string, + default_showing_table?: string, primary_color?: string, secondary_color?: string, logo_url?: string, company_name?: string, tables_audit?: boolean, + table_categories?: TableCategory[] } \ No newline at end of file diff --git a/frontend/src/app/models/ui-settings.ts b/frontend/src/app/models/ui-settings.ts index 5e4ff662a..90c103377 100644 --- a/frontend/src/app/models/ui-settings.ts +++ b/frontend/src/app/models/ui-settings.ts @@ -10,6 +10,7 @@ export interface TableSettingsUI { export interface ConnectionSettingsUI { shownTableTitles: boolean; tables: { [tableName: string]: TableSettingsUI }; + tableFoldersExpanded?: string[]; } export interface UiSettings { diff --git a/frontend/src/app/services/connections.service.ts b/frontend/src/app/services/connections.service.ts index c751fab84..a237afa0d 100644 --- a/frontend/src/app/services/connections.service.ts +++ b/frontend/src/app/services/connections.service.ts @@ -504,4 +504,35 @@ 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; + }) + ); + } }