Skip to content

Commit 9b44456

Browse files
authored
feat(mailing-lists): implement view page and edit mode improvements (#219)
* feat(mailing-lists): implement view page and edit mode improvements LFXV2-952 - Add mailing list detail view page with breadcrumb, settings display - Add subscribers placeholder component for future functionality - Add RouterLink navigation from table titles to view page - Display group name as read-only in edit mode - Fix double-prefixing bug when populating form in edit mode - Fix API tag name for getMailingListById - Add audience access and visibility label constants - Update type labels to be more descriptive 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix: manage string spacing Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent fee0fef commit 9b44456

15 files changed

+586
-33
lines changed

apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.html

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,44 @@ <h2 class="text-lg font-normal">Basic Information</h2>
1919
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
2020
<!-- Left Column - Mailing List Name -->
2121
<div class="flex flex-col gap-2">
22-
<label for="mailing-list-name" class="block text-sm text-neutral-950"> Mailing List Name <span class="text-red-500">*</span> </label>
23-
<lfx-input-text
24-
size="small"
25-
[form]="form()"
26-
control="group_name"
27-
id="mailing-list-name"
28-
placeholder="e.g., dev, announce, community"
29-
styleClass="w-full"
30-
data-testid="mailing-list-basic-info-name-input">
31-
</lfx-input-text>
32-
<p class="text-xs text-gray-500">Use a simple, purpose-based name (e.g., dev, announce).</p>
33-
<p class="text-xs text-gray-500 mt-2">
34-
Email address: <span class="font-medium">{{ emailPreview() }}</span>
35-
</p>
36-
@if (form().get('group_name')?.errors?.['required'] && form().get('group_name')?.touched) {
37-
<p class="mt-1 text-sm text-red-600">Mailing list name is required</p>
38-
}
39-
@if (form().get('group_name')?.errors?.['minlength'] && form().get('group_name')?.touched) {
40-
<p class="mt-1 text-sm text-red-600">Name must be at least 3 characters</p>
41-
}
42-
@if (groupNameTooLong() && form().get('group_name')?.touched) {
43-
<p class="mt-1 text-sm text-red-600">{{ groupNameLengthError() }}</p>
44-
}
45-
@if (form().get('group_name')?.errors?.['pattern'] && form().get('group_name')?.touched) {
46-
<p class="mt-1 text-sm text-red-600">Name can only contain letters, numbers, hyphens, and underscores</p>
22+
<label for="mailing-list-name" class="block text-sm text-neutral-950">
23+
Mailing List Name
24+
@if (!isEditMode()) {
25+
<span class="text-red-500">*</span>
26+
}
27+
</label>
28+
@if (isEditMode()) {
29+
<!-- Read-only display in edit mode -->
30+
<div class="px-3 py-2 bg-gray-100 border border-gray-200 rounded-md text-sm text-neutral-700" data-testid="mailing-list-basic-info-name-readonly">
31+
{{ emailPreview() }}
32+
</div>
33+
<p class="text-xs text-gray-500">Mailing list name cannot be changed after creation.</p>
34+
} @else {
35+
<lfx-input-text
36+
size="small"
37+
[form]="form()"
38+
control="group_name"
39+
id="mailing-list-name"
40+
placeholder="e.g., dev, announce, community"
41+
styleClass="w-full"
42+
data-testid="mailing-list-basic-info-name-input">
43+
</lfx-input-text>
44+
<p class="text-xs text-gray-500">Use a simple, purpose-based name (e.g., dev, announce).</p>
45+
<p class="text-xs text-gray-500 mt-2">
46+
Email address: <span class="font-medium">{{ emailPreview() }}</span>
47+
</p>
48+
@if (form().get('group_name')?.errors?.['required'] && form().get('group_name')?.touched) {
49+
<p class="mt-1 text-sm text-red-600">Mailing list name is required</p>
50+
}
51+
@if (form().get('group_name')?.errors?.['minlength'] && form().get('group_name')?.touched) {
52+
<p class="mt-1 text-sm text-red-600">Name must be at least 3 characters</p>
53+
}
54+
@if (groupNameTooLong() && form().get('group_name')?.touched) {
55+
<p class="mt-1 text-sm text-red-600">{{ groupNameLengthError() }}</p>
56+
}
57+
@if (form().get('group_name')?.errors?.['pattern'] && form().get('group_name')?.touched) {
58+
<p class="mt-1 text-sm text-red-600">Name can only contain letters, numbers, hyphens, and underscores</p>
59+
}
4760
}
4861
</div>
4962

apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class MailingListBasicInfoComponent {
2121
public readonly service = input<GroupsIOService | null>(null);
2222
public readonly prefix = input<string>('');
2323
public readonly maxGroupNameLength = input<number>(34);
24+
public readonly isEditMode = input<boolean>(false);
2425

2526
public readonly projectName = computed(() => {
2627
return this.service()?.project_name || this.projectContextService.selectedProject()?.name || this.projectContextService.selectedFoundation()?.name || '';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<lfx-card data-testid="mailing-list-subscribers-card">
5+
<div class="flex items-center justify-between mb-4">
6+
<h3 class="text-sm font-medium">Subscribers</h3>
7+
<lfx-button
8+
icon="fa-light fa-user-plus"
9+
label="Add Subscriber"
10+
size="small"
11+
severity="secondary"
12+
[outlined]="true"
13+
[disabled]="true"
14+
data-testid="mailing-list-add-subscriber-button">
15+
</lfx-button>
16+
</div>
17+
18+
<lfx-table
19+
[value]="subscribers"
20+
[paginator]="subscribers.length > 10"
21+
[rows]="10"
22+
[rowsPerPageOptions]="[10, 25, 50]"
23+
sortField="name"
24+
[sortOrder]="1"
25+
data-testid="mailing-list-subscribers-table">
26+
<!-- Header Template -->
27+
<ng-template #header>
28+
<tr>
29+
<th>Name</th>
30+
<th>Title</th>
31+
<th>Organization</th>
32+
<th>Email</th>
33+
<th>Delivery Mode</th>
34+
<th>Role</th>
35+
<th></th>
36+
</tr>
37+
</ng-template>
38+
39+
<!-- Body Template -->
40+
<ng-template #body let-subscriber>
41+
<tr [attr.data-testid]="'subscriber-row-' + subscriber.uid">
42+
<td>{{ subscriber.name }}</td>
43+
<td>{{ subscriber.title || '-' }}</td>
44+
<td>{{ subscriber.organization || '-' }}</td>
45+
<td>{{ subscriber.email }}</td>
46+
<td>{{ subscriber.deliveryMode }}</td>
47+
<td>{{ subscriber.role }}</td>
48+
<td>
49+
<lfx-button
50+
icon="fa-light fa-ellipsis-vertical"
51+
severity="secondary"
52+
size="small"
53+
[text]="true"
54+
styleClass="h-8 w-8 text-gray-500 hover:text-gray-700 hover:bg-gray-100"
55+
[attr.data-testid]="'subscriber-actions-' + subscriber.uid">
56+
</lfx-button>
57+
</td>
58+
</tr>
59+
</ng-template>
60+
61+
<!-- Empty Message Template -->
62+
<ng-template #emptymessage>
63+
<tr>
64+
<td colspan="7" class="text-center py-8">
65+
<i class="fa-light fa-users text-3xl text-gray-300 mb-2 block"></i>
66+
<p class="text-sm text-gray-500">No subscribers yet</p>
67+
<p class="text-xs text-gray-400 mt-1">Subscribers will appear here once the API integration is complete</p>
68+
</td>
69+
</tr>
70+
</ng-template>
71+
</lfx-table>
72+
</lfx-card>
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Component, input } 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 { MAILING_LIST_LABEL } from '@lfx-one/shared/constants';
9+
import { GroupsIOMailingList } from '@lfx-one/shared/interfaces';
10+
11+
/**
12+
* Placeholder interface for mailing list subscribers
13+
* Will be replaced with actual interface when API is available
14+
*/
15+
export interface MailingListSubscriber {
16+
uid: string;
17+
name: string;
18+
email: string;
19+
title?: string;
20+
organization?: string;
21+
deliveryMode: 'individual' | 'digest' | 'none';
22+
role: 'owner' | 'moderator' | 'member';
23+
}
24+
25+
@Component({
26+
selector: 'lfx-mailing-list-subscribers',
27+
imports: [CardComponent, ButtonComponent, TableComponent],
28+
templateUrl: './mailing-list-subscribers.component.html',
29+
styleUrl: './mailing-list-subscribers.component.scss',
30+
})
31+
export class MailingListSubscribersComponent {
32+
// Input
33+
public mailingList = input<GroupsIOMailingList | null>(null);
34+
35+
// Constants
36+
protected readonly mailingListLabel = MAILING_LIST_LABEL;
37+
38+
// Placeholder data - will be replaced with actual API data
39+
public readonly subscribers: MailingListSubscriber[] = [];
40+
public readonly loading = false;
41+
}

apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-table/mailing-list-table.component.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
<!-- Maintainer View Row -->
4242
<!-- Name Column -->
4343
<td>
44-
<a [attr.data-testid]="'mailing-list-title-' + mailingList.uid">
44+
<a
45+
[routerLink]="['/mailing-lists', mailingList.uid]"
46+
class="text-blue-600 hover:text-blue-700 hover:underline"
47+
[attr.data-testid]="'mailing-list-title-' + mailingList.uid">
4548
{{ mailingList.title }}
4649
</a>
4750
</td>
@@ -103,7 +106,9 @@
103106
<!-- Mailing List Column -->
104107
<td>
105108
<div class="flex flex-col gap-1">
106-
<span class="text-sm font-medium">{{ mailingList.title }}</span>
109+
<a [routerLink]="['/mailing-lists', mailingList.uid]" class="text-sm font-medium text-blue-600 hover:text-blue-700 hover:underline">
110+
{{ mailingList.title }}
111+
</a>
107112
<span class="text-xs text-gray-500">{{ mailingList | groupEmail }}</span>
108113
</div>
109114
</td>

apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-table/mailing-list-table.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { Component, input, output } from '@angular/core';
5+
import { RouterLink } from '@angular/router';
56
import { ButtonComponent } from '@components/button/button.component';
67
import { CardComponent } from '@components/card/card.component';
78
import { TableComponent } from '@components/table/table.component';
@@ -24,6 +25,7 @@ import { TooltipModule } from 'primeng/tooltip';
2425
TableComponent,
2526
TagComponent,
2627
TooltipModule,
28+
RouterLink,
2729
GroupEmailPipe,
2830
MailingListVisibilitySeverityPipe,
2931
MailingListTypeLabelPipe,

apps/lfx-one/src/app/modules/mailing-lists/mailing-list-manage/mailing-list-manage.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ <h2>Mailing list service hasn't been set up yet for this project. To create a ma
118118
[formValue]="formValue"
119119
[service]="selectedService()"
120120
[prefix]="servicePrefix()"
121-
[maxGroupNameLength]="maxGroupNameLength()">
121+
[maxGroupNameLength]="maxGroupNameLength()"
122+
[isEditMode]="isEditMode()">
122123
</lfx-mailing-list-basic-info>
123124
</ng-template>
124125
</p-step-panel>

apps/lfx-one/src/app/modules/mailing-lists/mailing-list-manage/mailing-list-manage.component.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,20 @@ export class MailingListManageComponent {
217217
}
218218

219219
private populateFormWithMailingListData(mailingList: GroupsIOMailingList): void {
220+
// Strip prefix from group_name in edit mode to prevent double-prefixing on submit
221+
// The prefix is added back in prepareMailingListData when submitting
222+
let groupName = mailingList.group_name;
223+
const servicePrefix = mailingList.service?.prefix;
224+
225+
if (servicePrefix && mailingList.service?.type !== 'primary') {
226+
const prefixWithSeparator = `${servicePrefix}-`;
227+
if (groupName.startsWith(prefixWithSeparator)) {
228+
groupName = groupName.slice(prefixWithSeparator.length);
229+
}
230+
}
231+
220232
this.form().patchValue({
221-
group_name: mailingList.group_name,
233+
group_name: groupName,
222234
description: mailingList.description || '',
223235
audience_access: mailingList.audience_access || MailingListAudienceAccess.PUBLIC,
224236
type: mailingList.type || MailingListType.DISCUSSION_OPEN,

0 commit comments

Comments
 (0)