From 0c7b1ae1c148df76e4aa5c58f768a17b3df2bb98 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Mon, 7 Jul 2025 16:15:23 +0200 Subject: [PATCH] frontend: refactor wizard state service to use signals for state management and enhance all related components by removing unnecessary params and methods. Configured app to show error messages in the stepper for better user feedback. --- .../selectable-card.component.html | 4 +- .../selectable-card.component.ts | 110 ++--- .../step-indicator.component.html | 15 +- .../step-indicator.component.ts | 145 ++----- .../wizard-nav/wizard-nav.component.html | 27 +- .../wizard-nav/wizard-nav.component.ts | 116 ++---- .../src/lib/models/wizard.model.ts | 31 +- .../room-wizard/room-wizard.component.html | 95 ++--- .../room-wizard/room-wizard.component.ts | 137 ++---- .../basic-info/basic-info.component.html | 18 +- .../steps/basic-info/basic-info.component.ts | 83 +--- .../recording-layout.component.html | 4 +- .../recording-layout.component.ts | 59 +-- .../recording-preferences.component.html | 3 + .../recording-preferences.component.ts | 88 +--- .../recording-trigger.component.html | 4 +- .../recording-trigger.component.ts | 54 +-- .../room-preferences.component.ts | 55 +-- .../src/lib/services/wizard-state.service.ts | 391 +++++++++--------- frontend/src/app/app.config.ts | 13 +- 20 files changed, 491 insertions(+), 961 deletions(-) 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 + } ] };