├── .gitignore ├── LICENSE.md ├── README.md ├── examples └── simple │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.tsx │ ├── CustomerList.tsx │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── package.json ├── packages └── ra-datagrid │ ├── README.md │ ├── package.json │ ├── src │ ├── Datagrid.tsx │ └── index.tsx │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | esm 4 | build 5 | .eslintcache 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present, Francois Zaninotto, Marmelab 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 NON INFRINGEMENT. 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 | # ra-datagrid 2 | 3 | Integration of [Material-ui's ``](https://material-ui.com/components/data-grid/) into react-admin. 4 | 5 | https://user-images.githubusercontent.com/99944/118509356-3e82ba00-b730-11eb-86ac-3f9895529e94.mp4 6 | 7 | ## Features 8 | 9 | - Server-side filtering, sorting and pagination via the `dataProvider` 10 | - Row selection 11 | - Cell editing 12 | - Hide / show columns 13 | - All the material-ui `` features (additional props are passed down to it) 14 | 15 | ## Usage 16 | 17 | Replace react-admin's `` by the one exported by this package. Make sure you set `pagination={false}` on the list as the Datagrid embarks its own pagination. 18 | 19 | ```tsx 20 | import { List } from "react-admin"; 21 | import { GridColDef, GridValueGetterParams } from "@material-ui/data-grid"; 22 | import { Datagrid, simpleFilterOperator } from "ra-datagrid"; 23 | 24 | const columns: GridColDef[] = [ 25 | { 26 | field: "id", 27 | headerName: "Id", 28 | type: "string", 29 | width: 100, 30 | disableClickEventBubbling: true, 31 | filterOperators: simpleFilterOperator, 32 | }, 33 | { 34 | field: "last_name", 35 | headerName: "Full name", 36 | description: "This column has a value getter and is not sortable.", 37 | sortable: true, 38 | flex: 1, 39 | valueGetter: (params: GridValueGetterParams) => 40 | `${params.row.first_name || ""} ${params.row.last_name || ""}`, 41 | disableClickEventBubbling: true, 42 | }, 43 | { 44 | field: "last_seen", 45 | headerName: "Last seen", 46 | type: "date", 47 | valueGetter: (params: GridValueGetterParams) => 48 | new Date(params.row.last_seen || ""), 49 | width: 160, 50 | disableClickEventBubbling: true, 51 | editable: true, 52 | filterOperators: simpleFilterOperator, 53 | }, 54 | { 55 | field: "total_spent", 56 | headerName: "Spent", 57 | type: "number", 58 | width: 150, 59 | disableClickEventBubbling: true, 60 | editable: true, 61 | filterOperators: simpleFilterOperator, 62 | }, 63 | ]; 64 | 65 | export const CustomerList = (props: any) => ( 66 | 67 | 68 | 69 | ); 70 | ``` 71 | 72 | ## Contributing 73 | 74 | ```sh 75 | ## install dependencies for core package and example 76 | yarn install 77 | ## build JS from ts 78 | yarn build 79 | ## run example 80 | yarn start 81 | ``` 82 | 83 | ## License 84 | 85 | MIT, sponsored by marmelab 86 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datagrid-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "React example starter project", 6 | "keywords": [ 7 | "react", 8 | "starter" 9 | ], 10 | "main": "src/index.js", 11 | "dependencies": { 12 | "@material-ui/data-grid": "4.0.0-alpha.26", 13 | "@types/lodash": "^4.14.168", 14 | "@types/react-dom": "^17.0.3", 15 | "data-generator-retail": "3.14.5", 16 | "ra-data-fakerest": "3.13.5", 17 | "ra-datagrid": "1.0.0", 18 | "react": "17.0.2", 19 | "react-admin": "3.14.5", 20 | "react-dom": "17.0.2", 21 | "react-scripts": "4.0.1" 22 | }, 23 | "devDependencies": { 24 | "@babel/runtime": "7.13.8", 25 | "typescript": "4.1.3" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test --env=jsdom", 31 | "eject": "react-scripts eject" 32 | }, 33 | "browserslist": [ 34 | ">0.2%", 35 | "not dead", 36 | "not ie <= 11", 37 | "not op_mini all" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /examples/simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 24 | 33 | ra-datagrid demo 34 | 35 | 36 | 37 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/simple/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as RA from "react-admin"; 2 | import generateData from "data-generator-retail"; 3 | import fakerestDataProvider from "ra-data-fakerest"; 4 | import { CustomerList } from "./CustomerList"; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple/src/CustomerList.tsx: -------------------------------------------------------------------------------- 1 | import * as RA from "react-admin"; 2 | import { GridColDef, GridValueGetterParams } from "@material-ui/data-grid"; 3 | import { Datagrid, simpleFilterOperator } from "ra-datagrid"; 4 | 5 | const columns: GridColDef[] = [ 6 | { 7 | field: "id", 8 | headerName: "Id", 9 | type: "string", 10 | width: 100, 11 | disableClickEventBubbling: true, 12 | filterOperators: simpleFilterOperator, 13 | }, 14 | { 15 | field: "last_name", 16 | headerName: "Full name", 17 | description: "This column has a value getter and is not sortable.", 18 | sortable: true, 19 | flex: 1, 20 | valueGetter: (params: GridValueGetterParams) => 21 | `${params.row.first_name || ""} ${params.row.last_name || ""}`, 22 | disableClickEventBubbling: true, 23 | }, 24 | { 25 | field: "last_seen", 26 | headerName: "Last seen", 27 | type: "date", 28 | valueGetter: (params: GridValueGetterParams) => 29 | new Date(params.row.last_seen || ""), 30 | width: 160, 31 | disableClickEventBubbling: true, 32 | editable: true, 33 | filterOperators: simpleFilterOperator, 34 | }, 35 | { 36 | field: "total_spent", 37 | headerName: "Spent", 38 | type: "number", 39 | width: 150, 40 | disableClickEventBubbling: true, 41 | editable: true, 42 | filterOperators: simpleFilterOperator, 43 | }, 44 | ]; 45 | 46 | export const CustomerList = (props: any) => ( 47 | 48 | 49 | 50 | ); 51 | -------------------------------------------------------------------------------- /examples/simple/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | ReactDOM.render(, rootElement); 8 | -------------------------------------------------------------------------------- /examples/simple/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ra-datagrid-workspaces", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*", 9 | "examples/*" 10 | ], 11 | "scripts": { 12 | "start": "yarn workspace datagrid-demo start", 13 | "build": "yarn workspace ra-datagrid build" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/ra-datagrid/README.md: -------------------------------------------------------------------------------- 1 | # ra-datagrid 2 | 3 | Integration of [Material-ui's ``](https://material-ui.com/components/data-grid/) into react-admin. 4 | 5 | ## Features 6 | 7 | - Server-side filtering, sorting and pagination via the `dataProvider` 8 | - Row selection 9 | - Cell editing 10 | - Hide / show columns 11 | - All the material-ui `` features (additional props are passed down to it) 12 | 13 | ## Usage 14 | 15 | Replace react-admin's `` by the one exported by this package. Make sure you set `pagination={false}` on the list as the Datagrid embarks its own pagination. 16 | 17 | ```tsx 18 | import { List } from "react-admin"; 19 | import { GridColDef, GridValueGetterParams } from "@material-ui/data-grid"; 20 | import { Datagrid, simpleFilterOperator } from "ra-datagrid"; 21 | 22 | const columns: GridColDef[] = [ 23 | { 24 | field: "id", 25 | headerName: "Id", 26 | type: "string", 27 | width: 100, 28 | disableClickEventBubbling: true, 29 | filterOperators: simpleFilterOperator, 30 | }, 31 | { 32 | field: "last_name", 33 | headerName: "Full name", 34 | description: "This column has a value getter and is not sortable.", 35 | sortable: true, 36 | flex: 1, 37 | valueGetter: (params: GridValueGetterParams) => 38 | `${params.row.first_name || ""} ${params.row.last_name || ""}`, 39 | disableClickEventBubbling: true, 40 | }, 41 | { 42 | field: "last_seen", 43 | headerName: "Last seen", 44 | type: "date", 45 | valueGetter: (params: GridValueGetterParams) => 46 | new Date(params.row.last_seen || ""), 47 | width: 160, 48 | disableClickEventBubbling: true, 49 | editable: true, 50 | filterOperators: simpleFilterOperator, 51 | }, 52 | { 53 | field: "total_spent", 54 | headerName: "Spent", 55 | type: "number", 56 | width: 150, 57 | disableClickEventBubbling: true, 58 | editable: true, 59 | filterOperators: simpleFilterOperator, 60 | }, 61 | ]; 62 | 63 | export const CustomerList = (props: any) => ( 64 | 65 | 66 | 67 | ); 68 | ``` 69 | 70 | ## Contributing 71 | 72 | ```sh 73 | ## install dependencies for core package and example 74 | yarn install 75 | ## build JS from ts 76 | yarn build 77 | ## run example 78 | yarn start 79 | ``` 80 | 81 | ## License 82 | 83 | MIT, sponsored by marmelab -------------------------------------------------------------------------------- /packages/ra-datagrid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ra-datagrid", 3 | "version": "1.0.0", 4 | "description": "Integration of Material-ui's into react-admin", 5 | "main": "lib/index.js", 6 | "module": "esm/index.js", 7 | "sideEffects": false, 8 | "repository": "https://github.com/marmelab/ra-datagrid", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@material-ui/data-grid": "4.0.0-alpha.26", 12 | "@types/lodash": "^4.14.168", 13 | "@types/react-dom": "^17.0.3", 14 | "lodash": "^4.17.21", 15 | "react": "17.0.2", 16 | "react-admin": "3.14.5", 17 | "react-dom": "17.0.2" 18 | }, 19 | "devDependencies": { 20 | "typescript": "4.1.3", 21 | "rimraf": "^2.6.3" 22 | }, 23 | "files": [ 24 | "*.md", 25 | "lib", 26 | "esm", 27 | "src", 28 | "docs" 29 | ], 30 | "scripts": { 31 | "build": "yarn run build-cjs && yarn run build-esm", 32 | "build-cjs": "rimraf ./lib && tsc", 33 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015", 34 | "watch": "tsc --outDir esm --module es2015 --watch" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ra-datagrid/src/Datagrid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as RA from "react-admin"; 3 | import { 4 | DataGrid as MuiDatagrid, 5 | DataGridProps as MuiDatagridProps, 6 | GridSortModel, 7 | GridSortModelParams, 8 | GridPageChangeParams, 9 | GridFilterItem, 10 | GridFilterModel, 11 | GridFilterModelParams, 12 | GridSelectionModelChangeParams, 13 | GridEditCellPropsParams, 14 | GridFilterModelState, 15 | getGridStringOperators, 16 | } from "@material-ui/data-grid"; 17 | import isEqual from "lodash/isEqual"; 18 | 19 | const operatorParamsToModel = new Map([ 20 | // boolean operators 21 | ["is", "is"], 22 | ["not", "not"], 23 | // date operators 24 | ["after", "after"], 25 | ["onOrAfter", "onOrAfter"], 26 | ["before", "before"], 27 | ["onOrBefore", "onOrBefore"], 28 | // numeric operators 29 | ["eq", "="], 30 | ["neq", "!="], 31 | ["gt", ">"], 32 | ["gte", ">="], 33 | ["lt", "<"], 34 | ["lte", "<="], 35 | // string operators 36 | ["contains", "contains"], 37 | // ["equals", "equals"], // default operator 38 | ["sw", "startsWith"], 39 | ["ew", "endsWith"], 40 | ]); 41 | 42 | const operatorModelToParams = new Map( 43 | Array.from(operatorParamsToModel, (entry) => [entry[1], entry[0]]) 44 | ); 45 | 46 | const operatorTest = /(.+)_(\w+)$/; 47 | 48 | export const defaultConvertFilterValuesToFilterModel = ( 49 | filterValues: any 50 | ): GridFilterItem[] => 51 | Object.keys(filterValues).map((name) => { 52 | const match = name.match(operatorTest); 53 | if (!match) { 54 | return { 55 | columnField: name, 56 | operatorValue: "equals", 57 | value: filterValues[name], 58 | }; 59 | } 60 | const [, realName, operator] = match; 61 | if (operatorParamsToModel.has(operator)) { 62 | return { 63 | columnField: realName, 64 | operatorValue: operatorParamsToModel.get(operator), 65 | value: filterValues[name], 66 | }; 67 | } else { 68 | return { 69 | columnField: name, 70 | operatorValue: "equals", 71 | value: filterValues[name], 72 | }; 73 | } 74 | }); 75 | 76 | export const defaultConvertFilterModelToFilterValues = ( 77 | filterModel: GridFilterModelState 78 | ): any => 79 | filterModel.items.reduce((acc, item) => { 80 | if (typeof item.columnField !== "undefined" && item.operatorValue) { 81 | if (operatorModelToParams.has(item.operatorValue)) { 82 | acc[ 83 | `${item.columnField}_${operatorModelToParams.get(item.operatorValue)}` 84 | ] = item.value; 85 | } else { 86 | acc[item.columnField] = item.value; 87 | } 88 | } 89 | return acc; 90 | }, {}); 91 | 92 | export const simpleFilterOperator = getGridStringOperators().filter( 93 | (operator: any) => operator.value === "equals" 94 | ); 95 | 96 | export interface DatagridProps extends Omit { 97 | convertFilterValuesToFilterModel?: (filterValues: any) => GridFilterItem[]; 98 | convertFilterModelToFilterValues?: (filterModel: GridFilterModelState) => any; 99 | } 100 | 101 | export const Datagrid = (props: DatagridProps) => { 102 | const { 103 | columns, 104 | convertFilterValuesToFilterModel = defaultConvertFilterValuesToFilterModel, 105 | convertFilterModelToFilterValues = defaultConvertFilterModelToFilterValues, 106 | } = props; 107 | const { 108 | // data 109 | ids, 110 | data, 111 | total, 112 | error, 113 | loading, 114 | // sorting 115 | currentSort, 116 | setSort, 117 | // pagination 118 | page, 119 | setPage, 120 | perPage, 121 | setPerPage, 122 | // filtering 123 | filterValues, 124 | setFilters, 125 | // selection 126 | selectedIds, 127 | onSelect, 128 | } = RA.useListContext(); 129 | const resource = RA.useResourceContext(); 130 | const notify = RA.useNotify(); 131 | const [update] = RA.useUpdate(resource, ""); 132 | 133 | // sorting logic 134 | 135 | const [sortModel, setSortModel] = React.useState([ 136 | { 137 | field: currentSort.field, 138 | sort: currentSort.order.toLowerCase() === "desc" ? "desc" : "asc", 139 | }, 140 | ]); 141 | 142 | React.useEffect(() => { 143 | if ( 144 | sortModel[0].field === currentSort.field && 145 | sortModel[0].sort === currentSort.order.toLowerCase() 146 | ) { 147 | return; 148 | } 149 | setSortModel([ 150 | { 151 | field: currentSort.field, 152 | sort: currentSort.order.toLowerCase() === "desc" ? "desc" : "asc", 153 | }, 154 | ]); 155 | }, [currentSort, sortModel]); 156 | 157 | const handleSortModelChange = (params: GridSortModelParams) => { 158 | if (params.sortModel.length === 0) { 159 | // reset sort 160 | setSort("id", "asc"); 161 | return; 162 | } 163 | if (params.sortModel !== sortModel) { 164 | setSort( 165 | params.sortModel[0].field, 166 | params.sortModel[0].sort?.toUpperCase() 167 | ); 168 | } 169 | }; 170 | 171 | // pagination logic 172 | 173 | const handlePageChange = (params: GridPageChangeParams) => { 174 | setPage(params.page + 1); 175 | }; 176 | 177 | const handlePageSizeChange = (params: GridPageChangeParams) => { 178 | setPerPage(params.pageSize); 179 | }; 180 | 181 | // filtering logic 182 | 183 | const [filterModel, setFilterModel] = React.useState({ 184 | items: convertFilterValuesToFilterModel(filterValues), 185 | }); 186 | 187 | React.useEffect(() => { 188 | const filterItems = convertFilterValuesToFilterModel(filterValues); 189 | if (isEqual(filterModel.items, filterItems)) { 190 | return; 191 | } 192 | setFilterModel({ 193 | items: filterItems, 194 | }); 195 | }, [filterValues, filterModel]); 196 | 197 | const handleFilterChange = (params: GridFilterModelParams) => { 198 | setFilters(convertFilterModelToFilterValues(params.filterModel), {}); 199 | }; 200 | 201 | // selection logic 202 | 203 | const handleSelectionChange = (params: GridSelectionModelChangeParams) => { 204 | onSelect(params.selectionModel); 205 | }; 206 | 207 | // edition logic 208 | 209 | const handleEditCellChangeCommitted = (params: GridEditCellPropsParams) => { 210 | update( 211 | { 212 | payload: { 213 | id: params.id, 214 | data: { 215 | [params.field]: params.props.value, 216 | }, 217 | previousData: data[params.id], 218 | }, 219 | }, 220 | { 221 | onFailure: (error) => { 222 | notify( 223 | typeof error === "string" 224 | ? error 225 | : error.message || "ra.notification.http_error", 226 | "warning", 227 | { 228 | _: 229 | typeof error === "string" 230 | ? error 231 | : error && error.message 232 | ? error.message 233 | : undefined, 234 | } 235 | ); 236 | }, 237 | } 238 | ); 239 | }; 240 | 241 | const rest: unknown = RA.sanitizeListRestProps(props); 242 | 243 | return ( 244 | data[id])} 251 | loading={loading} 252 | error={error} 253 | // sorting 254 | sortingMode="server" 255 | sortModel={sortModel} 256 | onSortModelChange={handleSortModelChange} 257 | // pagination 258 | paginationMode="server" 259 | page={page - 1} 260 | pageSize={perPage} 261 | rowCount={total} 262 | rowsPerPageOptions={[5, 10, 20]} 263 | onPageChange={handlePageChange} 264 | onPageSizeChange={handlePageSizeChange} 265 | // filtering 266 | filterMode="server" 267 | filterModel={filterModel} 268 | onFilterModelChange={handleFilterChange} 269 | // selection 270 | checkboxSelection 271 | hideFooterSelectedRowCount 272 | selectionModel={selectedIds} 273 | onSelectionModelChange={handleSelectionChange} 274 | // edition 275 | onEditCellChangeCommitted={handleEditCellChangeCommitted} 276 | {...rest} 277 | /> 278 | ); 279 | }; 280 | -------------------------------------------------------------------------------- /packages/ra-datagrid/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Datagrid'; -------------------------------------------------------------------------------- /packages/ra-datagrid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "allowJs": false 9 | }, 10 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"], 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "react-jsx" 21 | } 22 | } 23 | --------------------------------------------------------------------------------