diff --git a/CHANGELOG.md b/CHANGELOG.md index da249b6f6..d4724a721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,18 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +[#546](https://github.com/plotly/dash-table/issues/546) +- New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export + ### Fixed [#460](https://github.com/plotly/dash-table/issues/460) - The `datestartswith` relational operator now supports number comparison - Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type +[#546](https://github.com/plotly/dash-table/issues/546) +- Visible columns are used correctly for both header and data rows + [#591](https://github.com/plotly/dash-table/issues/591) - Fixed row and column selection when multiple tables are present diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 0e9da24d2..a3ce46156 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -798,8 +798,9 @@ export default class ControlledTable extends PureComponent tooltip_duration ); - const { export_format, export_headers, virtual, merge_duplicate_headers } = this.props; + const { export_columns, export_format, export_headers, virtual, merge_duplicate_headers } = this.props; const buttonProps = { + export_columns, export_format, virtual_data: virtual, columns, diff --git a/src/dash-table/components/Export/index.tsx b/src/dash-table/components/Export/index.tsx index 4e8bf1f24..63a75fe6c 100644 --- a/src/dash-table/components/Export/index.tsx +++ b/src/dash-table/components/Export/index.tsx @@ -1,33 +1,37 @@ import XLSX from 'xlsx'; import React from 'react'; -import { IDerivedData, Columns } from 'dash-table/components/Table/props'; +import { IDerivedData, Columns, ExportHeaders, ExportFormat, ExportColumns } from 'dash-table/components/Table/props'; import { createWorkbook, createHeadings, createWorksheet } from './utils'; import getHeaderRows from 'dash-table/derived/header/headerRows'; interface IExportButtonProps { columns: Columns; - export_format: string; + export_columns: ExportColumns; + export_format: ExportFormat; virtual_data: IDerivedData; visibleColumns: Columns; - export_headers: string; + export_headers: ExportHeaders; merge_duplicate_headers: boolean; } export default React.memo((props: IExportButtonProps) => { - const { columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props; - const isFormatSupported = export_format === 'csv' || export_format === 'xlsx'; + const { columns, export_columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props; + const isFormatSupported = export_format === ExportFormat.Csv || export_format === ExportFormat.Xlsx; + + const exportedColumns = export_columns === ExportColumns.Visible ? visibleColumns : columns; const handleExport = () => { - const columnID = visibleColumns.map(column => column.id); - const columnHeaders = visibleColumns.map(column => column.name); + const columnID = exportedColumns.map(column => column.id); + const columnHeaders = exportedColumns.map(column => column.name); + const maxLength = getHeaderRows(columns); - const heading = (export_headers !== 'none') ? createHeadings(columnHeaders, maxLength) : []; + const heading = (export_headers !== ExportHeaders.None) ? createHeadings(columnHeaders, maxLength) : []; const ws = createWorksheet(heading, virtual_data.data, columnID, export_headers, merge_duplicate_headers); const wb = createWorkbook(ws); - if (export_format === 'xlsx') { + if (export_format === ExportFormat.Xlsx) { XLSX.writeFile(wb, 'Data.xlsx', {bookType: 'xlsx', type: 'buffer'}); - } else if (export_format === 'csv') { + } else if (export_format === ExportFormat.Csv) { XLSX.writeFile(wb, 'Data.csv', {bookType: 'csv', type: 'buffer'}); } }; diff --git a/src/dash-table/components/Export/utils.tsx b/src/dash-table/components/Export/utils.tsx index b322f97df..c5c308ccc 100644 --- a/src/dash-table/components/Export/utils.tsx +++ b/src/dash-table/components/Export/utils.tsx @@ -1,13 +1,13 @@ import * as R from 'ramda'; import XLSX from 'xlsx'; -import { Data } from 'dash-table/components/Table/props'; +import { Data, ExportHeaders } from 'dash-table/components/Table/props'; interface IMergeObject { s: {r: number, c: number}; e: {r: number, c: number}; } -export function transformMultDimArray(array: (string | string[])[], maxLength: number): string[][] { +export function transformMultiDimArray(array: (string | string[])[], maxLength: number): string[][] { const newArray: string[][] = array.map(row => { if (row instanceof Array && row.length < maxLength) { return row.concat(Array(maxLength - row.length).fill('')); @@ -53,24 +53,30 @@ export function createWorkbook(ws: XLSX.WorkSheet) { return wb; } -export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: string, mergeDuplicateHeaders: boolean ) { - const ws = XLSX.utils.aoa_to_sheet(heading); - if (exportHeader === 'display' || exportHeader === 'names' || exportHeader === 'none') { - XLSX.utils.sheet_add_json(ws, data, { - header: columnID, - skipHeader: true, - origin: heading.length - }); - if (exportHeader === 'display' && mergeDuplicateHeaders) { +export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: ExportHeaders, mergeDuplicateHeaders: boolean) { + const ws = XLSX.utils.aoa_to_sheet([]); + + data = R.map(R.pick(columnID))(data); + + if (exportHeader === ExportHeaders.Display || exportHeader === ExportHeaders.Names || exportHeader === ExportHeaders.None) { + XLSX.utils.sheet_add_json(ws, heading, { skipHeader: true }); + + const contentOptions = heading.length > 0 ? + { header: columnID, skipHeader: true, origin: heading.length } : + { skipHeader: true }; + + XLSX.utils.sheet_add_json(ws, data, contentOptions); + + if (exportHeader === ExportHeaders.Display && mergeDuplicateHeaders) { ws['!merges'] = getMergeRanges(heading); } - } else if (exportHeader === 'ids') { + } else if (exportHeader === ExportHeaders.Ids) { XLSX.utils.sheet_add_json(ws, data, { header: columnID }); } return ws; } export function createHeadings(columnHeaders: (string | string[])[], maxLength: number) { - const transformedArray = transformMultDimArray(columnHeaders, maxLength); + const transformedArray = transformMultiDimArray(columnHeaders, maxLength); return R.transpose(transformedArray); } \ No newline at end of file diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index b66829651..9b7aa1f6c 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -23,6 +23,24 @@ export enum ColumnType { Datetime = 'datetime' } +export enum ExportColumns { + All = 'all', + Visible = 'visible' +} + +export enum ExportFormat { + Csv = 'csv', + Xlsx = 'xlsx', + None = 'none' +} + +export enum ExportHeaders { + Ids = 'ids', + Names = 'names', + None = 'none', + Display = 'display' +} + export enum SortMode { Single = 'single', Multi = 'multi' @@ -327,8 +345,9 @@ interface IDefaultProps { css: IStylesheetRule[]; data: Data; editable: boolean; - export_format: 'csv' | 'xlsx' | 'none'; - export_headers: 'ids' | 'names' | 'none' | 'display'; + export_columns: ExportColumns; + export_format: ExportFormat; + export_headers: ExportHeaders; fill_width: boolean; filter_query: string; filter_action: TableAction; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 3cc026f23..bf3b28081 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -88,6 +88,7 @@ export const defaultProps = { columns: [], data: [], editable: false, + export_columns: 'visible', export_format: 'none', include_headers_on_copy_paste: false, selected_cells: [], @@ -503,6 +504,13 @@ export const propTypes = { column_id: PropTypes.string }), + /** + * Denotes the columns that will be used in the export data file. + * If `all`, all columns will be used (visible + hidden). If `visible`, + * only the visible columns will be used. Defaults to `visible`. + */ + export_columns: PropTypes.oneOf(['all', 'visible']), + /** * Denotes the type of the export data file, * Defaults to `'none'` diff --git a/src/dash-table/dash/Sanitizer.ts b/src/dash-table/dash/Sanitizer.ts index 4b5a9e366..f6609504a 100644 --- a/src/dash-table/dash/Sanitizer.ts +++ b/src/dash-table/dash/Sanitizer.ts @@ -12,7 +12,9 @@ import { Selection, SanitizedProps, SortAsNull, - TableAction + TableAction, + ExportFormat, + ExportHeaders } from 'dash-table/components/Table/props'; import headerRows from 'dash-table/derived/header/headerRows'; import resolveFlag from 'dash-table/derived/cell/resolveFlag'; @@ -79,10 +81,10 @@ export default class Sanitizer { const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns); let headerFormat = props.export_headers; - if (props.export_format === 'xlsx' && R.isNil(headerFormat)) { - headerFormat = 'names'; - } else if (props.export_format === 'csv' && R.isNil(headerFormat)) { - headerFormat = 'ids'; + if (props.export_format === ExportFormat.Xlsx && R.isNil(headerFormat)) { + headerFormat = ExportHeaders.Names; + } else if (props.export_format === ExportFormat.Csv && R.isNil(headerFormat)) { + headerFormat = ExportHeaders.Ids; } return R.merge(props, { diff --git a/tests/cypress/tests/unit/exportUtils_tests.ts b/tests/cypress/tests/unit/exportUtils_tests.ts index e635cfb19..d5600fb21 100644 --- a/tests/cypress/tests/unit/exportUtils_tests.ts +++ b/tests/cypress/tests/unit/exportUtils_tests.ts @@ -1,44 +1,45 @@ -import { transformMultDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils'; +import { transformMultiDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils'; import * as R from 'ramda'; +import { ExportHeaders } from 'dash-table/components/Table/props'; describe('export', () => { - describe('transformMultDimArray', () => { + describe('transformMultiDimArray', () => { it('array with only strings', () => { const testedArray = []; - const transformedArray = transformMultDimArray(testedArray, 0); + const transformedArray = transformMultiDimArray(testedArray, 0); const expectedArray = []; expect(transformedArray).to.deep.equal(expectedArray); }); it('array with only strings', () => { const testedArray = ['a', 'b', 'c', 'd']; - const transformedArray = transformMultDimArray(testedArray, 0); + const transformedArray = transformMultiDimArray(testedArray, 0); const expectedArray = [['a'], ['b'], ['c'], ['d']]; expect(transformedArray).to.deep.equal(expectedArray); }); it ('array with strings and strings array with same length', () => { const testedArray = ['a', ['b', 'c'], ['b', 'd']]; - const transformedArray = transformMultDimArray(testedArray, 2); + const transformedArray = transformMultiDimArray(testedArray, 2); const expectedArray = [['a', 'a'], ['b', 'c'], ['b', 'd']]; expect(transformedArray).to.deep.equal(expectedArray); }); it ('2D strings array', () => { const testedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']]; - const transformedArray = transformMultDimArray(testedArray, 3); + const transformedArray = transformMultiDimArray(testedArray, 3); const expectedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']]; expect(transformedArray).to.deep.equal(expectedArray); }); it ('multidimensional array', () => { const testedArray = [['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']]; - const transformedArray = transformMultDimArray(testedArray, 4); + const transformedArray = transformMultiDimArray(testedArray, 4); const expectedArray = [['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']]; expect(transformedArray).to.deep.equal(expectedArray); }); it ('multidimensional array with strings', () => { const testedArray = ['rows', ['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']]; - const transformedArray = transformMultDimArray(testedArray, 4); + const transformedArray = transformMultiDimArray(testedArray, 4); const expectedArray = [['rows', 'rows', 'rows', 'rows'], ['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']]; expect(transformedArray).to.deep.equal(expectedArray); }); @@ -210,20 +211,24 @@ describe('export', () => { }); describe('createWorksheet ', () => { - const Headings = [['rows', 'rows', 'b'], - ['rows', 'c', 'c'], - ['rows', 'e', 'f'], - ['rows', 'rows', 'rows']]; + const Headings = [ + ['rows', 'rows', 'b'], + ['rows', 'c', 'c'], + ['rows', 'e', 'f'], + ['rows', 'rows', 'rows'] + ]; + const data = [ - {col1: 1, col2: 2, col3: 3}, - {col1: 2, col2: 3, col3: 4}, - {col1: 1, col2: 2, col3: 3} + { col1: 1, col2: 2, col3: 'x', col4: 3 }, + { col1: 2, col2: 3, col3: 'x', col4: 4 }, + { col1: 1, col2: 2, col3: 'x', col4: 3 } ]; - const columnID = ['col1', 'col2', 'col3']; + + const columnID = ['col1', 'col2', 'col4']; it('create sheet with column names as headers for name or display header mode', () => { - const wsName = createWorksheet(Headings, data, columnID, 'names', true); - const wsDisplay = createWorksheet(Headings, data, columnID, 'display', true); - const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, 'display', false); + const wsName = createWorksheet(Headings, data, columnID, ExportHeaders.Names, true); + const wsDisplay = createWorksheet(Headings, data, columnID, ExportHeaders.Display, true); + const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, ExportHeaders.Display, false); const expectedWS = { A1: {t: 's', v: 'rows'}, A2: {t: 's', v: 'rows'}, @@ -256,7 +261,7 @@ describe('export', () => { expect(wsDisplay).to.deep.equal(expectedWSDisplay); }); it('create sheet with column ids as headers', () => { - const ws = createWorksheet(Headings, data, columnID, 'ids', true); + const ws = createWorksheet(Headings, data, columnID, ExportHeaders.Ids, true); const expectedWS = { A1: {t: 's', v: 'col1'}, A2: {t: 'n', v: 1}, @@ -266,7 +271,7 @@ describe('export', () => { B2: {t: 'n', v: 2}, B3: {t: 'n', v: 3}, B4: {t: 'n', v: 2}, - C1: {t: 's', v: 'col3'}, + C1: {t: 's', v: 'col4'}, C2: {t: 'n', v: 3}, C3: {t: 'n', v: 4}, C4: {t: 'n', v: 3}}; @@ -274,7 +279,7 @@ describe('export', () => { expect(ws).to.deep.equal(expectedWS); }); it('create sheet with no headers', () => { - const ws = createWorksheet([], data, columnID, 'none', true); + const ws = createWorksheet([], data, columnID, ExportHeaders.None, true); const expectedWS = { A1: {t: 'n', v: 1}, A2: {t: 'n', v: 2}, @@ -290,11 +295,11 @@ describe('export', () => { }); it('create sheet with undefined column for clearable columns', () => { const newData = [ - {col2: 2, col3: 3}, - {col2: 3, col3: 4}, - {col2: 2, col3: 3} + {col2: 2, col4: 3}, + {col2: 3, col4: 4}, + {col2: 2, col4: 3} ]; - const ws = createWorksheet(Headings, newData, columnID, 'display', false); + const ws = createWorksheet(Headings, newData, columnID, ExportHeaders.Display, false); const expectedWS = {A1: {t: 's', v: 'rows'}, A2: {t: 's', v: 'rows'}, A3: {t: 's', v: 'rows'}, diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index 2458a444c..a5b318caa 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -46,7 +46,7 @@ fixtures.forEach(fixture => { }); import dataset from './../../assets/gapminder.csv'; -import { TableAction } from 'dash-table/components/Table/props'; +import { TableAction, ExportFormat } from 'dash-table/components/Table/props'; storiesOf('DashTable/Without Data', module) .add('with 1 column', () => ()) .add('Export Button for csv file', () => ()) .add('No export Button for file formatted not supported', () => ());