mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-13 01:07:22 -04:00
788 lines
30 KiB
TypeScript
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;
|
|
}
|
|
|
|
|
|
}
|