fix(reward-print, reward-popup, reward-destination): improve reward cart stability and UX - fix: remove console.log statement from calculate-price-value helper - fix: add loading/pending state to print button to prevent duplicate prints - fix: debounce reward selection resource reloading to prevent race conditions - fix: correct reward cart item destination-info alignment and flex behavior - fix: support OrderType in OrderDestinationComponent alongside OrderTypeFeature - fix: use unitPrice instead of total for price calculations in reward items - refactor: update calculatePriceValue test descriptions for clarity - fix: fallback to order.orderType when features don't contain orderType The reward selection popup now properly waits for all resources to reload before resolving, preventing timing issues with cart synchronization. Print button shows pending state during print operations. Destination info components now handle both legacy OrderType and new OrderTypeFeature enums for better compatibility. Ref: #5442, #5445
@isa/common/print
A comprehensive print management library for Angular applications providing printer discovery, selection, and unified print operations across label and office printers.
Overview
The Common Print library provides a platform-aware print service that simplifies printer management and print operations in the ISA application. It supports both label printers (for shipping labels, product tags) and office printers (for documents, receipts), with intelligent fallback strategies and user-friendly printer selection dialogs. The library seamlessly integrates with the print API backend and provides reusable components for consistent print UX across the application.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Platform Behavior
- Component Reference
- Testing
- Architecture Notes
Features
- Dual printer type support - Label printers and office printers with dedicated endpoints
- Platform-aware behavior - Automatic direct printing on desktop, dialog on mobile
- Smart printer selection - Remembers default printer per type, fallback to dialog if needed
- Reusable UI components - Print button component with integrated state management
- Print dialog - User-friendly printer selection with error display and retry logic
- Error handling - Graceful fallback from direct print to dialog on failures
- TypeScript type safety - Strongly typed printer models and API interfaces
- Angular CDK integration - Platform detection for iOS/Android awareness
- Standalone components - Modern Angular architecture with explicit imports
Quick Start
1. Import and Inject the Service
import { Component, inject } from '@angular/core';
import { PrintService, PrinterType } from '@isa/common/print';
@Component({
selector: 'app-shipping-label',
template: '...'
})
export class ShippingLabelComponent {
#printService = inject(PrintService);
}
2. Simple Direct Print (Platform-Aware)
async printLabel(): Promise<void> {
const result = await this.#printService.print({
printerType: PrinterType.LABEL,
printFn: async (printer) => {
// Your print logic here
await this.#labelPrintApi.print(printer.key, this.labelData);
}
});
if (result.printer) {
console.log(`Printed to: ${result.printer.value}`);
} else {
console.log('Print cancelled by user');
}
}
3. Force Print Dialog
async printWithDialog(): Promise<void> {
const result = await this.#printService.print({
printerType: PrinterType.OFFICE,
printFn: async (printer) => {
await this.#documentPrintApi.print(printer.key, this.document);
},
directPrint: false // Always show dialog
});
}
4. Use the Print Button Component
import { Component } from '@angular/core';
import { PrintButtonComponent, PrinterType } from '@isa/common/print';
@Component({
selector: 'app-invoice',
template: `
<common-print-button
[printerType]="PrinterType.OFFICE"
[printFn]="printInvoice"
[directPrint]="false">
Rechnung drucken
</common-print-button>
`,
imports: [PrintButtonComponent]
})
export class InvoiceComponent {
PrinterType = PrinterType;
printInvoice = async (printer: Printer) => {
await this.#api.printInvoice(printer.key, this.invoiceId);
};
}
Core Concepts
Printer Types
The library supports two distinct printer categories:
1. Label Printers (PrinterType.LABEL)
- Purpose: Specialized thermal label printers for shipping labels, product tags, barcodes
- API Endpoint:
PrintLabelPrinters() - Common Use Cases: Shipping labels, warehouse labels, product tags, return labels
- Typical Printers: Zebra, Dymo label printers
2. Office Printers (PrinterType.OFFICE)
- Purpose: Standard laser/inkjet printers for documents and receipts
- API Endpoint:
PrintOfficePrinters() - Common Use Cases: Invoices, receipts, reports, customer documents
- Typical Printers: HP, Canon, Epson office printers
Printer Model
All printers share a common interface:
interface Printer {
/** Unique identifier for the printer (printer name/ID) */
key: string;
/** Display name shown to users */
value: string;
/** Whether this printer is currently selected as default */
selected: boolean;
/** Whether this printer is currently enabled for use */
enabled: boolean;
}
Platform-Aware Printing
The library automatically adapts behavior based on platform:
Desktop (Windows, macOS, Linux):
- Attempts direct print to selected default printer
- Falls back to dialog only on errors
- Optimizes for speed and minimal user interaction
Mobile (Android, iOS):
- Always shows printer selection dialog
- No direct print due to platform limitations
- Ensures user has control over print destination
Platform Detection:
const isMobile = this.#platform.ANDROID || this.#platform.IOS;
const allowDirectPrint = !isMobile && selectedPrinter;
Print Flow States
┌─────────────────────────────────────────────────────────────┐
│ Print Requested │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Fetch Available Printers (Label or Office) │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌──────┴──────┐
│ │
Desktop? Mobile?
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Direct Print │ │ Show Dialog │
│ (if selected)│ │ (always) │
└──────┬───────┘ └─────────┬───────┘
│ │
Success? │
│ │
┌────┴────┐ │
│ │ │
Yes No │
│ │ │
│ └───────────────┘
│ │
▼ ▼
Return ┌─────────────────┐
Printer │ User Selects │
│ & Prints │
└────────┬────────┘
│
┌────┴────┐
│ │
Success? Cancel?
│ │
▼ ▼
Return Return
Printer undefined
Direct Print Control
The directPrint parameter offers fine-grained control:
| Value | Behavior |
|---|---|
undefined (default) |
Platform-aware: direct print on desktop with selected printer, dialog on mobile |
true |
Always attempt direct print (requires selected printer, throws error if none) |
false |
Always show dialog, even on desktop with selected printer |
API Reference
PrintService
Main service for printer discovery and print operations.
labelPrinters(): Promise<Printer[]>
Retrieves all available label printers from the print API.
Returns: Promise resolving to array of label printers
Example:
const labelPrinters = await this.#printService.labelPrinters();
labelPrinters.forEach(printer => {
console.log(`${printer.value} - ${printer.selected ? 'Default' : 'Available'}`);
});
officePrinters(): Promise<Printer[]>
Retrieves all available office printers from the print API.
Returns: Promise resolving to array of office printers
Example:
const officePrinters = await this.#printService.officePrinters();
const defaultPrinter = officePrinters.find(p => p.selected);
printers(printerType: PrinterType): Promise<Printer[]>
Generic method to retrieve printers of a specific type.
Parameters:
printerType: PrinterType- The type of printers to retrieve
Returns: Promise resolving to array of printers
Example:
const printers = await this.#printService.printers(PrinterType.LABEL);
print(options): Promise<{ printer?: Printer }>
Initiates a print operation with intelligent platform-aware behavior.
Parameters:
options.printerType: PrinterType- The type of printer to useoptions.printFn: (printer: Printer) => PromiseLike<unknown>- Function that performs the actual printoptions.directPrint?: boolean | undefined- Direct print control (see Direct Print Control)
Returns: Promise resolving to object with selected printer (or undefined if cancelled)
Throws:
- Error from
printFnif direct print fails anddirectPrint: true - Errors propagate to dialog for retry if
directPrintis undefined
Example:
const result = await this.#printService.print({
printerType: PrinterType.LABEL,
printFn: async (printer) => {
await this.#api.printLabel(printer.key, labelData);
},
directPrint: undefined // Platform-aware
});
if (result.printer) {
this.#logger.info(`Label printed to ${result.printer.value}`);
}
PrinterType
Enum-like object for printer type constants.
Values:
PrinterType.LABEL = 'label'- Label printer typePrinterType.OFFICE = 'office'- Office printer type
Type:
type PrinterType = 'label' | 'office';
Example:
import { PrinterType } from '@isa/common/print';
if (printerType === PrinterType.LABEL) {
// Handle label printing
}
Usage Examples
Basic Label Printing
import { Component, inject } from '@angular/core';
import { PrintService, PrinterType } from '@isa/common/print';
import { logger } from '@isa/core/logging';
@Component({
selector: 'app-shipping',
template: `
<button (click)="printShippingLabel()">
Print Shipping Label
</button>
`
})
export class ShippingComponent {
#printService = inject(PrintService);
#logger = logger(() => ({ component: 'ShippingComponent' }));
async printShippingLabel(): Promise<void> {
try {
const result = await this.#printService.print({
printerType: PrinterType.LABEL,
printFn: async (printer) => {
await this.#shippingApi.printLabel({
printerId: printer.key,
orderId: this.orderId,
trackingNumber: this.trackingNumber
});
}
});
if (result.printer) {
this.#logger.info(`Shipping label printed to ${result.printer.value}`);
}
} catch (error) {
this.#logger.error('Failed to print shipping label', error as Error);
}
}
}
Invoice Printing with Forced Dialog
async printInvoice(): Promise<void> {
// Always show dialog to let user choose printer
const result = await this.#printService.print({
printerType: PrinterType.OFFICE,
printFn: async (printer) => {
await this.#documentApi.printInvoice(
printer.key,
this.invoiceId,
{ copies: 2 }
);
},
directPrint: false // Force dialog even on desktop
});
if (result.printer) {
this.showSuccessMessage(`Invoice printed to ${result.printer.value}`);
}
}
Checking Available Printers
import { Component, OnInit, inject, signal } from '@angular/core';
import { PrintService, Printer, PrinterType } from '@isa/common/print';
@Component({
selector: 'app-printer-status',
template: `
<h3>Available Label Printers</h3>
@for (printer of labelPrinters(); track printer.key) {
<div>
{{ printer.value }}
@if (printer.selected) {
<span>(Default)</span>
}
@if (!printer.enabled) {
<span>(Offline)</span>
}
</div>
}
`
})
export class PrinterStatusComponent implements OnInit {
#printService = inject(PrintService);
labelPrinters = signal<Printer[]>([]);
async ngOnInit(): Promise<void> {
const printers = await this.#printService.labelPrinters();
this.labelPrinters.set(printers);
}
}
Print Button with Custom Styling
import { Component } from '@angular/core';
import { PrintButtonComponent, PrinterType, Printer } from '@isa/common/print';
@Component({
selector: 'app-receipt',
template: `
<div class="receipt-actions">
<common-print-button
[printerType]="PrinterType.OFFICE"
[printFn]="printReceipt"
[directPrint]="allowDirectPrint"
class="receipt-print-button">
<ng-icon name="isaActionPrinter" class="mr-2"></ng-icon>
Bon drucken
</common-print-button>
</div>
`,
styles: [`
.receipt-print-button {
width: 100%;
justify-content: center;
}
`],
imports: [PrintButtonComponent]
})
export class ReceiptComponent {
PrinterType = PrinterType;
allowDirectPrint = true;
printReceipt = async (printer: Printer): Promise<void> => {
await this.#receiptApi.print({
printerId: printer.key,
receiptId: this.receiptId,
format: 'thermal'
});
};
}
Error Handling with Retry Logic
async printWithRetry(maxRetries = 3): Promise<void> {
let attempts = 0;
while (attempts < maxRetries) {
try {
const result = await this.#printService.print({
printerType: PrinterType.LABEL,
printFn: async (printer) => {
await this.#api.printLabel(printer.key, this.labelData);
}
});
if (result.printer) {
this.#logger.info(`Print successful on attempt ${attempts + 1}`);
return;
} else {
// User cancelled
this.#logger.info('Print cancelled by user');
return;
}
} catch (error) {
attempts++;
this.#logger.warn(`Print attempt ${attempts} failed`, error as Error);
if (attempts >= maxRetries) {
this.showErrorMessage('Print failed after multiple attempts');
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
Conditional Printer Type Selection
import { Component, computed, signal } from '@angular/core';
import { PrintButtonComponent, PrinterType } from '@isa/common/print';
@Component({
selector: 'app-document',
template: `
<label>
<input type="checkbox" [(ngModel)]="useLabel" />
Print on label printer
</label>
<common-print-button
[printerType]="printerType()"
[printFn]="printDocument">
Print Document
</common-print-button>
`,
imports: [PrintButtonComponent]
})
export class DocumentComponent {
useLabel = signal(false);
printerType = computed(() =>
this.useLabel()
? PrinterType.LABEL
: PrinterType.OFFICE
);
printDocument = async (printer: Printer): Promise<void> => {
await this.#api.print({
printerId: printer.key,
documentId: this.documentId,
format: this.useLabel() ? 'label' : 'a4'
});
};
}
Platform Behavior
Desktop Platforms (Windows, macOS, Linux)
Default Behavior (directPrint: undefined):
- Fetches available printers for the specified type
- Identifies the default printer (where
selected: true) - If a default printer exists:
- Attempts direct print to that printer
- On success: Returns immediately with the printer
- On error: Shows dialog with error message for retry
- If no default printer:
- Shows printer selection dialog immediately
Forced Direct Print (directPrint: true):
- Throws error if no selected printer exists
- Does not show dialog on failure (error propagates to caller)
Forced Dialog (directPrint: false):
- Always shows dialog regardless of selected printer
- Useful for user confirmation or when switching printers
Mobile Platforms (Android, iOS)
All Cases:
- Always shows printer selection dialog
- Direct print is never attempted (platform limitations)
directPrintparameter is effectively ignored- User must manually confirm printer selection
Platform Detection:
// Angular CDK Platform service
const isMobile = this.#platform.ANDROID || this.#platform.IOS;
Print Dialog Features
When the dialog is shown:
- Printer List: All available printers with selection state
- Default Preselection: Default printer is pre-selected
- Status Indicators: Shows enabled/disabled state
- Error Display: Shows errors from failed direct print attempts
- Retry Logic: Allows user to select different printer and retry
- Cancel Option: Returns
undefinedif user cancels
Component Reference
PrintButtonComponent
Reusable button component with integrated print functionality and state management.
Selector: common-print-button
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
printerType |
PrinterType |
Yes | The type of printer to use (LABEL or OFFICE) |
printFn |
(printer: Printer) => PromiseLike<void> |
Yes | Function that executes the print logic |
directPrint |
boolean | undefined |
No | Controls direct print behavior (default: platform-aware) |
Outputs: None (uses signals for state)
Signals:
printing: Signal<boolean>- Indicates if a print operation is in progress
Content Projection:
- Supports transcluded content for button label
- Includes printer icon by default
Example:
<common-print-button
[printerType]="PrinterType.LABEL"
[printFn]="myPrintFunction"
[directPrint]="false">
Custom Button Text
</common-print-button>
Features:
- Automatic loading state management via
printingsignal - Error logging to ISA logging service
- Disables button during print operations
- Printer icon from
@isa/icons
PrintDialogComponent
Internal dialog component for printer selection (not typically used directly).
Selector: common-print-dialog
Data Interface:
interface PrinterDialogData {
printers: Printer[];
error?: unknown;
print: (printer: Printer) => PromiseLike<unknown>;
}
Result Interface:
interface PrinterDialogResult {
printer: Printer | undefined;
}
Features:
- Listbox for printer selection
- Default printer preselection
- Error display with formatting
- Print button with loading state
- Cancel button
Usage (via PrintService):
// Dialog is automatically shown by PrintService
// No need to use directly
Testing
The library uses Jest for testing.
Running Tests
# Run tests for this library
npx nx test common-print --skip-nx-cache
# Run tests with coverage
npx nx test common-print --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test common-print --watch
Test Structure
The library includes unit tests covering:
- Service Methods - Printer fetching and print orchestration
- Platform Detection - Mobile vs desktop behavior
- Direct Print Logic - Default printer selection and fallback
- Error Handling - Dialog display on failures
- Component State - Print button loading states
- Dialog Interaction - Printer selection and print execution
Example Test
import { TestBed } from '@angular/core/testing';
import { Platform } from '@angular/cdk/platform';
import { PrintService } from './print.service';
import { PrintService as PrintApiService } from '@generated/swagger/print-api';
describe('PrintService', () => {
let service: PrintService;
let mockPrintApi: jest.Mocked<PrintApiService>;
let mockPlatform: jest.Mocked<Platform>;
beforeEach(() => {
mockPrintApi = {
PrintLabelPrinters: jest.fn(),
PrintOfficePrinters: jest.fn()
} as any;
mockPlatform = {
ANDROID: false,
IOS: false
} as any;
TestBed.configureTestingModule({
providers: [
PrintService,
{ provide: PrintApiService, useValue: mockPrintApi },
{ provide: Platform, useValue: mockPlatform }
]
});
service = TestBed.inject(PrintService);
});
it('should fetch label printers', async () => {
const mockPrinters = [
{ key: 'printer1', value: 'Label Printer 1', selected: true, enabled: true }
];
mockPrintApi.PrintLabelPrinters.mockReturnValue(
of({ result: mockPrinters })
);
const printers = await service.labelPrinters();
expect(printers).toEqual(mockPrinters);
expect(mockPrintApi.PrintLabelPrinters).toHaveBeenCalled();
});
it('should attempt direct print on desktop with selected printer', async () => {
// Test implementation...
});
});
Architecture Notes
Current Architecture
Components/Features
↓
PrintButtonComponent (optional, reusable UI)
↓
PrintService (main orchestration)
↓
├─→ PrintDialogComponent (printer selection UI)
├─→ Platform (CDK platform detection)
└─→ PrintApiService (generated API client)
Design Patterns
1. Strategy Pattern (Platform-Aware Behavior)
The service uses platform detection to choose the appropriate print strategy:
const directPrintAllowed =
directPrint === undefined
? selectedPrinter && !(this.#platform.ANDROID || this.#platform.IOS)
: directPrint;
Benefits:
- Single API for all platforms
- Automatic optimization per platform
- Easy to test with mocked Platform service
2. Facade Pattern (Unified Print API)
PrintService provides a simplified interface over complex printer management:
// Simple API hides:
// - Printer discovery
// - Platform detection
// - Dialog management
// - Error recovery
await this.#printService.print({ printerType, printFn });
3. Dependency Injection (Modern Angular)
Uses inject() function for clean service composition:
#printService = inject(PrintApiService);
#printDialog = injectDialog(PrintDialogComponent, { title: 'Drucken' });
#platform = inject(Platform);
Known Architectural Considerations
1. Dialog Integration (Low Priority)
Current State:
- Dialog is injected via
injectDialog()from@isa/ui/dialog - Dialog component is tightly coupled to service
Potential Improvement:
- Consider injectable dialog strategy for testing
- Allows mocking dialog interactions in unit tests
Impact: Low - current design works well for production use
2. Print Function Type Safety (Medium Priority)
Current State:
printFnaccepts any PromiseLike return type- No type enforcement on print API contract
Potential Improvement:
interface PrintFunction<TData> {
(printer: Printer, data: TData): PromiseLike<void>;
}
Impact: Medium - would improve type safety at call sites
3. Printer Caching (Low Priority)
Current State:
- Fetches printers on every print operation
- No caching layer
Potential Improvement:
- Cache printers for short duration (60 seconds)
- Invalidate on print errors (printer might be offline)
- Reduces API calls for rapid print operations
Impact: Low - current performance is acceptable
Dependencies
Required Libraries
@angular/core- Angular framework@angular/cdk/platform- Platform detection@generated/swagger/print-api- Generated print API client@isa/ui/dialog- Dialog system@isa/ui/buttons- Button components@isa/ui/input-controls- Listbox component@isa/icons- Icon library@isa/core/logging- Logging servicerxjs- Reactive programming
Path Alias
Import from: @isa/common/print
Performance Considerations
- Lazy Dialog Loading - Dialog component only loaded when needed
- Platform Detection Caching - Platform service caches detection results
- First Value Resolution - Uses
firstValueFrom()for clean Promise conversion - Component OnPush - All components use OnPush change detection
Future Enhancements
Potential improvements identified:
- Print History - Track recent print operations for audit trail
- Printer Status Polling - Real-time printer availability updates
- Print Preview - Optional preview before printing (office printers)
- Batch Printing - Print multiple documents to same printer
- Print Queue - Queue multiple print jobs for sequential processing
- Custom Paper Sizes - Support for custom paper size selection (label printers)
- Print Templates - Reusable print configurations per printer type
License
Internal ISA Frontend library - not for external distribution.