try fix settings enablement

This commit is contained in:
Flaminel
2025-05-30 23:18:36 +03:00
parent 84d984082c
commit d078ea288c
4 changed files with 776 additions and 116 deletions

View File

@@ -1,5 +1,5 @@
{
"tabWidth": 4,
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": true,

View File

@@ -19,16 +19,14 @@
[min]="1"
[max]="59"
buttonLayout="horizontal"
inputStyleClass="schedule-value"
[disabled]="!queueCleanerForm.get('enabled')?.value">
inputStyleClass="schedule-value">
</p-inputNumber>
<p-selectButton
formControlName="type"
[options]="scheduleUnitOptions"
optionLabel="label"
optionValue="value"
[disabled]="!queueCleanerForm.get('enabled')?.value">
optionValue="value">
</p-selectButton>
</div>
<small class="form-helper-text">How often the queue cleaner should run</small>
@@ -37,7 +35,7 @@
<div class="field-row">
<label class="field-label">Run Sequentially</label>
<div class="field-input">
<p-checkbox formControlName="runSequentially" [binary]="true" inputId="qcRunSequentially" [disabled]="!queueCleanerForm.get('enabled')?.value"></p-checkbox>
<p-checkbox formControlName="runSequentially" [binary]="true" inputId="qcRunSequentially"></p-checkbox>
<small class="form-helper-text">When enabled, jobs will run one after another instead of in parallel</small>
</div>
</div>
@@ -45,7 +43,7 @@
<div class="field-row">
<label class="field-label">Ignored Downloads Path</label>
<div class="field-input">
<input pInputText formControlName="ignoredDownloadsPath" [disabled]="!queueCleanerForm.get('enabled')?.value" />
<input pInputText formControlName="ignoredDownloadsPath" />
<small class="form-helper-text">Path to the file containing ignored downloads</small>
</div>
</div>
@@ -53,7 +51,7 @@
<!-- Detailed Settings in Accordion -->
<p-accordion [multiple]="true" styleClass="mt-3">
<!-- Failed Import Settings -->
<p-accordionTab header="Failed Import Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
<p-accordionTab header="Failed Import Settings">
<div class="field-row">
<label class="field-label">Max Strikes</label>
<div class="field-input">
@@ -71,7 +69,7 @@
<div class="field-row">
<label class="field-label">Ignore Private</label>
<div class="field-input">
<p-checkbox formControlName="failedImportIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="failedImportIgnorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
</div>
</div>
@@ -79,7 +77,7 @@
<div class="field-row">
<label class="field-label">Delete Private</label>
<div class="field-input">
<p-checkbox formControlName="failedImportDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="failedImportDeletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
</div>
</div>
@@ -87,14 +85,14 @@
<div class="field-row">
<label class="field-label">Ignore Patterns</label>
<div class="field-input">
<p-chips formControlName="failedImportIgnorePatterns" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3" placeholder="Add pattern and press Enter"></p-chips>
<p-chips formControlName="failedImportIgnorePatterns" placeholder="Add pattern and press Enter"></p-chips>
<small class="form-helper-text">Patterns to ignore (e.g., *sample*)</small>
</div>
</div>
</p-accordionTab>
<!-- Stalled Settings -->
<p-accordionTab header="Stalled Download Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
<p-accordionTab header="Stalled Download Settings">
<div class="field-row">
<label class="field-label">Max Strikes</label>
<div class="field-input">
@@ -112,7 +110,7 @@
<div class="field-row">
<label class="field-label">Reset Strikes On Progress</label>
<div class="field-input">
<p-checkbox formControlName="stalledResetStrikesOnProgress" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="stalledResetStrikesOnProgress" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, strikes will be reset if download progress is made</small>
</div>
</div>
@@ -120,7 +118,7 @@
<div class="field-row">
<label class="field-label">Ignore Private</label>
<div class="field-input">
<p-checkbox formControlName="stalledIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="stalledIgnorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
</div>
</div>
@@ -128,14 +126,14 @@
<div class="field-row">
<label class="field-label">Delete Private</label>
<div class="field-input">
<p-checkbox formControlName="stalledDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="stalledDeletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
</div>
</div>
</p-accordionTab>
<!-- Downloading Metadata Settings -->
<p-accordionTab header="Downloading Metadata Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
<p-accordionTab header="Downloading Metadata Settings">
<div class="field-row">
<label class="field-label">Max Strikes</label>
<div class="field-input">
@@ -152,7 +150,7 @@
</p-accordionTab>
<!-- Slow Download Settings -->
<p-accordionTab header="Slow Download Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
<p-accordionTab header="Slow Download Settings">
<div class="field-row">
<label class="field-label">Max Strikes</label>
<div class="field-input">
@@ -170,7 +168,7 @@
<div class="field-row">
<label class="field-label">Reset Strikes On Progress</label>
<div class="field-input">
<p-checkbox formControlName="slowResetStrikesOnProgress" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="slowResetStrikesOnProgress" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, strikes will be reset if download progress is made</small>
</div>
</div>
@@ -178,7 +176,7 @@
<div class="field-row">
<label class="field-label">Ignore Private</label>
<div class="field-input">
<p-checkbox formControlName="slowIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="slowIgnorePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
</div>
</div>
@@ -186,7 +184,7 @@
<div class="field-row">
<label class="field-label">Delete Private</label>
<div class="field-input">
<p-checkbox formControlName="slowDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
<p-checkbox formControlName="slowDeletePrivate" [binary]="true"></p-checkbox>
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
</div>
</div>
@@ -196,7 +194,6 @@
<div class="field-input">
<app-byte-size-input
formControlName="slowMinSpeed"
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"
[min]="0"
placeholder="Enter minimum speed"
helpText="Minimum speed threshold for slow downloads (e.g., 100KB/s)">
@@ -212,8 +209,7 @@
[showButtons]="true"
[min]="0"
[max]="168"
buttonLayout="horizontal"
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3">
buttonLayout="horizontal">
</p-inputNumber>
<small class="form-helper-text">Maximum time allowed for slow downloads (in hours)</small>
</div>
@@ -224,7 +220,6 @@
<div class="field-input">
<app-byte-size-input
formControlName="slowIgnoreAboveSize"
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"
[min]="0"
placeholder="Enter size threshold"
helpText="Size threshold above which slow downloads are ignored">

View File

@@ -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<void>();
@Output() error = new EventEmitter<string>();
// 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<void>();
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;
}

View File

@@ -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<void>();
@Output() error = new EventEmitter<string>();
// 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<void>();
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;
}
}