Files
Lorenz Hilpert 7950994d66 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
2025-11-27 10:38:52 +00:00
..

@isa/checkout/feature/select-branch-dropdown

Branch selection dropdown components for the checkout domain, enabling users to select a branch for their checkout session.

Overview

The Select Branch Dropdown feature library provides two UI components for branch selection in the checkout flow:

  • SelectedBranchDropdownComponent - High-level component that integrates with CheckoutMetadataService for automatic persistence
  • BranchDropdownComponent - Low-level component with ControlValueAccessor support for custom form integration

Table of Contents

Features

  • Branch Selection - Dropdown interface for selecting a branch
  • Metadata Integration - Persists selected branch in checkout metadata via CheckoutMetadataService.setSelectedBranch()
  • Standalone Component - Modern Angular standalone architecture
  • Responsive Design - Works across tablet and desktop layouts
  • E2E Testing Support - Includes data-what and data-which attributes

Quick Start

1. Import the Component

import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';

@Component({
  selector: 'app-checkout',
  imports: [SelectedBranchDropdownComponent],
  template: `
    <checkout-selected-branch-dropdown [tabId]="tabId()" />
  `
})
export class CheckoutComponent {
  tabId = injectTabId();
}

2. Using the Low-Level Component with Forms

import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';

@Component({
  selector: 'app-branch-form',
  imports: [BranchDropdownComponent, ReactiveFormsModule],
  template: `
    <checkout-branch-dropdown formControlName="branch" />
  `
})
export class BranchFormComponent {}

Component API Reference

SelectedBranchDropdownComponent

High-level branch selection component with automatic CheckoutMetadataService integration.

Selector: checkout-selected-branch-dropdown

Inputs:

Input Type Required Default Description
tabId number Yes - The tab ID for metadata storage
appearance DropdownAppearance No AccentOutline Visual style of the dropdown

Example:

<checkout-selected-branch-dropdown
  [tabId]="tabId()"
  [appearance]="DropdownAppearance.AccentOutline"
/>

BranchDropdownComponent

Low-level branch dropdown with ControlValueAccessor support for reactive forms.

Selector: checkout-branch-dropdown

Inputs:

Input Type Required Default Description
appearance DropdownAppearance No AccentOutline Visual style of the dropdown

Outputs:

Output Type Description
selected Branch | null Two-way bindable selected branch (model)

Example:

<!-- With ngModel -->
<checkout-branch-dropdown [(selected)]="selectedBranch" />

<!-- With reactive forms -->
<checkout-branch-dropdown formControlName="branch" />

Usage Examples

Basic Usage in Checkout Flow

import { Component } from '@angular/core';
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
import { injectTabId } from '@isa/core/tabs';

@Component({
  selector: 'app-checkout-header',
  imports: [SelectedBranchDropdownComponent],
  template: `
    <div class="checkout-header">
      <h1>Checkout</h1>
      <checkout-selected-branch-dropdown [tabId]="tabId()" />
    </div>
  `
})
export class CheckoutHeaderComponent {
  tabId = injectTabId();
}

Using BranchDropdownComponent with Reactive Forms

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';

@Component({
  selector: 'app-branch-form-example',
  imports: [BranchDropdownComponent, ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <checkout-branch-dropdown formControlName="branch" />
    </form>
  `
})
export class BranchFormExampleComponent {
  #fb = inject(FormBuilder);
  form = this.#fb.group({
    branch: [null]
  });
}

Reading Selected Branch from Metadata

import { Component, inject, computed } from '@angular/core';
import { CheckoutMetadataService } from '@isa/checkout/data-access';
import { injectTabId } from '@isa/core/tabs';

@Component({
  selector: 'app-branch-reader-example',
  template: `
    @if (selectedBranch(); as branch) {
      <p>Selected Branch: {{ branch.name }}</p>
    } @else {
      <p>No branch selected</p>
    }
  `
})
export class BranchReaderExampleComponent {
  #checkoutMetadata = inject(CheckoutMetadataService);
  tabId = injectTabId();

  selectedBranch = computed(() => {
    return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
  });
}

State Management

CheckoutMetadataService Integration

The component integrates with CheckoutMetadataService from @isa/checkout/data-access:

// Set selected branch (stores the full Branch object)
this.#checkoutMetadata.setSelectedBranch(tabId, branch);

// Get selected branch (returns Branch | undefined)
const branch = this.#checkoutMetadata.getSelectedBranch(tabId);

// Clear selected branch
this.#checkoutMetadata.setSelectedBranch(tabId, undefined);

State Persistence:

  • Branch selection persists in tab metadata
  • Available across all checkout components in the same tab
  • Cleared when tab is closed or session ends

Dependencies

Required Libraries

Angular Core

  • @angular/core - Angular framework
  • @angular/forms - Form controls and ControlValueAccessor

ISA Feature Libraries

  • @isa/checkout/data-access - CheckoutMetadataService, BranchesResource, Branch type
  • @isa/core/logging - Logger utility

ISA UI Libraries

  • @isa/ui/input-controls - DropdownButtonComponent, DropdownFilterComponent, DropdownOptionComponent

Path Alias

Import from: @isa/checkout/feature/select-branch-dropdown

// Import components
import {
  SelectedBranchDropdownComponent,
  BranchDropdownComponent
} from '@isa/checkout/feature/select-branch-dropdown';

Testing

The library uses Vitest with Angular Testing Utilities for testing.

Running Tests

# Run tests for this library
npx nx test checkout-feature-select-branch-dropdown --skip-nx-cache

# Run tests with coverage
npx nx test checkout-feature-select-branch-dropdown --coverage.enabled=true --skip-nx-cache

# Run tests in watch mode
npx nx test checkout-feature-select-branch-dropdown --watch

Test Configuration

  • Framework: Vitest
  • Test Runner: @nx/vite:test
  • Coverage: v8 provider with Cobertura reports
  • JUnit XML: testresults/junit-checkout-feature-select-branch-dropdown.xml
  • Coverage XML: coverage/libs/checkout/feature/select-branch-dropdown/cobertura-coverage.xml

Testing Recommendations

Component Testing

import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SelectedBranchDropdownComponent } from './selected-branch-dropdown.component';
import { CheckoutMetadataService, BranchesResource } from '@isa/checkout/data-access';
import { signal } from '@angular/core';

describe('SelectedBranchDropdownComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [SelectedBranchDropdownComponent],
      providers: [
        {
          provide: CheckoutMetadataService,
          useValue: {
            setSelectedBranch: vi.fn(),
            getSelectedBranch: vi.fn(() => null)
          }
        },
        {
          provide: BranchesResource,
          useValue: {
            resource: {
              hasValue: () => true,
              value: () => []
            }
          }
        }
      ]
    });
  });

  it('should create', () => {
    const fixture = TestBed.createComponent(SelectedBranchDropdownComponent);
    fixture.componentRef.setInput('tabId', 1);
    fixture.detectChanges();
    expect(fixture.componentInstance).toBeTruthy();
  });
});

E2E Attribute Requirements

All interactive elements should include data-what and data-which attributes:

<!-- Dropdown -->
<select
  data-what="branch-dropdown"
  data-which="branch-selection">
</select>

<!-- Option -->
<option
  data-what="branch-option"
  [attr.data-which]="branch.id">
</option>

Best Practices

1. Always Use Tab Context

Ensure tab context is available when using the component:

// Good
const tabId = this.#tabService.activatedTabId();
if (tabId) {
  this.#checkoutMetadata.setSelectedBranch(tabId, branch);
}

// Bad (missing tab context validation)
this.#checkoutMetadata.setSelectedBranch(null, branch);

2. Clear Branch on Checkout Completion

Always clear branch selection when checkout is complete:

// Good
completeCheckout() {
  // ... checkout logic
  this.#checkoutMetadata.setSelectedBranch(tabId, undefined);
}

// Bad (leaves stale branch)
completeCheckout() {
  // ... checkout logic
  // Forgot to clear branch!
}

3. Use Computed Signals for Derived State

Leverage Angular signals for reactive values:

// Good
selectedBranch = computed(() => {
  return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
});

// Bad (manual tracking)
selectedBranch: Branch | undefined;
ngOnInit() {
  effect(() => {
    this.selectedBranch = this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
  });
}

4. Follow OnPush Change Detection

Always use OnPush change detection for performance:

// Good
@Component({
  selector: 'checkout-select-branch-dropdown',
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})

// Bad (default change detection)
@Component({
  selector: 'checkout-select-branch-dropdown',
  // Missing changeDetection
})

5. Add E2E Attributes

Always include data-what and data-which attributes:

// Good
<select
  data-what="branch-dropdown"
  data-which="branch-selection"
  [(ngModel)]="selectedBranch">
</select>

// Bad (no E2E attributes)
<select [(ngModel)]="selectedBranch">
</select>

Architecture Notes

Component Structure

SelectedBranchDropdownComponent (high-level, with metadata integration)
├── Uses BranchDropdownComponent internally
├── CheckoutMetadataService integration
├── Tab context via tabId input
└── Automatic branch persistence

BranchDropdownComponent (low-level, form-compatible)
├── ControlValueAccessor implementation
├── BranchesResource for loading options
├── DropdownButtonComponent from @isa/ui/input-controls
└── Filter support via DropdownFilterComponent

Data Flow

SelectedBranchDropdownComponent:
1. Component receives tabId input
   ↓
2. Reads current branch from CheckoutMetadataService.getSelectedBranch(tabId)
   ↓
3. User selects branch from dropdown
   ↓
4. setSelectedBranch(tabId, branch) called
   ↓
5. Other checkout components react to metadata change

BranchDropdownComponent:
1. BranchesResource loads available branches
   ↓
2. User selects branch from dropdown
   ↓
3. selected model emits new value
   ↓
4. ControlValueAccessor notifies form (if used with forms)

License

Internal ISA Frontend library - not for external distribution.