Skip to content

Commit 404633e

Browse files
delchevclaude
andauthored
feat(harmonia): Export CSV + Print on every list surface (manage, list, master, personal, partner) (#6354)
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 <noreply@anthropic.com>
1 parent 89fc5fe commit 404633e

15 files changed

Lines changed: 218 additions & 5 deletions

File tree

components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/basePage.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,63 @@ function basePage() {
3333
formatNumber(value, pattern) {
3434
return window.HarmoniaFormat.number(value, pattern);
3535
},
36+
37+
/**
38+
* Translate a column header: columns carry an optional i18next key (tkey) next to the
39+
* design-time label, exactly like the table headers render them.
40+
*/
41+
columnHeader(col) {
42+
return (window.T && col.tkey) ? window.T(col.tkey, col.label) : col.label;
43+
},
44+
45+
/**
46+
* Download rows as a CSV file. Values come from cellText(row, col) - the SAME resolver the
47+
* table cells use, so FK columns carry their referenced labels and dates their formatted
48+
* form, never raw ids or serialized arrays. The BOM makes Excel decode UTF-8 (Cyrillic
49+
* included) without an import wizard.
50+
*/
51+
exportRowsCsv(rows, columns, cellText, filename) {
52+
const esc = (v) => {
53+
const s = String(v == null ? '' : v);
54+
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
55+
};
56+
const head = columns.map((c) => esc(this.columnHeader(c))).join(',');
57+
const lines = rows.map((r) => columns.map((c) => esc(cellText(r, c))).join(','));
58+
const blob = new Blob(['\ufeff' + [head].concat(lines).join('\r\n')], { type: 'text/csv;charset=utf-8' });
59+
const a = document.createElement('a');
60+
a.href = URL.createObjectURL(blob);
61+
a.download = filename;
62+
a.click();
63+
URL.revokeObjectURL(a.href);
64+
},
65+
66+
/**
67+
* Print rows as a minimal table document in a new window (the browser dialog covers paper and
68+
* Save as PDF). Same data path as the CSV export: the full filtered set through cellText.
69+
*/
70+
printRows(rows, columns, cellText, title) {
71+
const esc = (v) => String(v == null ? '' : v)
72+
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
73+
const th = columns.map((c) =>
74+
'<th class="' + (c.number ? 'text-right' : '') + '">' + esc(this.columnHeader(c)) + '</th>').join('');
75+
const body = rows.map((r) => '<tr>' + columns.map((c) =>
76+
'<td class="' + (c.number ? 'text-right' : '') + '">' + esc(cellText(r, c)) + '</td>').join('') + '</tr>').join('');
77+
const html = '<!doctype html><html><head><title>' + esc(title) + '</title><style>'
78+
+ 'body{font-family:system-ui,-apple-system,sans-serif;margin:24px;color:#111}'
79+
+ 'h1{font-size:18px;margin:0 0 4px}'
80+
+ '.meta{font-size:11px;color:#555;margin:0 0 16px}'
81+
+ 'table{border-collapse:collapse;width:100%;font-size:12px}'
82+
+ 'th,td{border:1px solid #999;padding:4px 8px;text-align:left}'
83+
+ 'th{background:#eee}.text-right{text-align:right}'
84+
+ '</style></head><body><h1>' + esc(title) + '</h1>'
85+
+ '<p class="meta">' + rows.length + ' rows - ' + esc(new Date().toLocaleString()) + '</p>'
86+
+ '<table><thead><tr>' + th + '</tr></thead><tbody>' + body + '</tbody></table></body></html>';
87+
const w = window.open('', '_blank');
88+
if (!w) return;
89+
w.document.write(html);
90+
w.document.close();
91+
w.focus();
92+
w.print();
93+
},
3694
};
3795
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ document.addEventListener('alpine:init', () => {
114114
#end
115115
],
116116

117+
// ----- Export / print of the own rows: same columns and cell resolution as the table
118+
// (FK labels, status text, formatted dates) via the shared basePage helpers.
119+
exportCell(row, col) {
120+
if (row[col.name] == null || row[col.name] === '') return '';
121+
return col.status ? this.statusText(row) : this.display(row, col);
122+
},
123+
exportCsv() {
124+
this.exportRowsCsv(this.items, this.columns, (r, c) => this.exportCell(r, c), '${name}.csv');
125+
},
126+
printList() {
127+
this.printRows(this.items, this.columns, (r, c) => this.exportCell(r, c),
128+
'My ' + (window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}'));
129+
},
130+
117131
newEntity() { this.navigate('/my/${name}/create'); },
118132
openEntity(row) { this.navigate('/my/${name}/' + row.#foreach($property in $properties)#if($property.dataPrimaryKey)${property.name}#end#end + '/edit'); },
119133
}));

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
<i role="img" x-h-lucide data-lucide="plus"></i>
1212
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
1313
</button>
14+
<!-- Export the own rows as CSV / print them (Save as PDF via the browser dialog). -->
15+
<button x-h-button data-variant="outline" data-size="sm" :disabled="items.length === 0" @click="exportCsv()">
16+
<i role="img" x-h-lucide data-lucide="download"></i><span x-text="T('$projectName:${tprefix}.defaults.export', 'Export')"></span>
17+
</button>
18+
<button x-h-button data-variant="outline" data-size="sm" :disabled="items.length === 0" @click="printList()">
19+
<i role="img" x-h-lucide data-lucide="printer"></i><span x-text="T('$projectName:${tprefix}.defaults.print', 'Print')"></span>
20+
</button>
1421
</div>
1522

1623
<div x-show="state === 'loading'" class="p-4"><div x-h-spinner></div></div>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ document.addEventListener('alpine:init', () => {
8686
#end
8787
],
8888

89+
// ----- Export / print of the partner's own rows: same columns and cell resolution as the
90+
// table (status text, formatted dates) via the shared basePage helpers.
91+
exportCell(row, col) {
92+
if (row[col.name] == null || row[col.name] === '') return '';
93+
return col.status ? this.statusText(row) : this.display(row, col);
94+
},
95+
exportCsv() {
96+
this.exportRowsCsv(this.items, this.columns, (r, c) => this.exportCell(r, c), '${name}.csv');
97+
},
98+
printList() {
99+
this.printRows(this.items, this.columns, (r, c) => this.exportCell(r, c),
100+
window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}');
101+
},
102+
89103
newEntity() { this.navigate('/partner/${name}/create'); },
90104
openEntity(row) { this.navigate('/partner/${name}/' + row.#foreach($property in $properties)#if($property.dataPrimaryKey)${property.name}#end#end + '/edit'); },
91105
}));

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
<i role="img" x-h-lucide data-lucide="plus"></i>
1212
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
1313
</button>
14+
<!-- Export the own rows as CSV / print them (Save as PDF via the browser dialog). -->
15+
<button x-h-button data-variant="outline" data-size="sm" :disabled="items.length === 0" @click="exportCsv()">
16+
<i role="img" x-h-lucide data-lucide="download"></i><span x-text="T('$projectName:${tprefix}.defaults.export', 'Export')"></span>
17+
</button>
18+
<button x-h-button data-variant="outline" data-size="sm" :disabled="items.length === 0" @click="printList()">
19+
<i role="img" x-h-lucide data-lucide="printer"></i><span x-text="T('$projectName:${tprefix}.defaults.print', 'Print')"></span>
20+
</button>
1421
</div>
1522

1623
<div x-show="state === 'loading'" class="p-4"><div x-h-spinner></div></div>

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,27 @@ document.addEventListener('alpine:init', () => {
165165
this.currentPage = Math.max(1, Math.min(n, this.totalPages));
166166
this.refreshIcons();
167167
},
168+
169+
// ----- Export / print: the FULL filtered+sorted set (not the current page), cells resolved
170+
// exactly like the table (FK labels, formatted dates) via the shared basePage helpers.
171+
exportColumns: [
172+
#foreach($property in $properties)
173+
#if(!$property.dataAutoIncrement && $property.widgetIsMajor)
174+
{ 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 },
175+
#end
176+
#end
177+
],
178+
exportCell(row, col) {
179+
const v = row[col.name];
180+
if (v == null || v === '') return '';
181+
return col.fk ? this.lookupText(col.name, v) : this.displayValue(v, !!col.date);
182+
},
183+
exportCsv() {
184+
this.exportRowsCsv(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c), '${name}.csv');
185+
},
186+
printList() {
187+
this.printRows(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c),
188+
window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}');
189+
},
168190
}));
169191
}, { once: true });

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
<svg x-h-icon data-icon="search" class="size-4" role="img" aria-label="search"></svg>
1616
</div>
1717
</div>
18+
<!-- Export the filtered rows as CSV / print them (Save as PDF via the browser dialog). -->
19+
<button x-h-button data-variant="outline" data-size="sm" :disabled="filteredItems.length === 0" @click="exportCsv()">
20+
<i role="img" x-h-lucide data-lucide="download"></i><span x-text="T('$projectName:${tprefix}.defaults.export', 'Export')"></span>
21+
</button>
22+
<button x-h-button data-variant="outline" data-size="sm" :disabled="filteredItems.length === 0" @click="printList()">
23+
<i role="img" x-h-lucide data-lucide="printer"></i><span x-text="T('$projectName:${tprefix}.defaults.print', 'Print')"></span>
24+
</button>
1825
<!-- Developer-contributed page actions for this view (customActions extension point). -->
1926
<template x-for="action in $store.customActions.getActions(caView, 'page')" :key="action.id">
2027
<button x-h-button data-variant="outline" data-size="sm" @click="$store.customActions.trigger(action)">

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/list-page.js.template

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,28 @@ document.addEventListener('alpine:init', () => {
267267

268268
goPage(n) { this.currentPage = Math.max(1, Math.min(n, this.totalPages)); this.refreshIcons(); },
269269

270+
// ----- Export / print: the FULL filtered+sorted set (not the current page), cells resolved
271+
// exactly like the table (FK labels, formatted dates) via the shared basePage helpers.
272+
exportColumns: [
273+
#foreach($property in $properties)
274+
#if(!$property.dataAutoIncrement && $property.widgetIsMajor)
275+
{ 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 },
276+
#end
277+
#end
278+
],
279+
exportCell(row, col) {
280+
const v = row[col.name];
281+
if (v == null || v === '') return '';
282+
return col.fk ? this.lookupText(col.name, v) : this.displayValue(v, !!col.date);
283+
},
284+
exportCsv() {
285+
this.exportRowsCsv(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c), '${name}.csv');
286+
},
287+
printList() {
288+
this.printRows(this.sortedItems, this.exportColumns, (r, c) => this.exportCell(r, c),
289+
window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}');
290+
},
291+
270292
newEntity() { window.PineconeRouter.navigate('/${name}/create'); },
271293
editEntity(row) { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(row.${primaryKeysString}) + '/edit'); },
272294
previewEntity(row) { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(row.${primaryKeysString}) + '/preview'); },

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/list-view.html.template

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
<i role="img" x-h-lucide data-lucide="plus"></i>
2020
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
2121
</button>
22+
<!-- Export the filtered rows as CSV / print them (Save as PDF via the browser dialog). -->
23+
<button x-h-button data-variant="outline" data-size="sm" :disabled="sortedItems.length === 0" @click="exportCsv()">
24+
<i role="img" x-h-lucide data-lucide="download"></i><span x-text="T('$projectName:${tprefix}.defaults.export', 'Export')"></span>
25+
</button>
26+
<button x-h-button data-variant="outline" data-size="sm" :disabled="sortedItems.length === 0" @click="printList()">
27+
<i role="img" x-h-lucide data-lucide="printer"></i><span x-text="T('$projectName:${tprefix}.defaults.print', 'Print')"></span>
28+
</button>
2229
<!-- Developer-contributed page actions for this view (customActions extension point). -->
2330
<template x-for="action in $store.customActions.getActions(caView, 'page')" :key="action.id">
2431
<button x-h-button data-variant="outline" data-size="sm" @click="$store.customActions.trigger(action)">

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/master/master-page.js.template

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ document.addEventListener('alpine:init', () => {
144144
return 'default';
145145
},
146146

147+
// ----- Export / print: the FULL filtered set (not the current page), cells resolved exactly
148+
// like the table (FK labels, formatted dates) via the shared basePage helpers.
149+
exportColumns: [
150+
#foreach($property in $properties)
151+
#if(!$property.dataAutoIncrement && $property.widgetIsMajor)
152+
{ 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 },
153+
#end
154+
#end
155+
],
156+
exportCell(row, col) {
157+
const v = row[col.name];
158+
if (v == null || v === '') return '';
159+
return col.fk ? this.lookupText(col.name, v) : this.displayValue(v, !!col.date);
160+
},
161+
exportCsv() {
162+
this.exportRowsCsv(this.filteredMasters, this.exportColumns, (r, c) => this.exportCell(r, c), '${name}.csv');
163+
},
164+
printList() {
165+
this.printRows(this.filteredMasters, this.exportColumns, (r, c) => this.exportCell(r, c),
166+
window.T ? window.T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}') : '${menuLabel}');
167+
},
168+
147169
newMaster() { window.PineconeRouter.navigate('/${name}/create'); },
148170
editMaster(row) { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(row.${primaryKeysString}) + '/edit'); },
149171
previewMaster(row) { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(row.${primaryKeysString}) + '/preview'); },

0 commit comments

Comments
 (0)