Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to export colId as header in CSV / XSLX instead of label (#688) #692

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/server/lib/DocApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1205,12 +1205,13 @@ export class DocWorkerApi {
this._app.get('/api/docs/:docId/download/xlsx', canView, withDoc(async (activeDoc, req, res) => {
// Query DB for doc metadata to get the doc title (to use as the filename).
const {name: docTitle} = await this._dbManager.getDoc(req);
const options = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : {
const options: DownloadOptions = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : {
filename: docTitle,
tableId: '',
viewSectionId: undefined,
filters: [],
sortOrder: [],
header: 'label'
};
await downloadXLSX(activeDoc, req, res, options);
}));
Expand Down
7 changes: 6 additions & 1 deletion app/server/lib/Export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {BaseFormatter, createFullFormatterFromDocData} from 'app/common/ValueFor
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {RequestWithLogin} from 'app/server/lib/Authorizer';
import {docSessionFromRequest} from 'app/server/lib/DocSession';
import {optIntegerParam, optJsonParam, stringParam} from 'app/server/lib/requestUtils';
import {optIntegerParam, optJsonParam, optStringParam, stringParam} from 'app/server/lib/requestUtils';
import {ServerColumnGetters} from 'app/server/lib/ServerColumnGetters';
import * as express from 'express';
import * as _ from 'underscore';
Expand Down Expand Up @@ -90,6 +90,8 @@ export interface ExportData {
docSettings: DocumentSettings;
}

export type ExportHeader = 'colId' | 'label';

/**
* Export parameters that identifies a section, filters, sort order.
*/
Expand All @@ -99,6 +101,7 @@ export interface ExportParameters {
sortOrder?: number[];
filters?: Filter[];
linkingFilter?: FilterColValues;
header?: ExportHeader;
}

/**
Expand All @@ -117,13 +120,15 @@ export function parseExportParameters(req: express.Request): ExportParameters {
const sortOrder = optJsonParam(req.query.activeSortSpec, []) as number[];
const filters: Filter[] = optJsonParam(req.query.filters, []);
const linkingFilter: FilterColValues = optJsonParam(req.query.linkingFilter, null);
const header = optStringParam(req.query.header, 'header', {allowed: ['label', 'colId']}) as ExportHeader | undefined;

return {
tableId,
viewSectionId,
sortOrder,
filters,
linkingFilter,
header,
};
}

Expand Down
57 changes: 37 additions & 20 deletions app/server/lib/ExportCSV.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ApiError} from 'app/common/ApiError';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {FilterColValues} from "app/common/ActiveDocAPI";
import {DownloadOptions, ExportData, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import {DownloadOptions, ExportData, ExportHeader, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import log from 'app/server/lib/log';
import * as bluebird from 'bluebird';
import contentDisposition from 'content-disposition';
Expand All @@ -17,11 +17,13 @@ bluebird.promisifyAll(csv);
export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request,
res: express.Response, options: DownloadOptions) {
log.info('Generating .csv file...');
const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter} = options;
const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options;
const data = viewSectionId ?
await makeCSVFromViewSection(
activeDoc, viewSectionId, sortOrder || null, filters || null, linkingFilter || null, req) :
await makeCSVFromTable(activeDoc, tableId, req);
await makeCSVFromViewSection({
activeDoc, viewSectionId, sortOrder: sortOrder || null, filters: filters || null,
linkingFilter: linkingFilter || null, header, req
}) :
await makeCSVFromTable({activeDoc, tableId, header, req});
res.set('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', contentDisposition(filename + '.csv'));
res.send(data);
Expand All @@ -32,36 +34,51 @@ export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request,
*
* See https://github.com/wdavidw/node-csv for API details.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} viewSectionId - id of the viewsection to export.
* @param {Integer[]} activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} filters (optional) - filters defined from ui.
* @param {Object} options - options for the export.
* @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} options.viewSectionId - id of the viewsection to export.
* @param {Integer[]} options.activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} options.filters (optional) - filters defined from ui.
* @param {FilterColValues} options.linkingFilter (optional) - linking filter defined from ui.
* @param {string} options.header (optional) - which field of the column to use as header
* @param {express.Request} options.req - the request object.
*
* @return {Promise<string>} Promise for the resulting CSV.
*/
export async function makeCSVFromViewSection(
export async function makeCSVFromViewSection({
activeDoc, viewSectionId, sortOrder = null, filters = null, linkingFilter = null, header, req
}: {
activeDoc: ActiveDoc,
viewSectionId: number,
sortOrder: number[] | null,
filters: Filter[] | null,
linkingFilter: FilterColValues | null,
req: express.Request) {
header?: ExportHeader,
req: express.Request
}) {

const data = await exportSection(activeDoc, viewSectionId, sortOrder, filters, linkingFilter, req);
const file = convertToCsv(data);
const file = convertToCsv(data, { header });
return file;
}

/**
* Returns a csv stream of a table that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} tableId - id of the table to export.
* @param {Object} options - options for the export.
* @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} options.tableId - id of the table to export.
* @param {string} options.header (optional) - which field of the column to use as header
* @param {express.Request} options.req - the request object.
*
* @return {Promise<string>} Promise for the resulting CSV.
*/
export async function makeCSVFromTable(
export async function makeCSVFromTable({ activeDoc, tableId, header, req }: {
activeDoc: ActiveDoc,
tableId: string,
req: express.Request) {
header?: ExportHeader,
req: express.Request
}) {

if (!activeDoc.docData) {
throw new Error('No docData in active document');
Expand All @@ -76,21 +93,21 @@ export async function makeCSVFromTable(
}

const data = await exportTable(activeDoc, tableRef, req);
const file = convertToCsv(data);
const file = convertToCsv(data, { header });
return file;
}

function convertToCsv({
rowIds,
access,
columns: viewColumns,
docSettings
}: ExportData) {
}: ExportData, options: { header?: ExportHeader }) {

// create formatters for columns
const formatters = viewColumns.map(col => col.formatter);
// Arrange the data into a row-indexed matrix, starting with column headers.
const csvMatrix = [viewColumns.map(col => col.label)];
const colPropertyAsHeader = options.header ?? 'label';
const csvMatrix = [viewColumns.map(col => col[colPropertyAsHeader])];
// populate all the rows with values as strings
rowIds.forEach(row => {
csvMatrix.push(access.map((getter, c) => formatters[c].formatAny(getter(row))));
Expand Down
64 changes: 41 additions & 23 deletions app/server/lib/workerExporter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {PassThrough} from 'stream';
import {FilterColValues} from "app/common/ActiveDocAPI";
import {ActiveDocSource, doExportDoc, doExportSection, doExportTable,
ExportData, ExportParameters, Filter} from 'app/server/lib/Export';
ExportData, ExportHeader, ExportParameters, Filter} from 'app/server/lib/Export';
import {createExcelFormatter} from 'app/server/lib/ExcelFormatter';
import * as log from 'app/server/lib/log';
import {Alignment, Border, Buffer as ExcelBuffer, stream as ExcelWriteStream,
Expand Down Expand Up @@ -79,67 +79,84 @@ export async function doMakeXLSXFromOptions(
stream: Stream,
options: ExportParameters
) {
const {tableId, viewSectionId, filters, sortOrder, linkingFilter} = options;
const {tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options;
if (viewSectionId) {
return doMakeXLSXFromViewSection(activeDocSource, testDates, stream, viewSectionId,
sortOrder || null, filters || null, linkingFilter || null);
return doMakeXLSXFromViewSection({activeDocSource, testDates, stream, viewSectionId, header,
sortOrder: sortOrder || null, filters: filters || null, linkingFilter: linkingFilter || null});
} else if (tableId) {
return doMakeXLSXFromTable(activeDocSource, testDates, stream, tableId);
return doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header});
} else {
return doMakeXLSX(activeDocSource, testDates, stream);
return doMakeXLSX({activeDocSource, testDates, stream, header});
}
}

/**
* @async
* Returns a XLSX stream of a view section that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} viewSectionId - id of the viewsection to export.
* @param {Integer[]} activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} filters (optional) - filters defined from ui.
* @param {Object} options - options for the export.
* @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to.
* @param {Integer} options.viewSectionId - id of the viewsection to export.
* @param {Integer[]} options.activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} options.filters (optional) - filters defined from ui.
* @param {FilterColValues} options.linkingFilter (optional)
* @param {Stream} options.stream - the stream to write to.
* @param {boolean} options.testDates - whether to use static dates for testing.
* @param {string} options.header (optional) - which field of the column to use as header
*/
async function doMakeXLSXFromViewSection(
async function doMakeXLSXFromViewSection({
activeDocSource, testDates, stream, viewSectionId, sortOrder, filters, linkingFilter, header
}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
viewSectionId: number,
sortOrder: number[] | null,
filters: Filter[] | null,
linkingFilter: FilterColValues | null,
) {
header?: ExportHeader,
}) {
const data = await doExportSection(activeDocSource, viewSectionId, sortOrder, filters, linkingFilter);
const {exportTable, end} = convertToExcel(stream, testDates);
const {exportTable, end} = convertToExcel(stream, testDates, {header});
exportTable(data);
return end();
}

/**
* @async
* Returns a XLSX stream of a table that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} tableId - id of the table to export.
* @param {Object} options - options for the export.
* @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to.
* @param {Integer} options.tableId - id of the table to export.
* @param {Stream} options.stream - the stream to write to.
* @param {boolean} options.testDates - whether to use static dates for testing.
* @param {string} options.header (optional) - which field of the column to use as header
*
*/
async function doMakeXLSXFromTable(
async function doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
tableId: string,
) {
header?: ExportHeader,
}) {
const data = await doExportTable(activeDocSource, {tableId});
const {exportTable, end} = convertToExcel(stream, testDates);
const {exportTable, end} = convertToExcel(stream, testDates, {header});
exportTable(data);
return end();
}

/**
* Creates excel document with all tables from an active Grist document.
*/
async function doMakeXLSX(
async function doMakeXLSX({activeDocSource, testDates, stream, header}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
): Promise<void|ExcelBuffer> {
const {exportTable, end} = convertToExcel(stream, testDates);
header?: ExportHeader,
}): Promise<void|ExcelBuffer> {
const {exportTable, end} = convertToExcel(stream, testDates, {header});
await doExportDoc(activeDocSource, async (table: ExportData) => exportTable(table));
return end();
}
Expand All @@ -152,7 +169,7 @@ async function doMakeXLSX(
* (The second option is for grist-static; at the time of writing
* WorkbookWriter doesn't appear to be available in a browser context).
*/
function convertToExcel(stream: Stream|undefined, testDates: boolean): {
function convertToExcel(stream: Stream|undefined, testDates: boolean, options: { header?: ExportHeader }): {
exportTable: (table: ExportData) => void,
end: () => Promise<void|ExcelBuffer>,
} {
Expand Down Expand Up @@ -206,7 +223,8 @@ function convertToExcel(stream: Stream|undefined, testDates: boolean): {
const formatters = columns.map(col => createExcelFormatter(col.formatter.type, col.formatter.widgetOpts));
// Generate headers for all columns with correct styles for whole column.
// Actual header style for a first row will be overwritten later.
ws.columns = columns.map((col, c) => ({ header: col.label, style: formatters[c].style() }));
const colHeader = options.header ?? 'label';
ws.columns = columns.map((col, c) => ({ header: col[colHeader], style: formatters[c].style() }));
// style up the header row
for (let i = 1; i <= columns.length; i++) {
// apply to all rows (including header)
Expand Down
Loading