Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions apps/lfx-one/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"ngx-cookie-service-ssr": "^19.1.2",
"pino-http": "^10.5.0",
"primeng": "^20.4.0",
"quill": "^2.0.3",
"rxjs": "~7.8.2",
"snowflake-sdk": "^2.3.1",
"tslib": "^2.8.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ <h2 class="text-lg font-normal">Basic Information</h2>
<!-- Project Ownership - Read-only context -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<p class="text-sm text-neutral-950">
This mailing list belongs to: <span class="font-medium">{{ projectName() }}</span>
This mailing list belongs to: <span class="font-medium">{{ projectName() }} - {{ service()?.domain || '' }}</span>
</p>
</div>

Expand Down Expand Up @@ -50,23 +50,19 @@ <h2 class="text-lg font-normal">Basic Information</h2>
<!-- Right Column - Description -->
<div class="flex flex-col gap-2">
<label for="mailing-list-description" class="block text-sm text-neutral-950"> What is this list used for? <span class="text-red-500">*</span> </label>
<lfx-textarea
<lfx-editor
[form]="form()"
control="description"
id="mailing-list-description"
placeholder="e.g., General development discussion and code reviews"
[rows]="4"
styleClass="w-full min-h-24"
data-testid="mailing-list-basic-info-description-input">
</lfx-textarea>
[style]="{ height: '125px' }"
dataTest="mailing-list-basic-info-description-input">
</lfx-editor>
<p class="text-xs text-gray-500">This helps members understand when to use this list.</p>
@if (form().get('description')?.errors?.['required'] && form().get('description')?.touched) {
<p class="mt-1 text-sm text-red-600">Description is required</p>
}
@if (form().get('description')?.errors?.['minlength'] && form().get('description')?.touched) {
} @else if (form().get('description')?.errors?.['minlength'] && form().get('description')?.touched) {
<p class="mt-1 text-sm text-red-600">Description must be at least 11 characters</p>
}
@if (form().get('description')?.errors?.['maxlength'] && form().get('description')?.touched) {
} @else if (form().get('description')?.errors?.['maxlength'] && form().get('description')?.touched) {
<p class="mt-1 text-sm text-red-600">Description cannot exceed 500 characters</p>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, computed, input, Signal } from '@angular/core';
import { Component, computed, inject, input, Signal } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ProjectContextService } from '@app/shared/services/project-context.service';
import { EditorComponent } from '@components/editor/editor.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
import { TextareaComponent } from '@components/textarea/textarea.component';
import { GroupsIOService } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-mailing-list-basic-info',
imports: [ReactiveFormsModule, InputTextComponent, TextareaComponent],
imports: [ReactiveFormsModule, InputTextComponent, EditorComponent],
templateUrl: './mailing-list-basic-info.component.html',
})
export class MailingListBasicInfoComponent {
private readonly projectContextService = inject(ProjectContextService);

public readonly form = input.required<FormGroup>();
public readonly formValue = input.required<Signal<Record<string, unknown>>>();
public readonly service = input<GroupsIOService | null>(null);
public readonly prefix = input<string>('');
public readonly maxGroupNameLength = input<number>(34);

public readonly projectName = computed(() => {
return this.service()?.project_name || 'Your Project';
return this.service()?.project_name || this.projectContextService.selectedProject()?.name || this.projectContextService.selectedFoundation()?.name || '';
});

public readonly serviceDomain = computed(() => {
return this.service()?.domain || 'groups.io';
});

public readonly isSharedService = computed(() => {
const service = this.service();
if (!service) return false;

const currentProjectUid = this.projectContextService.selectedProject()?.uid || this.projectContextService.selectedFoundation()?.uid;

// If service type is explicitly 'shared', it's a shared service
if (service.type === 'shared') return true;

// If current project UID doesn't match service project UID, and service is primary, it's being used as shared
if (currentProjectUid !== service.project_uid && service.type === 'primary') return true;

return false;
});

public readonly emailPreview = computed(() => {
const groupName = (this.formValue()()?.['group_name'] as string) || 'listname';
const prefixValue = this.prefix();
const domain = this.serviceDomain();
return `${prefixValue}${groupName}@${domain}`;

// For shared services, include prefix with hyphen
if (this.isSharedService()) {
return `${prefixValue}-${groupName}@${domain}`;
}

// For non-shared services, no prefix
return `${groupName}@${domain}`;
});

public readonly groupNameTooLong = computed(() => {
Expand All @@ -42,10 +67,15 @@ export class MailingListBasicInfoComponent {

public readonly groupNameLengthError = computed(() => {
const maxLength = this.maxGroupNameLength();
const prefixValue = this.prefix();
if (prefixValue) {
return `Name cannot exceed ${maxLength} characters (prefix "${prefixValue}" uses ${prefixValue.length} of 34 allowed)`;
const prefixValue = this.prefix() + '-';

// For shared services, include hyphen in the count
if (this.isSharedService() && prefixValue) {
const prefixWithHyphenLength = prefixValue.length + 1; // +1 for the hyphen
return `Name cannot exceed ${maxLength} characters (prefix "${prefixValue}" uses ${prefixWithHyphenLength} of 34 allowed)`;
}

// For non-shared services, no prefix consideration
return `Name cannot exceed ${maxLength} characters`;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<!-- Description Column -->
<td>
<div class="line-clamp-2 text-xs">
{{ mailingList.description || '-' }}
{{ (mailingList.description | stripHtml) || '-' }}
</div>
</td>

Expand Down Expand Up @@ -111,7 +111,7 @@
<!-- Description Column -->
<td>
<div class="line-clamp-2 text-xs">
{{ mailingList.description || '-' }}
{{ (mailingList.description | stripHtml) || '-' }}
</div>
</td>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { MailingListTypeLabelPipe } from '@pipes/mailing-list-type-label.pipe';
import { MailingListVisibilitySeverityPipe } from '@pipes/mailing-list-visibility-severity.pipe';
import { RemainingGroupsTooltipPipe } from '@pipes/remaining-groups-tooltip.pipe';
import { SliceLinkedGroupsPipe } from '@pipes/slice-linked-groups.pipe';
import { StripHtmlPipe } from '@pipes/strip-html.pipe';
import { TooltipModule } from 'primeng/tooltip';

@Component({
Expand All @@ -28,6 +29,7 @@ import { TooltipModule } from 'primeng/tooltip';
MailingListTypeLabelPipe,
RemainingGroupsTooltipPipe,
SliceLinkedGroupsPipe,
StripHtmlPipe,
],
templateUrl: './mailing-list-table.component.html',
styleUrl: './mailing-list-table.component.scss',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ButtonComponent } from '@components/button/button.component';
import { CardComponent } from '@components/card/card.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
import { SelectComponent } from '@components/select/select.component';
import { MAILING_LIST_LABEL } from '@lfx-one/shared/constants';
import { COMMITTEE_LABEL, MAILING_LIST_LABEL } from '@lfx-one/shared/constants';
import { GroupsIOMailingList, MailingListCommittee, ProjectContext } from '@lfx-one/shared/interfaces';
import { FeatureFlagService } from '@services/feature-flag.service';
import { MailingListService } from '@services/mailing-list.service';
Expand Down Expand Up @@ -186,7 +186,7 @@ export class MailingListDashboardComponent {
value: committee.uid,
}));

return [{ label: 'All Committees', value: null }, ...committeeOptions];
return [{ label: 'All ' + COMMITTEE_LABEL.plural, value: null }, ...committeeOptions];
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,53 @@
<h1 class="mb-6" data-testid="mailing-list-manage-title">
{{ isEditMode() ? 'Edit Mailing List' : 'Create Mailing List' }}
</h1>
@if (hasNoServices()) {
<!-- No Services Empty State -->
<lfx-card data-testid="mailing-list-no-services-card">
<div class="p-8 md:p-12">
<div class="max-w-2xl mx-auto text-center flex flex-col gap-6">
<!-- Icon -->
<div class="flex justify-center">
<div class="w-20 h-20 rounded-full bg-slate-100 flex items-center justify-center">
<i class="fa-light fa-envelope text-4xl text-slate-400"></i>
</div>
</div>

<!-- Message -->
<h2>Mailing list service hasn't been set up yet for this project. To create a mailing list for your project, contact support.</h2>

<!-- Stepper -->
<p-stepper
[value]="currentStep()"
(valueChange)="goToStep($event)"
[linear]="!isEditMode()"
styleClass="mailing-list-manage-stepper mb-6"
data-testid="mailing-list-manage-stepper">
<p-step-list>
<p-step [value]="1" data-testid="mailing-list-manage-step-1"></p-step>
<p-step [value]="2" data-testid="mailing-list-manage-step-2"></p-step>
<p-step [value]="3" data-testid="mailing-list-manage-step-3"></p-step>
</p-step-list>
<!-- Actions -->
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<lfx-button
href="https://jira.linuxfoundation.org/plugins/servlet/desk/portal/2"
target="_blank"
rel="noopener noreferrer"
label="Contact Support"
severity="info"
data-testid="mailing-list-contact-support-btn">
</lfx-button>
</div>
</div>
</div>
</lfx-card>
} @else {
<!-- Stepper -->
<p-stepper
[value]="currentStep()"
(valueChange)="goToStep($event)"
[linear]="!isEditMode()"
styleClass="mailing-list-manage-stepper mb-6"
data-testid="mailing-list-manage-stepper">
<p-step-list>
<p-step [value]="1" data-testid="mailing-list-manage-step-1"></p-step>
<p-step [value]="2" data-testid="mailing-list-manage-step-2"></p-step>
<p-step [value]="3" data-testid="mailing-list-manage-step-3"></p-step>
</p-step-list>

<p-step-panels>
<!-- Form Card -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm px-7">
<!-- Navigation Controls (Above Content) -->
@if (!hasNoServices()) {
<p-step-panels>
<!-- Form Card -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm px-7">
<!-- Navigation Controls (Above Content) -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<!-- Previous Button -->
<div>
Expand Down Expand Up @@ -80,61 +108,39 @@ <h1 class="mb-6" data-testid="mailing-list-manage-title">
}
</div>
</div>
}

<!-- Step Content -->
<div class="p-6 md:p-8">
<p-step-panel [value]="1" data-testid="mailing-list-manage-panel-1">
<ng-template #content let-activateCallback="activateCallback">
@if (hasNoServices()) {
<!-- No Services Warning -->
<lfx-message styleClass="my-0.5" severity="warn" data-testid="mailing-list-no-services-warning">
<ng-template #content>
<div class="flex flex-col gap-2">
<span class="font-semibold">Mailing List Service Not Configured</span>
<span>
To create a mailing list, a Groups.io service must be set up for this project or its parent project. Please
<a
href="https://jira.linuxfoundation.org/plugins/servlet/desk/portal/2"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-800 underline">
open a support ticket
</a>
to request the service configuration.
</span>
</div>
</ng-template>
</lfx-message>
} @else {
<!-- Step Content -->
<div class="p-6 md:p-8">
<p-step-panel [value]="1" data-testid="mailing-list-manage-panel-1">
<ng-template #content let-activateCallback="activateCallback">
<lfx-mailing-list-basic-info
[form]="form()"
[formValue]="formValue"
[service]="selectedService()"
[prefix]="servicePrefix()"
[maxGroupNameLength]="maxGroupNameLength()">
</lfx-mailing-list-basic-info>
}
</ng-template>
</p-step-panel>
</ng-template>
</p-step-panel>

<p-step-panel [value]="2" data-testid="mailing-list-manage-panel-2">
<ng-template #content let-activateCallback="activateCallback">
<lfx-mailing-list-settings [form]="form()"></lfx-mailing-list-settings>
</ng-template>
</p-step-panel>
<p-step-panel [value]="2" data-testid="mailing-list-manage-panel-2">
<ng-template #content let-activateCallback="activateCallback">
<lfx-mailing-list-settings [form]="form()"></lfx-mailing-list-settings>
</ng-template>
</p-step-panel>

<p-step-panel [value]="3" data-testid="mailing-list-manage-panel-3">
<ng-template #content let-activateCallback="activateCallback">
<!-- Step 3: People & Groups - Placeholder -->
<div class="text-center text-gray-500 py-8">
<p>Step 3: People & Groups Component</p>
<p class="text-sm mt-2">Coming next</p>
</div>
</ng-template>
</p-step-panel>
<p-step-panel [value]="3" data-testid="mailing-list-manage-panel-3">
<ng-template #content let-activateCallback="activateCallback">
<!-- Step 3: People & Groups - Placeholder -->
<div class="text-center text-gray-500 py-8">
<p>Step 3: People & Groups Component</p>
<p class="text-sm mt-2">Coming next</p>
</div>
</ng-template>
</p-step-panel>
</div>
</div>
</div>
</p-step-panels>
</p-stepper>
</p-step-panels>
</p-stepper>
}
</div>
Loading