From 0d5a3074c8531036a8a427517e2afbd16c16708b Mon Sep 17 00:00:00 2001 From: George Date: Wed, 14 Oct 2026 15:39:53 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20columnas=20de=20las=20listas=20de?= =?UTF-8?q?=20valores=20(cliente,=20art=C3=ADculo,=20almac=C3=A9n,=20impue?= =?UTF-8?q?sto)=20configurables=20por=20documento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 4 +- .../features/documents/document-form.page.ts | 16 +++-- .../features/documents/document-layouts.ts | 53 ++++++++++++++- .../documents/screen-extensions.spec.ts | 68 +++++++++++++++++++ .../features/documents/screen-extensions.ts | 7 +- .../src/app/features/documents/screen-kit.ts | 5 +- 6 files changed, 141 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1e3a377..c875535 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,9 @@ Hecho (commits en `main`): monorepo · API (config, capa SAP con sesión compart Pendiente, en orden: 1. **Paso 9 — formulario de documentos:** hecho para ventas y compras (`document-form.page`, `document-model.ts`: cargar, crear, actualizar con `If-Match`/412, navegación, totales con `SalesTaxCodes` (también en compras), solo lectura), probado con SAP simulado y contra `SBODemoCL` (leer, crear, actualizar, líneas, 412). Un código escrito a mano (socio, artículo, almacén, impuesto) se resuelve al salir del campo con `SlLookupApi` (mismo recurso y filtro base que la lista "…"); si no existe se avisa. "Copiar de/a" hecho (`document-copy.ts`: flujos estándar y `BaseType/BaseEntry/BaseLine`; probado: orden de compra 2883 → recepción 299, orden de venta 315 → entrega 580, ambas órdenes quedaron cerradas). Formularios sin socio ni totales **hechos** (`document-layouts.ts`, configuración declarativa: cabecera de 2 columnas, columnas de línea, colección `DocumentLines` o `StockTransferLines`): entradas y salidas de inventario, transferencias de stock, solicitudes de traslado y solicitudes de compra. Entradas, salidas y transferencias contabilizadas son de solo lectura (SAP no las modifica); solicitudes de traslado y de compra se crean y modifican. Probado contra `SBODemoCL`: solicitud de compra 2 (la 1 es una prueba por API), solicitud de traslado 472 (cantidad modificada) y entrada de inventario 155 (1 unidad de `3MG30080714 M 10.5` en el almacén 8803). Limitaciones conocidas: los artículos por **lote o serie** no se pueden contabilizar (SAP pide `BatchNumbers`/`SerialNumbers`); las líneas de compra/inventario llevan `UoMEntry` solo si el artículo tiene grupo de unidades (`uomEntryOf`; sin él SAP responde "specify a UoM code"); los formularios de ventas y compras (`marketing`) aún no lo envían, y los de compra fallan con artículos sin unidad de compra por defecto. **Cómo se define un formulario:** el formulario base (`document-form.page`) no sabe de qué documento se trata; recibe de la pantalla su `layout` (`DocumentLayout` en `document-layouts.ts`): `header` y `footer` (cada uno con `left`/`right`, máximo 2 columnas) son arreglos de campos tomados del catálogo `FIELDS` (texto, fecha, número, almacén, socio, folio, calculado), `columns` son las columnas de línea y `lines` la colección que se guarda. Cada pantalla (en la carpeta de su módulo) declara los suyos (la orden de venta no lleva folio; las facturas, notas de crédito, entregas y recepciones sí, con `TAX_DOCUMENT`); agregar o quitar un campo es editar ese arreglo, sin tocar el HTML. En ventas y compras la pantalla puede agregar campos (`headerLeft`/`headerRight`), reemplazar la cabecera (`header`) o elegir sus columnas de línea del catálogo `LINE_COLUMNS` (`lineColumns: [LINE_COLUMNS.item, LINE_COLUMNS.quantity, …]`; lo que se envía por línea sale de ellas); sin elegir, el documento lleva la cabecera y todas las columnas base. -**Campos por cliente (estándar adicionales o `U_*`):** no se editan las pantallas base. Cada módulo tiene su archivo de extensiones, vacío en `main`: `ventas/ventas.extensions.ts`, `compras/compras.extensions.ts` e `inventario/inventario.extensions.ts` (reunidos en `client-extensions.ts`). La rama `cliente/` declara ahí, por ruta de pantalla, qué suma: `headerLeft`/`headerRight`/`footerLeft`/`footerRight` (campos; tipos `text`, `date`, `number`, `textarea`, `select` con `options`, `warehouse`…), `lineColumns` (se agregan antes del total y se envían a SAP) y `listColumns`. Solo suma, no quita; una ruta inexistente o ajena al módulo lanza un error al arrancar. El test «el proyecto base no usa campos de usuario» revisa `BASE_SCREENS`, no las extensiones. Verificado: el proxy `/api/sl` deja pasar nombres `U_*` en `$select`/`$orderby` y en el cuerpo (el error de un campo inexistente lo da SAP). Límite: una columna de línea de usuario con lista de valores válidos aún no tiene editor desplegable (solo texto/número). Los formularios con socio muestran `NumAtCard` y el RUT (`FederalTaxID`, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (`FOLIO_PATHS` en `document-screens.ts`) añaden el folio tributario, que en el Service Layer es `FolioPrefixString` + `FolioNumber` (se edita como texto y se envía como número); las listas de facturas también los muestran. Falta: botón PDF, fila del flete y toolbar de ancho completo. +**Campos por cliente (estándar adicionales o `U_*`):** no se editan las pantallas base. Cada módulo tiene su archivo de extensiones, vacío en `main`: `ventas/ventas.extensions.ts`, `compras/compras.extensions.ts` e `inventario/inventario.extensions.ts` (reunidos en `client-extensions.ts`). La rama `cliente/` declara ahí, por ruta de pantalla, qué suma: `headerLeft`/`headerRight`/`footerLeft`/`footerRight` (campos; tipos `text`, `date`, `number`, `textarea`, `select` con `options`, `warehouse`…), `lineColumns` (se agregan antes del total y se envían a SAP) y `listColumns`. Solo suma, no quita; una ruta inexistente o ajena al módulo lanza un error al arrancar. El test «el proyecto base no usa campos de usuario» revisa `BASE_SCREENS`, no las extensiones. Verificado: el proxy `/api/sl` deja pasar nombres `U_*` en `$select`/`$orderby` y en el cuerpo (el error de un campo inexistente lo da SAP). Límite: una columna de línea de usuario con lista de valores válidos aún no tiene editor desplegable (solo texto/número). + +**Columnas de las ventanas de selección (cliente, artículo, almacén, impuesto):** son configurables por documento. En una pantalla del base (ventas/compras): `lookups: { item: [...], partner: [...], warehouse: [...], tax: [...] }` en el último parámetro de `marketing(...)` *sustituye* las columnas de esa lista (la primera columna es el código que se escribe en el campo). En la rama del cliente: `lookupColumns` en su archivo de extensiones *suma* columnas al final (por ejemplo `U_Marca` en artículos). El recurso, el filtro y el orden de cada lista no cambian; para los documentos de inventario o solicitudes se usa `withLookupColumns(layout, cols, 'replace' | 'add')`. Las columnas de almacén aplican también al almacén de la cabecera y al de origen de una transferencia. Los formularios con socio muestran `NumAtCard` y el RUT (`FederalTaxID`, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (`FOLIO_PATHS` en `document-screens.ts`) añaden el folio tributario, que en el Service Layer es `FolioPrefixString` + `FolioNumber` (se edita como texto y se envía como número); las listas de facturas también los muestran. Falta: botón PDF, fila del flete y toolbar de ancho completo. 2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible". 3. ~~Pantalla web de permisos~~ — hecha (`/admin/permisos`). 4. Auditoría (`audit_log` en SQLite; hoy solo líneas `AUDIT` en el log) y `nestjs-cls` para el usuario en los logs. diff --git a/apps/web/src/app/features/documents/document-form.page.ts b/apps/web/src/app/features/documents/document-form.page.ts index 67dc75b..b26a5b9 100644 --- a/apps/web/src/app/features/documents/document-form.page.ts +++ b/apps/web/src/app/features/documents/document-form.page.ts @@ -26,7 +26,7 @@ import { updatePayload, validateLayout, } from './document-model'; -import { type ComputedSource, WAREHOUSE_LOOKUP, hasFolio, headerKeysOf, requiredHeaderOf } from './document-layouts'; +import { type ComputedSource, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasFolio, headerKeysOf, requiredHeaderOf } from './document-layouts'; import { type CopyOrigin, copyFromDocument, copySources, copyTargets } from './document-copy'; import { DOCUMENT_SCREENS, type DocumentScreen, docNum, documentStatus, menuLabel } from './document-screens'; import { ITEMS_MULTI_ADD } from './line-columns'; @@ -253,22 +253,24 @@ export class DocumentFormPage { // --- configuración de la vista -------------------------------------------------------------- protected readonly columns = this.layout.columns; + private readonly itemColumn = this.layout.columns.find((c) => c.key === 'ItemCode') ?? this.layout.columns[0]; protected readonly newLine = () => this.layout.newLine(this.header()); protected readonly recalc = this.layout.recalc; protected readonly multi: GridMultiAdd = { ...ITEMS_MULTI_ADD, - toLine: (item) => this.layout.columns[0].lookup!.apply(item, this.layout.newLine(this.header())), + lookup: this.itemColumn.lookup!.config, + toLine: (item) => this.itemColumn.lookup!.apply(item, this.layout.newLine(this.header())), + }; + protected readonly warehouseLookup: VhLookup = { + ...WAREHOUSE_LOOKUP, + columns: this.layout.lookups?.warehouse ?? WAREHOUSE_LOOKUP.columns, }; - protected readonly warehouseLookup = WAREHOUSE_LOOKUP; protected readonly partnerLookup: VhLookup = { title: this.partner?.type === 'cSupplier' ? 'Proveedores' : 'Clientes', resource: 'BusinessPartners', baseFilter: `CardType eq '${this.partner?.type ?? 'cCustomer'}'`, orderby: 'CardCode', - columns: [ - { key: 'CardCode', title: 'Código', width: '130px', mono: true }, - { key: 'CardName', title: 'Nombre' }, - ], + columns: this.layout.lookups?.partner ?? PARTNER_COLUMNS, extraFields: ['FederalTaxID'], }; diff --git a/apps/web/src/app/features/documents/document-layouts.ts b/apps/web/src/app/features/documents/document-layouts.ts index a53e882..84eda11 100644 --- a/apps/web/src/app/features/documents/document-layouts.ts +++ b/apps/web/src/app/features/documents/document-layouts.ts @@ -1,4 +1,5 @@ import type { GridColumn, GridLine } from '../../shared/grid/grid.types'; +import type { ListColumn } from '../../shared/list/list.types'; import type { VhLookup } from '../../shared/value-help/value-help.types'; import type { DocRecord, LinesSpec } from './document-model'; import { DOCUMENT_LINE_COLUMNS, lineColumnsFor, lineFieldsOf, newDocumentLine, recalcLine, uomEntryOf } from './line-columns'; @@ -25,6 +26,12 @@ export type HeaderField = | { type: 'folio'; label: string; prefixKey: string; numberKey: string } | { type: 'computed'; label: string; source: ComputedSource }; +/** Las listas de valores (ventanas modales) de un formulario. */ +export type LookupName = 'partner' | 'item' | 'warehouse' | 'tax'; + +/** Columnas que muestra cada lista de valores; la primera es el código que se escribe en el campo. */ +export type LookupColumns = Partial>; + export interface FieldColumns { /** Dos columnas como máximo (regla del proyecto base). */ left: readonly HeaderField[]; @@ -47,6 +54,8 @@ export interface DocumentLayout { newLine: (header: DocRecord) => GridLine; /** Recalcula una línea tras un cambio (total = cantidad × precio…). */ recalc?: (line: GridLine, changedKey: string) => GridLine; + /** Columnas de las listas de valores de este documento; las que falten usan las del base. */ + lookups?: LookupColumns; /** Una vez contabilizado ya no se modifica (SAP no permite cambiar entradas, salidas ni transferencias). */ immutable?: boolean; /** Ajustes a la cabecera antes de crear. */ @@ -91,6 +100,45 @@ export const itemColumn: GridColumn = { /** Lista de almacenes de la cabecera (la misma de las líneas). */ export const WAREHOUSE_LOOKUP: VhLookup = warehouseColumn.lookup!.config; +/** Columnas del base de la lista de clientes y proveedores. */ +export const PARTNER_COLUMNS: readonly ListColumn[] = [ + { key: 'CardCode', title: 'Código', width: '130px', mono: true }, + { key: 'CardName', title: 'Nombre' }, +]; + +/** Columna de línea que abre cada lista de valores. */ +const LOOKUP_OF_COLUMN: Readonly> = { + ItemCode: 'item', + WarehouseCode: 'warehouse', + FromWarehouseCode: 'warehouse', + TaxCode: 'tax', +}; + +const defaultLookupColumns = (layout: DocumentLayout, name: LookupName): readonly ListColumn[] => { + if (layout.lookups?.[name]) return layout.lookups[name]!; + if (name === 'partner') return PARTNER_COLUMNS; + return layout.columns.find((c) => LOOKUP_OF_COLUMN[c.key] === name)?.lookup?.config.columns ?? []; +}; + +/** + * Cambia las columnas de las listas de valores de un documento. `replace` las sustituye por las + * indicadas (el base lo usa); `add` las suma a las que ya tiene (lo usan las extensiones de cliente). + * El recurso, el filtro y el orden de cada lista no cambian. + */ +export function withLookupColumns(layout: DocumentLayout, changes: LookupColumns, mode: 'replace' | 'add'): DocumentLayout { + const next: Partial> = { ...layout.lookups }; + for (const name of Object.keys(changes) as LookupName[]) { + const given = changes[name] ?? []; + next[name] = mode === 'add' ? [...defaultLookupColumns(layout, name), ...given] : given; + } + const columns = layout.columns.map((c) => { + const name = LOOKUP_OF_COLUMN[c.key]; + const cols = name ? next[name] : undefined; + return cols && c.lookup ? { ...c, lookup: { ...c.lookup, config: { ...c.lookup.config, columns: cols } } } : c; + }); + return { ...layout, columns, lookups: next }; +} + export const blankLine = (): GridLine => ({ ItemCode: '', ItemDescription: '', Quantity: 1, WarehouseCode: '' }); // --- documentos con socio de negocio (ventas y compras) ------------------------------------- @@ -110,9 +158,11 @@ export function marketingLayout(opts: { right?: readonly HeaderField[]; header?: FieldColumns; lineColumns?: readonly GridColumn[]; + /** Columnas de las listas de valores (cliente, artículo, almacén, impuesto) de este documento. */ + lookups?: LookupColumns; }): DocumentLayout { const columns = lineColumnsFor(opts.taxResource, opts.lineColumns); - return { + const layout: DocumentLayout = { lines: { key: 'DocumentLines', fields: lineFieldsOf(columns), base: true }, header: opts.header ?? { left: [ @@ -139,6 +189,7 @@ export function marketingLayout(opts: { recalc: recalcLine, requiredLine: [], }; + return opts.lookups ? withLookupColumns(layout, opts.lookups, 'replace') : layout; } // --- consultas sobre un layout -------------------------------------------------------------- diff --git a/apps/web/src/app/features/documents/screen-extensions.spec.ts b/apps/web/src/app/features/documents/screen-extensions.spec.ts index dc2f5c6..e6e48b4 100644 --- a/apps/web/src/app/features/documents/screen-extensions.spec.ts +++ b/apps/web/src/app/features/documents/screen-extensions.spec.ts @@ -81,6 +81,7 @@ describe('formulario con campos de cliente', () => { ], headerRight: [{ type: 'text', key: 'U_Obra', label: 'Obra' }], lineColumns: [{ key: 'U_Lote', title: 'Lote' }], + lookupColumns: { partner: [{ key: 'U_Zona', title: 'Zona' }], item: [{ key: 'U_Marca', title: 'Marca' }] }, }, }), '/ventas/ordenes', @@ -127,6 +128,19 @@ describe('formulario con campos de cliente', () => { expect([...el().querySelectorAll('input')].map((i) => i.value)).toEqual(expect.arrayContaining(['Torre 3', 'L-77'])); }); + it('la ventana de artículos de una línea muestra las columnas del documento', async () => { + await open(order); + const itemBtn = el().querySelector('.pick button') as HTMLButtonElement; + itemBtn.click(); + await tick(); + const req = ctrl.expectOne((r) => r.url === '/api/sl/Items'); + expect(req.request.params.get('$select')).toContain('U_Marca'); + req.flush({ value: [{ ItemCode: 'A1', ItemName: 'Pieza', U_Marca: 'Acme' }], 'odata.count': '1' }); + await tick(); + expect([...el().querySelectorAll('.dlg thead th')].map((t) => t.textContent?.trim())).toEqual(expect.arrayContaining(['Código', 'Descripción', 'Marca'])); + expect(el().querySelector('.dlg tbody')!.textContent).toContain('Acme'); + }); + it('al modificar envía solo el campo de usuario que cambió', async () => { await open(order); page().setHeader('U_Canal', 'T'); @@ -154,3 +168,57 @@ describe('formulario con campos de cliente', () => { ctrl.expectOne('/api/sl/Orders/5').flush(order); }); }); + +describe('columnas de las listas de valores por documento', () => { + const find = (screens: readonly { path: string }[], path: string) => by(screens, path).layout; + const itemCols = (path: string, screens = BASE_SCREENS) => + find(screens, path).columns.find((c) => c.key === 'ItemCode')!.lookup!.config.columns.map((c) => c.key); + + it('por defecto todos los documentos muestran código y descripción del artículo', () => { + expect(itemCols('/ventas/ordenes')).toEqual(['ItemCode', 'ItemName']); + expect(find(BASE_SCREENS, '/ventas/ordenes').lookups).toBeUndefined(); + }); + + it('una pantalla del base cambia las columnas de su lista sin afectar a las demás', async () => { + const { marketingLayout } = await import('./document-layouts'); + const layout = marketingLayout({ + partnerLabel: 'Cliente', + taxResource: 'SalesTaxCodes', + lookups: { + item: [{ key: 'ItemCode', title: 'Código' }, { key: 'ItemName', title: 'Descripción' }, { key: 'QuantityOnStock', title: 'Stock', type: 'number' }], + partner: [{ key: 'CardCode', title: 'Código' }, { key: 'CardName', title: 'Nombre' }, { key: 'Phone1', title: 'Teléfono' }], + }, + }); + expect(layout.columns.find((c) => c.key === 'ItemCode')!.lookup!.config.columns.map((c) => c.key)).toEqual(['ItemCode', 'ItemName', 'QuantityOnStock']); + expect(layout.lookups!.partner!.map((c) => c.key)).toEqual(['CardCode', 'CardName', 'Phone1']); + // el recurso, el filtro y los campos extra de la lista no cambian + const cfg = layout.columns.find((c) => c.key === 'ItemCode')!.lookup!.config; + expect(cfg.resource).toBe('Items'); + expect(cfg.extraFields).toEqual(expect.arrayContaining(['UoMGroupEntry'])); + expect(itemCols('/ventas/facturas')).toEqual(['ItemCode', 'ItemName']); + }); + + it('una extensión de cliente suma columnas al final de las del base', () => { + const out = applyExtensions(BASE_SCREENS, { + '/ventas/ordenes': { + lookupColumns: { + item: [{ key: 'U_Marca', title: 'Marca' }], + partner: [{ key: 'U_Zona', title: 'Zona' }], + warehouse: [{ key: 'U_Sucursal', title: 'Sucursal' }], + }, + }, + }); + expect(itemCols('/ventas/ordenes', out)).toEqual(['ItemCode', 'ItemName', 'U_Marca']); + const l = find(out, '/ventas/ordenes'); + expect(l.lookups!.partner!.map((c) => c.key)).toEqual(['CardCode', 'CardName', 'U_Zona']); + expect(l.columns.find((c) => c.key === 'WarehouseCode')!.lookup!.config.columns.map((c) => c.key)).toEqual(['WarehouseCode', 'WarehouseName', 'U_Sucursal']); + expect(itemCols('/ventas/facturas', out)).toEqual(['ItemCode', 'ItemName']); + }); + + it('la lista de almacenes se aplica a las dos columnas de almacén de una transferencia', () => { + const out = applyExtensions(BASE_SCREENS, { '/inventario/transferencias': { lookupColumns: { warehouse: [{ key: 'U_Sucursal', title: 'Sucursal' }] } } }); + const cols = find(out, '/inventario/transferencias').columns.filter((c) => c.lookup && /Warehouse/.test(c.key)); + expect(cols).toHaveLength(2); + for (const c of cols) expect(c.lookup!.config.columns.at(-1)!.key).toBe('U_Sucursal'); + }); +}); diff --git a/apps/web/src/app/features/documents/screen-extensions.ts b/apps/web/src/app/features/documents/screen-extensions.ts index 66e11eb..04264f7 100644 --- a/apps/web/src/app/features/documents/screen-extensions.ts +++ b/apps/web/src/app/features/documents/screen-extensions.ts @@ -1,6 +1,6 @@ import type { GridColumn } from '../../shared/grid/grid.types'; import type { ListColumn } from '../../shared/list/list.types'; -import type { DocumentLayout, FieldColumns, HeaderField } from './document-layouts'; +import { type DocumentLayout, type FieldColumns, type HeaderField, type LookupColumns, withLookupColumns } from './document-layouts'; import type { DocumentScreen } from './screen-kit'; import { lineFieldsOf } from './line-columns'; @@ -18,6 +18,8 @@ export interface ScreenExtension { lineColumns?: readonly GridColumn[]; /** Columnas nuevas de la lista, al final. */ listColumns?: readonly ListColumn[]; + /** Columnas que se suman al final de las listas de valores (ventanas de selección) de este documento. */ + lookupColumns?: LookupColumns; } /** Extensiones por ruta de la pantalla (`/ventas/facturas`). */ @@ -34,13 +36,14 @@ function extendLayout(layout: DocumentLayout, ext: ScreenExtension): DocumentLay const columns = at < 0 ? [...layout.columns, ...extra] : [...layout.columns.slice(0, at), ...extra, ...layout.columns.slice(at)]; const fields = [...new Set([...layout.lines.fields, ...lineFieldsOf(extra)])]; - return { + const extended: DocumentLayout = { ...layout, header: withFields(layout.header, ext.headerLeft, ext.headerRight), footer: withFields(layout.footer, ext.footerLeft, ext.footerRight), columns, lines: { ...layout.lines, fields }, }; + return ext.lookupColumns ? withLookupColumns(extended, ext.lookupColumns, 'add') : extended; } /** diff --git a/apps/web/src/app/features/documents/screen-kit.ts b/apps/web/src/app/features/documents/screen-kit.ts index 3584ecd..63228e2 100644 --- a/apps/web/src/app/features/documents/screen-kit.ts +++ b/apps/web/src/app/features/documents/screen-kit.ts @@ -3,7 +3,7 @@ import { formatNumber } from '../../core/format'; import type { ListFilter } from '../../core/sl/list-filters'; import type { ListColumn, ListRow, Tone } from '../../shared/list/list.types'; import type { GridColumn } from '../../shared/grid/grid.types'; -import { type DocumentLayout, FIELDS, type FieldColumns, type HeaderField, marketingLayout } from './document-layouts'; +import { type DocumentLayout, FIELDS, type FieldColumns, type HeaderField, type LookupColumns, marketingLayout } from './document-layouts'; /** Pantalla de un documento: lista de los últimos ingresados + formulario (nuevo / ver-editar). */ export interface DocumentScreen { @@ -118,6 +118,8 @@ export interface MarketingExtras { header?: FieldColumns; /** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */ lineColumns?: readonly GridColumn[]; + /** Columnas de las listas de valores de este documento (cliente, artículo, almacén, impuesto). */ + lookups?: LookupColumns; } /** Documentos tributarios: la cabecera suma el folio (en el Service Layer `FolioPrefixString` y `FolioNumber`). */ @@ -139,6 +141,7 @@ export const marketing = ( right: extras.headerRight, header: extras.header, lineColumns: extras.lineColumns, + lookups: extras.lookups, }), resource, createLabel,