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 @@ -37,21 +37,11 @@ <h1 class="text-xl font-semibold text-slate-900" data-testid="meeting-create-tit

<p-step-panel [value]="2" data-testid="meeting-create-panel-2">
<ng-template #content let-activateCallback="activateCallback">
<div class="min-h-96 flex items-center justify-center text-slate-500">
<p>Step 3: Platform & Features - To be implemented</p>
</div>
<lfx-meeting-platform-features [form]="form()"></lfx-meeting-platform-features>
</ng-template>
</p-step-panel>

<p-step-panel [value]="3" data-testid="meeting-create-panel-3">
<ng-template #content let-activateCallback="activateCallback">
<div class="min-h-96 flex items-center justify-center text-slate-500">
<p>Step 4: Participants - To be implemented</p>
</div>
</ng-template>
</p-step-panel>

<p-step-panel [value]="4" data-testid="meeting-create-panel-4">
<ng-template #content let-activateCallback="activateCallback">
<div class="min-h-96 flex items-center justify-center text-slate-500">
<p>Step 5: Resources & Summary - To be implemented</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ import { MessageService } from 'primeng/api';
import { StepperModule } from 'primeng/stepper';

import { MeetingDetailsComponent } from '../meeting-details/meeting-details.component';
import { MeetingPlatformFeaturesComponent } from '../meeting-platform-features/meeting-platform-features.component';
import { MeetingTypeSelectionComponent } from '../meeting-type-selection/meeting-type-selection.component';

@Component({
selector: 'lfx-meeting-create',
standalone: true,
imports: [CommonModule, StepperModule, ButtonComponent, ReactiveFormsModule, MeetingTypeSelectionComponent, MeetingDetailsComponent],
imports: [
CommonModule,
StepperModule,
ButtonComponent,
ReactiveFormsModule,
MeetingTypeSelectionComponent,
MeetingDetailsComponent,
MeetingPlatformFeaturesComponent,
],
templateUrl: './meeting-create.component.html',
})
export class MeetingCreateComponent {
Expand All @@ -33,7 +42,7 @@ export class MeetingCreateComponent {

// Stepper state
public currentStep = signal<number>(0);
public readonly totalSteps = 5;
public readonly totalSteps = 4;

// Form state
public form = signal<FormGroup>(this.createMeetingFormGroup());
Expand Down Expand Up @@ -78,6 +87,11 @@ export class MeetingCreateComponent {
public nextStep(): void {
const next = this.currentStep() + 1;
if (next < this.totalSteps && this.canNavigateToStep(next)) {
// Auto-generate title when moving from step 1 to step 2
if (this.currentStep() === 0 && next === 1) {
this.generateMeetingTitle();
}

this.currentStep.set(next);
this.scrollToStepper();
}
Expand Down Expand Up @@ -218,10 +232,7 @@ export class MeetingCreateComponent {
case 2: // Platform & Features
return !!form.get('meetingTool')?.value;

case 3: // Participants (optional but should not have validation errors)
return true;

case 4: // Resources & Summary (optional)
case 3: // Resources & Summary (optional)
return true;

default:
Expand Down Expand Up @@ -253,7 +264,7 @@ export class MeetingCreateComponent {
recurrence: new FormControl('none'),

// Step 3: Platform & Features
meetingTool: new FormControl('', [Validators.required]),
meetingTool: new FormControl('zoom', [Validators.required]),
recording_enabled: new FormControl(false),
transcripts_enabled: new FormControl(false),
youtube_enabled: new FormControl(false),
Expand Down Expand Up @@ -459,4 +470,29 @@ export class MeetingCreateComponent {
});
}
}

private generateMeetingTitle(): void {
const form = this.form();
const meetingType = form.get('meeting_type')?.value;
const startDate = form.get('startDate')?.value;

// Only auto-generate if we have meeting type and the title is empty
const currentTitle = form.get('topic')?.value;
if (meetingType && (!currentTitle || currentTitle.trim() === '')) {
const formattedDate = startDate
? new Date(startDate).toLocaleDateString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
})
: new Date().toLocaleDateString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
});

const generatedTitle = `${meetingType} Meeting - ${formattedDate}`;
form.get('topic')?.setValue(generatedTitle);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ <h4 class="text-sm font-semibold text-gray-900 mb-1">AI Agenda Generator</h4>
id="meeting-agenda"
placeholder="Enter meeting agenda and key discussion points"
[rows]="6"
styleClass="w-full max-h-40 overflow-y-auto"
[autoResize]="true"
styleClass="w-full"
data-testid="meeting-details-agenda-textarea"></lfx-textarea>
<p class="mt-1 text-xs text-gray-500">A clear agenda helps participants prepare and keeps discussions focused</p>
@if (form().get('agenda')?.errors?.['required'] && form().get('agenda')?.touched) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,6 @@ export class MeetingDetailsComponent implements OnInit {
this.generateRecurrenceOptions(value as Date);
});

// Auto-generate title when meeting type and date are available
this.form()
.get('startDate')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.generateMeetingTitleIfNeeded();
});

// Watch for isRecurring changes to reset recurrence
this.form()
.get('isRecurring')
Expand Down Expand Up @@ -201,20 +193,6 @@ export class MeetingDetailsComponent implements OnInit {
return { weekOfMonth, isLastWeek };
}

// Auto-title generation
private generateMeetingTitleIfNeeded(): void {
const meetingType = this.form().get('meeting_type')?.value;
const startDate = this.form().get('startDate')?.value;
const currentTitle = this.form().get('topic')?.value;

// Only auto-generate if we have both type and date, and the current title is empty or was auto-generated
if (meetingType && startDate && (!currentTitle || this.titleWasAutoGenerated())) {
const newTitle = this.generateMeetingTitle(meetingType, startDate);
this.form().get('topic')?.setValue(newTitle);
this.titleWasAutoGenerated.set(true);
}
}

private generateMeetingTitle(meetingType: string, date: Date): string {
const formattedDate = new Date(date).toLocaleDateString('en-US', {
month: '2-digit',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="space-y-6">
<!-- Header -->
<div class="text-center mb-8">
<h2 class="text-xl font-semibold text-slate-900 mb-2">Meeting Platform & Features</h2>
<p class="text-slate-500 text-sm">Choose your meeting platform and enable helpful features.</p>
</div>

<!-- Meeting Platform Selection -->
<div class="space-y-4">
<h3 class="text-base font-medium text-slate-900">Meeting Platform <span class="text-red-500">*</span></h3>

<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
@for (platform of platformOptions; track platform.value) {
<div
(click)="selectPlatform(platform.value)"
[ngClass]="{
'cursor-pointer hover:border-primary hover:bg-blue-50': platform.available,
'cursor-not-allowed bg-slate-50 opacity-60': !platform.available,
'border-primary bg-blue-50': platform.available && form().get('meetingTool')?.value === platform.value,
'border-slate-200 bg-white': platform.available && form().get('meetingTool')?.value !== platform.value,
}"
class="p-4 border rounded-lg transition-all relative"
[attr.data-testid]="'platform-option-' + platform.value">
@if (!platform.available) {
<div class="absolute bottom-2 right-2">
<span class="bg-slate-600 text-white text-xs px-2 py-1 rounded-full">Coming Soon</span>
</div>
}

<div class="flex items-start gap-3">
<div class="p-2 rounded-lg flex-shrink-0" [style.background-color]="platform.color + '15'" [style.color]="platform.color">
<i [class]="platform.icon + ' text-sm'"></i>
</div>
<div>
<h4 class="text-sm font-semibold text-slate-900">{{ platform.label }}</h4>
<p class="text-xs text-slate-600 mt-1">{{ platform.description }}</p>
</div>
</div>
</div>
}
</div>

@if (form().get('meetingTool')?.errors?.['required'] && form().get('meetingTool')?.touched) {
<p class="mt-1 text-sm text-red-600" data-testid="platform-error">Please select a meeting platform</p>
}
</div>

<!-- Info Section -->
<div class="bg-blue-50 border border-blue-200 p-4 rounded-lg">
<div class="flex items-start gap-3">
<div class="bg-primary flex items-center justify-center p-1.5 rounded-full">
<i class="fa-light fa-info-circle text-white text-sm"></i>
</div>
<div>
<h4 class="text-sm font-semibold text-slate-900 mb-1">Meeting Features</h4>
<p class="text-sm text-slate-600">
Open source thrives on transparency and accessibility. Recording meetings, generating transcripts, and enabling features helps include community
members who can't attend live and creates valuable documentation.
</p>
</div>
</div>
</div>

<!-- Meeting Features -->
<div class="space-y-4">
<h3 class="text-base font-medium text-slate-900">Meeting Features</h3>

@for (feature of features; track feature.key) {
<div class="bg-white border border-slate-200 p-4 rounded-lg hover:border-primary transition-colors" [attr.data-testid]="'feature-' + feature.key">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg" [style.background-color]="feature.color + '15'" [style.color]="feature.color">
<i [class]="feature.icon + ' text-sm'"></i>
</div>
<div>
<div class="flex items-center gap-2">
<label [for]="feature.key" class="text-sm font-semibold text-slate-900 cursor-pointer">{{ feature.title }}</label>
@if (feature.recommended) {
<span class="bg-blue-100 text-primary text-xs px-2 py-1 rounded-full font-medium">Recommended</span>
}
</div>
<p class="text-xs text-slate-600">{{ feature.description }}</p>
</div>
</div>
<lfx-toggle [form]="form()" [control]="feature.key" [id]="feature.key" [attr.data-testid]="'toggle-' + feature.key"></lfx-toggle>
</div>
</div>
}
</div>

<!-- Additional Options (if recording is enabled) -->
@if (form().get('recording_enabled')?.value) {
<div class="bg-slate-50 border border-slate-200 p-4 rounded-lg space-y-4" data-testid="recording-options">
<h4 class="text-sm font-semibold text-slate-900">Recording Options</h4>

<!-- Recording Access -->
<div>
<label for="recording-access" class="block text-sm font-medium text-slate-700 mb-1">Recording Access</label>
<lfx-select
size="small"
[form]="form()"
control="recording_access"
[options]="recordingAccessOptions"
placeholder="Select recording access"
styleClass="w-full"
id="recording-access"
data-testid="recording-access-select"></lfx-select>
<p class="mt-1 text-xs text-slate-500">Who can view the meeting recording</p>
</div>

<!-- AI Summary Access (if AI summary is enabled) -->
@if (form().get('zoom_ai_enabled')?.value) {
<div data-testid="ai-summary-options">
<label for="ai-summary-access" class="block text-sm font-medium text-slate-700 mb-1">AI Summary Access</label>
<lfx-select
size="small"
[form]="form()"
control="ai_summary_access"
[options]="aiSummaryAccessOptions"
placeholder="Select AI summary access"
styleClass="w-full"
id="ai-summary-access"
data-testid="ai-summary-access-select"></lfx-select>
<p class="mt-1 text-xs text-slate-500">Who can access the AI-generated meeting summary</p>

<!-- Require AI Summary Approval -->
<div class="flex items-center justify-between mt-3">
<div>
<label for="require-ai-approval" class="text-sm font-medium text-slate-900">Require AI Summary Approval</label>
<p class="text-xs text-slate-500">AI summary must be reviewed before sharing</p>
</div>
<lfx-toggle [form]="form()" control="require_ai_summary_approval" id="require-ai-approval" data-testid="require-ai-approval-toggle"></lfx-toggle>
</div>
</div>
}
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, input } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { SelectComponent } from '@components/select/select.component';
import { ToggleComponent } from '@components/toggle/toggle.component';
import { AI_SUMMARY_ACCESS_OPTIONS, MEETING_FEATURES, MEETING_PLATFORMS, RECORDING_ACCESS_OPTIONS } from '@lfx-pcc/shared/constants';
import { TooltipModule } from 'primeng/tooltip';

@Component({
selector: 'lfx-meeting-platform-features',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, SelectComponent, ToggleComponent, TooltipModule],
templateUrl: './meeting-platform-features.component.html',
})
export class MeetingPlatformFeaturesComponent {
// Form group input from parent
public readonly form = input.required<FormGroup>();

// Constants from shared package
public readonly platformOptions = MEETING_PLATFORMS;
public readonly features = MEETING_FEATURES;
public readonly recordingAccessOptions = RECORDING_ACCESS_OPTIONS;
public readonly aiSummaryAccessOptions = AI_SUMMARY_ACCESS_OPTIONS;

public selectPlatform(platform: string): void {
const platformOption = this.platformOptions.find((p) => p.value === platform);
if (platformOption?.available) {
this.form().get('meetingTool')?.setValue(platform);
}
}

public toggleFeature(featureKey: string, enabled: boolean): void {
this.form().get(featureKey)?.setValue(enabled);
}
}
1 change: 1 addition & 0 deletions packages/shared/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export * from './colors';
export * from './committees';
export * from './file-upload';
export * from './font-sizes';
export * from './meeting';
export * from './server';
export * from './timezones';
Loading
Loading