Skip to content

Commit e997afd

Browse files
feat(committees): add quick-filter chips on Members tab (#684)
* feat(committees): add quick-filter chips on Members tab Add a row of clickable filter chips above the existing search/filter row on the committee Members tab. Four chips ship in this PR: - All (n) — default, shows all members - Voting Reps (n) — filters to members with voting_status = "Voting Rep" - Observers (n) — filters to members with voting_status = "Observer" - Chairs (n) — filters to members with role.name in {Chair, Vice Chair} Selecting a chip resets the dropdown filters (role / votingStatus / organization) so the user starts from a known state; search and dropdowns still compose on top of the chip selection. The "At Risk" chip is intentionally deferred — it depends on attendance data from LFXV2-1705. The All Voting Status dropdown is kept so users can still reach the long-tail statuses (Alternate Voting Rep, Emeritus, None) that don't have dedicated chips. LFXV2-1717 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(review): address PR #684 review feedback Address review comments from @coderabbitai and @copilot-pull-request-reviewer: - committee-members.component.html: add type="button" and [attr.aria-pressed] to each chip button so assistive tech can announce which filter is active and to defend against accidental form submit. Addresses both reviewers' a11y comment (toggle group needs aria-pressed). - committee-members.component.ts: replace hardcoded 'Voting Rep', 'Observer', 'Chair', 'Vice Chair' string literals in the chip counts and chipFilteredMembers computed with CommitteeMemberVotingStatus and CommitteeMemberRole enum references from @lfx-one/shared/enums. No runtime change — the enum values are these same strings — but the component will now break-build if the canonical enums drift, instead of silently mis-filtering. Per @copilot-pull-request-reviewer. Resolves 3 review threads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(review): address PR #684 review feedback from @dealako Address two observations from @dealako: - packages/shared/src/interfaces/committee.interface.ts: introduce a CommitteeMemberFilterChip type alias ('all' | 'voting' | 'observers' | 'chairs') and re-use it in three places where it was previously duplicated inline (memberFilterChip signal, chipFilteredMembers switch parameter, selectChip method parameter). Adding a new chip is now a one-line change. Per .claude/rules/component-organization.md §5 and @dealako's first observation. - committee-members.component.{ts,html}: replace the four near-identical chip button blocks with an @for loop over a chipConfig() computed that carries { key, label, count } for each chip. Cuts the template roughly in half and eliminates copy-paste drift risk if a future styling change forgets one button. Per @dealako's second observation. The "Chairs" label is intentionally kept short (covers both Chair and Vice Chair) — chip labels favor brevity and the leadership intent is clear from context. Responded to @dealako's design-question observation on the PR. Resolves @dealako's review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(review): address PR #684 CodeRabbit nitpick — wrap chip row committee-members.component.html: add `flex-wrap` to the chip row container so the chips wrap onto a second line on narrow viewports instead of horizontally clipping. Pure presentational change; no behavior difference at desktop widths. Per @coderabbitai's quick-win nitpick. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(review): address PR #684 review feedback from @MRashad26 Address three convention-compliance comments from @MRashad26 citing .claude/rules/component-organization.md §3 and §5: - packages/shared/src/interfaces/committee.interface.ts: collapse the 3-line JSDoc on CommitteeMemberFilterChip into a single line so it matches the project's "no comments unless WHY is non-obvious" convention. - packages/shared/src/interfaces/committee.interface.ts: extract the previously-inline `{ key, label, count }` chip shape into a named CommitteeMemberFilterChipConfig interface, per §5 (interfaces belong in @lfx-one/shared/interfaces, never inline in component files). - committee-members.component.ts: move the five computed signals (votingRepCount, observerCount, chairCount, chipConfig, chipFilteredMembers) out of the "Simple writable signals" block. The three simple counts now live inline under "Computed signals" next to canManageMembers/isMembersVisible; chipConfig and chipFilteredMembers — both non-trivial (array literal / switch) — now use the private init function pattern (initChipConfig, initChipFilteredMembers) per §3, mirroring the existing initializeFilteredMembers() pattern in the same file. No runtime behavior change — purely structural and typing cleanup so the class layout matches the project's organization rules. Resolves 3 review threads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> --------- Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a1a0724 commit e997afd

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@
77
@if (!isMembersVisible()) {
88
<div class="text-sm text-gray-500 text-center py-6" data-testid="members-hidden-placeholder">Member list is not publicly visible for this group.</div>
99
} @else {
10+
<!-- Quick Filter Chips -->
11+
<div class="flex flex-wrap items-center gap-2 mb-4" data-testid="members-filter-chips">
12+
@for (chip of chipConfig(); track chip.key) {
13+
<button
14+
type="button"
15+
class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
16+
[attr.aria-pressed]="memberFilterChip() === chip.key"
17+
[class]="memberFilterChip() === chip.key ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
18+
(click)="selectChip(chip.key)"
19+
[attr.data-testid]="'members-chip-' + chip.key">
20+
{{ chip.label }} ({{ chip.count }})
21+
</button>
22+
}
23+
</div>
24+
1025
<!-- Search, Filter Controls, and Add Member -->
1126
<div class="flex flex-col md:flex-row md:items-center gap-3 md:gap-4 mb-6">
1227
<!-- Search Input -->

apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ import { SelectComponent } from '@components/select/select.component';
1515
import { TableComponent } from '@components/table/table.component';
1616
import { TagComponent } from '@components/tag/tag.component';
1717
import { COMMITTEE_LABEL } from '@lfx-one/shared/constants';
18-
import { Committee, CommitteeMember, CommitteePermissionLevel, CommitteeUser, TagSeverity } from '@lfx-one/shared/interfaces';
18+
import { CommitteeMemberRole, CommitteeMemberVotingStatus } from '@lfx-one/shared/enums';
19+
import {
20+
Committee,
21+
CommitteeMember,
22+
CommitteeMemberFilterChip,
23+
CommitteeMemberFilterChipConfig,
24+
CommitteePermissionLevel,
25+
CommitteeUser,
26+
TagSeverity,
27+
} from '@lfx-one/shared/interfaces';
1928
import { CommitteeService } from '@services/committee.service';
2029
import { ConfirmationService, MenuItem, MessageService } from 'primeng/api';
2130
import { ConfirmDialogModule } from 'primeng/confirmdialog';
@@ -65,6 +74,7 @@ export class CommitteeMembersComponent implements OnInit {
6574
// Simple writable signals
6675
public selectedMember = signal<CommitteeMember | null>(null);
6776
public isDeleting = signal<boolean>(false);
77+
public memberFilterChip = signal<CommitteeMemberFilterChip>('all');
6878
public memberActionMenuItems: MenuItem[] = [];
6979
public committeeLabel = COMMITTEE_LABEL;
7080

@@ -77,6 +87,19 @@ export class CommitteeMembersComponent implements OnInit {
7787
if (!committee) return false;
7888
return committee.member_visibility !== 'hidden' || this.canManageMembers();
7989
});
90+
public readonly votingRepCount: Signal<number> = computed(
91+
() => this.members().filter((m) => m.voting?.status === CommitteeMemberVotingStatus.VOTING_REP).length
92+
);
93+
public readonly observerCount: Signal<number> = computed(
94+
() => this.members().filter((m) => m.voting?.status === CommitteeMemberVotingStatus.OBSERVER).length
95+
);
96+
public readonly chairCount: Signal<number> = computed(
97+
() => this.members().filter((m) => m.role?.name === CommitteeMemberRole.CHAIR || m.role?.name === CommitteeMemberRole.VICE_CHAIR).length
98+
);
99+
100+
// Complex computed signals — use private init functions
101+
public readonly chipConfig: Signal<CommitteeMemberFilterChipConfig[]> = this.initChipConfig();
102+
private readonly chipFilteredMembers: Signal<CommitteeMember[]> = this.initChipFilteredMembers();
80103

81104
// Filter-related variables
82105
public filterForm: FormGroup;
@@ -136,6 +159,11 @@ export class CommitteeMembersComponent implements OnInit {
136159
return 'Member';
137160
}
138161

162+
public selectChip(chip: CommitteeMemberFilterChip): void {
163+
this.memberFilterChip.set(chip);
164+
this.filterForm.patchValue({ role: null, votingStatus: null, organization: null });
165+
}
166+
139167
public openAddMemberDialog(): void {
140168
const dialogRef = this.dialogService.open(AddMemberDialogComponent, {
141169
header: 'Add Member',
@@ -420,7 +448,7 @@ export class CommitteeMembersComponent implements OnInit {
420448

421449
private initializeFilteredMembers(): Signal<CommitteeMember[]> {
422450
return computed(() => {
423-
let filtered = this.members();
451+
let filtered = this.chipFilteredMembers();
424452

425453
// Apply search filter
426454
const searchTerm = this.searchTerm().toLowerCase();
@@ -456,4 +484,30 @@ export class CommitteeMembersComponent implements OnInit {
456484
return filtered;
457485
});
458486
}
487+
488+
private initChipConfig(): Signal<CommitteeMemberFilterChipConfig[]> {
489+
return computed(() => [
490+
{ key: 'all', label: 'All', count: this.members().length },
491+
{ key: 'voting', label: 'Voting Reps', count: this.votingRepCount() },
492+
{ key: 'observers', label: 'Observers', count: this.observerCount() },
493+
{ key: 'chairs', label: 'Chairs', count: this.chairCount() },
494+
]);
495+
}
496+
497+
private initChipFilteredMembers(): Signal<CommitteeMember[]> {
498+
return computed(() => {
499+
const chip = this.memberFilterChip();
500+
const members = this.members();
501+
switch (chip) {
502+
case 'voting':
503+
return members.filter((m) => m.voting?.status === CommitteeMemberVotingStatus.VOTING_REP);
504+
case 'observers':
505+
return members.filter((m) => m.voting?.status === CommitteeMemberVotingStatus.OBSERVER);
506+
case 'chairs':
507+
return members.filter((m) => m.role?.name === CommitteeMemberRole.CHAIR || m.role?.name === CommitteeMemberRole.VICE_CHAIR);
508+
default:
509+
return members;
510+
}
511+
});
512+
}
459513
}

packages/shared/src/interfaces/committee.interface.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ export interface CommitteeSettingsData {
310310
/** Status of an open vote */
311311
export type CommitteeVoteStatus = 'open' | 'closed' | 'cancelled';
312312

313+
/** Quick-filter chip keys for the committee Members tab; `'all'` is the default. */
314+
export type CommitteeMemberFilterChip = 'all' | 'voting' | 'observers' | 'chairs';
315+
316+
/** A single chip entry in the committee Members quick-filter row. */
317+
export interface CommitteeMemberFilterChipConfig {
318+
key: CommitteeMemberFilterChip;
319+
label: string;
320+
count: number;
321+
}
322+
313323
/**
314324
* An open or recent vote in a governing board or oversight committee.
315325
*/

0 commit comments

Comments
 (0)