mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-31 01:48:49 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44994d5b21 |
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
code/frontend/src/app/shared/directives/index.ts
Normal file
1
code/frontend/src/app/shared/directives/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './numeric-input.directive';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user