diff --git a/packages/elements/src/components/Spreadsheet/__stubs__/index.tsx b/packages/elements/src/components/Spreadsheet/__stubs__/index.tsx index 52af4c25d1..d66c23f69d 100644 --- a/packages/elements/src/components/Spreadsheet/__stubs__/index.tsx +++ b/packages/elements/src/components/Spreadsheet/__stubs__/index.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { Cell, SetData, SetSelected, SetContextMenuProp } from '../types' -import ReactDataSheet from 'react-datasheet' +import { Cell, SetData, SetSelected, SetContextMenuProp, AfterCellsChanged } from '../types' export const parseResult = { data: [ @@ -92,14 +91,14 @@ export const data: Cell[][] = [ ], ] -export const cellRenderProps: ReactDataSheet.CellRendererProps = { +export const cellRenderProps = { row: 3, col: 10, cell: { value: 'row3@gmail.com' }, selected: false, editing: false, updated: false, - attributesRenderer: jest.fn() as ReactDataSheet.AttributesRenderer, + attributesRenderer: jest.fn() as any, className: 'cell', style: { background: 'red' }, onMouseDown: jest.fn(), @@ -118,3 +117,5 @@ export const selectedMatrix = { start: { i: 0, j: 1 }, end: { i: 2, j: 3 }, } + +export const afterCellsChanged = jest.fn() as AfterCellsChanged diff --git a/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/context-menu.tsx.snap b/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/context-menu.tsx.snap index 618d37a58c..938e8d965d 100644 --- a/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/context-menu.tsx.snap +++ b/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/context-menu.tsx.snap @@ -32,15 +32,6 @@ exports[`ContextMenu should match snapshot with visible false 1`] = ` Clear row -
  • - - Clear column - -
  • -
  • - - Remove column - -
  • @@ -110,15 +92,6 @@ exports[`ContextMenu should match snapshot with visible true 1`] = ` Clear row -
  • - - Clear column - -
  • -
  • - - Remove column - -
  • diff --git a/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap b/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap index 7e5650d314..6ea37ae511 100644 --- a/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap +++ b/packages/elements/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap @@ -15,6 +15,7 @@ exports[`customCellRenderer should match snapshot with CustomComponent 1`] = ` value="row3@gmail.com" > `; @@ -106,7 +111,7 @@ exports[`Spreadsheet should match snapshot with full props 1`] = ` onCellsChanged={[Function]} onContextMenu={[Function]} onSelect={[Function]} - overflow="clip" + overflow="wrap" rowRenderer={[Function]} selected={null} sheetRenderer={[Function]} @@ -124,9 +129,14 @@ exports[`Spreadsheet should match snapshot with full props 1`] = ` "visible": false, } } + data={ + Array [ + Array [], + ] + } + onCellsChanged={[Function]} selected={null} setContextMenuProp={[Function]} - setData={[Function]} /> `; diff --git a/packages/elements/src/components/Spreadsheet/__tests__/context-menu.tsx b/packages/elements/src/components/Spreadsheet/__tests__/context-menu.tsx index c6d56bd3d4..a7d152d971 100644 --- a/packages/elements/src/components/Spreadsheet/__tests__/context-menu.tsx +++ b/packages/elements/src/components/Spreadsheet/__tests__/context-menu.tsx @@ -1,24 +1,24 @@ import * as React from 'react' import { shallow } from 'enzyme' -import { - ContextMenu, - handleContextClick, - clearRowSetData, - clearColSetData, - removeRowSetData, - removeColSetData, -} from '../context-menu' -import { selectedMatrix, setData, setContextMenuProp, data } from '../__stubs__' +import { ContextMenu, handleContextClick, clearRow, clearCol, removeRow, removeCol } from '../context-menu' +import { selectedMatrix, setContextMenuProp, data } from '../__stubs__' + +const onCellsChanged = jest.fn() + +afterEach(() => { + jest.clearAllMocks() +}) describe('ContextMenu', () => { it('should match snapshot with visible true', () => { expect( shallow( , ), ).toMatchSnapshot() @@ -28,10 +28,11 @@ describe('ContextMenu', () => { expect( shallow( , ), ).toMatchSnapshot() @@ -45,11 +46,8 @@ describe('handleContextClick', () => { id: '', }, } - const fn = handleContextClick(selectedMatrix, setData, setContextMenuProp) - afterEach(() => { - jest.clearAllMocks() - }) + const fn = handleContextClick(data, selectedMatrix, setContextMenuProp, onCellsChanged) it('should return correct value clear-row', () => { const mockEvent = { @@ -106,237 +104,152 @@ describe('handleContextClick', () => { }) }) -describe('clearRowSetData', () => { - it('should return correct data', () => { +describe('clearRow', () => { + it('should clear correct data', () => { const currentRowIndex = 1 - const expectedResult = [ - [ - { value: 'Office name' }, - { value: 'Building Name' }, - { value: 'Building No.' }, - { value: 'Address 1' }, - { value: 'Address 2' }, - { value: 'Address 3' }, - { value: 'Address 4' }, - { value: 'Post Code' }, - { value: 'Telephone' }, - { value: 'Fax' }, - { value: 'Email' }, - ], - [ - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - { value: '' }, - ], - [ - { value: 'London2' }, - { value: 'The Black House' }, - { value: '11' }, - { value: 'Test Addres' }, - { value: '' }, - { value: 'Adress 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '087 471 929' }, - { value: '' }, - { value: 'row2@gmail.com' }, - ], + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'London2' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + clearRow(data, currentRowIndex, onCellsChanged) + const expectedChangedCells = [ + { cell: { value: 'London2' }, row: currentRowIndex, col: 0, value: '' }, + { + cell: { value: 'The Black House' }, + row: currentRowIndex, + col: 1, + value: '', + }, + ] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) + }) + it('should not clear readOnly row', () => { + const currentRowIndex = 1 + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], [ - { value: 'New York' }, - { value: 'Building A' }, - { value: '11' }, - { value: '' }, - { value: '' }, - { value: 'City Z' }, - { value: '' }, - { value: 'AL7187' }, - { value: '017 7162 9121' }, - { value: '' }, - { value: 'row3@gmail.com' }, + { value: 'London2', readOnly: true }, + { value: 'The Black House', readOnly: true }, ], + [{ value: 'New York' }, { value: 'Building A' }], ] - const fn = clearRowSetData(currentRowIndex) - const result = fn(data) - expect(result).toEqual(expectedResult) + clearRow(data, currentRowIndex, onCellsChanged) + const expectedChangedCells = [] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) }) }) -describe('clearColSetData', () => { - it('should return correct data', () => { +describe('clearCol', () => { + it('should clear correct data', () => { const currentColIndex = 1 - const expectedResult = [ - [ - { value: 'Office name' }, - { value: '' }, - { value: 'Building No.' }, - { value: 'Address 1' }, - { value: 'Address 2' }, - { value: 'Address 3' }, - { value: 'Address 4' }, - { value: 'Post Code' }, - { value: 'Telephone' }, - { value: 'Fax' }, - { value: 'Email' }, - ], - [ - { value: 'London' }, - { value: '' }, - { value: '15' }, - { value: 'London 1' }, - { value: '' }, - { value: 'Londom 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '0845 0000' }, - { value: '' }, - { value: 'row1@gmail.com' }, - ], - [ - { value: 'London2' }, - { value: '' }, - { value: '11' }, - { value: 'Test Addres' }, - { value: '' }, - { value: 'Adress 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '087 471 929' }, - { value: '' }, - { value: 'row2@gmail.com' }, - ], - [ - { value: 'New York' }, - { value: '' }, - { value: '11' }, - { value: '' }, - { value: '' }, - { value: 'City Z' }, - { value: '' }, - { value: 'AL7187' }, - { value: '017 7162 9121' }, - { value: '' }, - { value: 'row3@gmail.com' }, - ], + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'London2' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + clearCol(data, currentColIndex, onCellsChanged) + const expectedChangedCells = [ + { cell: { value: 'Building Name' }, row: 0, col: currentColIndex, value: '' }, + { + cell: { value: 'The Black House' }, + row: 1, + col: currentColIndex, + value: '', + }, + { + cell: { value: 'Building A' }, + row: 2, + col: currentColIndex, + value: '', + }, ] - const fn = clearColSetData(currentColIndex) - const result = fn(data) - expect(result).toEqual(expectedResult) + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) + }) + it('should not clear readOnly col', () => { + const currentColIndex = 1 + const data = [ + [{ value: 'Office name' }, { value: 'Building Name', readOnly: true }], + [{ value: 'London2' }, { value: 'The Black House', readOnly: true }], + [{ value: 'New York' }, { value: 'Building A', readOnly: true }], + ] + clearCol(data, currentColIndex, onCellsChanged) + const expectedChangedCells = [] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) }) }) -describe('removeRowSetData', () => { - it('should return correct data', () => { +describe('removeRow', () => { + it('should remove correct data', () => { const currentRowIndex = 1 - const expectedResult = [ - [ - { value: 'Office name' }, - { value: 'Building Name' }, - { value: 'Building No.' }, - { value: 'Address 1' }, - { value: 'Address 2' }, - { value: 'Address 3' }, - { value: 'Address 4' }, - { value: 'Post Code' }, - { value: 'Telephone' }, - { value: 'Fax' }, - { value: 'Email' }, - ], - [ - { value: 'London2' }, - { value: 'The Black House' }, - { value: '11' }, - { value: 'Test Addres' }, - { value: '' }, - { value: 'Adress 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '087 471 929' }, - { value: '' }, - { value: 'row2@gmail.com' }, - ], + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'London2' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + removeRow(data, currentRowIndex, onCellsChanged) + const expectedChangedCells = [ + { cell: { value: 'London2' }, row: currentRowIndex, col: 0, value: null }, + { + cell: { value: 'The Black House' }, + row: currentRowIndex, + col: 1, + value: null, + }, + ] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) + }) + it('should not remove readOnly row', () => { + const currentRowIndex = 1 + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], [ - { value: 'New York' }, - { value: 'Building A' }, - { value: '11' }, - { value: '' }, - { value: '' }, - { value: 'City Z' }, - { value: '' }, - { value: 'AL7187' }, - { value: '017 7162 9121' }, - { value: '' }, - { value: 'row3@gmail.com' }, + { value: 'London2', readOnly: true }, + { value: 'The Black House', readOnly: true }, ], + [{ value: 'New York' }, { value: 'Building A' }], ] - const fn = removeRowSetData(currentRowIndex) - const result = fn(data) - expect(result).toEqual(expectedResult) + removeRow(data, currentRowIndex, onCellsChanged) + const expectedChangedCells = [] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) }) }) -describe('removeColSetData', () => { - it('should return correct data', () => { +describe('removeCol', () => { + it('should clear correct data', () => { const currentColIndex = 1 - const expectedResult = [ - [ - { value: 'Office name' }, - { value: 'Building No.' }, - { value: 'Address 1' }, - { value: 'Address 2' }, - { value: 'Address 3' }, - { value: 'Address 4' }, - { value: 'Post Code' }, - { value: 'Telephone' }, - { value: 'Fax' }, - { value: 'Email' }, - ], - [ - { value: 'London' }, - { value: '15' }, - { value: 'London 1' }, - { value: '' }, - { value: 'Londom 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '0845 0000' }, - { value: '' }, - { value: 'row1@gmail.com' }, - ], - [ - { value: 'London2' }, - { value: '11' }, - { value: 'Test Addres' }, - { value: '' }, - { value: 'Adress 3' }, - { value: '' }, - { value: 'EC12NH' }, - { value: '087 471 929' }, - { value: '' }, - { value: 'row2@gmail.com' }, - ], - [ - { value: 'New York' }, - { value: '11' }, - { value: '' }, - { value: '' }, - { value: 'City Z' }, - { value: '' }, - { value: 'AL7187' }, - { value: '017 7162 9121' }, - { value: '' }, - { value: 'row3@gmail.com' }, - ], + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'London2' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + removeCol(data, currentColIndex, onCellsChanged) + const expectedChangedCells = [ + { cell: { value: 'Building Name' }, row: 0, col: currentColIndex, value: null }, + { + cell: { value: 'The Black House' }, + row: 1, + col: currentColIndex, + value: null, + }, + { + cell: { value: 'Building A' }, + row: 2, + col: currentColIndex, + value: null, + }, + ] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) + }) + it('should not clear readOnly col', () => { + const currentColIndex = 1 + const data = [ + [{ value: 'Office name' }, { value: 'Building Name', readOnly: true }], + [{ value: 'London2' }, { value: 'The Black House', readOnly: true }], + [{ value: 'New York' }, { value: 'Building A', readOnly: true }], ] - const fn = removeColSetData(currentColIndex) - const result = fn(data) - expect(result).toEqual(expectedResult) + removeCol(data, currentColIndex, onCellsChanged) + const expectedChangedCells = [] + expect(onCellsChanged).toHaveBeenCalledWith(expectedChangedCells) }) }) diff --git a/packages/elements/src/components/Spreadsheet/__tests__/handlers.tsx b/packages/elements/src/components/Spreadsheet/__tests__/handlers.tsx index 1ce7702bae..9944a44d85 100644 --- a/packages/elements/src/components/Spreadsheet/__tests__/handlers.tsx +++ b/packages/elements/src/components/Spreadsheet/__tests__/handlers.tsx @@ -24,6 +24,7 @@ import { setSelected, parseResult, setContextMenuProp, + afterCellsChanged, } from '../__stubs__' import { getMaxRowAndCol, convertDataToCsv, unparseDataToCsvString, validatedDataGenerate } from '../utils' @@ -204,7 +205,7 @@ describe('onSelectCell', () => { describe('customCellRenderer', () => { it('should match snapshot without CustomComponent', () => { - const CellComponent = customCellRenderer(data, setData, setSelected) + const CellComponent = customCellRenderer(data, setData, setSelected, afterCellsChanged) expect(shallow()).toMatchSnapshot() }) it('should match snapshot with CustomComponent', () => { @@ -216,7 +217,7 @@ describe('customCellRenderer', () => { CustomComponent, }, } - const CellComponent = customCellRenderer(data, setData, setSelected) + const CellComponent = customCellRenderer(data, setData, setSelected, afterCellsChanged) expect(shallow()).toMatchSnapshot() }) @@ -226,7 +227,7 @@ describe('customCellRenderer', () => { cell: { value: '11aa', isValidated: false }, } - const CellComponent = customCellRenderer(data, setData, setSelected) + const CellComponent = customCellRenderer(data, setData, setSelected, afterCellsChanged) expect(shallow()).toMatchSnapshot() }) }) @@ -306,16 +307,170 @@ describe('handleAddNewRow', () => { }) describe('handleCellsChanged', () => { - it('should call setData with correct arg', () => { - const fn = handleCellsChanged(data, setData, validate) + const data = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'London2' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + afterAll(() => { + ;(validatedDataGenerate as jest.Mock).mockImplementation(() => 'validated data') + }) + it('should return with underfined if changes length = 0', () => { + const fn = handleCellsChanged(data, setData, validate, afterCellsChanged) + const result = fn([]) + expect(result).toBeUndefined() + }) - const changes = [{ row: 1, col: 2, value: 'new' }] + it('remove row case should work', () => { + const changes = [ + { cell: { value: 'London2' }, row: 1, col: 0, value: null }, + { + cell: { value: 'The Black House' }, + row: 1, + col: 1, + value: null, + }, + ] + const fn = handleCellsChanged(data, setData, validate, afterCellsChanged) fn(changes) - const expectedData = data.map(row => [...row]) - expectedData[1][2] = { ...expectedData[1][2], value: 'new' } - expect(validatedDataGenerate).toHaveBeenCalledWith(expectedData, validate) + const expectedNewData = [ + [{ value: 'Office name' }, { value: 'Building Name' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + const expectedChangedCells = [ + { oldCell: { value: 'London2' }, row: 1, col: 0, newCell: { value: null } }, + { + oldCell: { value: 'The Black House' }, + row: 1, + col: 1, + newCell: { value: null }, + }, + ] + expect(validatedDataGenerate).toHaveBeenCalledWith(expectedNewData, validate) + expect(afterCellsChanged).toHaveBeenCalledWith(expectedChangedCells, 'validated data', setData) expect(setData).toHaveBeenCalledWith('validated data') }) + + it('remove col case should work', () => { + const changes = [ + { cell: { value: 'Building name' }, row: 0, col: 1, value: null }, + { + cell: { value: 'The Black House' }, + row: 1, + col: 1, + value: null, + }, + { + cell: { value: 'Building A' }, + row: 2, + col: 1, + value: null, + }, + ] + const fn = handleCellsChanged(data, setData, validate, afterCellsChanged) + fn(changes) + const expectedNewData = [[{ value: 'Office name' }], [{ value: 'London2' }], [{ value: 'New York' }]] + const expectedChangedCells = [ + { oldCell: { value: 'Building Name' }, row: 0, col: 1, newCell: { value: null } }, + { + oldCell: { value: 'The Black House' }, + row: 1, + col: 1, + newCell: { value: null }, + }, + { + oldCell: { value: 'Building A' }, + row: 2, + col: 1, + newCell: { value: null }, + }, + ] + expect(validatedDataGenerate).toHaveBeenCalledWith(expectedNewData, validate) + expect(afterCellsChanged).toHaveBeenCalledWith(expectedChangedCells, 'validated data', setData) + expect(setData).toHaveBeenCalledWith('validated data') + }) + + it('other cases should work', () => { + const changes = [ + { cell: { value: 'Office name' }, row: 0, col: 0, value: '' }, + { + cell: { value: 'London2' }, + row: 1, + col: 0, + value: '', + }, + ] + const fn = handleCellsChanged(data, setData, validate, afterCellsChanged) + const newDataWithValidate = [ + [ + { value: '', isValidated: true }, + { value: 'Building Name', isValidated: true }, + ], + [ + { value: '', isValidated: true }, + { value: 'The Black House', isValidated: true }, + ], + [ + { value: 'New York', isValidated: true }, + { value: 'Building A', isValidated: true }, + ], + ] + ;(validatedDataGenerate as jest.Mock).mockImplementation(() => newDataWithValidate) + fn(changes) + const expectedNewData = [ + [{ value: '' }, { value: 'Building Name' }], + [{ value: '' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + const expectedChangedCells = [ + { oldCell: { value: 'Office name' }, row: 0, col: 0, newCell: { value: '', isValidated: true } }, + { + oldCell: { value: 'London2' }, + row: 1, + col: 0, + newCell: { value: '', isValidated: true }, + }, + ] + expect(validatedDataGenerate).toHaveBeenCalledWith(expectedNewData, validate) + expect(afterCellsChanged).toHaveBeenCalledWith(expectedChangedCells, newDataWithValidate, setData) + expect(setData).toHaveBeenCalledWith(newDataWithValidate) + }) + it('should work with afterCellsChanged undefined', () => { + const changes = [ + { cell: { value: 'Office name' }, row: 0, col: 0, value: '' }, + { + cell: { value: 'London2' }, + row: 1, + col: 0, + value: '', + }, + ] + const fn = handleCellsChanged(data, setData, validate, undefined) + const newDataWithValidate = [ + [ + { value: '', isValidated: true }, + { value: 'Building Name', isValidated: true }, + ], + [ + { value: '', isValidated: true }, + { value: 'The Black House', isValidated: true }, + ], + [ + { value: 'New York', isValidated: true }, + { value: 'Building A', isValidated: true }, + ], + ] + ;(validatedDataGenerate as jest.Mock).mockImplementation(() => newDataWithValidate) + fn(changes) + const expectedNewData = [ + [{ value: '' }, { value: 'Building Name' }], + [{ value: '' }, { value: 'The Black House' }], + [{ value: 'New York' }, { value: 'Building A' }], + ] + + expect(validatedDataGenerate).toHaveBeenCalledWith(expectedNewData, validate) + expect(setData).toHaveBeenCalledWith(newDataWithValidate) + }) }) describe('handleOnChangeInput', () => { @@ -418,7 +573,7 @@ describe('handleAfterDataChanged', () => { const afterDataChanged = jest.fn() const fn = handleAfterDataChanged(data, data, afterDataChanged) fn() - expect(afterDataChanged).toHaveBeenCalledWith(data, 'changes') + expect(afterDataChanged).toHaveBeenCalledWith('changes', data) }) it('should not call afterDataChanged if it undefined', () => { const data = [ diff --git a/packages/elements/src/components/Spreadsheet/context-menu.tsx b/packages/elements/src/components/Spreadsheet/context-menu.tsx index acf3e28299..584c48272d 100644 --- a/packages/elements/src/components/Spreadsheet/context-menu.tsx +++ b/packages/elements/src/components/Spreadsheet/context-menu.tsx @@ -1,58 +1,92 @@ import * as React from 'react' -import { ContextMenuData, SetData, Cell, SelectedMatrix, ContextMenuFCProps } from './types' +import { ContextMenuData, SetContextMenuProp, Cell, SelectedMatrix, ContextMenuFCProps, OnCellsChanged } from './types' import { hideContextMenu } from './handlers' -export const clearRowSetData = (currentRowIndex: number) => (prevData: Cell[][]): Cell[][] => { - const newData = prevData.map((row, rowIndex) => { - /* Loop through row, check if row index = current selected row, - if equal, clear that row value */ - return rowIndex === currentRowIndex ? row.map(cell => ({ ...cell, value: '' })) : row - }) - return newData +// clear means set value to empty string "" +export const clearRow = (data: Cell[][], currentRowIndex: number, onCellsChanged: OnCellsChanged) => { + const oldRow = data[currentRowIndex] + const changedCells = oldRow + .map((oldCell, colIndex) => ({ + cell: oldRow[colIndex], + row: currentRowIndex, + col: colIndex, + value: '', + })) + .filter(({ cell: { readOnly } }) => !readOnly) + // trigger onCellsChanged + onCellsChanged(changedCells) } -export const clearColSetData = (currentColIndex: number) => (prevData: Cell[][]): Cell[][] => { - const newData = prevData.map(row => { - /* Loop through row, in each row, loop through cells, check if col index = current selected col - if equal, clear that cell value */ - return row.map((cell, colIndex) => { - return colIndex === currentColIndex ? { ...cell, value: '' } : cell - }) - }) - return newData +export const clearCol = (data: Cell[][], currentColIndex: number, onCellsChanged: OnCellsChanged) => { + const oldCol = data.map(row => row[currentColIndex]) + const changedCells = oldCol + .map((cell, rowIndex) => ({ + cell: oldCol[rowIndex], + row: rowIndex, + col: currentColIndex, + value: '', + })) + .filter(({ cell: { readOnly } }) => !readOnly) + + // trigger onCellsChanged + onCellsChanged(changedCells) } -export const removeRowSetData = (currentRowIndex: number) => (prevData: Cell[][]): Cell[][] => { - const newData = prevData.filter((row, rowIndex) => row && rowIndex !== currentRowIndex) - return newData +// remove is completely remove the data from sheet +export const removeRow = (data: Cell[][], currentRowIndex: number, onCellsChanged: OnCellsChanged) => { + const oldRow = data[currentRowIndex] + /* After remove, set value to null in changedCells so afterCellsChaned can detect that it was removed */ + const changedCells = oldRow + .map((oldCell, colIndex) => ({ + cell: oldCell, + row: currentRowIndex, + col: colIndex, + value: null, + })) + .filter(({ cell: { readOnly } }) => !readOnly) + onCellsChanged(changedCells) } -export const removeColSetData = (currentColIndex: number) => (prevData: Cell[][]): Cell[][] => { - const newData = prevData.map(row => row.filter((cell, colIndex) => cell && colIndex !== currentColIndex)) - return newData +export const removeCol = (data: Cell[][], currentColIndex: number, onCellsChanged: OnCellsChanged) => { + const oldCol = data.map(row => row[currentColIndex]) + const changedCells = oldCol + .map((cell, rowIndex) => ({ + cell: oldCol[rowIndex], + row: rowIndex, + col: currentColIndex, + value: null, + })) + .filter(({ cell: { readOnly } }) => !readOnly) + // trigger onCellsChanged + onCellsChanged(changedCells) } /** delegate event handler */ -export const handleContextClick = (selected: SelectedMatrix | null, setData: SetData, setContextMenuProp) => event => { +export const handleContextClick = ( + data: Cell[][], + selected: SelectedMatrix | null, + setContextMenuProp: SetContextMenuProp, + onCellsChanged: OnCellsChanged, +) => event => { event.stopPropagation() const { start: { i: currentRowIndex, j: currentColIndex }, } = selected as SelectedMatrix switch (event.target.id) { case 'clear-row': - setData(clearRowSetData(currentRowIndex)) - setContextMenuProp() + clearRow(data, currentRowIndex, onCellsChanged) + setContextMenuProp(hideContextMenu) return 'clear-row' case 'clear-col': - setData(clearColSetData(currentColIndex)) - setContextMenuProp() + clearCol(data, currentColIndex, onCellsChanged) + setContextMenuProp(hideContextMenu) return 'clear-col' case 'remove-row': - setData(removeRowSetData(currentRowIndex)) - setContextMenuProp() + removeRow(data, currentRowIndex, onCellsChanged) + setContextMenuProp(hideContextMenu) return 'remove-row' case 'remove-col': - setData(removeColSetData(currentColIndex)) - setContextMenuProp() + removeCol(data, currentColIndex, onCellsChanged) + setContextMenuProp(hideContextMenu) return 'remove-col' default: return '' @@ -82,10 +116,11 @@ const dataMenu: ContextMenuData[] = [ id: 'clear-row', text: 'Clear row', }, - { - id: 'clear-col', - text: 'Clear column', - }, + // temporary disable + // { + // id: 'clear-col', + // text: 'Clear column', + // }, ], }, { @@ -95,26 +130,28 @@ const dataMenu: ContextMenuData[] = [ id: 'remove-row', text: 'Remove row', }, - { - id: 'remove-col', - text: 'Remove column', - }, + // temporary disable + // { + // id: 'remove-col', + // text: 'Remove column', + // }, ], }, ] export const ContextMenu: React.FC = ({ + data, selected, contextMenuProp: { visible, top, left }, - setData, setContextMenuProp, + onCellsChanged, }) => { const visibleClass = visible ? 'spreadsheet-context-menu-visible' : 'spreadsheet-context-menu-hidden' return (
    {createMenu(dataMenu)}
    diff --git a/packages/elements/src/components/Spreadsheet/handlers.tsx b/packages/elements/src/components/Spreadsheet/handlers.tsx index 0a46171ee1..ac4f87d70b 100644 --- a/packages/elements/src/components/Spreadsheet/handlers.tsx +++ b/packages/elements/src/components/Spreadsheet/handlers.tsx @@ -7,9 +7,11 @@ import { SetContextMenuProp, SetData, SetSelected, - ChangedCells, ValidateFunction, ContextMenuProp, + AfterDataChanged, + AfterCellsChanged, + ChangesArray, } from './types' import { getMaxRowAndCol, @@ -53,9 +55,12 @@ export const handleContextMenu = (setContextMenuProp: SetContextMenuProp) => e = } /** all the customization of cell go here */ -export const customCellRenderer = (data: Cell[][], setData: SetData, setSelected: SetSelected) => ( - props: ReactDataSheet.CellRendererProps, -) => { +export const customCellRenderer = ( + data: Cell[][], + setData: SetData, + setSelected: SetSelected, + afterCellsChanged?: AfterCellsChanged, +) => (props: ReactDataSheet.CellRendererProps) => { const { style: defaultStyle, cell, onDoubleClick, ...restProps } = props const { CustomComponent = false, @@ -110,7 +115,13 @@ export const customCellRenderer = (data: Cell[][], setData: SetData, setSelected onDoubleClick={onDoubleClickCell(payload, setSelected, onDoubleClick)} > {CustomComponent ? ( - + ) : ( props.children )} @@ -148,12 +159,51 @@ export const handleAddNewRow = (data: Cell[][], setData: SetData, validate?: Val setData(dataWithIsValidated) } -export const handleCellsChanged = (data: Cell[][], setData: SetData, validate?: ValidateFunction) => changes => { +export const handleCellsChanged = ( + data: Cell[][], + setData: SetData, + validate?: ValidateFunction, + afterCellsChanged?: AfterCellsChanged, +) => (changes: ChangesArray): any => { + if (changes.length === 0) { + return + } const newData = data.map(row => [...row]) - changes.forEach(({ row, col, value }) => { - newData[row][col] = { ...newData[row][col], value } - }) + // remove row case + let newCell: Cell = { value: 'newValue' } + if (changes.every(({ value, row }, index, changesArray) => value === null && row === changesArray[0].row)) { + const rowIndexToRemove = changes[0].row + newData.splice(rowIndexToRemove, 1) + newCell = { value: null } + } + // remove column case + else if (changes.every(({ value, col }, index, changesArray) => value === null && col === changesArray[0].col)) { + const colIndexToRemove = changes[0].col + newData.forEach((row, rowIndex) => { + newData[rowIndex].splice(colIndexToRemove, 1) + }) + newCell = { value: null } + } + // all other cases + else { + changes.forEach(({ row, col, value }) => { + newData[row][col] = { ...newData[row][col], value } + }) + } + // validate data after changed const dataWithIsValidated = validatedDataGenerate(newData, validate) + if (typeof afterCellsChanged === 'function') { + const changedCells = changes.map(({ row, col }) => ({ + oldCell: data[row][col], + row, + col, + /* Replace newCell with validated data, if cannot find, then it was deleted, + replace with value = null */ + newCell: newCell.value === null ? { value: null } : dataWithIsValidated[row][col], + })) + afterCellsChanged(changedCells, dataWithIsValidated, setData) + } + // and set to spreadsheet setData(dataWithIsValidated) } @@ -214,11 +264,11 @@ export const handleSetContextMenu = (setContextMenuProp: SetContextMenuProp) => export const handleAfterDataChanged = ( data: Cell[][], prevData?: Cell[][], - afterDataChanged?: (data: Cell[][], changedCells: ChangedCells) => any, + afterDataChanged?: AfterDataChanged, ) => () => { - const changedCells = changedCellsGenerate(data, prevData) if (typeof afterDataChanged === 'function') { - afterDataChanged(data, changedCells) + const changedCells = changedCellsGenerate(data, prevData) + afterDataChanged(changedCells, data) } } diff --git a/packages/elements/src/components/Spreadsheet/index.tsx b/packages/elements/src/components/Spreadsheet/index.tsx index 79e860d635..669d149fd3 100644 --- a/packages/elements/src/components/Spreadsheet/index.tsx +++ b/packages/elements/src/components/Spreadsheet/index.tsx @@ -57,8 +57,10 @@ export const Spreadsheet: React.FC = ({ hasUploadButton = true, hasDownloadButton = true, hasAddButton = true, - afterDataChanged, validate, + afterDataChanged, + afterCellsChanged, + ...rest }) => { const [selected, setSelected] = React.useState(null) @@ -72,7 +74,9 @@ export const Spreadsheet: React.FC = ({ left: 0, }) - const cellRenderer = React.useCallback(customCellRenderer(data, setData, setSelected), [data]) + const cellRenderer = React.useCallback(customCellRenderer(data, setData, setSelected, afterCellsChanged), [data]) + + const onCellsChanged = handleCellsChanged(data, setData, validate, afterCellsChanged) React.useEffect(handleSetContextMenu(setContextMenuProp.bind(null, hideContextMenu)), []) @@ -90,14 +94,15 @@ export const Spreadsheet: React.FC = ({
    {hasAddButton && } @@ -105,8 +110,9 @@ export const Spreadsheet: React.FC = ({
    ) @@ -114,5 +120,3 @@ export const Spreadsheet: React.FC = ({ export * from './types' export * from './utils' - -export * from './utils' diff --git a/packages/elements/src/components/Spreadsheet/spreadsheet.stories.tsx b/packages/elements/src/components/Spreadsheet/spreadsheet.stories.tsx index 1343c701cb..2784ad4f1b 100644 --- a/packages/elements/src/components/Spreadsheet/spreadsheet.stories.tsx +++ b/packages/elements/src/components/Spreadsheet/spreadsheet.stories.tsx @@ -1,6 +1,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import { Spreadsheet, setCurrentCellValue, Cell } from './index' +import { Spreadsheet, Cell } from './index' storiesOf('Spreadsheet', module) .add('Basic', () => { @@ -64,7 +64,7 @@ storiesOf('Spreadsheet', module) data={dataBasic} description={

    - Basic DataSheet + Basic Spreadsheet
    You can double click a column header to select the entire column's cells.
    @@ -151,7 +151,12 @@ storiesOf('Spreadsheet', module) } description={

    - DataSheet with validate + Spreadsheet with validate +
    + The validate function must return with correct format +
    + For example: return [ [true, false], [true, true] ] in case your spreadsheet has{' '} + 2x2 (row X column)
    Errors are marked with red background

    @@ -237,7 +242,7 @@ storiesOf('Spreadsheet', module) data={dataCustomStyle} description={

    - DataSheet with custom styles + Spreadsheet with custom styles
    Add custom style to cell by using className property of cell
    @@ -248,16 +253,24 @@ storiesOf('Spreadsheet', module) ) }) .add('Custom Component', () => { - /* follow this pattern to create custom eleemnt */ - const CustomComponent = ({ cellRenderProps, data, setData }) => { + /* follow this pattern to create custom component */ + const CustomComponent = ({ cellRenderProps, data, setData, afterCellsChanged }) => { return (