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
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ jobs:
echo "LFX_V2_SERVICE=http://lfx-api.dev.v2.cluster.linuxfound.info" >> $GITHUB_ENV
echo "NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222" >> $GITHUB_ENV
echo "M2M_AUTH_ISSUER_BASE_URL=https://linuxfoundation-dev.auth0.com/" >> $GITHUB_ENV
echo "M2M_AUTH_AUDIENCE=https://api-gw.dev.platform.linuxfoundation.org/" >> $GITHUB_ENV
echo "M2M_AUTH_AUDIENCE=https://lfx-api.dev.v2.cluster.linuxfound.info/" >> $GITHUB_ENV
echo "CI=true" >> $GITHUB_ENV

- name: Set up sensitive environment variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@

<!-- Recurring Badge -->
@if (meeting().recurrence) {
<div class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-400 text-white">
<div
class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-400 text-white"
[pTooltip]="meeting().recurrence | recurrenceSummary"
data-testid="recurring-badge">
<i class="fa-light fa-repeat"></i>
<span>Recurring</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ExpandableTextComponent } from '@components/expandable-text/expandable-
import { MenuComponent } from '@components/menu/menu.component';
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant } from '@lfx-pcc/shared';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { RecurrenceSummaryPipe } from '@app/shared/pipes/recurrence-summary.pipe';
import { MeetingService } from '@services/meeting.service';
import { ProjectService } from '@services/project.service';
import { AnimateOnScrollModule } from 'primeng/animateonscroll';
Expand All @@ -36,6 +37,7 @@ import { RegistrantModalComponent } from '../registrant-modal/registrant-modal.c
ButtonComponent,
MenuComponent,
MeetingTimePipe,
RecurrenceSummaryPipe,
AvatarComponent,
TooltipModule,
AnimateOnScrollModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ <h3 class="text-base font-medium text-gray-900">Date & Time</h3>
<lfx-select
size="small"
[form]="form()"
control="recurrence"
control="recurrenceType"
[options]="recurrenceOptions()"
placeholder="Select recurrence"
styleClass="w-full"
Expand All @@ -275,6 +275,13 @@ <h3 class="text-base font-medium text-gray-900">Date & Time</h3>
</div>
}

<!-- Custom Recurrence Pattern Component -->
@if (showCustomRecurrence()) {
<div class="mt-4">
<lfx-meeting-recurrence-pattern [form]="form()" data-testid="meeting-details-custom-recurrence"> </lfx-meeting-recurrence-pattern>
</div>
}

<!-- Early Join Time -->
<div>
<div class="flex items-center gap-2 mb-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import { TimePickerComponent } from '@components/time-picker/time-picker.compone
import { ToggleComponent } from '@components/toggle/toggle.component';
import { GenerateAgendaRequest, MeetingTemplate } from '@lfx-pcc/shared';
import { TIMEZONES } from '@lfx-pcc/shared/constants';
import { getWeekOfMonth } from '@lfx-pcc/shared/utils';
import { MeetingService } from '@services/meeting.service';
import { ProjectService } from '@services/project.service';
import { MessageService } from 'primeng/api';
import { TooltipModule } from 'primeng/tooltip';
import { finalize, take, tap } from 'rxjs';

import { AgendaTemplateSelectorComponent } from '../agenda-template-selector/agenda-template-selector.component';
import { MeetingRecurrencePatternComponent } from '../meeting-recurrence-pattern/meeting-recurrence-pattern.component';

@Component({
selector: 'lfx-meeting-details',
Expand All @@ -37,6 +39,7 @@ import { AgendaTemplateSelectorComponent } from '../agenda-template-selector/age
ToggleComponent,
TooltipModule,
AgendaTemplateSelectorComponent,
MeetingRecurrencePatternComponent,
],
templateUrl: './meeting-details.component.html',
})
Expand All @@ -59,6 +62,9 @@ export class MeetingDetailsComponent implements OnInit {
public readonly showTemplateSelector = signal<boolean>(false);
public readonly selectedTemplateId = signal<string | null>(null);

// Custom recurrence pattern signals
public readonly showCustomRecurrence = signal<boolean>(false);

// Auto-title generation signals
public readonly titleWasAutoGenerated = signal<boolean>(false);

Expand Down Expand Up @@ -112,14 +118,12 @@ export class MeetingDetailsComponent implements OnInit {
customDurationControl?.updateValueAndValidity();
});

// Reset recurrence selection when start date changes
// Update recurrence pattern when start date changes
this.form()
.get('startDate')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
// Reset recurrence to 'none' when date changes to avoid confusion
this.form().get('recurrence')?.setValue('none');
this.generateRecurrenceOptions(value as Date);
.subscribe((newDate) => {
this.handleStartDateChange(newDate as Date);
});

// Watch for isRecurring changes to reset recurrence
Expand All @@ -128,14 +132,30 @@ export class MeetingDetailsComponent implements OnInit {
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isRecurring) => {
if (!isRecurring) {
this.form().get('recurrence')?.setValue('none');
this.form().get('recurrenceType')?.setValue('none');
this.showCustomRecurrence.set(false);
} else {
const recurrence = this.form().get('recurrence')?.value;
if (!recurrence || recurrence === 'none') {
this.form().get('recurrence')?.setValue('weekly');
const recurrenceType = this.form().get('recurrenceType')?.value;
if (!recurrenceType || recurrenceType === 'none') {
this.form().get('recurrenceType')?.setValue('weekly');
}
}
});

// Watch for recurrence value changes to show/hide custom component and set recurrence FormGroup
this.form()
.get('recurrenceType')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((recurrenceValue) => {
this.showCustomRecurrence.set(recurrenceValue === 'custom');
this.updateRecurrenceFormGroup(recurrenceValue);
});

// Initialize showCustomRecurrence based on current form value
const currentRecurrence = this.form().get('recurrenceType')?.value;
if (currentRecurrence === 'custom') {
this.showCustomRecurrence.set(true);
}
}

// AI Helper public methods
Expand Down Expand Up @@ -183,7 +203,7 @@ export class MeetingDetailsComponent implements OnInit {
this.form().get('description')?.setValue(response.agenda);

// Set the AI-estimated duration
this.setAiEstimatedDuration(response.estimatedDuration);
this.setEstimatedDuration(response.estimatedDuration);

this.messageService.add({
severity: 'success',
Expand Down Expand Up @@ -225,12 +245,12 @@ export class MeetingDetailsComponent implements OnInit {
this.selectedTemplateId.set(template.id);

// Set duration based on template
this.setTemplateDuration(template.estimatedDuration);
this.setEstimatedDuration(template.estimatedDuration);

this.hideAgendaTemplateSelector();
}

private setTemplateDuration(estimatedDuration: number): void {
private setEstimatedDuration(estimatedDuration: number): void {
// Check if the estimated duration matches one of our standard options
const standardDuration = this.durationOptions.find((option) => typeof option.value === 'number' && option.value === estimatedDuration);

Expand All @@ -250,56 +270,146 @@ export class MeetingDetailsComponent implements OnInit {
const dayName = dayNames[date.getDay()];

// Calculate which occurrence of the day in the month (1st, 2nd, 3rd, 4th, or last)
const { weekOfMonth, isLastWeek } = this.getWeekOfMonth(date);
const { weekOfMonth, isLastWeek } = getWeekOfMonth(date);
const ordinals = ['', '1st', '2nd', '3rd', '4th'];
const ordinal = ordinals[weekOfMonth] || `${weekOfMonth}th`;

const options = [
{ label: 'Does not repeat', value: 'none' },
{ label: 'Daily', value: 'daily' },
{ label: `Weekly on ${dayName}`, value: 'weekly' },
{ label: 'Every weekday', value: 'weekdays' },
];

// If this is the last occurrence, show "Monthly on the last [day]" instead of "Monthly on the Nth [day]"
if (isLastWeek) {
options.splice(3, 0, { label: `Monthly on the last ${dayName}`, value: 'monthly_last' });
options.splice(4, 0, { label: `Monthly on the last ${dayName}`, value: 'monthly_last' });
} else {
options.splice(3, 0, { label: `Monthly on the ${ordinal} ${dayName}`, value: 'monthly_nth' });
options.splice(4, 0, { label: `Monthly on the ${ordinal} ${dayName}`, value: 'monthly_nth' });
}

// Add custom option at the end
options.push({ label: 'Custom...', value: 'custom' });

this.recurrenceOptions.set(options);
}

private getWeekOfMonth(date: Date): { weekOfMonth: number; isLastWeek: boolean } {
// Find the first occurrence of this day of week in the month
const targetDayOfWeek = date.getDay();
let firstOccurrence = 1;
while (new Date(date.getFullYear(), date.getMonth(), firstOccurrence).getDay() !== targetDayOfWeek) {
firstOccurrence++;
}
private handleStartDateChange(newDate: Date): void {
const currentRecurrenceType = this.form().get('recurrenceType')?.value;

// Calculate which week this date is in
const weekOfMonth = Math.floor((date.getDate() - firstOccurrence) / 7) + 1;
// Always regenerate options for the new date
this.generateRecurrenceOptions(newDate);

// Check if this is the last occurrence of this day in the month
const nextWeekDate = new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000);
const isLastWeek = nextWeekDate.getMonth() !== date.getMonth();
if (!currentRecurrenceType || currentRecurrenceType === 'none') {
// No recurrence set, nothing to update
return;
}

return { weekOfMonth, isLastWeek };
// Update recurrence patterns based on type
switch (currentRecurrenceType) {
case 'weekly':
// Update simple weekly pattern to new day
this.updateSimpleWeeklyPattern(newDate);
break;

case 'monthly_nth':
case 'monthly_last':
// Update monthly pattern based on new date position
this.updateMonthlyPattern(newDate, currentRecurrenceType);
break;
// Other pattern will be handled by the recurrence pattern component
}
}

private setAiEstimatedDuration(estimatedDuration: number): void {
// Check if the estimated duration matches one of our standard options
const standardDuration = this.durationOptions.find((option) => typeof option.value === 'number' && option.value === estimatedDuration);
private updateSimpleWeeklyPattern(newDate: Date): void {
const recurrenceFormGroup = this.form().get('recurrence');
if (recurrenceFormGroup) {
recurrenceFormGroup.patchValue({
weekly_days: String(newDate.getDay() + 1), // Convert 0-6 to 1-7
});
}
}

if (standardDuration) {
// Use standard duration option
this.form().get('duration')?.setValue(estimatedDuration);
this.form().get('customDuration')?.setValue(null);
} else {
// Use custom duration
this.form().get('duration')?.setValue('custom');
this.form().get('customDuration')?.setValue(estimatedDuration);
private updateMonthlyPattern(newDate: Date, recurrenceType: string): void {
const { weekOfMonth, isLastWeek } = getWeekOfMonth(newDate);
const recurrenceFormGroup = this.form().get('recurrence');

if (recurrenceFormGroup) {
// Update the recurrence type if it changed from nth to last or vice versa
if ((recurrenceType === 'monthly_nth' && isLastWeek) || (recurrenceType === 'monthly_last' && !isLastWeek)) {
this.form()
.get('recurrenceType')
?.setValue(isLastWeek ? 'monthly_last' : 'monthly_nth');
}

recurrenceFormGroup.patchValue({
monthly_week: isLastWeek ? -1 : weekOfMonth,
monthly_week_day: newDate.getDay() + 1, // Convert 0-6 to 1-7
});
}
}

private updateRecurrenceFormGroup(recurrenceType: string): void {
const recurrenceFormGroup = this.form().get('recurrence');
if (!recurrenceFormGroup) return;

const startDate = this.form().get('startDate')?.value as Date;

// Reset the recurrence FormGroup
recurrenceFormGroup.patchValue({
type: null,
repeat_interval: 0,
weekly_days: null,
monthly_day: null,
monthly_week: null,
monthly_week_day: null,
end_date_time: null,
end_times: null,
});

switch (recurrenceType) {
case 'none':
// For none, keep repeat_interval as 0
break;

case 'daily':
recurrenceFormGroup.patchValue({
type: 1, // Daily
repeat_interval: 1,
});
break;

case 'weekly':
recurrenceFormGroup.patchValue({
type: 2, // Weekly
repeat_interval: 1,
weekly_days: String(startDate.getDay() + 1), // Convert 0-6 to 1-7
});
break;

case 'weekdays':
recurrenceFormGroup.patchValue({
type: 2, // Weekly
repeat_interval: 1,
weekly_days: '2,3,4,5,6', // Monday to Friday
});
break;

case 'monthly_nth':
case 'monthly_last': {
const { weekOfMonth, isLastWeek } = getWeekOfMonth(startDate);
recurrenceFormGroup.patchValue({
type: 3, // Monthly
repeat_interval: 1,
monthly_week: isLastWeek ? -1 : weekOfMonth,
monthly_week_day: startDate.getDay() + 1, // Convert 0-6 to 1-7
});
break;
}

case 'custom':
// For custom, the recurrence pattern component will handle the values
break;
}
}
}
Loading