feat(web): resolver al salir del campo el socio y los códigos de línea escritos a mano
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b35aa6bfd
commit
cafe4bc035
9 files changed
+232
-4
No files matched your search
@@ -325,6 +325,67 @@ describe('LookupField', () => {
|
||||
(fixture.nativeElement.querySelector('label') as HTMLElement).click();
|
||||
expect(link).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('código escrito a mano (resolveOnLeave)', () => {
|
||||
const leave = async () => {
|
||||
input().dispatchEvent(new Event('focusout', { bubbles: true }));
|
||||
await tick(fixture);
|
||||
};
|
||||
const find = (code: string) =>
|
||||
ctrl.expectOne(
|
||||
(r) =>
|
||||
r.url === '/api/sl/BusinessPartners' &&
|
||||
r.params.get('$filter') === `(CardType eq 'cCustomer') and (CardCode eq '${code}')` &&
|
||||
r.params.get('$top') === '1',
|
||||
);
|
||||
|
||||
it('por defecto no consulta nada al salir', async () => {
|
||||
typeInto(input(), 'C0001');
|
||||
await leave();
|
||||
ctrl.expectNone((r) => r.url === '/api/sl/BusinessPartners');
|
||||
});
|
||||
|
||||
it('al salir busca el código con el filtro base y lo completa como si se eligiera', async () => {
|
||||
fixture.componentRef.setInput('resolveOnLeave', true);
|
||||
const picked: unknown[] = [];
|
||||
fixture.componentInstance.picked.subscribe((r) => picked.push(r));
|
||||
typeInto(input(), 'c0001');
|
||||
await leave();
|
||||
find('c0001').flush({ value: [{ CardCode: 'C0001', CardName: 'ACME' }] });
|
||||
await tick(fixture);
|
||||
expect(fixture.componentInstance.value()).toBe('C0001'); // el código canónico de SAP
|
||||
expect(picked).toEqual([{ CardCode: 'C0001', CardName: 'ACME' }]);
|
||||
expect(fixture.nativeElement.querySelector('.err')).toBeNull();
|
||||
});
|
||||
|
||||
it('si no existe, avisa y el aviso desaparece al seguir escribiendo', async () => {
|
||||
fixture.componentRef.setInput('resolveOnLeave', true);
|
||||
typeInto(input(), 'ZZZ');
|
||||
await leave();
|
||||
find('ZZZ').flush({ value: [] });
|
||||
await tick(fixture);
|
||||
expect(fixture.nativeElement.querySelector('.err')?.textContent).toContain('No existe «ZZZ»');
|
||||
typeInto(input(), 'ZZ');
|
||||
await tick(fixture);
|
||||
expect(fixture.nativeElement.querySelector('.err')).toBeNull();
|
||||
});
|
||||
|
||||
it('no repite la consulta si el código ya se resolvió', async () => {
|
||||
fixture.componentRef.setInput('resolveOnLeave', true);
|
||||
typeInto(input(), 'C0001');
|
||||
await leave();
|
||||
find('C0001').flush({ value: [{ CardCode: 'C0001', CardName: 'ACME' }] });
|
||||
await tick(fixture);
|
||||
await leave();
|
||||
ctrl.expectNone((r) => r.url === '/api/sl/BusinessPartners');
|
||||
});
|
||||
|
||||
it('un campo vacío no consulta', async () => {
|
||||
fixture.componentRef.setInput('resolveOnLeave', true);
|
||||
await leave();
|
||||
ctrl.expectNone((r) => r.url === '/api/sl/BusinessPartners');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Filas con varios controles, como en el mockup (Cliente = input + botón, Número = serie + número...).
|
||||
|
||||
@@ -2,12 +2,16 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
booleanAttribute,
|
||||
computed,
|
||||
Injector,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { SlLookupApi } from '../../core/sl/sl-lookup';
|
||||
import { ValueHelp } from '../value-help/value-help';
|
||||
import type { VhLookup, VhRow } from '../value-help/value-help.types';
|
||||
import { TextInput } from './controls';
|
||||
@@ -26,7 +30,7 @@ import { SboField } from './sbo-field';
|
||||
<sbo-field
|
||||
[label]="label()"
|
||||
[required]="required()"
|
||||
[error]="error()"
|
||||
[error]="error() || missingText()"
|
||||
action
|
||||
[linkable]="linkable()"
|
||||
(labelClick)="linkClick.emit()"
|
||||
@@ -38,6 +42,7 @@ import { SboField } from './sbo-field';
|
||||
[disabled]="disabled()"
|
||||
[required]="required()"
|
||||
(keydown.f4)="$event.preventDefault(); openList()"
|
||||
(focusout)="resolveTyped()"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -69,10 +74,22 @@ export class LookupField extends FieldBase {
|
||||
/** La etiqueta abre el objeto enlazado (evento `linkClick`). */
|
||||
readonly linkable = input(false, { transform: booleanAttribute });
|
||||
|
||||
/** Al salir del campo con un código escrito a mano, lo busca en la lista (como si se hubiera elegido). */
|
||||
readonly resolveOnLeave = input(false, { transform: booleanAttribute });
|
||||
|
||||
readonly picked = output<VhRow>();
|
||||
readonly linkClick = output<void>();
|
||||
|
||||
protected readonly listOpen = signal(false);
|
||||
/** Código que no existe; el aviso desaparece en cuanto el texto cambia. */
|
||||
private readonly missing = signal<{ code: string; text: string } | null>(null);
|
||||
protected readonly missingText = computed(() => {
|
||||
const m = this.missing();
|
||||
return m && m.code === this.value().trim() ? m.text : '';
|
||||
});
|
||||
private readonly injector = inject(Injector);
|
||||
/** Último código ya resuelto, para no repetir la consulta al salir y volver a entrar sin cambios. */
|
||||
private resolved = '';
|
||||
private readonly box = viewChild.required<TextInput>('box');
|
||||
|
||||
protected openList(): void {
|
||||
@@ -87,8 +104,33 @@ export class LookupField extends FieldBase {
|
||||
protected onPicked(rows: VhRow[]): void {
|
||||
const row = rows[0];
|
||||
if (!row) return;
|
||||
this.apply(row);
|
||||
}
|
||||
|
||||
private apply(row: VhRow): void {
|
||||
const key = this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? '';
|
||||
this.missing.set(null);
|
||||
this.value.set(String(row[key] ?? ''));
|
||||
this.resolved = this.value();
|
||||
this.picked.emit(row);
|
||||
}
|
||||
|
||||
protected async resolveTyped(): Promise<void> {
|
||||
const code = this.value().trim();
|
||||
if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) return;
|
||||
if (!code) {
|
||||
this.missing.set(null);
|
||||
this.resolved = '';
|
||||
return;
|
||||
}
|
||||
if (code === this.resolved) return;
|
||||
try {
|
||||
const row = await this.injector.get(SlLookupApi).find(this.lookup(), code);
|
||||
if (this.value().trim() !== code) return; // el usuario siguió escribiendo
|
||||
if (row) this.apply(row);
|
||||
else this.missing.set({ code, text: `No existe «${code}» en ${this.lookup().title.toLowerCase()}.` });
|
||||
} catch {
|
||||
this.missing.set({ code, text: 'No se pudo comprobar el código.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user