ciyidogan commited on
Commit
8ae3120
·
verified ·
1 Parent(s): 128160d

Update flare-ui/src/app/components/projects/projects.component.ts

Browse files
flare-ui/src/app/components/projects/projects.component.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, OnInit } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { FormsModule } from '@angular/forms';
4
  import { MatDialog, MatDialogModule } from '@angular/material/dialog';
@@ -16,9 +16,12 @@ import { MatIconModule } from '@angular/material/icon';
16
  import { MatMenuModule } from '@angular/material/menu';
17
  import { MatDividerModule } from '@angular/material/divider';
18
  import { ApiService, Project } from '../../services/api.service';
19
- import ProjectEditDialogComponent from '../../dialogs/project-edit-dialog/project-edit-dialog.component';
20
- import VersionEditDialogComponent from '../../dialogs/version-edit-dialog/version-edit-dialog.component';
21
- import ConfirmDialogComponent from '../../dialogs/confirm-dialog/confirm-dialog.component';
 
 
 
22
 
23
  @Component({
24
  selector: 'app-projects',
@@ -44,7 +47,7 @@ import ConfirmDialogComponent from '../../dialogs/confirm-dialog/confirm-dialog.
44
  templateUrl: './projects.component.html',
45
  styleUrls: ['./projects.component.scss']
46
  })
47
- export class ProjectsComponent implements OnInit {
48
  projects: Project[] = [];
49
  filteredProjects: Project[] = [];
50
  searchTerm = '';
@@ -56,6 +59,9 @@ export class ProjectsComponent implements OnInit {
56
 
57
  // For table view
58
  displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions'];
 
 
 
59
 
60
  constructor(
61
  private apiService: ApiService,
@@ -68,6 +74,11 @@ export class ProjectsComponent implements OnInit {
68
  this.loadEnvironment();
69
  }
70
 
 
 
 
 
 
71
  isSparkTabVisible(): boolean {
72
  // Environment bilgisini cache'ten al (eğer varsa)
73
  const env = localStorage.getItem('flare_environment');
@@ -78,27 +89,35 @@ export class ProjectsComponent implements OnInit {
78
  return true; // Default olarak göster
79
  }
80
 
81
- async loadProjects() {
82
  this.loading = true;
83
- try {
84
- this.projects = await this.apiService.getProjects(this.showDeleted).toPromise() || [];
85
- this.applyFilter();
86
- } catch (error) {
87
- this.showMessage('Failed to load projects', true);
88
- } finally {
89
- this.loading = false;
90
- }
 
 
 
 
 
 
91
  }
92
 
93
  private loadEnvironment() {
94
- this.apiService.getEnvironment().subscribe({
95
- next: (env) => {
96
- localStorage.setItem('flare_environment', JSON.stringify(env));
97
- },
98
- error: (err) => {
99
- console.error('Failed to load environment:', err);
100
- }
101
- });
 
 
102
  }
103
 
104
  applyFilter() {
@@ -125,114 +144,175 @@ export class ProjectsComponent implements OnInit {
125
  this.loadProjects();
126
  }
127
 
128
- createProject() {
129
- const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
130
- width: '500px',
131
- data: { mode: 'create' }
132
- });
133
-
134
- dialogRef.afterClosed().subscribe(result => {
135
- if (result) {
136
- this.loadProjects();
137
- }
138
- });
 
 
 
 
 
 
 
 
 
 
139
  }
140
 
141
- editProject(project: Project) {
142
- const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
143
- width: '500px',
144
- data: { mode: 'edit', project: { ...project } }
145
- });
146
-
147
- dialogRef.afterClosed().subscribe(result => {
148
- if (result) {
149
- // Listeyi güncelle
150
- const index = this.projects.findIndex(p => p.id === result.id);
151
- if (index !== -1) {
152
- this.projects[index] = result;
153
- this.applyFilter(); // Filtreyi yeniden uygula
154
- } else {
155
- this.loadProjects(); // Bulunamazsa tüm listeyi yenile
156
- }
157
- }
158
- });
159
- }
160
-
161
- async toggleProject(project: Project) {
162
  try {
163
- const result = await this.apiService.toggleProject(project.id).toPromise();
164
- project.enabled = result.enabled;
165
- this.showMessage(
166
- `Project "${project.name}" ${project.enabled ? 'enabled' : 'disabled'} successfully`,
167
- false
168
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  } catch (error) {
170
- this.showMessage('Failed to toggle project', true);
 
171
  }
172
  }
173
 
174
- manageVersions(project: Project) {
175
- const dialogRef = this.dialog.open(VersionEditDialogComponent, {
176
- width: '90vw',
177
- maxWidth: '1200px',
178
- height: '90vh',
179
- data: { project }
180
- });
181
 
182
- dialogRef.afterClosed().subscribe(result => {
183
- if (result) {
184
- this.loadProjects();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
  });
187
  }
188
 
189
- async deleteProject(project: Project) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  const hasVersions = project.versions && project.versions.length > 0;
191
  const message = hasVersions ?
192
  `Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` :
193
  `Are you sure you want to delete project "${project.name}"?`;
194
 
195
- const dialogRef = this.dialog.open(ConfirmDialogComponent, {
196
- width: '400px',
197
- data: {
198
- title: 'Delete Project',
199
- message: message,
200
- confirmText: 'Delete',
201
- confirmColor: 'warn'
202
- }
203
- });
204
-
205
- dialogRef.afterClosed().subscribe(async confirmed => {
206
  if (confirmed) {
207
- try {
208
- await this.apiService.deleteProject(project.id).toPromise();
209
- this.showMessage('Project deleted successfully', false);
210
- this.loadProjects();
211
- } catch (error: any) {
212
- const message = error.error?.detail || 'Failed to delete project';
213
- this.showMessage(message, true);
214
- }
 
 
 
 
215
  }
216
  });
217
  }
218
 
219
- async exportProject(project: Project) {
220
- try {
221
- const data = await this.apiService.exportProject(project.id).toPromise();
222
-
223
- // Create and download file
224
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
225
- const url = window.URL.createObjectURL(blob);
226
- const link = document.createElement('a');
227
- link.href = url;
228
- link.download = `${project.name}_export_${new Date().getTime()}.json`;
229
- link.click();
230
- window.URL.revokeObjectURL(url);
231
-
232
- this.showMessage('Project exported successfully', false);
233
- } catch (error) {
234
- this.showMessage('Failed to export project', true);
235
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
 
238
  importProject() {
@@ -248,12 +328,20 @@ export class ProjectsComponent implements OnInit {
248
  const text = await file.text();
249
  const data = JSON.parse(text);
250
 
251
- await this.apiService.importProject(data).toPromise();
252
- this.showMessage('Project imported successfully', false);
253
- this.loadProjects();
254
- } catch (error: any) {
255
- const message = error.error?.detail || 'Failed to import project';
256
- this.showMessage(message, true);
 
 
 
 
 
 
 
 
257
  }
258
  };
259
 
@@ -285,6 +373,65 @@ export class ProjectsComponent implements OnInit {
285
  return project.id;
286
  }
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  private showMessage(message: string, isError: boolean) {
289
  this.message = message;
290
  this.isError = isError;
 
1
+ import { Component, OnInit, OnDestroy } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { FormsModule } from '@angular/forms';
4
  import { MatDialog, MatDialogModule } from '@angular/material/dialog';
 
16
  import { MatMenuModule } from '@angular/material/menu';
17
  import { MatDividerModule } from '@angular/material/divider';
18
  import { ApiService, Project } from '../../services/api.service';
19
+ import { Subject, takeUntil } from 'rxjs';
20
+
21
+ // Dynamic imports for dialogs
22
+ const loadProjectEditDialog = () => import('../../dialogs/project-edit-dialog/project-edit-dialog.component');
23
+ const loadVersionEditDialog = () => import('../../dialogs/version-edit-dialog/version-edit-dialog.component');
24
+ const loadConfirmDialog = () => import('../../dialogs/confirm-dialog/confirm-dialog.component');
25
 
26
  @Component({
27
  selector: 'app-projects',
 
47
  templateUrl: './projects.component.html',
48
  styleUrls: ['./projects.component.scss']
49
  })
50
+ export class ProjectsComponent implements OnInit, OnDestroy {
51
  projects: Project[] = [];
52
  filteredProjects: Project[] = [];
53
  searchTerm = '';
 
59
 
60
  // For table view
61
  displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions'];
62
+
63
+ // Memory leak prevention
64
+ private destroyed$ = new Subject<void>();
65
 
66
  constructor(
67
  private apiService: ApiService,
 
74
  this.loadEnvironment();
75
  }
76
 
77
+ ngOnDestroy() {
78
+ this.destroyed$.next();
79
+ this.destroyed$.complete();
80
+ }
81
+
82
  isSparkTabVisible(): boolean {
83
  // Environment bilgisini cache'ten al (eğer varsa)
84
  const env = localStorage.getItem('flare_environment');
 
89
  return true; // Default olarak göster
90
  }
91
 
92
+ loadProjects() {
93
  this.loading = true;
94
+ this.apiService.getProjects(this.showDeleted)
95
+ .pipe(takeUntil(this.destroyed$))
96
+ .subscribe({
97
+ next: (projects) => {
98
+ this.projects = projects || [];
99
+ this.applyFilter();
100
+ this.loading = false;
101
+ },
102
+ error: (error) => {
103
+ this.loading = false;
104
+ this.showMessage('Failed to load projects', true);
105
+ console.error('Load projects error:', error);
106
+ }
107
+ });
108
  }
109
 
110
  private loadEnvironment() {
111
+ this.apiService.getEnvironment()
112
+ .pipe(takeUntil(this.destroyed$))
113
+ .subscribe({
114
+ next: (env) => {
115
+ localStorage.setItem('flare_environment', JSON.stringify(env));
116
+ },
117
+ error: (err) => {
118
+ console.error('Failed to load environment:', err);
119
+ }
120
+ });
121
  }
122
 
123
  applyFilter() {
 
144
  this.loadProjects();
145
  }
146
 
147
+ async createProject() {
148
+ try {
149
+ const { default: ProjectEditDialogComponent } = await loadProjectEditDialog();
150
+
151
+ const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
152
+ width: '500px',
153
+ data: { mode: 'create' }
154
+ });
155
+
156
+ dialogRef.afterClosed()
157
+ .pipe(takeUntil(this.destroyed$))
158
+ .subscribe(result => {
159
+ if (result) {
160
+ this.loadProjects();
161
+ this.showMessage('Project created successfully', false);
162
+ }
163
+ });
164
+ } catch (error) {
165
+ console.error('Failed to load dialog:', error);
166
+ this.showMessage('Failed to open dialog', true);
167
+ }
168
  }
169
 
170
+ async editProject(project: Project, event?: Event) {
171
+ if (event) {
172
+ event.stopPropagation();
173
+ }
174
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  try {
176
+ const { default: ProjectEditDialogComponent } = await loadProjectEditDialog();
177
+
178
+ const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
179
+ width: '500px',
180
+ data: { mode: 'edit', project: { ...project } }
181
+ });
182
+
183
+ dialogRef.afterClosed()
184
+ .pipe(takeUntil(this.destroyed$))
185
+ .subscribe(result => {
186
+ if (result) {
187
+ // Listeyi güncelle
188
+ const index = this.projects.findIndex(p => p.id === result.id);
189
+ if (index !== -1) {
190
+ this.projects[index] = result;
191
+ this.applyFilter(); // Filtreyi yeniden uygula
192
+ } else {
193
+ this.loadProjects(); // Bulunamazsa tüm listeyi yenile
194
+ }
195
+ this.showMessage('Project updated successfully', false);
196
+ }
197
+ });
198
  } catch (error) {
199
+ console.error('Failed to load dialog:', error);
200
+ this.showMessage('Failed to open dialog', true);
201
  }
202
  }
203
 
204
+ toggleProject(project: Project, event?: Event) {
205
+ if (event) {
206
+ event.stopPropagation();
207
+ }
 
 
 
208
 
209
+ const action = project.enabled ? 'disable' : 'enable';
210
+ const confirmMessage = `Are you sure you want to ${action} "${project.caption}"?`;
211
+
212
+ this.confirmAction(
213
+ `${action.charAt(0).toUpperCase() + action.slice(1)} Project`,
214
+ confirmMessage,
215
+ action.charAt(0).toUpperCase() + action.slice(1),
216
+ !project.enabled
217
+ ).then(confirmed => {
218
+ if (confirmed) {
219
+ this.apiService.toggleProject(project.id)
220
+ .pipe(takeUntil(this.destroyed$))
221
+ .subscribe({
222
+ next: (result) => {
223
+ project.enabled = result.enabled;
224
+ this.showMessage(
225
+ `Project ${project.enabled ? 'enabled' : 'disabled'} successfully`,
226
+ false
227
+ );
228
+ },
229
+ error: (error) => this.handleUpdateError(error, project.caption)
230
+ });
231
  }
232
  });
233
  }
234
 
235
+ async manageVersions(project: Project, event?: Event) {
236
+ if (event) {
237
+ event.stopPropagation();
238
+ }
239
+
240
+ try {
241
+ const { default: VersionEditDialogComponent } = await loadVersionEditDialog();
242
+
243
+ const dialogRef = this.dialog.open(VersionEditDialogComponent, {
244
+ width: '90vw',
245
+ maxWidth: '1200px',
246
+ height: '90vh',
247
+ data: { project }
248
+ });
249
+
250
+ dialogRef.afterClosed()
251
+ .pipe(takeUntil(this.destroyed$))
252
+ .subscribe(result => {
253
+ if (result) {
254
+ this.loadProjects();
255
+ }
256
+ });
257
+ } catch (error) {
258
+ console.error('Failed to load dialog:', error);
259
+ this.showMessage('Failed to open dialog', true);
260
+ }
261
+ }
262
+
263
+ deleteProject(project: Project, event?: Event) {
264
+ if (event) {
265
+ event.stopPropagation();
266
+ }
267
+
268
  const hasVersions = project.versions && project.versions.length > 0;
269
  const message = hasVersions ?
270
  `Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` :
271
  `Are you sure you want to delete project "${project.name}"?`;
272
 
273
+ this.confirmAction('Delete Project', message, 'Delete', true).then(confirmed => {
 
 
 
 
 
 
 
 
 
 
274
  if (confirmed) {
275
+ this.apiService.deleteProject(project.id)
276
+ .pipe(takeUntil(this.destroyed$))
277
+ .subscribe({
278
+ next: () => {
279
+ this.showMessage('Project deleted successfully', false);
280
+ this.loadProjects();
281
+ },
282
+ error: (error) => {
283
+ const message = error.error?.detail || 'Failed to delete project';
284
+ this.showMessage(message, true);
285
+ }
286
+ });
287
  }
288
  });
289
  }
290
 
291
+ exportProject(project: Project, event?: Event) {
292
+ if (event) {
293
+ event.stopPropagation();
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
+
296
+ this.apiService.exportProject(project.id)
297
+ .pipe(takeUntil(this.destroyed$))
298
+ .subscribe({
299
+ next: (data) => {
300
+ // Create and download file
301
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
302
+ const url = window.URL.createObjectURL(blob);
303
+ const link = document.createElement('a');
304
+ link.href = url;
305
+ link.download = `${project.name}_export_${new Date().getTime()}.json`;
306
+ link.click();
307
+ window.URL.revokeObjectURL(url);
308
+
309
+ this.showMessage('Project exported successfully', false);
310
+ },
311
+ error: (error) => {
312
+ this.showMessage('Failed to export project', true);
313
+ console.error('Export error:', error);
314
+ }
315
+ });
316
  }
317
 
318
  importProject() {
 
328
  const text = await file.text();
329
  const data = JSON.parse(text);
330
 
331
+ this.apiService.importProject(data)
332
+ .pipe(takeUntil(this.destroyed$))
333
+ .subscribe({
334
+ next: () => {
335
+ this.showMessage('Project imported successfully', false);
336
+ this.loadProjects();
337
+ },
338
+ error: (error) => {
339
+ const message = error.error?.detail || 'Failed to import project';
340
+ this.showMessage(message, true);
341
+ }
342
+ });
343
+ } catch (error) {
344
+ this.showMessage('Invalid file format', true);
345
  }
346
  };
347
 
 
373
  return project.id;
374
  }
375
 
376
+ handleUpdateError(error: any, projectName?: string): void {
377
+ if (error.status === 409 || error.raceCondition) {
378
+ const details = error.error?.details || error;
379
+ const lastUpdateUser = details.last_update_user || error.lastUpdateUser || 'another user';
380
+ const lastUpdateDate = details.last_update_date || error.lastUpdateDate;
381
+
382
+ const message = projectName
383
+ ? `Project "${projectName}" was modified by ${lastUpdateUser}. Please reload.`
384
+ : `Project was modified by ${lastUpdateUser}. Please reload.`;
385
+
386
+ this.snackBar.open(
387
+ message,
388
+ 'Reload',
389
+ {
390
+ duration: 0,
391
+ panelClass: ['error-snackbar', 'race-condition-snackbar']
392
+ }
393
+ ).onAction().subscribe(() => {
394
+ this.loadProjects();
395
+ });
396
+
397
+ // Log additional info if available
398
+ if (lastUpdateDate) {
399
+ console.info(`Last updated at: ${lastUpdateDate}`);
400
+ }
401
+ } else {
402
+ // Generic error handling
403
+ this.snackBar.open(
404
+ error.error?.detail || error.message || 'Operation failed',
405
+ 'Close',
406
+ {
407
+ duration: 5000,
408
+ panelClass: ['error-snackbar']
409
+ }
410
+ );
411
+ }
412
+ }
413
+
414
+ private async confirmAction(title: string, message: string, confirmText: string, dangerous: boolean): Promise<boolean> {
415
+ try {
416
+ const { default: ConfirmDialogComponent } = await loadConfirmDialog();
417
+
418
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
419
+ width: '400px',
420
+ data: {
421
+ title,
422
+ message,
423
+ confirmText,
424
+ confirmColor: dangerous ? 'warn' : 'primary'
425
+ }
426
+ });
427
+
428
+ return await dialogRef.afterClosed().toPromise() || false;
429
+ } catch (error) {
430
+ console.error('Failed to load confirm dialog:', error);
431
+ return false;
432
+ }
433
+ }
434
+
435
  private showMessage(message: string, isError: boolean) {
436
  this.message = message;
437
  this.isError = isError;