mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1894: feat(filter): enhance search trigger handling and event emissions
feat(filter): enhance search trigger handling and event emissions Refactor search components to emit specific search trigger types, improving tracking of user interactions. Update relevant components to handle 'input', 'filter', 'scan', and 'order-by' triggers, ensuring consistent behavior across the filter system. Refs: #5234
This commit is contained in:
committed by
Nino Righi
parent
72bcacefb6
commit
6d26f7f6c0
@@ -8,9 +8,7 @@
|
||||
|
||||
<remi-feature-remission-list-select></remi-feature-remission-list-select>
|
||||
|
||||
<filter-controls-panel
|
||||
(triggerSearch)="search(); searchTrigger.set('user')"
|
||||
></filter-controls-panel>
|
||||
<filter-controls-panel (triggerSearch)="search($event)"></filter-controls-panel>
|
||||
|
||||
<span
|
||||
class="text-isa-neutral-900 isa-text-body-2-regular self-start"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
withQueryParamsSync,
|
||||
FilterControlsPanelComponent,
|
||||
FilterService,
|
||||
SearchTrigger,
|
||||
} from '@isa/shared/filter';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
import { RemissionStartCardComponent } from './remission-start-card/remission-start-card.component';
|
||||
@@ -92,7 +93,7 @@ function querySettingsFactory() {
|
||||
},
|
||||
})
|
||||
export class RemissionListComponent {
|
||||
searchTrigger = signal<'user' | 'reload' | 'initial'>('initial');
|
||||
searchTrigger = signal<SearchTrigger | 'reload' | 'initial'>('initial');
|
||||
|
||||
/**
|
||||
* Activated route instance for accessing route data and params.
|
||||
@@ -145,6 +146,7 @@ export class RemissionListComponent {
|
||||
return {
|
||||
remissionListType: this.selectedRemissionListType(),
|
||||
queryToken: this.#filterService.query(),
|
||||
searchTrigger: this.searchTrigger(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -253,9 +255,13 @@ export class RemissionListComponent {
|
||||
|
||||
/**
|
||||
* Commits the current filter state and triggers a new search.
|
||||
*
|
||||
* @param trigger - The type of search trigger that initiated this search.
|
||||
* Used to track user interaction patterns and optimize search behavior.
|
||||
*/
|
||||
search(): void {
|
||||
search(trigger: SearchTrigger): void {
|
||||
this.#filterService.commit();
|
||||
this.searchTrigger.set(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,6 +302,23 @@ export class RemissionListComponent {
|
||||
return productGroup ? productGroup.value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed signal that determines if the current search was triggered by user interaction.
|
||||
* Returns true for user-initiated actions (input, filter changes, sort changes, scanning)
|
||||
* and false for automatic/system-initiated searches (reload, initial load).
|
||||
*
|
||||
* @returns True if search was user-initiated, false for system-initiated searches
|
||||
*/
|
||||
searchTriggeredByUser = computed(() => {
|
||||
const trigger = this.searchTrigger();
|
||||
return (
|
||||
trigger === 'input' ||
|
||||
trigger === 'filter' ||
|
||||
trigger === 'order-by' ||
|
||||
trigger === 'scan'
|
||||
);
|
||||
});
|
||||
|
||||
emptySearchResultEffect = effect(() => {
|
||||
const status = this.remissionResource.status();
|
||||
|
||||
@@ -310,7 +333,7 @@ export class RemissionListComponent {
|
||||
}
|
||||
|
||||
untracked(() => {
|
||||
if (this.searchTrigger() !== 'user') {
|
||||
if (!this.searchTriggeredByUser()) {
|
||||
return;
|
||||
}
|
||||
const isDepartment =
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
} from '@isa/remission/data-access';
|
||||
import { SearchTrigger } from '@isa/shared/filter';
|
||||
import { parseISO, compareDesc } from 'date-fns';
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ import { parseISO, compareDesc } from 'date-fns';
|
||||
* @param {Function} params - Function that returns parameters for the resource
|
||||
* @param {RemissionListType} params.remissionListType - Type of remission list to fetch
|
||||
* @param {QueryTokenInput} params.queryToken - Query parameters for filtering and sorting
|
||||
* @param {SearchTrigger | 'reload' | 'initial'} params.searchTrigger - How the search was triggered, affects filter behavior
|
||||
* @returns {Resource} Angular resource that manages the remission list data
|
||||
* @throws {Error} When no current stock is available
|
||||
* @throws {Error} When no supplier is available
|
||||
@@ -31,13 +33,20 @@ import { parseISO, compareDesc } from 'date-fns';
|
||||
* filter: { status: 'open' },
|
||||
* orderBy: 'date',
|
||||
* // ... other query parameters
|
||||
* }
|
||||
* },
|
||||
* searchTrigger: 'input'
|
||||
* }));
|
||||
*
|
||||
* @remarks
|
||||
* The searchTrigger parameter influences query behavior:
|
||||
* - 'scan': Clears existing filters to show scan-specific results
|
||||
* - Other triggers: Preserves existing filter state
|
||||
*/
|
||||
export const createRemissionListResource = (
|
||||
params: () => {
|
||||
remissionListType: RemissionListType;
|
||||
queryToken: QueryTokenInput;
|
||||
searchTrigger: SearchTrigger | 'reload' | 'initial';
|
||||
},
|
||||
) => {
|
||||
const remissionSearchService = inject(RemissionSearchService);
|
||||
@@ -46,7 +55,6 @@ export const createRemissionListResource = (
|
||||
return resource({
|
||||
params,
|
||||
loader: async ({ abortSignal, params }) => {
|
||||
console.log(params.queryToken);
|
||||
const assignedStock = await remissionStockService.fetchAssignedStock();
|
||||
|
||||
if (!assignedStock || !assignedStock.id) {
|
||||
@@ -67,12 +75,18 @@ export const createRemissionListResource = (
|
||||
| ListResponseArgs<ReturnSuggestion>
|
||||
| undefined;
|
||||
|
||||
const queryToken = { ...params.queryToken };
|
||||
|
||||
if (params.searchTrigger === 'scan') {
|
||||
queryToken.filter = {};
|
||||
}
|
||||
|
||||
if (params.remissionListType === RemissionListType.Pflicht) {
|
||||
res = await remissionSearchService.fetchList(
|
||||
{
|
||||
assignedStockId: assignedStock.id,
|
||||
supplierId: firstSupplier.id,
|
||||
...params.queryToken,
|
||||
...queryToken,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
class="flex flex-row gap-4 h-12"
|
||||
[appearance]="'results'"
|
||||
inputKey="qs"
|
||||
(triggerSearch)="triggerSearch.emit()"
|
||||
(triggerSearch)="triggerSearch.emit($event)"
|
||||
data-what="search-input"
|
||||
></filter-search-bar-input>
|
||||
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<filter-filter-menu-button
|
||||
(applied)="triggerSearch.emit()"
|
||||
(applied)="triggerSearch.emit('filter')"
|
||||
[rollbackOnClose]="true"
|
||||
></filter-filter-menu-button>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
></ui-icon-button>
|
||||
} @else {
|
||||
<filter-order-by-toolbar
|
||||
(toggled)="triggerSearch.emit()"
|
||||
(toggled)="triggerSearch.emit('order-by')"
|
||||
></filter-order-by-toolbar>
|
||||
}
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
@if (mobileBreakpoint() && showOrderByToolbarMobile()) {
|
||||
<filter-order-by-toolbar
|
||||
class="w-full"
|
||||
(toggled)="triggerSearch.emit()"
|
||||
(toggled)="triggerSearch.emit('order-by')"
|
||||
data-what="sort-toolbar-mobile"
|
||||
></filter-order-by-toolbar>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,28 @@ import { isaActionFilter, isaActionSort } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { OrderByToolbarComponent } from '../order-by';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { SearchTrigger } from '../types';
|
||||
|
||||
/**
|
||||
* Filter controls panel component that provides a unified interface for search and filtering operations.
|
||||
*
|
||||
* This component combines search input, filter menu, and sorting controls into a responsive panel.
|
||||
* It adapts its layout based on screen size, showing/hiding controls appropriately for mobile and desktop views.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <filter-controls-panel
|
||||
* (triggerSearch)="handleSearch($event)">
|
||||
* </filter-controls-panel>
|
||||
* ```
|
||||
*
|
||||
* Features:
|
||||
* - Responsive design that adapts to mobile/desktop layouts
|
||||
* - Integrated search bar with scanner support
|
||||
* - Filter menu with rollback functionality
|
||||
* - Sortable order-by controls
|
||||
* - Emits typed search trigger events
|
||||
*/
|
||||
@Component({
|
||||
selector: 'filter-controls-panel',
|
||||
templateUrl: './controls-panel.component.html',
|
||||
@@ -32,14 +53,26 @@ import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
providers: [provideIcons({ isaActionSort, isaActionFilter })],
|
||||
})
|
||||
export class FilterControlsPanelComponent {
|
||||
triggerSearch = output<void>();
|
||||
/**
|
||||
* Output event that emits when any search action is triggered.
|
||||
* Provides the specific SearchTrigger type to indicate how the search was initiated:
|
||||
* - 'input': Text input or search button
|
||||
* - 'filter': Filter menu changes
|
||||
* - 'order-by': Sort order changes
|
||||
* - 'scan': Barcode scan
|
||||
*/
|
||||
triggerSearch = output<SearchTrigger>();
|
||||
|
||||
/** Signal tracking whether the viewport is at tablet size or above */
|
||||
/**
|
||||
* Signal tracking whether the viewport is at tablet size or above.
|
||||
* Used to determine responsive layout behavior for mobile vs desktop.
|
||||
*/
|
||||
mobileBreakpoint = breakpoint([Breakpoint.Tablet]);
|
||||
|
||||
/**
|
||||
* Signal controlling the visibility of the order-by toolbar on mobile
|
||||
* Initially shows toolbar when NOT on mobile
|
||||
* Signal controlling the visibility of the order-by toolbar on mobile devices.
|
||||
* Initially shows toolbar when NOT on mobile, can be toggled by user on mobile.
|
||||
* Linked to mobileBreakpoint to automatically adjust when screen size changes.
|
||||
*/
|
||||
showOrderByToolbarMobile = linkedSignal(() => !this.mobileBreakpoint());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
type="text"
|
||||
[formControl]="control"
|
||||
[placeholder]="inp.placeholder"
|
||||
(keydown.enter)="triggerSearch.emit()"
|
||||
(keydown.enter)="triggerSearch.emit('input')"
|
||||
/>
|
||||
|
||||
<ui-search-bar-clear></ui-search-bar-clear>
|
||||
@@ -20,7 +20,7 @@
|
||||
type="submit"
|
||||
[color]="buttonColor()"
|
||||
[disabled]="control.invalid"
|
||||
(click)="triggerSearch.emit()"
|
||||
(click)="triggerSearch.emit('input')"
|
||||
name="isaActionSearch"
|
||||
></ui-icon-button>
|
||||
} @else if (appearance() === 'results') {
|
||||
@@ -30,7 +30,7 @@
|
||||
prefix
|
||||
[color]="'neutral'"
|
||||
[disabled]="control.invalid"
|
||||
(click)="triggerSearch.emit()"
|
||||
(click)="triggerSearch.emit('input')"
|
||||
name="isaActionSearch"
|
||||
></ui-icon-button>
|
||||
}
|
||||
|
||||
@@ -18,10 +18,26 @@ import { IconButtonColor, IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionSearch, isaActionScanner } from '@isa/icons';
|
||||
import { FilterService, TextFilterInput } from '../../core';
|
||||
import { InputType } from '../../types';
|
||||
import { InputType, SearchTrigger } from '../../types';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { ScannerButtonComponent } from '@isa/shared/scanner';
|
||||
|
||||
/**
|
||||
* A search bar input component with integrated scanner functionality for filtering systems.
|
||||
*
|
||||
* This component provides a unified search experience with text input and barcode scanning capabilities.
|
||||
* It integrates with the FilterService to manage search state and emit search trigger events.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <filter-search-bar-input
|
||||
* inputKey="qs"
|
||||
* appearance="main"
|
||||
* buttonColor="brand"
|
||||
* (triggerSearch)="handleSearch($event)">
|
||||
* </filter-search-bar-input>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'filter-search-bar-input',
|
||||
templateUrl: './search-bar-input.component.html',
|
||||
@@ -42,19 +58,60 @@ import { ScannerButtonComponent } from '@isa/shared/scanner';
|
||||
providers: [provideIcons({ isaActionSearch, isaActionScanner })],
|
||||
})
|
||||
export class SearchBarInputComponent {
|
||||
/**
|
||||
* FilterService instance for managing search filter state.
|
||||
* @readonly
|
||||
*/
|
||||
readonly filterService = inject(FilterService);
|
||||
|
||||
/**
|
||||
* Form control for managing the search input value.
|
||||
*/
|
||||
control = new FormControl();
|
||||
|
||||
/**
|
||||
* Signal that tracks changes to the form control's value.
|
||||
*/
|
||||
/**
|
||||
* Signal that tracks changes to the form control's value.
|
||||
*/
|
||||
valueChanges = toSignal(this.control.valueChanges);
|
||||
|
||||
/**
|
||||
* The unique key identifier for this input in the filter system.
|
||||
* @required
|
||||
*/
|
||||
inputKey = input.required<string>();
|
||||
|
||||
/**
|
||||
* The color scheme for the search button.
|
||||
* @default 'brand'
|
||||
*/
|
||||
buttonColor = input<IconButtonColor>('brand');
|
||||
|
||||
/**
|
||||
* The visual appearance variant of the search bar.
|
||||
* - 'main': Primary search bar with prominent styling
|
||||
* - 'results': Secondary search bar for filtering results
|
||||
* @default 'main'
|
||||
*/
|
||||
appearance = input<'main' | 'results'>('main');
|
||||
|
||||
/**
|
||||
* Computed CSS class name based on the appearance variant.
|
||||
* @returns CSS class string for styling the component appearance
|
||||
*/
|
||||
appearanceClass = computed(
|
||||
() => `filter-search-bar-input__${this.appearance()}`,
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves and manages the text filter input configuration.
|
||||
* Automatically synchronizes the form control value with the filter service state.
|
||||
*
|
||||
* @returns The TextFilterInput configuration object
|
||||
* @throws {Error} When no input is found for the specified key
|
||||
*/
|
||||
input = computed<TextFilterInput>(() => {
|
||||
const inputs = this.filterService.inputs();
|
||||
const input = inputs.find(
|
||||
@@ -71,8 +128,19 @@ export class SearchBarInputComponent {
|
||||
return input;
|
||||
});
|
||||
|
||||
triggerSearch = output();
|
||||
/**
|
||||
* Output event emitter that signals when a search should be triggered.
|
||||
* Emits different SearchTrigger types based on the action performed:
|
||||
* - 'input': Manual text input or search button click
|
||||
* - 'scan': Barcode scanning result
|
||||
*/
|
||||
triggerSearch = output<SearchTrigger>();
|
||||
|
||||
/**
|
||||
* Constructor sets up reactive effect to sync form control changes with filter service.
|
||||
* The effect tracks value changes and updates the filter service when the local
|
||||
* control value differs from the stored filter value.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.valueChanges();
|
||||
@@ -87,10 +155,16 @@ export class SearchBarInputComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles barcode scan results from the scanner component.
|
||||
* Updates the filter service with the scanned value and triggers a search.
|
||||
*
|
||||
* @param value - The scanned barcode value, or null if scan failed
|
||||
*/
|
||||
onScan(value: string | null): void {
|
||||
if (value) {
|
||||
this.filterService.setInputTextValue(this.inputKey(), value);
|
||||
this.triggerSearch.emit();
|
||||
this.triggerSearch.emit('scan');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,43 @@ export enum InputType {
|
||||
DateRange = 128,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration of search trigger types that indicate how a search was initiated.
|
||||
* Used throughout the filter system to track user interaction patterns and
|
||||
* optimize search behavior based on the trigger source.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Handle different search triggers
|
||||
* handleSearch(trigger: SearchTrigger) {
|
||||
* switch(trigger) {
|
||||
* case SearchTrigger.Input:
|
||||
* // User typed or clicked search
|
||||
* break;
|
||||
* case SearchTrigger.Scan:
|
||||
* // Barcode was scanned
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const SearchTrigger = {
|
||||
/** User manually entered text or clicked the search button */
|
||||
Input: 'input',
|
||||
/** User applied or changed filter settings */
|
||||
Filter: 'filter',
|
||||
/** Barcode scanner provided input */
|
||||
Scan: 'scan',
|
||||
/** User changed the sort order */
|
||||
OrderBy: 'order-by',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Type representing the possible search trigger values.
|
||||
* Derived from the SearchTrigger const object to ensure type safety.
|
||||
*/
|
||||
export type SearchTrigger = (typeof SearchTrigger)[keyof typeof SearchTrigger];
|
||||
|
||||
export interface QuerySettings {
|
||||
/**
|
||||
* Filter
|
||||
|
||||
Reference in New Issue
Block a user