Consolidar la experiencia visual de los modales alrededor de una sola base de presentación y retirar paulatinamente los modal services redundantes. Cada funcionalidad debe conservar una fuente de verdad para sus datos y su API. La pantalla o feature que abre el modal entrega la configuración y la fuente de datos; el modal presenta, busca, página, selecciona y devuelve un resultado.
La meta es reducir duplicación y acoplamiento, no crear un servicio global que conozca todas las APIs del ERP.
SelectDialogComponent de libs/shared/ui/src/lib/select-dialog/ como base para los diálogos de consulta y selección. Extenderlo solo mediante configuración tipada y slots/capacidades realmente comunes.loadPage/dataSource con filtros. Así los datos llegan desde el contexto que conoce el caso de uso y el modal consulta una sola fuente, explícita y testeable.libs/domain; DTOs y modelos de transporte propios de un backend se ubican en la librería de libs/data-access de la funcionalidad que los usa.La base común no debe importar un servicio de artículos, terceros, bancos u otro catálogo. Tampoco debe construir una URL ni decidir qué endpoint corresponde a una pantalla.
El contrato debe adaptarse al SelectDialogComponent ya existente y tiparse antes de implementarse. Ejemplo conceptual:
interface ModalPage<T> {
data: T[];
total: number;
}
interface SelectionModalConfig<T, TFilter = unknown> {
title: string;
columns: ReadonlyArray<SelectionColumn<T>>;
loadPage: (query: SelectionQuery<TFilter>) => Observable<ModalPage<T>> | Promise<ModalPage<T>>;
display: (item: T) => string;
idKey: keyof T;
selected?: T[];
multiSelect?: boolean;
filters?: TFilter;
}
El servicio genérico abre el componente con la configuración y devuelve Observable<T | null> para selección única o una colección tipada para selección múltiple. La función entregada debe estar ligada al servicio de datos del feature y recibir búsqueda, página, tamaño, orden y filtros; la base común controla estados de carga/error/vacío y no debe ocultar silenciosamente errores que el consumidor necesita mostrar.
Ejemplo de consumo conceptual; ajustar nombres al contrato final:
private readonly modalSelection = inject(ModalSelectionService);
private readonly articulos = inject(ArticuloService);
seleccionarArticulo(): void {
this.modalSelection.open<Articulo, ArticuloFilters>({
title: this.translation.translate(ARTICLES.SELECT.TITLE),
columns: this.articleColumns,
idKey: 'id',
display: (item) => `${item.codigo} - ${item.nombre}`,
filters: { soloConStock: true },
loadPage: (query) => this.articulos.search({ ...query, soloConStock: true }),
}).subscribe((item) => {
if (item) this.form.patchValue({ articuloId: item.id });
});
}
El ejemplo ilustra la dirección de dependencias, no una API ya implementada. La función search debe vivir en libs/data-access/<feature> (o en shared/data-access solamente si se trata de infraestructura transversal). El componente del modal recibe loadPage; no inyecta ArticuloService directamente.
La carpeta actual está en libs/shared/ui/src/lib/modals/{components,data-access,domain}. La separación objetivo recomendada es:
libs/shared/
ui/ # base visual, componentes/presentación genéricos
data-access/ # infraestructura transversal de diálogo si aplica
libs/domain/src/lib/ # entidades y contratos de negocio compartidos
libs/data-access/src/ # clientes API por funcionalidad de negocio
libs/<feature>/ # formularios y flujos exclusivos de una funcionalidad
Decisión por tipo de archivo:
| Elemento actual | Destino recomendado |
|---|---|
SelectDialogComponent, modal base y componentes visuales genéricos |
libs/shared/ui (actualizar su API como base común). |
| Servicio genérico que solo abre el modal | libs/shared/ui si depende de UI, o libs/shared/data-access no es apropiado si solo envuelve UI; preferir una fachada de diálogo en shared UI/infrastructure de presentación. |
*-modal.service.ts de un catálogo de negocio |
Eliminar gradualmente si solo configura el modal; trasladar su selección/configuración al feature consumidor. Si encapsula un caso reutilizado real, dejar una facade del feature, no un servicio API dentro de UI. |
| Servicio que llama API de artículos, bancos, terceros, CUIPO, cuentas, etc. | libs/data-access/src/<feature>/ (por ejemplo articulos, tesoreria, maestros, accounting). |
| Contrato de negocio compartido por varios módulos | libs/domain/src/lib/<dominio>/ y exportación por barril público. |
| DTO exclusivo de endpoint o respuesta | Junto al servicio de libs/data-access dueño de ese endpoint. |
| Formulario/importador exclusivo de contabilidad | Feature contable; conservar en shared UI solo controles genéricos de carga de archivo. |
| Helpers de paginación/ordenamiento sin dependencia de un catálogo | libs/shared/util o infraestructura común existente, si la reutilización es real. |
No se recomienda mover todos los servicios específicos a libs/shared/data-access: esa librería debe quedar transversal. La API del recurso de negocio debe vivir en el data-access correspondiente, evitando convertir shared en un módulo central de negocio.
La intención es que los modal services duplicados se deprequen gradualmente para dejar una sola fachada/base de apertura y una sola implementación visual de selector. No declarar @deprecated de inmediato a todos: primero clasificar cada servicio por consumidores y responsabilidad.
Un servicio puede marcarse @deprecated cuando:
SelectDialogComponent;Si contiene reglas o transformaciones específicas, extraerlas a feature/data-access antes de retirar el wrapper. Mantener el mismo servicio API para cada recurso cuando varios consumidores consulten el mismo recurso; “una sola fuente de datos” significa un único dueño/repositorio de cada endpoint, no fusionar todas las APIs en un solo archivo.
HttpService y API_CONFIG; retirar URLs de ambiente hardcodeadas.unknown/any solo donde el límite API lo exija, normalizando de inmediato).null y distinguirla de error de carga.domain no depende de UI ni HTTP; data-access puede depender de domain; shared/ui no importa servicios concretos de features.