invenio-app-rdm 13.0.0b2.dev0__py2.py3-none-any.whl → 13.0.0b2.dev2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. invenio_app_rdm/__init__.py +2 -2
  2. invenio_app_rdm/administration/records/records.py +5 -0
  3. invenio_app_rdm/config.py +4 -1
  4. invenio_app_rdm/records_ui/templates/semantic-ui/invenio_app_rdm/records/details/description.html +2 -2
  5. invenio_app_rdm/records_ui/templates/semantic-ui/invenio_app_rdm/records/macros/detail.html +26 -29
  6. invenio_app_rdm/records_ui/views/decorators.py +21 -0
  7. invenio_app_rdm/records_ui/views/deposits.py +38 -2
  8. invenio_app_rdm/records_ui/views/records.py +21 -5
  9. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/CompareRevisionsDropdown.js +83 -0
  10. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/ImpersonateUserForm.js +0 -1
  11. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/RevisionsDiffViewer.js +77 -0
  12. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/CompareRevisions.js +128 -0
  13. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/RecordResourceActions.js +43 -4
  14. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/api.js +5 -0
  15. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/routes.js +3 -0
  16. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/components/RecordsResultsListItem.js +10 -3
  17. invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/deposit/RDMDepositForm.js +1 -0
  18. invenio_app_rdm/theme/assets/semantic-ui/less/invenio_app_rdm/theme/elements/label.overrides +3 -3
  19. invenio_app_rdm/theme/assets/semantic-ui/less/invenio_app_rdm/theme/globals/site.overrides +2 -0
  20. invenio_app_rdm/theme/assets/semantic-ui/less/invenio_app_rdm/theme/views/item.overrides +4 -0
  21. invenio_app_rdm/theme/templates/semantic-ui/invenio_app_rdm/administration_page.html +15 -0
  22. invenio_app_rdm/theme/templates/semantic-ui/invenio_app_rdm/header.html +2 -1
  23. invenio_app_rdm/theme/webpack.py +5 -0
  24. invenio_app_rdm/translations/af/LC_MESSAGES/messages.mo +0 -0
  25. invenio_app_rdm/translations/ar/LC_MESSAGES/messages.mo +0 -0
  26. invenio_app_rdm/translations/bg/LC_MESSAGES/messages.mo +0 -0
  27. invenio_app_rdm/translations/ca/LC_MESSAGES/messages.mo +0 -0
  28. invenio_app_rdm/translations/cs/LC_MESSAGES/messages.mo +0 -0
  29. invenio_app_rdm/translations/da/LC_MESSAGES/messages.mo +0 -0
  30. invenio_app_rdm/translations/de/LC_MESSAGES/messages.mo +0 -0
  31. invenio_app_rdm/translations/de_AT/LC_MESSAGES/messages.mo +0 -0
  32. invenio_app_rdm/translations/de_DE/LC_MESSAGES/messages.mo +0 -0
  33. invenio_app_rdm/translations/el/LC_MESSAGES/messages.mo +0 -0
  34. invenio_app_rdm/translations/en_AT/LC_MESSAGES/messages.mo +0 -0
  35. invenio_app_rdm/translations/en_HU/LC_MESSAGES/messages.mo +0 -0
  36. invenio_app_rdm/translations/es/LC_MESSAGES/messages.mo +0 -0
  37. invenio_app_rdm/translations/es_CU/LC_MESSAGES/messages.mo +0 -0
  38. invenio_app_rdm/translations/es_MX/LC_MESSAGES/messages.mo +0 -0
  39. invenio_app_rdm/translations/et/LC_MESSAGES/messages.mo +0 -0
  40. invenio_app_rdm/translations/et_EE/LC_MESSAGES/messages.mo +0 -0
  41. invenio_app_rdm/translations/fa/LC_MESSAGES/messages.mo +0 -0
  42. invenio_app_rdm/translations/fa_IR/LC_MESSAGES/messages.mo +0 -0
  43. invenio_app_rdm/translations/fr/LC_MESSAGES/messages.mo +0 -0
  44. invenio_app_rdm/translations/fr_CI/LC_MESSAGES/messages.mo +0 -0
  45. invenio_app_rdm/translations/fr_FR/LC_MESSAGES/messages.mo +0 -0
  46. invenio_app_rdm/translations/gl/LC_MESSAGES/messages.mo +0 -0
  47. invenio_app_rdm/translations/hi_IN/LC_MESSAGES/messages.mo +0 -0
  48. invenio_app_rdm/translations/hr/LC_MESSAGES/messages.mo +0 -0
  49. invenio_app_rdm/translations/hu/LC_MESSAGES/messages.mo +0 -0
  50. invenio_app_rdm/translations/hu_HU/LC_MESSAGES/messages.mo +0 -0
  51. invenio_app_rdm/translations/it/LC_MESSAGES/messages.mo +0 -0
  52. invenio_app_rdm/translations/ja/LC_MESSAGES/messages.mo +0 -0
  53. invenio_app_rdm/translations/ka/LC_MESSAGES/messages.mo +0 -0
  54. invenio_app_rdm/translations/lt/LC_MESSAGES/messages.mo +0 -0
  55. invenio_app_rdm/translations/ne/LC_MESSAGES/messages.mo +0 -0
  56. invenio_app_rdm/translations/no/LC_MESSAGES/messages.mo +0 -0
  57. invenio_app_rdm/translations/pl/LC_MESSAGES/messages.mo +0 -0
  58. invenio_app_rdm/translations/pt/LC_MESSAGES/messages.mo +0 -0
  59. invenio_app_rdm/translations/ro/LC_MESSAGES/messages.mo +0 -0
  60. invenio_app_rdm/translations/ru/LC_MESSAGES/messages.mo +0 -0
  61. invenio_app_rdm/translations/rw/LC_MESSAGES/messages.mo +0 -0
  62. invenio_app_rdm/translations/sk/LC_MESSAGES/messages.mo +0 -0
  63. invenio_app_rdm/translations/sv/LC_MESSAGES/messages.mo +0 -0
  64. invenio_app_rdm/translations/sv_SE/LC_MESSAGES/messages.mo +0 -0
  65. invenio_app_rdm/translations/tr/LC_MESSAGES/messages.mo +0 -0
  66. invenio_app_rdm/translations/uk/LC_MESSAGES/messages.mo +0 -0
  67. invenio_app_rdm/translations/uk_UA/LC_MESSAGES/messages.mo +0 -0
  68. invenio_app_rdm/translations/zh_CN/LC_MESSAGES/messages.mo +0 -0
  69. invenio_app_rdm/translations/zh_TW/LC_MESSAGES/messages.mo +0 -0
  70. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/METADATA +38 -5
  71. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/RECORD +75 -71
  72. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/WHEEL +1 -1
  73. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/LICENSE +0 -0
  74. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/entry_points.txt +0 -0
  75. {invenio_app_rdm-13.0.0b2.dev0.dist-info → invenio_app_rdm-13.0.0b2.dev2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2019-2024 CERN.
3
+ # Copyright (C) 2019-2025 CERN.
4
4
  # Copyright (C) 2019-2022 Northwestern University.
5
5
  # Copyright (C) 2024 Graz University of Technology.
6
6
  #
@@ -17,6 +17,6 @@
17
17
  #
18
18
  # See PEP 0440 for details - https://www.python.org/dev/peps/pep-0440
19
19
 
20
- __version__ = "13.0.0b2.dev0"
20
+ __version__ = "13.0.0b2.dev2"
21
21
 
22
22
  __all__ = ("__version__",)
@@ -56,6 +56,11 @@ class RecordAdminListView(AdminResourceListView):
56
56
  "payload_schema": None,
57
57
  "order": 1,
58
58
  },
59
+ "compare": {
60
+ "text": _("Compare revisions"),
61
+ "payload_schema": None,
62
+ "order": 1,
63
+ },
59
64
  }
60
65
 
61
66
  search_config_name = "RDM_SEARCH"
invenio_app_rdm/config.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2019-2024 CERN.
3
+ # Copyright (C) 2019-2025 CERN.
4
4
  # Copyright (C) 2019-2020 Northwestern University.
5
5
  # Copyright (C) 2021-2024 Graz University of Technology.
6
6
  # Copyright (C) 2022-2024 KTH Royal Institute of Technology.
@@ -1461,6 +1461,9 @@ from invenio_app_rdm import __version__
1461
1461
  ADMINISTRATION_DISPLAY_VERSIONS = [("invenio-app-rdm", f"v{__version__}")]
1462
1462
  """Show the InvenioRDM version in the administration panel."""
1463
1463
 
1464
+ ADMINISTRATION_THEME_BASE_TEMPLATE = "invenio_app_rdm/administration_page.html"
1465
+ """Administration base template."""
1466
+
1464
1467
 
1465
1468
  APP_RDM_SUBCOMMUNITIES_LABEL = "Subcommunities"
1466
1469
  """Label for the subcommunities in the community browse page."""
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright (C) 2020 CERN.
2
+ Copyright (C) 2020-2025 CERN.
3
3
  Copyright (C) 2020 Northwestern University.
4
4
  Copyright (C) 2021 New York University.
5
5
 
@@ -15,7 +15,7 @@
15
15
  <h2 id="description-heading" class="sr-only">{{ _('Description') }}</h2>
16
16
  {# description data is being sanitized by marshmallow in the backend #}
17
17
  <div style="word-wrap: break-word;">
18
- <p>{{ description | safe }}</p>
18
+ {{ description | safe }}
19
19
  </div>
20
20
  </section>
21
21
  {% endif %}
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright (C) 2020-2024 CERN.
2
+ Copyright (C) 2020-2025 CERN.
3
3
  Copyright (C) 2024 Northwestern University.
4
4
 
5
5
  Invenio RDM Records is free software; you can redistribute it and/or modify
@@ -96,40 +96,37 @@
96
96
 
97
97
 
98
98
  {% macro _show_funding_item(item, index) %}
99
+ <dt class="ui tiny header">{{ item.funder.name if item.funder }}</dt>
99
100
  {%- if item.award -%}
100
- {%- if item.award.title_l10n -%}
101
- <dt class="ui tiny header">
102
- <span class="mr-5">
103
- {% if item.award.acronym %}
104
- {{ item.award.acronym }} –
105
- {% endif %}
106
-
107
- {{ item.award.title_l10n }}
108
- </span>
101
+ <dt class="ui tiny header">
102
+ <span class="mr-0 text-muted">
103
+ {% if item.award.acronym %}
104
+ {{ item.award.acronym }} –
105
+ {% endif %}
106
+
107
+ {%- if item.award.title_l10n -%}
108
+ {{ item.award.title_l10n }}
109
+ {%- endif -%}
110
+ </span>
109
111
 
110
- {%- if item.award.number -%}
111
- <span class="ui mini basic label ml-0 mr-5" id="number-label-{{ index }}">
112
- {{ item.award.number }}
113
- </span>
114
- {%- endif -%}
112
+ {%- if item.award.number -%}
113
+ <span class="ui mini basic label {% if not item.award.title_l10n %}ml-0{% endif %}" id="number-label-{{ index }}">
114
+ {{ item.award.number }}
115
+ </span>
116
+ {%- endif -%}
115
117
 
116
- {%- if item.award.identifiers -%}
117
- {% for identifier in item.award.identifiers if 'url' == identifier.scheme %}
118
- <a href="{{ identifier.identifier }}" target="_blank"
119
- rel="noopener noreferrer" aria-label="{{ _('Open external link') }}">
120
- <i class="external alternate icon"></i>
121
- </a>
122
- {%- endfor -%}
123
- {%- endif -%}
124
- </dt>
118
+ {%- if item.award.identifiers -%}
119
+ {% for identifier in item.award.identifiers if 'url' == identifier.scheme %}
120
+ <a href="{{ identifier.identifier }}" target="_blank"
121
+ rel="noopener noreferrer" aria-label="{{ _('Open external link') }}">
122
+ <i class="external alternate icon"></i>
123
+ </a>
124
+ {%- endfor -%}
125
125
  {%- endif -%}
126
- <dd class="text-muted">{{ item.funder.name if item.funder }}</dd>
127
- {%- else -%}
128
- <dt class="ui tiny header">{{ item.funder.name if item.funder }}</dt>
129
- {%- endif -%}
126
+ </dt>
127
+ {%- endif -%}
130
128
  {% endmacro %}
131
129
 
132
-
133
130
  {% macro show_references(references) %}
134
131
  <ul class="ui bulleted list details-list">
135
132
  {% for reference in references %}
@@ -403,3 +403,24 @@ def secret_link_or_login_required():
403
403
  return view
404
404
 
405
405
  return decorator
406
+
407
+
408
+ def no_cache_response(f):
409
+ """Add appropriate response headers to force no caching.
410
+
411
+ This decorator is used to prevent caching of the response in the browser. This is needed
412
+ in the deposit form as we initialize the form with the record metadata included in the html page
413
+ and we don't want the browser to cache this page so that the user always gets the latest version of the record.
414
+ """
415
+
416
+ @wraps(f)
417
+ def view(*args, **kwargs):
418
+ response = make_response(f(*args, **kwargs))
419
+
420
+ response.cache_control.no_cache = True
421
+ response.cache_control.no_store = True
422
+ response.cache_control.must_revalidate = True
423
+
424
+ return response
425
+
426
+ return view
@@ -23,6 +23,7 @@ from invenio_i18n.ext import current_i18n
23
23
  from invenio_rdm_records.proxies import current_rdm_records
24
24
  from invenio_rdm_records.records.api import get_files_quota
25
25
  from invenio_rdm_records.resources.serializers import UIJSONSerializer
26
+ from invenio_rdm_records.services.components.pids import _get_optional_doi_transitions
26
27
  from invenio_rdm_records.services.schemas import RDMRecordSchema
27
28
  from invenio_rdm_records.services.schemas.utils import dump_empty
28
29
  from invenio_records_resources.services.errors import PermissionDeniedError
@@ -34,6 +35,7 @@ from sqlalchemy.orm import load_only
34
35
 
35
36
  from ..utils import set_default_value
36
37
  from .decorators import (
38
+ no_cache_response,
37
39
  pass_draft,
38
40
  pass_draft_community,
39
41
  pass_draft_files,
@@ -45,7 +47,7 @@ from .filters import get_scheme_label
45
47
  #
46
48
  # Helpers
47
49
  #
48
- def get_form_pids_config():
50
+ def get_form_pids_config(record=None):
49
51
  """Prepare configuration for the pids field.
50
52
 
51
53
  Currently supporting only doi.
@@ -55,17 +57,45 @@ def get_form_pids_config():
55
57
  # FIXME: User provider.is_managed() requires tiny fix in config
56
58
  can_be_managed = True
57
59
  can_be_unmanaged = True
60
+ # We initialize the optional doi to empty to indicate that there is no restriction on the transitions
61
+ # This is valid for new uploads and when the DOI is required in an instance
62
+ optional_doi_transitions = []
58
63
  for scheme in service.config.pids_providers.keys():
59
64
  if not scheme == "doi":
60
65
  continue
66
+
61
67
  record_pid_config = current_app.config["RDM_PERSISTENT_IDENTIFIERS"]
62
68
  scheme_label = record_pid_config.get(scheme, {}).get("label", scheme)
63
69
  is_doi_required = record_pid_config.get(scheme, {}).get("required")
64
70
  default_selected = (
65
71
  record_pid_config.get(scheme, {}).get("ui", {}).get("default_selected")
66
72
  )
73
+ if record is not None and not is_doi_required:
74
+ sitename = current_app.config.get("THEME_SITENAME", "this repository")
75
+ previous_published_record = (
76
+ service.record_cls.get_latest_published_by_parent(record.parent)
77
+ )
78
+ optional_doi_transitions = _get_optional_doi_transitions(
79
+ previous_published_record
80
+ )
81
+ if optional_doi_transitions:
82
+ optional_doi_transitions["message"] = optional_doi_transitions.get(
83
+ "message"
84
+ ).format(sitename=sitename)
85
+ if set(optional_doi_transitions.get("allowed_providers", [])) - set(
86
+ ["external", "not_needed"]
87
+ ):
88
+ # In case we have locally managed provider as an allowed one, we need to
89
+ # select it by default. That is relevant for the case when the
90
+ # user creates a new version of the record and the previous version
91
+ # had a datacite DOI.
92
+ default_selected = "no"
93
+
94
+ # if the DOI is required but the default selected is not_needed then we set it to yes
95
+ # to force the user to mint a DOI
67
96
  if is_doi_required and default_selected == "not_needed":
68
97
  default_selected = "yes"
98
+
69
99
  pids_provider = {
70
100
  "scheme": scheme,
71
101
  "field_label": "Digital Object Identifier",
@@ -89,6 +119,7 @@ def get_form_pids_config():
89
119
  "unambiguously cited. Example: 10.1234/foo.bar"
90
120
  ).format(scheme_label=scheme_label),
91
121
  "default_selected": default_selected,
122
+ "optional_doi_transitions": optional_doi_transitions,
92
123
  }
93
124
  pids_providers.append(pids_provider)
94
125
 
@@ -332,6 +363,8 @@ def get_form_config(**kwargs):
332
363
  if record_quota:
333
364
  quota["maxStorage"] = record_quota["quota_size"]
334
365
 
366
+ record = kwargs.pop("record", None)
367
+
335
368
  return dict(
336
369
  vocabularies=VocabulariesOptions().dump(),
337
370
  autocomplete_names=conf.get(
@@ -339,7 +372,7 @@ def get_form_config(**kwargs):
339
372
  ),
340
373
  current_locale=str(current_i18n.locale),
341
374
  default_locale=conf.get("BABEL_DEFAULT_LOCALE", "en"),
342
- pids=get_form_pids_config(),
375
+ pids=get_form_pids_config(record=record),
343
376
  quota=quota,
344
377
  decimal_size_display=conf.get("APP_RDM_DISPLAY_DECIMAL_FILE_SIZES", True),
345
378
  links=dict(
@@ -390,6 +423,7 @@ def new_record():
390
423
  # Views
391
424
  #
392
425
  @login_required
426
+ @no_cache_response
393
427
  @pass_draft_community
394
428
  def deposit_create(community=None):
395
429
  """Create a new deposit."""
@@ -441,6 +475,7 @@ def deposit_create(community=None):
441
475
  @secret_link_or_login_required()
442
476
  @pass_draft(expand=True)
443
477
  @pass_draft_files
478
+ @no_cache_response
444
479
  def deposit_edit(pid_value, draft=None, draft_files=None, files_locked=True):
445
480
  """Edit an existing deposit."""
446
481
  # don't show draft's deposit form if the user can't edit it
@@ -490,6 +525,7 @@ def deposit_edit(pid_value, draft=None, draft_files=None, files_locked=True):
490
525
  # hide react community component
491
526
  hide_community_selection=community_use_jinja_header,
492
527
  is_doi_required=is_doi_required,
528
+ record=draft._record,
493
529
  )
494
530
 
495
531
  if is_doi_required and not record.get("pids", {}).get("doi"):
@@ -178,7 +178,7 @@ def record_detail(
178
178
  )
179
179
  except ValidationError:
180
180
  abort(404)
181
- # inject parent doi format for new drafts so we can show in preview
181
+ # inject parent doi format for new drafts so we can show in preview if parent doi is required
182
182
  if current_app.config["DATACITE_ENABLED"]:
183
183
  service = current_rdm_records.records_service
184
184
  datacite_provider = [
@@ -187,11 +187,27 @@ def record_detail(
187
187
  if p == "doi" and "datacite" in v
188
188
  ]
189
189
  if datacite_provider:
190
- datacite_provider = datacite_provider[0]
191
- parent_doi = datacite_provider.client.generate_doi(
192
- record._record.parent
190
+ should_mint_parent_doi = True
191
+ is_doi_required = (
192
+ current_app.config.get("RDM_PARENT_PERSISTENT_IDENTIFIERS", {})
193
+ .get("doi", {})
194
+ .get("required")
193
195
  )
194
- record_ui["ui"]["new_draft_parent_doi"] = parent_doi
196
+ if not is_doi_required:
197
+ # check if the draft has a reserved doi and mint parent doi only in that case
198
+ record_doi = record._record.pids.get("doi", {})
199
+ is_doi_reserved = record_doi.get(
200
+ "provider", ""
201
+ ) == "datacite" and record_doi.get("identifier")
202
+ if not is_doi_reserved:
203
+ should_mint_parent_doi = False
204
+
205
+ if should_mint_parent_doi:
206
+ datacite_provider = datacite_provider[0]
207
+ parent_doi = datacite_provider.client.generate_doi(
208
+ record._record.parent
209
+ )
210
+ record_ui["ui"]["new_draft_parent_doi"] = parent_doi
195
211
 
196
212
  # emit a record view stats event
197
213
  emitter = current_stats.get_event_emitter("record-view")
@@ -0,0 +1,83 @@
1
+ /*
2
+ * // This file is part of Invenio-App-Rdm
3
+ * // Copyright (C) 2025 CERN.
4
+ * //
5
+ * // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
6
+ * // under the terms of the MIT License; see LICENSE file for more details.
7
+ */
8
+
9
+ import React, { useState } from "react";
10
+ import PropTypes from "prop-types";
11
+ import { Grid, Dropdown, Button } from "semantic-ui-react";
12
+
13
+ export const CompareRevisionsDropdown = ({
14
+ loading,
15
+ options,
16
+ onCompare,
17
+ srcRevision: srcOption,
18
+ targetRevision: targetOption,
19
+ }) => {
20
+ // Local state for selected revisions
21
+ const [srcRevision, setSrcRevision] = useState(srcOption);
22
+ const [targetRevision, setTargetRevision] = useState(targetOption);
23
+
24
+ const handleCompare = () => {
25
+ onCompare(srcRevision, targetRevision);
26
+ };
27
+
28
+ return (
29
+ <Grid>
30
+ <Grid.Column mobile={16} computer={6} tablet={16} largeScreen={6} widescreen={6}>
31
+ <label htmlFor="source-revision">From</label>
32
+ <Dropdown
33
+ id="source-revision"
34
+ loading={loading}
35
+ placeholder="Source revision..."
36
+ fluid
37
+ selection
38
+ value={srcRevision}
39
+ onChange={(e, { value }) => setSrcRevision(value)}
40
+ options={options}
41
+ scrolling
42
+ />
43
+ </Grid.Column>
44
+ <Grid.Column mobile={16} computer={6} tablet={16} largeScreen={6} widescreen={6}>
45
+ <label htmlFor="target-revision">To</label>
46
+ <Dropdown
47
+ id="target-revision"
48
+ loading={loading}
49
+ placeholder="Target revision..."
50
+ fluid
51
+ selection
52
+ value={targetRevision}
53
+ onChange={(e, { value }) => setTargetRevision(value)}
54
+ options={options}
55
+ scrolling
56
+ />
57
+ </Grid.Column>
58
+ <Grid.Column
59
+ verticalAlign="bottom"
60
+ mobile={16}
61
+ computer={2}
62
+ tablet={16}
63
+ largeScreen={2}
64
+ widescreen={2}
65
+ >
66
+ <Button
67
+ onClick={handleCompare}
68
+ disabled={loading || !srcRevision || !targetRevision}
69
+ >
70
+ Compare
71
+ </Button>
72
+ </Grid.Column>
73
+ </Grid>
74
+ );
75
+ };
76
+
77
+ CompareRevisionsDropdown.propTypes = {
78
+ loading: PropTypes.bool.isRequired,
79
+ options: PropTypes.array.isRequired,
80
+ onCompare: PropTypes.func.isRequired,
81
+ srcRevision: PropTypes.object,
82
+ targetRevision: PropTypes.object,
83
+ };
@@ -83,7 +83,6 @@ export class ImpersonateUserForm extends Component {
83
83
  render() {
84
84
  const { error, loading } = this.state;
85
85
  const { user } = this.props;
86
- console.log({ user });
87
86
  return (
88
87
  <Formik
89
88
  onSubmit={this.handleSubmit}
@@ -0,0 +1,77 @@
1
+ /*
2
+ * // This file is part of Invenio-App-Rdm
3
+ * // Copyright (C) 2025 CERN.
4
+ * //
5
+ * // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
6
+ * // under the terms of the MIT License; see LICENSE file for more details.
7
+ */
8
+
9
+ import React, { Component } from "react";
10
+ import PropTypes from "prop-types";
11
+ import { Grid, Container } from "semantic-ui-react";
12
+ import { Differ, Viewer } from "json-diff-kit";
13
+
14
+ export class RevisionsDiffViewer extends Component {
15
+ constructor(props) {
16
+ super(props);
17
+ this.differ = new Differ({
18
+ detectCircular: true,
19
+ maxDepth: null,
20
+ showModifications: true,
21
+ arrayDiffMethod: "lcs",
22
+ ignoreCase: false,
23
+ ignoreCaseForKey: false,
24
+ recursiveEqual: true,
25
+ });
26
+
27
+ this.viewerProps = {
28
+ indent: 4,
29
+ lineNumbers: true,
30
+ highlightInlineDiff: true,
31
+ inlineDiffOptions: {
32
+ mode: "word",
33
+ wordSeparator: " ",
34
+ },
35
+ hideUnchangedLines: true,
36
+ syntaxHighlight: false,
37
+ virtual: true,
38
+ };
39
+
40
+ this.state = {
41
+ currentDiff: undefined,
42
+ };
43
+ }
44
+
45
+ componentDidUpdate(prevProps) {
46
+ const { diff } = this.props;
47
+
48
+ if (diff !== prevProps.diff) {
49
+ this.computeDiff();
50
+ }
51
+ }
52
+
53
+ computeDiff = () => {
54
+ const { diff } = this.props;
55
+ const _diff = this.differ.diff(diff?.srcRevision, diff?.targetRevision);
56
+ this.setState({ currentDiff: _diff });
57
+ };
58
+
59
+ render() {
60
+ const { currentDiff } = this.state;
61
+
62
+ return currentDiff ? (
63
+ <Grid>
64
+ <Grid.Column width={16}>
65
+ <Container fluid>
66
+ <Viewer diff={currentDiff} {...this.viewerProps} />
67
+ </Container>
68
+ </Grid.Column>
69
+ </Grid>
70
+ ) : null;
71
+ }
72
+ }
73
+
74
+ RevisionsDiffViewer.propTypes = {
75
+ diff: PropTypes.object,
76
+ viewerProps: PropTypes.object.isRequired,
77
+ };
@@ -0,0 +1,128 @@
1
+ /*
2
+ * // This file is part of Invenio-App-Rdm
3
+ * // Copyright (C) 2025 CERN.
4
+ * //
5
+ * // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
6
+ * // under the terms of the MIT License; see LICENSE file for more details.
7
+ */
8
+ import React, { Component } from "react";
9
+ import PropTypes from "prop-types";
10
+ import { RecordModerationApi } from "./api";
11
+ import { withCancel, ErrorMessage } from "react-invenio-forms";
12
+ import { Modal, Button, Grid } from "semantic-ui-react";
13
+ import { i18next } from "@translations/invenio_app_rdm/i18next";
14
+ import { CompareRevisionsDropdown } from "../components/CompareRevisionsDropdown";
15
+ import { RevisionsDiffViewer } from "../components/RevisionsDiffViewer";
16
+
17
+ export class CompareRevisions extends Component {
18
+ constructor(props) {
19
+ super(props);
20
+
21
+ this.state = {
22
+ loading: true,
23
+ error: undefined,
24
+ allRevisions: {},
25
+ srcRevision: undefined,
26
+ targetRevision: undefined,
27
+ diff: undefined,
28
+ };
29
+ }
30
+
31
+ async fetchRevisions() {
32
+ const { resource } = this.props;
33
+ this.setState({ loading: true });
34
+
35
+ try {
36
+ this.cancellableAction = withCancel(RecordModerationApi.getRevisions(resource));
37
+ const response = await this.cancellableAction.promise;
38
+ const revisions = await response.data;
39
+
40
+ this.setState({
41
+ allRevisions: revisions,
42
+ targetRevision: revisions[0],
43
+ srcRevision: revisions.length > 1 ? revisions[1] : revisions[0],
44
+ loading: false,
45
+ });
46
+ } catch (error) {
47
+ if (error === "UNMOUNTED") return;
48
+ this.setState({ error: error, loading: false });
49
+ console.error(error);
50
+ }
51
+ }
52
+
53
+ componentDidMount() {
54
+ this.fetchRevisions();
55
+ }
56
+
57
+ componentWillUnmount() {
58
+ this.cancellableAction && this.cancellableAction.cancel();
59
+ }
60
+
61
+ handleModalClose = () => {
62
+ const { actionCancelCallback } = this.props;
63
+ actionCancelCallback();
64
+ };
65
+
66
+ handleCompare = (srcRevision, targetRevision) => {
67
+ this.setState({ diff: { srcRevision, targetRevision } });
68
+ };
69
+
70
+ render() {
71
+ const { error, loading, allRevisions, srcRevision, targetRevision, diff } =
72
+ this.state;
73
+ const options = Object.values(allRevisions).map((rev) => ({
74
+ key: rev.updated,
75
+ text: `${rev.updated} (${rev.revision_id})`,
76
+ value: rev,
77
+ }));
78
+
79
+ return loading ? (
80
+ <p>Loading...</p>
81
+ ) : (
82
+ <>
83
+ <Modal.Content>
84
+ <CompareRevisionsDropdown
85
+ loading={loading}
86
+ options={options}
87
+ srcRevision={srcRevision}
88
+ targetRevision={targetRevision}
89
+ onCompare={this.handleCompare}
90
+ />
91
+ {error && (
92
+ <Modal.Content>
93
+ <ErrorMessage
94
+ header={i18next.t("Unable to fetch revisions.")}
95
+ content={error}
96
+ icon="exclamation"
97
+ className="text-align-left"
98
+ negative
99
+ />
100
+ </Modal.Content>
101
+ )}
102
+ <Modal.Content scrolling>
103
+ <RevisionsDiffViewer diff={this.state.diff} />
104
+ </Modal.Content>
105
+ </Modal.Content>
106
+ <Modal.Actions>
107
+ <Grid>
108
+ <Grid.Column floated="left" width={8} textAlign="left">
109
+ <Button
110
+ onClick={this.handleModalClose}
111
+ disabled={loading}
112
+ loading={loading}
113
+ aria-label={i18next.t("Cancel revision comparison")}
114
+ >
115
+ Close
116
+ </Button>
117
+ </Grid.Column>
118
+ </Grid>
119
+ </Modal.Actions>
120
+ </>
121
+ );
122
+ }
123
+ }
124
+
125
+ CompareRevisions.propTypes = {
126
+ resource: PropTypes.object.isRequired,
127
+ actionCancelCallback: PropTypes.func.isRequired,
128
+ };