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

administration: compare record revisions #2962

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions invenio_app_rdm/administration/records/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class RecordAdminListView(AdminResourceListView):
"payload_schema": None,
"order": 1,
},
"compare": {
"text": _("Diff"),
"payload_schema": None,
"order": 1,
},
}

search_config_name = "RDM_SEARCH"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* // This file is part of Invenio-App-Rdm
* // Copyright (C) 2023 CERN.
* //
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
* // under the terms of the MIT License; see LICENSE file for more details.
*/

import React, { Component } from "react";
import PropTypes from "prop-types";
import { NotificationContext } from "@js/invenio_administration";
import { withCancel, ErrorMessage } from "react-invenio-forms";

Check warning on line 12 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

'withCancel' is defined but never used

Check warning on line 12 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

'withCancel' is defined but never used
import { Button, Modal, Dropdown, Grid } from "semantic-ui-react";
import { i18next } from "@translations/invenio_app_rdm/i18next";
import { Differ, Viewer } from "json-diff-kit";

export class CompareRevisions extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error: undefined,
currentDiff: undefined,
revisions: {},
};
this.differ = new Differ({
detectCircular: true,
maxDepth: null,
showModifications: true,
arrayDiffMethod: "lcs",
ignoreCase: false,
ignoreCaseForKey: false,
recursiveEqual: true,
});
}

componentWillUnmount() {

Check warning on line 37 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

componentWillUnmount should be placed after componentDidMount

Check warning on line 37 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

componentWillUnmount should be placed after componentDidMount
this.cancellableAction && this.cancellableAction.cancel();
}

async componentDidMount() {
const { resource } = this.props;
this.setState({ loading: true });

Check failure on line 43 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Do not use setState in componentDidMount

Check failure on line 43 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Do not use setState in componentDidMount
const response = await fetch(
`https://127.0.0.1:5000/api/records/${resource.id}/revisions`
);
if (!response.ok) {
this.setState({ error: response.statusText, loading: false });

Check failure on line 48 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Do not use setState in componentDidMount

Check failure on line 48 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Do not use setState in componentDidMount
return;
}

const revisions = await response.json();
this.setState(

Check failure on line 53 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Do not use setState in componentDidMount

Check failure on line 53 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Do not use setState in componentDidMount
{
revisions: revisions.reduce((acc, item, index) => {
acc[index] = item; // Use version_id as the key
return acc;
}, {}),
},
() => {
this.setState({ loading: false });
}
);
}

static contextType = NotificationContext;

computeDiff = (before, after) => {
const diff = this.differ.diff(before, after);
this.setState({ currentDiff: diff });
};

handleModalClose = () => {
const { actionCancelCallback } = this.props;
actionCancelCallback();
};

render() {
const { error, loading, currentDiff } = this.state;

const viewerProps = {
indent: 4,
lineNumbers: true,
highlightInlineDiff: true,
inlineDiffOptions: {
mode: "word",
wordSeparator: " ",
},
hideUnchangedLines: true,
syntaxHighlight: false,
virtual: false,
};

return (
<>
{error && (
<ErrorMessage
header={i18next.t("Unable to fetch revisions.")}
content={error}
icon="exclamation"
className="text-align-left"
negative
/>
)}
<Modal.Content>
{loading && <p>Loading...</p>}
<Grid>
<Grid.Row>
<Dropdown
loading={loading}
placeholder="Select revision"
fluid
selection
value={this.state.selectedRevision}

Check warning on line 114 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Must use destructuring state assignment

Check warning on line 114 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Must use destructuring state assignment
onChange={(e, { value }) => {
this.computeDiff(
this.state.revisions[value],

Check warning on line 117 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Must use destructuring state assignment

Check warning on line 117 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Must use destructuring state assignment
this.state.revisions[0]

Check warning on line 118 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Must use destructuring state assignment

Check warning on line 118 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Must use destructuring state assignment
);
this.setState({ selectedRevision: value });
}}
options={Object.values(this.state.revisions).map((rev, index) => ({

Check warning on line 122 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Must use destructuring state assignment

Check warning on line 122 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Must use destructuring state assignment
key: index,
text: `Revision ${index}`,
value: index,
}))}
/>
</Grid.Row>
<Grid.Row>
{!loading && currentDiff && (
<Viewer diff={currentDiff} {...viewerProps} />
)}
</Grid.Row>
</Grid>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleModalClose} floated="left">
Close
</Button>
</Modal.Actions>
</>
);
}
}

CompareRevisions.propTypes = {
resource: PropTypes.object.isRequired,
actionCancelCallback: PropTypes.func.isRequired,
actionSuccessCallback: PropTypes.func.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import TombstoneForm from "./TombstoneForm";
import {CompareRevisions} from "./CompareRevisions";

Check failure on line 10 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Replace `CompareRevisions` with `·CompareRevisions·`

Check failure on line 10 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Replace `CompareRevisions` with `·CompareRevisions·`
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Button, Modal, Icon } from "semantic-ui-react";
Expand All @@ -28,6 +29,19 @@
onModalTriggerClick = (e, { payloadSchema, dataName, dataActionKey }) => {
const { resource } = this.props;

if (dataActionKey === "compare") {
this.setState({
modalOpen: true,
modalHeader: i18next.t("Compare with previous revision"),
modalBody: (
<CompareRevisions
actionSuccessCallback={this.handleSuccess}
actionCancelCallback={this.closeModal}
resource={resource}
/>
),
});
}
if (dataActionKey === "delete") {
this.setState({
modalOpen: true,
Expand Down Expand Up @@ -81,6 +95,25 @@
return (
<>
{Object.entries(actions).map(([actionKey, actionConfig]) => {
if (actionKey === "compare" && !resource.deletion_status.is_deleted) {
icon = "file code outline";
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
icon={icon}
fluid
basic
labelPosition="left"
>
{icon && <Icon name={icon} />}
{actionConfig.text}
</Element>
);
}
if (actionKey === "delete" && !resource.deletion_status.is_deleted) {
icon = "trash alternate";
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
return await http.post(APIRoutes.restore(record));
};

const getRevisions = async (record) => {
return await http.post(APIRoutes.compare(record));
};


Check failure on line 29 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/api.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Delete `⏎`

Check failure on line 29 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/api.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Delete `⏎`
export const RecordModerationApi = {
deleteRecord: deleteRecord,
restoreRecord: restoreRecord,
getRevisions: getRevisions,
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const APIRoutesGenerators = {
delete: (record, idKeyPath = "id") => {
return `/api/records/${_get(record, idKeyPath)}/delete`;
},
compare: (record, idKeyPath = "id") => {
return `/api/records/${_get(record, idKeyPath)}/revisions`;
},

restore: (record, idKeyPath = "id") => {
return `/api/records/${_get(record, idKeyPath)}/restore`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// Invenio RDM is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

import 'json-diff-kit/dist/viewer-monokai.css';

Check failure on line 7 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/index.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

Replace `'json-diff-kit/dist/viewer-monokai.css'` with `"json-diff-kit/dist/viewer-monokai.css"`

Check failure on line 7 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/index.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

Replace `'json-diff-kit/dist/viewer-monokai.css'` with `"json-diff-kit/dist/viewer-monokai.css"`

import { initDefaultSearchComponents } from "@js/invenio_administration";
import { createSearchAppInit } from "@js/invenio_search_ui";
import { NotificationController } from "@js/invenio_administration";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,136 @@ dl.details-list {
vertical-align: sub;
}
}


// JSON diff kit copied css

.json-diff-viewer {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;

tr {
vertical-align: top;

.line-add {
background: #a5d6a7;
}

.line-remove {
background: #ef9a9a;
}

.line-modify {
background: #ffe082;
}

&:hover td {
position: relative;

&:before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.05);
content: '';
pointer-events: none;
}
}

&.message-line {
border-top: 1px solid;
border-bottom: 1px solid;
text-align: center;

td {
padding: 4px 0;
font-size: 12px;
}
}

&.expand-line {
text-align: center;

td {
padding: 4px 0;
}

&:hover td:before {
background: transparent;
}

.has-lines-before {
border-bottom: 1px solid;
}

.has-lines-after {
border-top: 1px solid;
}

button {
padding: 0;
border: none;
margin: 0 0.5em;
background: transparent;
color: #2196f3;
cursor: pointer;
font-size: 12px;
user-select: none;

&:hover {
text-decoration: underline;
}
}
}
}

td {
padding: 1px;
font-size: 0;

&.line-number {
box-sizing: content-box;
padding: 0 8px;
border-right: 1px solid;
font-family: monospace;
font-size: 14px;
text-align: right;
user-select: none;
}
}

pre {
overflow: hidden;
margin: 0;
font-size: 12px;
line-height: 16px;
white-space: pre-wrap;
word-break: break-all;

.inline-diff-add {
background: rgba(0, 0, 0, 0.08);
text-decoration: underline;
word-break: break-all;
}

.inline-diff-remove {
background: rgba(0, 0, 0, 0.08);
text-decoration: line-through;
word-break: break-all;
}
}

&-virtual pre {
overflow-x: auto;
white-space: pre;

&::-webkit-scrollbar {
display: none;
}
}
}

Loading