mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged PR 1841: feat(ui-input-controls, oms-return-process): introduce text field container,...
feat(ui-input-controls, oms-return-process): introduce text field container, clear, and errors components - Add `ui-text-field-container`, `ui-text-field-clear`, and `ui-text-field-errors` as standalone components for improved text field composition and error handling. - Update SCSS to include new styles for container, clear, and errors components, ensuring visual consistency and error highlighting. - Refactor `ReturnProcessProductQuestionComponent` to use the new containerized text field structure, improving template clarity and error display. - Update Storybook story for `TextField` to demonstrate new composition and error handling. - Export new components from the input-controls public API for external usage. Ref: #4989, #5058
This commit is contained in:
committed by
Lorenz Hilpert
parent
f5507a874c
commit
b261273228
@@ -1,24 +1,72 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { TextFieldComponent } from '@isa/ui/input-controls';
|
||||
import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import {
|
||||
TextFieldClearComponent,
|
||||
TextFieldComponent,
|
||||
TextFieldContainerComponent,
|
||||
TextFieldErrorsComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
|
||||
const meta: Meta<TextFieldComponent> = {
|
||||
interface TextFieldStoryProps {
|
||||
showClear: boolean;
|
||||
showError1: boolean;
|
||||
showError2: boolean;
|
||||
errorText1: string;
|
||||
errorText2: string;
|
||||
}
|
||||
|
||||
const meta: Meta<TextFieldStoryProps> = {
|
||||
component: TextFieldComponent,
|
||||
title: 'ui/input-controls/TextField',
|
||||
argTypes: {},
|
||||
argTypes: {
|
||||
showClear: { control: 'boolean', name: 'Show Clear Button' },
|
||||
showError1: { control: 'boolean', name: 'Show Error 1' },
|
||||
showError2: { control: 'boolean', name: 'Show Error 2' },
|
||||
errorText1: { control: 'text', name: 'Error 1 Text' },
|
||||
errorText2: { control: 'text', name: 'Error 2 Text' },
|
||||
},
|
||||
args: {
|
||||
showClear: true,
|
||||
showError1: false,
|
||||
showError2: false,
|
||||
errorText1: 'Eingabe ungültig',
|
||||
errorText2: 'Error Beispiel 2',
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
TextFieldClearComponent,
|
||||
TextFieldContainerComponent,
|
||||
TextFieldErrorsComponent,
|
||||
],
|
||||
}),
|
||||
],
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ui-text-field-container>
|
||||
<ui-text-field>
|
||||
<input type="text" placeholder="Enter your name" />
|
||||
<input type="text" placeholder="Enter your name" />
|
||||
<ui-text-field-clear *ngIf="showClear"></ui-text-field-clear>
|
||||
</ui-text-field>
|
||||
<ui-text-field-errors>
|
||||
<span *ngIf="showError1">{{ errorText1 }}</span>
|
||||
<span *ngIf="showError2">{{ errorText2 }}</span>
|
||||
</ui-text-field-errors>
|
||||
</ui-text-field-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TextFieldComponent>;
|
||||
type Story = StoryObj<TextFieldStoryProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
args: {
|
||||
showClear: true,
|
||||
showError1: false,
|
||||
showError2: false,
|
||||
errorText1: 'Eingabe ungültig',
|
||||
errorText2: 'Error Beispiel 2',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './lib/scanner.service';
|
||||
export * from './lib/render-if-scanner-is-ready.directive';
|
||||
export * from './lib/errors';
|
||||
|
||||
1
libs/core/scanner/src/lib/errors/index.ts
Normal file
1
libs/core/scanner/src/lib/errors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './platform-not-supported.error';
|
||||
@@ -0,0 +1,6 @@
|
||||
export class PlatformNotSupportedError extends Error {
|
||||
constructor() {
|
||||
super('ScannerService is only supported on iOS and Android platforms');
|
||||
this.name = 'PlatformNotSupportedError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { signal, Signal, WritableSignal } from '@angular/core';
|
||||
import {
|
||||
createDirectiveFactory,
|
||||
SpectatorDirective,
|
||||
} from '@ngneat/spectator/jest';
|
||||
import { ScannerReadyDirective } from './render-if-scanner-is-ready.directive';
|
||||
import { ScannerService } from './scanner.service';
|
||||
|
||||
// FULLY MOCK EXTERNAL DEPENDENCIES
|
||||
jest.mock('scandit-web-datacapture-core', () => ({
|
||||
configure: jest.fn(),
|
||||
}));
|
||||
jest.mock('scandit-web-datacapture-barcode', () => ({
|
||||
barcodeCaptureLoader: jest.fn(() => 'mockedBarcodeLoader'),
|
||||
}));
|
||||
const loggerInfo = jest.fn();
|
||||
const loggerError = jest.fn();
|
||||
jest.mock('@isa/core/logging', () => ({
|
||||
logger: jest.fn(() => ({ info: loggerInfo, error: loggerError })),
|
||||
}));
|
||||
|
||||
describe('ScannerReadyDirective', () => {
|
||||
let spectator: SpectatorDirective<ScannerReadyDirective>;
|
||||
let readySignal: WritableSignal<boolean>;
|
||||
let scannerServiceMock: jest.Mocked<ScannerService>;
|
||||
|
||||
const createDirective = createDirectiveFactory({
|
||||
directive: ScannerReadyDirective,
|
||||
mocks: [ScannerService], // mock out the real ScannerService (and its Config deps)
|
||||
detectChanges: false, // manual CD
|
||||
template: `<div *sharedScannerReady>Ready</div>`,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 1) instantiate
|
||||
spectator = createDirective();
|
||||
// 2) grab the Spectator mock
|
||||
scannerServiceMock = spectator.inject(ScannerService);
|
||||
// 3) override its `ready` with a real Angular signal
|
||||
readySignal = signal(false);
|
||||
(scannerServiceMock as any).ready = readySignal;
|
||||
});
|
||||
|
||||
it('should render when ready=true', () => {
|
||||
// ARRANGE
|
||||
readySignal.set(true);
|
||||
// ACT
|
||||
spectator.detectChanges();
|
||||
// ASSERT
|
||||
expect(spectator.query('div')).toHaveText('Ready');
|
||||
expect(spectator.query('div')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render when ready=false', () => {
|
||||
// ARRANGE
|
||||
readySignal.set(false);
|
||||
// ACT
|
||||
spectator.detectChanges();
|
||||
// ASSERT
|
||||
expect(spectator.query('div')).not.toExist();
|
||||
});
|
||||
|
||||
it('should update view when ready flips', () => {
|
||||
// ARRANGE
|
||||
readySignal.set(false);
|
||||
spectator.detectChanges();
|
||||
expect(spectator.query('div')).not.toExist();
|
||||
|
||||
// ACT
|
||||
readySignal.set(true);
|
||||
spectator.detectChanges();
|
||||
// ASSERT
|
||||
expect(spectator.query('div')).toHaveText('Ready');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Directive,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
inject,
|
||||
effect,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ScannerService } from './scanner.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[sharedScannerReady]',
|
||||
standalone: true,
|
||||
})
|
||||
export class ScannerReadyDirective {
|
||||
#scanner = inject(ScannerService);
|
||||
#vcRef = inject(ViewContainerRef);
|
||||
#tpl = inject(TemplateRef<unknown>);
|
||||
|
||||
scannerReadyElse = input<TemplateRef<unknown> | undefined>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.#updateView();
|
||||
});
|
||||
}
|
||||
|
||||
#updateView(): void {
|
||||
this.#vcRef.clear();
|
||||
if (this.#scanner.ready()) {
|
||||
this.#vcRef.createEmbeddedView(this.#tpl);
|
||||
return;
|
||||
}
|
||||
const elseTpl = this.scannerReadyElse();
|
||||
if (elseTpl) {
|
||||
this.#vcRef.createEmbeddedView(elseTpl);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
libs/core/scanner/src/lib/scanner.service.spec.ts
Normal file
141
libs/core/scanner/src/lib/scanner.service.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||
import {
|
||||
ScannerService,
|
||||
SCANDIT_LICENSE,
|
||||
SCANDIT_LIBRARY_LOCATION,
|
||||
} from './scanner.service';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { configure } from 'scandit-web-datacapture-core';
|
||||
|
||||
import { Config, CONFIG_DATA } from '@isa/core/config'; // <-- Use the real tokens
|
||||
|
||||
class MockConfig {
|
||||
get(key: string) {
|
||||
if (key === 'licence.scandit') return 'FAKE-LICENSE';
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('scandit-web-datacapture-core', () => ({
|
||||
configure: jest.fn(),
|
||||
}));
|
||||
jest.mock('scandit-web-datacapture-barcode', () => ({
|
||||
barcodeCaptureLoader: jest.fn(() => 'mockedBarcodeLoader'),
|
||||
}));
|
||||
const loggerInfo = jest.fn();
|
||||
const loggerError = jest.fn();
|
||||
jest.mock('@isa/core/logging', () => ({
|
||||
logger: jest.fn(() => ({
|
||||
info: loggerInfo,
|
||||
error: loggerError,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('ScannerService', () => {
|
||||
let spectator: SpectatorService<ScannerService>;
|
||||
|
||||
const createService = createServiceFactory({
|
||||
service: ScannerService,
|
||||
providers: [
|
||||
{ provide: SCANDIT_LICENSE, useValue: 'FAKE-LICENSE' },
|
||||
{ provide: SCANDIT_LIBRARY_LOCATION, useValue: 'https://fake-location/' },
|
||||
{ provide: Platform, useValue: { IOS: true, ANDROID: false } },
|
||||
{ provide: CONFIG_DATA, useValue: {} },
|
||||
{ provide: Config, useClass: MockConfig },
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
describe('configure', () => {
|
||||
it('should configure successfully on supported platform', async () => {
|
||||
(configure as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await spectator.service.configure();
|
||||
|
||||
expect(configure).toHaveBeenCalledWith({
|
||||
licenseKey: 'FAKE-LICENSE',
|
||||
libraryLocation: 'https://fake-location/',
|
||||
moduleLoaders: ['mockedBarcodeLoader'],
|
||||
});
|
||||
expect(loggerInfo).toHaveBeenCalledWith('Scanner initializing');
|
||||
expect(loggerInfo).toHaveBeenCalledWith('Scanner ready');
|
||||
});
|
||||
|
||||
it('should not re-configure if already initializing or ready', async () => {
|
||||
(configure as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await spectator.service.configure();
|
||||
await spectator.service.configure();
|
||||
|
||||
expect((configure as jest.Mock).mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw and log error if configure throws', async () => {
|
||||
(configure as jest.Mock).mockRejectedValue(new Error('Config failed'));
|
||||
|
||||
await expect(spectator.service.configure()).rejects.toThrow(
|
||||
'Config failed',
|
||||
);
|
||||
expect(loggerError).toHaveBeenCalledWith(
|
||||
'Failed to configure Scandit',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ready', () => {
|
||||
it('should return true if status is Ready', async () => {
|
||||
(configure as jest.Mock).mockResolvedValue(undefined);
|
||||
await spectator.service.configure();
|
||||
expect(spectator.service.ready()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if status is not Ready', () => {
|
||||
expect(spectator.service.ready()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('open', () => {
|
||||
it('should call configure and resolve undefined', async () => {
|
||||
(configure as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await spectator.service.open();
|
||||
|
||||
expect(configure).toHaveBeenCalled();
|
||||
expect(loggerInfo).toHaveBeenCalledWith('Scanner not implemented');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should propagate error from configure', async () => {
|
||||
(configure as jest.Mock).mockRejectedValue(new Error('Config failed'));
|
||||
|
||||
await expect(spectator.service.open()).rejects.toThrow('Config failed');
|
||||
});
|
||||
});
|
||||
|
||||
// Additional tests for edge cases and error handling
|
||||
|
||||
it('should set status to Error if configure throws', async () => {
|
||||
(configure as jest.Mock).mockRejectedValue(new Error('fail'));
|
||||
try {
|
||||
await spectator.service.configure();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
expect(spectator.service.ready()).toBe(false);
|
||||
expect(loggerError).toHaveBeenCalledWith(
|
||||
'Failed to configure Scandit',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call logger.info with "Scanner not implemented" when open is called', async () => {
|
||||
(configure as jest.Mock).mockResolvedValue(undefined);
|
||||
await spectator.service.open();
|
||||
expect(loggerInfo).toHaveBeenCalledWith('Scanner not implemented');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,18 @@
|
||||
import { inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
Injectable,
|
||||
InjectionToken,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { Config } from '@isa/core/config';
|
||||
import { z } from 'zod';
|
||||
import { configure } from 'scandit-web-datacapture-core';
|
||||
import { barcodeCaptureLoader, Symbology } from 'scandit-web-datacapture-barcode';
|
||||
|
||||
export { Symbology };
|
||||
import { barcodeCaptureLoader } from 'scandit-web-datacapture-barcode';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { PlatformNotSupportedError } from './errors/platform-not-supported.error';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
export const SCANDIT_LICENSE = new InjectionToken<string>('ScanditLicense', {
|
||||
factory() {
|
||||
@@ -12,39 +20,83 @@ export const SCANDIT_LICENSE = new InjectionToken<string>('ScanditLicense', {
|
||||
},
|
||||
});
|
||||
|
||||
export const SCANDIT_LIBRARY_LOCATION = new InjectionToken<string>('ScanditLibraryLocation', {
|
||||
factory() {
|
||||
return new URL('scandit', document.baseURI).toString();
|
||||
export const SCANDIT_LIBRARY_LOCATION = new InjectionToken<string>(
|
||||
'ScanditLibraryLocation',
|
||||
{
|
||||
factory() {
|
||||
return new URL('scandit', document.baseURI).toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const ScannerStatus = {
|
||||
None: 'none',
|
||||
Initializing: 'initializing',
|
||||
Ready: 'ready',
|
||||
Error: 'error',
|
||||
} as const;
|
||||
|
||||
export type ScannerStatus = (typeof ScannerStatus)[keyof typeof ScannerStatus];
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ScannerService {
|
||||
readonly licenseKey = inject(SCANDIT_LICENSE);
|
||||
#logger = logger(() => ({ service: 'ScannerService' }));
|
||||
#licenseKey = inject(SCANDIT_LICENSE);
|
||||
#platform = inject(Platform);
|
||||
#libraryLocation = inject(SCANDIT_LIBRARY_LOCATION);
|
||||
|
||||
readonly libraryLocation = inject(SCANDIT_LIBRARY_LOCATION);
|
||||
#status = signal<ScannerStatus>(ScannerStatus.None);
|
||||
|
||||
private configured = false;
|
||||
ready = computed(() => {
|
||||
untracked(() => {
|
||||
this.configure();
|
||||
});
|
||||
return this.#status() === ScannerStatus.Ready;
|
||||
});
|
||||
|
||||
async configure() {
|
||||
if (this.configured) {
|
||||
private checkPlatformSupported() {
|
||||
if (this.#platform.IOS || this.#platform.ANDROID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await configure({
|
||||
licenseKey: this.licenseKey,
|
||||
libraryLocation: this.libraryLocation,
|
||||
moduleLoaders: [barcodeCaptureLoader()],
|
||||
});
|
||||
|
||||
this.configured = true;
|
||||
throw new PlatformNotSupportedError();
|
||||
}
|
||||
|
||||
async open(args: { symbologies: Symbology[] }): Promise<string | undefined> {
|
||||
async configure() {
|
||||
const status = this.#status();
|
||||
if (
|
||||
status === ScannerStatus.Initializing ||
|
||||
status === ScannerStatus.Ready
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#status.set(ScannerStatus.Initializing);
|
||||
this.#logger.info('Scanner initializing');
|
||||
|
||||
try {
|
||||
this.checkPlatformSupported();
|
||||
|
||||
await configure({
|
||||
licenseKey: this.#licenseKey,
|
||||
libraryLocation: this.#libraryLocation,
|
||||
moduleLoaders: [barcodeCaptureLoader()],
|
||||
});
|
||||
} catch (error) {
|
||||
this.#status.set(ScannerStatus.Error);
|
||||
this.#logger.error('Failed to configure Scandit', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#status.set(ScannerStatus.Ready);
|
||||
this.#logger.info('Scanner ready');
|
||||
}
|
||||
|
||||
async open(): Promise<string | undefined> {
|
||||
await this.configure();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.warn('Scanner not implemented');
|
||||
this.#logger.info('Scanner not implemented');
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 1000);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
@let p = product();
|
||||
<div class="flex gap-6 flex-shrink-0 justify-between items-center">
|
||||
<div>
|
||||
{{ question().description }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-right">
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<ui-text-field-container>
|
||||
<ui-text-field size="small" class="min-w-[22.875rem]">
|
||||
<input
|
||||
uiInputControl
|
||||
@@ -11,36 +13,47 @@
|
||||
placeholder="EAN eingeben / scannen"
|
||||
type="text"
|
||||
[formControl]="control"
|
||||
(cleared)="resetProduct()"
|
||||
/>
|
||||
<button
|
||||
class="px-0"
|
||||
uiTextButton
|
||||
size="small"
|
||||
color="strong"
|
||||
(click)="check()"
|
||||
[pending]="status().fetching"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
@if (!p) {
|
||||
<button
|
||||
class="px-0"
|
||||
uiTextButton
|
||||
size="small"
|
||||
color="strong"
|
||||
(click)="check()"
|
||||
[pending]="status().fetching"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
} @else {
|
||||
<ui-text-field-clear></ui-text-field-clear>
|
||||
}
|
||||
</ui-text-field>
|
||||
</div>
|
||||
|
||||
<ui-text-field-errors>
|
||||
@if (!p && status().hasResult === false) {
|
||||
<span>Kein Artikel gefunden</span>
|
||||
}
|
||||
|
||||
@if (control.invalid && control.touched && !control.dirty) {
|
||||
<span>Die eingegebene EAN ist ungültig</span>
|
||||
}
|
||||
</ui-text-field-errors>
|
||||
</ui-text-field-container>
|
||||
|
||||
<ui-icon-button
|
||||
*sharedScannerReady
|
||||
class="self-start"
|
||||
type="button"
|
||||
[color]="'primary'"
|
||||
[disabled]="!!control?.value"
|
||||
(click)="check()"
|
||||
name="isaActionScanner"
|
||||
></ui-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@let p = product();
|
||||
|
||||
@if (!p && status().hasResult === false) {
|
||||
<div class="text-isa-accent-red isa-text-body-2-bold self-end">
|
||||
Kein Artikel gefunden
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (control.invalid && control.touched && !control.dirty) {
|
||||
<div class="text-isa-accent-red isa-text-body-2-bold self-end">
|
||||
Die eingegebene EAN ist ungültig
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (p) {
|
||||
<div class="mb-6 flex flex-col gap-4">
|
||||
<div class="isa-text-body-2-bold">Gelieferter Artikel:</div>
|
||||
|
||||
@@ -19,16 +19,22 @@ import {
|
||||
ReturnProcessProductQuestion,
|
||||
ReturnProcessStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
InputControlDirective,
|
||||
TextFieldClearComponent,
|
||||
TextFieldComponent,
|
||||
TextFieldContainerComponent,
|
||||
TextFieldErrorsComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { filter, pipe, switchMap, tap } from 'rxjs';
|
||||
import { CatalougeSearchService } from '@isa/catalogue/data-access';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionScanner } from '@isa/icons';
|
||||
import { ScannerReadyDirective } from '@isa/core/scanner';
|
||||
|
||||
const eanValidator: ValidatorFn = (
|
||||
control: AbstractControl,
|
||||
@@ -47,11 +53,17 @@ const eanValidator: ValidatorFn = (
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
TextFieldComponent,
|
||||
InputControlDirective,
|
||||
TextButtonComponent,
|
||||
TextFieldContainerComponent,
|
||||
TextFieldErrorsComponent,
|
||||
TextFieldComponent,
|
||||
TextFieldClearComponent,
|
||||
ProductImageDirective,
|
||||
IconButtonComponent,
|
||||
ScannerReadyDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaActionScanner })],
|
||||
})
|
||||
export class ReturnProcessProductQuestionComponent {
|
||||
#returnProcessStore = inject(ReturnProcessStore);
|
||||
@@ -81,6 +93,14 @@ export class ReturnProcessProductQuestionComponent {
|
||||
});
|
||||
}
|
||||
|
||||
resetProduct() {
|
||||
this.product.set(undefined);
|
||||
this.#returnProcessStore.removeAnswer(
|
||||
this.returnProcessId(),
|
||||
this.question().key,
|
||||
);
|
||||
}
|
||||
|
||||
check = rxMethod<void>(
|
||||
pipe(
|
||||
filter(() => this.control.valid),
|
||||
@@ -101,11 +121,7 @@ export class ReturnProcessProductQuestionComponent {
|
||||
);
|
||||
this.status.set({ fetching: false, hasResult: undefined });
|
||||
} else {
|
||||
this.product.set(undefined);
|
||||
this.#returnProcessStore.removeAnswer(
|
||||
this.returnProcessId(),
|
||||
this.question().key,
|
||||
);
|
||||
this.resetProduct();
|
||||
this.status.set({ fetching: false, hasResult: false });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,14 +32,13 @@
|
||||
}
|
||||
</ui-search-bar>
|
||||
|
||||
@if (canScan) {
|
||||
<ui-icon-button
|
||||
class="desktop:invisible"
|
||||
type="submit"
|
||||
[color]="'primary'"
|
||||
[disabled]="control.invalid"
|
||||
(click)="triggerSearch.emit()"
|
||||
name="isaActionScanner"
|
||||
></ui-icon-button>
|
||||
}
|
||||
<ui-icon-button
|
||||
*sharedScannerReady
|
||||
class="desktop:invisible"
|
||||
type="submit"
|
||||
[color]="'primary'"
|
||||
[disabled]="control.invalid"
|
||||
(click)="triggerSearch.emit()"
|
||||
name="isaActionScanner"
|
||||
></ui-icon-button>
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
} from '@isa/ui/search-bar';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { IconButtonColor, IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionSearch, isaActionScanner } from '@isa/icons';
|
||||
import { FilterService, TextFilterInput } from '../../core';
|
||||
import { InputType } from '../../types';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { ScannerReadyDirective } from '@isa/core/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'filter-search-bar-input',
|
||||
@@ -32,9 +32,9 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
imports: [
|
||||
UiSearchBarComponent,
|
||||
IconButtonComponent,
|
||||
NgIconComponent,
|
||||
ReactiveFormsModule,
|
||||
UiSearchBarClearComponent,
|
||||
ScannerReadyDirective,
|
||||
],
|
||||
host: {
|
||||
'[class]': "['filter-search-bar-input', appearanceClass()]",
|
||||
@@ -43,7 +43,6 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
})
|
||||
export class SearchBarInputComponent {
|
||||
readonly filterService = inject(FilterService);
|
||||
#platform = inject(Platform);
|
||||
|
||||
control = new FormControl();
|
||||
valueChanges = toSignal(this.control.valueChanges);
|
||||
@@ -52,8 +51,6 @@ export class SearchBarInputComponent {
|
||||
buttonColor = input<IconButtonColor>('brand');
|
||||
appearance = input<'main' | 'results'>('main');
|
||||
|
||||
canScan = this.#platform.ANDROID || this.#platform.IOS;
|
||||
|
||||
appearanceClass = computed(
|
||||
() => `filter-search-bar-input__${this.appearance()}`,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
@apply rounded-full cursor-pointer bg-isa-white;
|
||||
|
||||
&.disabled {
|
||||
@apply pointer-events-none; // Da es kein natives Button Element ist, müssen hier Pointer-Events deaktiviert werden
|
||||
}
|
||||
}
|
||||
|
||||
.ui-icon-button__small {
|
||||
height: 1.75rem;
|
||||
width: 1.75rem;
|
||||
}
|
||||
|
||||
.ui-icon-button__medium {
|
||||
|
||||
@@ -79,6 +79,7 @@ export class IconButtonComponent {
|
||||
iconSize = computed(() => {
|
||||
const size = this.size();
|
||||
|
||||
if (size === 'small') return `1rem`;
|
||||
if (size === 'medium') return `1.25rem`;
|
||||
if (size === 'large') return `1.5rem`;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export type TextButtonColor =
|
||||
(typeof TextButtonColor)[keyof typeof TextButtonColor];
|
||||
|
||||
export const IconButtonSize = {
|
||||
Small: 'small',
|
||||
Medium: 'medium',
|
||||
Large: 'large',
|
||||
} as const;
|
||||
|
||||
@@ -7,7 +7,10 @@ export * from './lib/dropdown/dropdown.component';
|
||||
export * from './lib/dropdown/dropdown.types';
|
||||
export * from './lib/listbox/listbox-item.directive';
|
||||
export * from './lib/listbox/listbox.directive';
|
||||
export * from './lib/text-field/text-field.component';
|
||||
export * from './lib/text-field/textarea.component';
|
||||
export * from './lib/text-field/text-field.component';
|
||||
export * from './lib/text-field/text-field-clear.component';
|
||||
export * from './lib/text-field/text-field-container.component';
|
||||
export * from './lib/text-field/text-field-errors.component';
|
||||
export * from './lib/chips/chips.component';
|
||||
export * from './lib/chips/chip-option.component';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
@use "./lib/checkbox/checkbox";
|
||||
@use "./lib/checkbox/checklist";
|
||||
@use "./lib/chips/chips";
|
||||
@use "./lib/dropdown/dropdown";
|
||||
@use "./lib/listbox/listbox";
|
||||
@use "./lib/text-field/text-field";
|
||||
@use "./lib/text-field/textarea";
|
||||
@use './lib/checkbox/checkbox';
|
||||
@use './lib/checkbox/checklist';
|
||||
@use './lib/chips/chips';
|
||||
@use './lib/dropdown/dropdown';
|
||||
@use './lib/listbox/listbox';
|
||||
@use './lib/text-field/text-field';
|
||||
@use './lib/text-field/text-field-clear';
|
||||
@use './lib/text-field/text-field-container';
|
||||
@use './lib/text-field/text-field-errors';
|
||||
@use './lib/text-field/textarea';
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Directive, inject, input, OnInit } from '@angular/core';
|
||||
import { Directive, inject, model, output } from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
|
||||
@Directive({ selector: 'input[uiInputControl]', host: { class: 'ui-input-control' } })
|
||||
@Directive({
|
||||
selector: 'input[uiInputControl]',
|
||||
host: { class: 'ui-input-control' },
|
||||
})
|
||||
export class InputControlDirective<T> {
|
||||
readonly control = inject(NgControl, { optional: true, self: true });
|
||||
|
||||
readonly value = input<T>();
|
||||
readonly value = model<T>();
|
||||
cleared = output<void>();
|
||||
|
||||
getValue(): T | undefined {
|
||||
if (this.control) {
|
||||
@@ -14,4 +18,13 @@ export class InputControlDirective<T> {
|
||||
|
||||
return this.value();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (this.control) {
|
||||
this.control.reset();
|
||||
} else {
|
||||
this.value.set(undefined);
|
||||
}
|
||||
this.cleared.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.ui-text-field-clear {
|
||||
ui-icon-button {
|
||||
@apply text-isa-neutral-900;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.ui-text-field-container {
|
||||
@apply flex flex-col gap-1 items-start justify-center;
|
||||
|
||||
&:has(.ui-text-field-errors > *) {
|
||||
.ui-text-field {
|
||||
@apply border-isa-accent-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.ui-text-field-errors {
|
||||
@apply flex flex-col text-isa-accent-red isa-text-caption-regular;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<ui-icon-button
|
||||
type="button"
|
||||
[color]="'neutral'"
|
||||
[size]="size()"
|
||||
(click)="hostComponent.inputControl().clear()"
|
||||
name="isaActionClose"
|
||||
></ui-icon-button>
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TextFieldComponent } from './text-field.component';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-text-field-clear',
|
||||
templateUrl: './text-field-clear.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: {
|
||||
'[class]': '["ui-text-field-clear", sizeClass()]',
|
||||
},
|
||||
providers: [provideIcons({ isaActionClose })],
|
||||
imports: [IconButtonComponent],
|
||||
})
|
||||
export class TextFieldClearComponent {
|
||||
hostComponent = inject(TextFieldComponent, { host: true });
|
||||
|
||||
size = this.hostComponent.size;
|
||||
|
||||
sizeClass = computed(() => {
|
||||
return `ui-text-field-clear__${this.size()}`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-content></ng-content>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-text-field-container',
|
||||
templateUrl: './text-field-container.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: {
|
||||
'[class]': '["ui-text-field-container"]',
|
||||
},
|
||||
})
|
||||
export class TextFieldContainerComponent {}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-content></ng-content>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-text-field-errors',
|
||||
templateUrl: './text-field-errors.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: {
|
||||
'[class]': '["ui-text-field-errors"]',
|
||||
},
|
||||
})
|
||||
export class TextFieldErrorsComponent {}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, contentChild, input } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
contentChild,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { InputControlDirective } from '../core/input-control.directive';
|
||||
|
||||
export const TextFieldSize = {
|
||||
|
||||
Reference in New Issue
Block a user