Skip to content
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

[WIP] History UI #4398

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions admin/client/App/screens/Item/actions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import assign from 'object-assign';

import {
SELECT_ITEM,
LOAD_DATA,
Expand Down Expand Up @@ -77,6 +79,32 @@ export function loadRelationshipItemData ({ columns, refList, relationship, rela
};
}

export function loadItemRevision ({ revId }) {
return (dispatch, getState) => {
const currentItemID = getState().item.id;
dispatch({
type: LOAD_DATA,
});
const state = getState();
const list = state.lists.currentList;

list.loadItemRevision(state.item.id, revId, (err, revData) => {
if (getState().item.id !== currentItemID) return;

if (err || !revData) {
dispatch(dataLoadingError(err));
} else {
const data = assign({},
getState().item.data,
revData.data,
{ rev: revData.revision }
);

dispatch(dataLoaded(data));
}
});
}
}

/**
* Called when data of the current item is loaded
Expand Down
50 changes: 49 additions & 1 deletion admin/client/App/screens/Item/components/EditFormHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { connect } from 'react-redux';
import Toolbar from './Toolbar';
import ToolbarSection from './Toolbar/ToolbarSection';
import EditFormHeaderSearch from './EditFormHeaderSearch';
import HistoryPopout from './HistoryPopout';
import { Link } from 'react-router';

import Drilldown from './Drilldown';
import Popout from '../../../shared/Popout';
import PopoutList from '../../../shared/Popout/PopoutList';
import { GlyphButton, ResponsiveText } from '../../../elemental';

import { loadItemRevision } from '../actions'

export const EditFormHeader = React.createClass({
displayName: 'EditFormHeader',
propTypes: {
Expand All @@ -20,6 +25,8 @@ export const EditFormHeader = React.createClass({
getInitialState () {
return {
searchString: '',
isHistoryOpen: false,
rev: null
};
},
toggleCreate (visible) {
Expand Down Expand Up @@ -124,12 +131,53 @@ export const EditFormHeader = React.createClass({
);
},
renderInfo () {
const buttons = [];

if (this.props.list.history) {
buttons.push(this.renderHistoryButton());
buttons.push(this.renderHistoryPopout());
buttons.push(" ");
}

buttons.push(this.renderCreateButton())

return (
<ToolbarSection right>
{this.renderCreateButton()}
{buttons}
</ToolbarSection>
);
},
toggleHistory (value) {
this.setState({
isHistoryOpen: value
});
},
applyRevision (rev) {
this.setState({ rev });
this.props.dispatch(loadItemRevision({ revId: rev._id }));
},
renderHistoryButton () {
return (
<GlyphButton id="itemHistoryButton" color="default" glyph="history" position="left" onClick={() => this.toggleHistory(true)}>
<ResponsiveText hiddenXS="History" visibleXS="History" />
</GlyphButton>
);
},
renderHistoryPopout() {
if (!this.props.list.history) {
return;
}

return (
<HistoryPopout
isOpen={this.state.isHistoryOpen}
onCancel={() => this.toggleHistory(false)}
onApply={this.applyRevision}
relativeToID="itemHistoryButton"
rev={this.state.rev}
{...this.props} />
);
},
renderCreateButton () {
const { nocreate, autocreate, singular } = this.props.list;

Expand Down
101 changes: 101 additions & 0 deletions admin/client/App/screens/Item/components/HistoryPopout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import { findDOMNode } from 'react-dom';
import { connect } from 'react-redux';
import moment from 'moment';

import Popout from '../../../shared/Popout';
import PopoutList from '../../../shared/Popout/PopoutList';

export default React.createClass({
displayName: 'HistoryPopout',
propTypes: {
data: React.PropTypes.object,
list: React.PropTypes.object
},
getInitialState () {
return {
selectedRev: this.props.rev
};
},
setSelectedRevision (rev) {
this.setState({
selectedRev: rev
});
},
historyColumns () {
const { revisions } = this.props.data.history;
const { rev } = this.props.data;
let { selectedRev } = this.state;

if (revisions == null || revisions.length === 0) {
return <span>This item has no revisions.</span>;
}

if (selectedRev == null) {
if (rev != null) {
selectedRev = rev;
}
}

return revisions.map((r, i) => {
let selected = selectedRev == null
? i === 0
: selectedRev._id === r._id;

const name = `${r.u.name.first} ${r.u.name.last}`.trim();
const label = `${moment(r.t).format('LLL')} (${name})`;

return (<PopoutList.Item
key={'rev_' + i}
icon={selected ? 'check' : ''}
iconHover={selected ? '' : 'check'}
isSelected={selected}
label={label}
onClick={() => { this.setSelectedRevision(r); }} />
);
})
},
renderFooter () {
const { revisions } = this.props.data.history;

if (revisions == null || revisions.length === 0) {
return;
}

if (this.state.selectedRev) {
return (
<Popout.Footer
primaryButtonAction={() => this.props.onApply(this.state.selectedRev)}
primaryButtonLabel="Apply"
secondaryButtonAction={this.onCancel}
secondaryButtonLabel="Cancel" />
);
} else {
return (
<Popout.Footer
secondaryButtonAction={this.onCancel}
secondaryButtonLabel="Cancel" />
);
}
},
onCancel () {
this.setState(this.getInitialState());

this.props.onCancel();
},
render () {
if (!this.props.list.history) {
return;
}

return (
<Popout {...this.props} onCancel={this.onCancel}>
<Popout.Header title="Revisions" />
<Popout.Body scrollable>
{this.historyColumns()}
</Popout.Body>
{this.renderFooter()}
</Popout>
);
},
});
5 changes: 5 additions & 0 deletions admin/client/App/screens/Item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ var ItemView = React.createClass({
createIsOpen: visible,
});
},
toggleHistory () {
this.setState({
historyIsOpen: true
})
},
// Render this items relationships
renderRelationships () {
const { relationships } = this.props.currentList;
Expand Down
1 change: 1 addition & 0 deletions admin/client/App/screens/Item/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function item (state = initialState, action) {
case LOAD_DATA:
return assign({}, state, {
loading: true,
ready: false,
});
case DATA_LOADING_SUCCESS:
return assign({}, state, {
Expand Down
23 changes: 23 additions & 0 deletions admin/client/utils/List.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,29 @@ List.prototype.loadItem = function (itemId, options, callback) {
});
};

/**
* Load a specific item's historical revision via the API
*
* @param {String} itemId The id of the item we want to load
* @param {String} revId The id of the revision to be loaded
* @param {Function} callback
*/
List.prototype.loadItemRevision = function (itemId, revId, callback) {
let url = Keystone.adminPath + '/api/' + this.path + '/' + itemId + '/rev/' + revId;
xhr({
url: url,
responseType: 'json',
}, (err, resp, data) => {
if (err) return callback(err);
// Pass the data as result or error, depending on the statusCode
if (resp.statusCode === 200) {
callback(null, data);
} else {
callback(data);
}
});
};

/**
* Load all items of a list, optionally passing objects to build a query string
* for sorting or searching
Expand Down
24 changes: 24 additions & 0 deletions admin/server/api/item/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@ module.exports = function (req, res) {

var tasks = [];
var drilldown;
var history;

if (req.list.get('history')) {
tasks.push(function (cb) {
req.list.HistoryModel.find({
i: item._id
}, {
_id: 1,
t: 1,
o: 1,
u: 1,
c: 1
})
.sort('-t')
.populate('u', 'name')
.exec(function (err, result) {
if (err) return cb(err);

history = { revisions: result };
cb();
});
});
}

/* Drilldown (optional, provided if ?drilldown=true in querystring) */
if (req.query.drilldown === 'true' && req.list.get('drilldown')) {
Expand Down Expand Up @@ -112,6 +135,7 @@ module.exports = function (req, res) {
}
res.json(_.assign(req.list.getData(item, fields), {
drilldown: drilldown,
history: history
}));
});
});
Expand Down
24 changes: 24 additions & 0 deletions admin/server/api/item/revision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = function (req, res) {
var keystone = req.keystone;

if (!req.list.get('history')) {
return res.status(400).json({ err: 'history not enabled for list', id: req.params.list });
}

req.list.HistoryModel
.findById(req.params.revisionId)
.populate('u', 'name')
.exec(function (err, result) {
if (err) return res.status(500).json({ err: 'database error', detail: err });
if (!result) return res.status(404).json({ err: 'not found', id: req.params.id });

var data = req.list.getData(new req.list.model(result.d))
data.id = req.params.id
delete result.d

res.json({
revision: result,
data: data
});
});
}
1 change: 1 addition & 0 deletions admin/server/app/createDynamicRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ module.exports = function createDynamicRouter (keystone) {
router.post('/api/:list/delete', initList, require('../api/list/delete'));
// items
router.get('/api/:list/:id', initList, require('../api/item/get'));
router.get('/api/:list/:id/rev/:revisionId', initList, require('../api/item/revision'))
router.post('/api/:list/:id', initList, require('../api/item/update'));
router.post('/api/:list/:id/delete', initList, require('../api/list/delete'));
router.post('/api/:list/:id/sortOrder/:sortOrder/:newOrder', initList, require('../api/item/sortOrder'));
Expand Down
1 change: 1 addition & 0 deletions lib/list/getOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function getOptions () {
defaultSort: this.options.defaultSort,
fields: {},
hidden: this.options.hidden,
history: this.options.history,
initialFields: _.map(this.initialFields, 'path'),
key: this.key,
label: this.label,
Expand Down