Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ada825d
feat(committees): add committee detail shell with core overview tab
manishdixitlfx Mar 12, 2026
f2ed216
feat(committees): add overview tab type-specific cards and BFF sub-re…
manishdixitlfx Mar 12, 2026
da55ff5
fix(committees): address Copilot review findings on PR #295
manishdixitlfx Mar 12, 2026
8bf9c43
feat(committees): wire members tab with full CRUD and governance fields
manishdixitlfx Mar 12, 2026
ab51db6
feat(committees): add invite, join and application review dialogs
manishdixitlfx Mar 12, 2026
565ab1c
fix(committees): address CodeRabbit and Copilot review findings on PR…
manishdixitlfx Mar 12, 2026
30fffb7
feat(committees): add committee settings tab
manishdixitlfx Mar 12, 2026
5450942
feat(committees): add upcoming meetings tab to committee detail
manishdixitlfx Mar 12, 2026
b52ed22
fix(committees): wire meetings endpoint and align JoinMode enum values
manishdixitlfx Mar 16, 2026
4ac0a8e
fix(committees): fix meetings tab data source, permissions, and acces…
manishdixitlfx Mar 16, 2026
4c5f35c
feat(committees): add committee create and edit form
manishdixitlfx Mar 12, 2026
05c7e45
feat(committees): add votes tab with open/closed votes and resolutions
manishdixitlfx Mar 15, 2026
0bee947
feat(committees): integrate vote results drawer into committee votes tab
manishdixitlfx Mar 15, 2026
0a7344e
refactor(committees): replace card-based votes tab with reusable tabl…
manishdixitlfx Mar 15, 2026
5c3f386
fix(committees): address Copilot review findings
manishdixitlfx Mar 15, 2026
e54fa57
fix(committees): reset totalRecords on count fetch error
manishdixitlfx Mar 15, 2026
c4ef034
feat(committees): add observers stat card to group detail overview
manishdixitlfx Mar 15, 2026
6993ec4
feat(committees): add key topics chip list to overview
manishdixitlfx Mar 15, 2026
1a0c7c6
feat(committees): surface last meeting summary on overview tab
manishdixitlfx Mar 15, 2026
34c11a5
fix(committees): address Copilot review findings for overview enhance…
manishdixitlfx Mar 15, 2026
44c5a42
fix(committees): remove unused map import and decouple meetings from …
manishdixitlfx Mar 16, 2026
c5f5499
feat(committees): add Surveys tab with BFF endpoint and Create Survey…
manishdixitlfx Mar 16, 2026
ebfeb18
chore: remove accidentally added skills dir
manishdixitlfx Mar 17, 2026
602fb06
fix(committees): address review findings on surveys tab PR
manishdixitlfx Mar 17, 2026
b1e3ada
fix(committees): address review findings on surveys tab PR
manishdixitlfx Mar 17, 2026
a386498
fix(committees): reset survey drawer state when committeeUid changes
manishdixitlfx Mar 17, 2026
e1f79de
feat(committees): add committee detail shell with dashboard sub-resou…
manishdixitlfx Mar 18, 2026
1973bb1
feat(committees): extract members and votes tabs for separate PRs
manishdixitlfx Mar 18, 2026
fd66d0d
fix(committees): address review feedback on detail shell
manishdixitlfx Mar 18, 2026
f3abd69
feat(committees): add votes tab with reusable table UI and results dr…
manishdixitlfx Mar 18, 2026
a8946b3
feat(committees): add assign leadership dialog for chair and co-chair…
manishdixitlfx Mar 18, 2026
daccba6
feat(committees): add members tab with full CRUD and governance fields
manishdixitlfx Mar 18, 2026
8189248
fix(committees): address review feedback on leadership dialog
manishdixitlfx Mar 19, 2026
85606e0
fix(committees): address review feedback on detail overview PR
manishdixitlfx Mar 19, 2026
4a22ae4
fix(committees): address review feedback on votes tab component
manishdixitlfx Mar 19, 2026
b988e1e
fix(committees): address review feedback on members tab PR
manishdixitlfx Mar 19, 2026
649f316
Merge branch 'main' into feat/LFXV2-committees-detail-overview
manishdixitlfx Mar 19, 2026
9b2653f
Merge branch 'feat/LFXV2-committees-detail-overview' into feat/LFXV2-…
manishdixitlfx Mar 19, 2026
3ceb7fd
Merge pull request #336 from linuxfoundation/feat/LFXV2-committees-me…
manishdixitlfx Mar 19, 2026
bb322d8
Merge branch 'feat/LFXV2-committees-detail-overview' into feat/LFXV2-…
manishdixitlfx Mar 19, 2026
d08453c
Merge branch 'feat/LFXV2-committees-detail-overview' into feat/LFXV2-…
manishdixitlfx Mar 19, 2026
145e7e5
Merge pull request #334 from linuxfoundation/feat/LFXV2-committees-le…
manishdixitlfx Mar 19, 2026
9a5b3cf
Merge pull request #337 from linuxfoundation/feat/LFXV2-committees-vo…
manishdixitlfx Mar 19, 2026
2721250
fix(committees): resolve merge artifacts and address round 2 review o…
manishdixitlfx Mar 19, 2026
7574494
fix(committees): address round 3 review findings on detail overview
manishdixitlfx Mar 19, 2026
cb0640a
fix(committees): address round 4 review findings on detail overview
manishdixitlfx Mar 19, 2026
2d78552
fix(committees): fix misleading vote zeros and add error logging to s…
manishdixitlfx Mar 19, 2026
da1d414
fix(committees): address round 5 review findings on detail overview
manishdixitlfx Mar 19, 2026
83a9d08
feat(committees): add surveys tab with BFF endpoint and create survey…
manishdixitlfx Mar 18, 2026
259bd24
fix(committees): address review feedback on surveys tab component
manishdixitlfx Mar 19, 2026
696c5a0
fix(committees): address review findings on surveys tab
manishdixitlfx Mar 19, 2026
2a66b38
fix(committees): address review findings on surveys tab PR
manishdixitlfx Mar 19, 2026
0073545
Merge pull request #332 from linuxfoundation/feat/LFXV2-committees-su…
manishdixitlfx Mar 19, 2026
212127a
fix(committees): address round 6 review findings on detail overview
manishdixitlfx Mar 19, 2026
46bdf90
fix(committees): remove endpoints for non-existent upstream APIs
manishdixitlfx Mar 19, 2026
3fe8e02
fix(committees): remove unused CommitteeSurveysListComponent import
manishdixitlfx Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ export class CommitteeManageComponent {
// Update existing committee
this.committeeService.updateCommittee(this.committeeId()!, committeeData).subscribe({
next: () => this.handleCommitteeSuccess('updated'),
error: (error) => this.handleCommitteeError(error, 'update'),
error: (err: unknown) => this.handleCommitteeError('update', err),
});
} else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error swallowing — All console.error calls were removed from error handlers here (and in several other places in this file) but no replacement logging or telemetry was added. If there's a global HTTP error interceptor that handles this, this is fine — but if not, debugging production issues will be very difficult since errors are silently discarded. Please verify centralized error handling covers these cases, or consider keeping at minimum a structured log call.

// Create new committee
this.committeeService.createCommittee(committeeData).subscribe({
next: (committee) => this.handleCreateSuccess(committee),
error: (error) => this.handleCommitteeError(error, 'create'),
error: (err: unknown) => this.handleCommitteeError('create', err),
});
}
}
Expand Down Expand Up @@ -246,8 +246,7 @@ export class CommitteeManageComponent {
this.showMemberOperationToast(totalSuccess, totalFailed, totalSuccess + totalFailed);
this.router.navigate(['/groups']);
},
error: (error) => {
console.error('Error processing member changes:', error);
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
Expand Down Expand Up @@ -333,8 +332,7 @@ export class CommitteeManageComponent {
// Navigate back to committees list
this.router.navigate(['/groups']);
},
error: (error: unknown) => {
console.error('Error saving committee and members:', error);
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
Expand Down Expand Up @@ -455,10 +453,11 @@ export class CommitteeManageComponent {
}
}

private handleCommitteeError(error: unknown, operation: 'create' | 'update'): void {
console.error(`Error ${operation} committee:`, error);
private handleCommitteeError(operation: 'create' | 'update', error?: unknown): void {
this.submitting.set(false);

console.error(`Failed to ${operation} committee:`, error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover console.error.

Problem: handleCommitteeError still calls console.error here.

Why it's a problem: Every other error handler modified in this PR removed console.error in favor of toast-only feedback (e.g., createMemberOperation, the two error: () handlers above). Leaving this one creates an inconsistency and leaks error details to the browser console in production.

Fix: Remove the console.error line. The messageService.add() toast below already handles user-facing feedback. If server-side visibility is needed, the backend controller already logs the error.


this.messageService.add({
severity: 'error',
summary: 'Error',
Expand Down Expand Up @@ -570,10 +569,7 @@ export class CommitteeManageComponent {
private createMemberOperation(type: string, operation: () => Observable<unknown>) {
return operation().pipe(
switchMap(() => of({ type, success: 1, failed: 0 })),
catchError((error) => {
console.error(`Error ${type} member:`, error);
return of({ type, success: 0, failed: 1 });
})
catchError(() => of({ type, success: 0, failed: 1 }))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ <h2 class="text-lg font-semibold text-gray-900 mb-2">Something Went Wrong</h2>
<lfx-breadcrumb [model]="breadcrumbItems()" data-testid="committee-view-breadcrumb"></lfx-breadcrumb>
<div class="flex items-center gap-3">
<h1 data-testid="committee-view-name">{{ committee()?.name }}</h1>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Was removing the lock icon (fa-light fa-lock) for private/non-public committees intentional? Previously there was an @if (!committee()?.public) block showing a lock icon next to the committee name here. It's been removed with no replacement or mention in the PR description.

@if (!committee()?.public) {
<i class="fa-light fa-lock w-4 h-4 text-gray-400"></i>
}
</div>
@if (committee()?.description) {
<p class="text-gray-500 max-w-2xl" data-testid="committee-view-description">{{ committee()?.description }}</p>
Expand All @@ -54,7 +51,7 @@ <h1 data-testid="committee-view-name">{{ committee()?.name }}</h1>
}
<span class="text-xs text-gray-400">Created {{ committee()?.created_at | date: 'MMM d, y' }}</span>
@if (committee()?.updated_at) {
<span class="text-xs text-gray-400">· Updated {{ committee()?.updated_at | date: 'MMM d, y' }}</span>
<span class="text-xs text-gray-400">&middot; Updated {{ committee()?.updated_at | date: 'MMM d, y' }}</span>
}
</div>
</div>
Expand All @@ -67,7 +64,7 @@ <h1 data-testid="committee-view-name">{{ committee()?.name }}</h1>
}
</div>

<!-- Tabs -->
<!-- Tab Bar -->
<div class="flex gap-1 border-b" data-testid="committee-view-tabs">
<button
(click)="activeTab.set('overview')"
Expand Down Expand Up @@ -186,7 +183,4 @@ <h1 data-testid="committee-view-name">{{ committee()?.name }}</h1>
}
</div>
}

<!-- Confirmation Dialog -->
<p-confirmDialog></p-confirmDialog>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { BreadcrumbComponent } from '@components/breadcrumb/breadcrumb.component';
import { ButtonComponent } from '@components/button/button.component';
import { TagComponent } from '@components/tag/tag.component';
import { RouteLoadingComponent } from '@components/loading/route-loading.component';
import { Committee, CommitteeMemberVisibility, getCommitteeCategorySeverity, TagSeverity } from '@lfx-one/shared';
import { CommitteeService } from '@services/committee.service';
import { RouteLoadingComponent } from '@components/loading/route-loading.component';
import { MenuItem, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { catchError, combineLatest, finalize, of, switchMap } from 'rxjs';

import { CommitteeOverviewComponent } from '../components/committee-overview/committee-overview.component';
Expand All @@ -25,7 +24,6 @@ type CommitteeTab = 'overview' | 'members' | 'votes' | 'meetings' | 'surveys' |
BreadcrumbComponent,
ButtonComponent,
TagComponent,
ConfirmDialogModule,
RouterLink,
RouteLoadingComponent,
DatePipe,
Expand Down Expand Up @@ -77,6 +75,17 @@ export class CommitteeViewComponent {
this.refresh.update((v) => v + 1);
}

public createSurvey(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code — createSurvey() is never called.

Problem: This method is defined but never referenced from the template or any other component.

Why it's a problem: Dead code adds confusion — future developers may assume it's wired up somewhere.

Fix: Either remove the method entirely, or if it's intended for a "Create Survey" button in the surveys tab, wire it to a button in committee-view.component.html with a click handler.

const committee = this.committee();
if (!committee) return;
this.router.navigate(['/surveys/create'], {
queryParams: {
committee_uid: committee.uid,
committee_name: committee.name,
},
});
}

// -- Private initializer functions --
private initializeCommittee(): Signal<Committee | null> {
return toSignal(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<form [formGroup]="form" (ngSubmit)="onSubmit()" class="flex flex-col gap-6">
<!-- Member Selection -->
<div>
<label for="member-select" class="block text-sm font-medium text-gray-700 mb-1">
Select Member <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<lfx-select
size="small"
[form]="form"
control="member_uid"
[options]="memberOptions"
[filter]="true"
filterBy="label"
filterPlaceholder="Search by name or organization..."
placeholder="Choose a member"
styleClass="w-full"
appendTo="body"
[showClear]="true"
id="member-select"
data-testid="assign-leadership-member-select">
</lfx-select>
</div>

<!-- Effective Date -->
<div>
<label for="elected-date" class="block text-sm font-medium text-gray-700 mb-1">Effective Date</label>
<lfx-calendar
[form]="form"
control="elected_date"
placeholder="Select effective date"
[showButtonBar]="true"
appendTo="body"
id="elected-date"
data-testid="assign-leadership-elected-date">
</lfx-calendar>
</div>

<!-- Form Actions -->
<div class="flex justify-between pt-6 border-t">
<div>
@if (currentLeader) {
<lfx-button
[label]="'Remove ' + roleLabel"
severity="danger"
[outlined]="true"
[loading]="removing()"
[disabled]="submitting() || removing()"
(onClick)="onRemove()"
size="small"
type="button"
data-testid="assign-leadership-remove-btn">
</lfx-button>
}
</div>
<div class="flex gap-3">
<lfx-button
label="Cancel"
severity="secondary"
[outlined]="true"
(onClick)="onCancel()"
size="small"
type="button"
data-testid="assign-leadership-cancel-btn">
</lfx-button>
<lfx-button
[label]="'Assign ' + roleLabel"
[loading]="submitting()"
[disabled]="!form.value.member_uid || submitting() || removing()"
type="submit"
size="small"
data-testid="assign-leadership-submit-btn">
</lfx-button>
</div>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, inject, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ButtonComponent } from '@components/button/button.component';
import { CalendarComponent } from '@components/calendar/calendar.component';
import { SelectComponent } from '@components/select/select.component';
import { CommitteeMemberRole } from '@lfx-one/shared/enums';
import { Committee, CommitteeLeadership, CommitteeMember, CreateCommitteeMemberRequest, LeadershipRole } from '@lfx-one/shared/interfaces';
import { formatDateToISOString } from '@lfx-one/shared/utils';
import { CommitteeService } from '@services/committee.service';
import { MessageService } from 'primeng/api';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { catchError, of, switchMap } from 'rxjs';

@Component({
selector: 'lfx-assign-leadership-dialog',
imports: [ReactiveFormsModule, ButtonComponent, CalendarComponent, SelectComponent],
templateUrl: './assign-leadership-dialog.component.html',
})
export class AssignLeadershipDialogComponent {
private readonly config = inject(DynamicDialogConfig);
private readonly dialogRef = inject(DynamicDialogRef);
private readonly committeeService = inject(CommitteeService);
private readonly messageService = inject(MessageService);

public readonly role: LeadershipRole;
public readonly committee: Committee;
public readonly members: CommitteeMember[];
public readonly currentLeader: CommitteeLeadership | null;
public readonly roleLabel: string;

public form: FormGroup;

public submitting = signal(false);
public removing = signal(false);

public memberOptions: { label: string; value: string }[];

public constructor() {
this.role = this.config.data?.role ?? 'chair';
this.committee = this.config.data?.committee;
this.members = this.config.data?.members ?? [];
this.currentLeader = this.config.data?.currentLeader ?? null;

if (!this.committee) {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Committee data is missing. Please try again.',
});
this.dialogRef.close();
}

this.roleLabel = this.role === 'chair' ? 'Chair' : 'Co-Chair';

this.form = new FormGroup({
member_uid: new FormControl(this.currentLeader?.uid ?? null),
elected_date: new FormControl(this.currentLeader?.elected_date ? new Date(this.currentLeader.elected_date) : null),
});

this.memberOptions = this.initializeMemberOptions();
}

public onCancel(): void {
this.dialogRef.close();
}

public onSubmit(): void {
const memberUid = this.form.value.member_uid;
if (!memberUid) return;

const selectedMember = this.members.find((m) => m.uid === memberUid);
if (!selectedMember) return;

this.submitting.set(true);

const leadership: CommitteeLeadership = {
uid: selectedMember.uid,
first_name: selectedMember.first_name,
last_name: selectedMember.last_name,
email: selectedMember.email,
elected_date: formatDateToISOString(this.form.value.elected_date) || undefined,
organization: selectedMember.organization?.name,
};

const roleName = this.role === 'chair' ? CommitteeMemberRole.CHAIR : CommitteeMemberRole.VICE_CHAIR;
const roleUpdate: Partial<CreateCommitteeMemberRequest> = {
role: {
name: roleName,
start_date: formatDateToISOString(this.form.value.elected_date) || null,
},
};

this.committeeService
.updateCommitteeMember(this.committee.uid, memberUid, roleUpdate)
.pipe(
switchMap(() => {
if (this.currentLeader && this.currentLeader.uid !== memberUid) {
return this.committeeService
.updateCommitteeMember(this.committee.uid, this.currentLeader.uid, {
role: { name: CommitteeMemberRole.NONE },
})
.pipe(
catchError(() => {
// Assignment succeeded but clearing the previous leader's role failed.
// Warn the user so they can resolve the duplicate manually.
this.messageService.add({
severity: 'warn',
summary: 'Partial Update',
detail: `New ${this.roleLabel.toLowerCase()} assigned, but the previous leader's role could not be cleared automatically.`,
});
return of(null);
})
);
}
return of(null);
})
)
.subscribe({
next: () => {
this.submitting.set(false);
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: `${this.roleLabel} assigned successfully`,
});
this.dialogRef.close({ role: this.role, leadership });
},
error: () => {
this.submitting.set(false);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: `Failed to assign ${this.roleLabel.toLowerCase()}`,
});
this.dialogRef.close();
},
});
}

public onRemove(): void {
if (!this.currentLeader) return;

this.removing.set(true);

this.committeeService.updateCommitteeMember(this.committee.uid, this.currentLeader.uid, { role: { name: CommitteeMemberRole.NONE } }).subscribe({
next: () => {
this.removing.set(false);
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: `${this.roleLabel} removed`,
});
this.dialogRef.close({ role: this.role, leadership: null });
},
error: () => {
this.removing.set(false);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: `Failed to remove ${this.roleLabel.toLowerCase()}`,
});
},
});
}

private initializeMemberOptions(): { label: string; value: string }[] {
return this.members.map((m) => ({
label: `${m.first_name} ${m.last_name}${m.organization?.name ? ` — ${m.organization.name}` : ''}`,
value: m.uid,
}));
}
}
Loading
Loading