Spaces:
Building
Building
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); | |
} | |
} |