mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
committed by
Nino Righi
parent
4589146e31
commit
7950994d66
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user