From 9e6f625d0545162f34d1e7268e29bc456155da19 Mon Sep 17 00:00:00 2001 From: delchev Date: Tue, 21 Jul 2026 22:30:15 +0300 Subject: [PATCH] feat(harmonia): Export CSV + Print on every list surface (manage, list, master, personal, partner) Every generated list page gains toolbar Export and Print actions, closing the gap where only documents (.print) and report pages could produce an output file: - shared basePage helpers: exportRowsCsv() downloads the rows as UTF-8 CSV (BOM so Excel decodes localized text, RFC-quoted cells, translated headers) and printRows() renders them into a minimal print window (the browser dialog covers paper and Save as PDF) - one implementation for all shells; - each list template (manage list, plain list, master list, personal my-list, partner list) emits its exportColumns metadata mirroring the table columns and resolves cells exactly like the table: FK columns export their referenced labels, dates their formatted form - never raw ids or serialized arrays; - exports cover the FULL filtered(+sorted) set, not the current page; the personal/partner lists export only the own rows the scoped controller serves (sensitive/owner columns are absent from their column sets by construction); - new defaults.export catalog key; the report table/chart toolbar labels (Refresh/Export/Print) now translate through the same defaults keys; - IntentEmissionCoverageIT asserts the helpers + toolbar buttons on the manage, master, personal and partner list fixtures (ran green locally, 1/1). Co-Authored-By: Claude Fable 5 --- .../shell/js/components/pages/basePage.js | 58 +++++++++++++++++++ .../ui/my/my-list-page.js.template | 14 +++++ .../ui/my/my-list-view.html.template | 7 +++ .../ui/partner/partner-list-page.js.template | 14 +++++ .../partner/partner-list-view.html.template | 7 +++ .../ui/perspective/list/page.js.template | 22 +++++++ .../ui/perspective/list/view.html.template | 7 +++ .../perspective/manage/list-page.js.template | 22 +++++++ .../manage/list-view.html.template | 7 +++ .../master/master-page.js.template | 22 +++++++ .../master/master-view.html.template | 7 +++ .../report/chart-view.html.template | 4 +- .../report/table-view.html.template | 6 +- .../ui/translations.json.template | 1 + .../tests/api/IntentEmissionCoverageIT.java | 25 ++++++++ 15 files changed, 218 insertions(+), 5 deletions(-) diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/basePage.js b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/basePage.js index 07883d93cce..e79b77f99c1 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/basePage.js +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/basePage.js @@ -33,5 +33,63 @@ function basePage() { formatNumber(value, pattern) { return window.HarmoniaFormat.number(value, pattern); }, + + /** + * Translate a column header: columns carry an optional i18next key (tkey) next to the + * design-time label, exactly like the table headers render them. + */ + columnHeader(col) { + return (window.T && col.tkey) ? window.T(col.tkey, col.label) : col.label; + }, + + /** + * Download rows as a CSV file. Values come from cellText(row, col) - the SAME resolver the + * table cells use, so FK columns carry their referenced labels and dates their formatted + * form, never raw ids or serialized arrays. The BOM makes Excel decode UTF-8 (Cyrillic + * included) without an import wizard. + */ + exportRowsCsv(rows, columns, cellText, filename) { + const esc = (v) => { + const s = String(v == null ? '' : v); + return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; + }; + const head = columns.map((c) => esc(this.columnHeader(c))).join(','); + const lines = rows.map((r) => columns.map((c) => esc(cellText(r, c))).join(',')); + const blob = new Blob(['\ufeff' + [head].concat(lines).join('\r\n')], { type: 'text/csv;charset=utf-8' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); + }, + + /** + * Print rows as a minimal table document in a new window (the browser dialog covers paper and + * Save as PDF). Same data path as the CSV export: the full filtered set through cellText. + */ + printRows(rows, columns, cellText, title) { + const esc = (v) => String(v == null ? '' : v) + .replace(/&/g, '&').replace(//g, '>'); + const th = columns.map((c) => + '' + esc(this.columnHeader(c)) + '').join(''); + const body = rows.map((r) => '' + columns.map((c) => + '' + esc(cellText(r, c)) + '').join('') + '').join(''); + const html = '' + esc(title) + '

' + esc(title) + '

' + + '

' + rows.length + ' rows - ' + esc(new Date().toLocaleString()) + '

' + + '' + th + '' + body + '
'; + const w = window.open('', '_blank'); + if (!w) return; + w.document.write(html); + w.document.close(); + w.focus(); + w.print(); + }, }; } diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-page.js.template index d2ee982bf73..b70e3913ba8 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-page.js.template @@ -114,6 +114,20 @@ document.addEventListener('alpine:init', () => { #end ], + // ----- Export / print of the own rows: same columns and cell resolution as the table + // (FK labels, status text, formatted dates) via the shared basePage helpers. + exportCell(row, col) { + if (row[col.name] == null || row[col.name] === '') return ''; + return col.status ? this.statusText(row) : this.display(row, col); + }, + exportCsv() { + this.exportRowsCsv(this.items, this.columns, (r, c) => this.exportCell(r, c), '${name}.csv'); + }, + printList() { + this.printRows(this.items, this.columns, (r, c) => this.exportCell(r, c), + 'My ' + (window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}')); + }, + newEntity() { this.navigate('/my/${name}/create'); }, openEntity(row) { this.navigate('/my/${name}/' + row.#foreach($property in $properties)#if($property.dataPrimaryKey)${property.name}#end#end + '/edit'); }, })); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-view.html.template index 6ba3bd9279d..482791cfe02 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-view.html.template @@ -11,6 +11,13 @@ + + +
diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-page.js.template index fcd56426206..d62f0e8e1ed 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-page.js.template @@ -86,6 +86,20 @@ document.addEventListener('alpine:init', () => { #end ], + // ----- Export / print of the partner's own rows: same columns and cell resolution as the + // table (status text, formatted dates) via the shared basePage helpers. + exportCell(row, col) { + if (row[col.name] == null || row[col.name] === '') return ''; + return col.status ? this.statusText(row) : this.display(row, col); + }, + exportCsv() { + this.exportRowsCsv(this.items, this.columns, (r, c) => this.exportCell(r, c), '${name}.csv'); + }, + printList() { + this.printRows(this.items, this.columns, (r, c) => this.exportCell(r, c), + window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}'); + }, + newEntity() { this.navigate('/partner/${name}/create'); }, openEntity(row) { this.navigate('/partner/${name}/' + row.#foreach($property in $properties)#if($property.dataPrimaryKey)${property.name}#end#end + '/edit'); }, })); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-view.html.template index ca3a4e4b8d0..b4b029243b5 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/partner/partner-list-view.html.template @@ -11,6 +11,13 @@ + + +
diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template index 50a2a80fc2c..9b8a27f9255 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template @@ -165,5 +165,27 @@ document.addEventListener('alpine:init', () => { this.currentPage = Math.max(1, Math.min(n, this.totalPages)); this.refreshIcons(); }, + + // ----- Export / print: the FULL filtered+sorted set (not the current page), cells resolved + // exactly like the table (FK labels, formatted dates) via the shared basePage helpers. + exportColumns: [ +#foreach($property in $properties) +#if(!$property.dataAutoIncrement && $property.widgetIsMajor) + { name: '${property.name}', label: '#if($property.widgetLabel)${property.widgetLabel}#else${property.name}#end', tkey: '$projectName:${tprefix}.t.${property.dataName}'#if($property.widgetType == "DROPDOWN" || $property.widgetType == "DOCUMENT_STATUS"), fk: true#elseif($property.isNumberType), number: true#end#if($property.isDateType), date: true#end }, +#end +#end + ], + exportCell(row, col) { + const v = row[col.name]; + if (v == null || v === '') return ''; + return col.fk ? this.lookupText(col.name, v) : this.displayValue(v, !!col.date); + }, + exportCsv() { + this.exportRowsCsv(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c), '${name}.csv'); + }, + printList() { + this.printRows(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c), + window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}'); + }, })); }, { once: true }); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template index 347f5a5256c..f234ce87ee8 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template @@ -15,6 +15,13 @@ + + +