Exportación
Exportación CSV lista para usar, y un contrato de exportación configurable para alcance, campos y orden de columnas.
Exportar a CSV
Agrega un botón de exportación al toolbar con export. Exporta las columnas visibles en su orden actual (respeta ocultar/reordenar), la página actual por defecto:
defineListConfig<Product>({
export: true, // botón CSV de página actual
table: {
columns: [
{ key: 'name', header: 'Nombre' },
// render devuelve JSX → da un valor plano para serializar:
{
key: 'price',
header: 'Precio',
render: p => <b>{money(p.price)}</b>,
exportValue: p => p.price,
},
{ key: 'actions', header: '', render: rowActions, exportable: false }, // se omite
],
},
})exportValue?(item)— valor plano para una columna cuyorenderes JSX. Si se omite, usaitem[key](admite rutas con punto).exportable: false— excluye una columna (ej. una columna de acciones).
Exportar todo. Pasa un ExportConfig para ofrecer también la opción "exportar todo":
export: {
fileName: 'productos', // por defecto el id de la lista
fetchAll: query => listAll(query), // endpoint masivo (recibe la query actual)
}dataen memoria — "exportar todo" se ofrece automáticamente (todo ya está en el navegador).adapterasíncrono — "exportar todo" aparece solo cuando conectasfetchAll. listkit nunca recorre tu adaptador página por página; apuntafetchAlla un endpoint masivo/stream dedicado que aplique la query actual en el servidor. El botón muestra un spinner mientras corre.- Usa
allowExportAll: falsepara forzar solo-página-actual.
El CSV es nativo (sin dependencia extra) y lleva BOM UTF-8 para que Excel lea los acentos correctamente. Los helpers exportRowsToCsv / rowsToCsv / downloadCsv se exportan para botones personalizados.
Exportación configurable (alcance, campos, orden)
Con export activo, "Exportar…" abre un diálogo de configuración antes de
generar el archivo: el usuario elige el alcance (página actual / filas
seleccionadas / todos los resultados), qué campos incluir — premarcados con
las columnas visibles en ese momento — y su orden. Con
export.configurable: false se recupera el menú de un clic.
El universo exportable sale por defecto de las columnas elegibles de la tabla.
Declara fields para ofrecer propiedades que la tabla nunca muestra,
agrupadas al estilo Stripe:
export: {
fileName: 'pedidos',
groups: [
{ id: 'pedido', label: 'Pedido' },
{ id: 'cliente', label: 'Cliente' },
],
fields: [
{ key: 'reference', label: 'Folio', group: 'pedido' },
{ key: 'total', label: 'Importe', group: 'pedido', value: o => o.total },
{ key: 'placedAt', label: 'Fecha', group: 'pedido' }, // → YYYY-MM-DD, hora local
{ key: 'customer.name', label: 'Cliente', group: 'cliente' },
{ key: 'customer.taxId', label: 'RFC', group: 'cliente' }, // no es columna
{ key: 'products.name', label: 'Productos', group: 'cliente' }, // array → "A; B"
],
},- Un campo sin
valuelee sukeycomo path con puntos, atravesando arrays (products.name→ todos los nombres, unidos con'; '; configurable por campo conjoin). - Las fechas (y strings ISO del servidor) se renderizan
YYYY-MM-DDen hora local — ordenable en hoja de cálculo y sin el día corrido cerca de medianoche. Elige'datetime'/'iso'/función custom víadateFormat(global) odate(por campo). maxRows(default 50,000) limita un export "todos"; un archivo truncado lo dice en el diálogo — nunca en silencio.
Seleccionar más allá de la página. Al seleccionar una página completa, la
barra ofrece "Seleccionar los N resultados" — una selección virtual (no se
cargan filas). Desmarcar filas acumula exclusiones; el export envía
excludeKeys en vez de materializar nada.
Escalar con un resolver. Para una lista con servidor, cablea
export.resolve — recibe el ExportRequest completo (alcance, query, keys de
campos en orden, include/exclude keys) y devuelve filas (nunca un archivo
pre-armado, para que el formato por campo sea idéntico en todo alcance):
// cliente
export: {
resolve: async request => {
const res = await fetch('/api/orders/export', {
method: 'POST', // recomendado: los filtros suelen llevar PII y las keys no caben en una URL
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(exportRequestToBody(request)),
})
return res.json() // { rows, truncated?, total? }
},
},
// servidor (estilo Express) — Mongo:
const request = parseExportRequest(req.body, { fields: EXPORT_KEYS })
if (!request) return res.status(400).end()
const { filter, sort, projection, skip, limit } = buildMongoExport(request, {
fields: FIELDS, // el mismo whitelist del endpoint de lista
exportPaths: EXPORT_PATHS, // key de campo → path Mongo de confianza
tiebreak: { _id: 1 }, // obligatorio: un export necesita orden total
})
const rows = await Model.find(filter, projection).sort(sort).skip(skip).limit(limit).lean()
res.json({ rows, total: await Model.countDocuments(filter) })
// servidor — Postgres:
const { sql, params } = buildSqlExport(request, {
table: 'orders o',
fields: FIELDS,
exportColumns: {
reference: 'o.reference',
'products.name': { relation: { table: 'order_item i', on: 'i.order_id = o.id', column: 'i.product_name', orderBy: 'i.pos' } },
},
fallbackSort: 'o.created_at DESC, o.id DESC',
tiebreak: ', o.id',
idColumn: 'o.id',
})
const { rows } = await pool.query(sql, params)Las listas de keys se enlazan como un solo parámetro = ANY($n) (un
IN ($1, $2, …) con miles de keys revienta el límite de parámetros del
driver). El fetchAll legado sigue funcionando como resolver de
scope: 'all'.
Sin resolver: las listas in-memory soportan los tres alcances nativamente; un
adapter de servidor sin resolve ofrece página + seleccionados, con "todos"
deshabilitado con explicación en el diálogo.