@@ -11,6 +11,7 @@ import {
1111 headerLabel ,
1212 formatRowCells ,
1313 formatRowForJson ,
14+ cellFontColor ,
1415 type ExportFieldMeta ,
1516} from './export-format.js' ;
1617import { runImport } from './import-runner.js' ;
@@ -783,7 +784,7 @@ function rowsToCsv(
783784 * ended. Dynamically imported so `node:stream` / `exceljs` stay out of the
784785 * module's static graph.
785786 */
786- async function createXlsxStream ( res : any ) : Promise < {
787+ async function createXlsxStream ( res : any , useStyles = false ) : Promise < {
787788 ws : any ;
788789 finalize : ( ) => Promise < void > ;
789790} > {
@@ -797,7 +798,7 @@ async function createXlsxStream(res: any): Promise<{
797798 passthrough . on ( 'error' , reject ) ;
798799 } ) ;
799800
800- const wb = new ExcelJS . stream . xlsx . WorkbookWriter ( { stream : passthrough , useStyles : false } ) ;
801+ const wb = new ExcelJS . stream . xlsx . WorkbookWriter ( { stream : passthrough , useStyles } ) ;
801802 const ws = wb . addWorksheet ( 'Export' ) ;
802803
803804 return {
@@ -4154,6 +4155,11 @@ export class RestServer {
41544155 // Streams the response so 50k-row exports do not buffer in memory; the
41554156 // xlsx path pipes exceljs' streaming writer straight onto the response.
41564157 // Filename suggests `${object}-${YYYY-MM-DD}.${ext}` for browsers.
4158+ //
4159+ // xlsx only: select / radio cells are coloured with their option's
4160+ // `color` as the font colour (white cell background) when the effective
4161+ // limit is <= 10000. Larger exports drop styling for performance and set
4162+ // `X-Export-Styles: dropped` (else `applied`); csv / json are unaffected.
41574163 this . routeManager . register ( {
41584164 method : 'GET' ,
41594165 path : `${ dataPath } /:object/export` ,
@@ -4177,9 +4183,17 @@ export class RestServer {
41774183 const includeHeader = String ( q . header ?? 'true' ) . toLowerCase ( ) !== 'false' ;
41784184 const HARD_CAP = 50_000 ;
41794185 const MAX_CHUNK = 5_000 ;
4186+ // Styled xlsx (per-cell font colour from select options) is far
4187+ // heavier than a bare value dump, so cap it well below HARD_CAP;
4188+ // above this the export still succeeds, just without colours.
4189+ const STYLE_ROW_CAP = 10_000 ;
41804190 const requestedLimit = q . limit != null ? Math . max ( 1 , Number ( q . limit ) || 0 ) : 10_000 ;
41814191 const limit = Math . min ( requestedLimit , HARD_CAP ) ;
41824192 const chunkSize = Math . min ( MAX_CHUNK , Math . max ( 50 , q . page != null ? Number ( q . page ) || 500 : 500 ) ) ;
4193+ // Colour cells only for xlsx within the style cap; decided up
4194+ // front (before streaming) since we can't know the true row
4195+ // count until the stream drains.
4196+ const styled = format === 'xlsx' && limit <= STYLE_ROW_CAP ;
41834197
41844198 let filter : any = undefined ;
41854199 if ( typeof q . filter === 'string' && q . filter . length > 0 ) {
@@ -4267,13 +4281,17 @@ export class RestServer {
42674281 }
42684282 res . header ( 'X-Export-Format' , format ) ;
42694283 res . header ( 'X-Export-Limit' , String ( limit ) ) ;
4284+ // Signal whether select-option colours were applied. Only
4285+ // meaningful for xlsx; 'dropped' means the limit exceeded the
4286+ // style cap so the workbook is colourless but complete.
4287+ if ( format === 'xlsx' ) res . header ( 'X-Export-Styles' , styled ? 'applied' : 'dropped' ) ;
42704288 res . header ( 'Cache-Control' , 'no-store' ) ;
42714289
42724290 let exported = 0 ;
42734291 let firstChunk = true ;
42744292 let skip = 0 ;
42754293 if ( format === 'json' ) res . write ( '[' ) ;
4276- const xlsx = format === 'xlsx' ? await createXlsxStream ( res ) : null ;
4294+ const xlsx = format === 'xlsx' ? await createXlsxStream ( res , styled ) : null ;
42774295
42784296 while ( exported < limit ) {
42794297 const take = Math . min ( chunkSize , limit - exported ) ;
@@ -4312,8 +4330,16 @@ export class RestServer {
43124330 if ( firstChunk && includeHeader ) {
43134331 xlsx ! . ws . addRow ( ( fields ?? [ ] ) . map ( ( f ) => headerLabel ( f , metaMap ) ) ) . commit ( ) ;
43144332 }
4333+ const cols = fields ?? [ ] ;
43154334 for ( const row of rows ) {
4316- xlsx ! . ws . addRow ( formatRowCells ( row , fields ?? [ ] , metaMap ) ) . commit ( ) ;
4335+ const r = xlsx ! . ws . addRow ( formatRowCells ( row , cols , metaMap ) ) ;
4336+ if ( styled ) {
4337+ cols . forEach ( ( f , i ) => {
4338+ const argb = cellFontColor ( row ?. [ f ] , metaMap . get ( f ) ) ;
4339+ if ( argb ) r . getCell ( i + 1 ) . font = { color : { argb } } ;
4340+ } ) ;
4341+ }
4342+ r . commit ( ) ;
43174343 }
43184344 } else {
43194345 for ( let i = 0 ; i < rows . length ; i ++ ) {
0 commit comments