@@ -14,6 +14,7 @@ import { FormsModule } from '@angular/forms';
1414import { RouterModule } from '@angular/router' ;
1515import { BackendService } from '@lib/services' ;
1616import { TruncatePipe } from '@lib/providers/truncate.pipe' ;
17+ import { firstValueFrom } from 'rxjs' ;
1718
1819@Component ( {
1920 selector : 'app-resultgrid' ,
@@ -48,6 +49,9 @@ export class ResultGridComponent implements OnInit {
4849
4950 queryResults : any [ ] = [ ] ; // data.queries[]
5051 activeQueryIndex : number = 0 ;
52+ showExportModal : boolean = false ;
53+ exportScope : 'current' | 'all' = 'current' ;
54+ exportFormat : 'csv' | 'json' = 'csv' ;
5155
5256 // Convenience getter for template access
5357 get activeResult ( ) : any | null {
@@ -362,4 +366,215 @@ export class ResultGridComponent implements OnInit {
362366 }
363367 return 'string' ;
364368 }
369+
370+ openExportModal ( ) : void {
371+ this . exportScope = 'current' ;
372+ this . showExportModal = true ;
373+ this . _cdr . markForCheck ( ) ;
374+ }
375+
376+ closeExportModal ( ) : void {
377+ this . showExportModal = false ;
378+ this . _cdr . markForCheck ( ) ;
379+ }
380+
381+ async confirmExport ( format : 'json' | 'csv' | 'excel' ) : Promise < void > {
382+ let data : any [ ] = [ ] ;
383+
384+ if ( this . exportScope === 'all' ) {
385+ // prefer already-available full payload
386+ const active = this . activeResult ;
387+ if ( active ?. allRows && Array . isArray ( active . allRows ) && active . allRows . length ) {
388+ data = active . allRows ;
389+ } else {
390+ // try to fetch all pages from backend
391+ data = await this . fetchAllRows ( ) ;
392+ }
393+ } else {
394+ data = this . rows || [ ] ;
395+ }
396+
397+ if ( ! data || data . length === 0 ) {
398+ window . alert ( 'No rows available to export.' ) ;
399+ this . closeExportModal ( ) ;
400+ return ;
401+ }
402+
403+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) ;
404+ const scope = this . exportScope === 'all' ? 'all' : 'current' ;
405+ const baseName = `result-${ scope } -${ timestamp } ` ;
406+
407+ if ( format === 'json' ) {
408+ const blob = new Blob ( [ this . convertToJSON ( data ) ] , { type : 'application/json;charset=utf-8' } ) ;
409+ this . downloadFile ( `${ baseName } .json` , blob ) ;
410+ } else if ( format === 'csv' ) {
411+ const csv = this . convertToCSV ( data ) ;
412+ const blob = new Blob ( [ csv ] , { type : 'text/csv;charset=utf-8' } ) ;
413+ this . downloadFile ( `${ baseName } .csv` , blob ) ;
414+ } else if ( format === 'excel' ) {
415+ const html = this . convertToExcel ( data ) ;
416+ const blob = new Blob ( [ html ] , { type : 'application/vnd.ms-excel;charset=utf-8' } ) ;
417+ this . downloadFile ( `${ baseName } .xls` , blob ) ;
418+ }
419+
420+ this . closeExportModal ( ) ;
421+ }
422+
423+ private getDataForExport ( ) : any [ ] {
424+ // export current grid rows for "page"
425+ if ( this . exportScope === 'current' ) {
426+ return this . rows || [ ] ;
427+ }
428+
429+ // export "full" - try to use any available full result payload
430+ const active = this . queryResults [ this . activeQueryIndex ] ;
431+ // If server provided full data field (custom), respect it
432+ if ( active ?. allRows && Array . isArray ( active . allRows ) && active . allRows . length > 0 ) {
433+ return active . allRows ;
434+ }
435+
436+ // If pagination indicates more results on server, inform user and fallback to current page
437+ if ( active ?. pagination ?. hasMore ) {
438+ // keep short / impersonal
439+ window . alert ( 'Full result set is not fully loaded; exporting current page only.' ) ;
440+ return this . rows || [ ] ;
441+ }
442+
443+ // fallback to whatever rows are present
444+ return active ?. rows || this . rows || [ ] ;
445+ }
446+
447+ private convertToJSON ( data : any [ ] ) : string {
448+ return JSON . stringify ( data , null , 2 ) ;
449+ }
450+
451+ private convertToCSV ( data : any [ ] ) : string {
452+ if ( ! data || data . length === 0 ) return '' ;
453+
454+ const cols = this . headers && this . headers . length > 0 ? this . headers : Object . keys ( data [ 0 ] || { } ) ;
455+ const escape = ( v : any ) => {
456+ if ( v === null || v === undefined ) return '' ;
457+ const s = String ( v ) ;
458+ // escape double quotes
459+ if ( s . includes ( '"' ) || s . includes ( ',' ) || s . includes ( '\n' ) || s . includes ( '\r' ) ) {
460+ return `"${ s . replace ( / " / g, '""' ) } "` ;
461+ }
462+ return s ;
463+ } ;
464+
465+ const lines = [ ] ;
466+ lines . push ( cols . join ( ',' ) ) ;
467+ for ( const row of data ) {
468+ const vals = cols . map ( ( c ) => escape ( row [ c ] ) ) ;
469+ lines . push ( vals . join ( ',' ) ) ;
470+ }
471+ return lines . join ( '\r\n' ) ;
472+ }
473+
474+ private convertToExcel ( data : any [ ] ) : string {
475+ // Build a minimal HTML table which Excel can open
476+ const cols = this . headers && this . headers . length > 0 ? this . headers : Object . keys ( data [ 0 ] || { } ) ;
477+ const headerRow = cols . map ( ( c ) => `<th style="border:1px solid #ccc;padding:4px;background:#f0f0f0;">${ this . escapeHtml ( c ) } </th>` ) . join ( '' ) ;
478+ const bodyRows = data
479+ . map ( ( row ) => {
480+ const cells = cols
481+ . map ( ( c ) => `<td style="border:1px solid #ccc;padding:4px;">${ this . escapeHtml ( row [ c ] ) } </td>` )
482+ . join ( '' ) ;
483+ return `<tr>${ cells } </tr>` ;
484+ } )
485+ . join ( '' ) ;
486+ return `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><table border="0" cellpadding="0" cellspacing="0">${ '<thead><tr>' + headerRow + '</tr></thead>' } <tbody>${ bodyRows } </tbody></table></body></html>` ;
487+ }
488+
489+ private escapeHtml ( value : any ) : string {
490+ if ( value === null || value === undefined ) return '' ;
491+ return String ( value )
492+ . replace ( / & / g, '&' )
493+ . replace ( / < / g, '<' )
494+ . replace ( / > / g, '>' )
495+ . replace ( / " / g, '"' ) ;
496+ }
497+
498+ private downloadFile ( filename : string , blob : Blob ) : void {
499+ const url = URL . createObjectURL ( blob ) ;
500+ const a = document . createElement ( 'a' ) ;
501+ a . href = url ;
502+ a . download = filename ;
503+ document . body . appendChild ( a ) ;
504+ a . click ( ) ;
505+ a . remove ( ) ;
506+ URL . revokeObjectURL ( url ) ;
507+ }
508+
509+ private async fetchAllRows ( ) : Promise < any [ ] > {
510+ const active = this . activeResult ;
511+ // choose base SQL: prefer the active statement, else fall back to original triggerQuery
512+ const query = active ?. query || this . triggerQuery ;
513+ const pageSize = active ?. pagination ?. pageSize || this . pageSize ;
514+ const totalPagesFromMeta = active ?. pagination ?. totalPages ;
515+ const totalRowsFromMeta = active ?. totalRows || this . totalRows ;
516+
517+ // compute pages to fetch
518+ let totalPages = 1 ;
519+ if ( typeof totalPagesFromMeta === 'number' && totalPagesFromMeta > 0 ) {
520+ totalPages = totalPagesFromMeta ;
521+ } else if ( totalRowsFromMeta && pageSize ) {
522+ totalPages = Math . ceil ( totalRowsFromMeta / pageSize ) ;
523+ }
524+
525+ // safety cap
526+ const MAX_PAGES = 1000 ;
527+ totalPages = Math . min ( totalPages || 1 , MAX_PAGES ) ;
528+
529+ const allRows : any [ ] = [ ] ;
530+
531+ for ( let p = 1 ; p <= totalPages ; p ++ ) {
532+ try {
533+ const resp : any = await firstValueFrom (
534+ this . _dbService . executeQuery ( query , this . dbName , { page : p , pageSize } ) ,
535+ ) ;
536+
537+ let pageRows : any [ ] = [ ] ;
538+
539+ if ( ! resp ) {
540+ break ;
541+ }
542+
543+ // handle multi-query response vs single-query
544+ if ( Array . isArray ( resp . queries ) ) {
545+ // try to find matching statement by text, fallback to first
546+ const found = resp . queries . find ( ( q : any ) => q . query === query ) || resp . queries [ 0 ] ;
547+ pageRows = Array . isArray ( found ?. rows ) ? found . rows : [ ] ;
548+ } else {
549+ pageRows = Array . isArray ( resp . rows ) ? resp . rows : [ ] ;
550+ }
551+
552+ if ( pageRows . length > 0 ) {
553+ allRows . push ( ...pageRows ) ;
554+ }
555+
556+ // if server indicates no more pages, break early
557+ const pageMeta =
558+ Array . isArray ( resp . queries )
559+ ? ( resp . queries . find ( ( q : any ) => q . query === query ) || resp . queries [ 0 ] ) ?. pagination
560+ : resp ?. pagination ;
561+ if ( pageMeta && pageMeta . hasMore === false ) {
562+ break ;
563+ }
564+
565+ // If server returned fewer rows than pageSize, assume last page
566+ if ( pageRows . length < pageSize ) {
567+ break ;
568+ }
569+ } catch ( err ) {
570+ // stop on error and return what we have
571+ console . error ( 'Error fetching page' , p , err ) ;
572+ break ;
573+ }
574+ }
575+
576+ return allRows ;
577+ }
578+
579+
365580}
0 commit comments