-
Notifications
You must be signed in to change notification settings - Fork 88
#7 Permissions #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
05b51be
70b19e4
7351d07
7663ba1
ef5701e
9cbe922
406d2ab
e6126cb
2513f1e
107e1fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,121 @@ | ||
import React from 'react'; | ||
import { css } from 'emotion'; | ||
import React, { Component, Fragment } from 'react'; | ||
import makeCancelable from 'makecancelable'; | ||
import { Markup } from 'interweave'; | ||
|
||
const styles = { | ||
title: css` | ||
text-decoration: underline; | ||
`, | ||
}; | ||
import Loading from '../../Helpers/Loading'; | ||
import { Table, TableBody, TableHeaderSimple } from '../../UI'; | ||
|
||
const Permissions = () => ( | ||
<div> | ||
<h1 className={styles.title}>Permissions</h1> | ||
<p>This will be the permissions page.</p> | ||
</div> | ||
); | ||
const Permissions = class Permissions extends Component { | ||
state = { | ||
loaded: false, | ||
}; | ||
componentDidMount() { | ||
this.cancelFetch = makeCancelable( | ||
Promise.all([ | ||
fetch( | ||
`${ | ||
process.env.REACT_APP_DRUPAL_BASE_URL | ||
}/admin-api/permissions?_format=json`, | ||
).then(res => res.json()), | ||
fetch( | ||
`${ | ||
process.env.REACT_APP_DRUPAL_BASE_URL | ||
}/jsonapi/user_role/user_role`, | ||
).then(res => res.json()), | ||
]) | ||
.then(([permissions, { data: roles }]) => | ||
this.setState({ | ||
rawPermissions: Object.keys(permissions).map( | ||
key => permissions[key], | ||
), | ||
renderablePermissions: Object.keys(permissions).map( | ||
key => permissions[key], | ||
), | ||
roles, | ||
loaded: true, | ||
}), | ||
) | ||
.catch(err => this.setState({ loaded: false, err })), | ||
); | ||
} | ||
componentWillUnmount() { | ||
this.cancelFetch(); | ||
} | ||
groupPermissions = permissions => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice code! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing we will need to figure out, is how to handle polyfills. Since this uses |
||
Object.entries( | ||
permissions.reduce((acc, cur) => { | ||
acc[cur.provider] = acc[cur.provider] || []; | ||
acc[cur.provider].push(cur); | ||
return acc; | ||
}, {}), | ||
); | ||
createTableRows = (groupedPermissions, roles) => | ||
[].concat( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarity, I'm not after any change here, I'm genuinely curious. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @e0ipso It's a bit subtle, but this is the easiest way to flatten the two-dimensional array that's created from constructing the table rows. |
||
...groupedPermissions.map(([permissionGroupName, permissions]) => [ | ||
{ | ||
key: `permissionGroup-${permissionGroupName}`, | ||
colspan: roles.length + 1, | ||
tds: [<b>{permissionGroupName}</b>], | ||
}, | ||
...permissions.map(permission => ({ | ||
key: `permissionGroup-${permissionGroupName}-${permission.title}`, | ||
tds: [ | ||
<Markup content={permission.title} />, | ||
...roles.map( | ||
({ attributes }) => | ||
attributes.is_admin && attributes.id === 'administrator' ? ( | ||
<input type="checkbox" checked /> | ||
) : ( | ||
<input | ||
type="checkbox" | ||
checked={attributes.permissions.includes(permission.id)} | ||
/> | ||
), | ||
), | ||
], | ||
})), | ||
]), | ||
); | ||
handleKeyPress = event => { | ||
const input = event.target.value.toLowerCase(); | ||
this.setState(prevState => ({ | ||
...prevState, | ||
renderablePermissions: prevState.rawPermissions.filter( | ||
({ title, description, provider, provider_label }) => | ||
`${title}${description}${provider}${provider_label}`.includes(input), | ||
), | ||
})); | ||
}; | ||
render() { | ||
return !this.state.loaded ? ( | ||
<Loading /> | ||
) : ( | ||
<Fragment> | ||
<input | ||
type="text" | ||
placeholder="Filter by name, description or module" | ||
onChange={this.handleKeyPress} | ||
onKeyDown={this.handleKeyPress} | ||
/> | ||
<Table zebra> | ||
<TableHeaderSimple | ||
data={[ | ||
'PERMISSION', | ||
...this.state.roles.map(({ attributes: { label } }) => | ||
label.toUpperCase(), | ||
), | ||
]} | ||
/> | ||
<TableBody | ||
rows={this.createTableRows( | ||
this.groupPermissions(this.state.renderablePermissions), | ||
this.state.roles, | ||
)} | ||
/> | ||
</Table> | ||
</Fragment> | ||
); | ||
} | ||
}; | ||
|
||
export default Permissions; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React from 'react'; | ||
import { css } from 'emotion'; | ||
|
||
const spinner = css` | ||
margin: 100px auto 0; | ||
width: 70px; | ||
text-align: center; | ||
> div { | ||
width: 10px; | ||
height: 10px; | ||
background-color: #333; | ||
|
||
border-radius: 100%; | ||
display: inline-block; | ||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; | ||
animation: sk-bouncedelay 1.4s infinite ease-in-out both; | ||
} | ||
.bounce1 { | ||
-webkit-animation-delay: -0.32s; | ||
animation-delay: -0.32s; | ||
} | ||
.bounce2 { | ||
-webkit-animation-delay: -0.16s; | ||
animation-delay: -0.16s; | ||
} | ||
@-webkit-keyframes sk-bouncedelay { | ||
0%, | ||
80%, | ||
100% { | ||
-webkit-transform: scale(0); | ||
} | ||
40% { | ||
-webkit-transform: scale(1); | ||
} | ||
} | ||
@keyframes sk-bouncedelay { | ||
0%, | ||
80%, | ||
100% { | ||
-webkit-transform: scale(0); | ||
transform: scale(0); | ||
} | ||
40% { | ||
-webkit-transform: scale(1); | ||
transform: scale(1); | ||
} | ||
} | ||
`; | ||
|
||
const Loading = () => ( | ||
<div className={spinner}> | ||
<div className="bounce1" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've had some really Bad Times(tm) with using class names this way when the project starts to grow. The best way we've found is to have an object called styles and then do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, this totally makes sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we resolved this? |
||
<div className="bounce2" /> | ||
<div className="bounce3" /> | ||
</div> | ||
); | ||
|
||
export default Loading; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { TR, TD, TABLE, TBODY, THEAD } from './table'; | ||
|
||
export { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we do this instead of directly importing from a table file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this so if we ever have any other components, they are all importable from a single file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally I think we should come back at some point and have a look at the bigger picture to understand how we can make the entire app more readable and not just single bits and pieces. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had a look at some other libraries and the lodash method seems to be popular where you pull in the thing you need like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @justafish @dawehner We can pretty easily switch up the exports here. Probably better to set a good initial pattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mattgrill cool cool, let's do away with index.js then. Not sure what we should have the exports as exactly? My preference would be: |
||
TR as TableRow, | ||
TD as TableData, | ||
TABLE as Table, | ||
TBODY as TableBody, | ||
THEAD as TableHeaderSimple, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
import { css } from 'emotion'; | ||
import { | ||
node, | ||
bool, | ||
oneOfType, | ||
arrayOf, | ||
string, | ||
shape, | ||
number, | ||
} from 'prop-types'; | ||
|
||
const generateIDs = arr => | ||
arr.map(value => ({ | ||
value, | ||
id: Math.random() | ||
.toString(36) | ||
.substring(2), | ||
})); | ||
|
||
const TABLE = ({ children, zebra, ...props }) => { | ||
const styles = css` | ||
${zebra ? 'tbody tr:nth-child(odd) {background-color: #e8e8e8;}' : ''}; | ||
`; | ||
return ( | ||
<table className={styles} {...props}> | ||
{children} | ||
</table> | ||
); | ||
}; | ||
TABLE.propTypes = { | ||
children: oneOfType([arrayOf(node), node]).isRequired, | ||
zebra: bool, | ||
}; | ||
TABLE.defaultProps = { | ||
zebra: false, | ||
}; | ||
|
||
const TR = ({ children, ...props }) => <tr {...props}>{children}</tr>; | ||
TR.propTypes = { | ||
children: oneOfType([arrayOf(node), node]).isRequired, | ||
}; | ||
|
||
const TD = ({ children, ...props }) => <td {...props}>{children}</td>; | ||
TD.propTypes = { | ||
children: oneOfType([arrayOf(node), node]).isRequired, | ||
}; | ||
|
||
const THEAD = ({ data }) => ( | ||
<thead> | ||
<TR>{data.map(label => <TD key={`column-${label}`}>{label}</TD>)}</TR> | ||
</thead> | ||
); | ||
THEAD.propTypes = { | ||
data: arrayOf(string).isRequired, | ||
}; | ||
|
||
const TBODY = ({ rows }) => ( | ||
<tbody> | ||
{rows.map(({ colspan, tds, key }) => ( | ||
<TR key={key}> | ||
{generateIDs(tds).map(({ id, value }) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the advantage of doing this over using the array index? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React gets sad if you use the index as part of the key. |
||
<TD key={`td-${key}-${id}`} colSpan={colspan || undefined}> | ||
{value} | ||
</TD> | ||
))} | ||
</TR> | ||
))} | ||
</tbody> | ||
); | ||
TBODY.propTypes = { | ||
rows: arrayOf( | ||
shape({ | ||
colspan: number, | ||
key: string, | ||
tds: arrayOf(node).isRequired, | ||
}), | ||
).isRequired, | ||
}; | ||
|
||
export { TR, TD, TABLE, TBODY, THEAD }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3700,6 +3700,13 @@ interpret@^1.0.0: | |
version "1.1.0" | ||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" | ||
|
||
interweave@^8.0.2: | ||
version "8.0.2" | ||
resolved "https://registry.yarnpkg.com/interweave/-/interweave-8.0.2.tgz#51001199c58d311f8b1d0100c4f6b9d494e97480" | ||
dependencies: | ||
babel-runtime "^6.26.0" | ||
prop-types "^15.6.0" | ||
|
||
invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: | ||
version "2.2.3" | ||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688" | ||
|
@@ -4647,6 +4654,10 @@ make-dir@^1.0.0: | |
dependencies: | ||
pify "^3.0.0" | ||
|
||
makecancelable@^1.0.0: | ||
version "1.0.0" | ||
resolved "https://registry.yarnpkg.com/makecancelable/-/makecancelable-1.0.0.tgz#c7e2606e59db7a4bf8098ff5b52d7f13173499e5" | ||
|
||
[email protected]: | ||
version "1.0.11" | ||
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably want to add the
Accept: application/vnd.api+json
header. This will enable 404/403 errors in JSON format (if any). Otherwise they'll come back as HTML errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍