Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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;
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