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 24, 2025
1 parent 02deb31 commit 30e7244
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 1 deletion.
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": _("Compare revisions"),
"payload_schema": None,
"order": 1,
},
}

search_config_name = "RDM_SEARCH"
Expand Down
3 changes: 3 additions & 0 deletions invenio_app_rdm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,9 @@ def github_link_render(record):
ADMINISTRATION_DISPLAY_VERSIONS = [("invenio-app-rdm", f"v{__version__}")]
"""Show the InvenioRDM version in the administration panel."""

ADMINISTRATION_THEME_BASE_TEMPLATE = "invenio_app_rdm/administration_page.html"
"""Administration base template."""


APP_RDM_SUBCOMMUNITIES_LABEL = "Subcommunities"
"""Label for the subcommunities in the community browse page."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* // This file is part of Invenio-App-Rdm
* // Copyright (C) 2025 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 from "react";
import PropTypes from "prop-types";
import { Grid, Dropdown, Button } from "semantic-ui-react";

export const CompareRevisionsDropdown = ({
loading,
options,
srcRevision,
targetRevision,
onSrcChange,
onTargetChange,
onCompare,
}) => {
return (
<Grid>
<Grid.Column mobile={16} computer={6} tablet={16} largeScreen={6} widescreen={6}>
<label htmlFor="source-revision">From</label>
<Dropdown
id="source-revision"
loading={loading}
placeholder="Source revision..."
fluid
selection
value={srcRevision}
onChange={(e, { value }) => onSrcChange(value)}
options={options}
scrolling
/>
</Grid.Column>
<Grid.Column mobile={16} computer={6} tablet={16} largeScreen={6} widescreen={6}>
<label htmlFor="target-revision">To</label>
<Dropdown
id="target-revision"
loading={loading}
placeholder="Target revision..."
fluid
selection
value={targetRevision}
onChange={(e, { value }) => onTargetChange(value)}
options={options}
scrolling
/>
</Grid.Column>
<Grid.Column
verticalAlign="bottom"
mobile={16}
computer={2}
tablet={16}
largeScreen={2}
widescreen={2}
>
<Button onClick={onCompare}>Compare</Button>
</Grid.Column>
</Grid>
);
};

CompareRevisionsDropdown.propTypes = {
loading: PropTypes.bool.isRequired,
options: PropTypes.array.isRequired,
srcRevision: PropTypes.object,

Check warning on line 69 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/CompareRevisionsDropdown.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

propType "srcRevision" is not required, but has no corresponding defaultProps declaration

Check warning on line 69 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/CompareRevisionsDropdown.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

propType "srcRevision" is not required, but has no corresponding defaultProps declaration
targetRevision: PropTypes.object,

Check warning on line 70 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/CompareRevisionsDropdown.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

propType "targetRevision" is not required, but has no corresponding defaultProps declaration

Check warning on line 70 in invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/CompareRevisionsDropdown.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

propType "targetRevision" is not required, but has no corresponding defaultProps declaration
onSrcChange: PropTypes.func.isRequired,
onTargetChange: PropTypes.func.isRequired,
onCompare: PropTypes.func.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export class ImpersonateUserForm extends Component {
render() {
const { error, loading } = this.state;
const { user } = this.props;
console.log({ user });
return (
<Formik
onSubmit={this.handleSubmit}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* // This file is part of Invenio-App-Rdm
* // Copyright (C) 2025 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 { RecordModerationApi } from "./api";
import { withCancel, ErrorMessage } from "react-invenio-forms";
import { Modal, Grid, Segment, Button } from "semantic-ui-react";
import { i18next } from "@translations/invenio_app_rdm/i18next";
import { Differ, Viewer } from "json-diff-kit";
import { CompareRevisionsDropdown } from "../components/CompareRevisionsDropdown";

export class CompareRevisions extends Component {
constructor(props) {
super(props);
this.differ = new Differ({
detectCircular: true,
maxDepth: null,
showModifications: true,
arrayDiffMethod: "lcs",
ignoreCase: false,
ignoreCaseForKey: false,
recursiveEqual: true,
});
this.viewerProps = {
indent: 4,
lineNumbers: true,
highlightInlineDiff: true,
inlineDiffOptions: {
mode: "word",
wordSeparator: " ",
},
hideUnchangedLines: true,
syntaxHighlight: false,
virtual: true,
};

this.state = {
loading: true,
error: undefined,
currentDiff: undefined,
allRevisions: {},
srcRevision: undefined,
targetRevision: undefined,
};
}

componentWillUnmount() {

Check warning 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)

componentWillUnmount should be placed after componentDidMount

Check warning 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)

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 59 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 59 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
try {
this.cancellableAction = withCancel(RecordModerationApi.getRevisions(resource));
const response = await this.cancellableAction.promise;
const revisions = await response.data;
console.log(revisions);

this.setState({

Check failure on line 66 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 66 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
allRevisions: revisions,
targetRevision: revisions[0],
srcRevision: revisions.length > 1 ? revisions[1] : revisions[0],
loading: false,
});
} catch (error) {
if (error === "UNMOUNTED") return;
this.setState({ error: error, loading: false });

Check failure on line 74 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 74 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
console.error(error);
}
}

computeDiff = () => {
const { srcRevision, targetRevision } = this.state;
if (srcRevision && targetRevision) {
const diff = this.differ.diff(srcRevision, targetRevision);
this.setState({ currentDiff: diff });
}
};

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

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

const options = Object.values(allRevisions).map((rev) => ({
key: rev.updated,
text: `${rev.updated} (${rev.revision_id})`,
value: rev,
}));

return (
<>
<Modal.Content>
<CompareRevisionsDropdown
loading={loading}
options={options}
srcRevision={srcRevision}
targetRevision={targetRevision}
onSrcChange={(value) => this.setState({ srcRevision: value })}
onTargetChange={(value) => this.setState({ targetRevision: value })}
onCompare={this.computeDiff}
/>
{error && (
<Modal.Content>
<ErrorMessage
header={i18next.t("Unable to fetch revisions.")}
content={error}
icon="exclamation"
className="text-align-left"
negative
/>
</Modal.Content>
)}
<Modal.Content scrolling>
{loading && <p>Loading...</p>}
{!loading && currentDiff && (
<Grid>
<Grid.Column width={14}>
<Segment>
<Viewer diff={currentDiff} {...this.viewerProps} />
</Segment>
</Grid.Column>
</Grid>
)}
</Modal.Content>
</Modal.Content>
<Modal.Actions>
<Grid>
<Grid.Column floated="left" width={8} textAlign="left">
<Button
onClick={this.handleModalClose}
disabled={loading}
loading={loading}
aria-label={i18next.t("Cancel revision comparison")}
>
Close
</Button>
</Grid.Column>
</Grid>
</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";
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 revisions"),
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,16 @@ const restoreRecord = async (record) => {
return await http.post(APIRoutes.restore(record));
};

const getRevisions = async (record) => {
return await http.get(APIRoutes.compare(record), {
headers: {
Accept: "application/json",
},
});
};

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,5 @@ dl.details-list {
vertical-align: sub;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{# -*- coding: utf-8 -*-

This file is part of Invenio.
Copyright (C) 2015-2025 CERN.

Invenio is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
#}

{%- extends "invenio_theme/page.html" -%}

{%- block css %}
{{ super()}}
<link rel="stylesheet" href="/static/css/json-diff-kit.css">
{%- endblock css %}
5 changes: 5 additions & 0 deletions invenio_app_rdm/theme/webpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"react-invenio-forms": "^4.0.0",
"react-searchkit": "^3.0.0",
"yup": "^0.32.0",
"json-diff-kit": "^1.0.30",
},
aliases={
# Define Semantic-UI theme configuration needed by
Expand Down Expand Up @@ -87,6 +88,10 @@
"from": "../node_modules/tinymce/skins/ui/oxide/content.min.css",
"to": "../../static/dist/js/skins/ui/oxide",
},
{
"from": "../node_modules/json-diff-kit/dist/viewer.css",
"to": "../../static/css/json-diff-kit.css",
},
],
),
},
Expand Down

0 comments on commit 30e7244

Please sign in to comment.