From d078ea288c3fd68fb4bf9e2d7dc151dda501c227 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 30 May 2025 23:18:36 +0300 Subject: [PATCH] try fix settings enablement --- code/UI/.prettierrc | 2 +- .../queue-cleaner-settings.component.html | 41 +- .../queue-cleaner-settings.component.ts | 377 ++++++++++---- .../queue-cleaner-settings.component.ts.new | 472 ++++++++++++++++++ 4 files changed, 776 insertions(+), 116 deletions(-) create mode 100644 code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts.new diff --git a/code/UI/.prettierrc b/code/UI/.prettierrc index cecc252b..faaae249 100644 --- a/code/UI/.prettierrc +++ b/code/UI/.prettierrc @@ -1,5 +1,5 @@ { - "tabWidth": 4, + "tabWidth": 2, "useTabs": false, "singleQuote": false, "bracketSpacing": true, diff --git a/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html index fde348f8..c815aebe 100644 --- a/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html +++ b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.html @@ -19,16 +19,14 @@ [min]="1" [max]="59" buttonLayout="horizontal" - inputStyleClass="schedule-value" - [disabled]="!queueCleanerForm.get('enabled')?.value"> + inputStyleClass="schedule-value"> + optionValue="value"> How often the queue cleaner should run @@ -37,7 +35,7 @@
- + When enabled, jobs will run one after another instead of in parallel
@@ -45,7 +43,7 @@
- + Path to the file containing ignored downloads
@@ -53,7 +51,7 @@ - +
@@ -71,7 +69,7 @@
- + When enabled, private torrents will be ignored
@@ -79,7 +77,7 @@
- + When enabled, private torrents will be deleted
@@ -87,14 +85,14 @@
- + Patterns to ignore (e.g., *sample*)
- +
@@ -112,7 +110,7 @@
- + When enabled, strikes will be reset if download progress is made
@@ -120,7 +118,7 @@
- + When enabled, private torrents will be ignored
@@ -128,14 +126,14 @@
- + When enabled, private torrents will be deleted
- +
@@ -152,7 +150,7 @@ - +
@@ -170,7 +168,7 @@
- + When enabled, strikes will be reset if download progress is made
@@ -178,7 +176,7 @@
- + When enabled, private torrents will be ignored
@@ -186,7 +184,7 @@
- + When enabled, private torrents will be deleted
@@ -196,7 +194,6 @@
@@ -212,8 +209,7 @@ [showButtons]="true" [min]="0" [max]="168" - buttonLayout="horizontal" - [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"> + buttonLayout="horizontal"> Maximum time allowed for slow downloads (in hours)
@@ -224,7 +220,6 @@
diff --git a/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts index fe77fd58..ca6c842a 100644 --- a/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts +++ b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts @@ -1,25 +1,26 @@ -import { Component, EventEmitter, OnInit, Output, effect, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { QueueCleanerConfigStore } from './queue-cleaner-config.store'; -import { QueueCleanerConfig, ScheduleUnit } 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 { 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 { QueueCleanerConfig, ScheduleUnit } 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"; // 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'; -import { MessageService } from 'primeng/api'; +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"; +import { MessageService } from "primeng/api"; @Component({ - selector: 'app-queue-cleaner-settings', + selector: "app-queue-cleaner-settings", standalone: true, imports: [ CommonModule, @@ -34,75 +35,79 @@ import { MessageService } from 'primeng/api'; SelectButtonModule, ChipsModule, ToastModule, - ByteSizeInputComponent + ByteSizeInputComponent, ], providers: [QueueCleanerConfigStore, MessageService], - templateUrl: './queue-cleaner-settings.component.html', - styleUrls: ['./queue-cleaner-settings.component.scss'] + templateUrl: "./queue-cleaner-settings.component.html", + styleUrls: ["./queue-cleaner-settings.component.scss"], }) -export class QueueCleanerSettingsComponent implements OnInit { +export class QueueCleanerSettingsComponent implements OnDestroy { @Output() saved = new EventEmitter(); @Output() error = new EventEmitter(); // Queue Cleaner Configuration Form queueCleanerForm: FormGroup; - + // Schedule unit options for job schedules scheduleUnitOptions = [ - { label: 'Seconds', value: ScheduleUnit.Seconds }, - { label: 'Minutes', value: ScheduleUnit.Minutes }, - { label: 'Hours', value: ScheduleUnit.Hours } + { label: "Seconds", value: ScheduleUnit.Seconds }, + { label: "Minutes", value: ScheduleUnit.Minutes }, + { label: "Hours", value: ScheduleUnit.Hours }, ]; - + // Inject the necessary services private formBuilder = inject(FormBuilder); private messageService = inject(MessageService); private queueCleanerStore = inject(QueueCleanerConfigStore); - + // Signals from the store readonly queueCleanerConfig = this.queueCleanerStore.config; readonly queueCleanerLoading = this.queueCleanerStore.loading; readonly queueCleanerSaving = this.queueCleanerStore.saving; readonly queueCleanerError = this.queueCleanerStore.error; - + + // Subject for unsubscribing from observables when component is destroyed + private destroy$ = new Subject(); + constructor() { - // Initialize the queue cleaner form + // Initialize the queue cleaner form with proper disabled states this.queueCleanerForm = this.formBuilder.group({ enabled: [false], jobSchedule: this.formBuilder.group({ - every: [5, [Validators.required, Validators.min(1)]], - type: [ScheduleUnit.Minutes] + every: [{value: 5, disabled: true}, [Validators.required, Validators.min(1)]], + type: [{value: ScheduleUnit.Minutes, disabled: true}], }), - runSequentially: [false], - ignoredDownloadsPath: [''], - + runSequentially: [{value: false, disabled: true}], + ignoredDownloadsPath: [{value: "", disabled: true}], + // Failed Import settings failedImportMaxStrikes: [0, [Validators.min(0)]], - failedImportIgnorePrivate: [false], - failedImportDeletePrivate: [false], - failedImportIgnorePatterns: [[]], - + failedImportIgnorePrivate: [{value: false, disabled: true}], + failedImportDeletePrivate: [{value: false, disabled: true}], + failedImportIgnorePatterns: [{value: [], disabled: true}], + // Stalled settings stalledMaxStrikes: [0, [Validators.min(0)]], - stalledResetStrikesOnProgress: [false], - stalledIgnorePrivate: [false], - stalledDeletePrivate: [false], - + stalledResetStrikesOnProgress: [{value: false, disabled: true}], + stalledIgnorePrivate: [{value: false, disabled: true}], + stalledDeletePrivate: [{value: false, disabled: true}], + // Downloading Metadata settings downloadingMetadataMaxStrikes: [0, [Validators.min(0)]], - + // Slow Download settings slowMaxStrikes: [0, [Validators.min(0)]], - slowResetStrikesOnProgress: [false], - slowIgnorePrivate: [false], - slowDeletePrivate: [false], - slowMinSpeed: [''], - slowMaxTime: [0, [Validators.min(0)]], - slowIgnoreAboveSize: [''] + slowResetStrikesOnProgress: [{value: false, disabled: true}], + slowIgnorePrivate: [{value: false, disabled: true}], + slowDeletePrivate: [{value: false, disabled: true}], + slowMinSpeed: [{value: "", disabled: true}], + slowMaxTime: [{value: 0, disabled: true}], + slowIgnoreAboveSize: [{value: "", disabled: true}], }); - } - - ngOnInit() { + + // Set up form control value change subscriptions to manage dependent control states + this.setupFormValueChangeListeners(); + // Create an effect to update the form when the configuration changes effect(() => { const config = this.queueCleanerConfig(); @@ -112,22 +117,22 @@ export class QueueCleanerSettingsComponent implements OnInit { enabled: config.enabled, runSequentially: config.runSequentially, ignoredDownloadsPath: config.ignoredDownloadsPath, - + // Failed Import settings failedImportMaxStrikes: config.failedImportMaxStrikes, failedImportIgnorePrivate: config.failedImportIgnorePrivate, failedImportDeletePrivate: config.failedImportDeletePrivate, failedImportIgnorePatterns: config.failedImportIgnorePatterns, - + // Stalled settings stalledMaxStrikes: config.stalledMaxStrikes, stalledResetStrikesOnProgress: config.stalledResetStrikesOnProgress, stalledIgnorePrivate: config.stalledIgnorePrivate, stalledDeletePrivate: config.stalledDeletePrivate, - + // Downloading Metadata settings downloadingMetadataMaxStrikes: config.downloadingMetadataMaxStrikes, - + // Slow Download settings slowMaxStrikes: config.slowMaxStrikes, slowResetStrikesOnProgress: config.slowResetStrikesOnProgress, @@ -135,16 +140,19 @@ export class QueueCleanerSettingsComponent implements OnInit { slowDeletePrivate: config.slowDeletePrivate, slowMinSpeed: config.slowMinSpeed, slowMaxTime: config.slowMaxTime, - slowIgnoreAboveSize: config.slowIgnoreAboveSize + slowIgnoreAboveSize: config.slowIgnoreAboveSize, }); - + // Update job schedule if it exists if (config.jobSchedule) { - this.queueCleanerForm.get('jobSchedule')?.patchValue({ + this.queueCleanerForm.get("jobSchedule")?.patchValue({ every: config.jobSchedule.every, - type: config.jobSchedule.type + type: config.jobSchedule.type, }); } + + // Update form control disabled states based on the configuration + this.updateFormControlDisabledStates(config); } }); @@ -168,6 +176,185 @@ export class QueueCleanerSettingsComponent implements OnInit { }); } + /** + * 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 on the 'enabled' control + this.queueCleanerForm.get('enabled')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + this.updateMainControlsState(enabled); + }); + + // Listen for changes on 'failedImportMaxStrikes' control + this.queueCleanerForm.get('failedImportMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateFailedImportDependentControls(strikes); + }); + + // Listen for changes on 'stalledMaxStrikes' control + this.queueCleanerForm.get('stalledMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateStalledDependentControls(strikes); + }); + + // Listen for changes on 'slowMaxStrikes' control + this.queueCleanerForm.get('slowMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateSlowDependentControls(strikes); + }); + } + + /** + * Update form control disabled states based on the configuration + */ + private updateFormControlDisabledStates(config: QueueCleanerConfig): void { + const enabled = config.enabled; + const options = { onlySelf: true }; + + // Job schedule + if (enabled) { + this.queueCleanerForm.get("jobSchedule")?.enable(options); + this.queueCleanerForm.get("runSequentially")?.enable(options); + this.queueCleanerForm.get("ignoredDownloadsPath")?.enable(options); + } else { + this.queueCleanerForm.get("jobSchedule")?.disable(options); + this.queueCleanerForm.get("runSequentially")?.disable(options); + this.queueCleanerForm.get("ignoredDownloadsPath")?.disable(options); + } + + // Failed Import settings + const failedImportEnabled = enabled && config.failedImportMaxStrikes >= 3; + if (failedImportEnabled) { + this.queueCleanerForm.get("failedImportIgnorePrivate")?.enable(options); + this.queueCleanerForm.get("failedImportDeletePrivate")?.enable(options); + this.queueCleanerForm.get("failedImportIgnorePatterns")?.enable(options); + } else { + this.queueCleanerForm.get("failedImportIgnorePrivate")?.disable(options); + this.queueCleanerForm.get("failedImportDeletePrivate")?.disable(options); + this.queueCleanerForm.get("failedImportIgnorePatterns")?.disable(options); + } + + // Stalled settings + const stalledEnabled = enabled && config.stalledMaxStrikes >= 3; + if (stalledEnabled) { + this.queueCleanerForm.get("stalledResetStrikesOnProgress")?.enable(options); + this.queueCleanerForm.get("stalledIgnorePrivate")?.enable(options); + this.queueCleanerForm.get("stalledDeletePrivate")?.enable(options); + } else { + this.queueCleanerForm.get("stalledResetStrikesOnProgress")?.disable(options); + this.queueCleanerForm.get("stalledIgnorePrivate")?.disable(options); + this.queueCleanerForm.get("stalledDeletePrivate")?.disable(options); + } + + // Slow Download settings + const slowEnabled = enabled && config.slowMaxStrikes >= 3; + if (slowEnabled) { + this.queueCleanerForm.get("slowResetStrikesOnProgress")?.enable(options); + this.queueCleanerForm.get("slowIgnorePrivate")?.enable(options); + this.queueCleanerForm.get("slowDeletePrivate")?.enable(options); + this.queueCleanerForm.get("slowMinSpeed")?.enable(options); + this.queueCleanerForm.get("slowMaxTime")?.enable(options); + this.queueCleanerForm.get("slowIgnoreAboveSize")?.enable(options); + } else { + this.queueCleanerForm.get("slowResetStrikesOnProgress")?.disable(options); + this.queueCleanerForm.get("slowIgnorePrivate")?.disable(options); + this.queueCleanerForm.get("slowDeletePrivate")?.disable(options); + this.queueCleanerForm.get("slowMinSpeed")?.disable(options); + this.queueCleanerForm.get("slowMaxTime")?.disable(options); + this.queueCleanerForm.get("slowIgnoreAboveSize")?.disable(options); + } + } + + /** + * Update the state of main controls based on the 'enabled' control value + */ + private updateMainControlsState(enabled: boolean): void { + const options = { onlySelf: true }; + + if (enabled) { + this.queueCleanerForm.get('jobSchedule')?.enable(options); + this.queueCleanerForm.get('runSequentially')?.enable(options); + this.queueCleanerForm.get('ignoredDownloadsPath')?.enable(options); + } else { + this.queueCleanerForm.get('jobSchedule')?.disable(options); + this.queueCleanerForm.get('runSequentially')?.disable(options); + this.queueCleanerForm.get('ignoredDownloadsPath')?.disable(options); + } + } + + /** + * Update the state of Failed Import dependent controls based on the 'failedImportMaxStrikes' value + */ + private updateFailedImportDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('failedImportIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('failedImportDeletePrivate')?.enable(options); + this.queueCleanerForm.get('failedImportIgnorePatterns')?.enable(options); + } else { + this.queueCleanerForm.get('failedImportIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('failedImportDeletePrivate')?.disable(options); + this.queueCleanerForm.get('failedImportIgnorePatterns')?.disable(options); + } + } + + /** + * Update the state of Stalled dependent controls based on the 'stalledMaxStrikes' value + */ + private updateStalledDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('stalledResetStrikesOnProgress')?.enable(options); + this.queueCleanerForm.get('stalledIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('stalledDeletePrivate')?.enable(options); + } else { + this.queueCleanerForm.get('stalledResetStrikesOnProgress')?.disable(options); + this.queueCleanerForm.get('stalledIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('stalledDeletePrivate')?.disable(options); + } + } + + /** + * Update the state of Slow Download dependent controls based on the 'slowMaxStrikes' value + */ + private updateSlowDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('slowResetStrikesOnProgress')?.enable(options); + this.queueCleanerForm.get('slowIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('slowDeletePrivate')?.enable(options); + this.queueCleanerForm.get('slowMinSpeed')?.enable(options); + this.queueCleanerForm.get('slowMaxTime')?.enable(options); + this.queueCleanerForm.get('slowIgnoreAboveSize')?.enable(options); + } else { + this.queueCleanerForm.get('slowResetStrikesOnProgress')?.disable(options); + this.queueCleanerForm.get('slowIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('slowDeletePrivate')?.disable(options); + this.queueCleanerForm.get('slowMinSpeed')?.disable(options); + this.queueCleanerForm.get('slowMaxTime')?.disable(options); + this.queueCleanerForm.get('slowIgnoreAboveSize')?.disable(options); + } + } + /** * Save the queue cleaner configuration */ @@ -176,55 +363,55 @@ export class QueueCleanerSettingsComponent implements OnInit { // Mark all fields as touched to show validation errors this.markFormGroupTouched(this.queueCleanerForm); this.messageService.add({ - severity: 'error', - summary: 'Validation Error', - detail: 'Please correct the errors in the form before saving.', - life: 5000 + severity: "error", + summary: "Validation Error", + detail: "Please correct the errors in the form before saving.", + life: 5000, }); return; } - + // Get the form values - const formValues = this.queueCleanerForm.value; - + const formValues = this.queueCleanerForm.getRawValue(); // Get values including disabled fields + // Build the configuration object const config: QueueCleanerConfig = { enabled: formValues.enabled, // The cronExpression will be generated from the jobSchedule when saving - cronExpression: '', + cronExpression: "", jobSchedule: formValues.jobSchedule, runSequentially: formValues.runSequentially, - ignoredDownloadsPath: formValues.ignoredDownloadsPath || '', - + ignoredDownloadsPath: formValues.ignoredDownloadsPath || "", + // Failed Import settings failedImportMaxStrikes: formValues.failedImportMaxStrikes, failedImportIgnorePrivate: formValues.failedImportIgnorePrivate, failedImportDeletePrivate: formValues.failedImportDeletePrivate, failedImportIgnorePatterns: formValues.failedImportIgnorePatterns || [], - + // Stalled settings stalledMaxStrikes: formValues.stalledMaxStrikes, stalledResetStrikesOnProgress: formValues.stalledResetStrikesOnProgress, stalledIgnorePrivate: formValues.stalledIgnorePrivate, stalledDeletePrivate: formValues.stalledDeletePrivate, - + // Downloading Metadata settings downloadingMetadataMaxStrikes: formValues.downloadingMetadataMaxStrikes, - + // Slow Download settings slowMaxStrikes: formValues.slowMaxStrikes, slowResetStrikesOnProgress: formValues.slowResetStrikesOnProgress, slowIgnorePrivate: formValues.slowIgnorePrivate, slowDeletePrivate: formValues.slowDeletePrivate, - slowMinSpeed: formValues.slowMinSpeed || '', + slowMinSpeed: formValues.slowMinSpeed || "", slowMaxTime: formValues.slowMaxTime, - slowIgnoreAboveSize: formValues.slowIgnoreAboveSize || '' + slowIgnoreAboveSize: formValues.slowIgnoreAboveSize || "", }; - + // Save the configuration this.queueCleanerStore.saveConfig(config); } - + /** * Reset the queue cleaner configuration form to default values */ @@ -233,50 +420,56 @@ export class QueueCleanerSettingsComponent implements OnInit { enabled: false, jobSchedule: { every: 5, - type: ScheduleUnit.Minutes + type: ScheduleUnit.Minutes, }, runSequentially: false, - ignoredDownloadsPath: '', - + ignoredDownloadsPath: "", + // Failed Import settings failedImportMaxStrikes: 0, failedImportIgnorePrivate: false, failedImportDeletePrivate: false, failedImportIgnorePatterns: [], - + // Stalled settings stalledMaxStrikes: 0, stalledResetStrikesOnProgress: false, stalledIgnorePrivate: false, stalledDeletePrivate: false, - + // Downloading Metadata settings downloadingMetadataMaxStrikes: 0, - + // Slow Download settings slowMaxStrikes: 0, slowResetStrikesOnProgress: false, slowIgnorePrivate: false, slowDeletePrivate: false, - slowMinSpeed: '', + slowMinSpeed: "", slowMaxTime: 0, - slowIgnoreAboveSize: '' + slowIgnoreAboveSize: "", }); + + // Manually update control states after reset + this.updateMainControlsState(false); + this.updateFailedImportDependentControls(0); + this.updateStalledDependentControls(0); + this.updateSlowDependentControls(0); } - + /** * Mark all controls in a form group as touched */ private markFormGroupTouched(formGroup: FormGroup): void { - Object.values(formGroup.controls).forEach(control => { + 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 */ @@ -284,7 +477,7 @@ export class QueueCleanerSettingsComponent implements OnInit { const control = this.queueCleanerForm.get(controlName); return control ? control.touched && control.hasError(errorName) : false; } - + /** * Get nested form control errors */ @@ -293,7 +486,7 @@ export class QueueCleanerSettingsComponent implements OnInit { if (!parentControl || !(parentControl instanceof FormGroup)) { return false; } - + const control = parentControl.get(controlName); return control ? control.touched && control.hasError(errorName) : false; } diff --git a/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts.new b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts.new new file mode 100644 index 00000000..b39eb1a3 --- /dev/null +++ b/code/UI/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts.new @@ -0,0 +1,472 @@ +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 { QueueCleanerConfig, ScheduleUnit } 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"; + +// 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"; +import { MessageService } from "primeng/api"; + +@Component({ + selector: "app-queue-cleaner-settings", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + SettingsCardComponent, + CardModule, + InputTextModule, + CheckboxModule, + ButtonModule, + InputNumberModule, + AccordionModule, + SelectButtonModule, + ChipsModule, + ToastModule, + ByteSizeInputComponent, + ], + providers: [QueueCleanerConfigStore, MessageService], + templateUrl: "./queue-cleaner-settings.component.html", + styleUrls: ["./queue-cleaner-settings.component.scss"], +}) +export class QueueCleanerSettingsComponent implements OnDestroy { + @Output() saved = new EventEmitter(); + @Output() error = new EventEmitter(); + + // Queue Cleaner Configuration Form + queueCleanerForm: FormGroup; + + // Schedule unit options for job schedules + scheduleUnitOptions = [ + { label: "Seconds", value: ScheduleUnit.Seconds }, + { label: "Minutes", value: ScheduleUnit.Minutes }, + { label: "Hours", value: ScheduleUnit.Hours }, + ]; + + // Inject the necessary services + private formBuilder = inject(FormBuilder); + private messageService = inject(MessageService); + private queueCleanerStore = inject(QueueCleanerConfigStore); + + // Signals from the store + readonly queueCleanerConfig = this.queueCleanerStore.config; + readonly queueCleanerLoading = this.queueCleanerStore.loading; + readonly queueCleanerSaving = this.queueCleanerStore.saving; + readonly queueCleanerError = this.queueCleanerStore.error; + + // Subject for unsubscribing from observables when component is destroyed + private destroy$ = new Subject(); + + constructor() { + // Initialize the queue cleaner form with proper disabled states + this.queueCleanerForm = this.formBuilder.group({ + enabled: [false], + jobSchedule: this.formBuilder.group({ + every: [{value: 5, disabled: true}, [Validators.required, Validators.min(1)]], + type: [{value: ScheduleUnit.Minutes, disabled: true}], + }), + runSequentially: [{value: false, disabled: true}], + ignoredDownloadsPath: [{value: "", disabled: true}], + + // Failed Import settings + failedImportMaxStrikes: [0, [Validators.min(0)]], + failedImportIgnorePrivate: [{value: false, disabled: true}], + failedImportDeletePrivate: [{value: false, disabled: true}], + failedImportIgnorePatterns: [{value: [], disabled: true}], + + // Stalled settings + stalledMaxStrikes: [0, [Validators.min(0)]], + stalledResetStrikesOnProgress: [{value: false, disabled: true}], + stalledIgnorePrivate: [{value: false, disabled: true}], + stalledDeletePrivate: [{value: false, disabled: true}], + + // Downloading Metadata settings + downloadingMetadataMaxStrikes: [0, [Validators.min(0)]], + + // Slow Download settings + slowMaxStrikes: [0, [Validators.min(0)]], + slowResetStrikesOnProgress: [{value: false, disabled: true}], + slowIgnorePrivate: [{value: false, disabled: true}], + slowDeletePrivate: [{value: false, disabled: true}], + slowMinSpeed: [{value: "", disabled: true}], + slowMaxTime: [{value: 0, disabled: true}], + slowIgnoreAboveSize: [{value: "", disabled: true}], + }); + + // Set up form control value change subscriptions to manage dependent control states + this.setupFormValueChangeListeners(); + + // Create an effect to update the form when the configuration changes + effect(() => { + const config = this.queueCleanerConfig(); + if (config) { + // Update the form with the current configuration + this.queueCleanerForm.patchValue({ + enabled: config.enabled, + runSequentially: config.runSequentially, + ignoredDownloadsPath: config.ignoredDownloadsPath, + + // Failed Import settings + failedImportMaxStrikes: config.failedImportMaxStrikes, + failedImportIgnorePrivate: config.failedImportIgnorePrivate, + failedImportDeletePrivate: config.failedImportDeletePrivate, + failedImportIgnorePatterns: config.failedImportIgnorePatterns, + + // Stalled settings + stalledMaxStrikes: config.stalledMaxStrikes, + stalledResetStrikesOnProgress: config.stalledResetStrikesOnProgress, + stalledIgnorePrivate: config.stalledIgnorePrivate, + stalledDeletePrivate: config.stalledDeletePrivate, + + // Downloading Metadata settings + downloadingMetadataMaxStrikes: config.downloadingMetadataMaxStrikes, + + // Slow Download settings + slowMaxStrikes: config.slowMaxStrikes, + slowResetStrikesOnProgress: config.slowResetStrikesOnProgress, + slowIgnorePrivate: config.slowIgnorePrivate, + slowDeletePrivate: config.slowDeletePrivate, + slowMinSpeed: config.slowMinSpeed, + slowMaxTime: config.slowMaxTime, + slowIgnoreAboveSize: config.slowIgnoreAboveSize, + }); + + // Update job schedule if it exists + if (config.jobSchedule) { + this.queueCleanerForm.get("jobSchedule")?.patchValue({ + every: config.jobSchedule.every, + type: config.jobSchedule.type, + }); + } + + // Update form control disabled states based on the configuration + this.updateFormControlDisabledStates(config); + } + }); + + // Effect to emit events when save operation completes or errors + effect(() => { + const error = this.queueCleanerError(); + if (error) { + this.error.emit(error); + } + }); + + effect(() => { + const saving = this.queueCleanerSaving(); + if (saving === false) { + // This will run after a save operation is completed (whether successful or not) + // We check if there's no error to determine if it was successful + if (!this.queueCleanerError()) { + this.saved.emit(); + } + } + }); + } + + /** + * 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 on the 'enabled' control + this.queueCleanerForm.get('enabled')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + this.updateMainControlsState(enabled); + }); + + // Listen for changes on 'failedImportMaxStrikes' control + this.queueCleanerForm.get('failedImportMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateFailedImportDependentControls(strikes); + }); + + // Listen for changes on 'stalledMaxStrikes' control + this.queueCleanerForm.get('stalledMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateStalledDependentControls(strikes); + }); + + // Listen for changes on 'slowMaxStrikes' control + this.queueCleanerForm.get('slowMaxStrikes')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((strikes: number) => { + this.updateSlowDependentControls(strikes); + }); + } + + /** + * Update form control disabled states based on the configuration + */ + private updateFormControlDisabledStates(config: QueueCleanerConfig): void { + const enabled = config.enabled; + const options = { onlySelf: true }; + + // Job schedule + if (enabled) { + this.queueCleanerForm.get("jobSchedule")?.enable(options); + this.queueCleanerForm.get("runSequentially")?.enable(options); + this.queueCleanerForm.get("ignoredDownloadsPath")?.enable(options); + } else { + this.queueCleanerForm.get("jobSchedule")?.disable(options); + this.queueCleanerForm.get("runSequentially")?.disable(options); + this.queueCleanerForm.get("ignoredDownloadsPath")?.disable(options); + } + + // Failed Import settings + const failedImportEnabled = enabled && config.failedImportMaxStrikes >= 3; + this.queueCleanerForm.get("failedImportIgnorePrivate")?.setDisabled(!failedImportEnabled, options); + this.queueCleanerForm.get("failedImportDeletePrivate")?.setDisabled(!failedImportEnabled, options); + this.queueCleanerForm.get("failedImportIgnorePatterns")?.setDisabled(!failedImportEnabled, options); + + // Stalled settings + const stalledEnabled = enabled && config.stalledMaxStrikes >= 3; + this.queueCleanerForm.get("stalledResetStrikesOnProgress")?.setDisabled(!stalledEnabled, options); + this.queueCleanerForm.get("stalledIgnorePrivate")?.setDisabled(!stalledEnabled, options); + this.queueCleanerForm.get("stalledDeletePrivate")?.setDisabled(!stalledEnabled, options); + + // Slow Download settings + const slowEnabled = enabled && config.slowMaxStrikes >= 3; + this.queueCleanerForm.get("slowResetStrikesOnProgress")?.setDisabled(!slowEnabled, options); + this.queueCleanerForm.get("slowIgnorePrivate")?.setDisabled(!slowEnabled, options); + this.queueCleanerForm.get("slowDeletePrivate")?.setDisabled(!slowEnabled, options); + this.queueCleanerForm.get("slowMinSpeed")?.setDisabled(!slowEnabled, options); + this.queueCleanerForm.get("slowMaxTime")?.setDisabled(!slowEnabled, options); + this.queueCleanerForm.get("slowIgnoreAboveSize")?.setDisabled(!slowEnabled, options); + } + + /** + * Update the state of main controls based on the 'enabled' control value + */ + private updateMainControlsState(enabled: boolean): void { + const options = { onlySelf: true }; + + if (enabled) { + this.queueCleanerForm.get('jobSchedule')?.enable(options); + this.queueCleanerForm.get('runSequentially')?.enable(options); + this.queueCleanerForm.get('ignoredDownloadsPath')?.enable(options); + } else { + this.queueCleanerForm.get('jobSchedule')?.disable(options); + this.queueCleanerForm.get('runSequentially')?.disable(options); + this.queueCleanerForm.get('ignoredDownloadsPath')?.disable(options); + } + } + + /** + * Update the state of Failed Import dependent controls based on the 'failedImportMaxStrikes' value + */ + private updateFailedImportDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('failedImportIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('failedImportDeletePrivate')?.enable(options); + this.queueCleanerForm.get('failedImportIgnorePatterns')?.enable(options); + } else { + this.queueCleanerForm.get('failedImportIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('failedImportDeletePrivate')?.disable(options); + this.queueCleanerForm.get('failedImportIgnorePatterns')?.disable(options); + } + } + + /** + * Update the state of Stalled dependent controls based on the 'stalledMaxStrikes' value + */ + private updateStalledDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('stalledResetStrikesOnProgress')?.enable(options); + this.queueCleanerForm.get('stalledIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('stalledDeletePrivate')?.enable(options); + } else { + this.queueCleanerForm.get('stalledResetStrikesOnProgress')?.disable(options); + this.queueCleanerForm.get('stalledIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('stalledDeletePrivate')?.disable(options); + } + } + + /** + * Update the state of Slow Download dependent controls based on the 'slowMaxStrikes' value + */ + private updateSlowDependentControls(strikes: number): void { + const enable = strikes >= 3; + const options = { onlySelf: true }; + + if (enable) { + this.queueCleanerForm.get('slowResetStrikesOnProgress')?.enable(options); + this.queueCleanerForm.get('slowIgnorePrivate')?.enable(options); + this.queueCleanerForm.get('slowDeletePrivate')?.enable(options); + this.queueCleanerForm.get('slowMinSpeed')?.enable(options); + this.queueCleanerForm.get('slowMaxTime')?.enable(options); + this.queueCleanerForm.get('slowIgnoreAboveSize')?.enable(options); + } else { + this.queueCleanerForm.get('slowResetStrikesOnProgress')?.disable(options); + this.queueCleanerForm.get('slowIgnorePrivate')?.disable(options); + this.queueCleanerForm.get('slowDeletePrivate')?.disable(options); + this.queueCleanerForm.get('slowMinSpeed')?.disable(options); + this.queueCleanerForm.get('slowMaxTime')?.disable(options); + this.queueCleanerForm.get('slowIgnoreAboveSize')?.disable(options); + } + } + + /** + * Save the queue cleaner configuration + */ + saveQueueCleanerConfig(): void { + if (this.queueCleanerForm.invalid) { + // Mark all fields as touched to show validation errors + this.markFormGroupTouched(this.queueCleanerForm); + this.messageService.add({ + severity: "error", + summary: "Validation Error", + detail: "Please correct the errors in the form before saving.", + life: 5000, + }); + return; + } + + // Get the form values + const formValues = this.queueCleanerForm.getRawValue(); // Get values including disabled fields + + // Build the configuration object + const config: QueueCleanerConfig = { + enabled: formValues.enabled, + // The cronExpression will be generated from the jobSchedule when saving + cronExpression: "", + jobSchedule: formValues.jobSchedule, + runSequentially: formValues.runSequentially, + ignoredDownloadsPath: formValues.ignoredDownloadsPath || "", + + // Failed Import settings + failedImportMaxStrikes: formValues.failedImportMaxStrikes, + failedImportIgnorePrivate: formValues.failedImportIgnorePrivate, + failedImportDeletePrivate: formValues.failedImportDeletePrivate, + failedImportIgnorePatterns: formValues.failedImportIgnorePatterns || [], + + // Stalled settings + stalledMaxStrikes: formValues.stalledMaxStrikes, + stalledResetStrikesOnProgress: formValues.stalledResetStrikesOnProgress, + stalledIgnorePrivate: formValues.stalledIgnorePrivate, + stalledDeletePrivate: formValues.stalledDeletePrivate, + + // Downloading Metadata settings + downloadingMetadataMaxStrikes: formValues.downloadingMetadataMaxStrikes, + + // Slow Download settings + slowMaxStrikes: formValues.slowMaxStrikes, + slowResetStrikesOnProgress: formValues.slowResetStrikesOnProgress, + slowIgnorePrivate: formValues.slowIgnorePrivate, + slowDeletePrivate: formValues.slowDeletePrivate, + slowMinSpeed: formValues.slowMinSpeed || "", + slowMaxTime: formValues.slowMaxTime, + slowIgnoreAboveSize: formValues.slowIgnoreAboveSize || "", + }; + + // Save the configuration + this.queueCleanerStore.saveConfig(config); + } + + /** + * Reset the queue cleaner configuration form to default values + */ + resetQueueCleanerConfig(): void { + this.queueCleanerForm.reset({ + enabled: false, + jobSchedule: { + every: 5, + type: ScheduleUnit.Minutes, + }, + runSequentially: false, + ignoredDownloadsPath: "", + + // Failed Import settings + failedImportMaxStrikes: 0, + failedImportIgnorePrivate: false, + failedImportDeletePrivate: false, + failedImportIgnorePatterns: [], + + // Stalled settings + stalledMaxStrikes: 0, + stalledResetStrikesOnProgress: false, + stalledIgnorePrivate: false, + stalledDeletePrivate: false, + + // Downloading Metadata settings + downloadingMetadataMaxStrikes: 0, + + // Slow Download settings + slowMaxStrikes: 0, + slowResetStrikesOnProgress: false, + slowIgnorePrivate: false, + slowDeletePrivate: false, + slowMinSpeed: "", + slowMaxTime: 0, + slowIgnoreAboveSize: "", + }); + + // Manually update control states after reset + this.updateMainControlsState(false); + this.updateFailedImportDependentControls(0); + this.updateStalledDependentControls(0); + this.updateSlowDependentControls(0); + } + + /** + * 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.touched && control.hasError(errorName) : false; + } + + /** + * 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.touched && control.hasError(errorName) : false; + } +}