Implemented return search feature with main and result components.

-  **Feature**: Added return search main and result components
- 🎨 **Style**: Updated styles for return search components
- 🛠️ **Refactor**: Modified routing for return search functionality
- 📚 **Docs**: Updated documentation references in settings
This commit is contained in:
Lorenz Hilpert
2025-04-03 17:29:42 +02:00
parent 8a94da6868
commit da27745ebe
24 changed files with 536 additions and 45 deletions

View File

@@ -37,7 +37,7 @@
"file": ".github/testing-instructions.md"
},
{
"file": "docs/text-stack.md"
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
@@ -60,7 +60,7 @@
"file": ".github/testing-instructions.md"
},
{
"file": "docs/text-stack.md"
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
@@ -77,7 +77,7 @@
"file": ".github/review-instructions.md"
},
{
"file": "docs/text-stack.md"
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"

View File

@@ -163,7 +163,7 @@ const routes: Routes = [
children: [
{
path: 'return',
loadChildren: () => import('@feature/return/pages').then((m) => m.routes),
loadChildren: () => import('@isa/oms/feature/return-search').then((m) => m.routes),
},
],
},

View File

@@ -1,7 +1,3 @@
:host {
@apply flex flex-col pt-12 items-center;
}
.lib-return-main-page__loading-spinner {
@apply h-12 w-full flex items-center justify-center mb-12;
}

View File

@@ -1 +1 @@
export * from './lib/oms-feature-return-search/oms-feature-return-search.component';
export * from './lib/routes';

View File

@@ -0,0 +1,27 @@
<div class="flex flex-col items-center justify-center gap-4">
<h1 class="isa-text-subtitle-1-regular">Rückgabe starten</h1>
<p class="isa-text-body-1-regular text-center">
Scannen Sie den QR-Code auf der Rechnung oder suchen Sie den Beleg
<br />
via Rechnungsnummer, E-Mail-Adresse oder Kundennamen
</p>
</div>
<filter-search-bar-input
class="mt-[1.88rem] mb-[3.12rem]"
inputKey="qs"
(triggerSearch)="onSearch()"
></filter-search-bar-input>
@if (entityPending()) {
<div class="h-12 w-full flex items-center justify-center mb-12">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
<div class="flex flex-row gap-4">
@for (filterInput of filterInputs(); track filterInput.key) {
<filter-input-menu-button [filterInput]="filterInput" (applied)="onSearch()">
</filter-input-menu-button>
}
</div>

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-col pt-12 items-center;
}

View File

@@ -1,9 +1,62 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
import {
FilterService,
SearchBarInputComponent,
FilterInputMenuButtonComponent,
} from '@isa/shared/filter';
import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'oms-feature-return-search-main',
templateUrl: './return-search-main.component.html',
styleUrls: ['./return-search-main.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SearchBarInputComponent, IconButtonComponent, FilterInputMenuButtonComponent],
})
export class ReturnSearchMainComponent {}
export class ReturnSearchMainComponent {
#route = inject(ActivatedRoute);
#router = inject(Router);
private _processId = injectActivatedProcessId();
private _filterService = inject(FilterService);
private _returnSearchStore = inject(ReturnSearchStore);
private _entity = computed(() => {
const processId = this._processId();
if (processId) {
return this._returnSearchStore.getEntity(processId);
}
return undefined;
});
entityPending = computed(() => {
return this._entity()?.status === ReturnSearchStatus.Pending;
});
filterInputs = computed(() =>
this._filterService.inputs().filter((input) => input.group === 'filter'),
);
// TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status)
async onSearch() {
const processId = this._processId();
if (processId) {
await this._updateQueryParams();
this._returnSearchStore.search({
processId,
params: this._filterService.toParams(),
});
}
}
private async _updateQueryParams() {
await this.#router.navigate([], {
queryParams: this._filterService.toParams(),
relativeTo: this.#route,
replaceUrl: true,
});
}
}

View File

@@ -0,0 +1,37 @@
<ui-client-row>
<ui-client-row-content>
<h3 class="isa-text-subtitle-1-regular">{{ name() }}</h3>
</ui-client-row-content>
<ui-item-row-data>
<ui-item-row-data-row>
<ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold">
{{ receiptDate() | date: 'dd.MM.yy' }}
</span>
</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold"> {{ receiptNumber() }} </span>
</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Vorgangs-ID</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold"> {{ orderNumber() }} </span>
</ui-item-row-data-value>
</ui-item-row-data-row>
</ui-item-row-data>
<ui-item-row-data>
<ui-item-row-data-row>
<ui-item-row-data-label>Email</ui-item-row-data-label>
<ui-item-row-data-value>{{ email() }}</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Anschrift</ui-item-row-data-label>
<ui-item-row-data-value>{{ address() }}</ui-item-row-data-value>
</ui-item-row-data-row>
</ui-item-row-data>
</ui-client-row>

View File

@@ -0,0 +1,45 @@
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { ReceiptListItem } from '@isa/oms/data-access';
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
@Component({
selector: 'oms-feature-return-search-result-item',
templateUrl: './return-search-result-item.component.html',
styleUrls: ['./return-search-result-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ClientRowImports, ItemRowDataImports, DatePipe],
})
export class ReturnSearchResultItemComponent {
item = input.required<ReceiptListItem>();
name = computed(() => {
const firstName = this.item()?.billing?.person?.firstName;
const lastName = this.item()?.billing?.person?.lastName;
const buyerName = [lastName, firstName].filter((f) => !!f);
const organisation = [this.item()?.billing?.organisation?.name].filter((f) => !!f);
return [organisation.join(), buyerName.join(' ')].filter((f) => !!f).join(' - ');
});
receiptDate = computed(() => {
return this.item()?.printedDate;
});
receiptNumber = computed(() => {
return this.item()?.receiptNumber;
});
orderNumber = computed(() => {
return this.item()?.orderNumber;
});
email = computed(() => {
return this.item()?.billing?.communicationDetails?.email;
});
address = computed(() => {
const address = this.item()?.billing?.address;
return address ? [address.zipCode, address.city].join(' ') : '';
});
}

View File

@@ -0,0 +1,64 @@
<div class="w-full flex flex-row justify-between items-start">
<filter-search-bar-input
class="flex flex-row gap-4 h-12"
[appearance]="'results'"
inputKey="qs"
(triggerSearch)="onSearch()"
></filter-search-bar-input>
<div class="flex flex-row gap-4 items-center">
<filter-filter-menu-button
(applied)="onSearch()"
[rollbackOnClose]="true"
></filter-filter-menu-button>
@if (isMobileDevice()) {
<ui-icon-button (click)="toggleOrderByToolbar.set(!toggleOrderByToolbar())">
<ng-icon name="isaActionSort"></ng-icon>
</ui-icon-button>
} @else {
<lib-return-order-by-list></lib-return-order-by-list>
}
</div>
</div>
@if (isMobileDevice() && toggleOrderByToolbar()) {
<lib-return-order-by-list class="w-full"></lib-return-order-by-list>
}
<span class="text-isa-neutral-900 isa-text-body-2-regular self-start">
{{ entityHits() }} Einträge
</span>
@let items = entityItems();
@if (items.length > 0) {
<div class="flex flex-col gap-4 w-full items-center justify-center">
@for (item of items; track item.id) {
@defer (on viewport) {
<a [routerLink]="['../', 'receipt', item.id]" class="w-full">
<oms-feature-return-search-result-item
#listElement
[item]="item"
></oms-feature-return-search-result-item>
</a>
} @placeholder {
<!-- TODO: Den Spinner durch Skeleton Loader Kacheln ersetzen -->
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
}
@if (entityStatus() === ReturnSearchStatus.Pending) {
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
</div>
} @else if (items.length === 0 && entityStatus() === ReturnSearchStatus.Pending) {
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
} @else if (entityStatus() !== ReturnSearchStatus.Idle) {
<ui-empty-state [title]="emptyState().title" [description]="emptyState().description">
</ui-empty-state>
}

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-col gap-4 w-full justify-start items-center;
}

View File

@@ -1,9 +1,176 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
QueryList,
signal,
untracked,
viewChildren,
} from '@angular/core';
import { ReturnOrderByListComponent } from '@feature/return/containers';
import { injectActivatedProcessId } from '@isa/core/process';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import {
FilterMenuButtonComponent,
FilterService,
SearchBarInputComponent,
} from '@isa/shared/filter';
import { IconButtonComponent } from '@isa/ui/buttons';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { restoreScrollPosition } from '@isa/core/scroll-position';
import { Platform } from '@angular/cdk/platform';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSort } from '@isa/icons';
import { ReturnSearchEntity, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
type EmptyState = {
title: string;
description: string;
};
@Component({
selector: 'oms-feature-return-search-result',
templateUrl: './return-search-result.component.html',
styleUrls: ['./return-search-result.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
RouterLink,
ReturnSearchResultItemComponent,
ReturnOrderByListComponent,
IconButtonComponent,
SearchBarInputComponent,
EmptyStateComponent,
NgIconComponent,
FilterMenuButtonComponent,
],
providers: [provideIcons({ isaActionSort })],
})
export class ReturnSearchResultComponent {}
export class ReturnSearchResultComponent {
#route = inject(ActivatedRoute);
#router = inject(Router);
#platform = inject(Platform);
private _processId = injectActivatedProcessId();
private _returnSearchStore = inject(ReturnSearchStore);
private _filterService = inject(FilterService);
ReturnSearchStatus = ReturnSearchStatus;
filterInputs = computed(() =>
this._filterService.inputs().filter((input) => input.group === 'filter'),
);
private _entity = computed(() => {
const processId = this._processId();
if (processId) {
return this._returnSearchStore.getEntity(processId);
}
return undefined;
});
entityItems = computed(() => {
return this._entity()?.items ?? [];
});
entityHits = computed(() => {
return this._entity()?.hits ?? 0;
});
entityStatus = computed(() => {
return this._entity()?.status ?? ReturnSearchStatus.Idle;
});
emptyState = computed<EmptyState>(() => {
return {
title: 'Keine Suchergebnisse',
description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
};
});
listElements = viewChildren<QueryList<ReturnSearchResultItemComponent>>('listElement');
isMobileDevice = signal(this.#platform.ANDROID || this.#platform.IOS);
toggleOrderByToolbar = signal(false);
searchEffectFn = () =>
effect(() => {
const processId = this._processId();
const listLength = this.listElements().length;
untracked(async () => {
if (processId) {
const entity = this._entity();
if (entity) {
const isPending = this.entityStatus() === ReturnSearchStatus.Pending;
// Trigger reload search if no search request is already pending and
// 1. List scrolled to bottom
// 2. After Process change AND no items in entity
if (!isPending) {
this._reload({ processId, entity, listLength });
}
} else {
// Init Search after F5 / Refresh Page / No Entity Available
await this._initSearch();
}
}
});
});
constructor() {
this.searchEffectFn();
restoreScrollPosition();
}
// TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status)
async onSearch() {
const processId = this._processId();
if (processId) {
await this._updateQueryParams();
this._returnSearchStore.search({
processId,
params: this._filterService.toParams(),
});
}
}
private async _updateQueryParams() {
return await this.#router.navigate([], {
queryParams: this._filterService.toParams(),
relativeTo: this.#route,
replaceUrl: true,
});
}
private async _reload({
processId,
entity,
listLength,
}: {
processId: number;
entity: ReturnSearchEntity;
listLength: number;
}) {
const entityItemsLength = entity?.items?.length ?? 0;
const hits = entity?.hits ?? 0;
// Soll reloaden wenn man am Ende der Liste in der View angekommen ist und noch nicht alle Items insgesamt geladen wurden
if (listLength === entityItemsLength && hits !== entityItemsLength) {
await this._updateQueryParams();
this._returnSearchStore.reload({
processId,
params: this._filterService.toParams(),
});
}
}
private async _initSearch() {
const entities = this._returnSearchStore.entities();
// For routing away from the list this entities?.length === 0 check is necessary, otherwise the init search would trigger again
if (entities?.length === 0) {
await this.onSearch();
}
}
}

View File

@@ -3,13 +3,14 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnSearchStore } from '@isa/oms/data-access';
import { FilterService } from '@isa/shared/filter';
import { FilterService, provideQuerySettings } from '@isa/shared/filter';
@Component({
selector: 'oms-feature-return-search',
template: `<router-outlet></router-outlet>`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet],
providers: [provideQuerySettings(() => inject(ActivatedRoute).snapshot.data['querySettings'])],
host: {
'[class]': '"flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden"',
},
@@ -54,11 +55,11 @@ export class ReturnSearchComponent {
if (items) {
if (items?.length === 1) {
return await this._navigateTo(['receipt', items[0].id.toString()]);
return await this._navigateTo(['receipts', items[0].id.toString()]);
}
if (items?.length >= 0) {
return await this._navigateTo(['results']);
return await this._navigateTo(['receipts']);
}
}

View File

@@ -1,19 +1,21 @@
import { ActivatedRoute, Routes } from '@angular/router';
import { Routes } from '@angular/router';
import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
import { ReturnSearchComponent } from './return-search.component';
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
import { provideQuerySettings } from '@isa/shared/filter';
import { inject } from '@angular/core';
import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
export const routes: Routes = [
{
path: '',
component: ReturnSearchComponent,
resolve: { querySettings: querySettingsResolverFn },
providers: [provideQuerySettings(() => inject(ActivatedRoute).snapshot.data['querySettings'])],
children: [
{ path: '', component: ReturnSearchMainComponent },
{ path: 'returns', component: ReturnSearchMainComponent },
{ path: 'receipts', component: ReturnSearchResultComponent },
],
},
{
path: 'process',
loadChildren: () => import('@isa/oms/feature/return-process').then((feat) => feat.routes),
},
];

View File

@@ -51,7 +51,9 @@ export class FilterActionsComponent {
const inputKey = this.inputKey();
if (!inputKey) {
this.filterService.reset();
this.filterInputs().forEach((input) => {
this.filterService.resetInput(input.key);
});
} else {
this.filterService.resetInput(inputKey);
}

View File

@@ -1,7 +1,9 @@
import { inject, Injectable, InjectionToken, Provider } from '@angular/core';
import { computed, inject, Injectable, InjectionToken, Provider } from '@angular/core';
import { InputType, QuerySettingsDTO } from '../types';
import { getState, patchState, signalState } from '@ngrx/signals';
import { mapToFilter } from './mappings';
import { isEqual } from 'lodash-es';
import { FilterInput } from './schemas';
export const QUERY_SETTINGS = new InjectionToken<QuerySettingsDTO>('QuerySettings');
@@ -13,7 +15,9 @@ export function provideQuerySettings(factory: () => QuerySettingsDTO): Provider[
export class FilterService {
readonly settings = inject(QUERY_SETTINGS);
#commitdState = mapToFilter(this.settings);
private readonly defaultState = mapToFilter(this.settings);
#commitdState = structuredClone(this.defaultState);
#state = signalState(this.#commitdState);
@@ -65,6 +69,58 @@ export class FilterService {
}
}
/**
* Indicates whether the current state is the default state.
* This computed property checks if the current state is equal to the default state.
*/
isDefaultFilter = computed(() => {
const currentState = getState(this.#state);
return isEqual(currentState, this.defaultState);
});
isDefaultFilterInput(filterInput: FilterInput) {
const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
const defaultInputState = this.defaultState.inputs.find((i) => i.key === filterInput.key);
return isEqual(currentInputState, defaultInputState);
}
/**
* Indicates whether the current state is empty.
*/
isEmptyFilter = computed(() => {
const currentState = getState(this.#state);
return currentState.inputs.every((input) => {
if (input.type === InputType.Text) {
return !input.value;
}
if (input.type === InputType.Checkbox) {
return !input.selected?.length;
}
console.warn(`Input type not supported: ${input.type}`);
return true;
});
});
isEmptyFilterInput(filterInput: FilterInput) {
const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
if (currentInputState?.type === InputType.Text) {
return !currentInputState.value;
}
if (currentInputState?.type === InputType.Checkbox) {
return !currentInputState.selected?.length;
}
console.warn(`Input type not supported: ${currentInputState?.type}`);
return true;
}
/**
* Reverts the current state to the last committed state.
* This method restores the state by applying the previously saved committed state.

View File

@@ -2,9 +2,7 @@
@let options = input().options;
@if (inp && options) {
<div [formGroup]="checkboxes" class="flex flex-col items-center justify-start gap-5">
<label
class="w-full flex items-center gap-4 cursor-pointer isa-text-body-2-regular has-[:checked]:isa-text-body-2-bold"
>
<label class="w-full flex items-center gap-4 cursor-pointer isa-text-body-2-bold">
<ui-checkbox>
<input (click)="toggleSelection()" [checked]="allChecked" type="checkbox" />
</ui-checkbox>

View File

@@ -1,6 +1,13 @@
<ui-icon-button cdkOverlayOrigin #trigger="cdkOverlayOrigin" (click)="toggle()">
<button
uiIconButton
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
(click)="toggle()"
type="button"
[class.active]="isIconButtonActive()"
>
<ng-icon name="isaActionFilter"></ng-icon>
</ui-icon-button>
</button>
<ng-template
cdkConnectedOverlay

View File

@@ -1,5 +1,13 @@
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, inject, input, model, output } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
model,
output,
} from '@angular/core';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { FilterMenuComponent } from './filter-menu.component';
@@ -15,7 +23,6 @@ import { FilterService } from '../../core';
templateUrl: './filter-menu-button.component.html',
styleUrls: ['./filter-menu-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [IconButtonComponent, OverlayModule, NgIconComponent, FilterMenuComponent],
providers: [provideIcons({ isaActionFilter })],
})
@@ -24,6 +31,8 @@ export class FilterMenuButtonComponent {
#filter = inject(FilterService);
isIconButtonActive = computed(() => !this.#filter.isDefaultFilter());
/**
* Tracks the open state of the filter menu.
*/

View File

@@ -1,10 +1,12 @@
@if (!activeInput()) {
<button
class="rounded-t-[1.25rem] px-6 py-5 border-b border-isa-neutral-300"
class="rounded-t-[1.25rem] px-6 py-5 border-b border-isa-neutral-300 text-left text-isa-neutral-500 has-[.active]:text-isa-neutral-900"
(click)="filter.clear()"
type="button"
>
<span class="isa-text-body-2-bold text-isa-neutral-500"> Alle abwählen </span>
<span class="isa-text-body-2-bold" [class.active]="!filter.isEmptyFilter()">
Alle abwählen
</span>
</button>
@for (filterInput of filterInputs(); track filterInput.key) {
<button
@@ -12,20 +14,29 @@
class="px-6 py-5 border-b border-isa-neutral-300 inline-flex items-center gap-2 justify-between"
(click)="activeInput.set(filterInput)"
>
<span class="text-isa-neutral-900 isa-text-body-2-regular">
{{ filterInput.label }}
</span>
<div class="flex items-start gap-1">
<span class="text-isa-neutral-900 isa-text-body-2-regular">
{{ filterInput.label }}
</span>
@if (!filter.isDefaultFilterInput(filterInput)) {
<span
class="bg-isa-accent-red size-[0.375rem] rounded-full inline-block mt-[0.125rem]"
></span>
}
</div>
<ng-icon class="text-isa-neutral-900" name="isaActionChevronRight" size="1.5rem"></ng-icon>
</button>
}
} @else {
@let input = activeInput();
<button
class="rounded-t-[1.25rem] px-6 py-5 border-b border-isa-neutral-300"
(click)="filter.rollbackInput(input!.key); activeInput.set(undefined)"
class="flex justify-start items-center gap-2 rounded-t-[1.25rem] px-6 py-5 border-b border-isa-neutral-300 text-left text-isa-neutral-900 isa-text-body-2-regular"
(click)="activeInput.set(undefined)"
type="button"
>
<span class="isa-text-body-2-bold text-isa-neutral-500"> {{ input!.label }} </span>
<ng-icon name="isaActionChevronLeft" size="1rem"></ng-icon>
<span> {{ input!.label }} </span>
</button>
<filter-input-renderer class="overflow-scroll" [filterInput]="input!"></filter-input-renderer>

View File

@@ -22,7 +22,6 @@
[cdkConnectedOverlayOpen]="open()"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
[cdkConnectedOverlayScrollStrategy]="scrollStrategy"
[cdkConnectedOverlayOffsetX]="-10"
[cdkConnectedOverlayOffsetY]="18"
(backdropClick)="toggle()"

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, inject, input, model, output } from '@angular/core';
import { FilterInput } from '../../core';
import { FilterInput, FilterService } from '../../core';
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionChevronDown, isaActionChevronUp } from '@isa/icons';
@@ -20,6 +20,8 @@ import { FilterInputMenuComponent } from './input-menu.component';
export class FilterInputMenuButtonComponent {
scrollStrategy = inject(Overlay).scrollStrategies.block();
#filter = inject(FilterService);
/**
* Tracks the open state of the input menu.
*/
@@ -50,6 +52,11 @@ export class FilterInputMenuButtonComponent {
*/
applied = output<void>();
/**
* Determines whether to commit changes when the menu is closed.
*/
commitOnClose = input<boolean>(false);
/**
* Subscribes to the `applied` event to automatically close the menu.
*/
@@ -69,6 +76,10 @@ export class FilterInputMenuButtonComponent {
if (open) {
this.closed.emit();
if (this.commitOnClose()) {
this.#filter.commit();
}
} else {
this.opened.emit();
}

View File

@@ -57,10 +57,10 @@
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.8",
"@angular-devkit/core": "^19.1.8",
"@angular-devkit/schematics": "^19.1.8",
"@angular/cli": "^19.1.8",
"@angular-devkit/build-angular": "19.2.6",
"@angular-devkit/core": "19.2.6",
"@angular-devkit/schematics": "19.2.6",
"@angular/cli": "19.2.6",
"@angular/compiler-cli": "19.1.7",
"@angular/language-service": "19.1.7",
"@angular/pwa": "^19.2.0",