Skip to content

Commit ece3706

Browse files
authored
217 advanced clipboard copy for datagrids (#563)
* adds advanced clipboard copy for datagrids refs: #217 * adds copy selected or all table data, handles case where column names "?column?"
1 parent 9caa3cc commit ece3706

File tree

1 file changed

+136
-15
lines changed

1 file changed

+136
-15
lines changed

pgmanage/app/static/pgmanage_frontend/src/components/QueryResultTabs.vue

Lines changed: 136 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export default {
133133
table: null,
134134
heightSubtract: 200,
135135
colWidthArray: [],
136+
columns: []
136137
};
137138
},
138139
computed: {
@@ -206,6 +207,107 @@ export default {
206207
this.handleResize();
207208
},
208209
methods: {
210+
copyTableData(format) {
211+
const selectedData = this.getSelectedDataInDisplayOrder();
212+
const data = selectedData.length > 0 ? selectedData : this.table.getData();
213+
const headers = this.columns;
214+
215+
if (format === "json") {
216+
const jsonOutput = this.generateJson(data, headers);
217+
this.copyToClipboard(jsonOutput);
218+
} else if (format === "csv") {
219+
const csvOutput = this.generateCsv(data, headers);
220+
this.copyToClipboard(csvOutput);
221+
} else if (format === "markdown") {
222+
const markdownOutput = this.generateMarkdown(data, headers);
223+
this.copyToClipboard(markdownOutput);
224+
}
225+
},
226+
getSelectedDataInDisplayOrder() {
227+
const rowComponents = this.table.getSelectedRows();
228+
229+
const rowsWithPosition = rowComponents.map((row) => ({
230+
data: row.getData(),
231+
position: row.getPosition(),
232+
}));
233+
234+
rowsWithPosition.sort((a, b) => a.position - b.position);
235+
236+
return rowsWithPosition.map((row) => row.data);
237+
},
238+
copyToClipboard(text) {
239+
navigator.clipboard
240+
.writeText(text)
241+
.then(() => {
242+
})
243+
.catch((error) => {
244+
showToast("error", error);
245+
});
246+
},
247+
generateJson(data, headers) {
248+
const columns = headers.map((col, index) => ({
249+
field: index,
250+
title: col,
251+
}));
252+
253+
const mappedData = data.map((row) => {
254+
const mappedRow = {};
255+
columns.forEach((col) => {
256+
mappedRow[col.title] = row[col.field];
257+
});
258+
return mappedRow;
259+
});
260+
261+
return JSON.stringify(mappedData, null, 2);
262+
},
263+
generateCsv(data, headers) {
264+
const csvRows = [];
265+
266+
// Add header row
267+
csvRows.push(headers.join(settingsStore.csvDelimiter));
268+
269+
data.forEach((row) => {
270+
csvRows.push(row.join(settingsStore.csvDelimiter));
271+
});
272+
273+
return csvRows.join("\n");
274+
},
275+
generateMarkdown(data, headers) {
276+
const columnWidths = headers.map((header, index) => {
277+
const maxDataLength = data.reduce(
278+
(max, row) => Math.max(max, (row[index] || "").toString().length),
279+
0
280+
);
281+
return Math.max(header.length, maxDataLength);
282+
});
283+
284+
// Helper to pad strings to a given length
285+
const pad = (str, length) => str.toString().padEnd(length, " ");
286+
287+
const mdRows = [];
288+
289+
// Add padded header row
290+
mdRows.push(
291+
`| ${headers
292+
.map((header, index) => pad(header, columnWidths[index]))
293+
.join(" | ")} |`
294+
);
295+
296+
// Add separator row
297+
mdRows.push(
298+
`| ${columnWidths.map((width) => "-".repeat(width)).join(" | ")} |`
299+
);
300+
301+
data.forEach((row) => {
302+
mdRows.push(
303+
`| ${row
304+
.map((cell, index) => pad(cell || "", columnWidths[index]))
305+
.join(" | ")} |`
306+
);
307+
});
308+
309+
return mdRows.join("\n");
310+
},
209311
cellFormatter(cell, params, onRendered) {
210312
let cellVal = cell.getValue()
211313
if (isNil(cellVal)) {
@@ -304,23 +406,42 @@ export default {
304406
this.updateTableData(data);
305407
},
306408
prepareColumns(colNames, colTypes) {
307-
let cellContextMenu = [
308-
{
309-
label:
310-
'<div style="position: absolute;"><i class="fas fa-copy cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">Copy</div>',
311-
action: function (e, cell) {
312-
cell.getTable().copyToClipboard("selected");
409+
this.columns = colNames.map((colName, idx) => {
410+
return colName === '?column?' ? `column-${idx}` : colName
411+
})
412+
let cellContextMenu = () => {
413+
const isAnyRowsSelected = !!this.table.getSelectedData().length;
414+
const copyText = `${isAnyRowsSelected ? "selected" : "table data"}`
415+
return [
416+
{
417+
label: `<div style="position: absolute;"><i class="fas fa-copy cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">Copy ${copyText} as JSON</div>`,
418+
action: () => this.copyTableData("json"),
313419
},
314-
},
315-
{
316-
label:
317-
'<div style="position: absolute;"><i class="fas fa-edit cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">View Content</div>',
318-
action: (e, cell) => {
319-
cellDataModalStore.showModal(cell.getValue())
420+
{
421+
label: `<div style="position: absolute;"><i class="fas fa-copy cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">Copy ${copyText} as CSV</div>`,
422+
action: () => this.copyTableData("csv"),
320423
},
321-
},
322-
];
323-
let columns = colNames.map((col, idx) => {
424+
{
425+
label: `<div style="position: absolute;"><i class="fas fa-copy cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">Copy ${copyText} as Markdown</div>`,
426+
action: () => this.copyTableData("markdown"),
427+
},
428+
{
429+
label:
430+
'<div style="position: absolute;"><i class="fas fa-copy cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">Copy</div>',
431+
action: function (e, cell) {
432+
cell.getTable().copyToClipboard("selected");
433+
},
434+
},
435+
{
436+
label:
437+
'<div style="position: absolute;"><i class="fas fa-edit cm-all" style="vertical-align: middle;"></i></div><div style="padding-left: 30px;">View Content</div>',
438+
action: (e, cell) => {
439+
cellDataModalStore.showModal(cell.getValue())
440+
},
441+
},
442+
];
443+
}
444+
let columns = this.columns.map((col, idx) => {
324445
let formatTitle = function(col, idx) {
325446
if(colTypes?.length === 0 )
326447
return col

0 commit comments

Comments
 (0)