diff --git a/package.json b/package.json
index 0615624cd6..7c1e7d546a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@reapit/elements",
- "version": "0.5.17",
+ "version": "0.5.19",
"description": "A collection of React components and utilities for building apps for Reapit Marketplace",
"main": "dist/index.js",
"umd:main": "dist/elements.umd.production.js",
@@ -74,7 +74,7 @@
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"rimraf": "^2.7.0",
- "rollup": "^1.27.9",
+ "rollup": "1.27.13",
"rollup-plugin-scss": "^1.0.2",
"sass-loader": "^7.2.0",
"style-loader": "^1.0.0",
@@ -86,7 +86,8 @@
"tslint-config-prettier": "^1.18.0",
"tslint-config-standard": "^8.0.1",
"tslint-plugin-prettier": "^2.0.1",
- "typescript": "3.7.2"
+ "typescript": "3.7.2",
+ "webpack": "4.41.3"
},
"dependencies": {
"bulma": "^0.7.5",
@@ -97,6 +98,7 @@
"jsonwebtoken": "^8.5.1",
"pell": "^1.0.6",
"prop-types": "^15.7.2",
+ "react-datasheet": "^1.4.0",
"react-datepicker": "^2.9.6",
"react-google-map": "^3.1.1",
"react-google-maps-loader": "^4.2.5",
diff --git a/src/components/AcDynamicLinks/ac-dynamic-links.stories.tsx b/src/components/AcDynamicLinks/ac-dynamic-links.stories.tsx
index c9e77da48b..7852954524 100644
--- a/src/components/AcDynamicLinks/ac-dynamic-links.stories.tsx
+++ b/src/components/AcDynamicLinks/ac-dynamic-links.stories.tsx
@@ -144,7 +144,7 @@ export const dynamicLinkScenarios: DynamicLinkScenario[] = [
}
]
-storiesOf('AcDynamicLinks', module).add('AcButtonsAndLinks', () => (
+storiesOf('DynamicLinks', module).add('DynamicButtonsAndLinks', () => (
{dynamicLinkScenarios.map((scenario: DynamicLinkScenario, index: number) => (
diff --git a/src/components/Form/form.stories.tsx b/src/components/Form/form.stories.tsx
index dfb94c7f68..780e1bed9a 100644
--- a/src/components/Form/form.stories.tsx
+++ b/src/components/Form/form.stories.tsx
@@ -24,7 +24,13 @@ export const FormExample: React.SFC = () => (
Section One
Information about this section to help the user
-
+
@@ -36,7 +42,7 @@ export const FormExample: React.SFC = () => (
id="passwordConfirm"
type="password"
placeholder="********"
- name="password"
+ name="passwordConfirm"
labelText="Password Confirm"
/>
@@ -46,7 +52,13 @@ export const FormExample: React.SFC = () => (
Section Three
Information about this section to help the user
-
+
(
- Section Three
+ Section Four
Information about this section to help the user
- Section Four
+ Section Five
Information about this section to help the user
diff --git a/src/components/RadioSelect/__tests__/index.tsx b/src/components/RadioSelect/__tests__/index.tsx
index 574f0653dc..9ce4a62d5e 100644
--- a/src/components/RadioSelect/__tests__/index.tsx
+++ b/src/components/RadioSelect/__tests__/index.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { shallow, mount } from 'enzyme'
import RadioSelect from '../index'
-import { Formik, Form } from 'formik'
+import { Formik, Form, FormikValues } from 'formik'
describe('RadioSelect', () => {
it('should match snapshot', () => {
@@ -12,7 +12,7 @@ describe('RadioSelect', () => {
dataTest: 'mockDatatest',
options: [{ label: 'label', value: 'value' }, { label: 'label1', value: 'value1' }]
}
- const wrapper = shallow()
+ const wrapper = shallow()
expect(wrapper).toMatchSnapshot()
})
@@ -25,10 +25,10 @@ describe('RadioSelect', () => {
options: [{ label: 'label', value: 'value' }, { label: 'label1', value: 'value1' }]
}
const wrapper = mount(
-
- {() => (
+
+ {({ setFieldValue, values }) => (
)}
diff --git a/src/components/RadioSelect/index.stories.tsx b/src/components/RadioSelect/index.stories.tsx
index b72ded0bf9..b9eaa82b71 100644
--- a/src/components/RadioSelect/index.stories.tsx
+++ b/src/components/RadioSelect/index.stories.tsx
@@ -4,6 +4,7 @@ import { storiesOf } from '@storybook/react'
import { RadioSelect } from '.'
import { Formik, Form } from 'formik'
import { action } from '@storybook/addon-actions'
+import { Button } from '../Button'
storiesOf('RadioSelect', module).add('Primary', () => {
const mockProps = {
@@ -16,16 +17,20 @@ storiesOf('RadioSelect', module).add('Primary', () => {
return (
{
action('Form Values' + values)
+ console.log(values)
}}
>
- {() => (
+ {({ setFieldValue, values }) => (
)}
diff --git a/src/components/RadioSelect/index.tsx b/src/components/RadioSelect/index.tsx
index 226dc3565d..d1deaf58fd 100644
--- a/src/components/RadioSelect/index.tsx
+++ b/src/components/RadioSelect/index.tsx
@@ -13,12 +13,22 @@ export type RadioSelectProps = {
id: string
dataTest?: string
options: RadioSelectOption[]
+ setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void
+ state: any
}
-export const RadioSelect: React.FC = ({ name, labelText, id, dataTest, options }) => {
+export const RadioSelect: React.FC = ({
+ name,
+ labelText,
+ id,
+ dataTest,
+ options,
+ setFieldValue,
+ state
+}) => {
return (
- {({ field, meta }: FieldProps) => {
+ {({ meta }: FieldProps) => {
const hasError = checkError(meta)
const className = hasError ? 'input is-danger' : ''
return (
@@ -30,15 +40,16 @@ export const RadioSelect: React.FC = ({ name, labelText, id, d
{options.map(({ label, value }: RadioSelectOption, index: number) => (
setFieldValue(name, value)}
/>
-
+
))}
diff --git a/src/components/Spreadsheet/__stubs__/index.tsx b/src/components/Spreadsheet/__stubs__/index.tsx
new file mode 100644
index 0000000000..9b1e2da1e3
--- /dev/null
+++ b/src/components/Spreadsheet/__stubs__/index.tsx
@@ -0,0 +1,84 @@
+import * as React from 'react'
+import { Cell, SelectedMatrix } from '../types'
+import ReactDataSheet from 'react-datasheet'
+
+export const data: Cell[][] = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ { value: 'The White House' },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { 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' }
+ ]
+]
+
+export const cellRenderProps: ReactDataSheet.CellRendererProps = {
+ row: 3,
+ col: 10,
+ cell: { value: 'row3@gmail.com' },
+ selected: false,
+ editing: false,
+ updated: false,
+ attributesRenderer: jest.fn() as ReactDataSheet.AttributesRenderer,
+ className: 'cell',
+ style: { background: 'red' },
+ onMouseDown: jest.fn(),
+ onMouseOver: jest.fn(),
+ onDoubleClick: jest.fn(),
+ onContextMenu: jest.fn(),
+ children: hi
+}
+
+export const setData: React.Dispatch = jest.fn()
+
+export const setSelected: React.Dispatch = jest.fn()
+
+export const selectedMatrix = {
+ start: { i: 0, j: 1 },
+ end: { i: 2, j: 3 }
+}
diff --git a/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap b/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap
new file mode 100644
index 0000000000..32c06e75d1
--- /dev/null
+++ b/src/components/Spreadsheet/__tests__/__snapshots__/handlers.tsx.snap
@@ -0,0 +1,260 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`onDoubleClickCell customCellRenderer should match snapshot with CustomComponent 1`] = `
+
+
+ hi
+ ,
+ "className": "cell",
+ "col": 10,
+ "editing": false,
+ "onContextMenu": [MockFunction],
+ "onDoubleClick": [MockFunction],
+ "onMouseDown": [MockFunction],
+ "onMouseOver": [MockFunction],
+ "row": 3,
+ "selected": false,
+ "style": Object {
+ "background": "red",
+ },
+ "updated": false,
+ }
+ }
+ data={
+ Array [
+ Array [
+ Object {
+ "readOnly": true,
+ "value": "Office Name",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Building Name",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Building No.",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Address 1",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Address 2",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Address 3",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Address 4",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Post Code",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Telephone",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Fax",
+ },
+ Object {
+ "readOnly": true,
+ "value": "Email",
+ },
+ ],
+ Array [
+ Object {
+ "value": "London",
+ },
+ Object {
+ "value": "The White House",
+ },
+ Object {
+ "value": "15",
+ },
+ Object {
+ "value": "London 1",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "Londom 3",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "EC12NH",
+ },
+ Object {
+ "value": "0845 0000",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "row1@gmail.com",
+ },
+ ],
+ Array [
+ Object {
+ "value": "London2",
+ },
+ Object {
+ "value": "The Black House",
+ },
+ Object {
+ "value": "11",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "Adress 3",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "EC12NH",
+ },
+ Object {
+ "value": "087 471 929",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "row2@gmail.com",
+ },
+ ],
+ Array [
+ Object {
+ "value": "New York",
+ },
+ Object {
+ "value": "Building A",
+ },
+ Object {
+ "value": "11",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "City Z",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "AL7187",
+ },
+ Object {
+ "value": "017 7162 9121",
+ },
+ Object {
+ "value": "",
+ },
+ Object {
+ "value": "row3@gmail.com",
+ },
+ ],
+ ]
+ }
+ setData={[MockFunction]}
+ setSelected={[MockFunction]}
+ />
+ |
+`;
+
+exports[`onDoubleClickCell customCellRenderer should match snapshot with invalid cell 1`] = `
+
+
+ hi
+
+ |
+`;
+
+exports[`onDoubleClickCell customCellRenderer should match snapshot without CustomComponent 1`] = `
+
+
+ hi
+
+ |
+`;
diff --git a/src/components/Spreadsheet/__tests__/handlers.tsx b/src/components/Spreadsheet/__tests__/handlers.tsx
new file mode 100644
index 0000000000..96567db3e7
--- /dev/null
+++ b/src/components/Spreadsheet/__tests__/handlers.tsx
@@ -0,0 +1,147 @@
+import * as React from 'react'
+import ReactDataSheet from 'react-datasheet'
+import { shallow } from 'enzyme'
+import {
+ onDoubleClickCell,
+ valueRenderer,
+ onSelectCells,
+ customCellRenderer,
+ handleAddNewRow,
+ handleCellsChanged
+} from '../handlers'
+import { data, cellRenderProps, selectedMatrix, setData, setSelected } from '../__stubs__'
+
+const onDoubleClickDefault = jest.fn()
+
+afterEach(() => {
+ jest.resetAllMocks()
+})
+
+describe('valueRenderer', () => {
+ it('should return value', () => {
+ const result = valueRenderer(cellRenderProps.cell)
+ expect(result).toBe(cellRenderProps.cell.value)
+ })
+})
+
+describe('onDoubleClickCell', () => {
+ it('should call setSelected if row = 0 and isReadOnly, and return true', () => {
+ const payload = {
+ row: 0,
+ col: 1,
+ maxRowIndex: 3,
+ maxColIndex: 4,
+ isReadOnly: true
+ }
+ const fn = onDoubleClickCell(payload, setSelected, onDoubleClickDefault)
+ const result = fn(1, 2)
+ expect(onDoubleClickDefault).toHaveBeenCalledWith(1, 2)
+ expect(setSelected).toHaveBeenCalledWith({
+ start: { i: 0, j: payload.col },
+ end: {
+ i: payload.maxRowIndex,
+ j: payload.col
+ }
+ })
+ expect(result).toBe(true)
+ })
+ it('should not call setSelected when row !== 0, and return false', () => {
+ const payload = {
+ row: 2,
+ col: 1,
+ maxRowIndex: 3,
+ maxColIndex: 4,
+ isReadOnly: true
+ }
+ const fn = onDoubleClickCell(payload, setSelected, onDoubleClickDefault)
+ const result = fn(1, 2)
+ expect(onDoubleClickDefault).toHaveBeenCalledWith(1, 2)
+ expect(setSelected).not.toHaveBeenCalled()
+ expect(result).toBe(false)
+ })
+ it('should not call setSelected when isReadOnly = false, and return false', () => {
+ const payload = {
+ row: 2,
+ col: 1,
+ maxRowIndex: 3,
+ maxColIndex: 4,
+ isReadOnly: false
+ }
+ const fn = onDoubleClickCell(payload, setSelected, onDoubleClickDefault)
+ const result = fn(1, 2)
+ expect(onDoubleClickDefault).toHaveBeenCalledWith(1, 2)
+ expect(setSelected).not.toHaveBeenCalled()
+ expect(result).toBe(false)
+ })
+
+ describe('onSelectCell', () => {
+ it('should call setSelected with right arg', () => {
+ const fn = onSelectCells(setSelected)
+ fn(selectedMatrix)
+ expect(setSelected).toHaveBeenCalledWith(selectedMatrix)
+ })
+ })
+
+ describe('customCellRenderer', () => {
+ it('should match snapshot without CustomComponent', () => {
+ const CellComponent = customCellRenderer(data, setData, setSelected)
+ expect(shallow()).toMatchSnapshot()
+ })
+ it('should match snapshot with CustomComponent', () => {
+ const cellRenderPropsCustomComponent = {
+ ...cellRenderProps,
+ cell: {
+ ...cellRenderProps.cell,
+ CustomComponent: () => Custom Component
+ }
+ }
+ const CellComponent = customCellRenderer(data, setData, setSelected)
+ expect(shallow()).toMatchSnapshot()
+ })
+
+ it('should match snapshot with invalid cell', () => {
+ const cellRenderPropsInvalid = {
+ ...cellRenderProps,
+ cell: { value: '11aa', validate: cell => Number.isInteger(Number(cell.value)) }
+ }
+
+ const CellComponent = customCellRenderer(data, setData, setSelected)
+ expect(shallow()).toMatchSnapshot()
+ })
+ })
+})
+
+describe('handleAddNewRow', () => {
+ it('should call setData with correct arg', () => {
+ const getMaxRowAndCol = jest.fn(data => [data.length, data[0].length])
+ const fn = handleAddNewRow(data, setData)
+ fn()
+ const expectedResult = [...data]
+ expectedResult.push([
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' },
+ { value: '' }
+ ])
+ expect(setData).toHaveBeenCalledWith(expectedResult)
+ })
+})
+
+describe('handleCellsChanged', () => {
+ it('should call setData with correct arg', () => {
+ const fn = handleCellsChanged(data, setData)
+
+ const changes = [{ row: 1, col: 2, value: 'new' }]
+ fn(changes)
+ const expectResult = data.map(row => [...row])
+ expectResult[1][2] = { ...expectResult[1][2], value: 'new' }
+ expect(setData).toHaveBeenCalledWith(expectResult)
+ })
+})
diff --git a/src/components/Spreadsheet/__tests__/index.tsx b/src/components/Spreadsheet/__tests__/index.tsx
new file mode 100644
index 0000000000..061cbd5770
--- /dev/null
+++ b/src/components/Spreadsheet/__tests__/index.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react'
+import ReactDataSheet from 'react-datasheet'
+import { shallow } from 'enzyme'
+import { Spreadsheet } from '../index'
+import { data } from '../__stubs__'
+
+describe('Spreadsheet', () => {
+ it('should match snapshot with default props', () => {
+ expect(shallow())
+ })
+ it('should match snapshot with full props', () => {
+ expect(
+ shallow(
+
+ )
+ )
+ })
+})
diff --git a/src/components/Spreadsheet/__tests__/utils.ts b/src/components/Spreadsheet/__tests__/utils.ts
new file mode 100644
index 0000000000..c9bf020d6b
--- /dev/null
+++ b/src/components/Spreadsheet/__tests__/utils.ts
@@ -0,0 +1,30 @@
+import { data, setData } from '../__stubs__'
+import { getMaxRowAndCol, setCurrentCellValue } from '../utils'
+import { Cell } from '../types'
+
+afterEach(() => {
+ jest.resetAllMocks()
+})
+
+describe('getMaxRowAndCol', () => {
+ it('should return correct value with same-length rows and columns', () => {
+ const result = getMaxRowAndCol(data)
+ expect(result).toEqual([data.length, data[0].length])
+ })
+ it('should return correct value with different-length col', () => {
+ const newData = [...data]
+ newData.push([{ value: 'val' }])
+
+ const result = getMaxRowAndCol(newData)
+ expect(result).toEqual([newData.length, data[0].length])
+ })
+})
+
+describe('setCurrentCellValue', () => {
+ it('should call setData with correct arg', () => {
+ setCurrentCellValue('cell value', data, 2, 3, setData)
+ const newData = [...data]
+ newData[2][3].value = 'cell value'
+ expect(setData).toHaveBeenCalledWith(newData)
+ })
+})
diff --git a/src/components/Spreadsheet/handlers.tsx b/src/components/Spreadsheet/handlers.tsx
new file mode 100644
index 0000000000..84b20bb5fb
--- /dev/null
+++ b/src/components/Spreadsheet/handlers.tsx
@@ -0,0 +1,107 @@
+import * as React from 'react'
+import ReactDataSheet from 'react-datasheet'
+import { Cell, DoubleClickPayLoad, SelectedMatrix } from './types'
+import { getMaxRowAndCol } from './utils'
+
+export const valueRenderer = (cell: Cell): string => cell.value
+
+/** Double click on first read-only cell */
+export const onDoubleClickCell = (
+ payload: DoubleClickPayLoad,
+ setSelected: React.Dispatch,
+ onDoubleClickDefault
+) => (...args): boolean => {
+ /* trigger default handler from lib */
+ onDoubleClickDefault(...args)
+ const { row, col, maxRowIndex, isReadOnly } = payload
+ const isFirstRow = row === 0
+ if (isFirstRow && isReadOnly) {
+ /* select all row's cells */
+ setSelected({
+ start: { i: 0, j: col },
+ end: { i: maxRowIndex, j: col }
+ })
+ return true
+ }
+ return false
+}
+
+export const onSelectCells = (setSelected: React.Dispatch) => ({ start, end }: SelectedMatrix) => {
+ setSelected({ start, end })
+}
+
+/* export const handleContextMenu: ReactDataSheet.ContextMenuHandler = (e, cell, i, j) => {
+ console.log('sad')
+} */
+
+/** all the customization of cell go here */
+export const customCellRenderer = (
+ data: Cell[][],
+ setData: React.Dispatch,
+ setSelected: React.Dispatch
+) => (props: ReactDataSheet.CellRendererProps) => {
+ const { style: defaultStyle, cell, onDoubleClick, ...restProps } = props
+ const {
+ CustomComponent = false,
+ validate = () => true,
+ className = '',
+ readOnly,
+ style: customStyle,
+ ...restCell
+ } = cell
+ const isValid = validate(cell)
+ const [maxRowIndex, maxColIndex] = getMaxRowAndCol(data)
+ const payload = {
+ row: props.row,
+ col: props.col,
+ maxRowIndex,
+ maxColIndex,
+ isReadOnly: readOnly
+ }
+ const style = {
+ ...defaultStyle,
+ ...customStyle
+ }
+ return (
+
+ {CustomComponent ? (
+
+ ) : (
+ props.children
+ )}
+ |
+ )
+}
+
+export const handleAddNewRow = (data: Cell[][], setData: React.Dispatch) => () => {
+ const [maxRow] = getMaxRowAndCol(data)
+ const lastRow = data[maxRow - 1]
+ /* [
+ { readOnly: true, value: '' },
+ { value: 'A', readOnly: true },
+ { value: 'B', readOnly: true },
+ { value: 'C', readOnly: true },
+ { value: 'D', readOnly: true }
+ ] */
+ /* return new row with same type of last row */
+ const newEmptyRow = lastRow.map(e => ({ ...e, value: '' }))
+ const newData = [...data, newEmptyRow]
+ setData(newData)
+}
+
+export const handleCellsChanged = (
+ prevData: Cell[][],
+ setData: React.Dispatch /* setData from useState*/
+) => changes => {
+ const newData = prevData.map(row => [...row])
+ changes.forEach(({ row, col, value }) => {
+ newData[row][col] = { ...newData[row][col], value }
+ })
+ setData(newData)
+}
diff --git a/src/components/Spreadsheet/index.tsx b/src/components/Spreadsheet/index.tsx
new file mode 100644
index 0000000000..a2edc0c660
--- /dev/null
+++ b/src/components/Spreadsheet/index.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react'
+import { MyReactDataSheet, Cell, SpreadsheetProps, SelectedMatrix } from './types'
+import {
+ valueRenderer,
+ onSelectCells,
+ customCellRenderer,
+ handleAddNewRow,
+ handleCellsChanged
+ /* handleContextMenu */
+} from './handlers'
+import { Button } from '../Button'
+
+export const Spreadsheet: React.FC = ({
+ data: initialData,
+ description = '',
+ hasUploadButton = true,
+ hasDownloadButton = true,
+ hasAddButton = true
+}) => {
+ const [selected, setSelected] = React.useState(null)
+
+ const [data, setData] = React.useState(initialData)
+
+ const cellRenderer = React.useCallback(customCellRenderer(data, setData, setSelected), [data])
+ const onSelect = React.useCallback(onSelectCells(setSelected), [])
+ const onCellsChanged = React.useCallback(handleCellsChanged(data, setData), [data])
+ const addNewRow = React.useCallback(handleAddNewRow(data, setData), [data])
+
+ return (
+
+
+ {description}
+
+ {hasUploadButton && (
+
+
+
+ )}
+ {hasDownloadButton && (
+
+
+
+ )}
+
+
+
+
+ {hasAddButton && (
+
+
+
+ )}
+
+
+ )
+}
+
+export * from './types'
+export * from './utils'
diff --git a/src/components/Spreadsheet/spreadsheet.stories.tsx b/src/components/Spreadsheet/spreadsheet.stories.tsx
new file mode 100644
index 0000000000..29d2ecbc2b
--- /dev/null
+++ b/src/components/Spreadsheet/spreadsheet.stories.tsx
@@ -0,0 +1,391 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { Spreadsheet, setCurrentCellValue, Cell } from './index'
+
+storiesOf('Spreadsheet', module)
+ .add('Basic', () => {
+ const dataBasic = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ { value: 'The White House' },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { 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' }
+ ]
+ ]
+
+ return (
+
+ Basic DataSheet
+
+ You can double click a column header to select the entire column's cells.
+
+ Select one or multiple cells and press Delete/Backspace to delete it value.
+ | | | | | | | | |
+ }
+ />
+ )
+ })
+ .add('Validate', () => {
+ const dataValidate = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ { value: 'The White House' },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11aa', validate: cell => Number.isInteger(Number(cell.value)) },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { value: 'New York' },
+ { value: 'Building A' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'City Z' },
+ { value: '' },
+ { value: 'AL7187' },
+ { value: '017 7162 9121' },
+ { value: '' },
+ {
+ value: 'row3@com',
+ validate: cell => {
+ const emailRegex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
+ return emailRegex.test(cell.value)
+ }
+ }
+ ]
+ ]
+ return (
+
+ DataSheet with validate
+
+ Errors are marked with red background
+
+ }
+ />
+ )
+ })
+ .add('Custom Style', () => {
+ const dataBasic = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ { value: 'The White House' },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { 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' }
+ ]
+ ]
+ const dataCustomStyle = (dataBasic as Cell[][]).map((e, i) => {
+ if (i % 2 !== 0) {
+ /* customize by style */
+ return e.map(c => ({
+ ...c,
+ style: {
+ background: '#6A5ACD',
+ color: '#fff'
+ }
+ }))
+ }
+ /* customize by className */
+ return e.map(c => ({
+ ...c,
+ className: 'custom-classname-style'
+ }))
+ })
+
+ return (
+
+ DataSheet with custom styles
+
+ Add custom style to cell by using className property of cell
+
+ Or if you want to override default style, use style proprty
+
+ }
+ />
+ )
+ })
+ .add('Custom Component', () => {
+ /* follow this pattern to create custom eleemnt */
+ const CustomComponent = ({ cellRenderProps, data, setData }) => {
+ return (
+
+ )
+ }
+
+ const dataCustomComponent = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ {
+ value: 'The White House',
+ CustomComponent
+ },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { 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' }
+ ]
+ ]
+
+ return (
+
+
+ DataSheet with <select>
+
+
+ You can create a cell which include custom component by using CustomComponent property, here we use{' '}
+ <select>
as an example. Follow this pattern to create various types of custom
+ components
+
+ }
+ />
+ )
+ })
+ .add('Spreadsheet only', () => {
+ const dataBasic = [
+ [
+ { readOnly: true, value: 'Office Name' },
+ { readOnly: true, value: 'Building Name' },
+ { readOnly: true, value: 'Building No.' },
+ { readOnly: true, value: 'Address 1' },
+ { readOnly: true, value: 'Address 2' },
+ { readOnly: true, value: 'Address 3' },
+ { readOnly: true, value: 'Address 4' },
+ { readOnly: true, value: 'Post Code' },
+ { readOnly: true, value: 'Telephone' },
+ { readOnly: true, value: 'Fax' },
+ { readOnly: true, value: 'Email' }
+ ],
+ [
+ { value: 'London' },
+ { value: 'The White House' },
+ { value: '15' },
+ { value: 'London 1' },
+ { value: '' },
+ { value: 'Londom 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '0845 0000' },
+ { value: '' },
+ { value: 'row1@gmail.com' }
+ ],
+ [
+ { value: 'London2' },
+ { value: 'The Black House' },
+ { value: '11' },
+ { value: '' },
+ { value: '' },
+ { value: 'Adress 3' },
+ { value: '' },
+ { value: 'EC12NH' },
+ { value: '087 471 929' },
+ { value: '' },
+ { value: 'row2@gmail.com' }
+ ],
+ [
+ { 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' }
+ ]
+ ]
+
+ return
+ })
diff --git a/src/components/Spreadsheet/types.tsx b/src/components/Spreadsheet/types.tsx
new file mode 100644
index 0000000000..1fb36c1da3
--- /dev/null
+++ b/src/components/Spreadsheet/types.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import ReactDataSheet from 'react-datasheet'
+
+export class MyReactDataSheet extends ReactDataSheet {}
+
+/** Cell contain predefined value
+ * https://github.com/nadbm/react-datasheet/blob/master/types/react-datasheet.d.ts
+ * plus some properties below
+ */
+export interface Cell extends ReactDataSheet.Cell {
+ /** The value of the cell, always a string */
+ value: string
+ /** The validate function, receive Cell as param, must return boolean */
+ validate?: (cell: Cell) => boolean
+ /** Additional className for styling cell */
+ className?: string
+ style?: React.CSSProperties
+ CustomComponent?: React.FC<{
+ data: Cell[][]
+ cellRenderProps: ReactDataSheet.CellRendererProps
+ setData: React.Dispatch
+ setSelected: React.Dispatch
+ }>
+}
+
+export interface DoubleClickPayLoad {
+ row: number
+ col: number
+ maxRowIndex: number
+ maxColIndex: number
+ isReadOnly?: boolean
+}
+
+export interface SpreadsheetProps {
+ data: Cell[][]
+ description?: React.ReactNode
+ hasUploadButton?: boolean
+ hasDownloadButton?: boolean
+ hasAddButton?: boolean
+}
+
+export interface SelectedMatrix {
+ start: ReactDataSheet.Location
+ end: ReactDataSheet.Location
+}
diff --git a/src/components/Spreadsheet/utils.tsx b/src/components/Spreadsheet/utils.tsx
new file mode 100644
index 0000000000..66fda133d6
--- /dev/null
+++ b/src/components/Spreadsheet/utils.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react'
+import { Cell } from './types'
+
+/** Get max row and col of data */
+export const getMaxRowAndCol = (data: Cell[][]) => {
+ const maxRow = data.length
+ /* default to 0 */
+ let maxCol = 0
+ /* check every row to find max length of column */
+ data.forEach(row => {
+ const numberOfCurrentRowColumn = row.length
+ if (maxCol < numberOfCurrentRowColumn) {
+ maxCol = numberOfCurrentRowColumn
+ }
+ })
+ return [maxRow, maxCol]
+}
+
+export const setCurrentCellValue = (
+ cellData: string,
+ data: Cell[][],
+ row: number,
+ col: number,
+ setData: React.Dispatch
+): void => {
+ const newData = [...data]
+ newData[row][col].value = cellData
+ setData(newData)
+}
diff --git a/src/components/Wizard/index.tsx b/src/components/Wizard/index.tsx
index ee60ad23f6..f4b82ed66d 100644
--- a/src/components/Wizard/index.tsx
+++ b/src/components/Wizard/index.tsx
@@ -112,7 +112,7 @@ export interface WizardStepProps {
type: WizardActionType
context: WizardContextValues
}) => Promise
- onSubmit?: (params: { values: T; context: WizardContextValues }) => void
+ onSubmit?: (params: { values: T; context: WizardContextValues; form: FormikProps }) => void
}
Wizard.Step = function({ Component, initialValue, onNavigate, validate, onSubmit }: WizardStepProps) {
@@ -124,8 +124,8 @@ Wizard.Step = function({ Component, initialValue, onNavigate, validate, onSub
{
- onSubmit && onSubmit({ values, context })
+ onSubmit={(values, form) => {
+ onSubmit && onSubmit({ values, context, form: form as FormikProps })
}}
>
{form => {
@@ -135,7 +135,14 @@ Wizard.Step = function({ Component, initialValue, onNavigate, validate, onSub
rightRender = (
<>
{!isLast && (
-
) : (
onSubmit && onSubmit({ values: form.values, context, form })}
+ type="button"
variant="primary"
loading={isLoading}
className="ml-2"
diff --git a/src/components/Wizard/wizard.stories.tsx b/src/components/Wizard/wizard.stories.tsx
index 085938d943..b3b9b5dec0 100644
--- a/src/components/Wizard/wizard.stories.tsx
+++ b/src/components/Wizard/wizard.stories.tsx
@@ -34,15 +34,25 @@ const BasicUsage = () => {
afterClose={() => setVisible(false)}
leftFooterRender={({ context }) => #{context.currentIndex + 1} }
>
- Step 1 }>
- Step 2 }>
- Step 3 }>
+ context.close()}
+ id="step-1"
+ Component={() => Step 1 }
+ >
+ context.close()}
+ id="step-2"
+ Component={() => Step 2 }
+ >
+ context.close()}
+ id="step-3"
+ Component={() => Step 3 }
+ >
Step 4 }
- onSubmit={({ context }) => {
- context.close()
- }}
+ onSubmit={({ context }) => context.close()}
>
@@ -55,39 +65,50 @@ storiesOf('Wizard', module).add('Basic', () => )
//////////////////////////////////////////////////////////
const WorkWithFormElements = () => {
const [formData, setFormData] = useState({ firstName: '', lastName: '' })
+ const [visible, setVisible] = useState(false)
return (
-
-
- id="step-1"
- Component={() => (
-
- )}
- initialValue={{ firstName: formData.firstName }}
- validate={values => {
- let errors = {} as { firstName: string }
- if (!values.firstName) {
- errors.firstName = 'required'
- }
+
+ setVisible(true)} variant={'primary'} type={'button'}>
+ Open
+
+ setVisible(false)}>
+
+ id="step-1"
+ Component={() => (
+
+ )}
+ initialValue={{ firstName: formData.firstName }}
+ validate={values => {
+ let errors = {} as { firstName: string }
+ if (!values.firstName) {
+ errors.firstName = 'required'
+ }
- return errors
- }}
- onSubmit={({ values }) => {
- alert(JSON.stringify(values))
- }}
- onNavigate={async ({ form }) => {
- const values = form.values
- setFormData(prev => ({ ...prev, ...values }))
- return true
- }}
- >
- (
-
- )}
- >
-
+ return errors
+ }}
+ onSubmit={({ values, context }) => {
+ alert(JSON.stringify(values))
+ context.close()
+ }}
+ onNavigate={async ({ form }) => {
+ const values = form.values
+ setFormData(prev => ({ ...prev, ...values }))
+ return true
+ }}
+ >
+ (
+
+ )}
+ onSubmit={({ values, context }) => {
+ alert(JSON.stringify(values))
+ context.close()
+ }}
+ >
+
+
)
}
storiesOf('Wizard', module).add('HasForm', () => )
@@ -97,62 +118,83 @@ storiesOf('Wizard', module).add('HasForm', () => )
//////////////////////////////////////////////////////////
const CustomFooterNavigation = () => {
const [isLoading, setIsLoading] = useState(false)
+ const [visible, setVisible] = useState(false)
return (
- {
- const { steps, currentIndex, isLast } = context
- const onNavigate = steps[currentIndex].onNavigate
- return !isLast ? (
- {
- const errors = await form.validateForm()
- form.setErrors(errors)
- form.setTouched(Object.keys(errors).reduce((a, c) => ({ ...a, [c]: true }), {}))
- if (Object.keys(errors).length === 0) {
- setIsLoading(true)
- await (() => new Promise(resolve => setTimeout(resolve, 2000)))()
- setIsLoading(false)
- onNavigate && onNavigate({ context, form, type: 'next' })
- context.goNext()
- }
- }}
- >
- Next
-
- ) : null
- }}
- >
-
- id="step-1"
- Component={() => (
-
-
- You can only go next if the item is valid
-
- )}
- initialValue={{ formItem: '' }}
- validate={values => {
- let errors = {} as { formItem: string }
- if (!values.formItem) {
- errors.formItem = 'required'
- }
-
- return errors
+
+ setVisible(true)} variant="primary" type="button">
+ Open
+
+ setVisible(false)}
+ rightFooterRender={({ context, form }) => {
+ const { steps, currentIndex, isLast } = context
+ const onNavigate = steps[currentIndex].onNavigate
+ return !isLast ? (
+ {
+ const errors = await form.validateForm()
+ form.setErrors(errors)
+ form.setTouched(Object.keys(errors).reduce((a, c) => ({ ...a, [c]: true }), {}))
+ if (Object.keys(errors).length === 0) {
+ setIsLoading(true)
+ await (() => new Promise(resolve => setTimeout(resolve, 2000)))()
+ setIsLoading(false)
+ onNavigate && onNavigate({ context, form, type: 'next' })
+ context.goNext()
+ }
+ }}
+ >
+ Next
+
+ ) : null
}}
- >
- Success message }>
-
+ >
+
+ id="step-1"
+ Component={() => (
+
+
+ You can only go next if the item is valid
+
+ )}
+ initialValue={{ formItem: '' }}
+ validate={values => {
+ let errors = {} as { formItem: string }
+ if (!values.formItem) {
+ errors.formItem = 'required'
+ }
+
+ return errors
+ }}
+ onSubmit={async ({ context, form }) => {
+ const { steps, currentIndex } = context
+ const onNavigate = steps[currentIndex].onNavigate
+ const errors = await form.validateForm()
+ form.setErrors(errors)
+ form.setTouched(Object.keys(errors).reduce((a, c) => ({ ...a, [c]: true }), {}))
+ if (Object.keys(errors).length === 0) {
+ setIsLoading(true)
+ await (() => new Promise(resolve => setTimeout(resolve, 2000)))()
+ setIsLoading(false)
+ onNavigate && onNavigate({ context, form, type: 'next' })
+ context.goNext()
+ }
+ }}
+ >
+ Success message }>
+
+
)
}
storiesOf('Wizard', module).add('CustomNav', () => )
@@ -195,13 +237,24 @@ const ComponentFour = () => {
}
const AccessStateUsingHook = () => {
+ const [visible, setVisible] = useState(false)
+ const close = () => setVisible(false)
return (
- #{context.currentIndex + 1} }>
-
-
-
-
-
+
+ setVisible(true)}>
+ Open
+
+ setVisible(false)}
+ leftFooterRender={({ context }) => #{context.currentIndex + 1} }
+ >
+
+
+
+
+
+
)
}
storiesOf('Wizard', module).add('HasHook', () => )
diff --git a/src/index.tsx b/src/index.tsx
index 276a7e317f..e9873f5713 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -40,6 +40,7 @@ export * from './components/Form'
export * from './components/ProgressBar'
export * from './components/ToastMessage'
export * from './components/Helper'
+export * from './components/Spreadsheet'
// Utils
export * from './utils/validators'
diff --git a/src/styles/components/spreadsheet.scss b/src/styles/components/spreadsheet.scss
new file mode 100644
index 0000000000..06e1ac6645
--- /dev/null
+++ b/src/styles/components/spreadsheet.scss
@@ -0,0 +1,162 @@
+@import '../base/colors.scss';
+
+span.data-grid-container,
+span.data-grid-container:focus {
+ outline: none;
+}
+
+.data-grid-container .data-grid {
+ table-layout: fixed;
+ border-collapse: collapse;
+}
+
+.data-grid-container .data-grid .cell.updated {
+ background-color: rgba(0, 145, 253, 0.16);
+ transition: background-color 0ms ease;
+}
+.data-grid-container .data-grid .cell {
+ height: 17px;
+ user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ cursor: cell;
+ background-color: unset;
+ transition: background-color 500ms ease;
+ vertical-align: middle;
+ text-align: right;
+ border: 1px solid #ddd;
+ padding: 0;
+}
+.data-grid-container .data-grid .cell.selected {
+ border: 1px double rgb(33, 133, 208);
+ transition: none;
+ box-shadow: inset 0 -100px 0 rgba(33, 133, 208, 0.15);
+}
+
+.data-grid-container .data-grid .cell.read-only {
+ background: whitesmoke;
+ color: #999;
+ text-align: center;
+}
+
+.data-grid-container .data-grid .cell > .text {
+ padding: 2px 5px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.data-grid-container .data-grid .cell > input {
+ outline: none !important;
+ border: 2px solid rgb(33, 133, 208);
+ text-align: right;
+ width: calc(100% - 6px);
+ height: 11px;
+ background: none;
+ display: block;
+}
+
+.data-grid-container .data-grid .cell {
+ vertical-align: bottom;
+}
+
+.data-grid-container .data-grid .cell,
+.data-grid-container .data-grid.wrap .cell,
+.data-grid-container .data-grid.wrap .cell.wrap,
+.data-grid-container .data-grid .cell.wrap,
+.data-grid-container .data-grid.nowrap .cell.wrap,
+.data-grid-container .data-grid.clip .cell.wrap {
+ white-space: normal;
+}
+
+.data-grid-container .data-grid.nowrap .cell,
+.data-grid-container .data-grid.nowrap .cell.nowrap,
+.data-grid-container .data-grid .cell.nowrap,
+.data-grid-container .data-grid.wrap .cell.nowrap,
+.data-grid-container .data-grid.clip .cell.nowrap {
+ white-space: nowrap;
+ overflow-x: visible;
+}
+
+.data-grid-container .data-grid.clip .cell,
+.data-grid-container .data-grid.clip .cell.clip,
+.data-grid-container .data-grid .cell.clip,
+.data-grid-container .data-grid.wrap .cell.clip,
+.data-grid-container .data-grid.nowrap .cell.clip {
+ white-space: nowrap;
+ overflow-x: hidden;
+}
+
+.data-grid-container .data-grid .cell .value-viewer,
+.data-grid-container .data-grid .cell .data-editor {
+ display: block;
+}
+
+/* custom style */
+.spreadsheet {
+ width: 100%;
+ background-color: $white;
+ color: $black;
+ /* custom style */
+ .wrap-top {
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ .description {
+ flex: 7 1 70%;
+ }
+ .button-group {
+ flex: 3 0 30%;
+ display: flex;
+ justify-content: flex-end;
+ .download-button {
+ margin-left: 1rem;
+ }
+ }
+ }
+ .wrap-bottom {
+ margin-top: 1rem;
+ text-align: right;
+ }
+ /* lib style modify */
+ .data-grid-container .data-grid {
+ width: 100%;
+ margin: auto;
+ }
+ & input.data-editor {
+ width: 100% !important;
+ height: 100% !important;
+ }
+ .data-grid-container .data-grid .cell {
+ height: 2rem;
+ line-height: 2rem;
+ vertical-align: middle;
+ padding: 0 0.5rem 0 0.5rem;
+ text-align: left;
+ &.read-only {
+ color: $black;
+ font-weight: bold;
+ background-color: inherit;
+ }
+ &.error-cell {
+ background: $reapit-red !important;
+ color: $white !important;
+ }
+ & > input {
+ text-align: left;
+ }
+ }
+ tr:nth-child(odd) {
+ background-color: $white;
+ }
+ tr:nth-child(even) {
+ background-color: $grey-lighter;
+ }
+
+ /* custom style use for demo in storybook */
+
+ .custom-classname-style {
+ color: $reapit-light-blue;
+ }
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 0dae764265..f0558990c2 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -27,3 +27,4 @@
@import './components/pagination.scss';
@import './components/input.scss';
@import './components/helper.scss';
+@import './components/spreadsheet.scss';
diff --git a/yarn.lock b/yarn.lock
index 84c4071e18..10f779e9fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5584,11 +5584,16 @@ fast-glob@^3.0.3:
merge2 "^1.3.0"
micromatch "^4.0.2"
-fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@2.x:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -10806,6 +10811,11 @@ react-clientside-effect@^1.2.0:
dependencies:
"@babel/runtime" "^7.0.0"
+react-datasheet@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/react-datasheet/-/react-datasheet-1.4.0.tgz#da251307137a12de3b113c0a1ef62baf114bc3fc"
+ integrity sha512-MiBYQtvZYAEWN/2gJS84SEL4jNAOIJjdFI5i7AZ7BIx06p2PbjUMN7wgT9QTlhrfgFHAfM5tfgBA6ibDLMyVvA==
+
react-datepicker@^2.9.6:
version "2.9.6"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.9.6.tgz#26190c9f71692149d0d163398aa19e08626444b1"
@@ -11609,9 +11619,9 @@ resolve@1.x, resolve@^1.1.6, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2, r
path-parse "^1.0.6"
resolve@^1.10.0:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16"
- integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.0.tgz#6d14c6f9db9f8002071332b600039abf82053f64"
+ integrity sha512-uviWSi5N67j3t3UKFxej1loCH0VZn5XuqdNxoLShPcYPw6cUZn74K1VRj+9myynRX03bxIBEkwlkob/ujLsJVw==
dependencies:
path-parse "^1.0.6"
@@ -11784,19 +11794,19 @@ rollup-pluginutils@2.6.0:
estree-walker "^0.6.0"
micromatch "^3.1.10"
-rollup@^1.12.0:
- version "1.23.1"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.23.1.tgz#0315a0f5d0dfb056e6363e1dff05b89ac2da6b8e"
- integrity sha512-95C1GZQpr/NIA0kMUQmSjuMDQ45oZfPgDBcN0yZwBG7Kee//m7H68vgIyg+SPuyrTZ5PrXfyLK80OzXeKG5dAA==
+rollup@1.27.13:
+ version "1.27.13"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.13.tgz#d6d3500512daacbf8de54d2800de62d893085b90"
+ integrity sha512-hDi7M07MpmNSDE8YVwGVFA8L7n8jTLJ4lG65nMAijAyqBe//rtu4JdxjUBE7JqXfdpqxqDTbCDys9WcqdpsQvw==
dependencies:
"@types/estree" "*"
"@types/node" "*"
acorn "^7.1.0"
-rollup@^1.27.9:
- version "1.27.9"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.9.tgz#742f1234c1fa935f35149a433807da675b10f9a6"
- integrity sha512-8AfW4cJTPZfG6EXWwT/ujL4owUsDI1Xl8J1t+hvK4wDX81F5I4IbwP9gvGbHzxnV19fnU4rRABZQwZSX9J402Q==
+rollup@^1.12.0:
+ version "1.23.1"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.23.1.tgz#0315a0f5d0dfb056e6363e1dff05b89ac2da6b8e"
+ integrity sha512-95C1GZQpr/NIA0kMUQmSjuMDQ45oZfPgDBcN0yZwBG7Kee//m7H68vgIyg+SPuyrTZ5PrXfyLK80OzXeKG5dAA==
dependencies:
"@types/estree" "*"
"@types/node" "*"
@@ -12018,6 +12028,11 @@ serialize-javascript@^1.6.1, serialize-javascript@^1.7.0:
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
+serialize-javascript@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
+ integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
+
serve-favicon@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0"
@@ -12794,6 +12809,21 @@ terser-webpack-plugin@^1.2.4, terser-webpack-plugin@^1.4.1:
webpack-sources "^1.4.0"
worker-farm "^1.7.0"
+terser-webpack-plugin@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c"
+ integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
+ dependencies:
+ cacache "^12.0.2"
+ find-cache-dir "^2.1.0"
+ is-wsl "^1.1.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^2.1.2"
+ source-map "^0.6.1"
+ terser "^4.1.2"
+ webpack-sources "^1.4.0"
+ worker-farm "^1.7.0"
+
terser@^3.14.1:
version "3.17.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
@@ -13731,6 +13761,35 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1:
source-list-map "^2.0.0"
source-map "~0.6.1"
+webpack@4.41.3:
+ version "4.41.3"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.3.tgz#cb7592c43080337dbc9be9e98fc6478eb3981026"
+ integrity sha512-EcNzP9jGoxpQAXq1VOoTet0ik7/VVU1MovIfcUSAjLowc7GhcQku/sOXALvq5nPpSei2HF6VRhibeJSC3i/Law==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/wasm-edit" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+ acorn "^6.2.1"
+ ajv "^6.10.2"
+ ajv-keywords "^3.4.1"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^4.1.0"
+ eslint-scope "^4.0.3"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.4.0"
+ loader-utils "^1.2.3"
+ memory-fs "^0.4.1"
+ micromatch "^3.1.10"
+ mkdirp "^0.5.1"
+ neo-async "^2.6.1"
+ node-libs-browser "^2.2.1"
+ schema-utils "^1.0.0"
+ tapable "^1.1.3"
+ terser-webpack-plugin "^1.4.3"
+ watchpack "^1.6.0"
+ webpack-sources "^1.4.1"
+
webpack@^4.28.4, webpack@^4.33.0, webpack@^4.38.0:
version "4.41.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b"
| | | | |