Skip to content

Commit 8e9d53b

Browse files
authored
Merge pull request #2 from fizix-io/extract-storage
[RFR] Extract dialog and storage from CustomizableDatagrid
2 parents 13f5285 + 730fcc0 commit 8e9d53b

File tree

7 files changed

+271
-205
lines changed

7 files changed

+271
-205
lines changed

README.md

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
<p align="center">
2-
<a href="https://fizix.io/" rel="noopener" target="_blank"><img width="200" src="https://s3.eu-central-1.amazonaws.com/fizix-assets/images/logos/fizix-logo-black.png" alt="Fizix logo"></a></p>
3-
</p>
1+
2+
<div align="center">
3+
<div style="margin-bottom: 20px">
4+
[![npm](https://img.shields.io/npm/v/ra-customizable-datagrid.svg)](ra-customizable-datagrid)
5+
</div>
6+
7+
<a href="https://fizix.io" rel="noopener" target="_blank"><img width="70" src="https://s3.eu-central-1.amazonaws.com/fizix-assets/images/logos/fizix-logo-black.png" alt="Fizix logo"></a>
8+
</div>
49

510
<h1 align="center">ra-customizable-datagrid for <a rel="noopener" target="_blank" href="https://github.com/marmelab/react-admin/">React Admin</a></h1>
611

712
<div align="center">
813

9-
[React Admin](https://github.com/marmelab/react-admin/) plugin that allows to hide / show columns dynamically. Preferences are stored in local storage.
14+
[React Admin](https://github.com/marmelab/react-admin/) plugin that allows to hide / show columns dynamically.
1015

1116
</div>
1217

@@ -19,15 +24,18 @@
1924
<img width="800" src="./demo/demo.gif" alt="Demo">
2025
</p>
2126

22-
## How to run the demo locally
2327

24-
```
25-
$> npm run demo-install
26-
$> npm run demo
27-
```
28+
## Features
29+
30+
* Users can show/hide columns, obviously
31+
* Users preferences are stored by resource
32+
* The storage mechanism can be replaced
33+
* Developers can choose the default visible columns
34+
2835
## Installation
2936

3037
ra-customizable-datagrid is available from npm. You can install it (and its required dependencies) using:
38+
3139
```
3240
$> npm install --save ra-customizable-datagrid
3341
```
@@ -36,9 +44,7 @@ or
3644
$> yarn add ra-customizable-datagrid
3745
```
3846

39-
## At a glance
40-
41-
Just replace `Datagrid`
47+
Then replace React Admin `Datagrid` by `CustomizableDatagrid`
4248

4349
```jsx
4450
import CustomizableDatagrid from 'ra-customizable-datagrid';
@@ -53,9 +59,32 @@ const PostList = props => (
5359
);
5460
```
5561

56-
## Features
57-
* preferences are stored in local storage
58-
* `defaultColumns` prop is used to set which column is displayed at the first visit of the user
62+
## Configuration
63+
64+
### Storage
65+
66+
By default LocalStorage is used to store user preferences.
67+
68+
If you need to store them somewhere else, use the `storage` props like this :
69+
70+
```jsx
71+
<CustomizableDatagrid storage={CustomStorage}>
72+
```
73+
74+
where CustomStorage is an object with the `set` and `get` methods :
75+
76+
```js
77+
const CustomStorage = {
78+
get: (resourceName) => /* your own logic here */,
79+
set: (resourceName, selectedColumns) => /* your own logic here */,
80+
};
81+
```
82+
83+
### Default columns
84+
85+
All the columns are visible by default.
86+
87+
This behavior can be changed with the `defaultColumns` prop. Just pass an array containing the name of the columns you want to be visible.
5988

6089
```jsx
6190
import CustomizableDatagrid from 'ra-customizable-datagrid';
@@ -70,5 +99,13 @@ const PostList = props => (
7099
);
71100
```
72101

102+
## How to run the demo locally
103+
104+
```
105+
$> npm run demo-install
106+
$> npm run demo
107+
```
108+
73109
## License
110+
74111
`ra-customizable-datagrid` is licensed under the MIT License, sponsored and supported by <a href="https://fizix.io/" rel="noopener" target="_blank">Fizix</a>.

demo/demo.gif

43.3 KB
Loading

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
"description": "",
55
"main": "lib/index.js",
66
"scripts": {
7-
"build": "/node_modules/.bin/babel ./src/ -d lib",
7+
"build": "babel ./src/ -d lib",
88
"demo": "npm run start --prefix ./demo",
99
"demo-install": "yarn install --cwd ./demo && yarn install && npm run install:peers",
1010
"install:peers": "yarn add -P $(jq -r '.peerDependencies | to_entries | map(\"\\(.key)@\\(.value | tostring)\") | join(\" \")' package.json)",
11-
"prettier-js": "./node_modules/.bin/prettier --write '{src,demo}/**/*.js'"
11+
"prettier-js": "prettier --write '{src,demo}/**/*.js'"
1212
},
1313
"repository": {
1414
"type": "git",

src/CustomizableDatagrid.js

Lines changed: 85 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -3,178 +3,137 @@ import T from 'prop-types';
33

44
import { Datagrid } from 'react-admin';
55

6-
/* utils */
76
import isEmpty from 'lodash/isEmpty';
7+
import filter from 'lodash/filter';
8+
import get from 'lodash/get';
89

9-
/* icons */
10-
import Icon from '@material-ui/icons/ViewColumn';
11-
import IconClose from '@material-ui/icons/Close';
12-
13-
/* material-ui */
10+
import ColumnIcon from '@material-ui/icons/ViewColumn';
1411
import Button from '@material-ui/core/Button';
15-
import DialogTitle from '@material-ui/core/DialogTitle';
16-
import DialogContent from '@material-ui/core/DialogContent';
17-
import DialogActions from '@material-ui/core/DialogActions';
18-
import Dialog from '@material-ui/core/Dialog';
19-
import FormControlLabel from '@material-ui/core/FormControlLabel';
20-
import FormGroup from '@material-ui/core/FormGroup';
21-
import Checkbox from '@material-ui/core/Checkbox';
2212

23-
const LS = 'raColumnsConfig';
13+
import SelectionDialog from './SelectionDialog';
14+
import LocalStorage from './LocalStorage';
15+
16+
const arrayToSelection = values =>
17+
values.reduce((selection, columnName) => {
18+
selection[columnName] = true;
19+
return selection;
20+
}, {});
2421

2522
// CustomizableDatagrid allows to show/hide columns dynamically
2623
// the preferences are stored in local storage
2724
class CustomizableDatagrid extends Component {
2825
constructor(props) {
2926
super(props);
3027
this.state = {
31-
open: false,
32-
selection: {},
28+
modalOpened: false,
29+
selection: this.getInitialSelection(),
3330
};
31+
}
32+
33+
getColumnNames() {
34+
const { children } = this.props;
35+
return filter(React.Children.map(children, field => get(field, ['props', 'source'])));
36+
}
3437

35-
const { defaultColumns, resource, children } = props;
38+
getColumnLabels() {
39+
const { children } = this.props;
40+
return filter(
41+
React.Children.map(
42+
children,
43+
field =>
44+
field && {
45+
source: get(field, ['props', 'source']),
46+
label: get(field, ['props', 'label']),
47+
},
48+
),
49+
item => item && item.source,
50+
);
51+
}
3652

37-
let localStorageValue = null;
38-
let localStorageValueForResource = null;
53+
getInitialSelection() {
54+
const { defaultColumns, resource, children, storage } = this.props;
3955

40-
// we try to retrieve the local storage value for this resource
41-
try {
42-
localStorageValue = JSON.parse(
43-
isEmpty(window.localStorage.getItem(LS)) ? '{}' : window.localStorage.getItem(LS),
44-
);
45-
localStorageValueForResource = localStorageValue[resource] || {};
46-
} catch (e) {} // ignore - window.localStorage is unreliable
56+
const previousSelection = storage.get(resource);
4757

48-
// if this is the first time the user come to the application / view
49-
if (!isEmpty(defaultColumns) && isEmpty(localStorageValueForResource)) {
50-
defaultColumns.forEach(defaultValue => {
51-
this.state.selection[defaultValue] = true;
52-
});
53-
}
54-
// we try to apply the local storage state to our internal state
55-
else if (!isEmpty(localStorageValueForResource)) {
56-
Object.keys(localStorageValueForResource).forEach(key => {
57-
this.state.selection[key] = localStorageValueForResource[key];
58-
});
58+
// if we have a previously stored value, let's return it
59+
if (!isEmpty(previousSelection)) {
60+
return previousSelection;
5961
}
60-
// otherwise we fallback on the default behaviour
61-
// -> display all columns
62-
else {
63-
React.Children.forEach(children, field => {
64-
this.state.selection[field.props.source] = true;
65-
});
62+
63+
// if defaultColumns are set let's return them
64+
if (!isEmpty(defaultColumns)) {
65+
return arrayToSelection(defaultColumns);
6666
}
6767

68-
this.updateLocalStorage();
68+
// otherwise we fallback on the default behaviour : display all columns
69+
return arrayToSelection(this.getColumnNames());
6970
}
7071

71-
// updates the local storage with the internal state value
72-
updateLocalStorage = () => {
73-
const { resource } = this.props;
74-
const { selection } = this.state;
72+
// updates the storage with the internal state value
73+
updateStorage = () => {
74+
this.props.storage.set(this.props.resource, this.state.selection);
75+
};
7576

76-
// maybe there isnt an old value
77-
let oldValue = {};
78-
try {
79-
oldValue = JSON.parse(window.localStorage.getItem(LS));
80-
} catch (e) {}
77+
toggleColumn = columnName => {
78+
const previousSelection = this.state.selection;
79+
const selection = {
80+
...previousSelection,
81+
[columnName]: !previousSelection[columnName],
82+
};
83+
this.setState({ selection }, this.updateStorage);
84+
};
8185

82-
const value = JSON.stringify({
83-
...oldValue,
84-
[resource]: selection,
85-
});
86+
handleOpen = () => this.setState({ modalOpened: true });
87+
handleClose = () => this.setState({ modalOpened: false });
8688

87-
try {
88-
window.localStorage.setItem(LS, value);
89-
} catch (e) {}
90-
};
89+
renderChild = child => {
90+
const source = get(child, ['props', 'source']);
91+
const { selection } = this.state;
9192

92-
toggleColumn = event => {
93-
this.setState(
94-
{
95-
selection: {
96-
...this.state.selection,
97-
[event.target.value]: !this.state.selection[event.target.value],
98-
},
99-
},
100-
this.updateLocalStorage,
101-
);
102-
};
93+
// Show children without source, or children explicitly visible
94+
if (!source || selection[source]) {
95+
return React.cloneElement(child, {});
96+
}
10397

104-
handleOpen = () => this.setState({ open: true });
105-
handleClose = () => this.setState({ open: false });
98+
return null;
99+
};
106100

107101
render() {
108102
const { children, defaultColumns, ...rest } = this.props;
109-
110-
const columns = React.Children.map(children, field => field.props.source);
103+
const { selection, modalOpened } = this.state;
111104

112105
return (
113106
<div>
114107
<div style={{ float: 'right', marginRight: '1rem' }}>
115-
<Button
116-
variant="outlined"
117-
mini
118-
color="secondary"
119-
aria-label="add"
120-
onClick={this.handleOpen}
121-
>
122-
<Icon />
108+
<Button variant="outlined" mini aria-label="add" onClick={this.handleOpen}>
109+
<ColumnIcon />
123110
</Button>
124-
{this.state.open && (
125-
<Dialog
126-
maxWidth="xs"
127-
aria-labelledby="confirmation-dialog-title"
128-
open={this.state.open}
129-
onEscapeKeyDown={this.handleClose}
130-
onBackdropClick={this.handleClose}
131-
>
132-
<DialogTitle id="confirmation-dialog-title">Configuration</DialogTitle>
133-
<DialogContent>
134-
<FormGroup>
135-
{columns.map(column => (
136-
<FormControlLabel
137-
key={column}
138-
control={
139-
<Checkbox
140-
checked={!!this.state.selection[column]}
141-
onChange={this.toggleColumn}
142-
value={column}
143-
/>
144-
}
145-
label={column}
146-
/>
147-
))}
148-
</FormGroup>
149-
</DialogContent>
150-
<DialogActions>
151-
<Button onClick={this.handleClose} color="primary">
152-
<IconClose />
153-
</Button>
154-
</DialogActions>
155-
</Dialog>
156-
)}
157111
</div>
158-
<Datagrid {...rest}>
159-
{React.Children.map(
160-
children,
161-
child =>
162-
child && !!this.state.selection[child.props.source]
163-
? React.cloneElement(child, {})
164-
: null,
165-
)}
166-
</Datagrid>
112+
{modalOpened && (
113+
<SelectionDialog
114+
selection={selection}
115+
columns={this.getColumnLabels()}
116+
onColumnClicked={this.toggleColumn}
117+
onClose={this.handleClose}
118+
/>
119+
)}
120+
<Datagrid {...rest}>{React.Children.map(children, this.renderChild)}</Datagrid>
167121
</div>
168122
);
169123
}
170124
}
171125

172126
CustomizableDatagrid.propTypes = {
173127
defaultColumns: T.arrayOf(T.string),
128+
storage: T.shape({
129+
get: T.func.isRequired,
130+
set: T.func.isRequired,
131+
}),
174132
};
175133

176134
CustomizableDatagrid.defaultProps = {
177135
defaultColumns: [],
136+
storage: LocalStorage,
178137
};
179138

180139
export default CustomizableDatagrid;

0 commit comments

Comments
 (0)