invenio-app-rdm 14.0.0b0.dev3__py2.py3-none-any.whl → 14.0.0b1.dev1__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.
- invenio_app_rdm/__init__.py +1 -1
- invenio_app_rdm/administration/moderation/__init__.py +11 -0
- invenio_app_rdm/administration/moderation/requests.py +175 -0
- invenio_app_rdm/administration/templates/semantic-ui/invenio_app_rdm/administration/requests.html +14 -0
- invenio_app_rdm/administration/templates/semantic-ui/invenio_app_rdm/administration/requests_details.html +89 -0
- invenio_app_rdm/administration/user_moderation/user_moderation.py +4 -4
- invenio_app_rdm/config.py +47 -0
- invenio_app_rdm/records_ui/templates/semantic-ui/invenio_app_rdm/records/deposit.html +3 -0
- invenio_app_rdm/records_ui/templates/semantic-ui/invenio_app_rdm/records/details/side_bar/manage_menu.html +3 -3
- invenio_app_rdm/records_ui/utils.py +22 -1
- invenio_app_rdm/records_ui/views/deposits.py +62 -3
- invenio_app_rdm/records_ui/views/records.py +43 -1
- invenio_app_rdm/requests_ui/templates/semantic-ui/invenio_requests/guest-access-request/index.html +5 -5
- invenio_app_rdm/requests_ui/templates/semantic-ui/invenio_requests/record-deletion/index.html +93 -0
- invenio_app_rdm/requests_ui/views/requests.py +17 -2
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/requests/RequestsSearchResultItem.js +81 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/requests/RequestsSearchbarLayout.js +48 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/requests/index.js +22 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/components/RecordDeletion/DeletionModal.js +356 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/components/RecordDeletion/DeletionRadioGroup.js +44 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/components/RecordDeletion.js +81 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/deposit/RDMDepositForm.js +22 -2
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/deposit/index.js +2 -1
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ManageButton.js +72 -20
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordCommunitySubmission/RecordCommunitySubmissionModal.js +1 -1
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordManagement.js +10 -3
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/index.js +1 -0
- invenio_app_rdm/theme/templates/semantic-ui/invenio_app_rdm/help/search.de.html +2 -2
- invenio_app_rdm/theme/templates/semantic-ui/invenio_app_rdm/help/search.en.html +4 -4
- invenio_app_rdm/theme/templates/semantic-ui/invenio_app_rdm/help/search.sv.html +4 -4
- invenio_app_rdm/theme/webpack.py +2 -1
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/METADATA +23 -5
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/RECORD +37 -26
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/entry_points.txt +2 -0
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/WHEEL +0 -0
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/licenses/LICENSE +0 -0
- {invenio_app_rdm-14.0.0b0.dev3.dist-info → invenio_app_rdm-14.0.0b1.dev1.dist-info}/top_level.txt +0 -0
invenio_app_rdm/requests_ui/templates/semantic-ui/invenio_requests/guest-access-request/index.html
CHANGED
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
{# only display the header for the receiver #}
|
|
31
31
|
{%- set back_button_url = url_for("invenio_app_rdm_users.requests") %}
|
|
32
32
|
{{ inclusion_request_header(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
back_button_url=back_button_url,
|
|
34
|
+
back_button_text=_("Back to requests"),
|
|
35
|
+
request=invenio_request,
|
|
36
|
+
accepted=request_is_accepted
|
|
37
|
+
) }}
|
|
38
38
|
|
|
39
39
|
{%- else %}
|
|
40
40
|
<div class="ui grid rel-mt-2">
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
This file is part of Invenio.
|
|
4
|
+
Copyright (C) 2016-2025 CERN.
|
|
5
|
+
|
|
6
|
+
Invenio is free software; you can redistribute it and/or modify it
|
|
7
|
+
under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
#}
|
|
9
|
+
|
|
10
|
+
{% extends "invenio_requests/details/index.html" %}
|
|
11
|
+
|
|
12
|
+
{% set active_dashboard_menu_item = 'requests' %}
|
|
13
|
+
{% set active_community_header_menu_item = 'requests' %}
|
|
14
|
+
|
|
15
|
+
{%- block request_header %}
|
|
16
|
+
{% set back_button_url = url_for("invenio_app_rdm_users.requests") %}
|
|
17
|
+
{% from "invenio_requests/macros/request_header.html" import inclusion_request_header %}
|
|
18
|
+
{{ inclusion_request_header(
|
|
19
|
+
request=invenio_request,
|
|
20
|
+
record=record,
|
|
21
|
+
accepted=request_is_accepted,
|
|
22
|
+
back_button_url=back_button_url,
|
|
23
|
+
back_button_text=_("Back to requests")
|
|
24
|
+
) }}
|
|
25
|
+
|
|
26
|
+
<div class="twelve wide column mt-10">
|
|
27
|
+
<p><strong>{{ _("Reason:") }}</strong> {{ invenio_request["payload"]["reason_label"] }}</p>
|
|
28
|
+
<p><strong>{{ _("Justification:") }}</strong> {{ invenio_request["payload"]["comment"] }}</p>
|
|
29
|
+
</div>
|
|
30
|
+
{%- endblock request_header %}
|
|
31
|
+
|
|
32
|
+
{% block request_timeline %}
|
|
33
|
+
<div
|
|
34
|
+
class="ui container rdm-tab-container fluid rel-pt-2 ml-0-mobile mr-0-mobile"
|
|
35
|
+
id="request-request-deletion-tab-container"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
class="ui secondary pointing menu rdm-tab-menu"
|
|
39
|
+
id="request-deletion-request-tab"
|
|
40
|
+
>
|
|
41
|
+
<a
|
|
42
|
+
class="active item"
|
|
43
|
+
data-tab="conversation"
|
|
44
|
+
role="tab"
|
|
45
|
+
aria-selected="true"
|
|
46
|
+
aria-controls="conversation-tab-panel"
|
|
47
|
+
id="conversation-tab"
|
|
48
|
+
>
|
|
49
|
+
{{ _("Conversation") }}
|
|
50
|
+
</a>
|
|
51
|
+
|
|
52
|
+
{% if record_ui %}
|
|
53
|
+
<a
|
|
54
|
+
role="tab"
|
|
55
|
+
class="item"
|
|
56
|
+
data-tab="record"
|
|
57
|
+
aria-selected="false"
|
|
58
|
+
aria-controls="record-tab-panel"
|
|
59
|
+
id="record-tab"
|
|
60
|
+
>
|
|
61
|
+
{{ _("Record") }}
|
|
62
|
+
</a>
|
|
63
|
+
{% endif %}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div
|
|
67
|
+
class="ui bottom attached tab segment active borderless p-0"
|
|
68
|
+
data-tab="conversation"
|
|
69
|
+
role="tabpanel"
|
|
70
|
+
aria-labelledby="conversation-tab"
|
|
71
|
+
id="conversation-tab-panel"
|
|
72
|
+
>
|
|
73
|
+
{{ super() }}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{# The record tab content needs to be last since the HTML structure is complex and breaks following tab contents #}
|
|
77
|
+
{% if record_ui %}
|
|
78
|
+
<div
|
|
79
|
+
class="ui bottom attached tab segment borderless"
|
|
80
|
+
data-tab="record"
|
|
81
|
+
role="tabpanel"
|
|
82
|
+
aria-labelledby="record-tab"
|
|
83
|
+
id="record-tab-panel"
|
|
84
|
+
hidden="hidden"
|
|
85
|
+
>
|
|
86
|
+
{% set use_theme_basic_template = false %}
|
|
87
|
+
{% set preview_submission_request = true %}
|
|
88
|
+
{% include config.APP_RDM_RECORD_LANDING_PAGE_TEMPLATE %}
|
|
89
|
+
</div>
|
|
90
|
+
{% endif %}
|
|
91
|
+
|
|
92
|
+
</div>
|
|
93
|
+
{% endblock request_timeline %}
|
|
@@ -22,16 +22,20 @@ from invenio_communities.subcommunities.services.request import (
|
|
|
22
22
|
from invenio_communities.utils import identity_cache_key
|
|
23
23
|
from invenio_communities.views.communities import render_community_theme_template
|
|
24
24
|
from invenio_communities.views.decorators import pass_community
|
|
25
|
+
from invenio_i18n.ext import current_i18n
|
|
25
26
|
from invenio_pidstore.errors import PIDDoesNotExistError
|
|
26
27
|
from invenio_rdm_records.proxies import current_rdm_records_service
|
|
27
28
|
from invenio_rdm_records.requests import CommunityInclusion, CommunitySubmission
|
|
28
29
|
from invenio_rdm_records.resources.serializers import UIJSONSerializer
|
|
30
|
+
from invenio_rdm_records.services.errors import RecordDeletedException
|
|
29
31
|
from invenio_rdm_records.services.generators import CommunityInclusionNeed
|
|
30
32
|
from invenio_records_resources.services.errors import PermissionDeniedError
|
|
31
33
|
from invenio_requests.customizations import AcceptAction
|
|
32
34
|
from invenio_requests.resolvers.registry import ResolverRegistry
|
|
33
35
|
from invenio_requests.views.decorators import pass_request
|
|
34
36
|
from invenio_users_resources.proxies import current_user_resources
|
|
37
|
+
from invenio_vocabularies.proxies import current_service as vocabulary_service
|
|
38
|
+
from marshmallow_utils.fields.babel import gettext_from_dict
|
|
35
39
|
from sqlalchemy.orm.exc import NoResultFound
|
|
36
40
|
|
|
37
41
|
from ...records_ui.utils import get_external_resources
|
|
@@ -89,7 +93,7 @@ def _resolve_topic_record(request):
|
|
|
89
93
|
record = current_rdm_records_service.read_draft(
|
|
90
94
|
g.identity, pid, expand=True
|
|
91
95
|
)
|
|
92
|
-
except (NoResultFound, PIDDoesNotExistError):
|
|
96
|
+
except (NoResultFound, PIDDoesNotExistError, RecordDeletedException):
|
|
93
97
|
# We catch PIDDoesNotExistError because a published record with
|
|
94
98
|
# a soft-deleted draft will raise this error. The lines below
|
|
95
99
|
# will catch the case that a id does not exists and raise a
|
|
@@ -98,7 +102,7 @@ def _resolve_topic_record(request):
|
|
|
98
102
|
try:
|
|
99
103
|
# read published record
|
|
100
104
|
record = current_rdm_records_service.read(g.identity, pid, expand=True)
|
|
101
|
-
except NoResultFound:
|
|
105
|
+
except (NoResultFound, RecordDeletedException):
|
|
102
106
|
# record tab not displayed when the record is not found
|
|
103
107
|
# the request is probably not open anymore
|
|
104
108
|
pass
|
|
@@ -202,6 +206,17 @@ def user_dashboard_request_view(request, **kwargs):
|
|
|
202
206
|
else:
|
|
203
207
|
checks = ChecksAPI.get_runs(record._record)
|
|
204
208
|
|
|
209
|
+
if request_type == "record-deletion":
|
|
210
|
+
reason_title = vocabulary_service.read(
|
|
211
|
+
g.identity,
|
|
212
|
+
("removalreasons", request["payload"]["reason"]),
|
|
213
|
+
).to_dict()
|
|
214
|
+
request["payload"]["reason_label"] = gettext_from_dict(
|
|
215
|
+
reason_title["title"],
|
|
216
|
+
current_i18n.locale,
|
|
217
|
+
current_app.config.get("BABEL_DEFAULT_LOCALE", "en"),
|
|
218
|
+
)
|
|
219
|
+
|
|
205
220
|
return render_template(
|
|
206
221
|
f"invenio_requests/{request_type}/index.html",
|
|
207
222
|
base_template="invenio_app_rdm/users/base.html",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Invenio.
|
|
3
|
+
* Copyright (C) 2025 CERN.
|
|
4
|
+
*
|
|
5
|
+
* InvenioAppRdm 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 PropTypes from "prop-types";
|
|
10
|
+
import React, { Component } from "react";
|
|
11
|
+
import { Table } from "semantic-ui-react";
|
|
12
|
+
import { withState } from "react-searchkit";
|
|
13
|
+
import { AdminUIRoutes } from "@js/invenio_administration/src/routes.js";
|
|
14
|
+
import { UserListItemCompact, toRelativeTime } from "react-invenio-forms";
|
|
15
|
+
import { i18next } from "@translations/invenio_app_rdm/i18next";
|
|
16
|
+
|
|
17
|
+
class SearchResultItemComponent extends Component {
|
|
18
|
+
refreshAfterAction = () => {
|
|
19
|
+
const { updateQueryState, currentQueryState } = this.props;
|
|
20
|
+
updateQueryState(currentQueryState);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
render() {
|
|
24
|
+
const { result, columns, idKeyPath, listUIEndpoint } = this.props;
|
|
25
|
+
return (
|
|
26
|
+
<Table.Row>
|
|
27
|
+
<Table.Cell
|
|
28
|
+
key={`${columns[0][1]["text"]}-${columns[0][1]["order"]}`}
|
|
29
|
+
data-label={columns[0][1]["text"]}
|
|
30
|
+
className="word-break-all"
|
|
31
|
+
>
|
|
32
|
+
<a href={AdminUIRoutes.detailsView(listUIEndpoint, result, idKeyPath)}>
|
|
33
|
+
{/* TODO we need a better way to get the (translatable) label of a request type
|
|
34
|
+
https://github.com/inveniosoftware/invenio-requests/issues/414 */}
|
|
35
|
+
{result.type === "record-deletion" && "Record deletion"}
|
|
36
|
+
</a>
|
|
37
|
+
</Table.Cell>
|
|
38
|
+
<Table.Cell
|
|
39
|
+
key={`${columns[1][1]["text"]}-${columns[1][1]["order"]}`}
|
|
40
|
+
data-label={columns[1][1]["text"]}
|
|
41
|
+
className="word-break-all"
|
|
42
|
+
>
|
|
43
|
+
<UserListItemCompact
|
|
44
|
+
user={result.expanded.created_by}
|
|
45
|
+
id={result.created_by.user}
|
|
46
|
+
// TODO linkToDetailView= filter by user?
|
|
47
|
+
/>
|
|
48
|
+
{toRelativeTime(result.created)}
|
|
49
|
+
</Table.Cell>
|
|
50
|
+
<Table.Cell
|
|
51
|
+
key={`${columns[2][1]["text"]}-${columns[2][1]["order"]}`}
|
|
52
|
+
data-label={columns[2][1]["text"]}
|
|
53
|
+
className="word-break-all"
|
|
54
|
+
>
|
|
55
|
+
{result.expanded.last_reply ? (
|
|
56
|
+
<>
|
|
57
|
+
<UserListItemCompact
|
|
58
|
+
user={result.expanded.last_reply.created_by}
|
|
59
|
+
id={result.last_reply.created_by.user}
|
|
60
|
+
/>
|
|
61
|
+
{toRelativeTime(result.last_reply.created)}
|
|
62
|
+
</>
|
|
63
|
+
) : (
|
|
64
|
+
i18next.t("No replies")
|
|
65
|
+
)}
|
|
66
|
+
</Table.Cell>
|
|
67
|
+
</Table.Row>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
SearchResultItemComponent.propTypes = {
|
|
73
|
+
result: PropTypes.object.isRequired,
|
|
74
|
+
columns: PropTypes.array.isRequired,
|
|
75
|
+
updateQueryState: PropTypes.func.isRequired,
|
|
76
|
+
currentQueryState: PropTypes.object.isRequired,
|
|
77
|
+
idKeyPath: PropTypes.string.isRequired,
|
|
78
|
+
listUIEndpoint: PropTypes.string.isRequired,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const SearchResultItem = withState(SearchResultItemComponent);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* // This file is part of Invenio-Requests
|
|
3
|
+
* // Copyright (C) 2025 CERN.
|
|
4
|
+
* //
|
|
5
|
+
* // Invenio-Requests 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 PropTypes from "prop-types";
|
|
10
|
+
import React, { Component } from "react";
|
|
11
|
+
import { RequestStatusFilter } from "@js/invenio_requests/search";
|
|
12
|
+
import { SearchBar, Sort } from "react-searchkit";
|
|
13
|
+
|
|
14
|
+
export class RequestsSearchbarLayout extends Component {
|
|
15
|
+
render() {
|
|
16
|
+
const { config } = this.props;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{/* auto column grid used instead of SUI grid for better searchbar width adjustment */}
|
|
21
|
+
<div className="auto-column-grid">
|
|
22
|
+
<div className="flex column-mobile">
|
|
23
|
+
<div className="mobile only rel-mb-1 flex align-items-center justify-space-between">
|
|
24
|
+
<RequestStatusFilter keepFiltersOnUpdate />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="tablet only computer only rel-mr-2">
|
|
28
|
+
<RequestStatusFilter keepFiltersOnUpdate />
|
|
29
|
+
</div>
|
|
30
|
+
<div className="full-width">
|
|
31
|
+
<SearchBar fluid />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex align-items-center column-mobile">
|
|
35
|
+
<div className="full-width flex align-items-center justify-end column-mobile">
|
|
36
|
+
<Sort values={config.sortOptions} />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="rel-mb-1" />
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
RequestsSearchbarLayout.propTypes = {
|
|
47
|
+
config: PropTypes.object.isRequired,
|
|
48
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// This file is part of InvenioCommunities
|
|
2
|
+
// Copyright (C) 2025 CERN.
|
|
3
|
+
//
|
|
4
|
+
// Invenio RDM is free software; you can redistribute it and/or modify it
|
|
5
|
+
// under the terms of the MIT License; see LICENSE file for more details.
|
|
6
|
+
|
|
7
|
+
import { initDefaultSearchComponents } from "@js/invenio_administration";
|
|
8
|
+
import { createSearchAppInit } from "@js/invenio_search_ui";
|
|
9
|
+
import { RequestsSearchbarLayout } from "./RequestsSearchbarLayout";
|
|
10
|
+
import { SearchResultItem } from "./RequestsSearchResultItem";
|
|
11
|
+
|
|
12
|
+
const domContainer = document.getElementById("invenio-search-config");
|
|
13
|
+
|
|
14
|
+
const defaultComponents = initDefaultSearchComponents(domContainer);
|
|
15
|
+
|
|
16
|
+
const overridenComponents = {
|
|
17
|
+
...defaultComponents,
|
|
18
|
+
"SearchApp.searchbarContainer": RequestsSearchbarLayout,
|
|
19
|
+
"InvenioAdministration.SearchResultItem.layout": SearchResultItem,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
createSearchAppInit(overridenComponents, true, "invenio-search-config", false);
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// This file is part of InvenioRDM
|
|
2
|
+
// Copyright (C) 2025 CERN
|
|
3
|
+
//
|
|
4
|
+
// Invenio-app-rdm is free software; you can redistribute it and/or modify it
|
|
5
|
+
// under the terms of the MIT License; see LICENSE file for more details.
|
|
6
|
+
|
|
7
|
+
import { i18next } from "@translations/invenio_app_rdm/i18next";
|
|
8
|
+
import { Formik } from "formik";
|
|
9
|
+
import PropTypes from "prop-types";
|
|
10
|
+
import React, { Component } from "react";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ErrorMessage,
|
|
14
|
+
http,
|
|
15
|
+
SelectField,
|
|
16
|
+
TextAreaField,
|
|
17
|
+
withCancel,
|
|
18
|
+
} from "react-invenio-forms";
|
|
19
|
+
import {
|
|
20
|
+
Button,
|
|
21
|
+
Checkbox,
|
|
22
|
+
Icon,
|
|
23
|
+
Form,
|
|
24
|
+
Message,
|
|
25
|
+
Modal,
|
|
26
|
+
ModalActions,
|
|
27
|
+
ModalContent,
|
|
28
|
+
ModalHeader,
|
|
29
|
+
Table,
|
|
30
|
+
TableBody,
|
|
31
|
+
TableHeader,
|
|
32
|
+
TableHeaderCell,
|
|
33
|
+
TableRow,
|
|
34
|
+
} from "semantic-ui-react";
|
|
35
|
+
import * as Yup from "yup";
|
|
36
|
+
import DeletionRadioGroup from "./DeletionRadioGroup";
|
|
37
|
+
|
|
38
|
+
export class DeletionModal extends Component {
|
|
39
|
+
constructor(props) {
|
|
40
|
+
super(props);
|
|
41
|
+
const { recordDeletion } = this.props;
|
|
42
|
+
const checklistLength = recordDeletion["checklist"]?.length || 0;
|
|
43
|
+
this.initState = {
|
|
44
|
+
loading: false,
|
|
45
|
+
error: undefined,
|
|
46
|
+
checklistState: Array(checklistLength).fill(undefined),
|
|
47
|
+
checkboxState: Array(2).fill(false), // TODO dynamic count of number of checkboxes
|
|
48
|
+
messages: [],
|
|
49
|
+
};
|
|
50
|
+
this.state = { ...this.initState };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleClose = () => {
|
|
54
|
+
this.setState({
|
|
55
|
+
...this.initState,
|
|
56
|
+
});
|
|
57
|
+
const { handleClose } = this.props;
|
|
58
|
+
handleClose();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
handleRadioUpdate = (index, value) => {
|
|
62
|
+
const { recordDeletion } = this.props;
|
|
63
|
+
const { checklistState } = this.state;
|
|
64
|
+
const nextChecklistState = checklistState.map((c, i) => {
|
|
65
|
+
if (i === index) {
|
|
66
|
+
return value;
|
|
67
|
+
} else {
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const filteredChecklist = recordDeletion["checklist"].filter((_, index) => {
|
|
72
|
+
return nextChecklistState[index];
|
|
73
|
+
});
|
|
74
|
+
const newMessages = filteredChecklist.map((x) => x["message"]);
|
|
75
|
+
this.setState({ checklistState: nextChecklistState, messages: newMessages });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
handleCheckboxUpdate = (index) => {
|
|
79
|
+
const { checkboxState } = this.state;
|
|
80
|
+
const nextCheckboxState = checkboxState.map((c, i) => {
|
|
81
|
+
if (i === index) {
|
|
82
|
+
return !checkboxState[i];
|
|
83
|
+
} else {
|
|
84
|
+
return c;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.setState({ checkboxState: nextCheckboxState });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
deletionRequestSchema = (immediateDeletionAllowed) => {
|
|
91
|
+
if (immediateDeletionAllowed) {
|
|
92
|
+
return Yup.object({
|
|
93
|
+
reason: Yup.string().required("Required"),
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
return Yup.object({
|
|
97
|
+
reason: Yup.string().required("Required"),
|
|
98
|
+
comment: Yup.string()
|
|
99
|
+
.min(25, "Please write at least 25 characters")
|
|
100
|
+
.required("Required"),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
handleSubmit = async (values) => {
|
|
106
|
+
this.setState({ loading: true });
|
|
107
|
+
const { record } = this.props;
|
|
108
|
+
const payload = {
|
|
109
|
+
reason: values.reason,
|
|
110
|
+
comment: values.comment,
|
|
111
|
+
};
|
|
112
|
+
if ("request_deletion" in record.links) {
|
|
113
|
+
this.cancellableAction = withCancel(
|
|
114
|
+
http.post(record.links.request_deletion, payload)
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.cancellableAction.promise;
|
|
118
|
+
const data = response.data;
|
|
119
|
+
|
|
120
|
+
if (response.status === 200) {
|
|
121
|
+
window.location.reload();
|
|
122
|
+
} else if (response.status === 201) {
|
|
123
|
+
window.location.href = data.links.self_html;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.setState({ error: error });
|
|
127
|
+
console.error(error);
|
|
128
|
+
} finally {
|
|
129
|
+
this.setState({ loading: false });
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
this.setState({ error: "Could not submit deletion request", loading: false });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
render() {
|
|
137
|
+
const { open, recordDeletion, options } = this.props;
|
|
138
|
+
const { loading, error, checklistState, checkboxState, messages } = this.state;
|
|
139
|
+
|
|
140
|
+
const { checklist } = recordDeletion;
|
|
141
|
+
|
|
142
|
+
if (!("recordDeletion" in recordDeletion)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const immediateDeletionAllowed =
|
|
147
|
+
recordDeletion["recordDeletion"]["immediate_deletion"]["allowed"];
|
|
148
|
+
|
|
149
|
+
const modalHeaderText = immediateDeletionAllowed
|
|
150
|
+
? i18next.t("Delete record")
|
|
151
|
+
: i18next.t("Request deletion");
|
|
152
|
+
const deletionButtonText = immediateDeletionAllowed
|
|
153
|
+
? i18next.t("Delete immediately")
|
|
154
|
+
: i18next.t("Request deletion");
|
|
155
|
+
const deletionButtonClass = immediateDeletionAllowed ? "negative" : "primary";
|
|
156
|
+
|
|
157
|
+
const files = recordDeletion["context"]["files"];
|
|
158
|
+
const internalDoi = recordDeletion["context"]["internalDoi"];
|
|
159
|
+
|
|
160
|
+
const formDisabled =
|
|
161
|
+
checkboxState.some((v) => v === false) ||
|
|
162
|
+
checklistState.some((x) => x === true || x === undefined);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Modal
|
|
166
|
+
open={open}
|
|
167
|
+
closeIcon
|
|
168
|
+
onClose={this.handleClose}
|
|
169
|
+
role="dialog"
|
|
170
|
+
aria-modal="true"
|
|
171
|
+
tab-index="-1"
|
|
172
|
+
size="tiny"
|
|
173
|
+
closeOnDimmerClick={false}
|
|
174
|
+
onClick={(e) => e.stopPropagation()} // prevent interaction with dropdown
|
|
175
|
+
onKeyDown={(e) => e.stopPropagation()} // prevent interaction with dropdown
|
|
176
|
+
>
|
|
177
|
+
<ModalHeader>{modalHeaderText}</ModalHeader>
|
|
178
|
+
<Formik
|
|
179
|
+
onSubmit={this.handleSubmit}
|
|
180
|
+
validationSchema={this.deletionRequestSchema(immediateDeletionAllowed)}
|
|
181
|
+
initialValues={{ reason: "", comment: "" }}
|
|
182
|
+
validateOnChange={false}
|
|
183
|
+
validateOnBlur={false}
|
|
184
|
+
>
|
|
185
|
+
{({ handleSubmit }) => (
|
|
186
|
+
<Form>
|
|
187
|
+
<ModalContent>
|
|
188
|
+
<>
|
|
189
|
+
{immediateDeletionAllowed ? (
|
|
190
|
+
<p>
|
|
191
|
+
{
|
|
192
|
+
recordDeletion["recordDeletion"]["immediate_deletion"][
|
|
193
|
+
"policy"
|
|
194
|
+
]["description"]
|
|
195
|
+
}
|
|
196
|
+
</p>
|
|
197
|
+
) : (
|
|
198
|
+
<p>
|
|
199
|
+
{
|
|
200
|
+
recordDeletion["recordDeletion"]["request_deletion"]["policy"][
|
|
201
|
+
"description"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
<Message negative>
|
|
207
|
+
<Icon name="warning sign" />
|
|
208
|
+
{i18next.t("By deleting this record you acknowledge that:")}
|
|
209
|
+
<br />
|
|
210
|
+
<Checkbox
|
|
211
|
+
label={
|
|
212
|
+
/* eslint-disable-next-line jsx-a11y/label-has-associated-control */
|
|
213
|
+
<label>
|
|
214
|
+
<strong>
|
|
215
|
+
{files} file{files !== 1 ? "s" : ""}
|
|
216
|
+
</strong>{" "}
|
|
217
|
+
{i18next.t("will be deleted.")}
|
|
218
|
+
</label>
|
|
219
|
+
}
|
|
220
|
+
className="mt-5 mb-5"
|
|
221
|
+
onChange={() => this.handleCheckboxUpdate(0)}
|
|
222
|
+
/>
|
|
223
|
+
<br />
|
|
224
|
+
{internalDoi ? (
|
|
225
|
+
<Checkbox
|
|
226
|
+
label={
|
|
227
|
+
/* eslint-disable-next-line jsx-a11y/label-has-associated-control */
|
|
228
|
+
<label>
|
|
229
|
+
<strong>{i18next.t("The DOI cannot be reused")}</strong>{" "}
|
|
230
|
+
{i18next.t(
|
|
231
|
+
"and the DOI will resolve to a tombstone page with the record's citation (including title, author/s, publication year and publisher)"
|
|
232
|
+
)}
|
|
233
|
+
</label>
|
|
234
|
+
}
|
|
235
|
+
className="mb-5"
|
|
236
|
+
onChange={() => this.handleCheckboxUpdate(1)}
|
|
237
|
+
/>
|
|
238
|
+
) : (
|
|
239
|
+
<Checkbox
|
|
240
|
+
label={
|
|
241
|
+
/* eslint-disable-next-line jsx-a11y/label-has-associated-control */
|
|
242
|
+
<label>
|
|
243
|
+
{i18next.t(
|
|
244
|
+
"A tombstone page with the citation will replace the record page"
|
|
245
|
+
)}
|
|
246
|
+
</label>
|
|
247
|
+
}
|
|
248
|
+
className="mb-5"
|
|
249
|
+
onChange={() => this.handleCheckboxUpdate(1)}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
</Message>
|
|
253
|
+
</>
|
|
254
|
+
{checklist.length > 0 && (
|
|
255
|
+
<>
|
|
256
|
+
<strong>{i18next.t("Record deletion checklist:")}</strong>
|
|
257
|
+
<Table basic="very" unstackable className="mt-0">
|
|
258
|
+
<TableHeader>
|
|
259
|
+
<TableRow>
|
|
260
|
+
<TableHeaderCell />
|
|
261
|
+
<TableHeaderCell>{i18next.t("Yes")}</TableHeaderCell>
|
|
262
|
+
<TableHeaderCell>{i18next.t("No")}</TableHeaderCell>
|
|
263
|
+
</TableRow>
|
|
264
|
+
</TableHeader>
|
|
265
|
+
<TableBody>
|
|
266
|
+
{checklist.map((row, index) => (
|
|
267
|
+
<DeletionRadioGroup
|
|
268
|
+
index={index}
|
|
269
|
+
row={row}
|
|
270
|
+
key={row.name}
|
|
271
|
+
state={checklistState}
|
|
272
|
+
onStateChange={this.handleRadioUpdate}
|
|
273
|
+
/>
|
|
274
|
+
))}
|
|
275
|
+
</TableBody>
|
|
276
|
+
</Table>
|
|
277
|
+
{messages.length > 0 && (
|
|
278
|
+
<Message info>
|
|
279
|
+
{messages.length === 1 ? (
|
|
280
|
+
<p dangerouslySetInnerHTML={{ __html: messages[0] }} />
|
|
281
|
+
) : (
|
|
282
|
+
<Message.List>
|
|
283
|
+
{messages.map((message) => (
|
|
284
|
+
<Message.Item key={message}>
|
|
285
|
+
<p dangerouslySetInnerHTML={{ __html: message }} />
|
|
286
|
+
</Message.Item>
|
|
287
|
+
))}
|
|
288
|
+
</Message.List>
|
|
289
|
+
)}
|
|
290
|
+
</Message>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
293
|
+
)}
|
|
294
|
+
<SelectField
|
|
295
|
+
fluid
|
|
296
|
+
fieldPath="reason"
|
|
297
|
+
name="reason"
|
|
298
|
+
label={i18next.t("I want to delete this record because:")}
|
|
299
|
+
options={options}
|
|
300
|
+
required
|
|
301
|
+
disabled={formDisabled}
|
|
302
|
+
/>
|
|
303
|
+
|
|
304
|
+
{!immediateDeletionAllowed && (
|
|
305
|
+
<TextAreaField
|
|
306
|
+
fieldPath="comment"
|
|
307
|
+
name="comment"
|
|
308
|
+
label={i18next.t("Detailed justification:")}
|
|
309
|
+
required
|
|
310
|
+
placeholder={i18next.t(
|
|
311
|
+
"Your justification will not be shared publicly"
|
|
312
|
+
)}
|
|
313
|
+
disabled={formDisabled}
|
|
314
|
+
/>
|
|
315
|
+
)}
|
|
316
|
+
{error && (
|
|
317
|
+
<ErrorMessage
|
|
318
|
+
header={i18next.t("Unable to request deletion.")}
|
|
319
|
+
content={i18next.t(error)}
|
|
320
|
+
icon="exclamation"
|
|
321
|
+
className="text-align-left"
|
|
322
|
+
negative
|
|
323
|
+
/>
|
|
324
|
+
)}
|
|
325
|
+
</ModalContent>
|
|
326
|
+
<ModalActions>
|
|
327
|
+
<Button
|
|
328
|
+
onClick={this.handleClose}
|
|
329
|
+
content={i18next.t("Close")}
|
|
330
|
+
floated="left"
|
|
331
|
+
/>
|
|
332
|
+
<Button
|
|
333
|
+
content={deletionButtonText}
|
|
334
|
+
className={deletionButtonClass}
|
|
335
|
+
icon="trash alternate outline"
|
|
336
|
+
type="submit"
|
|
337
|
+
onClick={(event) => handleSubmit(event)}
|
|
338
|
+
loading={loading}
|
|
339
|
+
disabled={formDisabled || loading}
|
|
340
|
+
/>
|
|
341
|
+
</ModalActions>
|
|
342
|
+
</Form>
|
|
343
|
+
)}
|
|
344
|
+
</Formik>
|
|
345
|
+
</Modal>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
DeletionModal.propTypes = {
|
|
351
|
+
record: PropTypes.object.isRequired,
|
|
352
|
+
open: PropTypes.bool.isRequired,
|
|
353
|
+
handleClose: PropTypes.func.isRequired,
|
|
354
|
+
recordDeletion: PropTypes.object.isRequired,
|
|
355
|
+
options: PropTypes.array.isRequired,
|
|
356
|
+
};
|