Skip to content

Commit 865e0d3

Browse files
committed
fix(exports): bind PrimeNG columns metadata for table CSV exports
1 parent a1ba92d commit 865e0d3

8 files changed

Lines changed: 87 additions & 97 deletions

File tree

src/app/features/activity/components/activity-log/activity-log.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ <h2>Activity Log</h2>
3333
<p-table
3434
#dt
3535
[value]="filteredRows"
36+
[columns]="exportColumns"
37+
[exportFilename]="exportFileName"
3638
[loading]="isLoading"
3739
[rowHover]="true"
3840
[paginator]="true"

src/app/features/activity/components/activity-log/activity-log.component.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export class ActivityLogComponent implements OnInit, OnDestroy {
5757
];
5858

5959
readonly feedLimit = 500;
60+
readonly exportColumns = [
61+
{ field: 'actorDisplayName', header: 'Actor' },
62+
{ field: 'entityType', header: 'Entity' },
63+
{ field: 'typeLabel', header: 'Event Type' },
64+
{ field: 'projectName', header: 'Project' },
65+
{ field: 'taskTitle', header: 'Task' },
66+
{ field: 'occurredAtDate', header: 'Occurred At' }
67+
];
6068

6169
rows: ActivityLogRow[] = [];
6270
filteredRows: ActivityLogRow[] = [];
@@ -93,6 +101,11 @@ export class ActivityLogComponent implements OnInit, OnDestroy {
93101
return this.preferencesService.preferences().defaultTablePageSize;
94102
}
95103

104+
get exportFileName(): string {
105+
const date = new Date().toISOString().slice(0, 10);
106+
return `activity-log-${date}`;
107+
}
108+
96109
ngOnInit(): void {
97110
this.loadActivityLog();
98111
this.subscribeToLiveActivity();

src/app/features/admin/components/admin-dashboard/admin-dashboard.component.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ <h2>Admin Dashboard</h2>
3939
<p-table
4040
#dt
4141
[value]="users"
42+
[columns]="exportColumns"
43+
[exportFilename]="exportFileName"
4244
[lazy]="!isPreviewMode"
4345
[loading]="isLoading"
4446
[paginator]="true"
@@ -57,11 +59,11 @@ <h2>Admin Dashboard</h2>
5759
<button
5860
pButton
5961
type="button"
60-
[icon]="isExporting ? 'pi pi-spin pi-spinner' : 'pi pi-upload'"
62+
icon="pi pi-upload"
6163
label="Export CSV"
6264
class="p-button-text"
63-
(click)="exportUsersAsExcelCompatibleCsv()"
64-
[disabled]="isLoading || isExporting">
65+
(click)="exportCsv(dt)"
66+
[disabled]="isLoading || users.length === 0">
6567
</button>
6668

6769
<span class="table-search">

src/app/features/admin/components/admin-dashboard/admin-dashboard.component.ts

Lines changed: 15 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { TagModule } from 'primeng/tag';
99
import { ButtonModule } from 'primeng/button';
1010
import { CardModule } from 'primeng/card';
1111
import { TooltipModule } from 'primeng/tooltip';
12-
import { finalize, firstValueFrom } from 'rxjs';
12+
import { finalize } from 'rxjs';
1313
import { AdminUsersApiClient } from '../../../../core/api/clients/admin-users-api.client';
1414
import { AppRole } from '../../../../core/auth/models/app-role.model';
1515
import { AuthService } from '../../../../core/auth/services/auth.service';
@@ -48,7 +48,6 @@ export class AdminDashboardComponent implements OnInit {
4848

4949
users: UserSummaryDto[] = [];
5050
isLoading = false;
51-
isExporting = false;
5251
isPreviewMode = false;
5352
previewDetail: string | null = null;
5453
totalRecords = 0;
@@ -73,11 +72,13 @@ export class AdminDashboardComponent implements OnInit {
7372
{ label: AppRole.ProjectManager, value: AppRole.ProjectManager },
7473
{ label: AppRole.User, value: AppRole.User }
7574
];
76-
77-
get exportFileName(): string {
78-
const date = new Date().toISOString().slice(0, 10);
79-
return `admin-users-${date}`;
80-
}
75+
readonly exportColumns = [
76+
{ field: 'displayName', header: 'Display Name' },
77+
{ field: 'userName', header: 'Username' },
78+
{ field: 'email', header: 'Email' },
79+
{ field: 'roles', header: 'Roles' },
80+
{ field: 'isActive', header: 'Is Active' }
81+
];
8182

8283
get activeUsersCount(): number {
8384
return this.users.filter((user) => user.isActive).length;
@@ -107,6 +108,11 @@ export class AdminDashboardComponent implements OnInit {
107108
return this.authService.currentUserId();
108109
}
109110

111+
get exportFileName(): string {
112+
const date = new Date().toISOString().slice(0, 10);
113+
return `admin-users-${date}`;
114+
}
115+
110116
ngOnInit(): void {
111117
this.rows = this.preferencesService.preferences().defaultTablePageSize;
112118
this.loadUsers();
@@ -152,37 +158,8 @@ export class AdminDashboardComponent implements OnInit {
152158
this.loadUsers();
153159
}
154160

155-
async exportUsersAsExcelCompatibleCsv(): Promise<void> {
156-
if (this.isExporting) {
157-
return;
158-
}
159-
160-
this.isExporting = true;
161-
try {
162-
const header = ['Display Name', 'Username', 'Email', 'Is Active', 'Roles'];
163-
const usersForExport = await this.loadAllUsersForExport();
164-
const lines = usersForExport.map((user) => [
165-
user.displayName ?? '',
166-
user.userName ?? '',
167-
user.email ?? '',
168-
user.isActive ? 'Yes' : 'No',
169-
user.roles.join(' | ')
170-
]);
171-
172-
const csv = [header, ...lines]
173-
.map((line) => line.map((value) => this.escapeCsv(value)).join(','))
174-
.join('\n');
175-
176-
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
177-
const url = URL.createObjectURL(blob);
178-
const link = document.createElement('a');
179-
link.href = url;
180-
link.download = `${this.exportFileName}.csv`;
181-
link.click();
182-
URL.revokeObjectURL(url);
183-
} finally {
184-
this.isExporting = false;
185-
}
161+
exportCsv(table: Table): void {
162+
table.exportCSV();
186163
}
187164

188165
isStatusUpdating(userId: string): boolean {
@@ -278,41 +255,6 @@ export class AdminDashboardComponent implements OnInit {
278255
});
279256
}
280257

281-
private escapeCsv(value: string): string {
282-
const escaped = value.replace(/"/g, '""');
283-
return `"${escaped}"`;
284-
}
285-
286-
private async loadAllUsersForExport(): Promise<UserSummaryDto[]> {
287-
if (this.isPreviewMode) {
288-
return [...this.users];
289-
}
290-
291-
const pageSize = 100;
292-
let page = 1;
293-
let total = 0;
294-
const allUsers: UserSummaryDto[] = [];
295-
const search = this.getMergedSearchTerm();
296-
297-
do {
298-
const response = await firstValueFrom(
299-
this.adminUsersApiClient.getUsers({
300-
page,
301-
pageSize,
302-
search: search.length > 0 ? search : undefined,
303-
isActive: this.selectedStatus ?? undefined,
304-
role: this.selectedRole ?? undefined
305-
})
306-
);
307-
308-
total = response.total;
309-
allUsers.push(...response.items);
310-
page += 1;
311-
} while (allUsers.length < total);
312-
313-
return allUsers;
314-
}
315-
316258
private shouldUsePreviewMode(): boolean {
317259
return this.authService.authSession()?.isDebugSession === true && this.authService.canStartDebugSession();
318260
}

src/app/features/projects/components/project-list/project-list.component.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ <h2>All Projects</h2>
6767
<p-table
6868
#dt
6969
[value]="projects"
70+
[columns]="exportColumns"
71+
[exportFilename]="exportFileName"
7072
[rowHover]="true"
7173
[loading]="loading"
7274
[tableStyle]="{ 'min-width': '72rem' }"
@@ -129,7 +131,7 @@ <h2>All Projects</h2>
129131
<th style="min-width: 8rem;">
130132
<div class="column-head">
131133
Tasks
132-
<p-columnFilter type="numeric" field="taskItems.length" display="menu"></p-columnFilter>
134+
<p-columnFilter type="numeric" field="taskCount" display="menu"></p-columnFilter>
133135
</div>
134136
</th>
135137
<th style="min-width: 10rem;">Actions</th>
@@ -166,7 +168,7 @@ <h2>All Projects</h2>
166168
</td>
167169

168170
<td>
169-
<p-chip [label]="getProjectTaskCount(project).toString()"></p-chip>
171+
<p-chip [label]="project.taskCount.toString()"></p-chip>
170172
</td>
171173

172174
<td>

src/app/features/projects/components/project-list/project-list.component.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { AuthService } from '../../../../core/auth/services/auth.service';
1212
import { APP_ENVIRONMENT } from '../../../../core/config/app-environment.token';
1313
import { AppPreferencesService } from '../../../../core/preferences/app-preferences.service';
1414

15+
interface ProjectListRow extends ProjectDto {
16+
taskCount: number;
17+
}
18+
1519
@Component({
1620
selector: 'app-project-list',
1721
standalone: true,
@@ -29,15 +33,23 @@ export class ProjectListComponent implements OnInit, OnDestroy {
2933
private readonly preferencesService = inject(AppPreferencesService);
3034
private readonly destroy$ = new Subject<void>();
3135

32-
projects: ProjectDto[] = [];
33-
selectedProjects: ProjectDto[] = [];
36+
projects: ProjectListRow[] = [];
37+
selectedProjects: ProjectListRow[] = [];
3438
loading = true;
3539
searchValue: string | undefined;
3640
isPreviewMode = false;
3741
previewDetail: string | null = null;
3842
errors: Message[] = [];
3943
pendingProjectDeletionIds = new Set<string>();
40-
private readonly previewTaskCounts: Record<string, number> = {};
44+
readonly exportColumns = [
45+
{ field: 'name', header: 'Project' },
46+
{ field: 'description', header: 'Description' },
47+
{ field: 'createdByUserName', header: 'Created By' },
48+
{ field: 'createdAt', header: 'Created' },
49+
{ field: 'lastModifiedAt', header: 'Last Updated' },
50+
{ field: 'lastModifiedByUserName', header: 'Last Updated By' },
51+
{ field: 'taskCount', header: 'Tasks' }
52+
];
4153

4254
ngOnInit(): void {
4355
this.loadProjects();
@@ -70,7 +82,12 @@ export class ProjectListComponent implements OnInit, OnDestroy {
7082
return this.authService.hasAnyRole([...MANAGEMENT_ROLES]);
7183
}
7284

73-
trackByProjectId(_: number, project: ProjectDto): string {
85+
get exportFileName(): string {
86+
const date = new Date().toISOString().slice(0, 10);
87+
return `all-projects-${date}`;
88+
}
89+
90+
trackByProjectId(_: number, project: ProjectListRow): string {
7491
return project.id;
7592
}
7693

@@ -103,7 +120,7 @@ export class ProjectListComponent implements OnInit, OnDestroy {
103120
void this.router.navigate(['/projects/kanban'], { queryParams: { projectId } });
104121
}
105122

106-
canDeleteProject(project: ProjectDto): boolean {
123+
canDeleteProject(project: ProjectListRow): boolean {
107124
if (this.authService.hasRole(AppRole.Administrator)) {
108125
return true;
109126
}
@@ -115,7 +132,7 @@ export class ProjectListComponent implements OnInit, OnDestroy {
115132
return this.pendingProjectDeletionIds.has(projectId);
116133
}
117134

118-
deleteProject(project: ProjectDto): void {
135+
deleteProject(project: ProjectListRow): void {
119136
if (!this.canDeleteProject(project) || this.isProjectDeletePending(project.id)) {
120137
return;
121138
}
@@ -177,7 +194,7 @@ export class ProjectListComponent implements OnInit, OnDestroy {
177194
.pipe(takeUntil(this.destroy$))
178195
.subscribe({
179196
next: (projects) => {
180-
this.projects = projects;
197+
this.projects = this.toProjectRows(projects);
181198
this.selectedProjects = [];
182199
this.loading = false;
183200
},
@@ -193,30 +210,26 @@ export class ProjectListComponent implements OnInit, OnDestroy {
193210
});
194211
}
195212

196-
getProjectTaskCount(project: ProjectDto): number {
197-
if (project.taskItems?.length) {
198-
return project.taskItems.length;
199-
}
200-
201-
return this.previewTaskCounts[project.id] ?? 0;
202-
}
203-
204213
private shouldUsePreviewMode(): boolean {
205214
return this.authService.authSession()?.isDebugSession === true && this.authService.canStartDebugSession();
206215
}
207216

208217
private loadPreviewProjects(detail: string): void {
209218
this.isPreviewMode = true;
210219
this.previewDetail = detail;
211-
this.projects = this.buildPreviewProjects();
212-
this.previewTaskCounts['preview-platform-refresh'] = 3;
213-
this.previewTaskCounts['preview-mobile-portal'] = 1;
214-
this.previewTaskCounts['preview-security-hardening'] = 2;
220+
this.projects = this.toProjectRows(this.buildPreviewProjects());
215221
this.selectedProjects = [];
216222
this.errors = [];
217223
this.loading = false;
218224
}
219225

226+
private toProjectRows(projects: ProjectDto[]): ProjectListRow[] {
227+
return projects.map((project) => ({
228+
...project,
229+
taskCount: project.taskItems?.length ?? 0
230+
}));
231+
}
232+
220233
private buildPreviewProjects(): ProjectDto[] {
221234
const now = Date.now();
222235
return [

src/app/features/task-item/components/task-item-list/task-item-list.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ <h2>All Tasks</h2>
6161
<p-table
6262
#dt
6363
[value]="tasks"
64+
[columns]="exportColumns"
65+
[exportFilename]="exportFileName"
6466
dataKey="id"
6567
[loading]="isLoadingTasks"
6668
[rowHover]="true"

src/app/features/task-item/components/task-item-list/task-item-list.component.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ export class TaskItemListComponent implements OnInit, OnDestroy {
6060

6161
searchValue = '';
6262
pendingTaskIds = new Set<string>();
63+
readonly exportColumns = [
64+
{ field: 'title', header: 'Task' },
65+
{ field: 'description', header: 'Description' },
66+
{ field: 'status', header: 'Status' },
67+
{ field: 'dueDate', header: 'Due Date' },
68+
{ field: 'assignedUserName', header: 'Assignee' },
69+
{ field: 'lastModifiedAt', header: 'Last Updated' },
70+
{ field: 'lastModifiedByUserName', header: 'Last Updated By' }
71+
];
6372

6473
ngOnInit(): void {
6574
this.loadProjects();
@@ -132,6 +141,11 @@ export class TaskItemListComponent implements OnInit, OnDestroy {
132141
return this.preferencesService.preferences().defaultTablePageSize;
133142
}
134143

144+
get exportFileName(): string {
145+
const date = new Date().toISOString().slice(0, 10);
146+
return `all-tasks-${date}`;
147+
}
148+
135149
onProjectChange(): void {
136150
if (!this.selectedProjectId) {
137151
this.tasks = [];

0 commit comments

Comments
 (0)