Skip to content

Commit 09de2a9

Browse files
authored
feat(mailing-lists): add mailing lists dashboard (#216)
* feat(mailing-lists): add mailing lists dashboard Add mailing lists dashboard feature to LFX One: - Dashboard component with search and filtering by committee/status - Table component with visibility and type badges - Server-side mailing list service with CRUD operations - Frontend mailing list service for data fetching - Pipes for email formatting, type labels, and linked groups - Groups.io service management endpoints - Committee and service enrichment for mailing lists LFXV2-934 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(mailing-lists): refactor committees to array structure - Update MailingListCommittee interface with filters field - Remove committee_uid, committee_name, committee_filters from entity - Update CreateMailingListRequest to use committees array - Remove enrichWithCommittees (API now returns committee data directly) - Fix getMailingListsCount return type in frontend service - Fix template pluralization and pipe documentation LFXV2-934 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent 8981514 commit 09de2a9

26 files changed

+1821
-2
lines changed

apps/lfx-one/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export const routes: Routes = [
2727
path: 'groups',
2828
loadChildren: () => import('./modules/committees/committees.routes').then((m) => m.COMMITTEE_ROUTES),
2929
},
30+
{
31+
path: 'mailing-lists',
32+
loadChildren: () => import('./modules/mailing-lists/mailing-lists.routes').then((m) => m.MAILING_LIST_ROUTES),
33+
},
3034
{
3135
path: 'settings',
3236
loadChildren: () => import('./modules/settings/settings.routes').then((m) => m.SETTINGS_ROUTES),

apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
66
import { NavigationEnd, Router, RouterModule } from '@angular/router';
77
import { SidebarComponent } from '@components/sidebar/sidebar.component';
88
import { environment } from '@environments/environment';
9-
import { COMMITTEE_LABEL } from '@lfx-one/shared/constants';
9+
import { COMMITTEE_LABEL, MAILING_LIST_LABEL } from '@lfx-one/shared/constants';
1010
import { SidebarMenuItem } from '@lfx-one/shared/interfaces';
1111
import { AppService } from '@services/app.service';
1212
import { FeatureFlagService } from '@services/feature-flag.service';
@@ -51,6 +51,11 @@ export class MainLayoutComponent {
5151
icon: 'fa-light fa-users',
5252
routerLink: '/groups',
5353
},
54+
{
55+
label: MAILING_LIST_LABEL.plural,
56+
icon: 'fa-light fa-envelope',
57+
routerLink: '/mailing-lists',
58+
},
5459
{
5560
label: 'Projects',
5661
icon: 'fa-light fa-folder-open',
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<lfx-card styleClass="border-0 shadow-md">
5+
<lfx-table
6+
[value]="mailingLists()"
7+
[paginator]="mailingLists().length > 10"
8+
[rows]="10"
9+
[rowsPerPageOptions]="[10, 25, 50]"
10+
sortField="title"
11+
[sortOrder]="1"
12+
data-testid="mailing-list-dashboard-table">
13+
<!-- Header Template - Conditional based on isMaintainer -->
14+
<ng-template #header>
15+
<tr>
16+
@if (isMaintainer()) {
17+
<!-- Maintainer View Headers (7 columns) -->
18+
<th>Name</th>
19+
<th>List Email</th>
20+
<th class="min-w-[300px]">Description</th>
21+
<th>Visibility</th>
22+
<th>Posting</th>
23+
<th>Linked {{ committeeLabel.plural }}</th>
24+
<th>Subscribers</th>
25+
} @else {
26+
<!-- PMO View Headers (6 columns) -->
27+
<th>Mailing List</th>
28+
<th class="min-w-[300px]">Description</th>
29+
<th>Linked {{ committeeLabel.plural }}</th>
30+
<th>Subscribers</th>
31+
<th>Emails Sent</th>
32+
<th></th>
33+
}
34+
</tr>
35+
</ng-template>
36+
37+
<!-- Body Template -->
38+
<ng-template #body let-mailingList>
39+
<tr [attr.data-testid]="'mailing-list-row-' + mailingList.uid">
40+
@if (isMaintainer()) {
41+
<!-- Maintainer View Row -->
42+
<!-- Name Column -->
43+
<td>
44+
<a [attr.data-testid]="'mailing-list-title-' + mailingList.uid">
45+
{{ mailingList.title }}
46+
</a>
47+
</td>
48+
49+
<!-- List Email Column -->
50+
<td>
51+
<div class="flex items-center gap-1.5">
52+
<span class="text-xs">{{ mailingList | groupEmail }}</span>
53+
@if (mailingList.service?.url) {
54+
<a [href]="mailingList.service.url" target="_blank" rel="noopener noreferrer">
55+
<i class="fa-light fa-external-link-alt !text-[9px]"></i>
56+
</a>
57+
}
58+
</div>
59+
</td>
60+
61+
<!-- Description Column -->
62+
<td>
63+
<div class="line-clamp-2 text-xs">
64+
{{ mailingList.description || '-' }}
65+
</div>
66+
</td>
67+
68+
<!-- Visibility Column -->
69+
<td>
70+
<lfx-tag [value]="mailingList.public ? 'Public' : 'Private'" [severity]="mailingList.public | mailingListVisibilitySeverity"> </lfx-tag>
71+
</td>
72+
73+
<!-- Posting Column -->
74+
<td>
75+
<lfx-tag [value]="mailingList.type | mailingListTypeLabel" severity="info"> </lfx-tag>
76+
</td>
77+
78+
<!-- Linked Groups Column -->
79+
<td>
80+
<div class="flex flex-wrap gap-1">
81+
@if (mailingList.committees?.length) {
82+
@for (committee of mailingList.committees | sliceLinkedGroups: maxVisibleGroups; track committee.uid) {
83+
<lfx-tag [value]="committee.name" severity="info"> </lfx-tag>
84+
}
85+
@if (mailingList.committees.length > maxVisibleGroups) {
86+
<span
87+
class="bg-slate-100 text-slate-600 text-xs px-2 py-0.5 rounded cursor-pointer hover:bg-slate-200"
88+
[pTooltip]="mailingList.committees | remainingGroupsTooltip: maxVisibleGroups"
89+
tooltipPosition="top">
90+
+{{ mailingList.committees.length - maxVisibleGroups }} more
91+
</span>
92+
}
93+
} @else {
94+
<span class="text-gray-400 text-xs">-</span>
95+
}
96+
</div>
97+
</td>
98+
99+
<!-- Subscribers Column -->
100+
<td>-</td>
101+
} @else {
102+
<!-- PMO View Row -->
103+
<!-- Mailing List Column -->
104+
<td>
105+
<div class="flex flex-col gap-1">
106+
<span class="text-sm font-medium">{{ mailingList.title }}</span>
107+
<span class="text-xs text-gray-500">{{ mailingList | groupEmail }}</span>
108+
</div>
109+
</td>
110+
111+
<!-- Description Column -->
112+
<td>
113+
<div class="line-clamp-2 text-xs">
114+
{{ mailingList.description || '-' }}
115+
</div>
116+
</td>
117+
118+
<!-- Linked Groups Column -->
119+
<td>
120+
<div class="flex flex-wrap gap-1">
121+
@if (mailingList.committees?.length) {
122+
@for (committee of mailingList.committees | sliceLinkedGroups: maxVisibleGroups; track committee.uid) {
123+
<lfx-tag [value]="committee.name" severity="info"> </lfx-tag>
124+
}
125+
@if (mailingList.committees.length > maxVisibleGroups) {
126+
<span
127+
class="bg-slate-100 text-slate-600 text-xs px-2 py-0.5 rounded cursor-pointer hover:bg-slate-200"
128+
[pTooltip]="mailingList.committees | remainingGroupsTooltip: maxVisibleGroups"
129+
tooltipPosition="top">
130+
+{{ mailingList.committees.length - maxVisibleGroups }} more
131+
</span>
132+
}
133+
} @else {
134+
<span class="text-gray-400 text-xs">-</span>
135+
}
136+
</div>
137+
</td>
138+
139+
<!-- Subscribers Column -->
140+
<td>
141+
<a [attr.data-testid]="'mailing-list-subscribers-' + mailingList.uid"> - </a>
142+
</td>
143+
144+
<!-- Emails Sent Column -->
145+
<td>-</td>
146+
147+
<!-- Actions Column -->
148+
<td>
149+
<div class="flex items-center justify-center">
150+
<lfx-button
151+
icon="fa-light fa-ellipsis-vertical"
152+
severity="secondary"
153+
size="small"
154+
[text]="true"
155+
styleClass="h-8 w-8 text-gray-500 hover:text-gray-700 hover:bg-gray-100"
156+
[attr.data-testid]="'mailing-list-actions-' + mailingList.uid">
157+
</lfx-button>
158+
</div>
159+
</td>
160+
}
161+
</tr>
162+
</ng-template>
163+
164+
<!-- Empty Message Template -->
165+
<ng-template #emptymessage>
166+
<tr>
167+
<td [attr.colspan]="isMaintainer() ? 7 : 6" class="text-center py-8">
168+
<i class="fa-light fa-envelope text-3xl text-gray-400 mb-2"></i>
169+
<p class="text-sm text-gray-500">No mailing lists found</p>
170+
</td>
171+
</tr>
172+
</ng-template>
173+
</lfx-table>
174+
</lfx-card>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
:host {
5+
display: block;
6+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Component, input, output } from '@angular/core';
5+
import { ButtonComponent } from '@components/button/button.component';
6+
import { CardComponent } from '@components/card/card.component';
7+
import { TableComponent } from '@components/table/table.component';
8+
import { TagComponent } from '@components/tag/tag.component';
9+
import { COMMITTEE_LABEL, MAILING_LIST_LABEL, MAILING_LIST_MAX_VISIBLE_GROUPS } from '@lfx-one/shared';
10+
import { GroupsIOMailingList } from '@lfx-one/shared/interfaces';
11+
import { GroupEmailPipe } from '@pipes/group-email.pipe';
12+
import { MailingListTypeLabelPipe } from '@pipes/mailing-list-type-label.pipe';
13+
import { MailingListVisibilitySeverityPipe } from '@pipes/mailing-list-visibility-severity.pipe';
14+
import { RemainingGroupsTooltipPipe } from '@pipes/remaining-groups-tooltip.pipe';
15+
import { SliceLinkedGroupsPipe } from '@pipes/slice-linked-groups.pipe';
16+
import { TooltipModule } from 'primeng/tooltip';
17+
18+
@Component({
19+
selector: 'lfx-mailing-list-table',
20+
imports: [
21+
CardComponent,
22+
ButtonComponent,
23+
TableComponent,
24+
TagComponent,
25+
TooltipModule,
26+
GroupEmailPipe,
27+
MailingListVisibilitySeverityPipe,
28+
MailingListTypeLabelPipe,
29+
RemainingGroupsTooltipPipe,
30+
SliceLinkedGroupsPipe,
31+
],
32+
templateUrl: './mailing-list-table.component.html',
33+
styleUrl: './mailing-list-table.component.scss',
34+
})
35+
export class MailingListTableComponent {
36+
// Inputs
37+
public mailingLists = input.required<GroupsIOMailingList[]>();
38+
public isMaintainer = input<boolean>(false);
39+
public mailingListLabel = input<string>(MAILING_LIST_LABEL.singular);
40+
41+
// Constants
42+
protected readonly maxVisibleGroups = MAILING_LIST_MAX_VISIBLE_GROUPS;
43+
protected readonly committeeLabel = COMMITTEE_LABEL;
44+
45+
// Outputs
46+
public readonly refresh = output<void>();
47+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
5+
@if (mailingListsLoading()) {
6+
<div class="flex justify-center items-center min-h-96">
7+
<div class="text-center">
8+
<i class="fa-light fa-spinner-third fa-spin text-3xl text-blue-600 mb-4"></i>
9+
<p class="text-gray-600">Loading {{ mailingListLabel.toLowerCase() }} details...</p>
10+
</div>
11+
</div>
12+
} @else {
13+
<div class="mb-8">
14+
<div class="mb-8">
15+
<!-- Page Title with Create Mailing List Button -->
16+
<div class="flex justify-between items-center w-full gap-4">
17+
<h1>{{ mailingListLabelPlural }}</h1>
18+
@if (canCreateMailingList()) {
19+
<lfx-button
20+
[label]="'Create ' + mailingListLabel"
21+
icon="fa-light fa-envelope mr-1"
22+
severity="info"
23+
size="small"
24+
(onClick)="openCreateDialog()"
25+
data-testid="mailing-list-new-cta">
26+
</lfx-button>
27+
}
28+
</div>
29+
<p class="mt-2 text-gray-500" data-testid="mailing-list-dashboard-description">Manage communication channels for your project community.</p>
30+
</div>
31+
32+
<!-- Sticky Top Bar with Search and Filters -->
33+
<div class="sticky top-0 md:top-6 z-10 bg-white rounded-lg border border-gray-200 p-4 -mx-0 mb-8">
34+
<div class="flex flex-col md:flex-row md:items-center gap-3 md:gap-4">
35+
<!-- Search Input -->
36+
<div class="flex-1">
37+
<lfx-input-text
38+
[form]="searchForm"
39+
control="search"
40+
[placeholder]="'Search ' + mailingListLabel.toLowerCase() + '...'"
41+
icon="fa-light fa-search"
42+
styleClass="w-full"
43+
size="small"
44+
data-testid="mailing-list-search-input"></lfx-input-text>
45+
</div>
46+
47+
<!-- Committee Filter Dropdown -->
48+
<div class="w-full sm:w-48">
49+
<lfx-select
50+
[form]="searchForm"
51+
control="committee"
52+
size="small"
53+
[options]="committeeOptions()"
54+
placeholder="Select Committee"
55+
[showClear]="true"
56+
styleClass="w-full"
57+
data-testid="mailing-list-committee-filter"></lfx-select>
58+
</div>
59+
60+
<!-- Status Filter Dropdown (visibility: public/private) -->
61+
<div class="w-full sm:w-48">
62+
<lfx-select
63+
[form]="searchForm"
64+
control="status"
65+
size="small"
66+
[options]="statusOptions()"
67+
placeholder="Select Status"
68+
[showClear]="true"
69+
styleClass="w-full"
70+
data-testid="mailing-list-status-filter"></lfx-select>
71+
</div>
72+
</div>
73+
</div>
74+
</div>
75+
76+
<!-- Content Area - Table -->
77+
<div class="min-h-[400px]">
78+
@if (mailingLists().length === 0 && project()?.uid) {
79+
<!-- Empty state: No mailing lists exist -->
80+
<div class="flex items-center justify-center p-16 bg-white border border-dashed border-gray-300 rounded-lg">
81+
<div class="text-center max-w-md">
82+
<div class="text-gray-400 mb-4">
83+
<i class="fa-light fa-envelope text-[2rem] mb-4"></i>
84+
<h3 class="text-gray-600 mt-2">Your project has no {{ mailingListLabelPlural.toLowerCase() }}, yet.</h3>
85+
</div>
86+
</div>
87+
</div>
88+
} @else if (filteredMailingLists().length === 0) {
89+
<!-- Empty state: Mailing lists exist but filters returned no results -->
90+
<div class="flex items-center justify-center p-16 bg-white border border-dashed border-gray-300 rounded-lg">
91+
<div class="text-center max-w-md">
92+
<div class="text-gray-400 mb-4">
93+
<i class="fa-light fa-envelope text-[2rem] mb-4"></i>
94+
</div>
95+
<h3 class="text-xl font-semibold text-gray-900 mt-4">No {{ mailingListLabelPlural.toLowerCase() }} Found</h3>
96+
<p class="text-gray-600 mt-2">Try adjusting your search or filter criteria</p>
97+
</div>
98+
</div>
99+
} @else {
100+
<lfx-mailing-list-table
101+
[mailingLists]="filteredMailingLists()"
102+
[isMaintainer]="isMaintainer()"
103+
[mailingListLabel]="mailingListLabel"
104+
(refresh)="refreshMailingLists()">
105+
</lfx-mailing-list-table>
106+
}
107+
</div>
108+
}
109+
</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
:host {
5+
display: block;
6+
}

0 commit comments

Comments
 (0)