Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0fd6f96
add merge_duplicate_headers parameter
alinastarkov Jul 11, 2019
4145c5c
refactor
alinastarkov Jul 11, 2019
0cb825e
unit testing
alinastarkov Jul 11, 2019
a40c4fd
sanity tests for merged and unmerged mode
alinastarkov Jul 11, 2019
a81d2e9
comma typo
alinastarkov Jul 11, 2019
0a68117
add CHANGELOG.md
alinastarkov Jul 11, 2019
1854d3f
linting
alinastarkov Jul 11, 2019
7f0a3bb
linting
alinastarkov Jul 12, 2019
cbdfe14
add merge duplicate headers mode
alinastarkov Jul 12, 2019
eed1e4c
linting
alinastarkov Jul 12, 2019
f69106e
semantic
alinastarkov Jul 12, 2019
f45bb39
Merge branch 'master' into 491Bugs
Marc-Andre-Rivet Jul 15, 2019
9e4c3ab
remove merge
alinastarkov Jul 15, 2019
641c6dc
remove object.assign and use R.merge
alinastarkov Jul 15, 2019
fe03464
remove instanceof string
alinastarkov Jul 15, 2019
8069962
remove max length function
alinastarkov Jul 15, 2019
1ca78e4
remove instanceof string
alinastarkov Jul 15, 2019
e4b9800
dont need to transform if max length = 1
alinastarkov Jul 15, 2019
6abfb1c
different way to create a column
alinastarkov Jul 15, 2019
5aa1e73
add seperate edit headers
alinastarkov Jul 15, 2019
10d622d
Merge branch '491Bugs' of https://github.com/plotly/dash-table into 4…
alinastarkov Jul 15, 2019
257ff50
merge right instead of merge
alinastarkov Jul 15, 2019
a5f96ab
linting
alinastarkov Jul 15, 2019
e1b8f8c
merge master in branch
alinastarkov Jul 15, 2019
c4c2cb4
change type in testing suite
alinastarkov Jul 15, 2019
2f89274
columnIndex
alinastarkov Jul 15, 2019
0de0a2b
algorithm refactor
alinastarkov Jul 16, 2019
47348e0
Merge branch 'master' into 491Bugs
Marc-Andre-Rivet Jul 16, 2019
6634d59
refactor
alinastarkov Jul 16, 2019
a5d9de3
Merge branch '491Bugs' of https://github.com/plotly/dash-table into 4…
alinastarkov Jul 16, 2019
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions dash-main
Submodule dash-main added at 9819e5
9 changes: 9 additions & 0 deletions demo/AppMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum AppMode {
FixedTooltips = 'fixed,tooltips',
FixedVirtualized = 'fixed,virtualized',
Formatting = 'formatting',
MergeDuplicateHeaders = 'mergeDuplicateHeaders',
ReadOnly = 'readonly',
ColumnsInSpace = 'columnsInSpace',
TaleOfTwoTables = 'taleOfTwoTables',
Expand Down Expand Up @@ -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');

Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/dash-table/components/HeaderFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ export default class HeaderFactory {
sort_mode,
sort_by,
page_action,
setProps
setProps,
merge_duplicate_headers
);

const ops = this.getHeaderOpCells(
Expand Down
9 changes: 5 additions & 4 deletions src/dash-table/derived/header/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
}

Expand Down Expand Up @@ -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<R.KeyValuePair<any[], number[]>, JSX.Element[]>(R.map)(
([labels, indices], headerRowIndex) => {
Expand Down Expand Up @@ -121,7 +122,7 @@ function getter(
{renamable ?
(<span
className='column-header--edit'
onClick={editColumnName(column, columns, headerRowIndex, setProps)}
onClick={editColumnName(column, columns, headerRowIndex, setProps, merge_duplicate_headers)}
>
{`✎`}
</span>) :
Expand Down
42 changes: 25 additions & 17 deletions src/dash-table/utils/actions.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -18,7 +16,6 @@ function getGroupedColumnIndices(column, columns, headerRowIndex) {
break;
}
}

return { groupIndexFirst: columnIndex, groupIndexLast: lastColumnIndex };
}

Expand Down Expand Up @@ -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);
}
59 changes: 59 additions & 0 deletions tests/cypress/tests/standalone/edit_headers.ts
Original file line number Diff line number Diff line change
@@ -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', ``);
});
});
});
116 changes: 116 additions & 0 deletions tests/cypress/tests/unit/editColumnNames.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});