diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3df5a18..9e437b973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +#[Unreleased] +### Fixed +[#491](https://github.com/plotly/dash-table/issues/491) +- Fixed unconsistent behaviors when editing cell headers + ## [4.0.2] - 2019-07-15 ### Fixed [#489](https://github.com/plotly/dash-table/issues/489) diff --git a/dash-main b/dash-main new file mode 160000 index 000000000..9819e5eb6 --- /dev/null +++ b/dash-main @@ -0,0 +1 @@ +Subproject commit 9819e5eb6f7dcdd1c828c5e070af41f76214b6ff diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 42ac31814..e7e62dbb2 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -20,6 +20,7 @@ export enum AppMode { FixedTooltips = 'fixed,tooltips', FixedVirtualized = 'fixed,virtualized', Formatting = 'formatting', + MergeDuplicateHeaders = 'mergeDuplicateHeaders', ReadOnly = 'readonly', ColumnsInSpace = 'columnsInSpace', TaleOfTwoTables = 'taleOfTwoTables', @@ -314,6 +315,12 @@ function getFormattingState() { return state; } +function getMergeDuplicateHeadersState() { + const state = getDefaultState(); + state.tableProps.merge_duplicate_headers = true; + return state; +} + function getState() { const mode = Environment.searchParams.get('mode'); @@ -332,6 +339,8 @@ function getState() { return getReadonlyState(); case AppMode.ColumnsInSpace: return getSpaceInColumn(); + case AppMode.MergeDuplicateHeaders: + return getMergeDuplicateHeadersState(); case AppMode.Tooltips: return getTooltipsState(); case AppMode.Virtualized: diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index b11df1182..c9ebfd26f 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -96,7 +96,8 @@ export default class HeaderFactory { sort_mode, sort_by, page_action, - setProps + setProps, + merge_duplicate_headers ); const ops = this.getHeaderOpCells( diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 11fce31b5..947a4348f 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -55,9 +55,9 @@ function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: Se }; } -function editColumnName(column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, setProps: SetProps) { +function editColumnName(column: IVisibleColumn, columns: VisibleColumns, columnRowIndex: any, setProps: SetProps, mergeDuplicateHeaders: boolean) { return () => { - setProps(actions.editColumnName(column, columns, columnRowIndex)); + setProps(actions.editColumnName(column, columns, columnRowIndex, mergeDuplicateHeaders)); }; } @@ -87,7 +87,8 @@ function getter( mode: SortMode, sortBy: SortBy, paginationMode: TableAction, - setProps: SetProps + setProps: SetProps, + merge_duplicate_headers: boolean ): JSX.Element[][] { return R.addIndex, JSX.Element[]>(R.map)( ([labels, indices], headerRowIndex) => { @@ -121,7 +122,7 @@ function getter( {renamable ? ( {`✎`} ) : diff --git a/src/dash-table/utils/actions.js b/src/dash-table/utils/actions.js index 3e1241d50..76affd6d0 100644 --- a/src/dash-table/utils/actions.js +++ b/src/dash-table/utils/actions.js @@ -1,12 +1,10 @@ import * as R from 'ramda'; +import getHeaderRows from 'dash-table/derived/header/headerRows'; -function getGroupedColumnIndices(column, columns, headerRowIndex) { - const columnIndex = columns.indexOf(column); - - if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex)) { +function getGroupedColumnIndices(column, columns, headerRowIndex, mergeDuplicateHeaders, columnIndex) { + if (!column.name || (Array.isArray(column.name) && column.name.length < headerRowIndex) || !mergeDuplicateHeaders) { return { groupIndexFirst: columnIndex, groupIndexLast: columnIndex }; } - let lastColumnIndex = columnIndex; for (let i = columnIndex; i < columns.length; ++i) { @@ -18,7 +16,6 @@ function getGroupedColumnIndices(column, columns, headerRowIndex) { break; } } - return { groupIndexFirst: columnIndex, groupIndexLast: lastColumnIndex }; } @@ -53,23 +50,34 @@ export const clearSelection = { selected_cells: [] }; -export function editColumnName(column, columns, headerRowIndex) { +export function changeColumnHeader(column, columns, headerRowIndex, mergeDuplicateHeaders, newColumnName) { + let newColumns = columns; + const maxLength = getHeaderRows(newColumns); + const columnIndex = newColumns.findIndex(col => col.id === column.id); + + if (typeof column.name === 'string' && maxLength > 1) { + const newColumnNames = Array(maxLength).fill(column.name); + const cloneColumn = R.mergeRight(column, {name: newColumnNames}); + newColumns = newColumns.slice(0); + newColumns[columnIndex] = cloneColumn; + } + const { groupIndexFirst, groupIndexLast } = getGroupedColumnIndices( - column, columns, headerRowIndex + column, newColumns, headerRowIndex, mergeDuplicateHeaders, columnIndex ); - /* eslint no-alert: 0 */ - const newColumnName = window.prompt('Enter a new column name'); - let newColumns = R.clone(columns); + R.range(groupIndexFirst, groupIndexLast + 1).map(i => { let namePath; - if (R.type(columns[i].name) === 'Array') { + if (R.type(newColumns[i].name) === 'Array') { namePath = [i, 'name', headerRowIndex]; - } else { - namePath = [i, 'name']; } newColumns = R.set(R.lensPath(namePath), newColumnName, newColumns); }); - return { - columns: newColumns - }; + + return { columns: newColumns} ; +} + +export function editColumnName(column, columns, headerRowIndex, mergeDuplicateHeaders) { + const newColumnName = window.prompt('Enter a new column name'); + return changeColumnHeader(column, columns, headerRowIndex, mergeDuplicateHeaders, newColumnName); } diff --git a/tests/cypress/tests/standalone/edit_headers.ts b/tests/cypress/tests/standalone/edit_headers.ts new file mode 100644 index 000000000..9276dc059 --- /dev/null +++ b/tests/cypress/tests/standalone/edit_headers.ts @@ -0,0 +1,59 @@ +import DashTable from 'cypress/DashTable'; + +import { AppMode } from 'demo/AppMode'; + +describe(`edit, mode=${AppMode.Typed}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Typed}`); + DashTable.toggleScroll(false); + }); + + describe(`edit headers, mode=${AppMode.Typed}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Typed}`); + DashTable.toggleScroll(false); + }); + + it('changing the column 0 header should not change any column 0 headers below it ', () => { + cy.window().then((win: any) => { + cy.stub(win, 'prompt').returns('Hello'); + }); + cy.get('.dash-header.column-0 .column-header--edit').eq(0).click(); + cy.get('.dash-header.column-0 > div > span:last-child').should('have.html', `Hello`); + cy.get('table > tbody > tr:nth-child(2) > th.dash-header.column-0 > div > span:last-child').should('have.html', `rows`); + }); + + it('changing the column 1 header should not change any other headers', () => { + cy.window().then((win: any) => { + cy.stub(win, 'prompt').returns('Aloha'); + }); + cy.get('.dash-header.column-1 .column-header--edit').eq(0).click(); + cy.get('.dash-header.column-1 > div > span:last-child').should('have.html', `Aloha`); + cy.get('.dash-header.column-2 > div > span:last-child').should('have.html', `City`); + }); + }); + + describe(`edit headers, mode=${AppMode.MergeDuplicateHeaders}`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.MergeDuplicateHeaders}`); + DashTable.toggleScroll(false); + }); + it('changing the column 0 header should not change any column 0 headers below it ', () => { + cy.window().then((win: any) => { + cy.stub(win, 'prompt').returns('Otter'); + }); + cy.get('.dash-header.column-0 .column-header--edit').eq(0).click(); + cy.get('.dash-header.column-0 > div > span:last-child').should('have.html', `Otter`); + cy.get('table > tbody > tr:nth-child(2) > th.dash-header.column-0 > div > span:last-child').should('have.html', `rows`); + }); + it('changing the column 1 header should not change any other headers', () => { + cy.window().then((win: any) => { + cy.stub(win, 'prompt').returns('Aloha'); + }); + cy.get('.dash-header.column-1 .column-header--edit').eq(0).click(); + cy.get('.dash-header.column-0 > div > span:last-child').should('have.html', `rows`); + cy.get('.dash-header.column-1 > div > span:last-child').should('have.html', `Aloha`); + cy.get('.dash-header.column-6 > div > span:last-child').should('have.html', ``); + }); + }); +}); \ No newline at end of file diff --git a/tests/cypress/tests/unit/editColumnNames.ts b/tests/cypress/tests/unit/editColumnNames.ts new file mode 100644 index 000000000..304328703 --- /dev/null +++ b/tests/cypress/tests/unit/editColumnNames.ts @@ -0,0 +1,116 @@ +import { changeColumnHeader } from 'dash-table/utils/actions'; + +describe('changeColumnHeader', () => { + const columns = [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]; + describe('merge_duplicate_headers = false', () => { + const merge_duplicate_headers = false; + + it('change a header name with column names as array, name[1] of column[1] should be AAA', () => { + const column = {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 1, merge_duplicate_headers, 'AAA'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'AAA', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as string, column[0] name should be array and name[0] should be BBB', () => { + const column = {id: 'rows', type: 'numeric', name: 'rows', renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 0, merge_duplicate_headers, 'BBB'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: ['BBB', 'rows', 'rows'], renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as string, column[3] name should be array and name[2] should be CCC', () => { + const column = {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 2, merge_duplicate_headers, 'CCC'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'CCC'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + }); + + describe('merge_duplicate_headers = true', () => { + const merge_duplicate_headers = true; + + it('change a header name with column names as array, all City should change to Ville', () => { + const column = {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 0, merge_duplicate_headers, 'Ville'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['Ville', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['Ville', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['Ville', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as array, all Canada should change to Kanada', () => { + const column = {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 1, merge_duplicate_headers, 'Kanada'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Kanada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Kanada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as array, New York should change to Maui', () => { + const column = {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 2, merge_duplicate_headers, 'Maui'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: 'rows', renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'Maui'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as string, column[0] name should be [City, rows, rows ]', () => { + const column = {id: 'rows', type: 'numeric', name: 'rows', renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 0, merge_duplicate_headers, 'City'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: ['City', 'rows', 'rows'], renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + it('change a header name with column names as string, column[0] name should be [rows, ABC, rows ]', () => { + const column = {id: 'rows', type: 'numeric', name: 'rows', renamable: true}; + const returnColumn = changeColumnHeader(column, columns, 1, merge_duplicate_headers, 'ABC'); + const expectedColumn = {columns : [ + {id: 'rows', type: 'numeric', name: ['rows', 'ABC', 'rows'], renamable: true}, + {id: 'aaaa', type: 'numeric', name: ['City', 'Canada', 'Toronto'], renamable: true}, + {id: 'bbbb', type: 'numeric', name: ['City', 'Canada', 'Montreal'], renamable: true}, + {id: 'cccc', type: 'numeric', name: ['City', 'America', 'Boston'], renamable: true}, + {id: 'dddd', type: 'numeric', name: ['', 'America', 'New York'], renamable: true} + ]}; + expect(expectedColumn).to.deep.equal(returnColumn); + }); + }); +}); \ No newline at end of file