Compare commits

...

1 Commits

Author SHA1 Message Date
Flaminel
44994d5b21 Fix Notifiarr channel id input (#267) 2025-08-04 22:07:33 +03:00
5 changed files with 189 additions and 8 deletions

View File

@@ -53,16 +53,15 @@
title="Click for documentation">
</i>
Channel ID
</label>
<div class="field-input">
<p-inputNumber
placeholder="Enter channel ID"
formControlName="channelId"
[min]="0"
[useGrouping]="false"
>
</p-inputNumber>
<input
type="text"
pInputText
formControlName="channelId"
placeholder="Enter Discord channel ID"
numericInput
/>
<small class="form-helper-text">The Discord channel ID where notifications will be sent</small>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { Subject, takeUntil } from "rxjs";
import { NotificationConfigStore } from "./notification-config.store";
import { CanComponentDeactivate } from "../../core/guards";
import { NotificationsConfig } from "../../shared/models/notifications-config.model";
import { NumericInputDirective } from "../../shared/directives";
// PrimeNG Components
import { CardModule } from "primeng/card";
@@ -30,6 +31,7 @@ import { LoadingErrorStateComponent } from "../../shared/components/loading-erro
ButtonModule,
ToastModule,
LoadingErrorStateComponent,
NumericInputDirective,
],
providers: [NotificationConfigStore],
templateUrl: "./notification-settings.component.html",

View File

@@ -0,0 +1 @@
export * from './numeric-input.directive';

View File

@@ -0,0 +1,94 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NumericInputDirective } from './numeric-input.directive';
@Component({
template: `
<form [formGroup]="testForm">
<input
type="text"
formControlName="channelId"
numericInput
data-testid="numeric-input"
/>
</form>
`
})
class TestComponent {
testForm = new FormGroup({
channelId: new FormControl('')
});
}
describe('NumericInputDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let inputElement: HTMLInputElement;
let inputDebugElement: DebugElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [ReactiveFormsModule, NumericInputDirective]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
inputDebugElement = fixture.debugElement.query(By.css('[data-testid="numeric-input"]'));
inputElement = inputDebugElement.nativeElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should allow numeric input', () => {
// Simulate typing numbers
inputElement.value = '123456789';
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.testForm.get('channelId')?.value).toBe('123456789');
});
it('should remove non-numeric characters', () => {
// Simulate typing mixed input
inputElement.value = '123abc456def';
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.testForm.get('channelId')?.value).toBe('123456');
expect(inputElement.value).toBe('123456');
});
it('should handle Discord channel ID format', () => {
// Discord channel IDs are typically 18-19 digits
const discordChannelId = '123456789012345678';
inputElement.value = discordChannelId;
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.testForm.get('channelId')?.value).toBe(discordChannelId);
});
it('should prevent non-numeric keypress', () => {
const event = new KeyboardEvent('keydown', { keyCode: 65 }); // 'A' key
spyOn(event, 'preventDefault');
inputElement.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should allow control keys', () => {
const backspaceEvent = new KeyboardEvent('keydown', { keyCode: 8 }); // Backspace
spyOn(backspaceEvent, 'preventDefault');
inputElement.dispatchEvent(backspaceEvent);
expect(backspaceEvent.preventDefault).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,85 @@
import { Directive, HostListener } from '@angular/core';
import { NgControl } from '@angular/forms';
/**
* Directive that restricts input to numeric characters only.
* Useful for fields that need to accept very long numeric values like Discord channel IDs
* that exceed JavaScript's safe integer limits.
*
* Usage: <input type="text" numericInput formControlName="channelId" />
*/
@Directive({
selector: '[numericInput]',
standalone: true
})
export class NumericInputDirective {
private regex = /^\d*$/; // Only allow positive integers (no decimals or negative numbers)
constructor(private ngControl: NgControl) {}
@HostListener('input', ['$event'])
onInput(event: Event): void {
const input = event.target as HTMLInputElement;
const originalValue = input.value;
if (!this.regex.test(originalValue)) {
// Strip all non-numeric characters
const sanitized = originalValue.replace(/[^\d]/g, '');
// Update the form control value
this.ngControl.control?.setValue(sanitized);
// Update the input display value
input.value = sanitized;
}
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent): void {
// Allow: backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].indexOf(event.keyCode) !== -1 ||
// Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
(event.keyCode === 65 && event.ctrlKey === true) ||
(event.keyCode === 67 && event.ctrlKey === true) ||
(event.keyCode === 86 && event.ctrlKey === true) ||
(event.keyCode === 88 && event.ctrlKey === true) ||
// Allow: home, end, left, right
(event.keyCode >= 35 && event.keyCode <= 39)) {
return;
}
// Ensure that it is a number and stop the keypress
if ((event.shiftKey || (event.keyCode < 48 || event.keyCode > 57)) && (event.keyCode < 96 || event.keyCode > 105)) {
event.preventDefault();
}
}
@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent): void {
const paste = event.clipboardData?.getData('text') || '';
const sanitized = paste.replace(/[^\d]/g, '');
// If the paste content has non-numeric characters, prevent default and handle manually
if (sanitized !== paste) {
event.preventDefault();
const input = event.target as HTMLInputElement;
const currentValue = input.value;
const start = input.selectionStart || 0;
const end = input.selectionEnd || 0;
const newValue = currentValue.substring(0, start) + sanitized + currentValue.substring(end);
// Update both the input value and form control
input.value = newValue;
this.ngControl.control?.setValue(newValue);
// Set cursor position after pasted content
setTimeout(() => {
input.setSelectionRange(start + sanitized.length, start + sanitized.length);
});
}
// If paste content is all numeric, allow normal paste behavior
// The input event will handle form control synchronization
}
}