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 @@ -139,7 +139,7 @@ <h3 class="text-lg font-semibold text-gray-900">Guests ({{ registrantCount() }})
size="small"
[form]="searchForm"
control="search"
placeholder="Search guests..."
placeholder="Search invited guests..."
icon="fa-light fa-search"
styleClass="w-full"
dataTest="search-input">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,132 @@

<div class="registrant-form" data-testid="registrant-form-container">
<form [formGroup]="form()" data-testid="registrant-form-element">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4" data-testid="registrant-form-fields-grid">
<!-- First Name -->
<div data-testid="registrant-form-firstname-field" class="flex flex-col gap-1">
<label for="first_name" class="text-sm font-medium text-gray-700">First Name</label>
<lfx-input-text
size="small"
[form]="form()"
control="first_name"
label="First Name"
placeholder="Enter first name"
data-testid="registrant-form-firstname-input">
</lfx-input-text>
<!-- User Search (shown when not in individual fields mode) -->
@if (!showIndividualFields()) {
<div class="flex flex-col gap-2">
<div data-testid="registrant-form-search-container">
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700">Search for User</label>
</div>
<lfx-user-search
[form]="form()"
searchType="committee_member"
emailControl="email"
firstNameControl="first_name"
lastNameControl="last_name"
jobTitleControl="job_title"
organizationNameControl="org_name"
placeholder="Search user..."
styleClass="w-full"
class="w-full"
inputStyleClass="text-sm w-full"
panelStyleClass="text-sm w-full"
dataTestId="registrant-form-user-search"
(onUserSelect)="handleUserSelection()"
(onManualEntry)="switchToManualEntry()">
</lfx-user-search>
</div>
<div class="flex items-center justify-center gap-3 mt-3">
<span class="flex-1 border-t border-gray-200"></span>
<span class="text-xs text-gray-500 uppercase tracking-wider">or</span>
<span class="flex-1 border-t border-gray-200"></span>
</div>
<div class="text-center">
<button type="button" class="text-sm text-primary hover:text-primary-dark hover:underline transition-colors" (click)="switchToManualEntry()">
Enter details manually
</button>
</div>
</div>
}

<!-- Last Name -->
<div data-testid="registrant-form-lastname-field" class="flex flex-col gap-1">
<label for="last_name" class="text-sm font-medium text-gray-700">Last Name</label>
<lfx-input-text
size="small"
[form]="form()"
control="last_name"
label="Last Name"
placeholder="Enter last name"
data-testid="registrant-form-lastname-input">
</lfx-input-text>
</div>

<!-- Email -->
<div class="md:col-span-2 flex flex-col gap-1" data-testid="registrant-form-email-field">
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
<lfx-input-text
size="small"
[form]="form()"
control="email"
label="Email"
placeholder="Enter email address"
type="email"
data-testid="registrant-form-email-input">
</lfx-input-text>
@if (form().get('email')?.errors?.['required'] && form().get('email')?.touched && form().get('email')?.dirty) {
<p class="text-sm text-red-500">Email is required</p>
}
@if (form().get('email')?.errors?.['email'] && form().get('email')?.touched && form().get('email')?.dirty) {
<p class="text-sm text-red-500">Invalid email address</p>
<!-- Individual Fields (shown when in individual fields mode) -->
@if (showIndividualFields()) {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4" data-testid="registrant-form-fields-grid">
<!-- Back to Search Button -->
@if (!registrant()) {
<div class="md:col-span-2">
<button type="button" class="text-sm text-primary hover:underline" (click)="backToSearch()">← Back to search</button>
</div>
}
</div>

<!-- Job Title -->
<div data-testid="registrant-form-jobtitle-field" class="flex flex-col gap-1">
<label for="job_title" class="text-sm font-medium text-gray-700">Job Title</label>
<lfx-input-text
size="small"
[form]="form()"
control="job_title"
label="Job Title"
placeholder="Enter job title"
data-testid="registrant-form-jobtitle-input">
</lfx-input-text>
</div>
<!-- First Name -->
<div data-testid="registrant-form-firstname-field" class="flex flex-col gap-1">
<label for="first_name" class="text-sm font-medium text-gray-700">First Name</label>
<lfx-input-text
size="small"
[form]="form()"
control="first_name"
label="First Name"
placeholder="Enter first name"
data-testid="registrant-form-firstname-input">
</lfx-input-text>
</div>

<!-- 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-organization-search
[form]="form()"
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>
<!-- Last Name -->
<div data-testid="registrant-form-lastname-field" class="flex flex-col gap-1">
<label for="last_name" class="text-sm font-medium text-gray-700">Last Name</label>
<lfx-input-text
size="small"
[form]="form()"
control="last_name"
label="Last Name"
placeholder="Enter last name"
data-testid="registrant-form-lastname-input">
</lfx-input-text>
</div>

<!-- Email -->
<div class="md:col-span-2 flex flex-col gap-1" data-testid="registrant-form-email-field">
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
<lfx-input-text
size="small"
[form]="form()"
control="email"
label="Email"
placeholder="Enter email address"
type="email"
data-testid="registrant-form-email-input">
</lfx-input-text>
@if (form().get('email')?.errors?.['required'] && form().get('email')?.touched && form().get('email')?.dirty) {
<p class="text-sm text-red-500">Email is required</p>
}
@if (form().get('email')?.errors?.['email'] && form().get('email')?.touched && form().get('email')?.dirty) {
<p class="text-sm text-red-500">Invalid email address</p>
}
</div>

<!-- Job Title -->
<div data-testid="registrant-form-jobtitle-field" class="flex flex-col gap-1">
<label for="job_title" class="text-sm font-medium text-gray-700">Job Title</label>
<lfx-input-text
size="small"
[form]="form()"
control="job_title"
label="Job Title"
placeholder="Enter job title"
data-testid="registrant-form-jobtitle-input">
</lfx-input-text>
</div>

<!-- 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-organization-search
[form]="form()"
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 -->
<div class="md:col-span-2 flex flex-col gap-1" data-testid="registrant-form-host-field">
<lfx-checkbox [form]="form()" control="host" label="Meeting Host" [binary]="true" data-testid="registrant-form-host-checkbox"> </lfx-checkbox>
<!-- Host Toggle -->
<div class="md:col-span-2 flex flex-col gap-1" data-testid="registrant-form-host-field">
<lfx-checkbox [form]="form()" control="host" label="Meeting Host" [binary]="true" data-testid="registrant-form-host-checkbox"> </lfx-checkbox>
</div>
</div>
</div>
}
</form>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,77 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, input } from '@angular/core';
import { Component, input, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
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 { UserSearchComponent } from '@components/user-search/user-search.component';
import { MeetingRegistrant } from '@lfx-one/shared/interfaces';
import { filter, take } from 'rxjs';

@Component({
selector: 'lfx-registrant-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, InputTextComponent, CheckboxComponent, OrganizationSearchComponent],
imports: [CommonModule, ReactiveFormsModule, InputTextComponent, CheckboxComponent, OrganizationSearchComponent, UserSearchComponent],
templateUrl: './registrant-form.component.html',
})
export class RegistrantFormComponent {
// Inputs
public form = input.required<FormGroup>();
public registrant = input<MeetingRegistrant | null>(null);

// State to track whether we're showing search or individual fields
public showIndividualFields = signal(false);

public constructor() {
// If we have an existing registrant, show individual fields immediately
// Using toObservable to specifically track only registrant changes
toObservable(this.registrant)
.pipe(
filter((registrant) => registrant !== null),
take(1)
)
.subscribe(() => {
this.showIndividualFields.set(true);
});
}

/**
* Handle user selection from search component
* The form controls are already populated by the search component
*/
public handleUserSelection(): void {
// The search component has already populated the form controls
// Now show the individual input fields
this.showIndividualFields.set(true);
this.form().markAsDirty();
}

/**
* Switch to manual entry mode when user clicks "Enter details manually"
*/
public switchToManualEntry(): void {
// Clear the form for manual entry
const hostValue = this.form().get('host')?.value; // Preserve host checkbox state
this.form().reset();
this.form().get('host')?.setValue(hostValue);

// Show individual fields for manual entry
this.showIndividualFields.set(true);
}

/**
* Go back to search mode
*/
public backToSearch(): void {
// Reset form but preserve host checkbox
const hostValue = this.form().get('host')?.value;
this.form().reset();
this.form().get('host')?.setValue(hostValue);

// Switch back to search mode
this.showIndividualFields.set(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,15 @@
<ng-template pTemplate="empty" *ngIf="emptyTemplate">
<ng-container *ngTemplateOutlet="emptyTemplate"></ng-container>
</ng-template>

<!-- Item template -->
<ng-template pTemplate="item" let-item *ngIf="itemTemplate">
<ng-container *ngTemplateOutlet="itemTemplate; context: { $implicit: item }"></ng-container>
</ng-template>

<!-- Footer template -->
<ng-template pTemplate="footer" *ngIf="footerTemplate">
<ng-container *ngTemplateOutlet="footerTemplate"></ng-container>
</ng-template>
</p-autocomplete>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import { AutoCompleteCompleteEvent, AutoCompleteModule, AutoCompleteSelectEvent
export class AutocompleteComponent {
// Template reference for content projection
@ContentChild('empty', { static: false, descendants: false }) public emptyTemplate?: TemplateRef<any>;
@ContentChild('item', { static: false, descendants: false }) public itemTemplate?: TemplateRef<any>;
@ContentChild('footer', { static: false, descendants: false }) public footerTemplate?: TemplateRef<any>;

public form = input.required<FormGroup>();
public control = input.required<string>();
public placeholder = input<string>();
public suggestions = input<any[]>([]);
public styleClass = input<string>();
public inputStyleClass = input<string>();
public panelStyleClass = input<string>();
public delay = input<number>();
public delay = input<number>(300);
public minLength = input<number>(1);
public dataTestId = input<string>();
public optionLabel = input<string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { normalizeToUrl, OrganizationSuggestion } from '@lfx-one/shared';
import { AutoCompleteCompleteEvent, AutoCompleteSelectEvent } from 'primeng/autocomplete';
import { catchError, distinctUntilChanged, of, startWith, switchMap } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, of, startWith, switchMap } from 'rxjs';

import { OrganizationService } from '../../services/organization.service';
import { AutocompleteComponent } from '../autocomplete/autocomplete.component';
Expand Down Expand Up @@ -49,6 +49,7 @@ export class OrganizationSearchComponent {
const searchResults$ = this.organizationForm.get('organizationSearch')!.valueChanges.pipe(
startWith(''),
distinctUntilChanged(),
debounceTime(300),
switchMap((searchTerm: string | null) => {
const trimmedTerm = searchTerm?.trim() || '';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<lfx-autocomplete
[form]="userSearchForm"
control="userSearch"
[placeholder]="placeholder()"
[suggestions]="suggestions()"
[styleClass]="styleClass()"
[inputStyleClass]="inputStyleClass()"
[panelStyleClass]="panelStyleClass()"
[dataTestId]="dataTestId()"
optionLabel="displayName"
size="small"
appendTo="body"
(completeMethod)="onSearchComplete($event)"
(onSelect)="onUserSelected($event)"
(onClear)="onSearchClear()">
<ng-template #item let-user>
<div class="w-full flex justify-between items-center gap-2">
<div class="flex flex-col">
<div class="text-sm flex gap-1 items-center">
<span> {{ user.first_name }} {{ user.last_name }} </span>
@if (user.organization?.name) {
<span class="text-xs text-gray-500"> ({{ user.organization?.name }}) </span>
}
</div>
<span class="text-xs text-gray-500">{{ user.email }}</span>
</div>
</div>
</ng-template>
<!-- Empty template -->
<ng-template #empty>
<div class="p-3 text-center">
<p class="text-sm text-gray-600">No users found</p>
</div>
</ng-template>

<!-- Footer template - always shown -->
<ng-template #footer>
<div class="p-2 border-t border-gray-200 text-center">
<button type="button" class="text-sm text-primary hover:underline" (click)="onEnterManually()">Enter details manually</button>
</div>
</ng-template>
</lfx-autocomplete>
Loading