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

Feat: Revert Entity Revision(s) #887

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d7dedc1
feat: view author entity at previous revisions
tr1ten Oct 4, 2022
13143bd
feat: allow single author entity revert revsions
tr1ten Oct 4, 2022
3313367
allow viewing merge entity revision as well
tr1ten Oct 7, 2022
03b0579
recursively undo/redo the revisions
tr1ten Oct 15, 2022
c780895
display action buttons on single entity revisions
tr1ten Oct 15, 2022
88022a3
properly revert merge revisions
tr1ten Oct 16, 2022
f7c107d
hide action buttons from unauthenticated users
tr1ten Oct 16, 2022
d5d1c50
do all sql operations inside bookshelf transaction
tr1ten Oct 16, 2022
d9ce54a
prevent redirects from being cached by the browser
tr1ten Oct 17, 2022
8362e1c
fix redo merge
tr1ten Oct 18, 2022
803b02b
repalace old reverting method with new one
tr1ten Nov 4, 2022
e18aedd
show confirmation modal for reverting revision
tr1ten Nov 21, 2022
7fbf1ed
don't allow reverting to merge revisions
tr1ten Nov 21, 2022
ec945c5
support all entities for revert revision
tr1ten Nov 21, 2022
5fb64bf
don't allow reverting to deleted revision
tr1ten Nov 25, 2022
77dc572
test(revert-revision) revert from simple revision
tr1ten Nov 27, 2022
f2dd6ec
test(revert-revision): single entity revisions
tr1ten Nov 28, 2022
abc935b
Merge branch 'master' into feat/revert-revision
tr1ten Nov 29, 2022
4c78c6c
fix: handle null reviews properly
tr1ten Nov 29, 2022
6768845
add revision router for other entities as well
tr1ten Nov 29, 2022
15abe0d
test(revert): for multiple related entities
tr1ten Dec 1, 2022
37ad98a
add series type for revision revert
tr1ten Dec 30, 2022
2a09e92
test: add dynamic tests for simple revision
tr1ten Dec 30, 2022
7173387
test: fix dynamic tests
tr1ten Dec 30, 2022
cc49244
added dynamic tests for merge revisions
tr1ten Dec 30, 2022
2b3bcbd
Fix: test util to create publisher entity
tr1ten Dec 30, 2022
c959c35
test: improve dynamic tests performance
tr1ten Dec 30, 2022
07e235a
fix: only create entity when needed
tr1ten Dec 30, 2022
4541142
Merge branch 'master' into feat/revert-revision
MonkeyDo Feb 21, 2023
585f020
Merge branch 'master' into feat/revert-revision
MonkeyDo May 18, 2023
77028b9
Merge branch 'master' into pr/887
MonkeyDo Oct 10, 2023
6352973
chore: fix double import statements
MonkeyDo Oct 10, 2023
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"cross-env": "^7.0.3",
"date-fns": "^2.15.0",
"debug": "^4.3.2",
"deep-diff": "^1.0.2",
"express": "^4.18.2",
"express-session": "^1.17.1",
"express-slow-down": "^1.3.1",
Expand Down
8 changes: 4 additions & 4 deletions src/client/components/pages/entities/cb-review.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ class EntityReviews extends React.Component {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
reviews: props.entityReviews.reviews,
successfullyFetched: props.entityReviews.successfullyFetched
reviews: props.entityReviews?.reviews,
successfullyFetched: props.entityReviews?.successfullyFetched
};
this.entityType = props.entityType;
this.entityBBID = props.entityBBID;
Expand All @@ -84,7 +84,7 @@ class EntityReviews extends React.Component {
const data = await request.get(`/${this.entityType}/${this.entityBBID}/reviews`);

this.setState({
reviews: data.body.reviews,
reviews: data?.body?.reviews,
successfullyFetched: data.body.successfullyFetched
});
}
Expand All @@ -99,7 +99,7 @@ class EntityReviews extends React.Component {
};
const cbEntityType = mapEntityType[this.entityType];
const entityLink = `https://critiquebrainz.org/${cbEntityType}/${this.entityBBID}`;
if (this.state.reviews.reviews?.length && !_.isEmpty(this.state.reviews)) {
if (this.state.reviews?.reviews?.length && !_.isEmpty(this.state.reviews)) {
const {reviews: reviewsData} = this.state.reviews;
reviewContent = (
<React.Fragment>
Expand Down
25 changes: 23 additions & 2 deletions src/client/components/pages/entity-revisions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import PagerElement from './parts/pager';
import PropTypes from 'prop-types';
import React from 'react';
import RevisionsTable from './parts/revisions-table';
import {get} from 'lodash';
import request from 'superagent';


/**
Expand All @@ -43,6 +45,7 @@ class EntityRevisions extends React.Component {
// React does not autobind non-React class methods
this.renderHeader = this.renderHeader.bind(this);
this.searchResultsCallback = this.searchResultsCallback.bind(this);
this.onChangeMasterRevisionId = this.onChangeMasterRevisionId.bind(this);
this.paginationUrl = './revisions/revisions';
}

Expand Down Expand Up @@ -71,6 +74,16 @@ class EntityRevisions extends React.Component {
);
}

async onChangeMasterRevisionId(newMasterRevisionId) {
if (newMasterRevisionId === get(this.props.entity, 'revisionId', null)) { return; }
try {
await request.post('master').send({revisionId: newMasterRevisionId});
window.location.reload();
}
catch (err) {
// error handling
}
}

/**
* Renders the EntityRevisions page, which is a list of all the revisions
Expand All @@ -79,10 +92,14 @@ class EntityRevisions extends React.Component {
* @returns {ReactElement} a HTML document which displays the Revision page
*/
render() {
const revisionId = get(this.props.entity, 'revisionId', null);
return (
<div id="pageWithPagination">
<RevisionsTable
handleMasterChange={this.onChangeMasterRevisionId}
masterRevisionId={revisionId}
results={this.state.results}
showActions={this.props.showActions && this.props.user}
showEntities={this.props.showEntities}
showRevisionEditor={this.props.showRevisionEditor}
showRevisionNote={this.props.showRevisionNote}
Expand Down Expand Up @@ -110,17 +127,21 @@ EntityRevisions.propTypes = {
from: PropTypes.number,
nextEnabled: PropTypes.bool.isRequired,
revisions: PropTypes.array.isRequired,
showActions: PropTypes.bool,
showEntities: PropTypes.bool,
showRevisionEditor: PropTypes.bool,
showRevisionNote: PropTypes.bool,
size: PropTypes.number
size: PropTypes.number,
user: PropTypes.object
};
EntityRevisions.defaultProps = {
from: 0,
showActions: false,
showEntities: false,
showRevisionEditor: false,
showRevisionNote: false,
size: 20
size: 20,
user: null
};

export default EntityRevisions;
29 changes: 29 additions & 0 deletions src/client/components/pages/parts/confirmation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Button, Modal} from 'react-bootstrap';
import React from 'react';


type Props = {
title: string,
message: string,
show: boolean,
onConfirm: () => void,
onCancel: () => void
};
function ConfirmationModal(props:Props) {
return (
<Modal show={props.show} onHide={props.onCancel}>
<Modal.Header>
<Modal.Title>{props.title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{props.message}</p>
</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={props.onCancel}>Cancel</Button>
<Button onClick={props.onConfirm} >Confirm</Button>
</Modal.Footer>
</Modal>
);
}

export default ConfirmationModal;
76 changes: 73 additions & 3 deletions src/client/components/pages/parts/revisions-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,43 @@

import * as bootstrap from 'react-bootstrap';
import * as utilsHelper from '../../../helpers/utils';
import {faCodeBranch, faEye, faUndo} from '@fortawesome/free-solid-svg-icons';
import {genEntityIconHTMLElement, getEntityLabel, getEntityUrl} from '../../../helpers/entity';
import ConfirmationModal from './confirmation-modal';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import React from 'react';
import {faCodeBranch} from '@fortawesome/free-solid-svg-icons';


const {Table} = bootstrap;
const {Table, OverlayTrigger, Tooltip, Badge} = bootstrap;
const {formatDate, stringToHTMLWithLinks} = utilsHelper;

function RevisionsTable(props) {
const {results, showEntities, showRevisionNote, showRevisionEditor, tableHeading} = props;
const {results, showEntities, showActions, showRevisionNote, showRevisionEditor, tableHeading, masterRevisionId, handleMasterChange} = props;
const [show, setShow] = React.useState(false);
const [revisionId, setRevisionId] = React.useState(null);
const showConfirmModal = React.useCallback((rid) => {
setRevisionId(rid);
setShow(true);
}, []);
const hideConfirmModal = React.useCallback(() => setShow(false), []);
const onConfirm = React.useCallback(() => {
handleMasterChange(revisionId);
hideConfirmModal();
}, [revisionId]);
function makeClickHandler(rid) {
return () => showConfirmModal(rid);
}
return (
<div>
<ConfirmationModal
message={`Are you sure you want to revert all the changes from #${revisionId} revision?`}
show={show}
title="Revert Revision"
onCancel={hideConfirmModal}
onConfirm={onConfirm}
/>

<div>
<h1 className="text-center">{tableHeading}</h1>
</div>
Expand Down Expand Up @@ -59,6 +82,7 @@ function RevisionsTable(props) {
<th width="16%">Note</th> : null
}
<th width="16%">Date</th>
{showActions && <th className="col-md-2">Actions</th>}
</tr>
</thead>

Expand All @@ -83,6 +107,10 @@ function RevisionsTable(props) {
/>
</span>
}
{showActions && revision.revisionId === masterRevisionId &&
<Badge className="ml-2 bg-success text-white">
Active
</Badge>}
</a>
</td>
{
Expand Down Expand Up @@ -127,6 +155,42 @@ function RevisionsTable(props) {
</td> : null
}
<td>{formatDate(new Date(revision.createdAt), true)}</td>
{showActions &&
<td>
<div className="d-flex align-items-center">
<OverlayTrigger
delay={50}
overlay={
<Tooltip>
View entity at this revision
</Tooltip>}
placement="right"
>
<a href={`revision/${revision.revisionId}`}>
<FontAwesomeIcon className="mr-2 text-secondary" icon={faEye}/>
</a>
</OverlayTrigger>
<OverlayTrigger
delay={50}
overlay={
<Tooltip>
Revert entity to this revision
</Tooltip>}
placement="right"
>
<FontAwesomeIcon
className={`ml-2 cursor-pointer
${revision.revisionId === masterRevisionId || revision.isMerge ||
!revision.dataId ? 'text-muted' : 'text-danger'}`}
icon={faUndo}
onClick={revision.revisionId === masterRevisionId || revision.isMerge ||
!revision.dataId ? null :
makeClickHandler(revision.revisionId)}
/>
</OverlayTrigger>
</div>
</td>
}
</tr>
))
}
Expand All @@ -144,13 +208,19 @@ function RevisionsTable(props) {
}

RevisionsTable.propTypes = {
handleMasterChange: PropTypes.func,
masterRevisionId: PropTypes.number,
results: PropTypes.array.isRequired,
showActions: PropTypes.bool,
showEntities: PropTypes.bool,
showRevisionEditor: PropTypes.bool,
showRevisionNote: PropTypes.bool,
tableHeading: PropTypes.node
};
RevisionsTable.defaultProps = {
handleMasterChange: null,
masterRevisionId: null,
showActions: false,
showEntities: false,
showRevisionEditor: false,
showRevisionNote: false,
Expand Down
4 changes: 4 additions & 0 deletions src/client/stylesheets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,10 @@ div[class~=collapsing]+div[class=card-header] .accordion-arrow {
max-width: 200px;
}

.cursor-pointer{
cursor: pointer;
}

.search-results-heading{
color: #754e37;
}
Expand Down
53 changes: 52 additions & 1 deletion src/server/helpers/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {getWikipediaExtract, selectWikipediaPage} from './wikimedia';
import _ from 'lodash';
import {getAcceptedLanguageCodes} from './i18n';
import {getReviewsFromCB} from './critiquebrainz';
import {recursivelyGetMergedEntitiesBBIDs} from './revisions';
import {getWikidataId} from '../../common/helpers/wikimedia';
import log from 'log';

Expand Down Expand Up @@ -271,7 +272,9 @@ export async function redirectedBbid(req: $Request, res: $Response, next: NextFu
const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, bbid);
if (redirectBbid !== bbid) {
// res.location(`${req.baseUrl}/${redirectBbid}`);
return res.redirect(301, `${req.baseUrl}${req.path.replace(bbid, redirectBbid)}`);
// Prevent redirecting to the same page even after revert
// For more info on Cache headers see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
return res.set('Cache-control', 'no-cache').redirect(301, `${req.baseUrl}${req.path.replace(bbid, redirectBbid)}`);
}
}
catch (err) {
Expand Down Expand Up @@ -451,3 +454,51 @@ export function validateCollaboratorIdsForCollectionRemove(req, res, next) {

return next();
}

export function decodeUrlQueryParams(req:$Request, res:$Response, next:NextFunction) {
req.query = _.mapValues(req.query, decodeURIComponent);
return next();
}

export function makeRevisionLoader(modelName: string, additionalRels: Array<string>, errMessage: string) {
const relations = [
'aliasSet.aliases.language',
'annotation.lastRevision',
'defaultAlias',
'disambiguation',
'identifierSet.identifiers.type',
'relationshipSet.relationships.type',
'revision.revision'
].concat(additionalRels);
return async (req: $Request, res: $Response, next: NextFunction, revisionId: number) => {
const {orm}: any = req.app.locals;
const mainEntity:any = res.locals.entity;
const {bbid} = mainEntity;
const otherMergedBBIDs = await recursivelyGetMergedEntitiesBBIDs(orm, [bbid]);
try {
const Model = commonUtils.getEntityModelByType(orm, modelName);
const RevisionModel = orm[`${modelName}Revision`];
// check if it is valid revision or not
await new RevisionModel()
.query((qb) => {
qb.distinct(`${RevisionModel.prototype.tableName}.id`, 'revision.created_at');
qb.whereIn('bbid', [bbid, ...otherMergedBBIDs]);
qb.join('bookbrainz.revision', `${RevisionModel.prototype.tableName}.id`, '=', 'bookbrainz.revision.id');
qb.where('bookbrainz.revision.id', '=', revisionId);
qb.orderBy('revision.created_at', 'DESC');
}).fetch({debug: true, require: true});
const entity = await Model.forge({revisionId}).fetch({
require: true,
withRelated: relations
});
if (!entity.dataId) {
entity.deleted = true;
}
res.locals.entity = entity.toJSON();
return next();
}
catch (err) {
return next(new error.NotFoundError(errMessage, req));
}
};
}
Loading
Loading