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
Original file line number Diff line number Diff line change
Expand Up @@ -45,47 +45,19 @@
<lfx-input-text size="small" [form]="form()" control="job_title" id="job-title" placeholder="Enter job title" styleClass="w-full"></lfx-input-text>
</div>

<!-- Organization -->
<!-- Organization Search -->
<div>
<label for="organization" class="block text-sm font-medium text-gray-700 mb-1">
Organization
@if (form().get('organization_url')?.value) {
<span class="text-red-500">*</span>
}
</label>
<lfx-input-text
size="small"
<label for="organization-search" class="block text-sm font-medium text-gray-700 mb-1"> Organization </label>
<lfx-organization-search
[form]="form()"
control="organization"
id="organization"
placeholder="Enter organization name"
styleClass="w-full"></lfx-input-text>
@if (form().errors?.['organizationRequired'] && form().get('organization')?.touched) {
<p class="mt-1 text-xs text-red-600">Organization name is required when organization URL is provided</p>
}
</div>

<!-- Organization URL -->
<div>
<label for="organization-url" class="block text-sm font-medium text-gray-700 mb-1">
Organization URL
@if (form().get('organization')?.value) {
<span class="text-red-500">*</span>
}
</label>
<lfx-input-text
size="small"
[form]="form()"
control="organization_url"
id="organization-url"
placeholder="https://example.com"
styleClass="w-full"></lfx-input-text>
@if (form().get('organization_url')?.errors?.['pattern'] && form().get('organization_url')?.touched) {
<p class="mt-1 text-xs text-red-600">Please enter a valid URL</p>
}
@if (form().errors?.['organizationUrlRequired'] && form().get('organization_url')?.touched) {
<p class="mt-1 text-xs text-red-600">Organization URL is required when organization name is provided</p>
}
nameControl="organization"
domainControl="organization_url"
placeholder="Search for organization..."
styleClass="w-full"
inputStyleClass="text-sm w-full"
panelStyleClass="text-sm w-full"
dataTestId="member-form-organization-search">
</lfx-organization-search>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonComponent } from '@components/button/button.component';
import { CalendarComponent } from '@components/calendar/calendar.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
import { OrganizationSearchComponent } from '@components/organization-search/organization-search.component';
import { SelectComponent } from '@components/select/select.component';
import { MEMBER_ROLES, VOTING_STATUSES } from '@lfx-one/shared/constants';
import { CreateCommitteeMemberRequest } from '@lfx-one/shared/interfaces';
Expand All @@ -18,7 +19,7 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
@Component({
selector: 'lfx-member-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, SelectComponent, InputTextComponent, CalendarComponent],
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, SelectComponent, InputTextComponent, CalendarComponent, OrganizationSearchComponent],
templateUrl: './member-form.component.html',
styleUrl: './member-form.component.scss',
})
Expand Down Expand Up @@ -154,55 +155,20 @@ export class MemberFormComponent {
}

private createMemberFormGroup(): FormGroup {
return new FormGroup(
{
first_name: new FormControl('', [Validators.required]),
last_name: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
job_title: new FormControl(''),
organization: new FormControl(''),
organization_url: new FormControl('', [Validators.pattern('^https?://.+')]),
role: new FormControl(''),
voting_status: new FormControl(''),
appointed_by: new FormControl(''),
role_start: new FormControl(null),
role_end: new FormControl(null),
voting_status_start: new FormControl(null),
voting_status_end: new FormControl(null),
},
{ validators: this.organizationCrossFieldValidator() }
);
}

/**
* Custom validator that ensures if organization is provided, organization_url is also required and vice versa
*/
private organizationCrossFieldValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control) {
return null;
}

const organization = control.get('organization')?.value;
const organizationUrl = control.get('organization_url')?.value;

// If both are empty, that's valid
if (!organization && !organizationUrl) {
return null;
}

// If organization is provided but URL is missing
if (organization && !organizationUrl) {
return { organizationUrlRequired: true };
}

// If URL is provided but organization is missing
if (organizationUrl && !organization) {
return { organizationRequired: true };
}

// Both provided, validation passes
return null;
};
return new FormGroup({
first_name: new FormControl('', [Validators.required]),
last_name: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
job_title: new FormControl(''),
organization: new FormControl(''),
organization_url: new FormControl(''),
role: new FormControl(''),
voting_status: new FormControl(''),
appointed_by: new FormControl(''),
role_start: new FormControl(null),
role_end: new FormControl(null),
voting_status_start: new FormControl(null),
voting_status_end: new FormControl(null),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@
<!-- Organization -->
<div data-testid="registrant-form-organization-field" class="flex flex-col gap-1">
<label for="org_name" class="text-sm font-medium text-gray-700">Organization</label>
<lfx-input-text
size="small"
<lfx-organization-search
[form]="form()"
control="org_name"
label="Organization"
placeholder="Enter organization"
data-testid="registrant-form-organization-input">
</lfx-input-text>
nameControl="org_name"
placeholder="Search organizations..."
styleClass="w-full"
inputStyleClass="text-sm w-full"
panelStyleClass="text-sm w-full"
dataTestId="registrant-form-organization-input">
</lfx-organization-search>
</div>

<!-- Host Toggle -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { Component, input } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { CheckboxComponent } from '@components/checkbox/checkbox.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
import { OrganizationSearchComponent } from '@components/organization-search/organization-search.component';
import { MeetingRegistrant } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-registrant-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, InputTextComponent, CheckboxComponent],
imports: [CommonModule, ReactiveFormsModule, InputTextComponent, CheckboxComponent, OrganizationSearchComponent],
templateUrl: './registrant-form.component.html',
})
export class RegistrantFormComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
[autoOptionFocus]="autoOptionFocus()"
[completeOnFocus]="completeOnFocus()"
[panelStyleClass]="panelStyleClass()"
[autoHighlight]="autoHighlight()">
[autoHighlight]="autoHighlight()"
[appendTo]="appendTo()">
<!-- Empty message template -->
<ng-template pTemplate="empty" *ngIf="emptyTemplate">
<ng-container *ngTemplateOutlet="emptyTemplate"></ng-container>
</ng-template>
</p-autocomplete>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, ContentChild, input, output, TemplateRef } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { AutoCompleteCompleteEvent, AutoCompleteModule, AutoCompleteSelectEvent } from 'primeng/autocomplete';

@Component({
selector: 'lfx-autocomplete',
imports: [AutoCompleteModule, ReactiveFormsModule],
imports: [CommonModule, AutoCompleteModule, ReactiveFormsModule],
templateUrl: './autocomplete.component.html',
})
export class AutocompleteComponent {
// Template reference for content projection
@ContentChild('empty', { static: false, descendants: false }) public emptyTemplate?: TemplateRef<any>;
public form = input.required<FormGroup>();
public control = input.required<string>();
public placeholder = input<string>();
Expand All @@ -26,6 +29,7 @@ export class AutocompleteComponent {
public autoOptionFocus = input<boolean>(false);
public completeOnFocus = input<boolean>(false);
public autoHighlight = input<boolean>(false);
public appendTo = input<any>(undefined);

public readonly completeMethod = output<AutoCompleteCompleteEvent>();
public readonly onSelect = output<AutoCompleteSelectEvent>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="organization-search-container">
@if (!manualMode()) {
<lfx-autocomplete
[form]="organizationForm"
control="organizationSearch"
[placeholder]="placeholder()"
[suggestions]="suggestions()"
[styleClass]="styleClass()"
[inputStyleClass]="inputStyleClass()"
[panelStyleClass]="panelStyleClass()"
[dataTestId]="dataTestId()"
optionLabel="name"
optionValue="name"
appendTo="body"
(completeMethod)="onSearchComplete($event)"
(onSelect)="onOrganizationSelected($event)"
(onClear)="onSearchClear()">
<!-- Empty template -->
<ng-template #empty>
<div class="p-3 text-center">
<p class="text-sm text-gray-600 mb-2">No organizations found</p>
<button type="button" class="text-sm text-primary hover:underline" (click)="switchToManualMode()">Enter details manually</button>
</div>
</ng-template>
</lfx-autocomplete>
} @else {
<!-- Manual entry mode -->
<div class="flex flex-col gap-3">
@if (nameControl()) {
<lfx-input-text [form]="form()" [control]="nameControl()!" placeholder="Enter organization name" styleClass="w-full" size="small" />
}

@if (domainControl()) {
<lfx-input-text
[form]="form()"
[control]="domainControl()!"
placeholder="Enter website (e.g., example.com or https://example.com)"
styleClass="w-full"
size="small" />
}

<button type="button" class="text-xs text-primary hover:underline" (click)="switchToSearchMode()">← Back to search</button>
</div>
}
</div>
Loading