Skip to content

Commit 1ccba3a

Browse files
manishdixitlfxclaude
authored andcommitted
feat(committees): refactor members tab with governance fields and enhanced form (#342)
* feat(committees): refactor members tab with governance fields and enhanced form - Add MemberFormValue interface to shared package for typed form values - Add governance columns (appointed by, voting status, role dates) to members table - Add member_visibility gate to control column visibility based on committee settings - Add data-testid attributes for reliable test targeting - Add organization search with autocomplete in member form - Add individual toggle to disable org fields when member has no org affiliation - Add date pickers for role and voting status start/end dates - Add LinkedIn profile field to member form - Add ClearableEndDateValidator for cross-field date validation - Add buildOrganizationPayload helper for org data serialization - Use getRawValue() to capture disabled field values in form submission LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(committees): address review findings on members tab PR - Default isMembersVisible to false while committee is loading (fail closed for privacy — was flashing members before committee metadata arrived) - Add rel="noopener noreferrer" to org website link (tabnabbing prevention) - Move createMemberFormGroup() into constructor body after this.committee is assigned from config.data, so enable_voting validators are correctly applied LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(committees): address human review findings on members tab - Replace custom animate-pulse skeleton with PrimeNG <p-skeleton> for consistency - Replace window.open mailto with MenuItem.url (more accessible, SSR-safe natively) - Remove isPlatformBrowser/PLATFORM_ID (no longer needed) - Remove redundant onDateChange() — Angular re-runs group validators automatically - Remove is_individual toggle and make organization optional (simplifies form, removes unnecessary complexity for a UI-only field the API doesn't know about) - Remove Validators.required from organization control - Remove is_individual from MemberFormValue interface LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(committees): address MRashad26 review findings on members tab - Fix template literal in [href] binding — use string concatenation instead of backtick syntax which Angular templates don't support (runtime parse error) - Type delete error as HttpErrorResponse and restore console.error for debugging - Move computed signals (isBoardMember, isMaintainer, canManageMembers, isMembersVisible) to inline declarations with readonly, per component-organization.md pattern - Remove WritableSignal import (no longer needed) LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(committees): use dynamic command for mailto instead of static url MenuItem.url is evaluated once at ngOnInit when selectedMember is null, producing a static "mailto:undefined". Switch back to command callback with window.location.href for dynamic member resolution. LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org> * fix(committees): use MenuItem.url for mailto with dynamic member rebuild Rebuild memberActionMenuItems in toggleMemberActionMenu with the selected member so MenuItem.url is set to the correct mailto: at click time. PrimeNG renders this as a native <a href> — SSR-safe, accessible, no window reference. LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (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.6 (1M context) <noreply@anthropic.com> Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
1 parent 9a961d6 commit 1ccba3a

5 files changed

Lines changed: 417 additions & 235 deletions

File tree

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

Lines changed: 184 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -6,171 +6,208 @@
66
<lfx-card>
77
<div class="flex justify-between gap-4 mb-8">
88
<h3 class="text-lg font-medium text-gray-900">{{ committeeLabel.singular }} Members</h3>
9-
<!-- Add Member Button -->
10-
@if (canManageMembers()) {
11-
<lfx-button label="Add Member" icon="fa-light fa-user-plus" size="small" severity="secondary" (onClick)="openAddMemberDialog()"> </lfx-button>
12-
}
9+
<!-- Member Actions -->
10+
<div class="flex gap-2">
11+
@if (canManageMembers()) {
12+
<lfx-button
13+
icon="fa-light fa-user-plus"
14+
label="Add Member"
15+
size="small"
16+
severity="secondary"
17+
styleClass="p-button-outlined"
18+
(onClick)="openAddMemberDialog()"
19+
data-testid="add-member-btn">
20+
</lfx-button>
21+
}
22+
</div>
1323
</div>
1424

15-
<!-- Search and Filter Controls -->
16-
<div class="flex flex-col md:flex-row md:flex-wrap gap-4 mb-6">
17-
<!-- Search Input -->
18-
<div class="flex-1">
19-
<lfx-input-text [form]="filterForm" control="search" placeholder="Search members..." icon="fa-light fa-search" styleClass="w-full" size="small">
20-
</lfx-input-text>
21-
</div>
25+
@if (!isMembersVisible()) {
26+
<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>
27+
} @else {
28+
<!-- Search and Filter Controls -->
29+
<div class="flex flex-col md:flex-row md:flex-wrap gap-4 mb-6">
30+
<!-- Search Input -->
31+
<div class="flex-1">
32+
<lfx-input-text [form]="filterForm" control="search" placeholder="Search members..." icon="fa-light fa-search" styleClass="w-full" size="small">
33+
</lfx-input-text>
34+
</div>
2235

23-
<!-- Role Filter -->
24-
<div class="w-full flex-1 sm:flex-none md:w-48">
25-
<lfx-select
26-
[form]="filterForm"
27-
control="role"
28-
size="small"
29-
[options]="roleOptions()"
30-
placeholder="Filter by Role"
31-
[showClear]="true"
32-
styleClass="w-full">
33-
</lfx-select>
34-
</div>
36+
<!-- Role Filter -->
37+
<div class="w-full flex-1 sm:flex-none md:w-48">
38+
<lfx-select
39+
[form]="filterForm"
40+
control="role"
41+
size="small"
42+
[options]="roleOptions()"
43+
placeholder="Filter by Role"
44+
[showClear]="true"
45+
styleClass="w-full">
46+
</lfx-select>
47+
</div>
3548

36-
<!-- Voting Status Filter -->
37-
<div class="w-full flex-1 sm:flex-none md:w-48">
38-
<lfx-select
39-
[form]="filterForm"
40-
control="votingStatus"
41-
size="small"
42-
[options]="votingStatusOptions()"
43-
placeholder="Filter by Voting Status"
44-
[showClear]="true"
45-
styleClass="w-full">
46-
</lfx-select>
47-
</div>
49+
<!-- Voting Status Filter -->
50+
<div class="w-full flex-1 sm:flex-none md:w-48">
51+
<lfx-select
52+
[form]="filterForm"
53+
control="votingStatus"
54+
size="small"
55+
[options]="votingStatusOptions()"
56+
placeholder="Filter by Voting Status"
57+
[showClear]="true"
58+
styleClass="w-full">
59+
</lfx-select>
60+
</div>
4861

49-
<!-- Organization Filter -->
50-
<div class="w-full flex-1 sm:flex-none md:w-48">
51-
<lfx-select
52-
[form]="filterForm"
53-
control="organization"
54-
size="small"
55-
[options]="organizationOptions()"
56-
placeholder="Filter by Organization"
57-
[showClear]="true"
58-
styleClass="w-full">
59-
</lfx-select>
62+
<!-- Organization Filter -->
63+
<div class="w-full flex-1 sm:flex-none md:w-48">
64+
<lfx-select
65+
[form]="filterForm"
66+
control="organization"
67+
size="small"
68+
[options]="organizationOptions()"
69+
placeholder="Filter by Organization"
70+
[showClear]="true"
71+
styleClass="w-full">
72+
</lfx-select>
73+
</div>
6074
</div>
61-
</div>
6275

63-
<!-- Table View -->
64-
<lfx-table
65-
[value]="filteredMembers()"
66-
[paginator]="true"
67-
[rows]="10"
68-
[rowsPerPageOptions]="[10, 25, 50]"
69-
[showCurrentPageReport]="true"
70-
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} members"
71-
styleClass="p-datatable-sm"
72-
sortField="first_name"
73-
[sortOrder]="1">
74-
<ng-template #header>
75-
<tr>
76-
<th>
77-
<span class="text-sm font-sans text-gray-500">Name</span>
78-
</th>
79-
<th>
80-
<span class="text-sm font-sans text-gray-500">Email</span>
81-
</th>
82-
<th>
83-
<span class="text-sm font-sans text-gray-500">Organization</span>
84-
</th>
85-
@if (committee()?.enable_voting) {
76+
<!-- Table View -->
77+
<lfx-table
78+
[value]="filteredMembers()"
79+
[paginator]="true"
80+
[rows]="10"
81+
[rowsPerPageOptions]="[10, 25, 50]"
82+
[showCurrentPageReport]="true"
83+
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} members"
84+
styleClass="p-datatable-sm"
85+
sortField="first_name"
86+
[sortOrder]="1">
87+
<ng-template #header>
88+
<tr>
8689
<th>
87-
<span class="text-sm font-sans text-gray-500">Role</span>
90+
<span class="text-sm font-sans text-gray-500">Name</span>
8891
</th>
8992
<th>
90-
<span class="text-sm font-sans text-gray-500">Voting Status</span>
93+
<span class="text-sm font-sans text-gray-500">Email</span>
9194
</th>
92-
}
93-
<th style="width: 80px">
94-
<div class="flex justify-center">
95-
<span class="text-sm font-sans text-gray-500">Actions</span>
96-
</div>
97-
</th>
98-
</tr>
99-
</ng-template>
100-
101-
<ng-template #body let-member>
102-
<tr>
103-
<td>
104-
<span class="text-sm font-sans text-gray-900 font-medium">
105-
{{ member | fullName | titlecase }}
106-
</span>
107-
</td>
108-
<td>
109-
<span class="text-sm font-sans text-gray-500">{{ member.email || '-' }}</span>
110-
</td>
111-
<td>
112-
@if (member.organization?.website) {
113-
<a [href]="member.organization.website" target="_blank" class="text-primary hover:text-primary-600 text-sm font-sans">
114-
{{ member.organization.name }}
115-
</a>
116-
} @else {
117-
<span class="text-sm font-sans text-gray-500">{{ member.organization?.name || '-' }}</span>
95+
<th>
96+
<span class="text-sm font-sans text-gray-500">Organization</span>
97+
</th>
98+
@if (committee()?.enable_voting) {
99+
<th>
100+
<span class="text-sm font-sans text-gray-500">Role</span>
101+
</th>
102+
<th>
103+
<span class="text-sm font-sans text-gray-500">Voting Status</span>
104+
</th>
118105
}
119-
</td>
120-
@if (committee()?.enable_voting) {
106+
<th style="width: 80px">
107+
<div class="flex justify-center">
108+
<span class="text-sm font-sans text-gray-500">Actions</span>
109+
</div>
110+
</th>
111+
</tr>
112+
</ng-template>
113+
114+
<ng-template #body let-member>
115+
<tr>
121116
<td>
122-
<span class="text-sm font-sans text-gray-500">{{ member.role?.name || 'None' }}</span>
117+
<span class="text-sm font-sans text-gray-900 font-medium">
118+
{{ member | fullName | titlecase }}
119+
</span>
123120
</td>
124121
<td>
125-
<span class="text-sm font-sans text-gray-500">{{ member.voting?.status || 'None' }}</span>
122+
<span class="text-sm font-sans text-gray-500">{{ member.email || '-' }}</span>
126123
</td>
127-
}
128-
<td class="text-right relative">
129-
<lfx-menu #memberActionMenu [model]="memberActionMenuItems" [popup]="true" appendTo="body"> </lfx-menu>
130-
@if (canManageMembers()) {
131-
<lfx-button
132-
icon="fa-light fa-ellipsis-vertical"
133-
[text]="true"
134-
[rounded]="true"
135-
size="small"
136-
severity="secondary"
137-
(onClick)="toggleMemberActionMenu($event, member, memberActionMenu)">
138-
</lfx-button>
139-
} @else {
140-
<lfx-button
141-
icon="fa-light fa-envelope"
142-
[text]="true"
143-
[rounded]="true"
144-
size="small"
145-
severity="secondary"
146-
[href]="`mailto:${member.email}`"
147-
target="_blank"
148-
pTooltip="Send Message">
149-
</lfx-button>
150-
}
151-
</td>
152-
</tr>
153-
</ng-template>
154-
155-
<!-- Empty Message Template -->
156-
<ng-template #emptymessage>
157-
@if (membersLoading()) {
158-
<tr>
159-
<td [attr.colspan]="committee()?.enable_voting ? 6 : 4" class="text-center py-8">
160-
<i class="fa-light fa-circle-notch fa-spin text-4xl text-gray-400"></i>
124+
<td>
125+
@if (member.organization?.website) {
126+
<a [href]="member.organization.website" target="_blank" rel="noopener noreferrer" class="text-primary hover:text-primary-600 text-sm font-sans">
127+
{{ member.organization.name }}
128+
</a>
129+
} @else {
130+
<span class="text-sm font-sans text-gray-500">{{ member.organization?.name || '-' }}</span>
131+
}
161132
</td>
162-
</tr>
163-
} @else {
164-
<tr>
165-
<td [attr.colspan]="committee()?.enable_voting ? 6 : 4" class="text-center py-8">
166-
<div class="text-center">
167-
<i class="fa-light fa-eyes text-3xl text-gray-400 mb-2"></i>
168-
<p class="text-sm text-gray-500">No members found</p>
169-
</div>
133+
@if (committee()?.enable_voting) {
134+
<td>
135+
<span class="text-sm font-sans text-gray-500">{{ member.role?.name || 'None' }}</span>
136+
</td>
137+
<td>
138+
<span class="text-sm font-sans text-gray-500">{{ member.voting?.status || 'None' }}</span>
139+
</td>
140+
}
141+
<td class="text-right relative">
142+
<lfx-menu #memberActionMenu [model]="memberActionMenuItems" [popup]="true" appendTo="body"> </lfx-menu>
143+
@if (canManageMembers()) {
144+
<lfx-button
145+
icon="fa-light fa-ellipsis-vertical"
146+
[text]="true"
147+
[rounded]="true"
148+
size="small"
149+
severity="secondary"
150+
(onClick)="toggleMemberActionMenu($event, member, memberActionMenu)">
151+
</lfx-button>
152+
} @else {
153+
<lfx-button
154+
icon="fa-light fa-envelope"
155+
[text]="true"
156+
[rounded]="true"
157+
size="small"
158+
severity="secondary"
159+
[href]="'mailto:' + member.email"
160+
target="_blank"
161+
pTooltip="Send Message">
162+
</lfx-button>
163+
}
170164
</td>
171165
</tr>
172-
}
173-
</ng-template>
174-
</lfx-table>
166+
</ng-template>
167+
168+
<!-- Empty Message Template -->
169+
<ng-template #emptymessage>
170+
@if (membersLoading()) {
171+
<tr>
172+
<td [attr.colspan]="committee()?.enable_voting ? 6 : 4" class="text-center py-8">
173+
<div class="flex flex-col gap-4 px-4">
174+
@for (_ of [1, 2, 3]; track _) {
175+
<div class="flex items-center gap-4">
176+
<p-skeleton shape="circle" size="2rem" />
177+
<p-skeleton width="8rem" height="0.875rem" />
178+
<p-skeleton width="10rem" height="0.875rem" />
179+
<p-skeleton width="6rem" height="0.875rem" />
180+
</div>
181+
}
182+
</div>
183+
</td>
184+
</tr>
185+
} @else {
186+
<tr>
187+
<td [attr.colspan]="committee()?.enable_voting ? 6 : 4" class="text-center py-8">
188+
<div class="text-center">
189+
@if (groupBehavioralClass() === 'governing-board' || groupBehavioralClass() === 'oversight-committee') {
190+
<i class="fa-light fa-user-crown text-3xl text-gray-300 mb-2"></i>
191+
<p class="text-sm font-medium text-gray-600">No Board Members Found</p>
192+
<p class="text-xs text-gray-400 mt-1">Board members with voting rights will appear here once added.</p>
193+
} @else {
194+
<i class="fa-light fa-users-gear text-3xl text-gray-300 mb-2"></i>
195+
<p class="text-sm font-medium text-gray-600">No Contributors Yet</p>
196+
<p class="text-xs text-gray-400 mt-1">Contributors will appear here as they join this working group.</p>
197+
}
198+
@if (canManageMembers()) {
199+
<div class="mt-3">
200+
<button class="text-xs text-blue-600 hover:text-blue-700 font-medium" (click)="openAddMemberDialog()">
201+
<i class="fa-light fa-user-plus mr-1"></i>Add the first member
202+
</button>
203+
</div>
204+
}
205+
</div>
206+
</td>
207+
</tr>
208+
}
209+
</ng-template>
210+
</lfx-table>
211+
}
175212
</lfx-card>
176213
</div>

0 commit comments

Comments
 (0)