-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature:admin can rollback to a previous version of an entry, display…
…s diff
- Loading branch information
Peter Dinh
committed
Sep 26, 2017
1 parent
0ddcdff
commit e2229f6
Showing
17 changed files
with
758 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import xhr from 'xhr'; | ||
import assign from 'object-assign'; | ||
|
||
import { | ||
LOAD_REVISIONS, | ||
DATA_LOADING_SUCCESS, | ||
DATA_LOADING_ERROR, | ||
SELECT_REVISION, | ||
} from './constants'; | ||
|
||
export const loadRevisions = () => { | ||
return (dispatch, getState) => { | ||
dispatch({ type: LOAD_REVISIONS }); | ||
const state = getState(); | ||
const { singular, path } = state.lists.currentList; | ||
const { id } = state.item; | ||
const url = `${Keystone.adminPath}/api/${path}/${id}/revisions`; | ||
xhr({ | ||
url, | ||
method: 'POST', | ||
headers: assign({}, Keystone.csrf.header), | ||
json: { | ||
id, | ||
list: singular, | ||
}, | ||
}, (err, resp, body) => { | ||
if (err) return dispatch({ type: DATA_LOADING_ERROR, payload: err }); | ||
// Pass the body as result or error, depending on the statusCode | ||
if (resp.statusCode === 200) { | ||
if (!body.length) { | ||
dispatch(dataLoadingError(id)); | ||
} else { | ||
dispatch(dataLoaded(body)); | ||
} | ||
} else { | ||
dispatch(dataLoadingError()); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
export const dataLoaded = data => ({ | ||
type: DATA_LOADING_SUCCESS, | ||
payload: data, | ||
}); | ||
|
||
export const dataLoadingError = id => ({ | ||
type: DATA_LOADING_ERROR, | ||
payload: id, | ||
}); | ||
|
||
export const selectRevision = revision => { | ||
return (dispatch, getState) => { | ||
const state = getState(); | ||
const id = state.revisions.selectedRevision._id; | ||
if (id === revision._id) { | ||
dispatch({ type: SELECT_REVISION, payload: {} }); | ||
} else { | ||
dispatch({ type: SELECT_REVISION, payload: revision }); | ||
} | ||
}; | ||
}; | ||
|
||
export const applyChanges = router => { | ||
return (dispatch, getState) => { | ||
const state = getState(); | ||
const { currentList } = state.lists; | ||
const { id } = state.item; | ||
const { data, _id: rollbackId } = state.revisions.selectedRevision; | ||
const { currentItem } = state.revisions; | ||
const redirectUrl = `${Keystone.adminPath}/${currentList.path}/${id}`; | ||
const file = { | ||
filename: data.filename, | ||
mimetype: data.mimetype, | ||
path: data.path, | ||
originalname: data.originalname, | ||
size: data.size, | ||
url: data.url, | ||
}; | ||
data.filename ? data.file = file : data.file = ''; // empty string to rollback to no file | ||
// this is to account for text fields being undefined upon entry creation | ||
for (const k in currentItem) { | ||
if (!data[k]) data[k] = ''; | ||
} | ||
// delete target revision to prevent a clog of revisions of the same type | ||
xhr({ | ||
url: `${Keystone.adminPath}/api/${currentList.singular}/${rollbackId}/delete/revision`, | ||
method: 'POST', | ||
headers: assign({}, Keystone.csrf.header), | ||
}, () => { | ||
currentList.updateItem(id, data, (err, data) => { | ||
// TODO proper error handling | ||
dispatch({ type: SELECT_REVISION, payload: {} }); | ||
router.push(redirectUrl); | ||
}); | ||
}); | ||
}; | ||
}; |
46 changes: 46 additions & 0 deletions
46
admin/client/App/screens/Revision/components/RevisionHeader.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react'; | ||
import { Link } from 'react-router'; | ||
import { GlyphButton, FormInput } from '../../../elemental'; | ||
|
||
const RevisionHeader = ({ | ||
id, | ||
routeParams, | ||
}) => { | ||
const renderBack = () => { | ||
const backPath = `${Keystone.adminPath}/${routeParams.listId}/${id}`; | ||
return ( | ||
<GlyphButton | ||
component={Link} | ||
data-e2e-editform-header-back | ||
glyph="chevron-left" | ||
position="left" | ||
to={backPath} | ||
variant="link" | ||
> | ||
Back | ||
</GlyphButton> | ||
); | ||
}; | ||
|
||
return ( | ||
<div style={styles.container}> | ||
<span>{renderBack()}</span> | ||
<FormInput noedit style={styles.title}> | ||
Revisions for {id} | ||
</FormInput> | ||
</div> | ||
); | ||
}; | ||
|
||
const styles = { | ||
container: { | ||
borderBottom: '1px dashed #e1e1e1', | ||
margin: 'auto', | ||
paddingBottom: '1em', | ||
}, | ||
title: { | ||
fontSize: '1.3rem', | ||
}, | ||
}; | ||
|
||
export default RevisionHeader; |
123 changes: 123 additions & 0 deletions
123
admin/client/App/screens/Revision/components/RevisionItem.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
import moment from 'moment'; | ||
import ReactJson from 'react-json-view'; | ||
import { GlyphButton, ResponsiveText, Container } from '../../../elemental'; | ||
import RevisionListItem from './RevisionListItem'; | ||
import deepEql from '../utils/deepEql'; | ||
import { selectRevision } from '../actions'; | ||
|
||
const RevisionItem = ({ | ||
currentItem, | ||
handleButtonClick, | ||
revisions, | ||
router, | ||
selectedRevision, | ||
selectRevision, | ||
}) => { | ||
const renderDifferences = data => { | ||
const differences = []; | ||
const rjv = json => ( | ||
<ReactJson | ||
src={json} | ||
displayObjectSize={false} | ||
displayDataTypes={false} | ||
/> | ||
); | ||
|
||
const recursiveSearch = (currentItem, revision) => { | ||
for (const k in currentItem) { | ||
if (k === 'updatedBy' || k === 'updatedAt') continue; | ||
// when we're comparing relationship fields, ignore _id property since that gets updated after every save | ||
if (!deepEql(currentItem[k], revision[k], '_id')) { | ||
if (Array.isArray(currentItem[k])) { | ||
differences.push( | ||
<tr key={k}> | ||
<td>{k}</td> | ||
<td>{rjv(currentItem[k])}</td> | ||
<td>{rjv(revision[k])}</td> | ||
</tr> | ||
); | ||
continue; | ||
} | ||
if (Object.prototype.toString.call(revision[k]) === '[object Object]') { | ||
recursiveSearch(currentItem[k], revision[k]); | ||
continue; | ||
} | ||
differences.push( | ||
<tr key={k}> | ||
<td>{k}</td> | ||
<td>{currentItem[k]}</td> | ||
<td>{revision[k]}</td> | ||
</tr> | ||
); | ||
} | ||
} | ||
}; | ||
|
||
recursiveSearch(currentItem, data); | ||
|
||
return differences; | ||
}; | ||
|
||
const applyButton = ( | ||
<GlyphButton color="success" onClick={handleButtonClick}> | ||
<ResponsiveText hiddenXS={`Apply`} visibleXS="Apply" /> | ||
</GlyphButton> | ||
); | ||
|
||
const cancelButton = ( | ||
<GlyphButton color="danger" onClick={() => selectRevision({})}> | ||
<ResponsiveText hiddenXS={`Cancel`} visibleXS="Cancel" /> | ||
</GlyphButton> | ||
); | ||
|
||
return ( | ||
<div style={style}> | ||
{revisions.map(revision => { | ||
const active = selectedRevision._id === revision._id; | ||
const { first, last } = revision.user.name; | ||
return ( | ||
<div key={revision._id}> | ||
<RevisionListItem active={active} noedit onClick={() => selectRevision(revision)}> | ||
{moment(revision.time).format('YYYY-MM-DD hh:mm:ssa')} by {`${first} ${last}`} | ||
</RevisionListItem> | ||
{active | ||
? <div className="RevisionsItem__table--container"> | ||
<table className="RevisionsItem__table"> | ||
<tr> | ||
<th>Fields</th> | ||
<th>Current</th> | ||
<th>Rollback</th> | ||
</tr> | ||
{renderDifferences(revision.data)} | ||
</table> | ||
<div> | ||
<Container> | ||
{applyButton} {cancelButton} | ||
</Container> | ||
</div> | ||
</div> | ||
: null | ||
} | ||
</div> | ||
); | ||
} | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
const style = { | ||
textAlign: 'center', | ||
}; | ||
|
||
const mapStateToProps = state => ({ | ||
currentItem: state.revisions.currentItem, | ||
revisions: state.revisions.revisions, | ||
selectedRevision: state.revisions.selectedRevision, | ||
}); | ||
|
||
export default connect(mapStateToProps, { | ||
selectRevision, | ||
})(RevisionItem); |
98 changes: 98 additions & 0 deletions
98
admin/client/App/screens/Revision/components/RevisionListItem.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import React, { PropTypes } from 'react'; | ||
import { css, StyleSheet } from 'aphrodite/no-important'; | ||
|
||
import theme from '../../../../theme'; | ||
import { fade } from '../../../../utils/color'; | ||
|
||
/* eslint quote-props: ["error", "as-needed"] */ | ||
|
||
const RevisionListItem = ({ | ||
className, | ||
component: Component, | ||
cropText, | ||
multiline, | ||
noedit, // NOTE not used, just removed from props | ||
type, | ||
active, | ||
...props | ||
}) => { | ||
props.className = css( | ||
classes.noedit, | ||
cropText ? classes.cropText : null, | ||
multiline ? classes.multiline : null, | ||
active ? classes.anchor : null, | ||
className | ||
); | ||
|
||
return <Component {...props} />; | ||
}; | ||
|
||
RevisionListItem.propTypes = { | ||
component: PropTypes.oneOfType([ | ||
PropTypes.string, | ||
PropTypes.func, | ||
]), | ||
cropText: PropTypes.bool, | ||
}; | ||
RevisionListItem.defaultProps = { | ||
component: 'span', | ||
}; | ||
|
||
const anchorHoverAndFocusStyles = { | ||
backgroundColor: fade(theme.color.link, 10), | ||
borderColor: fade(theme.color.link, 10), | ||
color: theme.color.link, | ||
outline: 'none', | ||
textDecoration: 'underline', | ||
}; | ||
|
||
const classes = StyleSheet.create({ | ||
noedit: { | ||
appearance: 'none', | ||
backgroundColor: theme.input.background.noedit, | ||
backgroundImage: 'none', | ||
borderColor: theme.input.border.color.noedit, | ||
borderRadius: theme.input.border.radius, | ||
borderStyle: 'solid', | ||
borderWidth: theme.input.border.width, | ||
color: theme.color.gray80, | ||
cursor: 'pointer', | ||
display: 'inline-block', | ||
height: theme.input.height, | ||
lineHeight: theme.input.lineHeight, | ||
margin: '.1em 0', | ||
padding: `0 ${theme.input.paddingHorizontal}`, | ||
transition: 'border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s', | ||
verticalAlign: 'middle', | ||
width: '100%', | ||
|
||
// prevent empty inputs from collapsing by adding content | ||
':empty:before': { | ||
color: theme.color.gray40, | ||
content: '"(no value)"', | ||
}, | ||
}, | ||
|
||
multiline: { | ||
display: 'block', | ||
height: 'auto', | ||
lineHeight: '1.4', | ||
paddingBottom: '0.6em', | ||
paddingTop: '0.6em', | ||
}, | ||
|
||
// indicate clickability when using an anchor | ||
anchor: { | ||
backgroundColor: fade(theme.color.link, 10), | ||
borderColor: fade(theme.color.link, 10), | ||
color: theme.color.link, | ||
marginRight: 5, | ||
minWidth: 0, | ||
textDecoration: 'none', | ||
|
||
':hover': anchorHoverAndFocusStyles, | ||
':focus': anchorHoverAndFocusStyles, | ||
}, | ||
}); | ||
|
||
export default RevisionListItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const LOAD_REVISIONS = 'app/Revision/LOAD_REVISIONS'; | ||
export const DATA_LOADING_SUCCESS = 'app/Revision/DATA_LOADING_SUCCESS'; | ||
export const DATA_LOADING_ERROR = 'app/Revision/DATA_LOADING_ERROR'; | ||
export const SELECT_REVISION = 'app/Revision/SELECT_REVISION'; |
Oops, something went wrong.