flare / flare-ui /src /app /components /projects /projects.component.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
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');
@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);
}
}