Compare commits

...

2 Commits

Author SHA1 Message Date
Flaminel
88f40438af Fix validations and increased strikes limits (#212) 2025-07-01 13:18:50 +03:00
Flaminel
0a9ec06841 removed forgotten release step from MacOS workflow 2025-07-01 11:05:00 +03:00
18 changed files with 161 additions and 80 deletions

View File

@@ -363,14 +363,4 @@ jobs:
path: '${{ env.pkgName }}'
retention-days: 30
- name: Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
name: ${{ env.releaseVersion }}
tag_name: ${{ env.releaseVersion }}
repository: ${{ env.githubRepository }}
token: ${{ env.REPO_READONLY_PAT }}
make_latest: true
files: |
${{ env.pkgName }}
# Removed individual release step - handled by main release workflow

View File

@@ -101,7 +101,7 @@
<div class="field-input">
<input type="text" pInputText formControlName="cronExpression" placeholder="0 0/5 * ? * * *" />
</div>
<small *ngIf="contentBlockerForm.get('cronExpression')?.hasError('required') && contentBlockerForm.get('cronExpression')?.touched" class="p-error">Cron expression is required</small>
<small *ngIf="hasError('cronExpression', 'required')" class="p-error">Cron expression is required</small>
<small class="form-helper-text">Enter a valid Quartz cron expression (e.g., "0 0/5 * ? * * *" runs every 5 minutes)</small>
</div>
</div>

View File

@@ -605,7 +605,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.contentBlockerForm.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -633,7 +633,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
const control = parentControl.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}

View File

@@ -101,7 +101,7 @@
<div class="field-input">
<input type="text" pInputText formControlName="cronExpression" placeholder="0 0/5 * ? * * *" />
</div>
<small *ngIf="downloadCleanerForm.get('cronExpression')?.hasError('required') && downloadCleanerForm.get('cronExpression')?.touched" class="p-error">Cron expression is required</small>
<small *ngIf="hasError('cronExpression', 'required')" class="p-error">Cron expression is required</small>
<small class="form-helper-text">Enter a valid Quartz cron expression (e.g., "0 0/5 * ? * * *" runs every 5 minutes)</small>
</div>
</div>

View File

@@ -660,14 +660,14 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.downloadCleanerForm.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
* Check if the form has the unlinked categories validation error
*/
hasUnlinkedCategoriesError(): boolean {
return this.downloadCleanerForm.touched && this.downloadCleanerForm.hasError('unlinkedCategoriesRequired');
return this.downloadCleanerForm.dirty && this.downloadCleanerForm.hasError('unlinkedCategoriesRequired');
}
/**
@@ -695,7 +695,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
}
const control = parentControl.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -706,7 +706,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
if (!categoryGroup) return false;
const control = categoryGroup.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -715,7 +715,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
hasCategoryControlError(categoryIndex: number, controlName: string, errorName: string): boolean {
const categoryGroup = this.categoriesFormArray.at(categoryIndex);
const control = categoryGroup.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -723,7 +723,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
*/
hasCategoryGroupError(categoryIndex: number, errorName: string): boolean {
const categoryGroup = this.categoriesFormArray.at(categoryIndex);
return categoryGroup ? categoryGroup.touched && categoryGroup.hasError(errorName) : false;
return categoryGroup ? categoryGroup.dirty && categoryGroup.hasError(errorName) : false;
}
/**

View File

@@ -156,7 +156,7 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
*/
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
const control = form.get(controlName);
return control !== null && control.hasError(errorName) && control.touched;
return control !== null && control.hasError(errorName) && control.dirty;
}
/**

View File

@@ -355,7 +355,7 @@ export class GeneralSettingsComponent implements OnDestroy, CanComponentDeactiva
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.generalForm.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**

View File

@@ -42,6 +42,9 @@
decrementButtonIcon="pi pi-minus"
></p-inputNumber>
</div>
<small *ngIf="hasError('failedImportMaxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'min')" class="p-error">Value cannot be less than -1</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
</div>
</div>

View File

@@ -82,7 +82,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
constructor() {
// Initialize forms
this.globalForm = this.formBuilder.group({
failedImportMaxStrikes: [-1],
failedImportMaxStrikes: [-1, [Validators.required, Validators.min(-1), Validators.max(5000)]],
});
this.instanceForm = this.formBuilder.group({
@@ -211,11 +211,31 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
}
/**
* Check if a form control has an error
* Check if a form control has an error after it's been touched
*/
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
const control = form.get(controlName);
return control !== null && control.hasError(errorName) && control.touched;
hasError(formOrControlName: FormGroup | string, controlNameOrErrorName: string, errorName?: string): boolean {
if (formOrControlName instanceof FormGroup) {
// For instance form
const control = formOrControlName.get(controlNameOrErrorName);
return control !== null && control.hasError(errorName!) && control.dirty;
} else {
// For global form
const control = this.globalForm.get(formOrControlName);
return control ? control.dirty && control.hasError(controlNameOrErrorName) : false;
}
}
/**
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.globalForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}
const control = parentControl.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -406,8 +426,6 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
});
}
/**
* Get modal title based on mode
*/

View File

@@ -311,7 +311,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.notificationForm.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -319,7 +319,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
*/
hasNestedError(groupName: string, controlName: string, errorName: string): boolean {
const control = this.notificationForm.get(`${groupName}.${controlName}`);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**

View File

@@ -101,7 +101,7 @@
<div class="field-input">
<input type="text" pInputText formControlName="cronExpression" placeholder="0 0/5 * ? * * *" />
</div>
<small *ngIf="queueCleanerForm.get('cronExpression')?.hasError('required') && queueCleanerForm.get('cronExpression')?.touched" class="p-error">Cron expression is required</small>
<small *ngIf="hasError('cronExpression', 'required')" class="p-error">Cron expression is required</small>
<small class="form-helper-text">Enter a valid Quartz cron expression (e.g., "0 0/5 * ? * * *" runs every 5 minutes)</small>
</div>
</div>
@@ -128,15 +128,18 @@
title="Click for documentation"></i>
Max Strikes
</label>
<div class="field-input">
<p-inputNumber
formControlName="maxStrikes"
[showButtons]="true"
[min]="0"
[max]="10"
buttonLayout="horizontal"
>
</p-inputNumber>
<div>
<div class="field-input">
<p-inputNumber
formControlName="maxStrikes"
[showButtons]="true"
[min]="0"
buttonLayout="horizontal"
>
</p-inputNumber>
</div>
<small *ngIf="hasNestedError('failedImport', 'maxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasNestedError('failedImport', 'maxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text"
>Number of strikes before action is taken (0 to disable, min 3 to enable)</small
>
@@ -232,7 +235,7 @@
</p-inputNumber>
</div>
<small *ngIf="hasNestedError('stalled', 'maxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasNestedError('stalled', 'maxStrikes', 'max')" class="p-error">Value cannot exceed 100</small>
<small *ngIf="hasNestedError('stalled', 'maxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text"
>Number of strikes before action is taken (0 to disable, min 3 to enable)</small
>
@@ -311,7 +314,7 @@
</p-inputNumber>
</div>
<small *ngIf="hasNestedError('stalled', 'downloadingMetadataMaxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasNestedError('stalled', 'downloadingMetadataMaxStrikes', 'max')" class="p-error">Value cannot exceed 100</small>
<small *ngIf="hasNestedError('stalled', 'downloadingMetadataMaxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text"
>Number of strikes before action is taken (0 to disable, min 3 to enable)</small
>
@@ -351,7 +354,7 @@
</p-inputNumber>
</div>
<small *ngIf="hasNestedError('slow', 'maxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasNestedError('slow', 'maxStrikes', 'max')" class="p-error">Value cannot exceed 100</small>
<small *ngIf="hasNestedError('slow', 'maxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text"
>Number of strikes before action is taken (0 to disable, min 3 to enable)</small
>
@@ -422,16 +425,18 @@
title="Click for documentation"></i>
Maximum Time (hours)
</label>
<div class="field-input">
<p-inputNumber
formControlName="maxTime"
[showButtons]="true"
[min]="0"
buttonLayout="horizontal"
>
</p-inputNumber>
<div>
<div class="field-input">
<p-inputNumber
formControlName="maxTime"
[showButtons]="true"
[min]="0"
buttonLayout="horizontal"
>
</p-inputNumber>
</div>
<small *ngIf="hasNestedError('slow', 'maxTime', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasNestedError('slow', 'maxTime', 'max')" class="p-error">Value cannot exceed 168</small>
<small *ngIf="hasNestedError('slow', 'maxTime', 'max')" class="p-error">Value cannot exceed 1000</small>
<small class="form-helper-text">Maximum time allowed for slow downloads (0 means disabled)</small>
</div>
</div>

View File

@@ -142,7 +142,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
// Failed Import settings - nested group
failedImport: this.formBuilder.group({
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(100)]],
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
ignorePrivate: [{ value: false, disabled: true }],
deletePrivate: [{ value: false, disabled: true }],
ignoredPatterns: [{ value: [], disabled: true }],
@@ -150,21 +150,21 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
// Stalled settings - nested group
stalled: this.formBuilder.group({
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(100)]],
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(100)]],
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(100)]],
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(168)]],
maxTime: [{ value: 0, disabled: true }, [Validators.required, Validators.min(0), Validators.max(1000)]],
ignoreAboveSize: [{ value: "", disabled: true }],
}),
@@ -675,7 +675,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.queueCleanerForm.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -703,7 +703,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
}
const control = parentControl.get(controlName);
return control ? control.touched && control.hasError(errorName) : false;
return control ? control.dirty && control.hasError(errorName) : false;
}

View File

@@ -42,6 +42,9 @@
decrementButtonIcon="pi pi-minus"
></p-inputNumber>
</div>
<small *ngIf="hasError('failedImportMaxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'min')" class="p-error">Value cannot be less than -1</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
</div>
</div>

View File

@@ -82,7 +82,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
constructor() {
// Initialize forms
this.globalForm = this.formBuilder.group({
failedImportMaxStrikes: [-1],
failedImportMaxStrikes: [-1, [Validators.required, Validators.min(-1), Validators.max(5000)]],
});
this.instanceForm = this.formBuilder.group({
@@ -211,11 +211,31 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
}
/**
* Check if a form control has an error
* Check if a form control has an error after it's been touched
*/
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
const control = form.get(controlName);
return control !== null && control.hasError(errorName) && control.touched;
hasError(formOrControlName: FormGroup | string, controlNameOrErrorName: string, errorName?: string): boolean {
if (formOrControlName instanceof FormGroup) {
// For instance form
const control = formOrControlName.get(controlNameOrErrorName);
return control !== null && control.hasError(errorName!) && control.dirty;
} else {
// For global form
const control = this.globalForm.get(formOrControlName);
return control ? control.dirty && control.hasError(controlNameOrErrorName) : false;
}
}
/**
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.globalForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}
const control = parentControl.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -406,8 +426,6 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
});
}
/**
* Get modal title based on mode
*/

View File

@@ -42,6 +42,9 @@
decrementButtonIcon="pi pi-minus"
></p-inputNumber>
</div>
<small *ngIf="hasError('failedImportMaxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'min')" class="p-error">Value cannot be less than -1</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
</div>
</div>

View File

@@ -82,7 +82,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
constructor() {
// Initialize forms
this.globalForm = this.formBuilder.group({
failedImportMaxStrikes: [-1],
failedImportMaxStrikes: [-1, [Validators.required, Validators.min(-1), Validators.max(5000)]],
});
this.instanceForm = this.formBuilder.group({
@@ -211,11 +211,18 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
}
/**
* Check if a form control has an error
* Check if a form control has an error after it's been touched
*/
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
const control = form.get(controlName);
return control !== null && control.hasError(errorName) && control.touched;
hasError(formOrControlName: FormGroup | string, controlNameOrErrorName: string, errorName?: string): boolean {
if (formOrControlName instanceof FormGroup) {
// For instance form
const control = formOrControlName.get(controlNameOrErrorName);
return control !== null && control.hasError(errorName!) && control.dirty;
} else {
// For global form
const control = this.globalForm.get(formOrControlName);
return control ? control.dirty && control.hasError(controlNameOrErrorName) : false;
}
}
/**
@@ -412,4 +419,17 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
get modalTitle(): string {
return this.modalMode === 'add' ? 'Add Readarr Instance' : 'Edit Readarr Instance';
}
/**
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.globalForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}
const control = parentControl.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
}

View File

@@ -42,6 +42,9 @@
decrementButtonIcon="pi pi-minus"
></p-inputNumber>
</div>
<small *ngIf="hasError('failedImportMaxStrikes', 'required')" class="p-error">This field is required</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'min')" class="p-error">Value cannot be less than -1</small>
<small *ngIf="hasError('failedImportMaxStrikes', 'max')" class="p-error">Value cannot exceed 5000</small>
<small class="form-helper-text">Maximum number of strikes before removing a failed import (-1 to use global setting; 0 to disable)</small>
</div>
</div>

View File

@@ -82,7 +82,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
constructor() {
// Initialize forms
this.globalForm = this.formBuilder.group({
failedImportMaxStrikes: [-1],
failedImportMaxStrikes: [-1, [Validators.required, Validators.min(-1), Validators.max(5000)]],
});
this.instanceForm = this.formBuilder.group({
@@ -211,11 +211,31 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
}
/**
* Check if a form control has an error
* Check if a form control has an error after it's been touched
*/
hasError(form: FormGroup, controlName: string, errorName: string): boolean {
const control = form.get(controlName);
return control !== null && control.hasError(errorName) && control.touched;
hasError(formOrControlName: FormGroup | string, controlNameOrErrorName: string, errorName?: string): boolean {
if (formOrControlName instanceof FormGroup) {
// For instance form
const control = formOrControlName.get(controlNameOrErrorName);
return control !== null && control.hasError(errorName!) && control.dirty;
} else {
// For global form
const control = this.globalForm.get(formOrControlName);
return control ? control.dirty && control.hasError(controlNameOrErrorName) : false;
}
}
/**
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.globalForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}
const control = parentControl.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
/**
@@ -406,8 +426,6 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
});
}
/**
* Get modal title based on mode
*/