Skip to content

Commit 259b777

Browse files
authored
Merge pull request #9 from jsdrupal/permissions
#7 Permissions
2 parents 5ad7c58 + 107e1fa commit 259b777

File tree

5 files changed

+280
-13
lines changed

5 files changed

+280
-13
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"private": true,
55
"dependencies": {
66
"emotion": "^9.0.2",
7+
"interweave": "^8.0.2",
8+
"makecancelable": "^1.0.0",
79
"prop-types": "^15.6.1",
810
"react": "^16.2.0",
911
"react-dom": "^16.2.0",
Lines changed: 156 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,160 @@
1-
import React from 'react';
2-
import { css } from 'emotion';
1+
import React, { Component, Fragment } from 'react';
2+
import makeCancelable from 'makecancelable';
3+
import { Markup } from 'interweave';
34

4-
const styles = {
5-
title: css`
6-
text-decoration: underline;
7-
`,
8-
};
5+
import Loading from '../../Helpers/Loading';
6+
import { Table, TBody, THead } from '../../UI/table';
97

10-
const Permissions = () => (
11-
<div>
12-
<h1 className={styles.title}>Permissions</h1>
13-
<p>This will be the permissions page.</p>
14-
</div>
15-
);
8+
const Permissions = class Permissions extends Component {
9+
state = {
10+
loaded: false,
11+
rawPermissions: [],
12+
renderablePermissions: [],
13+
};
14+
componentDidMount() {
15+
this.cancelFetch = makeCancelable(
16+
Promise.all([
17+
fetch(
18+
`${
19+
process.env.REACT_APP_DRUPAL_BASE_URL
20+
}/admin-api/permissions?_format=json`,
21+
).then(res => res.json()),
22+
fetch(
23+
`${
24+
process.env.REACT_APP_DRUPAL_BASE_URL
25+
}/jsonapi/user_role/user_role`,
26+
{ headers: { Accept: 'application/vnd.api+json' } },
27+
).then(res => res.json()),
28+
])
29+
.then(([permissions, { data: roles }]) =>
30+
this.setState({
31+
rawPermissions: permissions,
32+
renderablePermissions: permissions,
33+
changedRoles: [],
34+
// Move admin roles to the right.
35+
roles: roles.sort((a, b) => {
36+
if (a.attributes.is_admin && b.attributes.is_admin) {
37+
return a.attributes.id - b.attributes.id;
38+
} else if (a.attributes.is_admin) {
39+
return 1;
40+
} else if (b.attributes.is_admin) {
41+
return -1;
42+
}
43+
return a.attributes.id - b.attributes.id;
44+
}),
45+
loaded: true,
46+
}),
47+
)
48+
.catch(err => this.setState({ loaded: false, err })),
49+
);
50+
}
51+
componentWillUnmount() {
52+
this.cancelFetch();
53+
}
54+
onPermissionCheck = (roleName, permission) => {
55+
this.setState(prevState => ({
56+
changedRoles: [...new Set(prevState.changedRoles).add(roleName).values()],
57+
roles: this.togglePermission(permission, roleName, prevState.roles),
58+
}));
59+
};
60+
togglePermission = (permission, roleName, roles) => {
61+
const roleIndex = roles.map(role => role.attributes.id).indexOf(roleName);
62+
const role = roles[roleIndex];
63+
const index = role.attributes.permissions.indexOf(permission);
64+
if (index !== -1) {
65+
role.attributes.permissions.splice(index, 1);
66+
} else {
67+
role.attributes.permissions.push(permission);
68+
}
69+
roles[roleIndex] = role;
70+
return roles;
71+
};
72+
groupPermissions = permissions =>
73+
Object.entries(
74+
permissions.reduce((acc, cur) => {
75+
acc[cur.provider] = acc[cur.provider] || {
76+
providerLabel: cur.provider_label,
77+
permissions: [],
78+
};
79+
acc[cur.provider].permissions.push(cur);
80+
return acc;
81+
}, {}),
82+
);
83+
createTableRows = (groupedPermissions, roles) =>
84+
[].concat(
85+
...groupedPermissions.map(
86+
([providerMachineName, { providerLabel, permissions }]) => [
87+
{
88+
key: `permissionGroup-${providerMachineName}`,
89+
colspan: roles.length + 1,
90+
tds: [[`td-${providerMachineName}`, <b>{providerLabel}</b>]],
91+
},
92+
...permissions.map(permission => ({
93+
key: `permissionGroup-${providerMachineName}-${permission.title}`,
94+
tds: [
95+
[
96+
`td-${providerMachineName}-${permission.title}`,
97+
<Markup content={permission.title} />,
98+
],
99+
...roles.map(({ attributes }, index) => [
100+
`td-${providerMachineName}-${permission.title}-${index}-cb`,
101+
attributes.is_admin && attributes.id === 'administrator' ? (
102+
<input type="checkbox" checked />
103+
) : (
104+
<input
105+
type="checkbox"
106+
onChange={() =>
107+
this.onPermissionCheck(attributes.id, permission.id)
108+
}
109+
checked={attributes.permissions.includes(permission.id)}
110+
/>
111+
),
112+
]),
113+
],
114+
})),
115+
],
116+
),
117+
);
118+
handleKeyPress = event => {
119+
const input = event.target.value.toLowerCase();
120+
this.setState(prevState => ({
121+
...prevState,
122+
renderablePermissions: prevState.rawPermissions.filter(
123+
({ title, description, provider, provider_label: providerLabel }) =>
124+
`${title}${description}${provider}${providerLabel}`.includes(input),
125+
),
126+
}));
127+
};
128+
render() {
129+
return !this.state.loaded ? (
130+
<Loading />
131+
) : (
132+
<Fragment>
133+
<input
134+
type="text"
135+
placeholder="Filter by name, description or module"
136+
onChange={this.handleKeyPress}
137+
onKeyDown={this.handleKeyPress}
138+
/>
139+
<Table zebra>
140+
<THead
141+
data={[
142+
'PERMISSION',
143+
...this.state.roles.map(({ attributes: { label } }) =>
144+
label.toUpperCase(),
145+
),
146+
]}
147+
/>
148+
<TBody
149+
rows={this.createTableRows(
150+
this.groupPermissions(this.state.renderablePermissions),
151+
this.state.roles,
152+
)}
153+
/>
154+
</Table>
155+
</Fragment>
156+
);
157+
}
158+
};
16159

17160
export default Permissions;

src/components/Helpers/Loading.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import { css, keyframes } from 'emotion';
3+
4+
const rotate = keyframes`
5+
from {
6+
transform: rotate(-10deg);
7+
}
8+
to {
9+
transform: rotate(10deg);
10+
}
11+
`;
12+
13+
const styles = {
14+
wrap: css`
15+
margin: 100px auto 0;
16+
`,
17+
peace: css`
18+
display: inline-block;
19+
vertical-align: middle;
20+
animation-direction: alternate;
21+
animation-iteration-count: infinite;
22+
animation-duration: 0.5s;
23+
animation-timing-function: cubic-bezier(0, 0, 1, 1);
24+
transform-origin: bottom;
25+
font-size: 50px;
26+
animation-name: ${rotate};
27+
`,
28+
};
29+
30+
const Loading = () => (
31+
<div className={styles.wrap}>
32+
<span className={styles.peace} role="img" aria-label="Peace Sign">
33+
✌️
34+
</span>
35+
</div>
36+
);
37+
38+
export default Loading;

src/components/UI/table.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { css } from 'emotion';
3+
import {
4+
node,
5+
bool,
6+
oneOfType,
7+
arrayOf,
8+
string,
9+
shape,
10+
number,
11+
} from 'prop-types';
12+
13+
const TABLE = ({ children, zebra, ...props }) => {
14+
const styles = css`
15+
${zebra ? 'tbody tr:nth-child(odd) {background-color: #e8e8e8;}' : ''};
16+
`;
17+
return (
18+
<table className={styles} {...props}>
19+
{children}
20+
</table>
21+
);
22+
};
23+
TABLE.propTypes = {
24+
children: oneOfType([arrayOf(node), node]).isRequired,
25+
zebra: bool,
26+
};
27+
TABLE.defaultProps = {
28+
zebra: false,
29+
};
30+
31+
const TR = ({ children, ...props }) => <tr {...props}>{children}</tr>;
32+
TR.propTypes = {
33+
children: oneOfType([arrayOf(node), node]).isRequired,
34+
};
35+
36+
const TD = ({ children, ...props }) => <td {...props}>{children}</td>;
37+
TD.propTypes = {
38+
children: oneOfType([arrayOf(node), node]).isRequired,
39+
};
40+
41+
const THEAD = ({ data }) => (
42+
<thead>
43+
<TR>{data.map(label => <TD key={`column-${label}`}>{label}</TD>)}</TR>
44+
</thead>
45+
);
46+
THEAD.propTypes = {
47+
data: arrayOf(string).isRequired,
48+
};
49+
50+
const TBODY = ({ rows }) => (
51+
<tbody>
52+
{rows.map(({ colspan, tds, key }) => (
53+
<TR key={key}>
54+
{tds.map(([tdKey, tdValue]) => (
55+
<TD key={tdKey} colSpan={colspan || undefined}>
56+
{tdValue}
57+
</TD>
58+
))}
59+
</TR>
60+
))}
61+
</tbody>
62+
);
63+
TBODY.propTypes = {
64+
rows: arrayOf(
65+
shape({
66+
colspan: number,
67+
key: string,
68+
tds: arrayOf(node).isRequired,
69+
}),
70+
).isRequired,
71+
};
72+
73+
export { TR, TD, TABLE as Table, TBODY as TBody, THEAD as THead };

yarn.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3700,6 +3700,13 @@ interpret@^1.0.0:
37003700
version "1.1.0"
37013701
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
37023702

3703+
interweave@^8.0.2:
3704+
version "8.0.2"
3705+
resolved "https://registry.yarnpkg.com/interweave/-/interweave-8.0.2.tgz#51001199c58d311f8b1d0100c4f6b9d494e97480"
3706+
dependencies:
3707+
babel-runtime "^6.26.0"
3708+
prop-types "^15.6.0"
3709+
37033710
invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
37043711
version "2.2.3"
37053712
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688"
@@ -4647,6 +4654,10 @@ make-dir@^1.0.0:
46474654
dependencies:
46484655
pify "^3.0.0"
46494656

4657+
makecancelable@^1.0.0:
4658+
version "1.0.0"
4659+
resolved "https://registry.yarnpkg.com/makecancelable/-/makecancelable-1.0.0.tgz#c7e2606e59db7a4bf8098ff5b52d7f13173499e5"
4660+
46504661
46514662
version "1.0.11"
46524663
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"

0 commit comments

Comments
 (0)