Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 7 additions & 3 deletions apps/lfx-one/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@
}
}

// .p-message-content {
// @apply flex items-start;
// }
.p-message-content {
@apply w-full;

.p-message-text {
@apply w-full;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ <h3 class="text-lg font-display font-semibold text-gray-900">Join This Meeting</

@if (authenticated() && user(); as user) {
<!-- Logged In State -->
<div class="space-y-3">
<div class="flex flex-col gap-3">
<div class="bg-blue-50 p-3 rounded-lg">
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
Expand All @@ -169,7 +169,7 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
<lfx-message
[severity]="canJoinMeeting() ? 'info' : 'warn'"
[icon]="canJoinMeeting() ? 'fa-light fa-check-circle' : 'fa-light fa-clock'"
styleClass="flex items-center justify-between">
styleClass="flex items-center justify-between w-full">
<ng-template #content>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-3 text-sm">
Expand Down Expand Up @@ -247,10 +247,10 @@ <h4 class="font-medium text-gray-900 font-sans mb-1">Sign in with your LFX accou
</div>

<!-- Manual Entry Form -->
<div class="space-y-3">
<div class="flex flex-col gap-3">
<h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>

<form [formGroup]="joinForm" class="space-y-3">
<form [formGroup]="joinForm" class="flex flex-col gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label for="name" class="flex items-center gap-1 text-sm font-medium text-gray-700 font-sans mb-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ExpandableTextComponent } from '@components/expandable-text/expandable-
import { InputTextComponent } from '@components/input-text/input-text.component';
import { MessageComponent } from '@components/message/message.component';
import { environment } from '@environments/environment';
import { extractUrlsWithDomains, Meeting, MeetingOccurrence, Project, User } from '@lfx-one/shared';
import { extractUrlsWithDomains, getCurrentOrNextOccurrence, Meeting, MeetingOccurrence, Project, User } from '@lfx-one/shared';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { MeetingService } from '@services/meeting.service';
import { UserService } from '@services/user.service';
Expand Down Expand Up @@ -149,32 +149,7 @@ export class MeetingJoinComponent {
private initializeCurrentOccurrence(): Signal<MeetingOccurrence | null> {
return computed(() => {
const meeting = this.meeting();
if (!meeting?.occurrences || meeting.occurrences.length === 0) {
return null;
}

const now = new Date();
const earlyJoinMinutes = meeting.early_join_time_minutes || 10;

// Find the first occurrence that is currently joinable (within the join window)
const joinableOccurrence = meeting.occurrences.find((occurrence) => {
const startTime = new Date(occurrence.start_time);
const earliestJoinTime = new Date(startTime.getTime() - earlyJoinMinutes * 60000);
const latestJoinTime = new Date(startTime.getTime() + occurrence.duration * 60000 + 40 * 60000); // 40 minutes after end

return now >= earliestJoinTime && now <= latestJoinTime;
});

if (joinableOccurrence) {
return joinableOccurrence;
}

// If no joinable occurrence, find the next future occurrence
const futureOccurrences = meeting.occurrences
.filter((occurrence) => new Date(occurrence.start_time) > now)
.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());

return futureOccurrences.length > 0 ? futureOccurrences[0] : null;
return getCurrentOrNextOccurrence(meeting);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ <h3 class="text-lg font-medium text-gray-900">Basic Information</h3>
<div>
<label for="committee-name" class="block text-sm font-medium text-gray-700 mb-1"> Committee Name <span class="text-red-500">*</span> </label>
<lfx-input-text size="small" [form]="form()" control="name" id="committee-name" placeholder="Enter committee name" styleClass="w-full"></lfx-input-text>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('name')?.errors?.['required'] && form().get('name')?.touched">Committee name is required</p>
@if (form().get('name')?.errors?.['required'] && form().get('name')?.touched) {
<p class="mt-1 text-sm text-red-600">Committee name is required</p>
}
</div>

<!-- Category -->
Expand All @@ -35,7 +37,9 @@ <h3 class="text-lg font-medium text-gray-900">Basic Information</h3>
placeholder="Select a category"
styleClass="w-full"
id="committee-category"></lfx-select>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('category')?.errors?.['required'] && form().get('category')?.touched">Category is required</p>
@if (form().get('category')?.errors?.['required'] && form().get('category')?.touched) {
<p class="mt-1 text-sm text-red-600">Category is required</p>
}
</div>

<!-- Parent Committee -->
Expand Down Expand Up @@ -116,16 +120,18 @@ <h3 class="text-lg font-medium text-gray-900">Public Settings</h3>
tooltipPosition="right"></lfx-toggle>

<!-- Conditional Public Name Field -->
<div *ngIf="form().get('public')?.value === true">
<label for="public-name" class="block text-sm font-medium text-gray-700 mb-1"> Public Name </label>
<lfx-input-text
size="small"
[form]="form()"
control="display_name"
id="public-name"
placeholder="Enter public display name"
styleClass="w-full"></lfx-input-text>
</div>
@if (form().get('public')?.value === true) {
<div>
<label for="public-name" class="block text-sm font-medium text-gray-700 mb-1"> Public Name </label>
<lfx-input-text
size="small"
[form]="form()"
control="display_name"
id="public-name"
placeholder="Enter public display name"
styleClass="w-full"></lfx-input-text>
</div>
}
</div>

<!-- SSO Settings Section -->
Expand Down Expand Up @@ -163,7 +169,9 @@ <h3 class="text-lg font-medium text-gray-900">Additional Information</h3>
type="url"
placeholder="https://example.com"
styleClass="w-full"></lfx-input-text>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('website')?.errors?.['pattern'] && form().get('website')?.touched">Please enter a valid URL</p>
@if (form().get('website')?.errors?.['pattern'] && form().get('website')?.touched) {
<p class="mt-1 text-sm text-red-600">Please enter a valid URL</p>
}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,31 @@
<div>
<label for="first-name" class="block text-sm font-medium text-gray-700 mb-1"> First Name <span class="text-red-500">*</span> </label>
<lfx-input-text size="small" [form]="form()" control="first_name" id="first-name" placeholder="Enter first name" styleClass="w-full"></lfx-input-text>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('first_name')?.errors?.['required'] && form().get('first_name')?.touched">
First name is required
</p>
@if (form().get('first_name')?.errors?.['required'] && form().get('first_name')?.touched) {
<p class="mt-1 text-xs text-red-600">First name is required</p>
}
</div>

<!-- Last Name -->
<div>
<label for="last-name" class="block text-sm font-medium text-gray-700 mb-1"> Last Name <span class="text-red-500">*</span> </label>
<lfx-input-text size="small" [form]="form()" control="last_name" id="last-name" placeholder="Enter last name" styleClass="w-full"></lfx-input-text>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('last_name')?.errors?.['required'] && form().get('last_name')?.touched">Last name is required</p>
@if (form().get('last_name')?.errors?.['required'] && form().get('last_name')?.touched) {
<p class="mt-1 text-xs text-red-600">Last name is required</p>
}
</div>
</div>

<!-- Email Address -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-red-500">*</span> </label>
<lfx-input-text size="small" [form]="form()" control="email" id="email" placeholder="Enter email address" styleClass="w-full"></lfx-input-text>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('email')?.errors?.['required'] && form().get('email')?.touched">Email address is required</p>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('email')?.errors?.['email'] && form().get('email')?.touched">Please enter a valid email address</p>
@if (form().get('email')?.errors?.['required'] && form().get('email')?.touched) {
<p class="mt-1 text-xs text-red-600">Email address is required</p>
}
@if (form().get('email')?.errors?.['email'] && form().get('email')?.touched) {
<p class="mt-1 text-xs text-red-600">Please enter a valid email address</p>
}
</div>
</div>

Expand All @@ -41,29 +47,45 @@

<!-- Organization -->
<div>
<label for="organization" class="block text-sm font-medium text-gray-700 mb-1"> Organization </label>
<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"
[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 </label>
<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>
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('organization_url')?.errors?.['pattern'] && form().get('organization_url')?.touched">
Please enter a valid URL
</p>
@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>
}
</div>
</div>

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

import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, ValidatorFn, 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';
Expand Down Expand Up @@ -94,8 +94,8 @@ export class MemberFormComponent {
organization:
formValue.organization || formValue.organization_url
? {
name: formValue.organization,
website: formValue.organization_url,
name: formValue.organization || null,
website: formValue.organization_url || null,
}
: null,
};
Expand Down Expand Up @@ -154,20 +154,55 @@ 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),
});
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;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@
}
</div>
<div class="flex items-center gap-1">
<lfx-button
icon="fa-light fa-copy"
[text]="true"
[rounded]="true"
size="small"
severity="secondary"
data-testid="copy-meeting-button"
(click)="copyMeetingLink()"
pTooltip="Copy Meeting"></lfx-button>
@if (!pastMeeting()) {
<lfx-button
icon="fa-light fa-copy"
[text]="true"
[rounded]="true"
size="small"
severity="secondary"
data-testid="copy-meeting-button"
(click)="copyMeetingLink()"
tooltip="Copy Meeting"></lfx-button>
}
@if (meeting().visibility === 'public') {
<lfx-button
icon="fa-light fa-share"
Expand All @@ -55,7 +57,7 @@
size="small"
severity="secondary"
data-testid="share-meeting-button"
pTooltip="Share Meeting"></lfx-button>
tooltip="Share Meeting"></lfx-button>
}
@if (project()?.slug && meeting().organizer) {
<lfx-button
Expand All @@ -65,6 +67,7 @@
size="small"
severity="secondary"
data-testid="edit-meeting-button"
tooltip="Edit Meeting"
[routerLink]="['/project', project()!.slug, 'meetings', meeting().uid, 'edit']"></lfx-button>

<lfx-button
Expand All @@ -90,13 +93,10 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight"
</h3>
}
</div>
@if (occurrence()?.start_time || meeting().start_time) {
@if (meetingStartTime()) {
<div class="flex items-center gap-1 text-xs text-gray-600 bg-gray-100 px-2 py-1 rounded flex-shrink-0" data-testid="meeting-datetime">
<i class="fa-light fa-calendar-days text-gray-400"></i>
<span
>{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'date' }} •
{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
>
<span>{{ meetingStartTime() | meetingTime: meeting().duration : 'date' }} • {{ meetingStartTime() | meetingTime: meeting().duration : 'time' }}</span>
</div>
}
</div>
Expand Down
Loading