Skip to content

Commit bb02336

Browse files
committed
feat(dashboards): add unified pending actions api and ui improvements
LFXV2-828 LFXV2-829 LFXV2-830 LFXV2-831 - Add unified /api/user/pending-actions endpoint consolidating surveys and meetings - Implement cookie registry service with clear cache functionality - Improve meeting creation flow UX with internal step tracking - Polish pending actions, foundation health, and my meetings UI styling - Fix persona selector to use dynamic project UID Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 0318dfd commit bb02336

File tree

23 files changed

+453
-106
lines changed

23 files changed

+453
-106
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"confirmdialog",
1111
"contentful",
1212
"Contentful",
13+
"DATEADD",
1314
"daygrid",
1415
"dismissable",
1516
"domcontentloaded",

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,14 @@ export class BoardMemberDashboardComponent {
8181
return project$.pipe(
8282
switchMap((project) => {
8383
// If no project/foundation selected, return empty array
84-
if (!project?.slug) {
84+
if (!project?.slug || !project?.uid) {
8585
return of([]);
8686
}
8787

88-
// Fetch survey actions from API
89-
return this.projectService.getPendingActionSurveys(project.slug).pipe(
88+
// Fetch all pending actions from unified backend endpoint
89+
return this.projectService.getPendingActions(project.slug, project.uid, 'board-member').pipe(
9090
catchError((error) => {
91-
console.error('Failed to fetch survey actions:', error);
92-
// Return empty array on error
91+
console.error('Failed to fetch pending actions:', error);
9392
return of([]);
9493
})
9594
);

apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export class FoundationHealthComponent {
196196
icon: metric.icon,
197197
title: metric.title,
198198
value: data.totalProjects.toLocaleString(),
199-
subtitle: 'Across all foundations',
199+
subtitle: `Total ${this.projectContextService.selectedFoundation()?.name} projects`,
200200
category: metric.category as MetricCategory,
201201
testId: metric.testId,
202202
customContentType: metric.customContentType,
@@ -240,7 +240,7 @@ export class FoundationHealthComponent {
240240
icon: metric.icon,
241241
title: metric.title,
242242
value: data.totalMembers.toLocaleString(),
243-
subtitle: 'Member organizations',
243+
subtitle: `Total ${this.projectContextService.selectedFoundation()?.name} members`,
244244
category: metric.category as MetricCategory,
245245
testId: metric.testId,
246246
customContentType: metric.customContentType,

apps/lfx-one/src/app/modules/dashboards/components/my-meetings/my-meetings.component.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
<section class="flex flex-col flex-1" data-testid="dashboard-my-meetings-section">
55
<!-- Header -->
66
<div class="flex items-center justify-between mb-4">
7-
<h2>My Meetings</h2>
7+
<h2 class="flex items-center gap-2">
8+
<i class="fa-light fa-calendar text-lg"></i>
9+
<span>My Meetings</span>
10+
</h2>
811
<lfx-button
912
label="View All"
1013
icon="fa-light fa-chevron-right"

apps/lfx-one/src/app/modules/dashboards/components/pending-actions/pending-actions.component.html

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,50 @@
33

44
<section class="flex flex-col flex-1" data-testid="dashboard-pending-actions-section">
55
<!-- Header -->
6-
<div class="flex items-center justify-between mb-4">
7-
<h2 class="py-1">Pending Actions</h2>
6+
<div class="flex items-center justify-between mb-4 px-2">
7+
<h2 class="py-1 flex items-center gap-2">
8+
<i class="fa-light fa-list-check text-lg"></i>
9+
<span>Pending Actions</span>
10+
</h2>
811
</div>
912

1013
<!-- Scrollable Content -->
11-
<div class="flex flex-col flex-1 max-h-[28.125rem] overflow-y-auto">
14+
<div class="flex flex-col flex-1 max-h-[28.125rem] overflow-y-auto p-2">
1215
@if (pendingActions().length > 0) {
1316
<div class="flex flex-col gap-3" data-testid="dashboard-pending-actions-list">
1417
@for (item of pendingActions(); track $index) {
1518
<div
1619
class="p-4 border-0 rounded-lg shadow-md"
1720
[ngClass]="{
1821
'bg-yellow-50': item.color === 'amber',
19-
'bg-blue-50': item.color === 'blue',
20-
'bg-green-50': item.color === 'green',
21-
'bg-purple-50': item.color === 'purple',
22+
'bg-blue-200': item.color === 'blue',
23+
'bg-green-200': item.color === 'green',
24+
'bg-purple-200': item.color === 'purple',
2225
}"
2326
[attr.data-testid]="'dashboard-pending-actions-item-' + item.type">
2427
<!-- Header with Type -->
2528
<div class="flex items-start justify-between mb-3">
26-
<div class="flex items-center gap-2">
29+
<div
30+
class="flex items-center gap-2 px-2 py-1 rounded-md text-xs"
31+
[ngClass]="{
32+
'bg-yellow-200': item.color === 'amber',
33+
'bg-blue-300': item.color === 'blue',
34+
'bg-green-300': item.color === 'green',
35+
'bg-purple-300': item.color === 'purple',
36+
}">
2737
<div
2838
[ngClass]="{
2939
'text-yellow-700': item.color === 'amber',
3040
'text-blue-700': item.color === 'blue',
3141
'text-green-700': item.color === 'green',
3242
'text-purple-700': item.color === 'purple',
3343
}">
34-
<i [ngClass]="[item.icon, 'w-4', 'h-4']"></i>
44+
<i [ngClass]="[item.icon, 'w-3']"></i>
3545
</div>
3646
<span
3747
class="text-xs font-medium"
3848
[ngClass]="{
39-
'text-yellow-700': item.color === 'amber',
49+
'text-amber-700': item.color === 'amber',
4050
'text-blue-700': item.color === 'blue',
4151
'text-green-700': item.color === 'green',
4252
'text-purple-700': item.color === 'purple',
@@ -48,14 +58,7 @@ <h2 class="py-1">Pending Actions</h2>
4858

4959
<!-- Action Text -->
5060
<div class="mb-4">
51-
<p
52-
class="text-sm font-medium"
53-
[ngClass]="{
54-
'text-yellow-900': item.color === 'amber',
55-
'text-blue-900': item.color === 'blue',
56-
'text-green-900': item.color === 'green',
57-
'text-purple-900': item.color === 'purple',
58-
}">
61+
<p class="text-base font-normal">
5962
{{ item.text }}
6063
</p>
6164
</div>
@@ -65,7 +68,7 @@ <h2 class="py-1">Pending Actions</h2>
6568
<lfx-button
6669
size="small"
6770
class="w-full"
68-
styleClass="w-full"
71+
styleClass="w-full text-sm"
6972
severity="secondary"
7073
rel="noopener noreferrer"
7174
[link]="true"
@@ -77,7 +80,7 @@ <h2 class="py-1">Pending Actions</h2>
7780
} @else {
7881
<lfx-button
7982
size="small"
80-
class="w-full"
83+
class="w-full text-sm"
8184
styleClass="w-full text-sm"
8285
severity="secondary"
8386
(onClick)="handleActionClick(item)"

apps/lfx-one/src/app/modules/meetings/components/meeting-type-selection/meeting-type-selection.component.html

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,6 @@
22
<!-- SPDX-License-Identifier: MIT -->
33

44
<div class="flex flex-col gap-6">
5-
@if (projectOptions().length > 0) {
6-
<div data-testid="meeting-project-selector">
7-
<label for="project-select" class="text-sm font-semibold text-slate-900 mb-2 block">Select Project</label>
8-
<p class="text-xs text-slate-500 mb-3">Choose which project this meeting is for. If not selected, the current project will be used.</p>
9-
<lfx-select
10-
class="w-full"
11-
styleClass="w-full"
12-
id="project-select"
13-
[form]="form()"
14-
control="selectedProjectUid"
15-
[options]="projectOptions()"
16-
placeholder="Select a project (optional)"
17-
[showClear]="true"
18-
data-testid="project-select-dropdown" />
19-
</div>
20-
}
21-
225
<div class="flex flex-col">
236
<div class="text-center mb-4">
247
<h2 class="mb-2">Meeting Type & Privacy</h2>

apps/lfx-one/src/app/modules/meetings/components/meeting-type-selection/meeting-type-selection.component.ts

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule } from '@angular/common';
5-
import { HttpParams } from '@angular/common/http';
65
import { Component, computed, inject, input } from '@angular/core';
7-
import { toSignal } from '@angular/core/rxjs-interop';
86
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
97
import { MessageComponent } from '@components/message/message.component';
10-
import { SelectComponent } from '@components/select/select.component';
118
import { ToggleComponent } from '@components/toggle/toggle.component';
129
import { lfxColors } from '@lfx-one/shared/constants';
1310
import { MeetingType } from '@lfx-one/shared/enums';
14-
import { Project } from '@lfx-one/shared/interfaces';
15-
import { ProjectContextService } from '@services/project-context.service';
16-
import { ProjectService } from '@services/project.service';
11+
import { PersonaService } from '@services/persona.service';
1712
import { TooltipModule } from 'primeng/tooltip';
18-
import { map, of } from 'rxjs';
1913

2014
interface MeetingTypeInfo {
2115
icon: string;
@@ -27,27 +21,15 @@ interface MeetingTypeInfo {
2721
@Component({
2822
selector: 'lfx-meeting-type-selection',
2923
standalone: true,
30-
imports: [CommonModule, ReactiveFormsModule, MessageComponent, SelectComponent, ToggleComponent, TooltipModule],
24+
imports: [CommonModule, ReactiveFormsModule, MessageComponent, ToggleComponent, TooltipModule],
3125
templateUrl: './meeting-type-selection.component.html',
3226
})
3327
export class MeetingTypeSelectionComponent {
34-
private readonly projectContextService = inject(ProjectContextService);
35-
private readonly projectService = inject(ProjectService);
28+
private readonly personaService = inject(PersonaService);
3629

3730
// Form group input from parent
3831
public readonly form = input.required<FormGroup>();
3932

40-
// Child projects signal
41-
public childProjects = this.initializeChildProjects();
42-
43-
// Map projects to select options
44-
public projectOptions = computed(() => {
45-
return this.childProjects().map((project: Project) => ({
46-
label: project.name,
47-
value: project.uid,
48-
}));
49-
});
50-
5133
// Meeting type options with their info - computed for template efficiency
5234
// Filtered based on user role (currently showing only maintainers, technical, and other)
5335
public readonly meetingTypeOptions = computed(() => {
@@ -62,7 +44,8 @@ export class MeetingTypeSelectionComponent {
6244

6345
// Filter to only show maintainers, technical, and other meeting types
6446
const allowedTypes = [MeetingType.MAINTAINERS, MeetingType.TECHNICAL, MeetingType.OTHER];
65-
const filteredOptions = allOptions.filter((option) => allowedTypes.includes(option.value));
47+
const filteredOptions =
48+
this.personaService.currentPersona() === 'maintainer' ? allOptions.filter((option) => allowedTypes.includes(option.value)) : allOptions;
6649

6750
return filteredOptions.map((option) => ({
6851
...option,
@@ -133,24 +116,4 @@ export class MeetingTypeSelectionComponent {
133116
this.form().get('meeting_type')?.setValue(meetingType);
134117
this.form().get('meeting_type')?.markAsTouched();
135118
}
136-
137-
// Get child projects for the current project
138-
private initializeChildProjects() {
139-
const currentProject = this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation();
140-
141-
if (!currentProject) {
142-
return toSignal(of([]), { initialValue: [] });
143-
}
144-
145-
const params = new HttpParams().set('tags', `parent_uid:${currentProject.uid}`);
146-
return toSignal(
147-
this.projectService.getProjects(params).pipe(
148-
map((projects: Project[]) => {
149-
// Filter out the current project from the list
150-
return projects.filter((project) => project.uid !== currentProject.uid && project.writer);
151-
})
152-
),
153-
{ initialValue: [] }
154-
);
155-
}
156119
}

apps/lfx-one/src/app/modules/meetings/meeting-manage/meeting-manage.component.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ export class MeetingManageComponent {
9494
// Initialize meeting attachments with refresh capability
9595
private attachmentsRefresh$ = new BehaviorSubject<void>(undefined);
9696
public attachments = this.initializeAttachments();
97-
// Stepper state
97+
// Stepper state - internal step tracking for create mode
98+
private internalStep = signal<number>(1);
9899
public currentStep = toSignal(of(1), { initialValue: 1 });
99100
public readonly totalSteps = TOTAL_STEPS;
100101
// Form state
@@ -124,18 +125,25 @@ export class MeetingManageComponent {
124125
);
125126

126127
public constructor() {
127-
// Initialize step from query parameter
128+
// Initialize step based on mode
129+
// In edit mode, read from query parameters
130+
// In create mode, use internal step tracking
128131
this.currentStep = toSignal(
129132
this.route.queryParamMap.pipe(
130133
switchMap((params) => {
131-
const stepParam = params.get('step');
132-
if (stepParam) {
133-
const step = parseInt(stepParam, 10);
134-
if (step >= 1 && step <= this.totalSteps) {
135-
return of(step);
134+
// In edit mode, use query parameters
135+
if (this.isEditMode()) {
136+
const stepParam = params.get('step');
137+
if (stepParam) {
138+
const step = parseInt(stepParam, 10);
139+
if (step >= 1 && step <= this.totalSteps) {
140+
return of(step);
141+
}
136142
}
143+
return of(1);
137144
}
138-
return of(1);
145+
// In create mode, use internal step signal
146+
return toObservable(this.internalStep);
139147
})
140148
),
141149
{ initialValue: 1 }
@@ -169,7 +177,13 @@ export class MeetingManageComponent {
169177

170178
public goToStep(step: number | undefined): void {
171179
if (step !== undefined && this.canNavigateToStep(step)) {
172-
this.router.navigate([], { queryParams: { step: step } });
180+
if (this.isEditMode()) {
181+
// In edit mode, update query params
182+
this.router.navigate([], { queryParams: { step: step } });
183+
} else {
184+
// In create mode, update internal step
185+
this.internalStep.set(step);
186+
}
173187
this.scrollToStepper();
174188
}
175189
}
@@ -182,15 +196,27 @@ export class MeetingManageComponent {
182196
this.generateMeetingTitle();
183197
}
184198

185-
this.router.navigate([], { queryParams: { step: next } });
199+
if (this.isEditMode()) {
200+
// In edit mode, update query params
201+
this.router.navigate([], { queryParams: { step: next } });
202+
} else {
203+
// In create mode, update internal step
204+
this.internalStep.set(next);
205+
}
186206
this.scrollToStepper();
187207
}
188208
}
189209

190210
public previousStep(): void {
191211
const previous = this.currentStep() - 1;
192212
if (previous >= 1) {
193-
this.router.navigate([], { queryParams: { step: previous } });
213+
if (this.isEditMode()) {
214+
// In edit mode, update query params
215+
this.router.navigate([], { queryParams: { step: previous } });
216+
} else {
217+
// In create mode, update internal step
218+
this.internalStep.set(previous);
219+
}
194220
this.scrollToStepper();
195221
}
196222
}
@@ -389,7 +415,7 @@ export class MeetingManageComponent {
389415
}
390416

391417
return {
392-
project_uid: formValue.selectedProjectUid || this.project()?.uid,
418+
project_uid: this.projectContextService.selectedProject()?.uid || this.projectContextService.selectedFoundation()?.uid || '',
393419
title: formValue.title,
394420
description: formValue.description || '',
395421
start_time: startDateTime,
@@ -814,7 +840,6 @@ export class MeetingManageComponent {
814840
return new FormGroup(
815841
{
816842
// Step 1: Meeting Type
817-
selectedProjectUid: new FormControl(''),
818843
meeting_type: new FormControl('', [Validators.required]),
819844
visibility: new FormControl(MeetingVisibility.PRIVATE),
820845
restricted: new FormControl(false),

apps/lfx-one/src/app/shared/components/persona-selector/persona-selector.component.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ export class PersonaSelectorComponent {
4444
.subscribe((value: PersonaType) => {
4545
if (value === 'board-member') {
4646
// TODO: DEMO - Remove when proper permissions are implemented
47-
this.projectContextService.setFoundation({
48-
uid: 'tlf',
49-
name: 'The Linux Foundation',
50-
slug: 'tlf',
51-
});
47+
const tlfProject = this.projectContextService.availableProjects.find((p) => p.slug === 'tlf');
48+
if (tlfProject) {
49+
this.projectContextService.setFoundation({
50+
uid: tlfProject.uid,
51+
name: tlfProject.name,
52+
slug: tlfProject.slug,
53+
});
54+
}
5255
}
5356

5457
this.personaService.setPersona(value);

apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
<lfx-persona-selector></lfx-persona-selector>
4646
</div>
4747

48+
<!-- Clear Cache nav item (feature flag controlled) -->
49+
@if (showClearCacheButton()) {
50+
<ng-container *ngTemplateOutlet="menuItem; context: { $implicit: clearCacheMenuItem }"></ng-container>
51+
}
52+
4853
@if (footerItems().length > 0) {
4954
@for (item of footerItemsWithTestIds(); track item.label) {
5055
<ng-container *ngTemplateOutlet="menuItem; context: { $implicit: item }"></ng-container>

0 commit comments

Comments
 (0)