Skip to content

Commit

Permalink
administration: compare record revisions
Browse files Browse the repository at this point in the history
  • Loading branch information
zzacharo committed Jan 18, 2025
1 parent 50ec8eb commit 48a54e6
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 0 deletions.
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 @@ export class RecordResourceActions extends Component {
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 @@ export class RecordResourceActions extends Component {
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 @@ const restoreRecord = async (record) => {
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;
}
}
}

0 comments on commit 48a54e6

Please sign in to comment.