├── .github └── workflows │ └── master.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.jsx ├── Badge.jsx ├── Table.jsx ├── cells │ ├── Cell.jsx │ ├── NumberCell.jsx │ ├── SelectCell.jsx │ └── TextCell.jsx ├── colors.js ├── header │ ├── AddColumnHeader.jsx │ ├── DataTypeIcon.jsx │ ├── Header.jsx │ ├── HeaderMenu.jsx │ └── TypesMenu.jsx ├── img │ ├── ArrowDown.jsx │ ├── ArrowLeft.jsx │ ├── ArrowRight.jsx │ ├── ArrowUp.jsx │ ├── Hash.jsx │ ├── Multi.jsx │ ├── Plus.jsx │ ├── Text.jsx │ └── Trash.jsx ├── index.jsx ├── scrollbarWidth.js ├── style.css └── utils.js └── vite.config.js /.github/workflows/master.yaml: -------------------------------------------------------------------------------- 1 | name: Editable React Table - Master CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm run lint 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build 4 | dist 5 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | .prettierrc.json 4 | .husky 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Archit Pandey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Editable React Table 2 | 3 | 4 | > [!NOTE] 5 | > [Rowstack](https://rowstack.io) is a professional React database component I built that's brilliantly designed, feature rich and performant. Check it out! 6 | 7 | [![Edit editable-react-table](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/editable-react-table-gchwp?fontsize=14&hidenavigation=1&theme=dark) 8 | 9 | ![editable-react-table](https://user-images.githubusercontent.com/30985772/118361385-dd7caa00-b5a8-11eb-808b-1b4075f4a09d.gif) 10 | 11 | ## Features 12 | 13 | - Resizing columns 14 | - Cell data types - number, text, select 15 | - Renaming column headers 16 | - Sort on individual columns 17 | - Adding columns to left or right of a column 18 | - Adding column to end 19 | - Editing data in cells 20 | - Adding a new row of data 21 | - Deleting a column 22 | - Changing column data types 23 | - Adding options for cells of type select 24 | 25 | ## Getting Started 26 | 27 | 1. Clone this repository. 28 | 29 | ```bash 30 | git clone https://github.com/archit-p/editable-react-table 31 | ``` 32 | 33 | 2. Install dependencies and build. 34 | 35 | ```bash 36 | npm install 37 | npm start 38 | ``` 39 | 40 | ## Contributions 41 | 42 | Contributions are welcome! Feel free to pick an item from the roadmap below or open a fresh issue! 43 | 44 | ## Roadmap 45 | 46 | - [x] Support for virtualized rows 47 | - [ ] Date data-type 48 | - [ ] Multi-Select data-type 49 | - [ ] Checkbox data-type 50 | - [ ] Animating dropdowns 51 | - [ ] Performance - supporting 100000 rows 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@popperjs/core": "^2.0.0", 7 | "clsx": "1.1.1", 8 | "faker": "^5.5.3", 9 | "immutability-helper": "^3.1.1", 10 | "react": "17.0.2", 11 | "react-contenteditable": "^3.3.5", 12 | "react-dom": "17.0.2", 13 | "react-popper": "^2.2.5", 14 | "react-table": "7.7.0", 15 | "react-window": "^1.8.6" 16 | }, 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "vite build", 20 | "serve": "vite preview", 21 | "prepare": "husky install", 22 | "lint": "prettier --check . && eslint src/**/*.jsx", 23 | "format": "prettier --write ." 24 | }, 25 | "devDependencies": { 26 | "@vitejs/plugin-react": "^3.0.0", 27 | "eslint": "^8.30.0", 28 | "eslint-config-react-app": "^7.0.1", 29 | "husky": "^6.0.0", 30 | "prettier": "^2.3.1", 31 | "vite": "4.x", 32 | "vite-plugin-eslint": "^1.8.1" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "eslintConfig": { 47 | "extends": [ 48 | "react-app", 49 | "react-app/jest" 50 | ] 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "echo \"[Husky] pre-commit\"" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | import './style.css'; 3 | import Table from './Table'; 4 | import { 5 | randomColor, 6 | shortId, 7 | makeData, 8 | ActionTypes, 9 | DataTypes, 10 | } from './utils'; 11 | import update from 'immutability-helper'; 12 | 13 | function reducer(state, action) { 14 | switch (action.type) { 15 | case ActionTypes.ADD_OPTION_TO_COLUMN: 16 | const optionIndex = state.columns.findIndex( 17 | column => column.id === action.columnId 18 | ); 19 | return update(state, { 20 | skipReset: { $set: true }, 21 | columns: { 22 | [optionIndex]: { 23 | options: { 24 | $push: [ 25 | { 26 | label: action.option, 27 | backgroundColor: action.backgroundColor, 28 | }, 29 | ], 30 | }, 31 | }, 32 | }, 33 | }); 34 | case ActionTypes.ADD_ROW: 35 | return update(state, { 36 | skipReset: { $set: true }, 37 | data: { $push: [{}] }, 38 | }); 39 | case ActionTypes.UPDATE_COLUMN_TYPE: 40 | const typeIndex = state.columns.findIndex( 41 | column => column.id === action.columnId 42 | ); 43 | switch (action.dataType) { 44 | case DataTypes.NUMBER: 45 | if (state.columns[typeIndex].dataType === DataTypes.NUMBER) { 46 | return state; 47 | } else { 48 | return update(state, { 49 | skipReset: { $set: true }, 50 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 51 | data: { 52 | $apply: data => 53 | data.map(row => ({ 54 | ...row, 55 | [action.columnId]: isNaN(row[action.columnId]) 56 | ? '' 57 | : Number.parseInt(row[action.columnId]), 58 | })), 59 | }, 60 | }); 61 | } 62 | case DataTypes.SELECT: 63 | if (state.columns[typeIndex].dataType === DataTypes.SELECT) { 64 | return state; 65 | } else { 66 | let options = []; 67 | state.data.forEach(row => { 68 | if (row[action.columnId]) { 69 | options.push({ 70 | label: row[action.columnId], 71 | backgroundColor: randomColor(), 72 | }); 73 | } 74 | }); 75 | return update(state, { 76 | skipReset: { $set: true }, 77 | columns: { 78 | [typeIndex]: { 79 | dataType: { $set: action.dataType }, 80 | options: { $push: options }, 81 | }, 82 | }, 83 | }); 84 | } 85 | case DataTypes.TEXT: 86 | if (state.columns[typeIndex].dataType === DataTypes.TEXT) { 87 | return state; 88 | } else if (state.columns[typeIndex].dataType === DataTypes.SELECT) { 89 | return update(state, { 90 | skipReset: { $set: true }, 91 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 92 | }); 93 | } else { 94 | return update(state, { 95 | skipReset: { $set: true }, 96 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 97 | data: { 98 | $apply: data => 99 | data.map(row => ({ 100 | ...row, 101 | [action.columnId]: row[action.columnId] + '', 102 | })), 103 | }, 104 | }); 105 | } 106 | default: 107 | return state; 108 | } 109 | case ActionTypes.UPDATE_COLUMN_HEADER: 110 | const index = state.columns.findIndex( 111 | column => column.id === action.columnId 112 | ); 113 | return update(state, { 114 | skipReset: { $set: true }, 115 | columns: { [index]: { label: { $set: action.label } } }, 116 | }); 117 | case ActionTypes.UPDATE_CELL: 118 | return update(state, { 119 | skipReset: { $set: true }, 120 | data: { 121 | [action.rowIndex]: { [action.columnId]: { $set: action.value } }, 122 | }, 123 | }); 124 | case ActionTypes.ADD_COLUMN_TO_LEFT: 125 | const leftIndex = state.columns.findIndex( 126 | column => column.id === action.columnId 127 | ); 128 | let leftId = shortId(); 129 | return update(state, { 130 | skipReset: { $set: true }, 131 | columns: { 132 | $splice: [ 133 | [ 134 | leftIndex, 135 | 0, 136 | { 137 | id: leftId, 138 | label: 'Column', 139 | accessor: leftId, 140 | dataType: DataTypes.TEXT, 141 | created: action.focus && true, 142 | options: [], 143 | }, 144 | ], 145 | ], 146 | }, 147 | }); 148 | case ActionTypes.ADD_COLUMN_TO_RIGHT: 149 | const rightIndex = state.columns.findIndex( 150 | column => column.id === action.columnId 151 | ); 152 | const rightId = shortId(); 153 | return update(state, { 154 | skipReset: { $set: true }, 155 | columns: { 156 | $splice: [ 157 | [ 158 | rightIndex + 1, 159 | 0, 160 | { 161 | id: rightId, 162 | label: 'Column', 163 | accessor: rightId, 164 | dataType: DataTypes.TEXT, 165 | created: action.focus && true, 166 | options: [], 167 | }, 168 | ], 169 | ], 170 | }, 171 | }); 172 | case ActionTypes.DELETE_COLUMN: 173 | const deleteIndex = state.columns.findIndex( 174 | column => column.id === action.columnId 175 | ); 176 | return update(state, { 177 | skipReset: { $set: true }, 178 | columns: { $splice: [[deleteIndex, 1]] }, 179 | }); 180 | case ActionTypes.ENABLE_RESET: 181 | return update(state, { skipReset: { $set: true } }); 182 | default: 183 | return state; 184 | } 185 | } 186 | 187 | function App() { 188 | const [state, dispatch] = useReducer(reducer, makeData(1000)); 189 | 190 | useEffect(() => { 191 | dispatch({ type: ActionTypes.ENABLE_RESET }); 192 | }, [state.data, state.columns]); 193 | 194 | return ( 195 |
203 |
204 |

Editable React Table - Demo

205 |
206 | 212 |
213 | 214 | ); 215 | } 216 | 217 | export default App; 218 | -------------------------------------------------------------------------------- /src/Badge.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Badge({ value, backgroundColor }) { 4 | return ( 5 | 12 | {value} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/Table.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import clsx from 'clsx'; 3 | import { 4 | useTable, 5 | useBlockLayout, 6 | useResizeColumns, 7 | useSortBy, 8 | } from 'react-table'; 9 | import Cell from './cells/Cell'; 10 | import Header from './header/Header'; 11 | import PlusIcon from './img/Plus'; 12 | import { ActionTypes } from './utils'; 13 | import { FixedSizeList } from 'react-window'; 14 | import scrollbarWidth from './scrollbarWidth'; 15 | 16 | const defaultColumn = { 17 | minWidth: 50, 18 | width: 150, 19 | maxWidth: 400, 20 | Cell: Cell, 21 | Header: Header, 22 | sortType: 'alphanumericFalsyLast', 23 | }; 24 | 25 | export default function Table({ 26 | columns, 27 | data, 28 | dispatch: dataDispatch, 29 | skipReset, 30 | }) { 31 | const sortTypes = useMemo( 32 | () => ({ 33 | alphanumericFalsyLast(rowA, rowB, columnId, desc) { 34 | if (!rowA.values[columnId] && !rowB.values[columnId]) { 35 | return 0; 36 | } 37 | 38 | if (!rowA.values[columnId]) { 39 | return desc ? -1 : 1; 40 | } 41 | 42 | if (!rowB.values[columnId]) { 43 | return desc ? 1 : -1; 44 | } 45 | 46 | return isNaN(rowA.values[columnId]) 47 | ? rowA.values[columnId].localeCompare(rowB.values[columnId]) 48 | : rowA.values[columnId] - rowB.values[columnId]; 49 | }, 50 | }), 51 | [] 52 | ); 53 | 54 | const { 55 | getTableProps, 56 | getTableBodyProps, 57 | headerGroups, 58 | rows, 59 | prepareRow, 60 | totalColumnsWidth, 61 | } = useTable( 62 | { 63 | columns, 64 | data, 65 | defaultColumn, 66 | dataDispatch, 67 | autoResetSortBy: !skipReset, 68 | autoResetFilters: !skipReset, 69 | autoResetRowState: !skipReset, 70 | sortTypes, 71 | }, 72 | useBlockLayout, 73 | useResizeColumns, 74 | useSortBy 75 | ); 76 | 77 | const RenderRow = React.useCallback( 78 | ({ index, style }) => { 79 | const row = rows[index]; 80 | prepareRow(row); 81 | return ( 82 |
83 | {row.cells.map(cell => ( 84 |
85 | {cell.render('Cell')} 86 |
87 | ))} 88 |
89 | ); 90 | }, 91 | [prepareRow, rows] 92 | ); 93 | 94 | function isTableResizing() { 95 | for (let headerGroup of headerGroups) { 96 | for (let column of headerGroup.headers) { 97 | if (column.isResizing) { 98 | return true; 99 | } 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | return ( 107 |
108 |
112 |
113 | {headerGroups.map(headerGroup => ( 114 |
115 | {headerGroup.headers.map(column => column.render('Header'))} 116 |
117 | ))} 118 |
119 |
120 | 126 | {RenderRow} 127 | 128 |
dataDispatch({ type: ActionTypes.ADD_ROW })} 131 | > 132 | 133 | 134 | 135 | New 136 |
137 |
138 |
139 |
140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /src/cells/Cell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DataTypes } from '../utils'; 3 | import TextCell from './TextCell'; 4 | import NumberCell from './NumberCell'; 5 | import SelectCell from './SelectCell'; 6 | 7 | export default function Cell({ 8 | value: initialValue, 9 | row: { index }, 10 | column: { id, dataType, options }, 11 | dataDispatch, 12 | }) { 13 | function getCellElement() { 14 | switch (dataType) { 15 | case DataTypes.TEXT: 16 | return ( 17 | 23 | ); 24 | case DataTypes.NUMBER: 25 | return ( 26 | 32 | ); 33 | case DataTypes.SELECT: 34 | return ( 35 | 42 | ); 43 | default: 44 | return ; 45 | } 46 | } 47 | 48 | return getCellElement(); 49 | } 50 | -------------------------------------------------------------------------------- /src/cells/NumberCell.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ContentEditable from 'react-contenteditable'; 3 | import { ActionTypes } from '../utils'; 4 | 5 | export default function NumberCell({ 6 | initialValue, 7 | columnId, 8 | rowIndex, 9 | dataDispatch, 10 | }) { 11 | const [value, setValue] = useState({ value: initialValue, update: false }); 12 | 13 | function onChange(e) { 14 | setValue({ value: e.target.value, update: false }); 15 | } 16 | 17 | function onBlur(e) { 18 | setValue(old => ({ value: old.value, update: true })); 19 | } 20 | 21 | useEffect(() => { 22 | setValue({ value: initialValue, update: false }); 23 | }, [initialValue]); 24 | 25 | useEffect(() => { 26 | if (value.update) { 27 | dataDispatch({ 28 | type: ActionTypes.UPDATE_CELL, 29 | columnId, 30 | rowIndex, 31 | value: value.value, 32 | }); 33 | } 34 | // eslint-disable-next-line react-hooks/exhaustive-deps 35 | }, [value.update, columnId, rowIndex]); 36 | 37 | return ( 38 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/cells/SelectCell.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { usePopper } from 'react-popper'; 4 | import Badge from '../Badge'; 5 | import { grey } from '../colors'; 6 | import PlusIcon from '../img/Plus'; 7 | import { ActionTypes, randomColor } from '../utils'; 8 | 9 | export default function SelectCell({ 10 | initialValue, 11 | options, 12 | columnId, 13 | rowIndex, 14 | dataDispatch, 15 | }) { 16 | const [selectRef, setSelectRef] = useState(null); 17 | const [selectPop, setSelectPop] = useState(null); 18 | const [showSelect, setShowSelect] = useState(false); 19 | const [showAdd, setShowAdd] = useState(false); 20 | const [addSelectRef, setAddSelectRef] = useState(null); 21 | const { styles, attributes } = usePopper(selectRef, selectPop, { 22 | placement: 'bottom-start', 23 | strategy: 'fixed', 24 | }); 25 | const [value, setValue] = useState({ value: initialValue, update: false }); 26 | 27 | useEffect(() => { 28 | setValue({ value: initialValue, update: false }); 29 | }, [initialValue]); 30 | 31 | useEffect(() => { 32 | if (value.update) { 33 | dataDispatch({ 34 | type: ActionTypes.UPDATE_CELL, 35 | columnId, 36 | rowIndex, 37 | value: value.value, 38 | }); 39 | } 40 | // eslint-disable-next-line react-hooks/exhaustive-deps 41 | }, [value, columnId, rowIndex]); 42 | 43 | useEffect(() => { 44 | if (addSelectRef && showAdd) { 45 | addSelectRef.focus(); 46 | } 47 | }, [addSelectRef, showAdd]); 48 | 49 | function getColor() { 50 | let match = options.find(option => option.label === value.value); 51 | return (match && match.backgroundColor) || grey(200); 52 | } 53 | 54 | function handleAddOption(e) { 55 | setShowAdd(true); 56 | } 57 | 58 | function handleOptionKeyDown(e) { 59 | if (e.key === 'Enter') { 60 | if (e.target.value !== '') { 61 | dataDispatch({ 62 | type: ActionTypes.ADD_OPTION_TO_COLUMN, 63 | option: e.target.value, 64 | backgroundColor: randomColor(), 65 | columnId, 66 | }); 67 | } 68 | setShowAdd(false); 69 | } 70 | } 71 | 72 | function handleOptionBlur(e) { 73 | if (e.target.value !== '') { 74 | dataDispatch({ 75 | type: ActionTypes.ADD_OPTION_TO_COLUMN, 76 | option: e.target.value, 77 | backgroundColor: randomColor(), 78 | columnId, 79 | }); 80 | } 81 | setShowAdd(false); 82 | } 83 | 84 | function handleOptionClick(option) { 85 | setValue({ value: option.label, update: true }); 86 | setShowSelect(false); 87 | } 88 | 89 | useEffect(() => { 90 | if (addSelectRef && showAdd) { 91 | addSelectRef.focus(); 92 | } 93 | }, [addSelectRef, showAdd]); 94 | 95 | return ( 96 | <> 97 |
setShowSelect(true)} 101 | > 102 | {value.value && ( 103 | 104 | )} 105 |
106 | {showSelect && ( 107 |
setShowSelect(false)} /> 108 | )} 109 | {showSelect && 110 | createPortal( 111 |
125 |
129 | {options.map(option => ( 130 |
handleOptionClick(option)} 133 | > 134 | 138 |
139 | ))} 140 | {showAdd && ( 141 |
148 | 155 |
156 | )} 157 |
161 | 164 | 165 | 166 | } 167 | backgroundColor={grey(200)} 168 | /> 169 |
170 |
171 |
, 172 | document.querySelector('#popper-portal') 173 | )} 174 | 175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /src/cells/TextCell.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ContentEditable from 'react-contenteditable'; 3 | import { ActionTypes } from '../utils'; 4 | 5 | export default function TextCell({ 6 | initialValue, 7 | columnId, 8 | rowIndex, 9 | dataDispatch, 10 | }) { 11 | const [value, setValue] = useState({ value: initialValue, update: false }); 12 | 13 | function onChange(e) { 14 | setValue({ value: e.target.value, update: false }); 15 | } 16 | 17 | function onBlur(e) { 18 | setValue(old => ({ value: old.value, update: true })); 19 | } 20 | 21 | useEffect(() => { 22 | setValue({ value: initialValue, update: false }); 23 | }, [initialValue]); 24 | 25 | useEffect(() => { 26 | if (value.update) { 27 | dataDispatch({ 28 | type: ActionTypes.UPDATE_CELL, 29 | columnId, 30 | rowIndex, 31 | value: value.value, 32 | }); 33 | } 34 | 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, [value.update, columnId, rowIndex]); 37 | 38 | return ( 39 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | export function grey(value) { 2 | let reference = { 3 | 50: '#fafafa', 4 | 100: '#f5f5f5', 5 | 200: '#eeeeee', 6 | 300: '#e0e0e0', 7 | 400: '#bdbdbd', 8 | 500: '#9e9e9e', 9 | 600: '#757575', 10 | 700: '#616161', 11 | 800: '#424242', 12 | 900: '#212121', 13 | }; 14 | 15 | return reference[value]; 16 | } 17 | -------------------------------------------------------------------------------- /src/header/AddColumnHeader.jsx: -------------------------------------------------------------------------------- 1 | import PlusIcon from '../img/Plus'; 2 | import React from 'react'; 3 | import { ActionTypes, Constants } from '../utils'; 4 | 5 | export default function AddColumnHeader({ getHeaderProps, dataDispatch }) { 6 | return ( 7 |
8 |
11 | dataDispatch({ 12 | type: ActionTypes.ADD_COLUMN_TO_LEFT, 13 | columnId: Constants.ADD_COLUMN_ID, 14 | focus: true, 15 | }) 16 | } 17 | > 18 | 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/header/DataTypeIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DataTypes } from '../utils'; 3 | import TextIcon from '../img/Text'; 4 | import MultiIcon from '../img/Multi'; 5 | import HashIcon from '../img/Hash'; 6 | 7 | export default function DataTypeIcon({ dataType }) { 8 | function getPropertyIcon(dataType) { 9 | switch (dataType) { 10 | case DataTypes.NUMBER: 11 | return ; 12 | case DataTypes.TEXT: 13 | return ; 14 | case DataTypes.SELECT: 15 | return ; 16 | default: 17 | return null; 18 | } 19 | } 20 | 21 | return getPropertyIcon(dataType); 22 | } 23 | -------------------------------------------------------------------------------- /src/header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { usePopper } from 'react-popper'; 3 | import { Constants } from '../utils'; 4 | import AddColumnHeader from './AddColumnHeader'; 5 | import DataTypeIcon from './DataTypeIcon'; 6 | import HeaderMenu from './HeaderMenu'; 7 | 8 | export default function Header({ 9 | column: { id, created, label, dataType, getResizerProps, getHeaderProps }, 10 | setSortBy, 11 | dataDispatch, 12 | }) { 13 | const [showHeaderMenu, setShowHeaderMenu] = useState(created || false); 14 | const [headerMenuAnchorRef, setHeaderMenuAnchorRef] = useState(null); 15 | const [headerMenuPopperRef, setHeaderMenuPopperRef] = useState(null); 16 | const headerMenuPopper = usePopper(headerMenuAnchorRef, headerMenuPopperRef, { 17 | placement: 'bottom', 18 | strategy: 'absolute', 19 | }); 20 | 21 | /* when the column is newly created, set it to open */ 22 | useEffect(() => { 23 | if (created) { 24 | setShowHeaderMenu(true); 25 | } 26 | }, [created]); 27 | 28 | function getHeader() { 29 | if (id === Constants.ADD_COLUMN_ID) { 30 | return ( 31 | 35 | ); 36 | } 37 | 38 | return ( 39 | <> 40 |
41 |
setShowHeaderMenu(true)} 44 | ref={setHeaderMenuAnchorRef} 45 | > 46 | 47 | 48 | 49 | {label} 50 |
51 |
52 |
53 | {showHeaderMenu && ( 54 |
setShowHeaderMenu(false)} /> 55 | )} 56 | {showHeaderMenu && ( 57 | 67 | )} 68 | 69 | ); 70 | } 71 | 72 | return getHeader(); 73 | } 74 | -------------------------------------------------------------------------------- /src/header/HeaderMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ArrowUpIcon from '../img/ArrowUp'; 3 | import ArrowDownIcon from '../img/ArrowDown'; 4 | import ArrowLeftIcon from '../img/ArrowLeft'; 5 | import ArrowRightIcon from '../img/ArrowRight'; 6 | import TrashIcon from '../img/Trash'; 7 | import { grey } from '../colors'; 8 | import TypesMenu from './TypesMenu'; 9 | import { usePopper } from 'react-popper'; 10 | import { ActionTypes, shortId } from '../utils'; 11 | import DataTypeIcon from './DataTypeIcon'; 12 | 13 | export default function HeaderMenu({ 14 | label, 15 | dataType, 16 | columnId, 17 | setSortBy, 18 | popper, 19 | popperRef, 20 | dataDispatch, 21 | setShowHeaderMenu, 22 | }) { 23 | const [inputRef, setInputRef] = useState(null); 24 | const [header, setHeader] = useState(label); 25 | const [typeReferenceElement, setTypeReferenceElement] = useState(null); 26 | const [typePopperElement, setTypePopperElement] = useState(null); 27 | const typePopper = usePopper(typeReferenceElement, typePopperElement, { 28 | placement: 'right', 29 | strategy: 'fixed', 30 | }); 31 | const [showTypeMenu, setShowTypeMenu] = useState(false); 32 | 33 | function onTypeMenuClose() { 34 | setShowTypeMenu(false); 35 | setShowHeaderMenu(false); 36 | } 37 | 38 | useEffect(() => { 39 | setHeader(label); 40 | }, [label]); 41 | 42 | useEffect(() => { 43 | if (inputRef) { 44 | inputRef.focus(); 45 | inputRef.select(); 46 | } 47 | }, [inputRef]); 48 | 49 | const buttons = [ 50 | { 51 | onClick: e => { 52 | dataDispatch({ 53 | type: ActionTypes.UPDATE_COLUMN_HEADER, 54 | columnId, 55 | label: header, 56 | }); 57 | setSortBy([{ id: columnId, desc: false }]); 58 | setShowHeaderMenu(false); 59 | }, 60 | icon: , 61 | label: 'Sort ascending', 62 | }, 63 | { 64 | onClick: e => { 65 | dataDispatch({ 66 | type: ActionTypes.UPDATE_COLUMN_HEADER, 67 | columnId, 68 | label: header, 69 | }); 70 | setSortBy([{ id: columnId, desc: true }]); 71 | setShowHeaderMenu(false); 72 | }, 73 | icon: , 74 | label: 'Sort descending', 75 | }, 76 | { 77 | onClick: e => { 78 | dataDispatch({ 79 | type: ActionTypes.UPDATE_COLUMN_HEADER, 80 | columnId, 81 | label: header, 82 | }); 83 | dataDispatch({ 84 | type: ActionTypes.ADD_COLUMN_TO_LEFT, 85 | columnId, 86 | focus: false, 87 | }); 88 | setShowHeaderMenu(false); 89 | }, 90 | icon: , 91 | label: 'Insert left', 92 | }, 93 | { 94 | onClick: e => { 95 | dataDispatch({ 96 | type: ActionTypes.UPDATE_COLUMN_HEADER, 97 | columnId, 98 | label: header, 99 | }); 100 | dataDispatch({ 101 | type: ActionTypes.ADD_COLUMN_TO_RIGHT, 102 | columnId, 103 | focus: false, 104 | }); 105 | setShowHeaderMenu(false); 106 | }, 107 | icon: , 108 | label: 'Insert right', 109 | }, 110 | { 111 | onClick: e => { 112 | dataDispatch({ type: ActionTypes.DELETE_COLUMN, columnId }); 113 | setShowHeaderMenu(false); 114 | }, 115 | icon: , 116 | label: 'Delete', 117 | }, 118 | ]; 119 | 120 | function handleColumnNameKeyDown(e) { 121 | if (e.key === 'Enter') { 122 | dataDispatch({ 123 | type: ActionTypes.UPDATE_COLUMN_HEADER, 124 | columnId, 125 | label: header, 126 | }); 127 | setShowHeaderMenu(false); 128 | } 129 | } 130 | 131 | function handleColumnNameChange(e) { 132 | setHeader(e.target.value); 133 | } 134 | 135 | function handleColumnNameBlur(e) { 136 | e.preventDefault(); 137 | dataDispatch({ 138 | type: ActionTypes.UPDATE_COLUMN_HEADER, 139 | columnId, 140 | label: header, 141 | }); 142 | } 143 | 144 | return ( 145 |
150 |
156 |
163 |
164 | 173 |
174 | 175 | Property Type 176 | 177 |
178 |
179 | 191 | {showTypeMenu && ( 192 | 200 | )} 201 |
202 |
203 |
204 | {buttons.map(button => ( 205 | 216 | ))} 217 |
218 |
219 |
220 | ); 221 | } 222 | -------------------------------------------------------------------------------- /src/header/TypesMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionTypes, DataTypes, shortId } from '../utils'; 3 | import DataTypeIcon from './DataTypeIcon'; 4 | 5 | function getLabel(type) { 6 | return type.charAt(0).toUpperCase() + type.slice(1); 7 | } 8 | 9 | export default function TypesMenu({ 10 | popper, 11 | popperRef, 12 | dataDispatch, 13 | setShowTypeMenu, 14 | onClose, 15 | columnId, 16 | }) { 17 | const types = [ 18 | { 19 | type: DataTypes.SELECT, 20 | onClick: e => { 21 | dataDispatch({ 22 | type: ActionTypes.UPDATE_COLUMN_TYPE, 23 | columnId, 24 | dataType: DataTypes.SELECT, 25 | }); 26 | onClose(); 27 | }, 28 | icon: , 29 | label: getLabel(DataTypes.SELECT), 30 | }, 31 | { 32 | type: DataTypes.TEXT, 33 | onClick: e => { 34 | dataDispatch({ 35 | type: ActionTypes.UPDATE_COLUMN_TYPE, 36 | columnId, 37 | dataType: DataTypes.TEXT, 38 | }); 39 | onClose(); 40 | }, 41 | icon: , 42 | label: getLabel(DataTypes.TEXT), 43 | }, 44 | { 45 | type: DataTypes.NUMBER, 46 | onClick: e => { 47 | dataDispatch({ 48 | type: ActionTypes.UPDATE_COLUMN_TYPE, 49 | columnId, 50 | dataType: DataTypes.NUMBER, 51 | }); 52 | onClose(); 53 | }, 54 | icon: , 55 | label: getLabel(DataTypes.NUMBER), 56 | }, 57 | ]; 58 | 59 | return ( 60 |
setShowTypeMenu(true)} 64 | onMouseLeave={() => setShowTypeMenu(false)} 65 | {...popper.attributes.popper} 66 | style={{ 67 | ...popper.styles.popper, 68 | width: 200, 69 | backgroundColor: 'white', 70 | zIndex: 4, 71 | }} 72 | > 73 | {types.map(type => ( 74 | 78 | ))} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/img/ArrowDown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Arrow() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/img/ArrowLeft.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Arrow() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/img/ArrowRight.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Arrow() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/img/ArrowUp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Arrow() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/img/Hash.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Hash() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/img/Multi.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Multi() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/img/Plus.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Plus() { 4 | return ( 5 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/img/Text.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Text() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/img/Trash.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Trash() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/scrollbarWidth.js: -------------------------------------------------------------------------------- 1 | const scrollbarWidth = () => { 2 | const scrollDiv = document.createElement('div'); 3 | scrollDiv.setAttribute( 4 | 'style', 5 | 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;' 6 | ); 7 | document.body.appendChild(scrollDiv); 8 | const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; 9 | document.body.removeChild(scrollDiv); 10 | return scrollbarWidth; 11 | }; 12 | 13 | export default scrollbarWidth; 14 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 2 | html { 3 | box-sizing: border-box; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | * { 13 | margin: 0px; 14 | padding: 0px; 15 | font-family: 'Inter', sans-serif; 16 | } 17 | 18 | #root { 19 | margin: 0px; 20 | padding: 0px; 21 | } 22 | 23 | .transition-fade-enter { 24 | opacity: 0; 25 | } 26 | 27 | .transition-fade-enter-active { 28 | opacity: 1; 29 | transition: opacity 300ms; 30 | } 31 | 32 | .transition-fade-exit { 33 | opacity: 1; 34 | } 35 | 36 | .transition-fade-exit-active { 37 | opacity: 0; 38 | transition: opacity 300ms; 39 | } 40 | 41 | .svg-icon svg { 42 | position: relative; 43 | height: 1.5em; 44 | width: 1.5em; 45 | top: 0.125rem; 46 | } 47 | 48 | .svg-text svg { 49 | stroke: #424242; 50 | } 51 | 52 | .svg-180 svg { 53 | transform: rotate(180deg); 54 | } 55 | 56 | .form-input { 57 | padding: 0.375rem; 58 | background-color: #eeeeee; 59 | border: none; 60 | border-radius: 4px; 61 | font-size: 0.875rem; 62 | color: #424242; 63 | } 64 | 65 | .form-input:focus { 66 | outline: none; 67 | box-shadow: 0 0 1px 2px #8ecae6; 68 | } 69 | 70 | .is-fullwidth { 71 | width: 100%; 72 | } 73 | 74 | .bg-white { 75 | background-color: white; 76 | } 77 | 78 | .data-input { 79 | white-space: pre-wrap; 80 | border: none; 81 | padding: 0.5rem; 82 | color: #424242; 83 | font-size: 1rem; 84 | border-radius: 4px; 85 | resize: none; 86 | background-color: white; 87 | box-sizing: border-box; 88 | flex: 1 1 auto; 89 | } 90 | 91 | .data-input:focus { 92 | outline: none; 93 | } 94 | 95 | .shadow-5 { 96 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.12), 97 | 0 4px 6px rgba(0, 0, 0, 0.12), 0 8px 16px rgba(0, 0, 0, 0.12), 98 | 0 16px 32px rgba(0, 0, 0, 0.12); 99 | } 100 | 101 | .svg-icon-sm svg { 102 | position: relative; 103 | height: 1rem; 104 | width: 1rem; 105 | top: 0.125rem; 106 | } 107 | 108 | .svg-gray svg { 109 | stroke: #9e9e9e; 110 | } 111 | 112 | .option-input { 113 | width: 100%; 114 | font-size: 1rem; 115 | border: none; 116 | background-color: transparent; 117 | } 118 | 119 | .option-input:focus { 120 | outline: none; 121 | } 122 | 123 | .noselect { 124 | -webkit-touch-callout: none; 125 | -webkit-user-select: none; 126 | -khtml-user-select: none; 127 | -moz-user-select: none; 128 | -ms-user-select: none; 129 | user-select: none; 130 | } 131 | 132 | .overlay { 133 | position: fixed; 134 | top: 0; 135 | left: 0; 136 | height: 100vh; 137 | width: 100vw; 138 | z-index: 2; 139 | overflow: hidden; 140 | } 141 | 142 | .sort-button { 143 | padding: 0.25rem 0.75rem; 144 | width: 100%; 145 | background-color: transparent; 146 | border: 0; 147 | font-size: 0.875rem; 148 | color: #757575; 149 | cursor: pointer; 150 | text-align: left; 151 | display: flex; 152 | align-items: center; 153 | } 154 | 155 | .sort-button:hover { 156 | background-color: #eeeeee; 157 | } 158 | 159 | .add-row { 160 | color: #9e9e9e; 161 | padding: 0.5rem; 162 | display: flex; 163 | align-items: center; 164 | font-size: 0.875rem; 165 | cursor: pointer; 166 | height: 50px; 167 | border: 1px solid #e0e0e0; 168 | } 169 | 170 | .add-row:hover { 171 | background-color: #f5f5f5; 172 | } 173 | 174 | .th { 175 | color: #9e9e9e; 176 | font-weight: 500; 177 | font-size: 0.875rem; 178 | cursor: pointer; 179 | } 180 | 181 | .th:hover { 182 | background-color: #f5f5f5; 183 | } 184 | 185 | .th-content { 186 | overflow-x: hidden; 187 | text-overflow: ellipsis; 188 | padding: 0.5rem; 189 | display: flex; 190 | align-items: center; 191 | height: 50px; 192 | } 193 | 194 | .td { 195 | overflow: hidden; 196 | color: #424242; 197 | align-items: stretch; 198 | padding: 0; 199 | display: flex; 200 | flex-direction: column; 201 | } 202 | 203 | .td-content { 204 | display: block; 205 | } 206 | 207 | .table { 208 | display: inline-block; 209 | border-spacing: 0; 210 | } 211 | 212 | .resizer { 213 | display: inline-block; 214 | background: transparent; 215 | width: 8px; 216 | height: 100%; 217 | position: absolute; 218 | right: 0; 219 | top: 0; 220 | transform: translateX(50%); 221 | z-index: 1; 222 | cursor: col-resize; 223 | touch-action: none; 224 | } 225 | 226 | .resizer:hover { 227 | background-color: #8ecae6; 228 | } 229 | 230 | .th, 231 | .td { 232 | white-space: nowrap; 233 | margin: 0; 234 | border-left: 1px solid #e0e0e0; 235 | border-top: 1px solid #e0e0e0; 236 | position: relative; 237 | } 238 | 239 | .th { 240 | border-bottom: 1px solid #e0e0e0; 241 | } 242 | 243 | .tr:last-child .td { 244 | border-bottom: 0; 245 | } 246 | 247 | .td:last-child { 248 | border-right: 1px solid #e0e0e0; 249 | } 250 | 251 | .th:last-child { 252 | border-right: 1px solid #e0e0e0; 253 | } 254 | 255 | .tr:first-child .td { 256 | border-top: 0; 257 | } 258 | 259 | .text-align-right { 260 | text-align: right; 261 | } 262 | 263 | .cell-padding { 264 | padding: 0.5rem; 265 | } 266 | 267 | .d-flex { 268 | display: flex; 269 | } 270 | 271 | .d-inline-block { 272 | display: inline-block; 273 | } 274 | 275 | .cursor-default { 276 | cursor: default; 277 | } 278 | 279 | .align-items-center { 280 | align-items: center; 281 | } 282 | 283 | .flex-wrap-wrap { 284 | flex-wrap: wrap; 285 | } 286 | 287 | .border-radius-md { 288 | border-radius: 5px; 289 | } 290 | 291 | .cursor-pointer { 292 | cursor: pointer; 293 | } 294 | 295 | .icon-margin { 296 | margin-right: 4px; 297 | } 298 | 299 | .font-weight-600 { 300 | font-weight: 600; 301 | } 302 | 303 | .font-weight-400 { 304 | font-weight: 400; 305 | } 306 | 307 | .font-size-75 { 308 | font-size: 0.75rem; 309 | } 310 | 311 | .flex-1 { 312 | flex: 1; 313 | } 314 | 315 | .mt-5 { 316 | margin-top: 0.5rem; 317 | } 318 | 319 | .mr-auto { 320 | margin-right: auto; 321 | } 322 | 323 | .ml-auto { 324 | margin-left: auto; 325 | } 326 | 327 | .mr-5 { 328 | margin-right: 0.5rem; 329 | } 330 | 331 | .justify-content-center { 332 | justify-content: center; 333 | } 334 | 335 | .flex-column { 336 | flex-direction: column; 337 | } 338 | 339 | .overflow-auto { 340 | overflow: auto; 341 | } 342 | 343 | .overflow-hidden { 344 | overflow: hidden; 345 | } 346 | 347 | .overflow-y-hidden { 348 | overflow-y: hidden; 349 | } 350 | 351 | .list-padding { 352 | padding: 4px 0px; 353 | } 354 | 355 | .bg-grey-200 { 356 | background-color: #eeeeee; 357 | } 358 | 359 | .color-grey-800 { 360 | color: #424242; 361 | } 362 | 363 | .color-grey-600 { 364 | color: #757575; 365 | } 366 | 367 | .color-grey-500 { 368 | color: #9e9e9e; 369 | } 370 | 371 | .border-radius-sm { 372 | border-radius: 4px; 373 | } 374 | 375 | .text-transform-uppercase { 376 | text-transform: uppercase; 377 | } 378 | 379 | .text-transform-capitalize { 380 | text-transform: capitalize; 381 | } 382 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | export function shortId() { 4 | return '_' + Math.random().toString(36).substr(2, 9); 5 | } 6 | 7 | export function randomColor() { 8 | return `hsl(${Math.floor(Math.random() * 360)}, 95%, 90%)`; 9 | } 10 | 11 | export function makeData(count) { 12 | let data = []; 13 | let options = []; 14 | for (let i = 0; i < count; i++) { 15 | let row = { 16 | ID: faker.mersenne.rand(), 17 | firstName: faker.name.firstName(), 18 | lastName: faker.name.lastName(), 19 | email: faker.internet.email(), 20 | age: Math.floor(20 + Math.random() * 20), 21 | music: faker.music.genre(), 22 | }; 23 | options.push({ label: row.music, backgroundColor: randomColor() }); 24 | 25 | data.push(row); 26 | } 27 | 28 | options = options.filter( 29 | (a, i, self) => self.findIndex(b => b.label === a.label) === i 30 | ); 31 | 32 | let columns = [ 33 | { 34 | id: 'firstName', 35 | label: 'First Name', 36 | accessor: 'firstName', 37 | minWidth: 100, 38 | dataType: DataTypes.TEXT, 39 | options: [], 40 | }, 41 | { 42 | id: 'lastName', 43 | label: 'Last Name', 44 | accessor: 'lastName', 45 | minWidth: 100, 46 | dataType: DataTypes.TEXT, 47 | options: [], 48 | }, 49 | { 50 | id: 'age', 51 | label: 'Age', 52 | accessor: 'age', 53 | width: 80, 54 | dataType: DataTypes.NUMBER, 55 | options: [], 56 | }, 57 | { 58 | id: 'email', 59 | label: 'E-Mail', 60 | accessor: 'email', 61 | width: 300, 62 | dataType: DataTypes.TEXT, 63 | options: [], 64 | }, 65 | { 66 | id: 'music', 67 | label: 'Music Preference', 68 | accessor: 'music', 69 | dataType: DataTypes.SELECT, 70 | width: 200, 71 | options: options, 72 | }, 73 | { 74 | id: Constants.ADD_COLUMN_ID, 75 | width: 20, 76 | label: '+', 77 | disableResizing: true, 78 | dataType: 'null', 79 | }, 80 | ]; 81 | return { columns: columns, data: data, skipReset: false }; 82 | } 83 | 84 | export const ActionTypes = Object.freeze({ 85 | ADD_OPTION_TO_COLUMN: 'add_option_to_column', 86 | ADD_ROW: 'add_row', 87 | UPDATE_COLUMN_TYPE: 'update_column_type', 88 | UPDATE_COLUMN_HEADER: 'update_column_header', 89 | UPDATE_CELL: 'update_cell', 90 | ADD_COLUMN_TO_LEFT: 'add_column_to_left', 91 | ADD_COLUMN_TO_RIGHT: 'add_column_to_right', 92 | DELETE_COLUMN: 'delete_column', 93 | ENABLE_RESET: 'enable_reset', 94 | }); 95 | 96 | export const DataTypes = Object.freeze({ 97 | NUMBER: 'number', 98 | TEXT: 'text', 99 | SELECT: 'select', 100 | }); 101 | 102 | export const Constants = Object.freeze({ 103 | ADD_COLUMN_ID: 999999, 104 | }); 105 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import eslint from 'vite-plugin-eslint'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), eslint()], 8 | }); 9 | --------------------------------------------------------------------------------