Spaces:
Paused
Paused
| import { Component, OnInit, OnDestroy } from '@angular/core'; | |
| import { CommonModule } from '@angular/common'; | |
| import { FormsModule } from '@angular/forms'; | |
| import { MatDialog, MatDialogModule } from '@angular/material/dialog'; | |
| import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; | |
| import { MatTableModule } from '@angular/material/table'; | |
| import { MatProgressBarModule } from '@angular/material/progress-bar'; | |
| import { MatButtonModule } from '@angular/material/button'; | |
| import { MatCheckboxModule } from '@angular/material/checkbox'; | |
| import { MatFormFieldModule } from '@angular/material/form-field'; | |
| import { MatInputModule } from '@angular/material/input'; | |
| import { MatButtonToggleModule } from '@angular/material/button-toggle'; | |
| import { MatCardModule } from '@angular/material/card'; | |
| import { MatChipsModule } from '@angular/material/chips'; | |
| import { MatIconModule } from '@angular/material/icon'; | |
| import { MatMenuModule } from '@angular/material/menu'; | |
| import { MatDividerModule } from '@angular/material/divider'; | |
| import { ApiService, Project } from '../../services/api.service'; | |
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | |
| import { authInterceptor } from '../../interceptors/auth.interceptor'; | |
| import { Subject, takeUntil } from 'rxjs'; | |
| // Dynamic imports for dialogs | |
| const loadProjectEditDialog = () => import('../../dialogs/project-edit-dialog/project-edit-dialog.component'); | |
| const loadVersionEditDialog = () => import('../../dialogs/version-edit-dialog/version-edit-dialog.component'); | |
| const loadConfirmDialog = () => import('../../dialogs/confirm-dialog/confirm-dialog.component'); | |
| ({ | |
| selector: 'app-projects', | |
| standalone: true, | |
| imports: [ | |
| CommonModule, | |
| FormsModule, | |
| HttpClientModule, | |
| MatTableModule, | |
| MatProgressBarModule, | |
| MatButtonModule, | |
| MatCheckboxModule, | |
| MatFormFieldModule, | |
| MatInputModule, | |
| MatButtonToggleModule, | |
| MatCardModule, | |
| MatChipsModule, | |
| MatIconModule, | |
| MatMenuModule, | |
| MatDividerModule, | |
| MatDialogModule, | |
| MatSnackBarModule | |
| ], | |
| providers: [ | |
| ApiService | |
| ], | |
| templateUrl: './projects.component.html', | |
| styleUrls: ['./projects.component.scss'] | |
| }) | |
| export class ProjectsComponent implements OnInit, OnDestroy { | |
| projects: Project[] = []; | |
| filteredProjects: Project[] = []; | |
| searchTerm = ''; | |
| showDeleted = false; | |
| viewMode: 'list' | 'card' = 'card'; | |
| loading = false; | |
| message = ''; | |
| isError = false; | |
| // For table view | |
| displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions']; | |
| // Memory leak prevention | |
| private destroyed$ = new Subject<void>(); | |
| constructor( | |
| private apiService: ApiService, | |
| private dialog: MatDialog, | |
| private snackBar: MatSnackBar | |
| ) {} | |
| ngOnInit() { | |
| this.loadProjects(); | |
| this.loadEnvironment(); | |
| } | |
| ngOnDestroy() { | |
| this.destroyed$.next(); | |
| this.destroyed$.complete(); | |
| } | |
| isSparkTabVisible(): boolean { | |
| // Environment bilgisini cache'ten al (eğer varsa) | |
| const env = localStorage.getItem('flare_environment'); | |
| if (env) { | |
| const config = JSON.parse(env); | |
| return !config.work_mode?.startsWith('gpt4o'); | |
| } | |
| return true; // Default olarak göster | |
| } | |
| loadProjects() { | |
| this.loading = true; | |
| this.apiService.getProjects(this.showDeleted) | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (projects) => { | |
| this.projects = projects || []; | |
| this.applyFilter(); | |
| this.loading = false; | |
| }, | |
| error: (error) => { | |
| this.loading = false; | |
| this.showMessage('Failed to load projects', true); | |
| console.error('Load projects error:', error); | |
| } | |
| }); | |
| } | |
| private loadEnvironment() { | |
| this.apiService.getEnvironment() | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (env) => { | |
| localStorage.setItem('flare_environment', JSON.stringify(env)); | |
| }, | |
| error: (err) => { | |
| console.error('Failed to load environment:', err); | |
| } | |
| }); | |
| } | |
| applyFilter() { | |
| this.filteredProjects = this.projects.filter(project => { | |
| const matchesSearch = !this.searchTerm || | |
| project.name.toLowerCase().includes(this.searchTerm.toLowerCase()) || | |
| (project.caption || '').toLowerCase().includes(this.searchTerm.toLowerCase()); | |
| const matchesDeleted = this.showDeleted || !project.deleted; | |
| return matchesSearch && matchesDeleted; | |
| }); | |
| } | |
| filterProjects() { | |
| this.applyFilter(); | |
| } | |
| onSearchChange() { | |
| this.applyFilter(); | |
| } | |
| onShowDeletedChange() { | |
| this.loadProjects(); | |
| } | |
| async createProject() { | |
| try { | |
| const { default: ProjectEditDialogComponent } = await loadProjectEditDialog(); | |
| const dialogRef = this.dialog.open(ProjectEditDialogComponent, { | |
| width: '500px', | |
| data: { mode: 'create' } | |
| }); | |
| dialogRef.afterClosed() | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe(result => { | |
| if (result) { | |
| this.loadProjects(); | |
| this.showMessage('Project created successfully', false); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Failed to load dialog:', error); | |
| this.showMessage('Failed to open dialog', true); | |
| } | |
| } | |
| async editProject(project: Project, event?: Event) { | |
| if (event) { | |
| event.stopPropagation(); | |
| } | |
| try { | |
| const { default: ProjectEditDialogComponent } = await loadProjectEditDialog(); | |
| const dialogRef = this.dialog.open(ProjectEditDialogComponent, { | |
| width: '500px', | |
| data: { mode: 'edit', project: { ...project } } | |
| }); | |
| dialogRef.afterClosed() | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe(result => { | |
| if (result) { | |
| // Listeyi güncelle | |
| const index = this.projects.findIndex(p => p.id === result.id); | |
| if (index !== -1) { | |
| this.projects[index] = result; | |
| this.applyFilter(); // Filtreyi yeniden uygula | |
| } else { | |
| this.loadProjects(); // Bulunamazsa tüm listeyi yenile | |
| } | |
| this.showMessage('Project updated successfully', false); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Failed to load dialog:', error); | |
| this.showMessage('Failed to open dialog', true); | |
| } | |
| } | |
| toggleProject(project: Project, event?: Event) { | |
| if (event) { | |
| event.stopPropagation(); | |
| } | |
| const action = project.enabled ? 'disable' : 'enable'; | |
| const confirmMessage = `Are you sure you want to ${action} "${project.caption}"?`; | |
| this.confirmAction( | |
| `${action.charAt(0).toUpperCase() + action.slice(1)} Project`, | |
| confirmMessage, | |
| action.charAt(0).toUpperCase() + action.slice(1), | |
| !project.enabled | |
| ).then(confirmed => { | |
| if (confirmed) { | |
| this.apiService.toggleProject(project.id) | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (result) => { | |
| project.enabled = result.enabled; | |
| this.showMessage( | |
| `Project ${project.enabled ? 'enabled' : 'disabled'} successfully`, | |
| false | |
| ); | |
| }, | |
| error: (error) => this.handleUpdateError(error, project.caption) | |
| }); | |
| } | |
| }); | |
| } | |
| async manageVersions(project: Project, event?: Event) { | |
| if (event) { | |
| event.stopPropagation(); | |
| } | |
| try { | |
| const { default: VersionEditDialogComponent } = await loadVersionEditDialog(); | |
| const dialogRef = this.dialog.open(VersionEditDialogComponent, { | |
| width: '90vw', | |
| maxWidth: '1200px', | |
| height: '90vh', | |
| data: { project } | |
| }); | |
| dialogRef.afterClosed() | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe(result => { | |
| if (result) { | |
| this.loadProjects(); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Failed to load dialog:', error); | |
| this.showMessage('Failed to open dialog', true); | |
| } | |
| } | |
| deleteProject(project: Project, event?: Event) { | |
| if (event) { | |
| event.stopPropagation(); | |
| } | |
| const hasVersions = project.versions && project.versions.length > 0; | |
| const message = hasVersions ? | |
| `Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` : | |
| `Are you sure you want to delete project "${project.name}"?`; | |
| this.confirmAction('Delete Project', message, 'Delete', true).then(confirmed => { | |
| if (confirmed) { | |
| this.apiService.deleteProject(project.id) | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: () => { | |
| this.showMessage('Project deleted successfully', false); | |
| this.loadProjects(); | |
| }, | |
| error: (error) => { | |
| const message = error.error?.detail || 'Failed to delete project'; | |
| this.showMessage(message, true); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| exportProject(project: Project, event?: Event) { | |
| if (event) { | |
| event.stopPropagation(); | |
| } | |
| this.apiService.exportProject(project.id) | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (data) => { | |
| // Create and download file | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `${project.name}_export_${new Date().getTime()}.json`; | |
| link.click(); | |
| window.URL.revokeObjectURL(url); | |
| this.showMessage('Project exported successfully', false); | |
| }, | |
| error: (error) => { | |
| this.showMessage('Failed to export project', true); | |
| console.error('Export error:', error); | |
| } | |
| }); | |
| } | |
| importProject() { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = '.json'; | |
| input.onchange = async (event: any) => { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| try { | |
| const text = await file.text(); | |
| const data = JSON.parse(text); | |
| this.apiService.importProject(data) | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: () => { | |
| this.showMessage('Project imported successfully', false); | |
| this.loadProjects(); | |
| }, | |
| error: (error) => { | |
| const message = error.error?.detail || 'Failed to import project'; | |
| this.showMessage(message, true); | |
| } | |
| }); | |
| } catch (error) { | |
| this.showMessage('Invalid file format', true); | |
| } | |
| }; | |
| input.click(); | |
| } | |
| getPublishedCount(project: Project): number { | |
| return project.versions?.filter(v => v.published).length || 0; | |
| } | |
| getRelativeTime(timestamp: string | undefined): string { | |
| if (!timestamp) return 'Never'; | |
| const date = new Date(timestamp); | |
| const now = new Date(); | |
| const diffMs = now.getTime() - date.getTime(); | |
| const diffMins = Math.floor(diffMs / 60000); | |
| const diffHours = Math.floor(diffMs / 3600000); | |
| const diffDays = Math.floor(diffMs / 86400000); | |
| if (diffMins < 60) return `${diffMins} minutes ago`; | |
| if (diffHours < 24) return `${diffHours} hours ago`; | |
| if (diffDays < 7) return `${diffDays} days ago`; | |
| return date.toLocaleDateString(); | |
| } | |
| trackByProjectId(index: number, project: Project): number { | |
| return project.id; | |
| } | |
| handleUpdateError(error: any, projectName?: string): void { | |
| if (error.status === 409 || error.raceCondition) { | |
| const details = error.error?.details || error; | |
| const lastUpdateUser = details.last_update_user || error.lastUpdateUser || 'another user'; | |
| const lastUpdateDate = details.last_update_date || error.lastUpdateDate; | |
| const message = projectName | |
| ? `Project "${projectName}" was modified by ${lastUpdateUser}. Please reload.` | |
| : `Project was modified by ${lastUpdateUser}. Please reload.`; | |
| this.snackBar.open( | |
| message, | |
| 'Reload', | |
| { | |
| duration: 0, | |
| panelClass: ['error-snackbar', 'race-condition-snackbar'] | |
| } | |
| ).onAction().subscribe(() => { | |
| this.loadProjects(); | |
| }); | |
| // Log additional info if available | |
| if (lastUpdateDate) { | |
| console.info(`Last updated at: ${lastUpdateDate}`); | |
| } | |
| } else { | |
| // Generic error handling | |
| this.snackBar.open( | |
| error.error?.detail || error.message || 'Operation failed', | |
| 'Close', | |
| { | |
| duration: 5000, | |
| panelClass: ['error-snackbar'] | |
| } | |
| ); | |
| } | |
| } | |
| private async confirmAction(title: string, message: string, confirmText: string, dangerous: boolean): Promise<boolean> { | |
| try { | |
| const { default: ConfirmDialogComponent } = await loadConfirmDialog(); | |
| const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
| width: '400px', | |
| data: { | |
| title, | |
| message, | |
| confirmText, | |
| confirmColor: dangerous ? 'warn' : 'primary' | |
| } | |
| }); | |
| return await dialogRef.afterClosed().toPromise() || false; | |
| } catch (error) { | |
| console.error('Failed to load confirm dialog:', error); | |
| return false; | |
| } | |
| } | |
| private showMessage(message: string, isError: boolean) { | |
| this.message = message; | |
| this.isError = isError; | |
| setTimeout(() => { | |
| this.message = ''; | |
| }, 5000); | |
| } | |
| } |