Merged PR 2057: feat(checkout): add branch selection to reward catalog

feat(checkout): add branch selection to reward catalog

- Add new select-branch-dropdown library with BranchDropdownComponent
  and SelectedBranchDropdownComponent for branch selection
- Extend DropdownButtonComponent with filter and option subcomponents
- Integrate branch selection into reward catalog page
- Add BranchesResource for fetching available branches
- Update CheckoutMetadataService with branch selection persistence
- Add comprehensive tests for dropdown components

Related work items: #5464
This commit is contained in:
Lorenz Hilpert
2025-11-27 10:38:52 +00:00
committed by Nino Righi
parent 4589146e31
commit 7950994d66
44 changed files with 3210 additions and 1580 deletions

View File

@@ -1,55 +1,57 @@
<div
class="w-full flex flex-row justify-between items-start"
[class.empty-filter-input]="!hasFilter() && !hasInput()"
>
@if (hasInput()) {
<filter-search-bar-input
class="flex flex-row gap-4 h-12"
[appearance]="'results'"
[inputKey]="inputKey()"
(triggerSearch)="triggerSearch.emit($event)"
data-what="search-input"
></filter-search-bar-input>
}
<div class="flex flex-row gap-4 items-center">
@for (switchFilter of switchFilters(); track switchFilter.filter.key) {
<filter-switch-menu-button
[filterInput]="switchFilter.filter"
[icon]="switchFilter.icon"
(toggled)="triggerSearch.emit('filter')"
></filter-switch-menu-button>
}
@if (hasFilter()) {
<filter-filter-menu-button
(applied)="triggerSearch.emit('filter')"
[rollbackOnClose]="true"
></filter-filter-menu-button>
}
@if (mobileBreakpoint()) {
<ui-icon-button
type="button"
(click)="showOrderByToolbarMobile.set(!showOrderByToolbarMobile())"
[class.active]="showOrderByToolbarMobile()"
data-what="sort-button-mobile"
name="isaActionSort"
></ui-icon-button>
} @else {
<filter-order-by-toolbar
[class.empty-filter-input-width]="!hasFilter() && !hasInput()"
(toggled)="triggerSearch.emit('order-by')"
data-what="sort-toolbar"
></filter-order-by-toolbar>
}
</div>
</div>
@if (mobileBreakpoint() && showOrderByToolbarMobile()) {
<filter-order-by-toolbar
class="w-full"
(toggled)="triggerSearch.emit('order-by')"
data-what="sort-toolbar-mobile"
></filter-order-by-toolbar>
}
<div
class="w-full flex flex-row justify-between items-start"
[class.empty-filter-input]="!hasFilter() && !hasInput()"
>
@if (hasInput()) {
<filter-search-bar-input
class="flex flex-row gap-4 h-12"
[appearance]="'results'"
[inputKey]="inputKey()"
(triggerSearch)="triggerSearch.emit($event)"
data-what="search-input"
></filter-search-bar-input>
}
<div class="flex flex-row gap-4 items-center">
<ng-content></ng-content>
@for (switchFilter of switchFilters(); track switchFilter.filter.key) {
<filter-switch-menu-button
[filterInput]="switchFilter.filter"
[icon]="switchFilter.icon"
(toggled)="triggerSearch.emit('filter')"
></filter-switch-menu-button>
}
@if (hasFilter()) {
<filter-filter-menu-button
(applied)="triggerSearch.emit('filter')"
[rollbackOnClose]="true"
></filter-filter-menu-button>
}
@if (mobileLayout()) {
<ui-icon-button
type="button"
(click)="showOrderByToolbarMobile.set(!showOrderByToolbarMobile())"
[class.active]="showOrderByToolbarMobile()"
data-what="sort-button-mobile"
name="isaActionSort"
></ui-icon-button>
} @else {
<filter-order-by-toolbar
[class.empty-filter-input-width]="!hasFilter() && !hasInput()"
(toggled)="triggerSearch.emit('order-by')"
data-what="sort-toolbar"
></filter-order-by-toolbar>
}
</div>
</div>
@if (mobileLayout() && showOrderByToolbarMobile()) {
<filter-order-by-toolbar
class="w-full"
(toggled)="triggerSearch.emit('order-by')"
data-what="sort-toolbar-mobile"
></filter-order-by-toolbar>
}

View File

@@ -1,135 +1,141 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
linkedSignal,
output,
signal,
ViewEncapsulation,
} from '@angular/core';
import { FilterMenuButtonComponent } from '../menus/filter-menu';
import { provideIcons } from '@ng-icons/core';
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 { InputType, SearchTrigger } from '../types';
import { FilterService, TextFilterInput, FilterInput } from '../core';
import { SearchBarInputComponent } from '../inputs';
import { SwitchMenuButtonComponent } from '../menus/switch-menu';
/**
* 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',
styleUrls: ['./controls-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
SearchBarInputComponent,
FilterMenuButtonComponent,
IconButtonComponent,
OrderByToolbarComponent,
SwitchMenuButtonComponent,
],
host: {
'[class]': "['filter-controls-panel']",
},
providers: [provideIcons({ isaActionSort, isaActionFilter })],
})
export class FilterControlsPanelComponent {
/**
* Service for managing filter state and operations.
*/
#filterService = inject(FilterService);
/**
* The unique key identifier for this input in the filter system.
* @default 'qs'
*/
inputKey = signal('qs');
/**
* Optional array of switch filter configurations to display as toggle switches.
* Each item should be a filter input with an associated icon.
* Switch filters are rendered to the left of the filter menu button.
*
* @example
* ```typescript
* switchFilters = [
* {
* filter: availabilityFilter,
* icon: 'isaActionCheck'
* }
* ]
* ```
*/
switchFilters = input<Array<{ filter: FilterInput; icon: string }>>([]);
/**
* 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.
* Used to determine responsive layout behavior for mobile vs desktop.
*/
mobileBreakpoint = breakpoint([Breakpoint.Tablet, Breakpoint.Desktop]);
/**
* 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());
/**
* Computed signal that determines if the search input is present in the filter inputs.
* This checks if there is a TextFilterInput with the specified inputKey.
* Used to conditionally render the search input in the template.
*/
hasInput = computed(() => {
const inputs = this.#filterService.inputs();
const input = inputs.find(
(input) => input.key === this.inputKey() && input.type === InputType.Text,
) as TextFilterInput;
return !!input;
});
/**
* Computed signal that checks if there are any active filters applied.
* This is determined by checking if there are any inputs of types other than Text.
*/
hasFilter = computed(() => {
const inputs = this.#filterService.inputs();
return inputs.some((input) => input.type !== InputType.Text);
});
}
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
linkedSignal,
output,
signal,
ViewEncapsulation,
} from '@angular/core';
import { FilterMenuButtonComponent } from '../menus/filter-menu';
import { provideIcons } from '@ng-icons/core';
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 { InputType, SearchTrigger } from '../types';
import { FilterService, TextFilterInput, FilterInput } from '../core';
import { SearchBarInputComponent } from '../inputs';
import { SwitchMenuButtonComponent } from '../menus/switch-menu';
/**
* 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',
styleUrls: ['./controls-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
SearchBarInputComponent,
FilterMenuButtonComponent,
IconButtonComponent,
OrderByToolbarComponent,
SwitchMenuButtonComponent,
],
host: {
'[class]': "['filter-controls-panel']",
},
providers: [provideIcons({ isaActionSort, isaActionFilter })],
})
export class FilterControlsPanelComponent {
/**
* Service for managing filter state and operations.
*/
#filterService = inject(FilterService);
/**
* The unique key identifier for this input in the filter system.
* @default 'qs'
*/
inputKey = signal('qs');
/**
* Optional array of switch filter configurations to display as toggle switches.
* Each item should be a filter input with an associated icon.
* Switch filters are rendered to the left of the filter menu button.
*
* @example
* ```typescript
* switchFilters = [
* {
* filter: availabilityFilter,
* icon: 'isaActionCheck'
* }
* ]
* ```
*/
switchFilters = input<Array<{ filter: FilterInput; icon: string }>>([]);
/**
* 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.
* Used to determine responsive layout behavior for mobile vs desktop.
*/
mobileBreakpoint = breakpoint([Breakpoint.Tablet, Breakpoint.Desktop]);
forceMobileLayout = input(false);
mobileLayout = computed(
() => this.forceMobileLayout() || this.mobileBreakpoint(),
);
/**
* 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.mobileLayout());
/**
* Computed signal that determines if the search input is present in the filter inputs.
* This checks if there is a TextFilterInput with the specified inputKey.
* Used to conditionally render the search input in the template.
*/
hasInput = computed(() => {
const inputs = this.#filterService.inputs();
const input = inputs.find(
(input) => input.key === this.inputKey() && input.type === InputType.Text,
) as TextFilterInput;
return !!input;
});
/**
* Computed signal that checks if there are any active filters applied.
* This is determined by checking if there are any inputs of types other than Text.
*/
hasFilter = computed(() => {
const inputs = this.#filterService.inputs();
return inputs.some((input) => input.type !== InputType.Text);
});
}