thatcher 1.0.50 → 1.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ui/csv-export.js +29 -0
- package/src/ui/grid-view-renderer.js +50 -4
- package/src/ui/page-handler.js +10 -2
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getColumns } from '@/ui/grid-view-renderer.js';
|
|
2
|
+
|
|
3
|
+
const FORMULA_PREFIXES = ['=', '+', '-', '@'];
|
|
4
|
+
|
|
5
|
+
function neutralizeFormula(str) {
|
|
6
|
+
if (str.length && FORMULA_PREFIXES.includes(str[0])) return `'${str}`;
|
|
7
|
+
return str;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function csvCell(value) {
|
|
11
|
+
if (value === null || value === undefined) return '';
|
|
12
|
+
let str = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
13
|
+
str = neutralizeFormula(str);
|
|
14
|
+
if (/[",\n\r]/.test(str)) str = `"${str.replace(/"/g, '""')}"`;
|
|
15
|
+
return str;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildCsv(entityName, spec, records) {
|
|
19
|
+
const columns = getColumns(spec);
|
|
20
|
+
const header = columns.map(([key, field]) => csvCell(field?.label || key)).join(',');
|
|
21
|
+
const rows = records.map(item =>
|
|
22
|
+
columns.map(([key]) => csvCell(item[key])).join(',')
|
|
23
|
+
);
|
|
24
|
+
return [header, ...rows].join('\r\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function csvFilename(entityName) {
|
|
28
|
+
return `${entityName}-export-${Date.now()}.csv`;
|
|
29
|
+
}
|
|
@@ -4,7 +4,7 @@ import { getDefaultSort, getAvailableFilters, getPageSize, getEntityLabel } from
|
|
|
4
4
|
|
|
5
5
|
const EMBEDDED_TYPES = new Set(['json', 'embedded']);
|
|
6
6
|
|
|
7
|
-
function getColumns(spec) {
|
|
7
|
+
export function getColumns(spec) {
|
|
8
8
|
const fields = spec?.fields || {};
|
|
9
9
|
const override = spec?.list?.columns;
|
|
10
10
|
if (Array.isArray(override) && override.length) {
|
|
@@ -25,12 +25,58 @@ function gridRow(entityName, item, columns) {
|
|
|
25
25
|
const cells = columns.map(([key, field]) => {
|
|
26
26
|
const value = item[key];
|
|
27
27
|
const rendered = fmtVal(value, key, item);
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const editableAttrs = isEditable(field)
|
|
29
|
+
? ` data-editable="${esc(key)}" data-entity="${esc(entityName)}" data-record-id="${esc(item.id)}"`
|
|
30
|
+
: '';
|
|
31
|
+
return `<td data-col="${esc(key)}"${editableAttrs}>${rendered}</td>`;
|
|
30
32
|
}).join('');
|
|
31
33
|
return `<tr data-row data-navigate="/${esc(entityName)}/${esc(item.id)}" style="cursor:pointer">${cells}</tr>`;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const GRID_EDIT_SCRIPT = `(function(){
|
|
37
|
+
function commitCell(td){
|
|
38
|
+
var input=td.querySelector('input,select');
|
|
39
|
+
if(!input)return;
|
|
40
|
+
var value=input.value;
|
|
41
|
+
var field=td.dataset.editable, entity=td.dataset.entity, id=td.dataset.recordId;
|
|
42
|
+
var original=td.dataset.originalHtml;
|
|
43
|
+
fetch('/api/'+entity+'/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({[field]:value})})
|
|
44
|
+
.then(function(r){if(!r.ok)throw new Error('Save failed');return r.json()})
|
|
45
|
+
.then(function(){location.reload()})
|
|
46
|
+
.catch(function(err){td.innerHTML=original;if(window.showToast)showToast(err.message,'error')});
|
|
47
|
+
}
|
|
48
|
+
function cancelCell(td){
|
|
49
|
+
var original=td.dataset.originalHtml;
|
|
50
|
+
if(original!==undefined)td.innerHTML=original;
|
|
51
|
+
}
|
|
52
|
+
document.addEventListener('dblclick',function(e){
|
|
53
|
+
var td=e.target.closest('[data-editable]');
|
|
54
|
+
if(!td||td.querySelector('input,select'))return;
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
var field=td.dataset.editable;
|
|
57
|
+
var current=(td.textContent||'').trim();
|
|
58
|
+
td.dataset.originalHtml=td.innerHTML;
|
|
59
|
+
var input=document.createElement('input');
|
|
60
|
+
input.type='text';
|
|
61
|
+
input.value=current;
|
|
62
|
+
input.className='grid-cell-input';
|
|
63
|
+
td.innerHTML='';
|
|
64
|
+
td.appendChild(input);
|
|
65
|
+
input.focus();
|
|
66
|
+
input.select();
|
|
67
|
+
input.addEventListener('blur',function(){commitCell(td)});
|
|
68
|
+
input.addEventListener('keydown',function(ke){
|
|
69
|
+
if(ke.key==='Enter'){ke.preventDefault();input.blur()}
|
|
70
|
+
else if(ke.key==='Escape'){ke.preventDefault();cancelCell(td)}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
document.addEventListener('click',function(e){
|
|
74
|
+
if(e.target.closest('[data-editable]')&&e.target.closest('[data-editable]').querySelector('input,select')){
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
}
|
|
77
|
+
},true);
|
|
78
|
+
})();`;
|
|
79
|
+
|
|
34
80
|
export function renderGridView(user, entityName, spec, records, options = {}) {
|
|
35
81
|
const label = getEntityLabel(spec, true) || entityName;
|
|
36
82
|
const columns = getColumns(spec);
|
|
@@ -74,5 +120,5 @@ export function renderGridView(user, entityName, spec, records, options = {}) {
|
|
|
74
120
|
</table>
|
|
75
121
|
</div>`;
|
|
76
122
|
|
|
77
|
-
return page(user, `${label} | Thatcher`, null, content, [TABLE_SCRIPT]);
|
|
123
|
+
return page(user, `${label} | Thatcher`, null, content, [TABLE_SCRIPT, GRID_EDIT_SCRIPT]);
|
|
78
124
|
}
|
package/src/ui/page-handler.js
CHANGED
|
@@ -224,11 +224,19 @@ export async function handlePage(pathname, req, res) {
|
|
|
224
224
|
let items = await list(entityName, {});
|
|
225
225
|
if (isClientUser(user) && user.client_id) items = items.filter(item => { if (item.client_id) return item.client_id === user.client_id; if (item.assigned_to) return item.assigned_to === user.id; return true; });
|
|
226
226
|
items = resolveRefFields(items, spec);
|
|
227
|
-
const
|
|
227
|
+
const params = reqUrl(req).searchParams;
|
|
228
|
+
if (params.get('export') === 'csv') {
|
|
229
|
+
const { buildCsv, csvFilename } = await lazyRenderer('csv-export.js');
|
|
230
|
+
const csv = buildCsv(entityName, spec, items);
|
|
231
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
232
|
+
res.setHeader('Content-Disposition', `attachment; filename="${csvFilename(entityName)}"`);
|
|
233
|
+
res.setHeader('Content-Length', Buffer.byteLength(csv, 'utf-8'));
|
|
234
|
+
res.writeHead(200); res.end(csv); return 'HANDLED';
|
|
235
|
+
}
|
|
236
|
+
const view = params.get('view');
|
|
228
237
|
if (view === 'board') return renderBoardView(user, entityName, spec, items);
|
|
229
238
|
if (view === 'grid') return renderGridView(user, entityName, spec, items);
|
|
230
239
|
if (view === 'calendar') {
|
|
231
|
-
const params = reqUrl(req).searchParams;
|
|
232
240
|
const month = params.has('month') ? Number(params.get('month')) : undefined;
|
|
233
241
|
const year = params.has('year') ? Number(params.get('year')) : undefined;
|
|
234
242
|
return renderCalendarView(user, entityName, spec, items, { month, year });
|