mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-03-06 07:16:10 -05:00
try add sonarr config #2
This commit is contained in:
@@ -69,70 +69,55 @@
|
||||
</div>
|
||||
|
||||
<!-- Instances Section -->
|
||||
<div class="section-header">
|
||||
<div class="section-header mt-4">
|
||||
<h3>Sonarr Instances</h3>
|
||||
<small class="form-helper-text">Configure multiple Sonarr server instances</small>
|
||||
<small>Configure multiple Sonarr server instances</small>
|
||||
</div>
|
||||
|
||||
<!-- Instances Table -->
|
||||
<div class="instances-table mt-2">
|
||||
<p-table [value]="instances.controls" [responsive]="true" styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th>API Key</th>
|
||||
<th style="width: 120px">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-instance let-i="rowIndex">
|
||||
<tr [formGroup]="instance">
|
||||
<td>{{ instance.get('name')?.value }}</td>
|
||||
<td>{{ instance.get('url')?.value }}</td>
|
||||
<td>{{ instance.get('apiKey')?.value | slice:0:8 }}•••••••</td>
|
||||
<td>
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-text p-button-sm"
|
||||
(click)="openEditInstanceDialog(i)"
|
||||
[disabled]="!sonarrForm.get('enabled')?.value"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-text p-button-danger p-button-sm"
|
||||
(click)="removeInstance(i)"
|
||||
[disabled]="!sonarrForm.get('enabled')?.value"
|
||||
></button>
|
||||
<!-- Instances Container -->
|
||||
<div class="instances-container">
|
||||
<!-- Empty state message when no instances -->
|
||||
<div *ngIf="instances.controls.length === 0" class="empty-instances-message p-3 text-center">
|
||||
<p>No Sonarr instances defined. Add an instance to start using Sonarr integration.</p>
|
||||
</div>
|
||||
|
||||
<!-- Instance cards -->
|
||||
<div class="instance-list">
|
||||
<div *ngFor="let instance of instances.controls; let i = index" class="instance-item" [formGroup]="getInstanceAsFormGroup(i)">
|
||||
<div class="instance-header">
|
||||
<div class="instance-title">
|
||||
<i class="pi pi-server instance-icon"></i>
|
||||
<input type="text" pInputText formControlName="name" placeholder="Instance name" class="instance-name-input" />
|
||||
</div>
|
||||
<button pButton type="button" icon="pi pi-trash" class="p-button-danger p-button-sm"
|
||||
(click)="removeInstance(i)" [disabled]="sonarrForm.disabled || !sonarrForm.get('enabled')?.value"></button>
|
||||
</div>
|
||||
<small *ngIf="hasInstanceFieldError(i, 'name', 'required')" class="p-error block">Name is required</small>
|
||||
|
||||
<div class="instance-content">
|
||||
<div class="instance-field">
|
||||
<label>URL</label>
|
||||
<div class="field-input">
|
||||
<input type="text" pInputText formControlName="url" placeholder="http://localhost:8989" required />
|
||||
<small *ngIf="hasInstanceFieldError(i, 'url', 'required')" class="p-error block">URL is required</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<div class="p-3">No Sonarr instances configured</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
|
||||
<!-- Add Instance Button -->
|
||||
<div class="mt-3">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Add Sonarr Instance"
|
||||
icon="pi pi-plus"
|
||||
class="p-button-outlined"
|
||||
(click)="openAddInstanceDialog()"
|
||||
[disabled]="!sonarrForm.get('enabled')?.value"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="instance-field">
|
||||
<label>API Key</label>
|
||||
<div class="field-input">
|
||||
<input type="password" pInputText formControlName="apiKey" placeholder="Your Sonarr API key" required />
|
||||
<small *ngIf="hasInstanceFieldError(i, 'apiKey', 'required')" class="p-error block">API key is required</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-content-end mt-3">
|
||||
<button pButton type="button" icon="pi pi-plus" label="Add Sonarr Instance"
|
||||
(click)="addInstance()" [disabled]="!sonarrForm.get('enabled')?.value" class="p-button-success"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
@@ -159,70 +144,3 @@
|
||||
</form>
|
||||
</div>
|
||||
</p-card>
|
||||
|
||||
<!-- Instance Dialog -->
|
||||
<p-dialog
|
||||
[(visible)]="showInstanceDialog"
|
||||
[header]="editingInstanceIndex !== null ? 'Edit Sonarr Instance' : 'Add Sonarr Instance'"
|
||||
[modal]="true"
|
||||
[style]="{ width: '450px' }"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
>
|
||||
<form [formGroup]="instanceForm" class="p-fluid">
|
||||
<div class="field mb-4">
|
||||
<label for="name" class="font-bold">Name*</label>
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
id="name"
|
||||
formControlName="name"
|
||||
placeholder="My Sonarr Server"
|
||||
required
|
||||
/>
|
||||
<small *ngIf="hasInstanceError('name', 'required')" class="p-error">Name is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field mb-4">
|
||||
<label for="url" class="font-bold">URL*</label>
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
id="url"
|
||||
formControlName="url"
|
||||
placeholder="http://localhost:8989"
|
||||
required
|
||||
/>
|
||||
<small *ngIf="hasInstanceError('url', 'required')" class="p-error">URL is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field mb-4">
|
||||
<label for="apiKey" class="font-bold">API Key*</label>
|
||||
<input
|
||||
type="password"
|
||||
pInputText
|
||||
id="apiKey"
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Sonarr API key"
|
||||
required
|
||||
/>
|
||||
<small *ngIf="hasInstanceError('apiKey', 'required')" class="p-error">API key is required</small>
|
||||
</div>
|
||||
</form>
|
||||
<ng-template pTemplate="footer">
|
||||
<button
|
||||
pButton
|
||||
label="Cancel"
|
||||
icon="pi pi-times"
|
||||
class="p-button-text"
|
||||
(click)="cancelInstanceDialog()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
label="Save"
|
||||
icon="pi pi-check"
|
||||
(click)="saveInstance()"
|
||||
[disabled]="instanceForm.invalid"
|
||||
></button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
|
||||
@@ -2,17 +2,115 @@
|
||||
|
||||
@import '../styles/settings-shared.scss';
|
||||
|
||||
.instances-table {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.instances-container {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.instance-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instance-item {
|
||||
background-color: var(--surface-hover);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.instance-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.instance-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.instance-icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.instance-name-input {
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-content {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.instance-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex: 0 0 200px;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.field-input {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-instances-message {
|
||||
background-color: var(--surface-ground);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--surface-border);
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ 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 { DialogModule } from "primeng/dialog";
|
||||
import { TableModule } from "primeng/table";
|
||||
import { ToastModule } from "primeng/toast";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { DropdownModule } from "primeng/dropdown";
|
||||
@@ -32,10 +29,7 @@ import { LoadingErrorStateComponent } from "../../shared/components/loading-erro
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
InputNumberModule,
|
||||
AccordionModule,
|
||||
SelectButtonModule,
|
||||
DialogModule,
|
||||
TableModule,
|
||||
ToastModule,
|
||||
DropdownModule,
|
||||
LoadingErrorStateComponent,
|
||||
@@ -50,18 +44,13 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
|
||||
// Sonarr Configuration Form
|
||||
sonarrForm: FormGroup;
|
||||
|
||||
|
||||
// Original form values for tracking changes
|
||||
private originalFormValues: any;
|
||||
|
||||
|
||||
// Track whether the form has actual changes compared to original values
|
||||
hasActualChanges = false;
|
||||
|
||||
// Dialog state
|
||||
showInstanceDialog = false;
|
||||
editingInstanceIndex: number | null = null;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
// SonarrSearchType options
|
||||
searchTypeOptions = [
|
||||
{ label: "Episode", value: SonarrSearchType.Episode },
|
||||
@@ -78,11 +67,11 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
private notificationService = inject(NotificationService);
|
||||
private sonarrStore = inject(SonarrConfigStore);
|
||||
|
||||
// Signals from the store
|
||||
readonly sonarrConfig = this.sonarrStore.config;
|
||||
readonly sonarrLoading = this.sonarrStore.loading;
|
||||
readonly sonarrSaving = this.sonarrStore.saving;
|
||||
readonly sonarrError = this.sonarrStore.error;
|
||||
// Signals from store
|
||||
sonarrConfig = this.sonarrStore.config;
|
||||
sonarrLoading = this.sonarrStore.loading;
|
||||
sonarrError = this.sonarrStore.error;
|
||||
sonarrSaving = this.sonarrStore.saving;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -99,27 +88,17 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
searchType: [SonarrSearchType.Episode, Validators.required],
|
||||
});
|
||||
|
||||
// Initialize the instance form
|
||||
this.instanceForm = this.formBuilder.group({
|
||||
id: [''],
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required]],
|
||||
apiKey: ['', [Validators.required]],
|
||||
});
|
||||
|
||||
// Add instances FormArray to main form
|
||||
this.sonarrForm.addControl('instances', this.formBuilder.array([]));
|
||||
|
||||
// Setup value change listeners
|
||||
this.setupFormValueChangeListeners();
|
||||
// Load Sonarr config data
|
||||
this.sonarrStore.loadConfig();
|
||||
|
||||
// Create an effect to respond to config changes
|
||||
// Setup effect to update form when config changes
|
||||
effect(() => {
|
||||
const config = this.sonarrConfig();
|
||||
if (config) {
|
||||
this.updateForm(config);
|
||||
this.storeOriginalValues();
|
||||
this.updateFormControlDisabledStates(config);
|
||||
this.updateFormFromConfig(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -133,97 +112,9 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up listeners for form control value changes to manage dependent control states
|
||||
* Update form with values from the configuration
|
||||
*/
|
||||
private setupFormValueChangeListeners(): void {
|
||||
// Listen for changes on the enabled control
|
||||
this.sonarrForm
|
||||
.get("enabled")
|
||||
?.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enabled) => {
|
||||
this.updateMainControlsState(enabled);
|
||||
});
|
||||
|
||||
// Listen for form changes to update the hasActualChanges flag
|
||||
this.sonarrForm.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.hasActualChanges = this.formValuesChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store original form values for dirty checking
|
||||
*/
|
||||
private storeOriginalValues(): void {
|
||||
this.originalFormValues = JSON.parse(JSON.stringify(this.sonarrForm.value));
|
||||
this.sonarrForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current form values are different from the original values
|
||||
*/
|
||||
private formValuesChanged(): boolean {
|
||||
return !this.isEqual(this.sonarrForm.value, this.originalFormValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare two objects for equality
|
||||
*/
|
||||
private isEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 == null || obj2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
const val1 = obj1[key];
|
||||
const val2 = obj2[key];
|
||||
const areObjects = typeof val1 === "object" && typeof val2 === "object";
|
||||
|
||||
if ((areObjects && !this.isEqual(val1, val2)) || (!areObjects && val1 !== val2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update form control disabled states based on the configuration
|
||||
*/
|
||||
private updateFormControlDisabledStates(config: SonarrConfig): void {
|
||||
const enabled = config.enabled;
|
||||
this.updateMainControlsState(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of main controls based on the 'enabled' control value
|
||||
*/
|
||||
private updateMainControlsState(enabled: boolean): void {
|
||||
const failedImportMaxStrikesControl = this.sonarrForm.get('failedImportMaxStrikes');
|
||||
const searchTypeControl = this.sonarrForm.get('searchType');
|
||||
|
||||
if (enabled) {
|
||||
failedImportMaxStrikesControl?.enable();
|
||||
searchTypeControl?.enable();
|
||||
} else {
|
||||
failedImportMaxStrikesControl?.disable();
|
||||
searchTypeControl?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the form with values from the configuration
|
||||
*/
|
||||
private updateForm(config: SonarrConfig): void {
|
||||
private updateFormFromConfig(config: SonarrConfig): void {
|
||||
// Update main form controls
|
||||
this.sonarrForm.patchValue({
|
||||
enabled: config.enabled,
|
||||
@@ -248,13 +139,77 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Store original form values for dirty checking
|
||||
this.storeOriginalValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instances form array
|
||||
* Store original form values for dirty checking
|
||||
*/
|
||||
get instances(): FormArray {
|
||||
return this.sonarrForm.get('instances') as FormArray;
|
||||
private storeOriginalValues(): void {
|
||||
this.originalFormValues = JSON.parse(JSON.stringify(this.sonarrForm.value));
|
||||
this.sonarrForm.markAsPristine();
|
||||
this.hasActualChanges = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current form values are different from the original values
|
||||
*/
|
||||
private formValuesChanged(): boolean {
|
||||
return !this.isEqual(this.sonarrForm.value, this.originalFormValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare two objects for equality
|
||||
*/
|
||||
private isEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 == null || obj2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
const val1 = obj1[key];
|
||||
const val2 = obj2[key];
|
||||
const areObjects = typeof val1 === "object" && typeof val2 === "object";
|
||||
|
||||
if ((areObjects && !this.isEqual(val1, val2)) || (!areObjects && val1 !== val2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update form control disabled states based on the configuration
|
||||
*/
|
||||
private updateFormControlDisabledStates(config: SonarrConfig): void {
|
||||
const enabled = config.enabled;
|
||||
this.updateMainControlsState(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of main controls based on the 'enabled' control value
|
||||
*/
|
||||
private updateMainControlsState(enabled: boolean): void {
|
||||
const failedImportMaxStrikesControl = this.sonarrForm.get('failedImportMaxStrikes');
|
||||
const searchTypeControl = this.sonarrForm.get('searchType');
|
||||
|
||||
if (enabled) {
|
||||
failedImportMaxStrikesControl?.enable();
|
||||
searchTypeControl?.enable();
|
||||
} else {
|
||||
failedImportMaxStrikesControl?.disable();
|
||||
searchTypeControl?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,7 +222,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
|
||||
// Get data from form
|
||||
const formValue = this.sonarrForm.getRawValue();
|
||||
|
||||
|
||||
// Create config object
|
||||
const sonarrConfig: SonarrConfig = {
|
||||
enabled: formValue.enabled,
|
||||
@@ -275,7 +230,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
searchType: formValue.searchType,
|
||||
instances: formValue.instances || []
|
||||
};
|
||||
|
||||
|
||||
// Save the configuration
|
||||
this.sonarrStore.saveConfig(sonarrConfig);
|
||||
|
||||
@@ -285,18 +240,18 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
if (!this.sonarrSaving()) {
|
||||
// Re-enable the form
|
||||
this.sonarrForm.enable();
|
||||
|
||||
|
||||
// If still disabled, update control states based on enabled state
|
||||
if (!this.sonarrForm.get('enabled')?.value) {
|
||||
this.updateMainControlsState(false);
|
||||
}
|
||||
|
||||
|
||||
// Update original values to match current form state
|
||||
this.storeOriginalValues();
|
||||
|
||||
|
||||
// Notify listeners that we've completed the save
|
||||
this.saved.emit();
|
||||
|
||||
|
||||
// Show success message
|
||||
this.notificationService.showSuccess("Sonarr configuration saved successfully");
|
||||
} else {
|
||||
@@ -304,16 +259,16 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
setTimeout(checkSaveCompletion, 100);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Start checking for save completion
|
||||
checkSaveCompletion();
|
||||
} else {
|
||||
// Form is invalid, show error message
|
||||
this.notificationService.showValidationError();
|
||||
|
||||
|
||||
// Emit error for parent components
|
||||
this.error.emit("Please fix validation errors before saving.");
|
||||
|
||||
|
||||
// Mark all controls as touched to show validation errors
|
||||
this.markFormGroupTouched(this.sonarrForm);
|
||||
}
|
||||
@@ -335,71 +290,27 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
|
||||
// Update control states after reset
|
||||
this.updateMainControlsState(false);
|
||||
|
||||
|
||||
// Mark form as dirty so the save button is enabled after reset
|
||||
this.sonarrForm.markAsDirty();
|
||||
this.hasActualChanges = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the instance dialog for adding a new instance
|
||||
* Add a new instance to the instances form array
|
||||
*/
|
||||
openAddInstanceDialog(): void {
|
||||
this.editingInstanceIndex = null;
|
||||
this.instanceForm.reset({
|
||||
id: '',
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: '',
|
||||
});
|
||||
this.showInstanceDialog = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the instance dialog for editing an existing instance
|
||||
*/
|
||||
openEditInstanceDialog(index: number): void {
|
||||
const instanceToEdit = this.instances.at(index).value;
|
||||
this.editingInstanceIndex = index;
|
||||
|
||||
this.instanceForm.reset({
|
||||
id: instanceToEdit.id || '',
|
||||
name: instanceToEdit.name,
|
||||
url: instanceToEdit.url,
|
||||
apiKey: instanceToEdit.apiKey,
|
||||
});
|
||||
|
||||
this.showInstanceDialog = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the instance from the dialog
|
||||
*/
|
||||
saveInstance(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceData = this.instanceForm.value;
|
||||
addInstance(): void {
|
||||
const instancesArray = this.sonarrForm.get('instances') as FormArray;
|
||||
|
||||
if (this.editingInstanceIndex !== null) {
|
||||
// Update existing instance
|
||||
instancesArray.at(this.editingInstanceIndex).patchValue(instanceData);
|
||||
} else {
|
||||
// Add new instance
|
||||
instancesArray.push(
|
||||
this.formBuilder.group({
|
||||
id: [instanceData.id || ''],
|
||||
name: [instanceData.name, Validators.required],
|
||||
url: [instanceData.url, Validators.required],
|
||||
apiKey: [instanceData.apiKey, Validators.required],
|
||||
})
|
||||
);
|
||||
}
|
||||
instancesArray.push(
|
||||
this.formBuilder.group({
|
||||
id: [''],
|
||||
name: ['', Validators.required],
|
||||
url: ['', Validators.required],
|
||||
apiKey: ['', Validators.required],
|
||||
})
|
||||
);
|
||||
|
||||
this.showInstanceDialog = false;
|
||||
this.sonarrForm.markAsDirty();
|
||||
this.hasActualChanges = true;
|
||||
}
|
||||
@@ -415,12 +326,21 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the instance dialog
|
||||
* Get the instances form array
|
||||
*/
|
||||
cancelInstanceDialog(): void {
|
||||
this.showInstanceDialog = false;
|
||||
get instances(): FormArray {
|
||||
return this.sonarrForm.get('instances') as FormArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance at the specified index as a FormGroup
|
||||
*/
|
||||
getInstanceAsFormGroup(index: number): FormGroup {
|
||||
return this.instances.at(index) as FormGroup;
|
||||
}
|
||||
|
||||
// hasInstanceFieldError is implemented below
|
||||
|
||||
/**
|
||||
* Mark all controls in a form group as touched
|
||||
*/
|
||||
@@ -435,18 +355,28 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form control has an error after it's been touched
|
||||
* Check if the form control has an error
|
||||
* @param controlName The name of the control to check
|
||||
* @param errorName The name of the error to check for
|
||||
* @returns True if the control has the specified error
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.sonarrForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
return control !== null && control.hasError(errorName) && control.touched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested form control errors
|
||||
* Check if an instance field has an error
|
||||
* @param instanceIndex The index of the instance in the array
|
||||
* @param fieldName The name of the field to check
|
||||
* @param errorName The name of the error to check for
|
||||
* @returns True if the field has the specified error
|
||||
*/
|
||||
hasInstanceError(controlName: string, errorName: string): boolean {
|
||||
const control = this.instanceForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
hasInstanceFieldError(instanceIndex: number, fieldName: string, errorName: string): boolean {
|
||||
const instancesArray = this.sonarrForm.get('instances') as FormArray;
|
||||
if (!instancesArray || !instancesArray.controls[instanceIndex]) return false;
|
||||
|
||||
const control = (instancesArray.controls[instanceIndex] as FormGroup).get(fieldName);
|
||||
return control !== null && control.hasError(errorName) && control.touched;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user