Files
Cleanuparr/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts
2025-08-01 19:45:01 +03:00

788 lines
30 KiB
TypeScript

import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { QueueCleanerConfigStore } from "./queue-cleaner-config.store";
import { CanComponentDeactivate } from "../../core/guards";
import {
QueueCleanerConfig,
ScheduleUnit,
FailedImportConfig,
StalledConfig,
SlowConfig,
ScheduleOptions
} from "../../shared/models/queue-cleaner-config.model";
import { SettingsCardComponent } from "../components/settings-card/settings-card.component";
import { ByteSizeInputComponent } from "../../shared/components/byte-size-input/byte-size-input.component";
import { MobileAutocompleteComponent } from "../../shared/components/mobile-autocomplete/mobile-autocomplete.component";
// PrimeNG Components
import { CardModule } from "primeng/card";
import { InputTextModule } from "primeng/inputtext";
import { CheckboxModule } from "primeng/checkbox";
import { ButtonModule } from "primeng/button";
import { InputNumberModule } from "primeng/inputnumber";
import { AccordionModule } from "primeng/accordion";
import { SelectButtonModule } from "primeng/selectbutton";
import { ChipsModule } from "primeng/chips";
import { ToastModule } from "primeng/toast";
// Using centralized NotificationService instead of MessageService
import { NotificationService } from "../../core/services/notification.service";
import { DocumentationService } from "../../core/services/documentation.service";
import { SelectModule } from "primeng/select";
import { AutoCompleteModule } from "primeng/autocomplete";
import { DropdownModule } from "primeng/dropdown";
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
import { ErrorHandlerUtil } from "../../core/utils/error-handler.util";
@Component({
selector: "app-queue-cleaner-settings",
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
CardModule,
InputTextModule,
CheckboxModule,
ButtonModule,
InputNumberModule,
AccordionModule,
SelectButtonModule,
ChipsModule,
ToastModule,
ByteSizeInputComponent,
SelectModule,
AutoCompleteModule,
DropdownModule,
LoadingErrorStateComponent,
MobileAutocompleteComponent,
],
providers: [QueueCleanerConfigStore],
templateUrl: "./queue-cleaner-settings.component.html",
styleUrls: ["./queue-cleaner-settings.component.scss"],
})
export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDeactivate {
@Output() saved = new EventEmitter<void>();
@Output() error = new EventEmitter<string>();
// Queue Cleaner Configuration Form
queueCleanerForm: FormGroup;
// Original form values for tracking changes
private originalFormValues: any;
// Track whether the form has actual changes compared to original values
hasActualChanges = false;
// Schedule unit options for job schedules
scheduleUnitOptions = [
{ label: "Seconds", value: ScheduleUnit.Seconds },
{ label: "Minutes", value: ScheduleUnit.Minutes },
{ label: "Hours", value: ScheduleUnit.Hours },
];
// Options for each schedule unit
scheduleValueOptions = {
[ScheduleUnit.Seconds]: ScheduleOptions[ScheduleUnit.Seconds].map(v => ({ label: v.toString(), value: v })),
[ScheduleUnit.Minutes]: ScheduleOptions[ScheduleUnit.Minutes].map(v => ({ label: v.toString(), value: v })),
[ScheduleUnit.Hours]: ScheduleOptions[ScheduleUnit.Hours].map(v => ({ label: v.toString(), value: v }))
};
// Display modes for schedule
scheduleModeOptions = [
{ label: 'Basic', value: false },
{ label: 'Advanced', value: true }
];
// Inject the necessary services
private formBuilder = inject(FormBuilder);
// Using the notification service for all toast messages
private notificationService = inject(NotificationService);
private documentationService = inject(DocumentationService);
private queueCleanerStore = inject(QueueCleanerConfigStore);
// Signals from the store
readonly queueCleanerConfig = this.queueCleanerStore.config;
readonly queueCleanerLoading = this.queueCleanerStore.loading;
readonly queueCleanerSaving = this.queueCleanerStore.saving;
readonly queueCleanerLoadError = this.queueCleanerStore.loadError; // Only for "Not connected" state
readonly queueCleanerSaveError = this.queueCleanerStore.saveError; // Only for toast notifications
// Track active accordion tabs
activeAccordionIndices: number[] = [];
// Subject for unsubscribing from observables when component is destroyed
private destroy$ = new Subject<void>();
/**
* Check if component can be deactivated (navigation guard)
*/
canDeactivate(): boolean {
return !this.queueCleanerForm.dirty;
}
/**
* Open field-specific documentation in a new tab
* @param fieldName The form field name (e.g., 'enabled', 'failedImport.maxStrikes')
*/
openFieldDocs(fieldName: string): void {
this.documentationService.openFieldDocumentation('queue-cleaner', fieldName);
}
constructor() {
// Initialize the queue cleaner form with proper disabled states
this.queueCleanerForm = this.formBuilder.group({
enabled: [false],
useAdvancedScheduling: [{ value: false, disabled: true }],
cronExpression: [{ value: '', disabled: true }, [Validators.required]],
jobSchedule: this.formBuilder.group({
every: [{ value: 5, disabled: true }, [Validators.required, Validators.min(1)]],
type: [{ value: ScheduleUnit.Minutes, disabled: true }],
}),
// Failed Import settings - nested group
failedImport: this.formBuilder.group({
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
ignorePrivate: [{ value: false, disabled: true }],
deletePrivate: [{ value: false, disabled: true }],
ignoredPatterns: [{ value: [], disabled: true }],
}),
// Stalled settings - nested group
stalled: this.formBuilder.group({
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
resetStrikesOnProgress: [{ value: false, disabled: true }],
ignorePrivate: [{ value: false, disabled: true }],
deletePrivate: [{ value: false, disabled: true }],
downloadingMetadataMaxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
}),
// Slow Download settings - nested group
slow: this.formBuilder.group({
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
resetStrikesOnProgress: [{ value: false, disabled: true }],
ignorePrivate: [{ value: false, disabled: true }],
deletePrivate: [{ value: false, disabled: true }],
minSpeed: [{ value: "", disabled: true }],
maxTime: [{ value: 0, disabled: true }, [Validators.required, Validators.min(0), Validators.max(1000)]],
ignoreAboveSize: [{ value: "", disabled: true }],
}),
});
// Create an effect to update the form when the configuration changes
// Effect to handle configuration changes
effect(() => {
const config = this.queueCleanerConfig();
if (config) {
// Handle the case where ignorePrivate is true but deletePrivate is also true
// This shouldn't happen, but if it does, correct it
const correctedConfig = { ...config };
// For Queue Cleaner (apply to all sections)
if (correctedConfig.failedImport?.ignorePrivate && correctedConfig.failedImport?.deletePrivate) {
correctedConfig.failedImport.deletePrivate = false;
}
if (correctedConfig.stalled?.ignorePrivate && correctedConfig.stalled?.deletePrivate) {
correctedConfig.stalled.deletePrivate = false;
}
if (correctedConfig.slow?.ignorePrivate && correctedConfig.slow?.deletePrivate) {
correctedConfig.slow.deletePrivate = false;
}
// Save original cron expression
const cronExpression = correctedConfig.cronExpression;
// Reset form with the corrected config values
this.queueCleanerForm.patchValue({
enabled: correctedConfig.enabled,
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
cronExpression: correctedConfig.cronExpression,
jobSchedule: correctedConfig.jobSchedule || {
every: 5,
type: ScheduleUnit.Minutes
},
failedImport: correctedConfig.failedImport,
stalled: correctedConfig.stalled,
slow: correctedConfig.slow,
});
// Then update all other dependent form control states
this.updateFormControlDisabledStates(correctedConfig);
// Store original values for dirty checking
this.storeOriginalValues();
// Mark form as pristine since we've just loaded the data
this.queueCleanerForm.markAsPristine();
}
});
// Effect to handle load errors - emit to LoadingErrorStateComponent for "Not connected" display
effect(() => {
const loadErrorMessage = this.queueCleanerLoadError();
if (loadErrorMessage) {
// Load errors should be shown as "Not connected to server" in LoadingErrorStateComponent
this.error.emit(loadErrorMessage);
}
});
// Effect to handle save errors - show as toast notifications for user to fix
effect(() => {
const saveErrorMessage = this.queueCleanerSaveError();
if (saveErrorMessage) {
// Check if this looks like a validation error from the backend
// These are typically user-fixable errors that should be shown as toasts
const isUserFixableError = ErrorHandlerUtil.isUserFixableError(saveErrorMessage);
if (isUserFixableError) {
// Show validation errors as toast notifications so user can fix them
this.notificationService.showError(saveErrorMessage);
} else {
// For non-user-fixable save errors, also emit to parent
this.error.emit(saveErrorMessage);
}
}
});
// Set up listeners for form value changes
this.setupFormValueChangeListeners();
}
/**
* Clean up subscriptions when component is destroyed
*/
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Set up listeners for form control value changes to manage dependent control states
*/
private setupFormValueChangeListeners(): void {
// Listen for changes to the 'enabled' control
const enabledControl = this.queueCleanerForm.get('enabled');
if (enabledControl) {
enabledControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((enabled: boolean) => {
this.updateMainControlsState(enabled);
});
}
// Add listeners for ignorePrivate changes in each section
['failedImport', 'stalled', 'slow'].forEach(section => {
const ignorePrivateControl = this.queueCleanerForm.get(`${section}.ignorePrivate`);
if (ignorePrivateControl) {
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((ignorePrivate: boolean) => {
const deletePrivateControl = this.queueCleanerForm.get(`${section}.deletePrivate`);
if (ignorePrivate && deletePrivateControl) {
// If ignoring private, uncheck and disable delete private
deletePrivateControl.setValue(false);
deletePrivateControl.disable({ onlySelf: true });
} else if (!ignorePrivate && deletePrivateControl) {
// If not ignoring private, enable delete private (if parent section is enabled)
const sectionEnabled = this.isSectionEnabled(section);
if (sectionEnabled) {
deletePrivateControl.enable({ onlySelf: true });
}
}
});
}
});
// Listen for changes to the 'useAdvancedScheduling' control
const advancedControl = this.queueCleanerForm.get('useAdvancedScheduling');
if (advancedControl) {
advancedControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((useAdvanced: boolean) => {
const enabled = this.queueCleanerForm.get('enabled')?.value || false;
const cronExpressionControl = this.queueCleanerForm.get('cronExpression');
const jobScheduleGroup = this.queueCleanerForm.get('jobSchedule') as FormGroup;
const everyControl = jobScheduleGroup?.get('every');
const typeControl = jobScheduleGroup?.get('type');
// Update scheduling controls based on mode, regardless of enabled state
if (useAdvanced) {
if (cronExpressionControl) cronExpressionControl.enable();
if (everyControl) everyControl.disable();
if (typeControl) typeControl.disable();
} else {
if (cronExpressionControl) cronExpressionControl.disable();
if (everyControl) everyControl.enable();
if (typeControl) typeControl.enable();
}
// Then respect the main enabled state - if disabled, disable all scheduling controls
if (!enabled) {
cronExpressionControl?.disable();
everyControl?.disable();
typeControl?.disable();
}
});
}
// Failed import settings
const failedImportMaxStrikesControl = this.queueCleanerForm.get("failedImport.maxStrikes");
if (failedImportMaxStrikesControl) {
failedImportMaxStrikesControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((strikes) => {
this.updateFailedImportDependentControls(strikes);
});
}
// Stalled settings
const stalledMaxStrikesControl = this.queueCleanerForm.get("stalled.maxStrikes");
if (stalledMaxStrikesControl) {
stalledMaxStrikesControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((strikes) => {
this.updateStalledDependentControls(strikes);
});
}
// Slow downloads settings
const slowMaxStrikesControl = this.queueCleanerForm.get("slow.maxStrikes");
if (slowMaxStrikesControl) {
slowMaxStrikesControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((strikes) => {
this.updateSlowDependentControls(strikes);
});
}
// Listen for changes to the schedule type to ensure dropdown isn't empty
const scheduleTypeControl = this.queueCleanerForm.get('jobSchedule.type');
if (scheduleTypeControl) {
scheduleTypeControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
// Ensure the selected value is valid for the new type
const everyControl = this.queueCleanerForm.get('jobSchedule.every');
const currentValue = everyControl?.value;
const scheduleType = this.queueCleanerForm.get('jobSchedule.type')?.value;
const validValues = ScheduleOptions[scheduleType as keyof typeof ScheduleOptions];
if (validValues && currentValue && !validValues.includes(currentValue)) {
everyControl?.setValue(validValues[0]);
}
});
}
// Listen to all form changes to check for actual differences from original values
this.queueCleanerForm.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.hasActualChanges = this.formValuesChanged();
});
}
/**
* Store original form values for dirty checking
*/
private storeOriginalValues(): void {
// Create a deep copy of the form values to ensure proper comparison
this.originalFormValues = JSON.parse(JSON.stringify(this.queueCleanerForm.getRawValue()));
this.hasActualChanges = false;
}
// Helper method to check if a section is enabled
private isSectionEnabled(section: string): boolean {
const mainEnabled = this.queueCleanerForm.get('enabled')?.value || false;
if (!mainEnabled) return false;
const maxStrikesControl = this.queueCleanerForm.get(`${section}.maxStrikes`);
const maxStrikes = maxStrikesControl?.value || 0;
return maxStrikes >= 3;
}
// Check if the current form values are different from the original values
private formValuesChanged(): boolean {
if (!this.originalFormValues) return false;
const currentValues = this.queueCleanerForm.getRawValue();
return !this.isEqual(currentValues, this.originalFormValues);
}
// Deep compare two objects for equality
private isEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || obj1 === null ||
typeof obj2 !== 'object' || obj2 === null) {
return obj1 === obj2;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!keys2.includes(key)) return false;
if (!this.isEqual(obj1[key], obj2[key])) return false;
}
return true;
}
/**
* Update form control disabled states based on the configuration
*/
private updateFormControlDisabledStates(config: QueueCleanerConfig): void {
// Update main form controls based on the 'enabled' state
this.updateMainControlsState(config.enabled);
// Check if failed import strikes are set and update dependent controls
if (config.failedImport?.maxStrikes !== undefined) {
this.updateFailedImportDependentControls(config.failedImport.maxStrikes);
}
// Check if stalled strikes are set and update dependent controls
if (config.stalled?.maxStrikes !== undefined) {
this.updateStalledDependentControls(config.stalled.maxStrikes);
}
// Check if slow download strikes are set and update dependent controls
if (config.slow?.maxStrikes !== undefined) {
this.updateSlowDependentControls(config.slow.maxStrikes);
}
}
/**
* Update the state of main controls based on the 'enabled' control value
*/
private updateMainControlsState(enabled: boolean): void {
const useAdvancedScheduling = this.queueCleanerForm.get('useAdvancedScheduling')?.value || false;
const cronExpressionControl = this.queueCleanerForm.get('cronExpression');
const jobScheduleGroup = this.queueCleanerForm.get('jobSchedule') as FormGroup;
const everyControl = jobScheduleGroup.get('every');
const typeControl = jobScheduleGroup.get('type');
if (enabled) {
// Enable scheduling controls based on mode
if (useAdvancedScheduling) {
cronExpressionControl?.enable();
everyControl?.disable();
typeControl?.disable();
} else {
cronExpressionControl?.disable();
everyControl?.enable();
typeControl?.enable();
}
// Enable the useAdvancedScheduling control
const useAdvancedSchedulingControl = this.queueCleanerForm.get('useAdvancedScheduling');
useAdvancedSchedulingControl?.enable();
// Update individual config sections only if they are enabled
const failedImportMaxStrikes = this.queueCleanerForm.get("failedImport.maxStrikes")?.value;
const stalledMaxStrikes = this.queueCleanerForm.get("stalled.maxStrikes")?.value;
const slowMaxStrikes = this.queueCleanerForm.get("slow.maxStrikes")?.value;
this.updateFailedImportDependentControls(failedImportMaxStrikes);
this.updateStalledDependentControls(stalledMaxStrikes);
this.updateSlowDependentControls(slowMaxStrikes);
} else {
// Disable all scheduling controls
cronExpressionControl?.disable();
everyControl?.disable();
typeControl?.disable();
// Disable the useAdvancedScheduling control
const useAdvancedSchedulingControl = this.queueCleanerForm.get('useAdvancedScheduling');
useAdvancedSchedulingControl?.disable();
// Save current active accordion state before clearing it
// This will be empty when we collapse all accordions
this.activeAccordionIndices = [];
}
}
/**
* Update the state of Failed Import dependent controls based on the 'maxStrikes' value
*/
private updateFailedImportDependentControls(strikes: number): void {
const enable = strikes >= 3;
const options = { onlySelf: true };
if (enable) {
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.enable(options);
this.queueCleanerForm.get("failedImport")?.get("ignoredPatterns")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("failedImport.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("failedImport.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.disable(options);
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.disable(options);
this.queueCleanerForm.get("failedImport")?.get("ignoredPatterns")?.disable(options);
}
}
/**
* Update the state of Stalled dependent controls based on the 'maxStrikes' value
*/
private updateStalledDependentControls(strikes: number): void {
const enable = strikes >= 3;
const options = { onlySelf: true };
if (enable) {
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.enable(options);
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("stalled.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("stalled.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.disable(options);
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.disable(options);
this.queueCleanerForm.get("stalled")?.get("deletePrivate")?.disable(options);
}
}
/**
* Update the state of Slow Download dependent controls based on the 'maxStrikes' value
*/
private updateSlowDependentControls(strikes: number): void {
const enable = strikes >= 3;
const options = { onlySelf: true };
if (enable) {
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.enable(options);
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.enable(options);
this.queueCleanerForm.get("slow")?.get("minSpeed")?.enable(options);
this.queueCleanerForm.get("slow")?.get("maxTime")?.enable(options);
this.queueCleanerForm.get("slow")?.get("ignoreAboveSize")?.enable(options);
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.queueCleanerForm.get("slow.ignorePrivate")?.value || false;
const deletePrivateControl = this.queueCleanerForm.get("slow.deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable(options);
} else if (deletePrivateControl) {
deletePrivateControl.disable(options);
}
} else {
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.disable(options);
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.disable(options);
this.queueCleanerForm.get("slow")?.get("deletePrivate")?.disable(options);
this.queueCleanerForm.get("slow")?.get("minSpeed")?.disable(options);
this.queueCleanerForm.get("slow")?.get("maxTime")?.disable(options);
this.queueCleanerForm.get("slow")?.get("ignoreAboveSize")?.disable(options);
}
}
/**
* Save the queue cleaner configuration
*/
saveQueueCleanerConfig(): void {
// Mark all form controls as touched to trigger validation messages
this.markFormGroupTouched(this.queueCleanerForm);
if (this.queueCleanerForm.valid) {
// Make a copy of the form values
const formValue = this.queueCleanerForm.getRawValue();
// Determine the correct cron expression to use
const cronExpression: string = formValue.useAdvancedScheduling ?
formValue.cronExpression :
// If in basic mode, generate cron expression from the schedule
this.queueCleanerStore.generateCronExpression(formValue.jobSchedule);
// Create the config object to be saved
const queueCleanerConfig: QueueCleanerConfig = {
enabled: formValue.enabled,
useAdvancedScheduling: formValue.useAdvancedScheduling,
cronExpression: cronExpression,
jobSchedule: formValue.jobSchedule,
failedImport: {
maxStrikes: formValue.failedImport?.maxStrikes || 0,
ignorePrivate: formValue.failedImport?.ignorePrivate || false,
deletePrivate: formValue.failedImport?.deletePrivate || false,
ignoredPatterns: formValue.failedImport?.ignoredPatterns || [],
},
stalled: {
maxStrikes: formValue.stalled?.maxStrikes || 0,
resetStrikesOnProgress: formValue.stalled?.resetStrikesOnProgress || false,
ignorePrivate: formValue.stalled?.ignorePrivate || false,
deletePrivate: formValue.stalled?.deletePrivate || false,
downloadingMetadataMaxStrikes: formValue.stalled?.downloadingMetadataMaxStrikes || 0,
},
slow: {
maxStrikes: formValue.slow?.maxStrikes || 0,
resetStrikesOnProgress: formValue.slow?.resetStrikesOnProgress || false,
ignorePrivate: formValue.slow?.ignorePrivate || false,
deletePrivate: formValue.slow?.deletePrivate || false,
minSpeed: formValue.slow?.minSpeed || "",
maxTime: formValue.slow?.maxTime || 0,
ignoreAboveSize: formValue.slow?.ignoreAboveSize || "",
},
};
// Save the configuration
this.queueCleanerStore.saveConfig(queueCleanerConfig);
// Setup a one-time check to mark form as pristine after successful save
// This pattern works with signals since we're not trying to pipe the signal itself
const checkSaveCompletion = () => {
const saving = this.queueCleanerSaving();
const saveError = this.queueCleanerSaveError();
if (!saving && !saveError) {
// Mark form as pristine after successful save
this.queueCleanerForm.markAsPristine();
// Update original values reference
this.storeOriginalValues();
// Emit saved event
this.saved.emit();
// Display success message
this.notificationService.showSuccess('Queue cleaner configuration saved successfully.');
} else if (!saving && saveError) {
// If there's a save error, we can stop checking
// Toast notification is already handled by the effect above
} else {
// If still saving, check again in a moment
setTimeout(checkSaveCompletion, 100);
}
};
// Start checking for save completion
checkSaveCompletion();
} else {
// Form is invalid, show error message
this.notificationService.showValidationError();
// Emit error for parent components
this.error.emit("Please fix validation errors before saving.");
}
}
/**
* Reset the queue cleaner configuration form to default values
*/
resetQueueCleanerConfig(): void {
this.queueCleanerForm.reset({
enabled: false,
useAdvancedScheduling: false,
cronExpression: "0 0/5 * * * ?",
jobSchedule: {
every: 5,
type: ScheduleUnit.Minutes,
},
// Failed Import settings (nested)
failedImport: {
maxStrikes: 0,
ignorePrivate: false,
deletePrivate: false,
ignoredPatterns: [],
},
// Stalled settings (nested)
stalled: {
maxStrikes: 0,
resetStrikesOnProgress: false,
ignorePrivate: false,
deletePrivate: false,
downloadingMetadataMaxStrikes: 0,
},
// Slow Download settings (nested)
slow: {
maxStrikes: 0,
resetStrikesOnProgress: false,
ignorePrivate: false,
deletePrivate: false,
minSpeed: "",
maxTime: 0,
ignoreAboveSize: "",
},
});
// Manually update control states after reset
this.updateMainControlsState(false);
this.updateFailedImportDependentControls(0);
this.updateStalledDependentControls(0);
this.updateSlowDependentControls(0);
// Mark form as dirty so the save button is enabled after reset
this.queueCleanerForm.markAsDirty();
}
/**
* Mark all controls in a form group as touched
*/
private markFormGroupTouched(formGroup: FormGroup): void {
Object.values(formGroup.controls).forEach((control) => {
control.markAsTouched();
if ((control as any).controls) {
this.markFormGroupTouched(control as FormGroup);
}
});
}
/**
* Check if a form control has an error after it's been touched
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.queueCleanerForm.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
* Get schedule value options based on the current schedule unit type
*/
getScheduleValueOptions(): {label: string, value: number}[] {
const scheduleType = this.queueCleanerForm.get('jobSchedule.type')?.value as ScheduleUnit;
if (scheduleType === ScheduleUnit.Seconds) {
return this.scheduleValueOptions[ScheduleUnit.Seconds];
} else if (scheduleType === ScheduleUnit.Minutes) {
return this.scheduleValueOptions[ScheduleUnit.Minutes];
} else if (scheduleType === ScheduleUnit.Hours) {
return this.scheduleValueOptions[ScheduleUnit.Hours];
}
return this.scheduleValueOptions[ScheduleUnit.Minutes]; // Default to minutes
}
/**
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.queueCleanerForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}
const control = parentControl.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
}