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.
This commit is contained in:
parent
ca22bd8a01
commit
0c7b1ae1c1
@ -13,7 +13,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Pro Badge -->
|
<!-- Pro Badge -->
|
||||||
@if (option.isPro && showProBadge) {
|
@if (option.isPro && showProBadge) {
|
||||||
<ov-pro-feature-badge badgeText="PRO"></ov-pro-feature-badge>
|
<ov-pro-feature-badge [badgeText]="proBadgeText" [badgeIcon]="proBadgeIcon"></ov-pro-feature-badge>
|
||||||
} @else if (showSelectionIndicator) {
|
} @else if (showSelectionIndicator) {
|
||||||
<!-- Selection Indicator -->
|
<!-- Selection Indicator -->
|
||||||
<div class="selection-indicator">
|
<div class="selection-indicator">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<!-- Image Section -->
|
<!-- Image Section -->
|
||||||
@if (shouldShowImage()) {
|
@if (shouldShowImage()) {
|
||||||
<div class="card-image" [style.aspect-ratio]="getImageAspectRatio()">
|
<div class="card-image" [style.aspect-ratio]="imageAspectRatio">
|
||||||
<img [src]="option.imageUrl" [alt]="option.title + ' preview'" loading="lazy" />
|
<img [src]="option.imageUrl" [alt]="option.title + ' preview'" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export interface SelectableOption {
|
|||||||
isPro?: boolean;
|
isPro?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
value?: any; // Additional data associated with the option
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +24,7 @@ export interface SelectableOption {
|
|||||||
export interface SelectionEvent {
|
export interface SelectionEvent {
|
||||||
optionId: string;
|
optionId: string;
|
||||||
option: SelectableOption;
|
option: SelectableOption;
|
||||||
previousSelection?: string;
|
previousSelection?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -45,7 +44,7 @@ export class SelectableCardComponent {
|
|||||||
* Currently selected value(s)
|
* Currently selected value(s)
|
||||||
* Can be a string (single select) or string[] (multi select)
|
* 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
|
* Whether multiple options can be selected simultaneously
|
||||||
@ -53,23 +52,6 @@ export class SelectableCardComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() allowMultiSelect: boolean = false;
|
@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)
|
* Whether to show the selection indicator (radio button)
|
||||||
* @default true
|
* @default true
|
||||||
@ -82,6 +64,18 @@ export class SelectableCardComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() showProBadge: boolean = true;
|
@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
|
* Whether to show the recommended badge
|
||||||
* @default true
|
* @default true
|
||||||
@ -107,27 +101,27 @@ export class SelectableCardComponent {
|
|||||||
@Input() imageAspectRatio: string = '16/9';
|
@Input() imageAspectRatio: string = '16/9';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom icon for the PRO badge
|
* Whether the card should show hover effects
|
||||||
* @default 'star'
|
* @default true
|
||||||
*/
|
*/
|
||||||
@Input() proBadgeIcon: string = 'star';
|
@Input() enableHover: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom text for the PRO badge
|
* Whether the card should show selection animations
|
||||||
* @default 'PRO'
|
* @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
|
* Event emitted when an option is selected
|
||||||
*/
|
*/
|
||||||
@Output() optionSelected = new EventEmitter<SelectionEvent>();
|
@Output() optionSelected = new EventEmitter<SelectionEvent>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Event emitted when the card is clicked (even if selection doesn't change)
|
|
||||||
*/
|
|
||||||
@Output() cardClicked = new EventEmitter<SelectableOption>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event emitted when the card is hovered
|
* Event emitted when the card is hovered
|
||||||
*/
|
*/
|
||||||
@ -157,48 +151,17 @@ export class SelectableCardComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit card clicked event
|
|
||||||
this.cardClicked.emit(this.option);
|
|
||||||
|
|
||||||
// Handle selection logic
|
|
||||||
const wasSelected = this.isOptionSelected(optionId);
|
const wasSelected = this.isOptionSelected(optionId);
|
||||||
let newSelection: string | string[] | null;
|
if (!this.allowMultiSelect && wasSelected) {
|
||||||
let previousSelection: string | undefined;
|
return; // No change if already selected
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit selection event
|
// Emit selection event
|
||||||
const selectionEvent: SelectionEvent = {
|
const selectionEvent: SelectionEvent = {
|
||||||
optionId,
|
optionId,
|
||||||
option: this.option,
|
option: this.option,
|
||||||
previousSelection
|
previousSelection: this.selectedValue
|
||||||
};
|
};
|
||||||
|
|
||||||
this.optionSelected.emit(selectionEvent);
|
this.optionSelected.emit(selectionEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,27 +192,21 @@ export class SelectableCardComponent {
|
|||||||
if (this.isOptionSelected(this.option.id)) {
|
if (this.isOptionSelected(this.option.id)) {
|
||||||
classes.push('selected');
|
classes.push('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.option.recommended && this.showRecommendedBadge) {
|
if (this.option.recommended && this.showRecommendedBadge) {
|
||||||
classes.push('recommended');
|
classes.push('recommended');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.option.isPro && this.showProBadge) {
|
if (this.option.isPro && this.showProBadge) {
|
||||||
classes.push('pro-feature');
|
classes.push('pro-feature');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.option.disabled) {
|
if (this.option.disabled) {
|
||||||
classes.push('disabled');
|
classes.push('disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.enableHover) {
|
if (!this.enableHover) {
|
||||||
classes.push('no-hover');
|
classes.push('no-hover');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.enableAnimations) {
|
if (!this.enableAnimations) {
|
||||||
classes.push('no-animations');
|
classes.push('no-animations');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.customClasses) {
|
if (this.customClasses) {
|
||||||
classes.push(this.customClasses);
|
classes.push(this.customClasses);
|
||||||
}
|
}
|
||||||
@ -278,21 +235,17 @@ export class SelectableCardComponent {
|
|||||||
if (this.option.recommended) {
|
if (this.option.recommended) {
|
||||||
statusParts.push('Recommended');
|
statusParts.push('Recommended');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.option.isPro) {
|
if (this.option.isPro) {
|
||||||
statusParts.push('PRO feature');
|
statusParts.push('PRO feature');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.option.disabled) {
|
if (this.option.disabled) {
|
||||||
statusParts.push('Disabled');
|
statusParts.push('Disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isOptionSelected(this.option.id)) {
|
if (this.isOptionSelected(this.option.id)) {
|
||||||
statusParts.push('Selected');
|
statusParts.push('Selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusLabel = statusParts.length > 0 ? `. ${statusParts.join(', ')}` : '';
|
const statusLabel = statusParts.length > 0 ? `. ${statusParts.join(', ')}` : '';
|
||||||
|
|
||||||
return `${baseLabel}${statusLabel}`;
|
return `${baseLabel}${statusLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,11 +265,4 @@ export class SelectableCardComponent {
|
|||||||
}
|
}
|
||||||
return !this.shouldShowImage() && !!this.option.icon;
|
return !this.shouldShowImage() && !!this.option.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get image aspect ratio styles
|
|
||||||
*/
|
|
||||||
getImageAspectRatio(): string {
|
|
||||||
return this.imageAspectRatio;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,22 @@
|
|||||||
<div class="step-indicator-wrapper" [attr.data-layout]="layoutType$ | async">
|
<div class="step-indicator-wrapper" [attr.data-layout]="layoutType$ | async">
|
||||||
<mat-stepper
|
<mat-stepper
|
||||||
#stepper
|
#stepper
|
||||||
[selectedIndex]="safeCurrentStepIndex"
|
[selectedIndex]="currentStepIndex()"
|
||||||
[orientation]="(stepperOrientation$ | async)!"
|
[orientation]="(stepperOrientation$ | async)!"
|
||||||
[linear]="false"
|
[linear]="false"
|
||||||
class="wizard-stepper"
|
class="wizard-stepper"
|
||||||
[attr.data-layout]="layoutType$ | async"
|
[attr.data-layout]="layoutType$ | async"
|
||||||
(selectionChange)="onStepClick($event)"
|
(selectionChange)="onStepClick($event)"
|
||||||
>
|
>
|
||||||
@for (step of visibleSteps; track step.id; let i = $index) {
|
@for (step of visibleSteps(); track step.id; let i = $index) {
|
||||||
<mat-step
|
<mat-step
|
||||||
[stepControl]="step.validationFormGroup"
|
[stepControl]="step.formGroup"
|
||||||
[state]="getStepState(step)"
|
[state]="getStepState(step)"
|
||||||
|
errorMessage="Invalid fields"
|
||||||
[editable]="true"
|
[editable]="true"
|
||||||
[label]="step.label"
|
[label]="step.label"
|
||||||
[completed]="step.isCompleted"
|
[completed]="step.isCompleted"
|
||||||
>
|
>
|
||||||
<!-- Custom step icon template -->
|
|
||||||
<ng-template matStepperIcon="edit">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template matStepperIcon="done">
|
|
||||||
<mat-icon>check</mat-icon>
|
|
||||||
</ng-template>
|
|
||||||
</mat-step>
|
</mat-step>
|
||||||
}
|
}
|
||||||
</mat-stepper>
|
</mat-stepper>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
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 { CommonModule } from '@angular/common';
|
||||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
import { Component, computed, input, output } from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIcon } from '@angular/material/icon';
|
|
||||||
import { MatStepperModule } from '@angular/material/stepper';
|
import { MatStepperModule } from '@angular/material/stepper';
|
||||||
import { WizardStep } from '@lib/models';
|
import { WizardStep } from '@lib/models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -13,29 +11,28 @@ import { map } from 'rxjs/operators';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-step-indicator',
|
selector: 'ov-step-indicator',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatStepperModule, MatIcon, MatButtonModule, ReactiveFormsModule],
|
imports: [CommonModule, MatStepperModule, ReactiveFormsModule],
|
||||||
templateUrl: './step-indicator.component.html',
|
templateUrl: './step-indicator.component.html',
|
||||||
styleUrl: './step-indicator.component.scss'
|
styleUrl: './step-indicator.component.scss'
|
||||||
})
|
})
|
||||||
export class StepIndicatorComponent implements OnChanges {
|
export class StepIndicatorComponent {
|
||||||
@Input() steps: WizardStep[] = [];
|
steps = input.required<WizardStep[]>();
|
||||||
@Input() allowNavigation: boolean = false;
|
currentStepIndex = input.required<number>();
|
||||||
@Input() editMode: boolean = false; // New input for edit mode
|
allowNavigation = input<boolean>(true);
|
||||||
@Input() currentStepIndex: number = 0;
|
editMode = input<boolean>(false);
|
||||||
@Output() stepClick = new EventEmitter<{ step: WizardStep; index: number }>();
|
|
||||||
@Output() layoutChange = new EventEmitter<'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact'>();
|
stepClick = output<{ index: number; step: WizardStep }>();
|
||||||
|
|
||||||
|
visibleSteps = computed<WizardStep[]>(() => this.steps().filter((step) => step.isVisible));
|
||||||
|
|
||||||
visibleSteps: WizardStep[] = [];
|
|
||||||
stepperOrientation$: Observable<StepperOrientation>;
|
stepperOrientation$: Observable<StepperOrientation>;
|
||||||
layoutType$: Observable<'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact'>;
|
layoutType$: Observable<'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact'>;
|
||||||
stepControls: { [key: string]: FormControl } = {};
|
|
||||||
|
|
||||||
constructor(private breakpointObserver: BreakpointObserver) {
|
constructor(private breakpointObserver: BreakpointObserver) {
|
||||||
// Enhanced responsive strategy:
|
// Enhanced responsive strategy:
|
||||||
// - Large desktop (>1200px): Vertical sidebar for space efficiency
|
// - Large desktop (>1200px): Vertical sidebar for space efficiency
|
||||||
// - Medium desktop (768-1200px): Horizontal compact
|
// - Medium desktop (768-1200px): Horizontal compact
|
||||||
// - Tablet/Mobile (<768px): Vertical compact
|
// - Tablet/Mobile (<768px): Vertical compact
|
||||||
|
|
||||||
const breakpointState$ = this.breakpointObserver.observe([
|
const breakpointState$ = this.breakpointObserver.observe([
|
||||||
'(min-width: 1200px)',
|
'(min-width: 1200px)',
|
||||||
'(min-width: 768px)',
|
'(min-width: 768px)',
|
||||||
@ -58,128 +55,34 @@ export class StepIndicatorComponent implements OnChanges {
|
|||||||
return layoutType === 'horizontal-compact' ? 'horizontal' : 'vertical';
|
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) {
|
onStepClick(event: StepperSelectionEvent) {
|
||||||
if (this.allowNavigation) {
|
if (this.allowNavigation()) {
|
||||||
const step = this.visibleSteps[event.selectedIndex];
|
const index = event.selectedIndex;
|
||||||
this.stepClick.emit({ step, 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 {
|
} else {
|
||||||
console.warn('Navigation is not allowed. Step click ignored:', event.selectedIndex);
|
console.warn('Navigation is not allowed. Step click ignored:', event.selectedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isStepClickable(step: WizardStep): boolean {
|
getStepState(step: WizardStep): StepState {
|
||||||
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' {
|
|
||||||
if (step.isCompleted && !step.isActive) {
|
if (step.isCompleted && !step.isActive) {
|
||||||
return 'done';
|
return 'done';
|
||||||
}
|
}
|
||||||
|
if (step.isActive && step.formGroup?.invalid) {
|
||||||
if (step.isActive && step.validationFormGroup?.invalid) {
|
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.isActive) {
|
if (step.isActive) {
|
||||||
return 'edit';
|
return 'edit';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.isCompleted) {
|
|
||||||
return 'done';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'number';
|
return 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
getStepIcon(step: WizardStep): string {
|
|
||||||
if (step.isCompleted) {
|
|
||||||
return 'check';
|
|
||||||
}
|
|
||||||
if (step.isActive) {
|
|
||||||
return 'edit';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<nav class="wizard-navigation" [class.loading]="config.isLoading" [class.compact]="config.isCompact" id="wizard-navigation">
|
<nav class="wizard-navigation" [class.loading]="false" [class.compact]="false" id="wizard-navigation">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<!-- Cancel Button -->
|
<!-- Cancel Button -->
|
||||||
@if (config.showCancel) {
|
@if (config.showCancel) {
|
||||||
@ -24,7 +24,6 @@
|
|||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
class="prev-btn"
|
class="prev-btn"
|
||||||
id="wizard-previous-btn"
|
id="wizard-previous-btn"
|
||||||
[disabled]="config.isPreviousDisabled"
|
|
||||||
(click)="onPrevious()"
|
(click)="onPrevious()"
|
||||||
[attr.aria-label]="'Go to previous step'"
|
[attr.aria-label]="'Go to previous step'"
|
||||||
>
|
>
|
||||||
@ -33,50 +32,46 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (config.showQuickCreate) {
|
@if (config.showSkipAndFinish) {
|
||||||
<!-- Skip Wizard Button -->
|
<!-- Skip Wizard Button -->
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
class="skip-btn"
|
class="finish-btn"
|
||||||
id="wizard-quick-create-btn"
|
id="wizard-quick-create-btn"
|
||||||
|
[disabled]="config.disableFinish ?? false"
|
||||||
(click)="skipAndFinish()"
|
(click)="skipAndFinish()"
|
||||||
type="button"
|
[attr.aria-label]="'Skip wizard and finish'"
|
||||||
>
|
>
|
||||||
<mat-icon>bolt</mat-icon>
|
<mat-icon>bolt</mat-icon>
|
||||||
Create with defaults
|
{{ config.skipAndFinishLabel || 'Skip and Finish' }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Next/Continue Button -->
|
<!-- Next Button -->
|
||||||
@if (config.showNext) {
|
@if (config.showNext) {
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
class="next-btn"
|
class="next-btn"
|
||||||
id="wizard-next-btn"
|
id="wizard-next-btn"
|
||||||
[disabled]="config.isNextDisabled"
|
|
||||||
(click)="onNext()"
|
(click)="onNext()"
|
||||||
[attr.aria-label]="'Continue to next step'"
|
[attr.aria-label]="'Continue to next step'"
|
||||||
>
|
>
|
||||||
{{ config.nextLabel || 'Next' }}
|
{{ config.nextLabel || 'Next' }}
|
||||||
<mat-icon class="trailing-icon">
|
<mat-icon class="trailing-icon">chevron_right</mat-icon>
|
||||||
{{ config.isLoading ? 'hourglass_empty' : 'chevron_right' }}
|
|
||||||
</mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Finish/Save Button -->
|
<!-- Finish Button -->
|
||||||
@if (config.showFinish) {
|
@if (config.showFinish) {
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
class="finish-btn"
|
class="finish-btn"
|
||||||
id="wizard-finish-btn"
|
id="wizard-finish-btn"
|
||||||
[disabled]="config.isFinishDisabled"
|
[disabled]="config.disableFinish ?? false"
|
||||||
(click)="onFinish()"
|
(click)="onFinish()"
|
||||||
[attr.aria-label]="'Complete wizard and save changes'"
|
[attr.aria-label]="'Complete wizard and save changes'"
|
||||||
>
|
>
|
||||||
<mat-icon class="leading-icon">
|
<mat-icon class="leading-icon">check</mat-icon>
|
||||||
{{ config.isLoading ? 'hourglass_empty' : 'check' }}
|
|
||||||
</mat-icon>
|
|
||||||
{{ config.finishLabel || 'Finish' }}
|
{{ config.finishLabel || 'Finish' }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
|
||||||
import { MatButton } from '@angular/material/button';
|
import { MatButton } from '@angular/material/button';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import type { WizardNavigationConfig, WizardNavigationEvent } from '@lib/models';
|
import type { WizardNavigationConfig, WizardNavigationEvent } from '@lib/models';
|
||||||
@ -7,30 +6,25 @@ import type { WizardNavigationConfig, WizardNavigationEvent } from '@lib/models'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-wizard-nav',
|
selector: 'ov-wizard-nav',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatButton, MatIcon],
|
imports: [MatButton, MatIcon],
|
||||||
templateUrl: './wizard-nav.component.html',
|
templateUrl: './wizard-nav.component.html',
|
||||||
styleUrl: './wizard-nav.component.scss'
|
styleUrl: './wizard-nav.component.scss'
|
||||||
})
|
})
|
||||||
export class WizardNavComponent implements OnInit, OnChanges {
|
export class WizardNavComponent {
|
||||||
/**
|
/**
|
||||||
* Navigation configuration with default values
|
* Navigation configuration with default values
|
||||||
*/
|
*/
|
||||||
@Input() config: WizardNavigationConfig = {
|
@Input() config: WizardNavigationConfig = {
|
||||||
showPrevious: true,
|
showPrevious: false,
|
||||||
showNext: true,
|
showNext: true,
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
showFinish: false,
|
showFinish: false,
|
||||||
showQuickCreate: true,
|
showSkipAndFinish: false,
|
||||||
|
disableFinish: false,
|
||||||
nextLabel: 'Next',
|
nextLabel: 'Next',
|
||||||
previousLabel: 'Previous',
|
previousLabel: 'Previous',
|
||||||
cancelLabel: 'Cancel',
|
cancelLabel: 'Cancel',
|
||||||
finishLabel: 'Finish',
|
finishLabel: 'Finish'
|
||||||
isNextDisabled: false,
|
|
||||||
isPreviousDisabled: false,
|
|
||||||
isFinishDisabled: false,
|
|
||||||
isLoading: false,
|
|
||||||
isCompact: false,
|
|
||||||
ariaLabel: 'Wizard navigation'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,90 +45,56 @@ export class WizardNavComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Output() navigate = new EventEmitter<WizardNavigationEvent>();
|
@Output() navigate = new EventEmitter<WizardNavigationEvent>();
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.validateConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
|
||||||
if (changes['config']) {
|
|
||||||
this.validateConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates navigation configuration
|
|
||||||
*/
|
|
||||||
private validateConfig() {
|
|
||||||
if (!this.config.nextLabel) this.config.nextLabel = 'Next';
|
|
||||||
if (!this.config.previousLabel) this.config.previousLabel = 'Previous';
|
|
||||||
if (!this.config.cancelLabel) this.config.cancelLabel = 'Cancel';
|
|
||||||
if (!this.config.finishLabel) this.config.finishLabel = 'Finish';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle previous step navigation
|
|
||||||
*/
|
|
||||||
onPrevious() {
|
onPrevious() {
|
||||||
if (!this.config.isPreviousDisabled && !this.config.isLoading) {
|
if (!this.config.showPrevious) return;
|
||||||
const event: WizardNavigationEvent = {
|
|
||||||
action: 'previous',
|
|
||||||
currentStepId: this.currentStepId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.previous.emit(event);
|
const event: WizardNavigationEvent = {
|
||||||
this.navigate.emit(event);
|
action: 'previous',
|
||||||
}
|
currentStepIndex: this.currentStepId
|
||||||
|
};
|
||||||
|
this.previous.emit(event);
|
||||||
|
this.navigate.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle next step navigation
|
|
||||||
*/
|
|
||||||
onNext() {
|
onNext() {
|
||||||
if (!this.config.isNextDisabled && !this.config.isLoading) {
|
if (!this.config.showNext) return;
|
||||||
const event: WizardNavigationEvent = {
|
|
||||||
action: 'next',
|
|
||||||
currentStepId: this.currentStepId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.next.emit(event);
|
const event: WizardNavigationEvent = {
|
||||||
this.navigate.emit(event);
|
action: 'next',
|
||||||
}
|
currentStepIndex: this.currentStepId
|
||||||
|
};
|
||||||
|
this.next.emit(event);
|
||||||
|
this.navigate.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle wizard cancellation
|
|
||||||
*/
|
|
||||||
onCancel() {
|
onCancel() {
|
||||||
if (!this.config.isLoading) {
|
if (!this.config.showCancel) return;
|
||||||
const event: WizardNavigationEvent = {
|
|
||||||
action: 'cancel',
|
|
||||||
currentStepId: this.currentStepId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.cancel.emit(event);
|
const event: WizardNavigationEvent = {
|
||||||
this.navigate.emit(event);
|
action: 'cancel',
|
||||||
}
|
currentStepIndex: this.currentStepId
|
||||||
|
};
|
||||||
|
this.cancel.emit(event);
|
||||||
|
this.navigate.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle wizard completion
|
|
||||||
*/
|
|
||||||
onFinish() {
|
onFinish() {
|
||||||
if (!this.config.isFinishDisabled && !this.config.isLoading) {
|
if (!this.config.showFinish) return;
|
||||||
const event: WizardNavigationEvent = {
|
|
||||||
action: 'finish',
|
|
||||||
currentStepId: this.currentStepId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.finish.emit(event);
|
const event: WizardNavigationEvent = {
|
||||||
this.navigate.emit(event);
|
action: 'finish',
|
||||||
}
|
currentStepIndex: this.currentStepId
|
||||||
|
};
|
||||||
|
this.finish.emit(event);
|
||||||
|
this.navigate.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
skipAndFinish() {
|
skipAndFinish() {
|
||||||
|
if (!this.config.showSkipAndFinish) return;
|
||||||
|
|
||||||
const event: WizardNavigationEvent = {
|
const event: WizardNavigationEvent = {
|
||||||
action: 'finish',
|
action: 'finish',
|
||||||
currentStepId: this.currentStepId
|
currentStepIndex: this.currentStepId
|
||||||
};
|
};
|
||||||
this.finish.emit(event);
|
this.finish.emit(event);
|
||||||
this.navigate.emit(event);
|
this.navigate.emit(event);
|
||||||
|
|||||||
@ -9,42 +9,27 @@ export interface WizardStep {
|
|||||||
isCompleted: boolean;
|
isCompleted: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isOptional?: boolean;
|
formGroup: FormGroup;
|
||||||
order: number;
|
|
||||||
validationFormGroup: FormGroup;
|
|
||||||
description?: string;
|
|
||||||
icon?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration interface for wizard navigation controls
|
* Configuration interface for wizard navigation controls
|
||||||
* Supports theming and responsive behavior
|
|
||||||
*/
|
*/
|
||||||
export interface WizardNavigationConfig {
|
export interface WizardNavigationConfig {
|
||||||
// Button visibility
|
// Button visibility flags
|
||||||
showPrevious: boolean;
|
showPrevious: boolean;
|
||||||
showNext: boolean;
|
showNext: boolean;
|
||||||
showCancel: boolean;
|
showCancel: boolean;
|
||||||
showFinish: boolean;
|
showFinish: boolean;
|
||||||
showQuickCreate: boolean; // Optional for quick create functionality
|
showSkipAndFinish: boolean; // Used for quick create actions
|
||||||
|
disableFinish?: boolean;
|
||||||
|
|
||||||
// Button labels (customizable)
|
// Button labels
|
||||||
nextLabel?: string;
|
nextLabel?: string;
|
||||||
previousLabel?: string;
|
previousLabel?: string;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
finishLabel?: string;
|
finishLabel?: string;
|
||||||
|
skipAndFinishLabel?: string;
|
||||||
// Button states
|
|
||||||
isNextDisabled: boolean;
|
|
||||||
isPreviousDisabled: boolean;
|
|
||||||
isFinishDisabled?: boolean;
|
|
||||||
|
|
||||||
// UI states
|
|
||||||
isLoading?: boolean;
|
|
||||||
isCompact?: boolean;
|
|
||||||
|
|
||||||
// Accessibility
|
|
||||||
ariaLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +37,5 @@ export interface WizardNavigationConfig {
|
|||||||
*/
|
*/
|
||||||
export interface WizardNavigationEvent {
|
export interface WizardNavigationEvent {
|
||||||
action: 'next' | 'previous' | 'cancel' | 'finish';
|
action: 'next' | 'previous' | 'cancel' | 'finish';
|
||||||
currentStepId?: number;
|
currentStepIndex?: number;
|
||||||
targetStepId?: string;
|
|
||||||
data?: any;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +1,53 @@
|
|||||||
<div class="wizard-container ov-theme-transition">
|
<div class="wizard-container ov-theme-transition">
|
||||||
<header class="wizard-header">
|
@if (steps().length !== 0) {
|
||||||
<!-- <div class="title">
|
<header class="wizard-header">
|
||||||
<mat-icon class="ov-room-icon">meeting_room</mat-icon>
|
<p class="subtitle">
|
||||||
<h2>Room Creation Wizard</h2>
|
{{
|
||||||
</div> -->
|
editMode ? 'Edit your room settings' : 'Create and configure your video room in a few simple steps'
|
||||||
<p class="subtitle">{{ editMode ? 'Edit your room settings' : 'Create and configure your video room in a few simple steps' }}</p>
|
}}
|
||||||
<ov-step-indicator
|
</p>
|
||||||
[steps]="steps"
|
<ov-step-indicator
|
||||||
[allowNavigation]="true"
|
[steps]="steps()"
|
||||||
[editMode]="editMode"
|
[currentStepIndex]="currentStepIndex()"
|
||||||
[currentStepIndex]="currentStepIndex"
|
[allowNavigation]="true"
|
||||||
(stepClick)="onStepClick($event)"
|
[editMode]="editMode"
|
||||||
(layoutChange)="onLayoutChange($event)"
|
(stepClick)="onStepClick($event)"
|
||||||
>
|
>
|
||||||
</ov-step-indicator>
|
</ov-step-indicator>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="wizard-content">
|
<main class="wizard-content">
|
||||||
<section class="step-content ov-surface">
|
<section class="step-content ov-surface">
|
||||||
@switch (currentStep?.id) {
|
@switch (currentStep()?.id) {
|
||||||
@case ('basic') {
|
@case ('basic') {
|
||||||
<ov-room-wizard-basic-info [editMode]="editMode"></ov-room-wizard-basic-info>
|
<ov-room-wizard-basic-info></ov-room-wizard-basic-info>
|
||||||
|
}
|
||||||
|
@case ('recording') {
|
||||||
|
<ov-recording-preferences></ov-recording-preferences>
|
||||||
|
}
|
||||||
|
@case ('recordingTrigger') {
|
||||||
|
<ov-recording-trigger></ov-recording-trigger>
|
||||||
|
}
|
||||||
|
@case ('recordingLayout') {
|
||||||
|
<ov-recording-layout></ov-recording-layout>
|
||||||
|
}
|
||||||
|
@case ('preferences') {
|
||||||
|
<ov-room-preferences></ov-room-preferences>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@case ('recording') {
|
</section>
|
||||||
<ov-recording-preferences></ov-recording-preferences>
|
</main>
|
||||||
}
|
|
||||||
@case ('recordingTrigger') {
|
|
||||||
<ov-recording-trigger></ov-recording-trigger>
|
|
||||||
}
|
|
||||||
@case ('recordingLayout') {
|
|
||||||
<ov-recording-layout></ov-recording-layout>
|
|
||||||
}
|
|
||||||
@case ('preferences') {
|
|
||||||
<ov-room-preferences></ov-room-preferences>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="wizard-footer">
|
<footer class="wizard-footer">
|
||||||
<ov-wizard-nav
|
<ov-wizard-nav
|
||||||
[config]="navigationConfig"
|
[config]="navigationConfig()"
|
||||||
[currentStepId]="currentStepIndex"
|
[currentStepId]="currentStepIndex()"
|
||||||
(previous)="onPrevious()"
|
(previous)="onPrevious()"
|
||||||
(next)="onNext()"
|
(next)="onNext()"
|
||||||
(cancel)="onCancel()"
|
(cancel)="onCancel()"
|
||||||
(finish)="onFinish($event)"
|
(finish)="onFinish()"
|
||||||
>
|
>
|
||||||
</ov-wizard-nav>
|
</ov-wizard-nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, computed, OnInit, Signal } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { StepIndicatorComponent, WizardNavComponent } from '@lib/components';
|
import { StepIndicatorComponent, WizardNavComponent } from '@lib/components';
|
||||||
import { WizardNavigationConfig, WizardNavigationEvent, WizardStep } from '@lib/models';
|
import { WizardNavigationConfig, WizardStep } from '@lib/models';
|
||||||
import { NavigationService, RoomService, RoomWizardStateService } from '@lib/services';
|
import { NavigationService, NotificationService, RoomService, RoomWizardStateService } from '@lib/services';
|
||||||
import { MeetRoom, MeetRoomOptions } from '@lib/typings/ce';
|
import { MeetRoomOptions } from '@lib/typings/ce';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
|
||||||
import { RoomWizardBasicInfoComponent } from './steps/basic-info/basic-info.component';
|
import { RoomWizardBasicInfoComponent } from './steps/basic-info/basic-info.component';
|
||||||
import { RecordingLayoutComponent } from './steps/recording-layout/recording-layout.component';
|
import { RecordingLayoutComponent } from './steps/recording-layout/recording-layout.component';
|
||||||
import { RecordingPreferencesComponent } from './steps/recording-preferences/recording-preferences.component';
|
import { RecordingPreferencesComponent } from './steps/recording-preferences/recording-preferences.component';
|
||||||
@ -34,79 +33,40 @@ import { RoomPreferencesComponent } from './steps/room-preferences/room-preferen
|
|||||||
templateUrl: './room-wizard.component.html',
|
templateUrl: './room-wizard.component.html',
|
||||||
styleUrl: './room-wizard.component.scss'
|
styleUrl: './room-wizard.component.scss'
|
||||||
})
|
})
|
||||||
export class RoomWizardComponent implements OnInit, OnDestroy {
|
export class RoomWizardComponent implements OnInit {
|
||||||
editMode: boolean = false;
|
editMode: boolean = false;
|
||||||
roomId: string | null = null;
|
roomId?: string;
|
||||||
existingRoomData: MeetRoomOptions | null = null;
|
existingRoomData?: MeetRoomOptions;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
steps: Signal<WizardStep[]>;
|
||||||
|
currentStep: Signal<WizardStep | undefined>;
|
||||||
steps: WizardStep[] = [];
|
currentStepIndex: Signal<number>;
|
||||||
currentStep: WizardStep | null = null;
|
navigationConfig: Signal<WizardNavigationConfig>;
|
||||||
currentStepIndex: number = 0;
|
|
||||||
currentLayout: 'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact' = 'horizontal-compact';
|
|
||||||
navigationConfig: WizardNavigationConfig = {
|
|
||||||
showPrevious: false,
|
|
||||||
showNext: true,
|
|
||||||
showCancel: true,
|
|
||||||
showFinish: false,
|
|
||||||
showQuickCreate: true,
|
|
||||||
nextLabel: 'Next',
|
|
||||||
previousLabel: 'Previous',
|
|
||||||
finishLabel: 'Create Room',
|
|
||||||
isNextDisabled: false,
|
|
||||||
isPreviousDisabled: true
|
|
||||||
};
|
|
||||||
wizardData: MeetRoomOptions = {};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private wizardState: RoomWizardStateService,
|
private wizardService: RoomWizardStateService,
|
||||||
protected roomService: RoomService,
|
protected roomService: RoomService,
|
||||||
|
protected notificationService: NotificationService,
|
||||||
private navigationService: NavigationService,
|
private navigationService: NavigationService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute
|
||||||
) {}
|
) {
|
||||||
|
this.steps = this.wizardService.steps;
|
||||||
|
this.currentStep = this.wizardService.currentStep;
|
||||||
|
this.currentStepIndex = this.wizardService.currentStepIndex;
|
||||||
|
this.navigationConfig = computed(() => this.wizardService.getNavigationConfig());
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
console.log('RoomWizard ngOnInit - starting');
|
|
||||||
|
|
||||||
// Detect edit mode from route
|
// Detect edit mode from route
|
||||||
this.detectEditMode();
|
this.detectEditMode();
|
||||||
|
|
||||||
// If in edit mode, load room data
|
// If in edit mode, load room data
|
||||||
if (this.editMode && this.roomId) {
|
if (this.editMode && this.roomId) {
|
||||||
this.navigationConfig.showQuickCreate = false;
|
|
||||||
await this.loadRoomData();
|
await this.loadRoomData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize wizard with edit mode and existing data
|
// Initialize wizard with edit mode and existing data
|
||||||
this.wizardState.initializeWizard(this.editMode, this.existingRoomData || undefined);
|
this.wizardService.initializeWizard(this.editMode, this.existingRoomData);
|
||||||
|
|
||||||
this.wizardState.steps$.pipe(takeUntil(this.destroy$)).subscribe((steps) => {
|
|
||||||
// Only update current step info after steps are available
|
|
||||||
|
|
||||||
if (steps.length > 0) {
|
|
||||||
this.steps = steps;
|
|
||||||
this.currentStep = this.wizardState.getCurrentStep();
|
|
||||||
this.currentStepIndex = this.wizardState.getCurrentStepIndex();
|
|
||||||
this.navigationConfig = this.wizardState.getNavigationConfig();
|
|
||||||
|
|
||||||
// Update navigation config for edit mode
|
|
||||||
if (this.editMode) {
|
|
||||||
this.navigationConfig.finishLabel = 'Update Room';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.wizardState.roomOptions$.pipe(takeUntil(this.destroy$)).subscribe((options) => {
|
|
||||||
this.wizardData = options;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.wizardState.currentStepIndex$.pipe(takeUntil(this.destroy$)).subscribe((index) => {
|
|
||||||
// Only update if we have visible steps
|
|
||||||
if (this.steps.filter((s) => s.isVisible).length > 0) {
|
|
||||||
this.currentStepIndex = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private detectEditMode() {
|
private detectEditMode() {
|
||||||
@ -116,7 +76,7 @@ export class RoomWizardComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Get roomId from route parameters when in edit mode
|
// Get roomId from route parameters when in edit mode
|
||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
this.roomId = this.route.snapshot.paramMap.get('roomId');
|
this.roomId = this.route.snapshot.paramMap.get('roomId') || undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,15 +84,8 @@ export class RoomWizardComponent implements OnInit, OnDestroy {
|
|||||||
if (!this.roomId) return;
|
if (!this.roomId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch room data from the service
|
const { roomIdPrefix, autoDeletionDate, preferences } = await this.roomService.getRoom(this.roomId);
|
||||||
const room: MeetRoom = await this.roomService.getRoom(this.roomId);
|
this.existingRoomData = { roomIdPrefix, autoDeletionDate, preferences };
|
||||||
|
|
||||||
// Convert MeetRoom to MeetRoomOptions
|
|
||||||
this.existingRoomData = {
|
|
||||||
roomIdPrefix: room.roomIdPrefix,
|
|
||||||
autoDeletionDate: room.autoDeletionDate,
|
|
||||||
preferences: room.preferences
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading room data:', error);
|
console.error('Error loading room data:', error);
|
||||||
// Navigate back to rooms list if room not found
|
// Navigate back to rooms list if room not found
|
||||||
@ -140,56 +93,42 @@ export class RoomWizardComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPrevious() {
|
onPrevious() {
|
||||||
this.wizardState.goToPreviousStep();
|
this.wizardService.goToPreviousStep();
|
||||||
this.currentStep = this.wizardState.getCurrentStep();
|
|
||||||
this.navigationConfig = this.wizardState.getNavigationConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNext() {
|
onNext() {
|
||||||
this.wizardState.goToNextStep();
|
this.wizardService.goToNextStep();
|
||||||
this.currentStep = this.wizardState.getCurrentStep();
|
|
||||||
this.navigationConfig = this.wizardState.getNavigationConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel() {
|
onStepClick(event: { index: number; step: WizardStep }) {
|
||||||
this.navigationService.navigateTo('rooms', undefined, true);
|
this.wizardService.goToStep(event.index);
|
||||||
this.wizardState.resetWizard();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepClick(event: { step: WizardStep; index: number }) {
|
async onCancel() {
|
||||||
this.wizardState.goToStep(event.index);
|
this.wizardService.resetWizard();
|
||||||
this.currentStep = this.wizardState.getCurrentStep();
|
await this.navigationService.navigateTo('rooms', undefined, true);
|
||||||
this.navigationConfig = this.wizardState.getNavigationConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayoutChange(layout: 'vertical-sidebar' | 'horizontal-compact' | 'vertical-compact') {
|
async onFinish() {
|
||||||
this.currentLayout = layout;
|
const roomOptions = this.wizardService.roomOptions();
|
||||||
}
|
console.log('Wizard completed with data:', roomOptions);
|
||||||
|
|
||||||
async onFinish(event: WizardNavigationEvent) {
|
|
||||||
const roomOptions = this.wizardState.getRoomOptions();
|
|
||||||
console.log('Wizard completed with data:', event, roomOptions);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.editMode && this.roomId && roomOptions.preferences) {
|
if (this.editMode && this.roomId && roomOptions.preferences) {
|
||||||
await this.roomService.updateRoom(this.roomId, roomOptions.preferences);
|
await this.roomService.updateRoom(this.roomId, roomOptions.preferences);
|
||||||
//TODO: Show success notification
|
this.notificationService.showSnackbar('Room updated successfully');
|
||||||
} else {
|
} else {
|
||||||
// Create new room
|
// Create new room
|
||||||
await this.roomService.createRoom(roomOptions);
|
await this.roomService.createRoom(roomOptions);
|
||||||
console.log('Room created successfully');
|
this.notificationService.showSnackbar('Room created successfully');
|
||||||
// TODO: Show error notification
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.navigationService.navigateTo('rooms', undefined, true);
|
await this.navigationService.navigateTo('rooms', undefined, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to ${this.editMode ? 'update' : 'create'} room:`, error);
|
const errorMessage = `Failed to ${this.editMode ? 'update' : 'create'} room`;
|
||||||
|
this.notificationService.showSnackbar(errorMessage);
|
||||||
|
console.error(errorMessage, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,9 +16,12 @@
|
|||||||
<!-- Room Prefix Field -->
|
<!-- Room Prefix Field -->
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
<mat-label>Room Name Prefix</mat-label>
|
<mat-label>Room Name Prefix</mat-label>
|
||||||
<input matInput formControlName="roomIdPrefix" placeholder="e.g. demo-room" maxlength="50" />
|
<input matInput formControlName="roomIdPrefix" placeholder="e.g. demo-room" />
|
||||||
<mat-icon matSuffix class="ov-settings-icon">label</mat-icon>
|
<mat-icon matSuffix class="ov-settings-icon">label</mat-icon>
|
||||||
<mat-hint>Optional prefix for room names. Leave empty for default naming.</mat-hint>
|
<mat-hint>Optional prefix for room names. Leave empty for default naming.</mat-hint>
|
||||||
|
@if (basicInfoForm.get('roomIdPrefix')?.hasError('maxlength')) {
|
||||||
|
<mat-error> Room name prefix cannot exceed 50 characters </mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Deletion Date Field -->
|
<!-- Deletion Date Field -->
|
||||||
@ -39,6 +42,7 @@
|
|||||||
matSuffix
|
matSuffix
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
type="button"
|
type="button"
|
||||||
|
[disabled]="basicInfoForm.get('autoDeletionDate')?.disabled"
|
||||||
(click)="clearDeletionDate()"
|
(click)="clearDeletionDate()"
|
||||||
matTooltip="Clear date selection"
|
matTooltip="Clear date selection"
|
||||||
class="clear-date-button"
|
class="clear-date-button"
|
||||||
@ -85,10 +89,14 @@
|
|||||||
<mat-icon matSuffix class="ov-settings-icon">access_time</mat-icon>
|
<mat-icon matSuffix class="ov-settings-icon">access_time</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="time-hint">
|
@if (!basicInfoForm.hasError('minFutureDateTime')) {
|
||||||
<mat-icon class="hint-icon material-symbols-outlined material-icons">auto_delete</mat-icon>
|
<div class="time-hint">
|
||||||
<span>Room will be deleted at {{ getFormattedDateTime() }}</span>
|
<mat-icon class="hint-icon material-symbols-outlined material-icons">auto_delete</mat-icon>
|
||||||
</div>
|
<span>Room will be deleted at {{ getFormattedDateTime() }}</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<mat-error> Deletion date and time must be at least one hour in the future </mat-error>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatNativeDateModule } from '@angular/material/core';
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
@ -17,7 +16,6 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
selector: 'ov-room-wizard-basic-info',
|
selector: 'ov-room-wizard-basic-info',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
@ -31,37 +29,18 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
templateUrl: './basic-info.component.html',
|
templateUrl: './basic-info.component.html',
|
||||||
styleUrl: './basic-info.component.scss'
|
styleUrl: './basic-info.component.scss'
|
||||||
})
|
})
|
||||||
export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy {
|
export class RoomWizardBasicInfoComponent implements OnDestroy {
|
||||||
@Input() editMode: boolean = false; // Input to control edit mode from parent component
|
|
||||||
basicInfoForm: FormGroup;
|
basicInfoForm: FormGroup;
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
// Arrays for time selection
|
// Arrays for time selection
|
||||||
hours = Array.from({ length: 24 }, (_, i) => ({ value: i, display: i.toString().padStart(2, '0') }));
|
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') }));
|
minutes = Array.from({ length: 60 }, (_, i) => ({ value: i, display: i.toString().padStart(2, '0') }));
|
||||||
|
|
||||||
constructor(
|
private destroy$ = new Subject<void>();
|
||||||
private fb: FormBuilder,
|
|
||||||
private wizardState: RoomWizardStateService
|
|
||||||
) {
|
|
||||||
this.basicInfoForm = this.fb.group({
|
|
||||||
roomIdPrefix: ['', [Validators.maxLength(50)]],
|
|
||||||
autoDeletionDate: [null],
|
|
||||||
autoDeletionHour: [23],
|
|
||||||
autoDeletionMinute: [59]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private wizardService: RoomWizardStateService) {
|
||||||
// Disable form controls in edit mode
|
const currentStep = this.wizardService.currentStep();
|
||||||
if (this.editMode) {
|
this.basicInfoForm = currentStep!.formGroup;
|
||||||
this.basicInfoForm.get('roomIdPrefix')?.disable();
|
|
||||||
this.basicInfoForm.get('autoDeletionDate')?.disable();
|
|
||||||
this.basicInfoForm.get('autoDeletionHour')?.disable();
|
|
||||||
this.basicInfoForm.get('autoDeletionMinute')?.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadExistingData();
|
|
||||||
|
|
||||||
this.basicInfoForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
this.basicInfoForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
this.saveFormData(value);
|
this.saveFormData(value);
|
||||||
@ -73,24 +52,6 @@ export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
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) {
|
private saveFormData(formValue: any) {
|
||||||
let autoDeletionDateTime: number | undefined = undefined;
|
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)
|
// Always save to wizard state (including when values are cleared)
|
||||||
this.wizardState.updateStepData('basic', stepData);
|
this.wizardService.updateStepData('basic', stepData);
|
||||||
}
|
|
||||||
|
|
||||||
clearForm() {
|
|
||||||
this.basicInfoForm.reset();
|
|
||||||
this.wizardState.updateStepData('basic', {
|
|
||||||
roomIdPrefix: '',
|
|
||||||
autoDeletionDate: undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get minDate(): Date {
|
get minDate(): Date {
|
||||||
return new Date();
|
const now = new Date();
|
||||||
}
|
now.setHours(now.getHours() + 1, 0, 0, 0); // Set to 1 hour in the future
|
||||||
|
return now;
|
||||||
clearDeletionDate() {
|
|
||||||
this.basicInfoForm.patchValue({
|
|
||||||
autoDeletionDate: null,
|
|
||||||
autoDeletionHour: 23, // Reset to default values
|
|
||||||
autoDeletionMinute: 59
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasDateSelected(): boolean {
|
get hasDateSelected(): boolean {
|
||||||
@ -160,4 +107,12 @@ export class RoomWizardBasicInfoComponent implements OnInit, OnDestroy {
|
|||||||
hour12: false
|
hour12: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearDeletionDate() {
|
||||||
|
this.basicInfoForm.patchValue({
|
||||||
|
autoDeletionDate: null,
|
||||||
|
autoDeletionHour: 23,
|
||||||
|
autoDeletionMinute: 59
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,8 @@
|
|||||||
[option]="option"
|
[option]="option"
|
||||||
[selectedValue]="selectedOption"
|
[selectedValue]="selectedOption"
|
||||||
[showSelectionIndicator]="true"
|
[showSelectionIndicator]="true"
|
||||||
[showProBadge]="true"
|
[showProBadge]="option.isPro ?? false"
|
||||||
[showRecommendedBadge]="false"
|
[showRecommendedBadge]="option.recommended ?? false"
|
||||||
[showImage]="true"
|
[showImage]="true"
|
||||||
[imageAspectRatio]="'1/1'"
|
[imageAspectRatio]="'1/1'"
|
||||||
[showImageAndIcon]="false"
|
[showImageAndIcon]="false"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@ -24,18 +24,14 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
templateUrl: './recording-layout.component.html',
|
templateUrl: './recording-layout.component.html',
|
||||||
styleUrl: './recording-layout.component.scss'
|
styleUrl: './recording-layout.component.scss'
|
||||||
})
|
})
|
||||||
export class RecordingLayoutComponent implements OnInit, OnDestroy {
|
export class RecordingLayoutComponent implements OnDestroy {
|
||||||
layoutForm: FormGroup;
|
layoutForm: FormGroup;
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
layoutOptions: SelectableOption[] = [
|
layoutOptions: SelectableOption[] = [
|
||||||
{
|
{
|
||||||
id: 'grid',
|
id: 'grid',
|
||||||
title: 'Grid Layout',
|
title: 'Grid Layout',
|
||||||
description: 'Show all participants in a grid view with equal sized tiles',
|
description: 'Show all participants in a grid view with equal sized tiles',
|
||||||
imageUrl: './assets/layouts/grid.png',
|
imageUrl: './assets/layouts/grid.png'
|
||||||
recommended: false,
|
|
||||||
isPro: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'speaker',
|
id: 'speaker',
|
||||||
@ -43,7 +39,8 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy {
|
|||||||
description: 'Highlight the active speaker with other participants below',
|
description: 'Highlight the active speaker with other participants below',
|
||||||
imageUrl: './assets/layouts/speaker.png',
|
imageUrl: './assets/layouts/speaker.png',
|
||||||
isPro: true,
|
isPro: true,
|
||||||
disabled: true
|
disabled: true,
|
||||||
|
recommended: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'single-speaker',
|
id: 'single-speaker',
|
||||||
@ -55,26 +52,15 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
private destroy$ = new Subject<void>();
|
||||||
private fb: FormBuilder,
|
|
||||||
private wizardState: RoomWizardStateService
|
|
||||||
) {
|
|
||||||
this.layoutForm = this.fb.group({
|
|
||||||
layoutType: ['grid'] // default to grid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private wizardService: RoomWizardStateService) {
|
||||||
// Load existing data if available
|
const currentStep = this.wizardService.currentStep();
|
||||||
this.loadExistingData();
|
this.layoutForm = currentStep!.formGroup;
|
||||||
|
|
||||||
// Subscribe to form changes for auto-save
|
|
||||||
this.layoutForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
this.layoutForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
this.saveFormData(value);
|
this.saveFormData(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save initial default value if no existing data
|
|
||||||
this.saveInitialDefaultIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -82,30 +68,9 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
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) {
|
private saveFormData(formValue: any) {
|
||||||
// Note: Recording layout type is not part of MeetRoomOptions
|
// Note: Recording layout type is not part of MeetRoomOptions
|
||||||
// This is UI state that affects recording layout but not stored in room options
|
// For now, just keep the form state
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onOptionSelect(event: SelectionEvent): void {
|
onOptionSelect(event: SelectionEvent): void {
|
||||||
@ -115,6 +80,6 @@ export class RecordingLayoutComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selectedOption(): string {
|
get selectedOption(): string {
|
||||||
return this.layoutForm.value.layoutType || 'grid'; // Default to grid if not set
|
return this.layoutForm.value.layoutType || 'grid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,9 @@
|
|||||||
<ov-selectable-card
|
<ov-selectable-card
|
||||||
[option]="option"
|
[option]="option"
|
||||||
[selectedValue]="selectedValue"
|
[selectedValue]="selectedValue"
|
||||||
|
[allowMultiSelect]="false"
|
||||||
|
[showSelectionIndicator]="true"
|
||||||
|
[showRecommendedBadge]="option.recommended ?? false"
|
||||||
(optionSelected)="onOptionSelect($event)"
|
(optionSelected)="onOptionSelect($event)"
|
||||||
></ov-selectable-card>
|
></ov-selectable-card>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@ -34,27 +34,26 @@ interface RecordingAccessOption {
|
|||||||
templateUrl: './recording-preferences.component.html',
|
templateUrl: './recording-preferences.component.html',
|
||||||
styleUrl: './recording-preferences.component.scss'
|
styleUrl: './recording-preferences.component.scss'
|
||||||
})
|
})
|
||||||
export class RecordingPreferencesComponent implements OnInit, OnDestroy {
|
export class RecordingPreferencesComponent implements OnDestroy {
|
||||||
recordingForm: FormGroup;
|
recordingForm: FormGroup;
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
isAnimatingOut = false;
|
isAnimatingOut = false;
|
||||||
|
|
||||||
recordingOptions: SelectableOption[] = [
|
recordingOptions: SelectableOption[] = [
|
||||||
{
|
|
||||||
id: 'disabled',
|
|
||||||
title: 'No Recording',
|
|
||||||
description: 'Room will not be recorded. Participants can join without recording concerns.',
|
|
||||||
icon: 'videocam_off'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'enabled',
|
id: 'enabled',
|
||||||
title: 'Allow Recording',
|
title: 'Allow Recording',
|
||||||
description:
|
description:
|
||||||
'Enable recording capabilities for this room. Recordings can be started manually or automatically.',
|
'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[] = [
|
recordingAccessOptions: RecordingAccessOption[] = [
|
||||||
{
|
{
|
||||||
value: MeetRecordingAccess.ADMIN,
|
value: MeetRecordingAccess.ADMIN,
|
||||||
@ -70,25 +69,15 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
private destroy$ = new Subject<void>();
|
||||||
private fb: FormBuilder,
|
|
||||||
private wizardState: RoomWizardStateService
|
|
||||||
) {
|
|
||||||
this.recordingForm = this.fb.group({
|
|
||||||
recordingEnabled: ['disabled'], // default to no recording
|
|
||||||
allowAccessTo: ['admin'] // default access level
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private wizardState: RoomWizardStateService) {
|
||||||
this.loadExistingData();
|
const currentStep = this.wizardState.currentStep();
|
||||||
|
this.recordingForm = currentStep!.formGroup;
|
||||||
|
|
||||||
this.recordingForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
this.recordingForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
this.saveFormData(value);
|
this.saveFormData(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save initial default value if no existing data
|
|
||||||
this.saveInitialDefaultIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -96,18 +85,6 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
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) {
|
private saveFormData(formValue: any) {
|
||||||
const enabled = formValue.recordingEnabled === 'enabled';
|
const enabled = formValue.recordingEnabled === 'enabled';
|
||||||
|
|
||||||
@ -123,16 +100,6 @@ export class RecordingPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.wizardState.updateStepData('recording', stepData);
|
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 {
|
onOptionSelect(event: SelectionEvent): void {
|
||||||
const previouslyEnabled = this.isRecordingEnabled;
|
const previouslyEnabled = this.isRecordingEnabled;
|
||||||
const willBeEnabled = event.optionId === 'enabled';
|
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 {
|
get selectedValue(): string {
|
||||||
return this.recordingForm.value.recordingEnabled;
|
return this.recordingForm.value.recordingEnabled || 'disabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRecordingEnabled(): boolean {
|
get isRecordingEnabled(): boolean {
|
||||||
return this.recordingForm.value.recordingEnabled === 'enabled';
|
return this.selectedValue === 'enabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
get shouldShowAccessSection(): boolean {
|
get shouldShowAccessSection(): boolean {
|
||||||
return this.isRecordingEnabled || this.isAnimatingOut;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,8 @@
|
|||||||
[selectedValue]="selectedOption"
|
[selectedValue]="selectedOption"
|
||||||
[allowMultiSelect]="false"
|
[allowMultiSelect]="false"
|
||||||
[showSelectionIndicator]="true"
|
[showSelectionIndicator]="true"
|
||||||
[showProBadge]="true"
|
[showProBadge]="option.isPro ?? false"
|
||||||
[showRecommendedBadge]="false"
|
[showRecommendedBadge]="option.recommended ?? false"
|
||||||
[enableHover]="!option.disabled"
|
[enableHover]="!option.disabled"
|
||||||
(optionSelected)="onOptionChange($event)"
|
(optionSelected)="onOptionChange($event)"
|
||||||
></ov-selectable-card>
|
></ov-selectable-card>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@ -24,18 +24,15 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
templateUrl: './recording-trigger.component.html',
|
templateUrl: './recording-trigger.component.html',
|
||||||
styleUrl: './recording-trigger.component.scss'
|
styleUrl: './recording-trigger.component.scss'
|
||||||
})
|
})
|
||||||
export class RecordingTriggerComponent implements OnInit, OnDestroy {
|
export class RecordingTriggerComponent implements OnDestroy {
|
||||||
triggerForm: FormGroup;
|
triggerForm: FormGroup;
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
triggerOptions: SelectableOption[] = [
|
triggerOptions: SelectableOption[] = [
|
||||||
{
|
{
|
||||||
id: 'manual',
|
id: 'manual',
|
||||||
title: 'Manual Recording',
|
title: 'Manual Recording',
|
||||||
description: 'Start recording manually when needed',
|
description: 'Start recording manually when needed',
|
||||||
icon: 'touch_app',
|
icon: 'touch_app',
|
||||||
recommended: true,
|
recommended: true
|
||||||
isPro: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'auto1',
|
id: 'auto1',
|
||||||
@ -55,26 +52,15 @@ export class RecordingTriggerComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
private destroy$ = new Subject<void>();
|
||||||
private fb: FormBuilder,
|
|
||||||
private wizardState: RoomWizardStateService
|
|
||||||
) {
|
|
||||||
this.triggerForm = this.fb.group({
|
|
||||||
triggerType: ['manual'] // default to manual
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private wizardService: RoomWizardStateService) {
|
||||||
// Load existing data if available
|
const currentStep = this.wizardService.currentStep();
|
||||||
this.loadExistingData();
|
this.triggerForm = currentStep!.formGroup;
|
||||||
|
|
||||||
// Subscribe to form changes for auto-save
|
|
||||||
this.triggerForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
this.triggerForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
this.saveFormData(value);
|
this.saveFormData(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save initial default value if no existing data
|
|
||||||
this.saveInitialDefaultIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -82,31 +68,9 @@ export class RecordingTriggerComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
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) {
|
private saveFormData(formValue: any) {
|
||||||
// Note: Recording trigger type is not part of MeetRoomOptions
|
// 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
|
// For now, just keep the form state
|
||||||
// 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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
@ -11,43 +9,22 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-room-preferences',
|
selector: 'ov-room-preferences',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule, MatCardModule, MatButtonModule, MatIconModule, MatSlideToggleModule],
|
imports: [ReactiveFormsModule, MatCardModule, MatIconModule, MatSlideToggleModule],
|
||||||
templateUrl: './room-preferences.component.html',
|
templateUrl: './room-preferences.component.html',
|
||||||
styleUrl: './room-preferences.component.scss'
|
styleUrl: './room-preferences.component.scss'
|
||||||
})
|
})
|
||||||
export class RoomPreferencesComponent implements OnInit, OnDestroy {
|
export class RoomPreferencesComponent implements OnDestroy {
|
||||||
preferencesForm: FormGroup;
|
preferencesForm: FormGroup;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(private wizardService: RoomWizardStateService) {
|
||||||
private fb: FormBuilder,
|
const currentStep = this.wizardService.currentStep();
|
||||||
private roomWizardStateService: RoomWizardStateService
|
this.preferencesForm = currentStep!.formGroup;
|
||||||
) {
|
|
||||||
this.preferencesForm = this.fb.group({
|
|
||||||
chatEnabled: [true],
|
|
||||||
virtualBackgroundsEnabled: [true]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.preferencesForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
this.saveFormData(value);
|
this.saveFormData(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save initial default values if no existing data
|
|
||||||
this.saveInitialDefaultIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -67,17 +44,7 @@ export class RoomPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.roomWizardStateService.updateStepData('preferences', stepData);
|
this.wizardService.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChatToggleChange(event: any): void {
|
onChatToggleChange(event: any): void {
|
||||||
@ -91,10 +58,10 @@ export class RoomPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get chatEnabled(): boolean {
|
get chatEnabled(): boolean {
|
||||||
return this.preferencesForm.value.chatEnabled;
|
return this.preferencesForm.value.chatEnabled || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get virtualBackgroundsEnabled(): boolean {
|
get virtualBackgroundsEnabled(): boolean {
|
||||||
return this.preferencesForm.value.virtualBackgroundsEnabled;
|
return this.preferencesForm.value.virtualBackgroundsEnabled || false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { computed, Injectable, signal } from '@angular/core';
|
||||||
import { FormBuilder, Validators } from '@angular/forms';
|
import { AbstractControl, FormBuilder, ValidationErrors, Validators } from '@angular/forms';
|
||||||
import { WizardNavigationConfig, WizardStep } from '@lib/models';
|
import { WizardNavigationConfig, WizardStep } from '@lib/models';
|
||||||
import { MeetRecordingAccess, MeetRoomOptions, MeetRoomPreferences } from '@lib/typings/ce';
|
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.
|
* Service to manage the state of the room creation wizard.
|
||||||
@ -12,35 +21,29 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class RoomWizardStateService {
|
export class RoomWizardStateService {
|
||||||
private readonly _formBuilder = inject(FormBuilder);
|
// Signals for reactive state management
|
||||||
|
private _steps = signal<WizardStep[]>([]);
|
||||||
// Observables for reactive state management
|
private _visibleSteps = computed(() => this._steps().filter((step) => step.isVisible));
|
||||||
private readonly _currentStepIndex = new BehaviorSubject<number>(0);
|
private _currentStepIndex = signal<number>(0);
|
||||||
private readonly _steps = new BehaviorSubject<WizardStep[]>([]);
|
private _roomOptions = signal<MeetRoomOptions>({
|
||||||
private readonly _roomOptions = new BehaviorSubject<MeetRoomOptions>({
|
preferences: DEFAULT_PREFERENCES
|
||||||
preferences: {
|
|
||||||
recordingPreferences: {
|
|
||||||
enabled: false,
|
|
||||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER
|
|
||||||
},
|
|
||||||
chatPreferences: { enabled: true },
|
|
||||||
virtualBackgroundPreferences: { enabled: true }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
public readonly currentStepIndex$ = this._currentStepIndex.asObservable();
|
public readonly steps = computed(() => this._steps());
|
||||||
public readonly steps$ = this._steps.asObservable();
|
public readonly currentStepIndex = computed(() => this._currentStepIndex());
|
||||||
public readonly roomOptions$ = this._roomOptions.asObservable();
|
public readonly currentStep = computed<WizardStep | undefined>(() => {
|
||||||
|
const visibleSteps = this._visibleSteps();
|
||||||
|
const currentIndex = this._currentStepIndex();
|
||||||
|
|
||||||
// Default room preferences following the platform's defaults
|
if (currentIndex < 0 || currentIndex >= visibleSteps.length) {
|
||||||
private readonly DEFAULT_PREFERENCES: MeetRoomPreferences = {
|
return undefined;
|
||||||
recordingPreferences: {
|
}
|
||||||
enabled: false,
|
|
||||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER
|
return visibleSteps[currentIndex];
|
||||||
},
|
});
|
||||||
chatPreferences: { enabled: true },
|
public readonly roomOptions = computed(() => this._roomOptions());
|
||||||
virtualBackgroundPreferences: { enabled: true }
|
|
||||||
};
|
constructor(private formBuilder: FormBuilder) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the wizard with base steps and default room options.
|
* Initializes the wizard with base steps and default room options.
|
||||||
@ -49,22 +52,17 @@ export class RoomWizardStateService {
|
|||||||
*/
|
*/
|
||||||
initializeWizard(editMode: boolean = false, existingData?: MeetRoomOptions): void {
|
initializeWizard(editMode: boolean = false, existingData?: MeetRoomOptions): void {
|
||||||
// Initialize room options with defaults merged with existing data
|
// 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 = {
|
const initialRoomOptions: MeetRoomOptions = {
|
||||||
...existingData,
|
...existingData,
|
||||||
preferences: mergedPreferences
|
preferences: {
|
||||||
|
...DEFAULT_PREFERENCES,
|
||||||
|
...(existingData?.preferences || {})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Initial room options created:', initialRoomOptions);
|
this._roomOptions.set(initialRoomOptions);
|
||||||
this._roomOptions.next(initialRoomOptions);
|
|
||||||
|
|
||||||
// Define base wizard steps
|
// Define wizard steps
|
||||||
const baseSteps: WizardStep[] = [
|
const baseSteps: WizardStep[] = [
|
||||||
{
|
{
|
||||||
id: 'basic',
|
id: 'basic',
|
||||||
@ -72,14 +70,62 @@ export class RoomWizardStateService {
|
|||||||
isCompleted: editMode, // In edit mode, mark as completed but not editable
|
isCompleted: editMode, // In edit mode, mark as completed but not editable
|
||||||
isActive: !editMode, // Start with basic step active in create mode
|
isActive: !editMode, // Start with basic step active in create mode
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
validationFormGroup: this._formBuilder.group({
|
formGroup: this.formBuilder.group(
|
||||||
roomPrefix: [
|
{
|
||||||
{ value: initialRoomOptions.roomIdPrefix || '', disabled: editMode },
|
roomIdPrefix: [
|
||||||
editMode ? [] : [Validators.minLength(2)]
|
{ value: initialRoomOptions.roomIdPrefix || '', disabled: editMode },
|
||||||
],
|
editMode ? [] : [Validators.maxLength(50)]
|
||||||
deletionDate: [{ value: initialRoomOptions.autoDeletionDate || '', disabled: editMode }]
|
],
|
||||||
}),
|
autoDeletionDate: [
|
||||||
order: 1
|
{
|
||||||
|
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',
|
id: 'recording',
|
||||||
@ -87,13 +133,32 @@ export class RoomWizardStateService {
|
|||||||
isCompleted: editMode, // In edit mode, all editable steps are completed
|
isCompleted: editMode, // In edit mode, all editable steps are completed
|
||||||
isActive: editMode, // Only active in edit mode
|
isActive: editMode, // Only active in edit mode
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
validationFormGroup: this._formBuilder.group({
|
formGroup: this.formBuilder.group({
|
||||||
enabled: [
|
recordingEnabled: initialRoomOptions.preferences!.recordingPreferences.enabled
|
||||||
initialRoomOptions.preferences?.recordingPreferences?.enabled ??
|
? 'enabled'
|
||||||
this.DEFAULT_PREFERENCES.recordingPreferences.enabled
|
: 'disabled',
|
||||||
]
|
allowAccessTo: initialRoomOptions.preferences!.recordingPreferences.allowAccessTo
|
||||||
}),
|
})
|
||||||
order: 2
|
},
|
||||||
|
{
|
||||||
|
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',
|
id: 'preferences',
|
||||||
@ -101,35 +166,19 @@ export class RoomWizardStateService {
|
|||||||
isCompleted: editMode, // In edit mode, all editable steps are completed
|
isCompleted: editMode, // In edit mode, all editable steps are completed
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
validationFormGroup: this._formBuilder.group({
|
formGroup: this.formBuilder.group({
|
||||||
chatPreferences: this._formBuilder.group({
|
chatEnabled: initialRoomOptions.preferences!.chatPreferences.enabled,
|
||||||
enabled: [
|
virtualBackgroundsEnabled: initialRoomOptions.preferences!.virtualBackgroundPreferences.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
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
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
|
// Update step visibility after index is set
|
||||||
setTimeout(() => {
|
this.updateStepsVisibility();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,8 +188,7 @@ export class RoomWizardStateService {
|
|||||||
* @param stepData - The data to update in the room options
|
* @param stepData - The data to update in the room options
|
||||||
*/
|
*/
|
||||||
updateStepData(stepId: string, stepData: Partial<MeetRoomOptions>): void {
|
updateStepData(stepId: string, stepData: Partial<MeetRoomOptions>): void {
|
||||||
console.log(`updateStepData called - stepId: '${stepId}', stepData:`, stepData);
|
const currentOptions = this._roomOptions();
|
||||||
const currentOptions = this._roomOptions.value;
|
|
||||||
let updatedOptions: MeetRoomOptions;
|
let updatedOptions: MeetRoomOptions;
|
||||||
|
|
||||||
switch (stepId) {
|
switch (stepId) {
|
||||||
@ -148,6 +196,7 @@ export class RoomWizardStateService {
|
|||||||
updatedOptions = {
|
updatedOptions = {
|
||||||
...currentOptions
|
...currentOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only update fields that are explicitly provided
|
// Only update fields that are explicitly provided
|
||||||
if ('roomIdPrefix' in stepData) {
|
if ('roomIdPrefix' in stepData) {
|
||||||
updatedOptions.roomIdPrefix = stepData.roomIdPrefix;
|
updatedOptions.roomIdPrefix = stepData.roomIdPrefix;
|
||||||
@ -155,8 +204,8 @@ export class RoomWizardStateService {
|
|||||||
if ('autoDeletionDate' in stepData) {
|
if ('autoDeletionDate' in stepData) {
|
||||||
updatedOptions.autoDeletionDate = stepData.autoDeletionDate;
|
updatedOptions.autoDeletionDate = stepData.autoDeletionDate;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
|
break;
|
||||||
case 'recording':
|
case 'recording':
|
||||||
updatedOptions = {
|
updatedOptions = {
|
||||||
...currentOptions,
|
...currentOptions,
|
||||||
@ -169,13 +218,11 @@ export class RoomWizardStateService {
|
|||||||
} as MeetRoomPreferences
|
} as MeetRoomPreferences
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'recordingTrigger':
|
case 'recordingTrigger':
|
||||||
case 'recordingLayout':
|
case 'recordingLayout':
|
||||||
// These steps don't directly modify room options but could store additional metadata
|
// These steps don't update room options
|
||||||
updatedOptions = { ...currentOptions };
|
updatedOptions = { ...currentOptions };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'preferences':
|
case 'preferences':
|
||||||
updatedOptions = {
|
updatedOptions = {
|
||||||
...currentOptions,
|
...currentOptions,
|
||||||
@ -196,77 +243,41 @@ export class RoomWizardStateService {
|
|||||||
} as MeetRoomPreferences
|
} as MeetRoomPreferences
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown step ID: ${stepId}`);
|
console.warn(`Unknown step ID: ${stepId}`);
|
||||||
updatedOptions = currentOptions;
|
updatedOptions = currentOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._roomOptions.next(updatedOptions);
|
this._roomOptions.set(updatedOptions);
|
||||||
console.log(`Updated room options for step '${stepId}':`, updatedOptions);
|
this.updateStepsVisibility();
|
||||||
this.updateStepVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the visibility of wizard steps based on current room options.
|
* Updates the visibility of wizard steps based on current room options.
|
||||||
* For example, recording-related steps are only visible when recording is enabled.
|
* For example, recording-related steps are only visible when recording is enabled.
|
||||||
*/
|
*/
|
||||||
private updateStepVisibility(): void {
|
private updateStepsVisibility(): void {
|
||||||
const currentSteps = this._steps.value;
|
const currentSteps = this._steps();
|
||||||
const currentOptions = this._roomOptions.value;
|
const currentOptions = this._roomOptions();
|
||||||
const recordingEnabled = currentOptions.preferences?.recordingPreferences?.enabled ?? false;
|
const recordingEnabled = currentOptions.preferences?.recordingPreferences.enabled ?? false;
|
||||||
|
|
||||||
// Determine if we're in edit mode by checking if basic step is completed and disabled
|
// Update recording steps visibility based on recordingEnabled
|
||||||
const basicStep = currentSteps.find((step) => step.id === 'basic');
|
const updatedSteps = currentSteps.map((step) => {
|
||||||
const isEditMode = !!basicStep?.validationFormGroup?.disabled;
|
if (step.id === 'recordingTrigger' || step.id === 'recordingLayout') {
|
||||||
|
return {
|
||||||
// Remove recording-specific steps if they exist
|
...step,
|
||||||
const filteredSteps = currentSteps.filter((step) => !['recordingTrigger', 'recordingLayout'].includes(step.id));
|
isVisible: recordingEnabled // Only show if recording is enabled
|
||||||
|
};
|
||||||
if (recordingEnabled) {
|
}
|
||||||
// Add recording-specific steps
|
return step;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
this._steps.set(updatedSteps);
|
||||||
this._steps.next(filteredSteps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToNextStep(): boolean {
|
goToNextStep(): boolean {
|
||||||
const currentIndex = this._currentStepIndex.value;
|
const currentIndex = this._currentStepIndex();
|
||||||
const steps = this._steps.value;
|
const visibleSteps = this._visibleSteps();
|
||||||
const visibleSteps = steps.filter((step) => step.isVisible);
|
|
||||||
if (currentIndex < visibleSteps.length - 1) {
|
if (currentIndex < visibleSteps.length - 1) {
|
||||||
// Mark current step as completed
|
// Mark current step as completed
|
||||||
const currentStep = visibleSteps[currentIndex];
|
const currentStep = visibleSteps[currentIndex];
|
||||||
@ -277,17 +288,18 @@ export class RoomWizardStateService {
|
|||||||
const nextStep = visibleSteps[currentIndex + 1];
|
const nextStep = visibleSteps[currentIndex + 1];
|
||||||
nextStep.isActive = true;
|
nextStep.isActive = true;
|
||||||
|
|
||||||
this._currentStepIndex.next(currentIndex + 1);
|
this._currentStepIndex.set(currentIndex + 1);
|
||||||
this._steps.next([...steps]);
|
const steps = this._steps();
|
||||||
|
this._steps.set([...steps]); // Trigger reactivity
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPreviousStep(): boolean {
|
goToPreviousStep(): boolean {
|
||||||
const currentIndex = this._currentStepIndex.value;
|
const currentIndex = this._currentStepIndex();
|
||||||
const steps = this._steps.value;
|
const visibleSteps = this._visibleSteps();
|
||||||
const visibleSteps = steps.filter((step) => step.isVisible);
|
|
||||||
|
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
// Deactivate current step
|
// Deactivate current step
|
||||||
@ -298,34 +310,26 @@ export class RoomWizardStateService {
|
|||||||
const previousStep = visibleSteps[currentIndex - 1];
|
const previousStep = visibleSteps[currentIndex - 1];
|
||||||
previousStep.isActive = true;
|
previousStep.isActive = true;
|
||||||
|
|
||||||
this._currentStepIndex.next(currentIndex - 1);
|
this._currentStepIndex.set(currentIndex - 1);
|
||||||
this._steps.next([...steps]);
|
const steps = this._steps();
|
||||||
|
this._steps.set([...steps]); // Trigger reactivity
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 {
|
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) {
|
if (targetIndex >= 0 && targetIndex < visibleSteps.length) {
|
||||||
// Deactivate current step
|
// Deactivate current step
|
||||||
const currentStep = this.getCurrentStep();
|
const currentStep = this.currentStep();
|
||||||
if (currentStep) {
|
if (currentStep) {
|
||||||
currentStep.isActive = false;
|
currentStep.isActive = false;
|
||||||
}
|
}
|
||||||
@ -334,54 +338,48 @@ export class RoomWizardStateService {
|
|||||||
const targetStep = visibleSteps[targetIndex];
|
const targetStep = visibleSteps[targetIndex];
|
||||||
targetStep.isActive = true;
|
targetStep.isActive = true;
|
||||||
|
|
||||||
this._currentStepIndex.next(targetIndex);
|
this._currentStepIndex.set(targetIndex);
|
||||||
this._steps.next([...this._steps.value]);
|
const steps = this._steps();
|
||||||
|
this._steps.set([...steps]); // Trigger reactivity
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNavigationConfig(): WizardNavigationConfig {
|
getNavigationConfig(): WizardNavigationConfig {
|
||||||
const currentIndex = this._currentStepIndex.value;
|
const currentIndex = this._currentStepIndex();
|
||||||
const steps = this._steps.value;
|
const visibleSteps = this._visibleSteps();
|
||||||
const visibleSteps = steps.filter((step) => step.isVisible);
|
|
||||||
const isLastStep = currentIndex === visibleSteps.length - 1;
|
|
||||||
const isFirstStep = currentIndex === 0;
|
const isFirstStep = currentIndex === 0;
|
||||||
|
const isLastStep = currentIndex === visibleSteps.length - 1;
|
||||||
|
|
||||||
// Determine if we're in edit mode
|
const isEditMode = this.isEditMode();
|
||||||
const basicStep = steps.find((step) => step.id === 'basic');
|
const isSomeStepInvalid = visibleSteps.some((step) => step.formGroup.invalid);
|
||||||
const isEditMode = !!(basicStep?.isCompleted && basicStep.validationFormGroup?.disabled);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showPrevious: !isFirstStep,
|
showPrevious: !isFirstStep,
|
||||||
showNext: !isLastStep,
|
showNext: !isLastStep,
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
showFinish: isLastStep,
|
showFinish: isLastStep,
|
||||||
showQuickCreate: !isEditMode && isFirstStep,
|
showSkipAndFinish: !isEditMode && isFirstStep,
|
||||||
|
disableFinish: isSomeStepInvalid,
|
||||||
nextLabel: 'Next',
|
nextLabel: 'Next',
|
||||||
previousLabel: 'Previous',
|
previousLabel: 'Previous',
|
||||||
finishLabel: isEditMode ? 'Update Room' : 'Create Room',
|
finishLabel: isEditMode ? 'Update Room' : 'Create Room',
|
||||||
isNextDisabled: false,
|
skipAndFinishLabel: 'Create with defaults'
|
||||||
isPreviousDisabled: isFirstStep
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current room options configured in the wizard.
|
* Checks if the wizard is in edit mode.
|
||||||
* @returns The current MeetRoomOptions object
|
* 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 {
|
private isEditMode(): boolean {
|
||||||
const options = this._roomOptions.getValue();
|
const visibleSteps = this._visibleSteps();
|
||||||
console.log('Getting room options:', options);
|
const basicStep = visibleSteps.find((step) => step.id === 'basic');
|
||||||
return options;
|
const isEditMode = !!basicStep && basicStep.isCompleted && basicStep.formGroup.disabled;
|
||||||
}
|
return isEditMode;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -389,11 +387,10 @@ export class RoomWizardStateService {
|
|||||||
*/
|
*/
|
||||||
resetWizard(): void {
|
resetWizard(): void {
|
||||||
const defaultOptions: MeetRoomOptions = {
|
const defaultOptions: MeetRoomOptions = {
|
||||||
preferences: this.DEFAULT_PREFERENCES
|
preferences: DEFAULT_PREFERENCES
|
||||||
};
|
};
|
||||||
|
this._roomOptions.set(defaultOptions);
|
||||||
this._roomOptions.next(defaultOptions);
|
this._steps.set([]);
|
||||||
this._currentStepIndex.next(0);
|
this._currentStepIndex.set(0);
|
||||||
this.initializeWizard();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { OpenViduComponentsModule, OpenViduComponentsConfig } from 'openvidu-components-angular';
|
|
||||||
import { environment } from '@environment/environment';
|
|
||||||
import { routes } from '@app/app.routes';
|
import { routes } from '@app/app.routes';
|
||||||
|
import { environment } from '@environment/environment';
|
||||||
import { httpInterceptor } from '@lib/interceptors/index';
|
import { httpInterceptor } from '@lib/interceptors/index';
|
||||||
|
import { OpenViduComponentsConfig, OpenViduComponentsModule } from 'openvidu-components-angular';
|
||||||
|
|
||||||
const ovComponentsconfig: OpenViduComponentsConfig = {
|
const ovComponentsconfig: OpenViduComponentsConfig = {
|
||||||
production: environment.production
|
production: environment.production
|
||||||
@ -17,6 +18,10 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
provideHttpClient(withInterceptors([httpInterceptor]))
|
provideHttpClient(withInterceptors([httpInterceptor])),
|
||||||
|
{
|
||||||
|
provide: STEPPER_GLOBAL_OPTIONS,
|
||||||
|
useValue: { showError: true } // Show error messages in stepper
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user