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 cuyo render es JSX. Si se omite, usa item[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)
}
  • data en memoria — "exportar todo" se ofrece automáticamente (todo ya está en el navegador).
  • adapter asíncrono — "exportar todo" aparece solo cuando conectas fetchAll. listkit nunca recorre tu adaptador página por página; apunta fetchAll a un endpoint masivo/stream dedicado que aplique la query actual en el servidor. El botón muestra un spinner mientras corre.
  • Usa allowExportAll: false para 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 value lee su key como path con puntos, atravesando arrays (products.name → todos los nombres, unidos con '; '; configurable por campo con join).
  • Las fechas (y strings ISO del servidor) se renderizan YYYY-MM-DD en hora local — ordenable en hoja de cálculo y sin el día corrido cerca de medianoche. Elige 'datetime'/'iso'/función custom vía dateFormat (global) o date (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.

On this page