diff --git a/src/app/core/_components/forms/custom-forms/superhashlist/new-superhashlist/new-superhashlist.component.html b/src/app/core/_components/forms/custom-forms/superhashlist/new-superhashlist/new-superhashlist.component.html index 16182a75b..f1748ba34 100644 --- a/src/app/core/_components/forms/custom-forms/superhashlist/new-superhashlist/new-superhashlist.component.html +++ b/src/app/core/_components/forms/custom-forms/superhashlist/new-superhashlist/new-superhashlist.component.html @@ -10,7 +10,7 @@ [isLoading]="isLoading" [placeholder]="'Hashlists...'" [items]="selectHashlists" - [label]="'Select or search Hashlists *'" + [label]="'Select or search Hashlists'" [mergeIdAndName]="true" formControlName="hashlistIds" style="flex: 1;" diff --git a/src/app/core/_components/forms/custom-forms/task/new-supertasks/new-supertasks.component.html b/src/app/core/_components/forms/custom-forms/task/new-supertasks/new-supertasks.component.html index caf2c435c..e6e4357c1 100644 --- a/src/app/core/_components/forms/custom-forms/task/new-supertasks/new-supertasks.component.html +++ b/src/app/core/_components/forms/custom-forms/task/new-supertasks/new-supertasks.component.html @@ -10,7 +10,8 @@ [isLoading]="isLoading" [placeholder]="'Tasks...'" [items]="selectPretasks" - [label]="'Select or search Tasks *'" + [label]="'Select or search Tasks'" + [isRequired]="true" [mergeIdAndName]="true" formControlName="pretasks" style="flex: 1;" diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.html b/src/app/hashlists/new-hashlist/new-hashlist.component.html index 61cb6c3f7..08437da0b 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.html +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.html @@ -13,7 +13,8 @@ [isLoading]="isLoadingHashtypes" [placeholder]="'Hashtypes...'" [items]="selectHashtypes" - [label]="'Hashtype *'" + [label]="'Select or search Hashtype'" + [isRequired]="true" formControlName="hashTypeId" [multiselectEnabled]="false" [mergeIdAndName]="true" diff --git a/src/app/shared/input/multiselect/multiselect.component.html b/src/app/shared/input/multiselect/multiselect.component.html index a190e576f..88173731f 100644 --- a/src/app/shared/input/multiselect/multiselect.component.html +++ b/src/app/shared/input/multiselect/multiselect.component.html @@ -5,34 +5,44 @@ {{label}} - + -
+
{{ item.id }} - {{ item.name.length > 30 ? (item.name | slice:0:30) + '...' : item.name }} + {{ item.name?.length > 30 ? (item.name | slice:0:30) + '...' : item.name }}
+ - + + - {{ label }} is required + + + This field is required. + diff --git a/src/app/shared/input/multiselect/multiselect.component.ts b/src/app/shared/input/multiselect/multiselect.component.ts index 1dc980cd3..696cb6d16 100644 --- a/src/app/shared/input/multiselect/multiselect.component.ts +++ b/src/app/shared/input/multiselect/multiselect.component.ts @@ -5,6 +5,7 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { AfterViewInit, ChangeDetectionStrategy, Component, Input, ViewChild, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; import { MatInput } from '@angular/material/input'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -28,22 +29,27 @@ import { SelectOption, extractIds } from '@src/app/shared/utils/forms'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: false }) -export class InputMultiSelectComponent extends AbstractInputComponent implements AfterViewInit { +export class InputMultiSelectComponent extends AbstractInputComponent implements AfterViewInit { @Input() label = 'Select or search:'; @Input() placeholder = 'Select or search'; @Input() isLoading = false; @Input() items: SelectOption[] = []; @Input() multiselectEnabled = true; @Input() mergeIdAndName = false; - @Input() initialHashlistId: any; + @Input() initialHashlistId: string | number; @ViewChild('selectInput', { read: MatInput }) selectInput: MatInput; private searchInputSubject = new Subject(); - filteredItems: Observable; - selectedItems: SelectOption[] = []; + filteredItems: Observable; searchTerm = ''; + // Visual chips + selectedItems: SelectOption[] = []; + + // Validation model (dummy, never displayed) + chipGridValidation: SelectOption[] = []; + readonly separatorKeysCodes: number[] = [COMMA, ENTER]; // ENTER and COMMA key codes constructor(private sanitizer: DomSanitizer) { @@ -79,19 +85,74 @@ export class InputMultiSelectComponent extends AbstractInputComponent imple } } + public addChip(item: SelectOption): void { + if (!this.selectedItems.find((i) => i.id === item.id)) { + // Update visual array + if (this.multiselectEnabled) { + this.selectedItems.push(item); + } else { + this.selectedItems = [item]; + } + + // Update validation array + this.chipGridValidation = [...this.selectedItems]; + + // Remove item from available list + const index = this.items.findIndex((i) => i.id === item.id); + if (index !== -1) this.items.splice(index, 1); + + // Update filtered items + this.searchInputSubject.next(this.searchTerm); + + // Notify Angular forms + this.onChangeValue(this.selectedItems); + } + } + + public remove(item: SelectOption): void { + const index = this.selectedItems.findIndex((i) => i.id === item.id); + if (index >= 0) { + this.selectedItems.splice(index, 1); + this.chipGridValidation = [...this.selectedItems]; + + // Put back to available items + this.items.push(item); + this.searchInputSubject.next(this.searchTerm); + + this.onChangeValue(this.selectedItems); + } + } + + // When typing a separator key (ENTER, COMMA) + onInputChipAdd(event: MatChipInputEvent) { + const value = event.value?.trim(); + if (!value) return; + + this.addChip({ id: value, name: value }); + event.chipInput.clear(); + this.searchTerm = ''; + } + + // When selecting from autocomplete + onAutocompleteSelect(selected: SelectOption) { + this.addChip(selected); + this.searchTerm = ''; + this.searchInputSubject.next(this.searchTerm); + } + /** * Handles the change in the input value and triggers the corresponding change events. * - * @param {any} value - The new value of the input. + * @param value - The new value of the input. * @returns {void} */ /** * Handles the change in the input value and triggers the corresponding change events. * - * @param {any} value - The new value of the input. + * @param value - The new value of the input. * @returns {void} */ - onChangeValue(value): void { + onChangeValue(value: SelectOption | SelectOption[]): void { if (!this.multiselectEnabled) { if (Array.isArray(value)) { this.value = extractIds(value, 'id')[0]; @@ -122,41 +183,18 @@ export class InputMultiSelectComponent extends AbstractInputComponent imple this.searchInputSubject.next(this.searchTerm); } - /** - * Removes the specified item from the selected items. - * - * @param {SelectOption} item - The item to be removed. - * @returns {void} - */ - public remove(item: SelectOption): void { - const index = this.selectedItems.indexOf(item); - - if (index >= 0) { - // Add the removed item back to the unselected items - this.items.push(item); - - this.items.sort((a, b) => parseInt(a.id) - parseInt(b.id)); - - // Remove the item from the selected items - this.selectedItems.splice(index, 1); - - // Update the filteredItems observable - this.searchInputSubject.next(this.searchTerm); - - // Notify about the change - this.onChangeValue(this.selectedItems); - // this.onTouched(); - } - } - /** * Filters the items based on the provided search value. * * @param {string} value - The search value to filter the items. * @returns {SelectOption[]} - The filtered array of items. */ - private _filter(value: string): SelectOption[] { - const filterValue = value.toLowerCase(); + private _filter(value: string | SelectOption): SelectOption[] { + // If a SelectOption is passed by accident, convert to string + const searchString = typeof value === 'string' ? value : (value.name ?? ''); + + const filterValue = searchString.toLowerCase(); + return this.items.filter((item: SelectOption) => { const nameToSearch = this.mergeIdAndName ? `${item.id} ${item.name}`.toLowerCase() : item.name.toLowerCase(); return nameToSearch.includes(filterValue); diff --git a/src/app/shared/input/number/number.component.html b/src/app/shared/input/number/number.component.html index 69a0aa12f..25df6a6b3 100644 --- a/src/app/shared/input/number/number.component.html +++ b/src/app/shared/input/number/number.component.html @@ -1,8 +1,9 @@ {{ title }}
- +
-
{{ error }}
- {{ hint }} + + {{ title }} is required +
diff --git a/src/app/shared/input/number/number.component.ts b/src/app/shared/input/number/number.component.ts index f4f41660d..71eecd2d8 100644 --- a/src/app/shared/input/number/number.component.ts +++ b/src/app/shared/input/number/number.component.ts @@ -1,7 +1,8 @@ -import { AbstractInputComponent } from '../abstract-input'; import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AbstractInputComponent } from '@src/app/shared/input/abstract-input'; + /** * Custom Input Number Component. * @@ -15,16 +16,16 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'; * ``` */ @Component({ - selector: 'input-number', - templateUrl: './number.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => InputNumberComponent), - multi: true - } - ], - standalone: false + selector: 'input-number', + templateUrl: './number.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputNumberComponent), + multi: true + } + ], + standalone: false }) export class InputNumberComponent extends AbstractInputComponent { constructor() { diff --git a/src/app/shared/input/text-area/text-area.component.html b/src/app/shared/input/text-area/text-area.component.html index cb933c15e..387345a92 100644 --- a/src/app/shared/input/text-area/text-area.component.html +++ b/src/app/shared/input/text-area/text-area.component.html @@ -4,21 +4,21 @@ matTooltip="{{ tooltip }}" matTooltipPosition="below" matTooltipClass="tooltip-custom-style" - container="body" + matTooltipShowDelay="500" aria-hidden="true" *ngIf="tooltip" > info + diff --git a/src/app/shared/input/text-area/text-area.component.ts b/src/app/shared/input/text-area/text-area.component.ts index 59e5b2658..e5b3b6975 100644 --- a/src/app/shared/input/text-area/text-area.component.ts +++ b/src/app/shared/input/text-area/text-area.component.ts @@ -1,7 +1,8 @@ -import { AbstractInputComponent } from '../abstract-input'; import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AbstractInputComponent } from '@src/app/shared/input/abstract-input'; + /** * Custom Input Text Component. * @@ -17,16 +18,16 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'; * ``` */ @Component({ - selector: 'input-text-area', - templateUrl: './text-area.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => InputTextAreaComponent), - multi: true - } - ], - standalone: false + selector: 'input-text-area', + templateUrl: './text-area.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputTextAreaComponent), + multi: true + } + ], + standalone: false }) export class InputTextAreaComponent extends AbstractInputComponent { constructor() { diff --git a/src/app/tasks/edit-supertasks/edit-supertasks.component.html b/src/app/tasks/edit-supertasks/edit-supertasks.component.html index 10bec855c..77548b727 100644 --- a/src/app/tasks/edit-supertasks/edit-supertasks.component.html +++ b/src/app/tasks/edit-supertasks/edit-supertasks.component.html @@ -15,8 +15,8 @@
Add Pretasks
-
diff --git a/src/app/tasks/new-tasks/new-tasks.component.html b/src/app/tasks/new-tasks/new-tasks.component.html index ecf525ab5..2e85e58ea 100644 --- a/src/app/tasks/new-tasks/new-tasks.component.html +++ b/src/app/tasks/new-tasks/new-tasks.component.html @@ -13,11 +13,12 @@ [isLoading]="isLoading" [placeholder]="'Hashlist...'" [items]="selectHashlists" - [label]="'Select or search Hashlist *'" + [label]="'Select or search Hashlist'" [mergeIdAndName]="true" [multiselectEnabled]="false" formControlName="hashlistId" [initialHashlistId]="isCopyHashlistId" + [isRequired]="true" >
@@ -29,7 +30,7 @@
- + @@ -39,7 +40,7 @@
- +
diff --git a/src/app/tasks/supertasks/applyhashlist.component.html b/src/app/tasks/supertasks/applyhashlist.component.html index 8d1f4dac2..ce55a166c 100644 --- a/src/app/tasks/supertasks/applyhashlist.component.html +++ b/src/app/tasks/supertasks/applyhashlist.component.html @@ -8,7 +8,7 @@ [isLoading]="isLoading" [placeholder]="'Hashlists...'" [items]="selectHashlists" - [label]="'Select or search Hashlists *'" + [label]="'Select or search Hashlists'" [mergeIdAndName]="true" [multiselectEnabled]="false" formControlName="hashlistId" diff --git a/src/app/users/edit-groups/edit-groups.component.html b/src/app/users/edit-groups/edit-groups.component.html index 7297754f3..28f01f1ed 100644 --- a/src/app/users/edit-groups/edit-groups.component.html +++ b/src/app/users/edit-groups/edit-groups.component.html @@ -17,8 +17,10 @@
Add Users
+ [label]="'Select or search Users'" [isRequired]="true" + [mergeIdAndName]="true" formControlName="userIds" style="flex: 1;"> + +
@@ -34,8 +36,10 @@
Add Agents
+ [label]="'Select or search Agents'" [mergeIdAndName]="true" formControlName="agentIds" + [isRequired]="true" style="flex: 1;"> + +