diff --git a/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.html b/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.html index 165c137..0e2b0e0 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.html +++ b/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.html @@ -13,7 +13,7 @@ > @if (option.isPro && showProBadge) { - + } @else if (showSelectionIndicator) {
@@ -27,7 +27,7 @@
@if (shouldShowImage()) { -
+
} diff --git a/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.ts b/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.ts index db8b960..478cdf3 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.ts @@ -16,7 +16,6 @@ export interface SelectableOption { isPro?: boolean; disabled?: boolean; badge?: string; - value?: any; // Additional data associated with the option } /** @@ -25,7 +24,7 @@ export interface SelectableOption { export interface SelectionEvent { optionId: string; option: SelectableOption; - previousSelection?: string; + previousSelection?: string | string[]; } @Component({ @@ -45,7 +44,7 @@ export class SelectableCardComponent { * Currently selected value(s) * Can be a string (single select) or string[] (multi select) */ - @Input() selectedValue: string | string[] | null = null; + @Input() selectedValue: string | string[] | undefined; /** * Whether multiple options can be selected simultaneously @@ -53,23 +52,6 @@ export class SelectableCardComponent { */ @Input() allowMultiSelect: boolean = false; - /** - * Whether the card should show hover effects - * @default true - */ - @Input() enableHover: boolean = true; - - /** - * Whether the card should show selection animations - * @default true - */ - @Input() enableAnimations: boolean = true; - - /** - * Custom CSS classes to apply to the card - */ - @Input() customClasses: string = ''; - /** * Whether to show the selection indicator (radio button) * @default true @@ -82,6 +64,18 @@ export class SelectableCardComponent { */ @Input() showProBadge: boolean = true; + /** + * Custom icon for the PRO badge + * @default 'crown' + */ + @Input() proBadgeIcon: string = 'crown'; + + /** + * Custom text for the PRO badge + * @default 'PRO' + */ + @Input() proBadgeText: string = 'PRO'; + /** * Whether to show the recommended badge * @default true @@ -107,27 +101,27 @@ export class SelectableCardComponent { @Input() imageAspectRatio: string = '16/9'; /** - * Custom icon for the PRO badge - * @default 'star' + * Whether the card should show hover effects + * @default true */ - @Input() proBadgeIcon: string = 'star'; + @Input() enableHover: boolean = true; /** - * Custom text for the PRO badge - * @default 'PRO' + * Whether the card should show selection animations + * @default true */ - @Input() proBadgeText: string = 'PRO'; + @Input() enableAnimations: boolean = true; + + /** + * Custom CSS classes to apply to the card + */ + @Input() customClasses: string = ''; /** * Event emitted when an option is selected */ @Output() optionSelected = new EventEmitter(); - /** - * Event emitted when the card is clicked (even if selection doesn't change) - */ - @Output() cardClicked = new EventEmitter(); - /** * Event emitted when the card is hovered */ @@ -157,48 +151,17 @@ export class SelectableCardComponent { return; } - // Emit card clicked event - this.cardClicked.emit(this.option); - - // Handle selection logic const wasSelected = this.isOptionSelected(optionId); - let newSelection: string | string[] | null; - let previousSelection: string | undefined; - - if (this.allowMultiSelect) { - // Multi-select logic - const currentArray = Array.isArray(this.selectedValue) ? [...this.selectedValue] : []; - - if (wasSelected) { - // Remove from selection - newSelection = currentArray.filter((id) => id !== optionId); - if (newSelection.length === 0) { - newSelection = null; - } - } else { - // Add to selection - newSelection = [...currentArray, optionId]; - } - } else { - // Single-select logic - if (wasSelected) { - // Deselect (optional behavior) - newSelection = null; - previousSelection = optionId; - } else { - // Select new option - previousSelection = Array.isArray(this.selectedValue) ? undefined : this.selectedValue || undefined; - newSelection = optionId; - } + if (!this.allowMultiSelect && wasSelected) { + return; // No change if already selected } // Emit selection event const selectionEvent: SelectionEvent = { optionId, option: this.option, - previousSelection + previousSelection: this.selectedValue }; - this.optionSelected.emit(selectionEvent); } @@ -229,27 +192,21 @@ export class SelectableCardComponent { if (this.isOptionSelected(this.option.id)) { classes.push('selected'); } - if (this.option.recommended && this.showRecommendedBadge) { classes.push('recommended'); } - if (this.option.isPro && this.showProBadge) { classes.push('pro-feature'); } - if (this.option.disabled) { classes.push('disabled'); } - if (!this.enableHover) { classes.push('no-hover'); } - if (!this.enableAnimations) { classes.push('no-animations'); } - if (this.customClasses) { classes.push(this.customClasses); } @@ -278,21 +235,17 @@ export class SelectableCardComponent { if (this.option.recommended) { statusParts.push('Recommended'); } - if (this.option.isPro) { statusParts.push('PRO feature'); } - if (this.option.disabled) { statusParts.push('Disabled'); } - if (this.isOptionSelected(this.option.id)) { statusParts.push('Selected'); } const statusLabel = statusParts.length > 0 ? `. ${statusParts.join(', ')}` : ''; - return `${baseLabel}${statusLabel}`; } @@ -312,11 +265,4 @@ export class SelectableCardComponent { } return !this.shouldShowImage() && !!this.option.icon; } - - /** - * Get image aspect ratio styles - */ - getImageAspectRatio(): string { - return this.imageAspectRatio; - } } diff --git a/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.html b/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.html index 5f0cce9..e7e3e4f 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.html +++ b/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.html @@ -1,29 +1,22 @@
- @for (step of visibleSteps; track step.id; let i = $index) { + @for (step of visibleSteps(); track step.id; let i = $index) { - - - edit - - - - check - } diff --git a/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.ts b/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.ts index febb207..0b5aa28 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/components/step-indicator/step-indicator.component.ts @@ -1,10 +1,8 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; -import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { StepperOrientation, StepperSelectionEvent, StepState } from '@angular/cdk/stepper'; import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; +import { Component, computed, input, output } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; import { MatStepperModule } from '@angular/material/stepper'; import { WizardStep } from '@lib/models'; import { Observable } from 'rxjs'; @@ -13,29 +11,28 @@ import { map } from 'rxjs/operators'; @Component({ selector: 'ov-step-indicator', standalone: true, - imports: [CommonModule, MatStepperModule, MatIcon, MatButtonModule, ReactiveFormsModule], + imports: [CommonModule, MatStepperModule, ReactiveFormsModule], templateUrl: './step-indicator.component.html', styleUrl: './step-indicator.component.scss' }) -export class StepIndicatorComponent implements OnChanges { - @Input() steps: WizardStep[] = []; - @Input() allowNavigation: boolean = false; - @Input() editMode: boolean = false; // New input for edit mode - @Input() currentStepIndex: number = 0; - @Output() stepClick = new EventEmitter<{ step: WizardStep; index: number }>(); - @Output() layoutChange = new EventEmitter<'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact'>(); +export class StepIndicatorComponent { + steps = input.required(); + currentStepIndex = input.required(); + allowNavigation = input(true); + editMode = input(false); + + stepClick = output<{ index: number; step: WizardStep }>(); + + visibleSteps = computed(() => this.steps().filter((step) => step.isVisible)); - visibleSteps: WizardStep[] = []; stepperOrientation$: Observable; layoutType$: Observable<'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact'>; - stepControls: { [key: string]: FormControl } = {}; constructor(private breakpointObserver: BreakpointObserver) { // Enhanced responsive strategy: // - Large desktop (>1200px): Vertical sidebar for space efficiency // - Medium desktop (768-1200px): Horizontal compact // - Tablet/Mobile (<768px): Vertical compact - const breakpointState$ = this.breakpointObserver.observe([ '(min-width: 1200px)', '(min-width: 768px)', @@ -58,128 +55,34 @@ export class StepIndicatorComponent implements OnChanges { return layoutType === 'horizontal-compact' ? 'horizontal' : 'vertical'; }) ); - - // Emit layout changes for parent component - this.layoutType$.subscribe((layoutType) => { - this.layoutChange.emit(layoutType); - }); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes['steps']) { - this.updateVisibleSteps(); - this.createStepControls(); - } - if (changes['currentStepIndex'] || changes['steps']) { - this.updateStepControls(); - } - } - - private updateVisibleSteps() { - this.visibleSteps = this.steps.filter((step) => step.isVisible); - } - - private createStepControls() { - this.stepControls = {}; - this.visibleSteps.forEach((step) => { - this.stepControls[step.id] = new FormControl({ - value: step.isCompleted, - disabled: !step.isCompleted && !step.isActive - }); - }); - } - - private updateStepControls() { - this.visibleSteps.forEach((step) => { - const control = this.stepControls[step.id]; - if (control) { - control.setValue(step.isCompleted); - if (step.isCompleted || step.isActive) { - control.enable(); - } else { - control.disable(); - } - } - }); - } - - getStepControl(step: WizardStep): FormGroup { - return step.validationFormGroup; - } - - get safeCurrentStepIndex(): number { - if (this.visibleSteps.length === 0) { - console.warn('No visible steps available. Defaulting to index 0.'); - return 0; - } - - // In edit mode, ensure the index is valid for visible steps - let adjustedIndex = this.currentStepIndex; - - // If we are in edit mode and the current index is greater than available visible steps - if (this.editMode && this.currentStepIndex >= this.visibleSteps.length) { - // Find the first active step in the visible steps - const activeStepIndex = this.visibleSteps.findIndex((step) => step.isActive); - adjustedIndex = activeStepIndex >= 0 ? activeStepIndex : 0; - } - - const safeIndex = Math.min(Math.max(0, adjustedIndex), this.visibleSteps.length - 1); - console.log('Safe current step index:', safeIndex, 'for visibleSteps length:', this.visibleSteps.length); - return safeIndex; } onStepClick(event: StepperSelectionEvent) { - if (this.allowNavigation) { - const step = this.visibleSteps[event.selectedIndex]; - this.stepClick.emit({ step, index: event.selectedIndex }); + if (this.allowNavigation()) { + const index = event.selectedIndex; + if (index < 0 || index >= this.visibleSteps().length) { + console.warn('Invalid step index:', index); + return; + } + + const step = this.visibleSteps()[index]; + this.stepClick.emit({ index, step }); } else { console.warn('Navigation is not allowed. Step click ignored:', event.selectedIndex); } } - isStepClickable(step: WizardStep): boolean { - if (!this.allowNavigation) { - return false; - } - if (this.editMode) { - // In edit mode, allow clicking on any step - return true; - } - - return step.isActive || step.isCompleted; - } - - isStepEditable(step: WizardStep): boolean { - return this.isStepClickable(step); - } - - getStepState(step: WizardStep): 'done' | 'edit' | 'error' | 'number' { + getStepState(step: WizardStep): StepState { if (step.isCompleted && !step.isActive) { return 'done'; } - - if (step.isActive && step.validationFormGroup?.invalid) { + if (step.isActive && step.formGroup?.invalid) { return 'error'; } - if (step.isActive) { return 'edit'; } - if (step.isCompleted) { - return 'done'; - } - return 'number'; } - - getStepIcon(step: WizardStep): string { - if (step.isCompleted) { - return 'check'; - } - if (step.isActive) { - return 'edit'; - } - return ''; - } } diff --git a/frontend/projects/shared-meet-components/src/lib/components/wizard-nav/wizard-nav.component.html b/frontend/projects/shared-meet-components/src/lib/components/wizard-nav/wizard-nav.component.html index 529b338..f85f307 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/wizard-nav/wizard-nav.component.html +++ b/frontend/projects/shared-meet-components/src/lib/components/wizard-nav/wizard-nav.component.html @@ -1,4 +1,4 @@ -
} diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/basic-info/basic-info.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/basic-info/basic-info.component.ts index 9d6c999..6e6a1e3 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/basic-info/basic-info.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/basic-info/basic-info.component.ts @@ -1,6 +1,5 @@ -import { CommonModule } from '@angular/common'; -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatNativeDateModule } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -17,7 +16,6 @@ import { Subject, takeUntil } from 'rxjs'; selector: 'ov-room-wizard-basic-info', standalone: true, imports: [ - CommonModule, ReactiveFormsModule, MatButtonModule, MatIconModule, @@ -31,37 +29,18 @@ import { Subject, takeUntil } from 'rxjs'; templateUrl: './basic-info.component.html', styleUrl: './basic-info.component.scss' }) -export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy { - @Input() editMode: boolean = false; // Input to control edit mode from parent component +export class RoomWizardBasicInfoComponent implements OnDestroy { basicInfoForm: FormGroup; - private destroy$ = new Subject(); // Arrays for time selection hours = Array.from({ length: 24 }, (_, i) => ({ value: i, display: i.toString().padStart(2, '0') })); minutes = Array.from({ length: 60 }, (_, i) => ({ value: i, display: i.toString().padStart(2, '0') })); - constructor( - private fb: FormBuilder, - private wizardState: RoomWizardStateService - ) { - this.basicInfoForm = this.fb.group({ - roomIdPrefix: ['', [Validators.maxLength(50)]], - autoDeletionDate: [null], - autoDeletionHour: [23], - autoDeletionMinute: [59] - }); - } + private destroy$ = new Subject(); - ngOnInit() { - // Disable form controls in edit mode - if (this.editMode) { - this.basicInfoForm.get('roomIdPrefix')?.disable(); - this.basicInfoForm.get('autoDeletionDate')?.disable(); - this.basicInfoForm.get('autoDeletionHour')?.disable(); - this.basicInfoForm.get('autoDeletionMinute')?.disable(); - } - - this.loadExistingData(); + constructor(private wizardService: RoomWizardStateService) { + const currentStep = this.wizardService.currentStep(); + this.basicInfoForm = currentStep!.formGroup; this.basicInfoForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { this.saveFormData(value); @@ -73,24 +52,6 @@ export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private loadExistingData() { - const roomOptions = this.wizardState.getRoomOptions(); - - if (roomOptions.autoDeletionDate) { - const date = new Date(roomOptions.autoDeletionDate); - this.basicInfoForm.patchValue({ - roomIdPrefix: roomOptions.roomIdPrefix || '', - autoDeletionDate: date, - autoDeletionHour: date.getHours(), - autoDeletionMinute: date.getMinutes() - }); - } else { - this.basicInfoForm.patchValue({ - roomIdPrefix: roomOptions.roomIdPrefix || '' - }); - } - } - private saveFormData(formValue: any) { let autoDeletionDateTime: number | undefined = undefined; @@ -110,27 +71,13 @@ export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy { }; // Always save to wizard state (including when values are cleared) - this.wizardState.updateStepData('basic', stepData); - } - - clearForm() { - this.basicInfoForm.reset(); - this.wizardState.updateStepData('basic', { - roomIdPrefix: '', - autoDeletionDate: undefined - }); + this.wizardService.updateStepData('basic', stepData); } get minDate(): Date { - return new Date(); - } - - clearDeletionDate() { - this.basicInfoForm.patchValue({ - autoDeletionDate: null, - autoDeletionHour: 23, // Reset to default values - autoDeletionMinute: 59 - }); + const now = new Date(); + now.setHours(now.getHours() + 1, 0, 0, 0); // Set to 1 hour in the future + return now; } get hasDateSelected(): boolean { @@ -160,4 +107,12 @@ export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy { hour12: false }); } + + clearDeletionDate() { + this.basicInfoForm.patchValue({ + autoDeletionDate: null, + autoDeletionHour: 23, + autoDeletionMinute: 59 + }); + } } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html index 2666ce2..b23fcc3 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html @@ -18,8 +18,8 @@ [option]="option" [selectedValue]="selectedOption" [showSelectionIndicator]="true" - [showProBadge]="true" - [showRecommendedBadge]="false" + [showProBadge]="option.isPro ?? false" + [showRecommendedBadge]="option.recommended ?? false" [showImage]="true" [imageAspectRatio]="'1/1'" [showImageAndIcon]="false" diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.ts index bcbcc4e..6a1a2f0 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -24,18 +24,14 @@ import { Subject, takeUntil } from 'rxjs'; templateUrl: './recording-layout.component.html', styleUrl: './recording-layout.component.scss' }) -export class RecordingLayoutComponent implements OnInit, OnDestroy { +export class RecordingLayoutComponent implements OnDestroy { layoutForm: FormGroup; - private destroy$ = new Subject(); - layoutOptions: SelectableOption[] = [ { id: 'grid', title: 'Grid Layout', description: 'Show all participants in a grid view with equal sized tiles', - imageUrl: './assets/layouts/grid.png', - recommended: false, - isPro: false + imageUrl: './assets/layouts/grid.png' }, { id: 'speaker', @@ -43,7 +39,8 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy { description: 'Highlight the active speaker with other participants below', imageUrl: './assets/layouts/speaker.png', isPro: true, - disabled: true + disabled: true, + recommended: true }, { id: 'single-speaker', @@ -55,26 +52,15 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy { } ]; - constructor( - private fb: FormBuilder, - private wizardState: RoomWizardStateService - ) { - this.layoutForm = this.fb.group({ - layoutType: ['grid'] // default to grid - }); - } + private destroy$ = new Subject(); - ngOnInit() { - // Load existing data if available - this.loadExistingData(); + constructor(private wizardService: RoomWizardStateService) { + const currentStep = this.wizardService.currentStep(); + this.layoutForm = currentStep!.formGroup; - // Subscribe to form changes for auto-save this.layoutForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { this.saveFormData(value); }); - - // Save initial default value if no existing data - this.saveInitialDefaultIfNeeded(); } ngOnDestroy() { @@ -82,30 +68,9 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private loadExistingData() { - // Note: This component doesn't need to store data in MeetRoomOptions - // Recording layout settings are typically stored as metadata or used for UI state only - this.layoutForm.patchValue({ - layoutType: 'grid' // Always default to grid - }); - } - - private saveInitialDefaultIfNeeded() { - // Always ensure grid is selected as default - if (!this.layoutForm.value.layoutType) { - this.layoutForm.patchValue({ - layoutType: 'grid' - }); - } - } - private saveFormData(formValue: any) { // Note: Recording layout type is not part of MeetRoomOptions - // This is UI state that affects recording layout but not stored in room options - // We could extend this to store in a metadata field if needed in the future - - // For now, just keep the form state - this affects UI behavior but not the final room creation - console.log('Recording layout type selected:', formValue.layoutType); + // For now, just keep the form state } onOptionSelect(event: SelectionEvent): void { @@ -115,6 +80,6 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy { } get selectedOption(): string { - return this.layoutForm.value.layoutType || 'grid'; // Default to grid if not set + return this.layoutForm.value.layoutType || 'grid'; } } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.html b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.html index 8c050b9..adc5f3f 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.html @@ -17,6 +17,9 @@ } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.ts index ea2ee05..3705ef0 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-preferences/recording-preferences.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -34,27 +34,26 @@ interface RecordingAccessOption { templateUrl: './recording-preferences.component.html', styleUrl: './recording-preferences.component.scss' }) -export class RecordingPreferencesComponent implements OnInit, OnDestroy { +export class RecordingPreferencesComponent implements OnDestroy { recordingForm: FormGroup; - private destroy$ = new Subject(); isAnimatingOut = false; recordingOptions: SelectableOption[] = [ - { - id: 'disabled', - title: 'No Recording', - description: 'Room will not be recorded. Participants can join without recording concerns.', - icon: 'videocam_off' - }, { id: 'enabled', title: 'Allow Recording', description: 'Enable recording capabilities for this room. Recordings can be started manually or automatically.', - icon: 'video_library' + icon: 'video_library', + recommended: true + }, + { + id: 'disabled', + title: 'No Recording', + description: 'Room will not be recorded. Participants can join without recording concerns.', + icon: 'videocam_off' } ]; - recordingAccessOptions: RecordingAccessOption[] = [ { value: MeetRecordingAccess.ADMIN, @@ -70,25 +69,15 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy { } ]; - constructor( - private fb: FormBuilder, - private wizardState: RoomWizardStateService - ) { - this.recordingForm = this.fb.group({ - recordingEnabled: ['disabled'], // default to no recording - allowAccessTo: ['admin'] // default access level - }); - } + private destroy$ = new Subject(); - ngOnInit() { - this.loadExistingData(); + constructor(private wizardState: RoomWizardStateService) { + const currentStep = this.wizardState.currentStep(); + this.recordingForm = currentStep!.formGroup; this.recordingForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { this.saveFormData(value); }); - - // Save initial default value if no existing data - this.saveInitialDefaultIfNeeded(); } ngOnDestroy() { @@ -96,18 +85,6 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private loadExistingData() { - const roomOptions = this.wizardState.getRoomOptions(); - const recordingPrefs = roomOptions.preferences?.recordingPreferences; - - if (recordingPrefs !== undefined) { - this.recordingForm.patchValue({ - recordingEnabled: recordingPrefs.enabled ? 'enabled' : 'disabled', - allowAccessTo: recordingPrefs.allowAccessTo || MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER - }); - } - } - private saveFormData(formValue: any) { const enabled = formValue.recordingEnabled === 'enabled'; @@ -123,16 +100,6 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy { this.wizardState.updateStepData('recording', stepData); } - private saveInitialDefaultIfNeeded() { - const roomOptions = this.wizardState.getRoomOptions(); - const recordingPrefs = roomOptions.preferences?.recordingPreferences; - - // If no existing data, save the default value - if (recordingPrefs === undefined) { - this.saveFormData(this.recordingForm.value); - } - } - onOptionSelect(event: SelectionEvent): void { const previouslyEnabled = this.isRecordingEnabled; const willBeEnabled = event.optionId === 'enabled'; @@ -155,36 +122,15 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy { } } - isOptionSelected(optionId: 'disabled' | 'enabled'): boolean { - return this.recordingForm.value.recordingEnabled === optionId; - } - get selectedValue(): string { - return this.recordingForm.value.recordingEnabled; + return this.recordingForm.value.recordingEnabled || 'disabled'; } get isRecordingEnabled(): boolean { - return this.recordingForm.value.recordingEnabled === 'enabled'; + return this.selectedValue === 'enabled'; } get shouldShowAccessSection(): boolean { return this.isRecordingEnabled || this.isAnimatingOut; } - - setRecommendedOption() { - this.recordingForm.patchValue({ - recordingEnabled: 'enabled' - }); - } - - setDefaultOption() { - this.recordingForm.patchValue({ - recordingEnabled: 'disabled' - }); - } - - get currentSelection(): SelectableOption | undefined { - const selectedId = this.recordingForm.value.recordingEnabled; - return this.recordingOptions.find((option) => option.id === selectedId); - } } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.html b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.html index 22ec2af..0762106 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.html @@ -19,8 +19,8 @@ [selectedValue]="selectedOption" [allowMultiSelect]="false" [showSelectionIndicator]="true" - [showProBadge]="true" - [showRecommendedBadge]="false" + [showProBadge]="option.isPro ?? false" + [showRecommendedBadge]="option.recommended ?? false" [enableHover]="!option.disabled" (optionSelected)="onOptionChange($event)" > diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.ts index 68d15bf..6ba6437 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-trigger/recording-trigger.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -24,18 +24,15 @@ import { Subject, takeUntil } from 'rxjs'; templateUrl: './recording-trigger.component.html', styleUrl: './recording-trigger.component.scss' }) -export class RecordingTriggerComponent implements OnInit, OnDestroy { +export class RecordingTriggerComponent implements OnDestroy { triggerForm: FormGroup; - private destroy$ = new Subject(); - triggerOptions: SelectableOption[] = [ { id: 'manual', title: 'Manual Recording', description: 'Start recording manually when needed', icon: 'touch_app', - recommended: true, - isPro: false + recommended: true }, { id: 'auto1', @@ -55,26 +52,15 @@ export class RecordingTriggerComponent implements OnInit, OnDestroy { } ]; - constructor( - private fb: FormBuilder, - private wizardState: RoomWizardStateService - ) { - this.triggerForm = this.fb.group({ - triggerType: ['manual'] // default to manual - }); - } + private destroy$ = new Subject(); - ngOnInit() { - // Load existing data if available - this.loadExistingData(); + constructor(private wizardService: RoomWizardStateService) { + const currentStep = this.wizardService.currentStep(); + this.triggerForm = currentStep!.formGroup; - // Subscribe to form changes for auto-save this.triggerForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { this.saveFormData(value); }); - - // Save initial default value if no existing data - this.saveInitialDefaultIfNeeded(); } ngOnDestroy() { @@ -82,31 +68,9 @@ export class RecordingTriggerComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private loadExistingData() { - // Note: This component doesn't need to store data in MeetRoomOptions - // Recording trigger settings are typically stored as metadata or used for UI state only - // For now, we'll use form state only - this.triggerForm.patchValue({ - triggerType: 'manual' // Always default to manual - }); - } - private saveFormData(formValue: any) { // Note: Recording trigger type is not part of MeetRoomOptions - // This is UI state that affects how recording is initiated but not stored in room options - // We could extend this to store in a metadata field if needed in the future - - // For now, just keep the form state - this affects UI behavior but not the final room creation - console.log('Recording trigger type selected:', formValue.triggerType); - } - - private saveInitialDefaultIfNeeded() { - // Always ensure manual is selected as default - if (!this.triggerForm.value.triggerType) { - this.triggerForm.patchValue({ - triggerType: 'manual' - }); - } + // For now, just keep the form state } /** diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-preferences/room-preferences.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-preferences/room-preferences.component.ts index b6b59bc..db7bdb0 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-preferences/room-preferences.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-preferences/room-preferences.component.ts @@ -1,7 +1,5 @@ -import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; @@ -11,43 +9,22 @@ import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'ov-room-preferences', standalone: true, - imports: [CommonModule, ReactiveFormsModule, MatCardModule, MatButtonModule, MatIconModule, MatSlideToggleModule], + imports: [ReactiveFormsModule, MatCardModule, MatIconModule, MatSlideToggleModule], templateUrl: './room-preferences.component.html', styleUrl: './room-preferences.component.scss' }) -export class RoomPreferencesComponent implements OnInit, OnDestroy { +export class RoomPreferencesComponent implements OnDestroy { preferencesForm: FormGroup; + private destroy$ = new Subject(); - constructor( - private fb: FormBuilder, - private roomWizardStateService: RoomWizardStateService - ) { - this.preferencesForm = this.fb.group({ - chatEnabled: [true], - virtualBackgroundsEnabled: [true] - }); - } + constructor(private wizardService: RoomWizardStateService) { + const currentStep = this.wizardService.currentStep(); + this.preferencesForm = currentStep!.formGroup; - ngOnInit(): void { - // Load existing data from wizard state - const roomOptions = this.roomWizardStateService.getRoomOptions(); - const preferences = roomOptions.preferences; - - if (preferences) { - this.preferencesForm.patchValue({ - chatEnabled: preferences.chatPreferences?.enabled ?? true, - virtualBackgroundsEnabled: preferences.virtualBackgroundPreferences?.enabled ?? true - }); - } - - // Auto-save form changes this.preferencesForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { this.saveFormData(value); }); - - // Save initial default values if no existing data - this.saveInitialDefaultIfNeeded(); } ngOnDestroy(): void { @@ -67,17 +44,7 @@ export class RoomPreferencesComponent implements OnInit, OnDestroy { } }; - this.roomWizardStateService.updateStepData('preferences', stepData); - } - - private saveInitialDefaultIfNeeded(): void { - const roomOptions = this.roomWizardStateService.getRoomOptions(); - const preferences = roomOptions.preferences; - - // If no existing preferences data, save the default values - if (!preferences?.chatPreferences || !preferences?.virtualBackgroundPreferences) { - this.saveFormData(this.preferencesForm.value); - } + this.wizardService.updateStepData('preferences', stepData); } onChatToggleChange(event: any): void { @@ -91,10 +58,10 @@ export class RoomPreferencesComponent implements OnInit, OnDestroy { } get chatEnabled(): boolean { - return this.preferencesForm.value.chatEnabled; + return this.preferencesForm.value.chatEnabled || false; } get virtualBackgroundsEnabled(): boolean { - return this.preferencesForm.value.virtualBackgroundsEnabled; + return this.preferencesForm.value.virtualBackgroundsEnabled || false; } } diff --git a/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts b/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts index ac960ae..4550878 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts @@ -1,8 +1,17 @@ -import { inject, Injectable } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; +import { computed, Injectable, signal } from '@angular/core'; +import { AbstractControl, FormBuilder, ValidationErrors, Validators } from '@angular/forms'; import { WizardNavigationConfig, WizardStep } from '@lib/models'; import { MeetRecordingAccess, MeetRoomOptions, MeetRoomPreferences } from '@lib/typings/ce'; -import { BehaviorSubject } from 'rxjs'; + +// Default room preferences following the app's defaults +const DEFAULT_PREFERENCES: MeetRoomPreferences = { + recordingPreferences: { + enabled: true, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + }, + chatPreferences: { enabled: true }, + virtualBackgroundPreferences: { enabled: true } +}; /** * Service to manage the state of the room creation wizard. @@ -12,35 +21,29 @@ import { BehaviorSubject } from 'rxjs'; providedIn: 'root' }) export class RoomWizardStateService { - private readonly _formBuilder = inject(FormBuilder); - - // Observables for reactive state management - private readonly _currentStepIndex = new BehaviorSubject(0); - private readonly _steps = new BehaviorSubject([]); - private readonly _roomOptions = new BehaviorSubject({ - preferences: { - recordingPreferences: { - enabled: false, - allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER - }, - chatPreferences: { enabled: true }, - virtualBackgroundPreferences: { enabled: true } - } + // Signals for reactive state management + private _steps = signal([]); + private _visibleSteps = computed(() => this._steps().filter((step) => step.isVisible)); + private _currentStepIndex = signal(0); + private _roomOptions = signal({ + preferences: DEFAULT_PREFERENCES }); - public readonly currentStepIndex$ = this._currentStepIndex.asObservable(); - public readonly steps$ = this._steps.asObservable(); - public readonly roomOptions$ = this._roomOptions.asObservable(); + public readonly steps = computed(() => this._steps()); + public readonly currentStepIndex = computed(() => this._currentStepIndex()); + public readonly currentStep = computed(() => { + const visibleSteps = this._visibleSteps(); + const currentIndex = this._currentStepIndex(); - // Default room preferences following the platform's defaults - private readonly DEFAULT_PREFERENCES: MeetRoomPreferences = { - recordingPreferences: { - enabled: false, - allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER - }, - chatPreferences: { enabled: true }, - virtualBackgroundPreferences: { enabled: true } - }; + if (currentIndex < 0 || currentIndex >= visibleSteps.length) { + return undefined; + } + + return visibleSteps[currentIndex]; + }); + public readonly roomOptions = computed(() => this._roomOptions()); + + constructor(private formBuilder: FormBuilder) {} /** * Initializes the wizard with base steps and default room options. @@ -49,22 +52,17 @@ export class RoomWizardStateService { */ initializeWizard(editMode: boolean = false, existingData?: MeetRoomOptions): void { // Initialize room options with defaults merged with existing data - console.log('Initializing wizard - editMode:', editMode, 'existingData:', existingData); - - const mergedPreferences: MeetRoomPreferences = { - ...this.DEFAULT_PREFERENCES, - ...existingData?.preferences - }; - const initialRoomOptions: MeetRoomOptions = { ...existingData, - preferences: mergedPreferences + preferences: { + ...DEFAULT_PREFERENCES, + ...(existingData?.preferences || {}) + } }; - console.log('Initial room options created:', initialRoomOptions); - this._roomOptions.next(initialRoomOptions); + this._roomOptions.set(initialRoomOptions); - // Define base wizard steps + // Define wizard steps const baseSteps: WizardStep[] = [ { id: 'basic', @@ -72,14 +70,62 @@ export class RoomWizardStateService { isCompleted: editMode, // In edit mode, mark as completed but not editable isActive: !editMode, // Start with basic step active in create mode isVisible: true, - validationFormGroup: this._formBuilder.group({ - roomPrefix: [ - { value: initialRoomOptions.roomIdPrefix || '', disabled: editMode }, - editMode ? [] : [Validators.minLength(2)] - ], - deletionDate: [{ value: initialRoomOptions.autoDeletionDate || '', disabled: editMode }] - }), - order: 1 + formGroup: this.formBuilder.group( + { + roomIdPrefix: [ + { value: initialRoomOptions.roomIdPrefix || '', disabled: editMode }, + editMode ? [] : [Validators.maxLength(50)] + ], + autoDeletionDate: [ + { + value: initialRoomOptions.autoDeletionDate + ? new Date(initialRoomOptions.autoDeletionDate) + : undefined, + disabled: editMode + } + ], + autoDeletionHour: [ + { + value: initialRoomOptions.autoDeletionDate + ? new Date(initialRoomOptions.autoDeletionDate).getHours() + : 23, + disabled: editMode + }, + editMode ? [] : [Validators.min(0), Validators.max(23)] + ], + autoDeletionMinute: [ + { + value: initialRoomOptions.autoDeletionDate + ? new Date(initialRoomOptions.autoDeletionDate).getMinutes() + : 59, + disabled: editMode + }, + editMode ? [] : [Validators.min(0), Validators.max(59)] + ] + }, + { + // Apply future date-time validation only if not in edit mode + validators: editMode + ? [] + : [ + (control: AbstractControl): ValidationErrors | null => { + const date = control.get('autoDeletionDate')?.value as Date | null; + const hour = control.get('autoDeletionHour')?.value as number | null; + const minute = control.get('autoDeletionMinute')?.value as number | null; + + if (!date) return null; + + const selected = new Date(date); + selected.setHours(hour ?? 0, minute ?? 0, 0, 0); + + const now = new Date(); + now.setMinutes(now.getMinutes() + 61, 0, 0); // Ensure at least 1 hour in the future + + return selected.getTime() < now.getTime() ? { minFutureDateTime: true } : null; + } + ] + } + ) }, { id: 'recording', @@ -87,13 +133,32 @@ export class RoomWizardStateService { isCompleted: editMode, // In edit mode, all editable steps are completed isActive: editMode, // Only active in edit mode isVisible: true, - validationFormGroup: this._formBuilder.group({ - enabled: [ - initialRoomOptions.preferences?.recordingPreferences?.enabled ?? - this.DEFAULT_PREFERENCES.recordingPreferences.enabled - ] - }), - order: 2 + formGroup: this.formBuilder.group({ + recordingEnabled: initialRoomOptions.preferences!.recordingPreferences.enabled + ? 'enabled' + : 'disabled', + allowAccessTo: initialRoomOptions.preferences!.recordingPreferences.allowAccessTo + }) + }, + { + id: 'recordingTrigger', + label: 'Recording Trigger', + isCompleted: editMode, // In edit mode, all editable steps are completed + isActive: false, + isVisible: false, // Initially hidden, will be shown based on recording settings + formGroup: this.formBuilder.group({ + triggerType: 'manual' + }) + }, + { + id: 'recordingLayout', + label: 'Recording Layout', + isCompleted: editMode, // In edit mode, all editable steps are completed + isActive: false, + isVisible: false, // Initially hidden, will be shown based on recording settings + formGroup: this.formBuilder.group({ + layoutType: 'grid' + }) }, { id: 'preferences', @@ -101,35 +166,19 @@ export class RoomWizardStateService { isCompleted: editMode, // In edit mode, all editable steps are completed isActive: false, isVisible: true, - validationFormGroup: this._formBuilder.group({ - chatPreferences: this._formBuilder.group({ - enabled: [ - initialRoomOptions.preferences?.chatPreferences?.enabled ?? - this.DEFAULT_PREFERENCES.chatPreferences.enabled - ] - }), - virtualBackgroundPreferences: this._formBuilder.group({ - enabled: [ - initialRoomOptions.preferences?.virtualBackgroundPreferences?.enabled ?? - this.DEFAULT_PREFERENCES.virtualBackgroundPreferences.enabled - ] - }) - }), - order: 5 + formGroup: this.formBuilder.group({ + chatEnabled: initialRoomOptions.preferences!.chatPreferences.enabled, + virtualBackgroundsEnabled: initialRoomOptions.preferences!.virtualBackgroundPreferences.enabled + }) } ]; - this._steps.next(baseSteps); + this._steps.set(baseSteps); + const initialStepIndex = editMode ? 1 : 0; // Skip basic step in edit mode + this._currentStepIndex.set(initialStepIndex); - // Set the initial step index after a short delay to ensure steps are processed - setTimeout(() => { - const initialStepIndex = editMode ? 1 : 0; // Skip basic step in edit mode - console.log('Setting initial step index:', initialStepIndex); - this._currentStepIndex.next(initialStepIndex); - - // Update step visibility after index is set - this.updateStepVisibility(); - }, 0); + // Update step visibility after index is set + this.updateStepsVisibility(); } /** @@ -139,8 +188,7 @@ export class RoomWizardStateService { * @param stepData - The data to update in the room options */ updateStepData(stepId: string, stepData: Partial): void { - console.log(`updateStepData called - stepId: '${stepId}', stepData:`, stepData); - const currentOptions = this._roomOptions.value; + const currentOptions = this._roomOptions(); let updatedOptions: MeetRoomOptions; switch (stepId) { @@ -148,6 +196,7 @@ export class RoomWizardStateService { updatedOptions = { ...currentOptions }; + // Only update fields that are explicitly provided if ('roomIdPrefix' in stepData) { updatedOptions.roomIdPrefix = stepData.roomIdPrefix; @@ -155,8 +204,8 @@ export class RoomWizardStateService { if ('autoDeletionDate' in stepData) { updatedOptions.autoDeletionDate = stepData.autoDeletionDate; } - break; + break; case 'recording': updatedOptions = { ...currentOptions, @@ -169,13 +218,11 @@ export class RoomWizardStateService { } as MeetRoomPreferences }; break; - case 'recordingTrigger': case 'recordingLayout': - // These steps don't directly modify room options but could store additional metadata + // These steps don't update room options updatedOptions = { ...currentOptions }; break; - case 'preferences': updatedOptions = { ...currentOptions, @@ -196,77 +243,41 @@ export class RoomWizardStateService { } as MeetRoomPreferences }; break; - default: console.warn(`Unknown step ID: ${stepId}`); updatedOptions = currentOptions; } - this._roomOptions.next(updatedOptions); - console.log(`Updated room options for step '${stepId}':`, updatedOptions); - this.updateStepVisibility(); + this._roomOptions.set(updatedOptions); + this.updateStepsVisibility(); } /** * Updates the visibility of wizard steps based on current room options. * For example, recording-related steps are only visible when recording is enabled. */ - private updateStepVisibility(): void { - const currentSteps = this._steps.value; - const currentOptions = this._roomOptions.value; - const recordingEnabled = currentOptions.preferences?.recordingPreferences?.enabled ?? false; + private updateStepsVisibility(): void { + const currentSteps = this._steps(); + const currentOptions = this._roomOptions(); + const recordingEnabled = currentOptions.preferences?.recordingPreferences.enabled ?? false; - // Determine if we're in edit mode by checking if basic step is completed and disabled - const basicStep = currentSteps.find((step) => step.id === 'basic'); - const isEditMode = !!basicStep?.validationFormGroup?.disabled; - - // Remove recording-specific steps if they exist - const filteredSteps = currentSteps.filter((step) => !['recordingTrigger', 'recordingLayout'].includes(step.id)); - - if (recordingEnabled) { - // Add recording-specific steps - const recordingSteps: WizardStep[] = [ - { - id: 'recordingTrigger', - label: 'Recording Trigger', - isCompleted: isEditMode, // In edit mode, mark as completed - isActive: false, - isVisible: true, - validationFormGroup: this._formBuilder.group({ - type: ['manual'] - }), - order: 3 - }, - { - id: 'recordingLayout', - label: 'Recording Layout', - isCompleted: isEditMode, // In edit mode, mark as completed - isActive: false, - isVisible: true, - validationFormGroup: this._formBuilder.group({ - layout: ['GRID'], - access: ['moderator-only'] - }), - order: 4 - } - ]; - - // Insert recording steps at the correct position (after recording step) - filteredSteps.splice(2, 0, ...recordingSteps); - } - - // Reorder steps based on visibility - filteredSteps.forEach((step, index) => { - step.order = index + 1; + // Update recording steps visibility based on recordingEnabled + const updatedSteps = currentSteps.map((step) => { + if (step.id === 'recordingTrigger' || step.id === 'recordingLayout') { + return { + ...step, + isVisible: recordingEnabled // Only show if recording is enabled + }; + } + return step; }); - - this._steps.next(filteredSteps); + this._steps.set(updatedSteps); } goToNextStep(): boolean { - const currentIndex = this._currentStepIndex.value; - const steps = this._steps.value; - const visibleSteps = steps.filter((step) => step.isVisible); + const currentIndex = this._currentStepIndex(); + const visibleSteps = this._visibleSteps(); + if (currentIndex < visibleSteps.length - 1) { // Mark current step as completed const currentStep = visibleSteps[currentIndex]; @@ -277,17 +288,18 @@ export class RoomWizardStateService { const nextStep = visibleSteps[currentIndex + 1]; nextStep.isActive = true; - this._currentStepIndex.next(currentIndex + 1); - this._steps.next([...steps]); + this._currentStepIndex.set(currentIndex + 1); + const steps = this._steps(); + this._steps.set([...steps]); // Trigger reactivity return true; } + return false; } goToPreviousStep(): boolean { - const currentIndex = this._currentStepIndex.value; - const steps = this._steps.value; - const visibleSteps = steps.filter((step) => step.isVisible); + const currentIndex = this._currentStepIndex(); + const visibleSteps = this._visibleSteps(); if (currentIndex > 0) { // Deactivate current step @@ -298,34 +310,26 @@ export class RoomWizardStateService { const previousStep = visibleSteps[currentIndex - 1]; previousStep.isActive = true; - this._currentStepIndex.next(currentIndex - 1); - this._steps.next([...steps]); + this._currentStepIndex.set(currentIndex - 1); + const steps = this._steps(); + this._steps.set([...steps]); // Trigger reactivity return true; } + return false; } - getCurrentStep(): WizardStep | null { - const steps = this._steps.value; - const visibleSteps = steps.filter((step) => step.isVisible); - const currentIndex = this._currentStepIndex.value; - - return visibleSteps[currentIndex] || null; - } - - getCurrentStepIndex(): number { - return this._currentStepIndex.value; - } - - getVisibleSteps(): WizardStep[] { - return this._steps.value.filter((step) => step.isVisible); - } - goToStep(targetIndex: number): boolean { - const visibleSteps = this.getVisibleSteps(); + const currentIndex = this._currentStepIndex(); + if (targetIndex === currentIndex) { + return false; // No change if the target index is the same as current + } + + const visibleSteps = this._visibleSteps(); + if (targetIndex >= 0 && targetIndex < visibleSteps.length) { // Deactivate current step - const currentStep = this.getCurrentStep(); + const currentStep = this.currentStep(); if (currentStep) { currentStep.isActive = false; } @@ -334,54 +338,48 @@ export class RoomWizardStateService { const targetStep = visibleSteps[targetIndex]; targetStep.isActive = true; - this._currentStepIndex.next(targetIndex); - this._steps.next([...this._steps.value]); + this._currentStepIndex.set(targetIndex); + const steps = this._steps(); + this._steps.set([...steps]); // Trigger reactivity return true; } + return false; } getNavigationConfig(): WizardNavigationConfig { - const currentIndex = this._currentStepIndex.value; - const steps = this._steps.value; - const visibleSteps = steps.filter((step) => step.isVisible); - const isLastStep = currentIndex === visibleSteps.length - 1; + const currentIndex = this._currentStepIndex(); + const visibleSteps = this._visibleSteps(); const isFirstStep = currentIndex === 0; + const isLastStep = currentIndex === visibleSteps.length - 1; - // Determine if we're in edit mode - const basicStep = steps.find((step) => step.id === 'basic'); - const isEditMode = !!(basicStep?.isCompleted && basicStep.validationFormGroup?.disabled); + const isEditMode = this.isEditMode(); + const isSomeStepInvalid = visibleSteps.some((step) => step.formGroup.invalid); return { showPrevious: !isFirstStep, showNext: !isLastStep, showCancel: true, showFinish: isLastStep, - showQuickCreate: !isEditMode && isFirstStep, + showSkipAndFinish: !isEditMode && isFirstStep, + disableFinish: isSomeStepInvalid, nextLabel: 'Next', previousLabel: 'Previous', finishLabel: isEditMode ? 'Update Room' : 'Create Room', - isNextDisabled: false, - isPreviousDisabled: isFirstStep + skipAndFinishLabel: 'Create with defaults' }; } /** - * Gets the current room options configured in the wizard. - * @returns The current MeetRoomOptions object + * Checks if the wizard is in edit mode. + * Edit mode is determined by whether the basic step is completed and its form is disabled. + * @returns True if in edit mode, false otherwise */ - getRoomOptions(): MeetRoomOptions { - const options = this._roomOptions.getValue(); - console.log('Getting room options:', options); - return options; - } - - /** - * Checks if the wizard was skipped (user is still on the first step). - * @returns True if the wizard was skipped, false otherwise - */ - isWizardSkipped(): boolean { - return this._currentStepIndex.getValue() === 0; + private isEditMode(): boolean { + const visibleSteps = this._visibleSteps(); + const basicStep = visibleSteps.find((step) => step.id === 'basic'); + const isEditMode = !!basicStep && basicStep.isCompleted && basicStep.formGroup.disabled; + return isEditMode; } /** @@ -389,11 +387,10 @@ export class RoomWizardStateService { */ resetWizard(): void { const defaultOptions: MeetRoomOptions = { - preferences: this.DEFAULT_PREFERENCES + preferences: DEFAULT_PREFERENCES }; - - this._roomOptions.next(defaultOptions); - this._currentStepIndex.next(0); - this.initializeWizard(); + this._roomOptions.set(defaultOptions); + this._steps.set([]); + this._currentStepIndex.set(0); } } diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 286b524..13257f9 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,11 +1,12 @@ -import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideRouter } from '@angular/router'; -import { OpenViduComponentsModule, OpenViduComponentsConfig } from 'openvidu-components-angular'; -import { environment } from '@environment/environment'; import { routes } from '@app/app.routes'; +import { environment } from '@environment/environment'; import { httpInterceptor } from '@lib/interceptors/index'; +import { OpenViduComponentsConfig, OpenViduComponentsModule } from 'openvidu-components-angular'; const ovComponentsconfig: OpenViduComponentsConfig = { production: environment.production @@ -17,6 +18,10 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - provideHttpClient(withInterceptors([httpInterceptor])) + provideHttpClient(withInterceptors([httpInterceptor])), + { + provide: STEPPER_GLOBAL_OPTIONS, + useValue: { showError: true } // Show error messages in stepper + } ] };