Skip to content

Commit 33446aa

Browse files
palkerecsenyizzacharo
authored andcommitted
feat(vcs): support for new VCS integration (frontend)
This commit only contains the frontend changes. - Added overrides for the VCS templates to make functionality specific to RDM. This involves moving some functions (e.g. showing the DOI) to RDM from the old Invenio GitHub, since they are more RDM specific. - Added a small React app to show the community selection modal when activating a repo, only on instances where communities are required to publish as per the config. - Madde small changes to the CommunitySelectionModal files to support using the modal without a record.
1 parent 685824c commit 33446aa

File tree

13 files changed

+465
-8
lines changed

13 files changed

+465
-8
lines changed

invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/components/CommunitySelectionModal/CommunityListItem.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const CommunityListItem = ({ result, record, isInitialSubmission }) => {
2727
const itemSelected = getChosenCommunity()?.id === result.id;
2828
const userMembership = userCommunitiesMemberships[result["id"]];
2929
const invalidPermissionLevel =
30-
record.access.record === "public" && result.access.visibility === "restricted";
30+
record?.access.record === "public" && result.access.visibility === "restricted";
3131
const canSubmitRecord = result.ui.permissions.can_submit_record;
3232
const hasTheme = get(result, "theme.enabled");
3333
const dedicatedUpload = isInitialSubmission && hasTheme;
@@ -126,10 +126,11 @@ export const CommunityListItem = ({ result, record, isInitialSubmission }) => {
126126

127127
CommunityListItem.propTypes = {
128128
result: PropTypes.object.isRequired,
129-
record: PropTypes.object.isRequired,
129+
record: PropTypes.object,
130130
isInitialSubmission: PropTypes.bool,
131131
};
132132

133133
CommunityListItem.defaultProps = {
134134
isInitialSubmission: true,
135+
record: null,
135136
};

invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/components/CommunitySelectionModal/CommunitySelectionModal.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class CommunitySelectionModalComponent extends Component {
2525
this.contextValue = {
2626
setLocalCommunity: this.setCommunity,
2727
getChosenCommunity: this.getChosenCommunity,
28-
userCommunitiesMemberships,
28+
userCommunitiesMemberships: userCommunitiesMemberships ?? {},
2929
displaySelected,
3030
};
3131
}
@@ -117,15 +117,15 @@ CommunitySelectionModalComponent.propTypes = {
117117
chosenCommunity: PropTypes.object,
118118
onCommunityChange: PropTypes.func.isRequired,
119119
trigger: PropTypes.object,
120-
userCommunitiesMemberships: PropTypes.object.isRequired,
120+
userCommunitiesMemberships: PropTypes.object,
121121
extraContentComponents: PropTypes.node,
122122
modalHeader: PropTypes.string,
123123
onModalChange: PropTypes.func,
124124
displaySelected: PropTypes.bool,
125125
modalOpen: PropTypes.bool,
126126
apiConfigs: PropTypes.object,
127127
handleClose: PropTypes.func.isRequired,
128-
record: PropTypes.object.isRequired,
128+
record: PropTypes.object,
129129
isInitialSubmission: PropTypes.bool,
130130
};
131131

@@ -139,6 +139,7 @@ CommunitySelectionModalComponent.defaultProps = {
139139
trigger: undefined,
140140
apiConfigs: undefined,
141141
isInitialSubmission: true,
142+
record: null,
142143
};
143144

144145
const mapStateToProps = (state) => ({

invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/components/CommunitySelectionModal/CommunitySelectionSearch.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export class CommunitySelectionSearch extends Component {
5757
const searchApi = new InvenioSearchApi(selectedSearchApi);
5858
const overriddenComponents = {
5959
[`${selectedAppId}.ResultsList.item`]: parametrize(CommunityListItem, {
60-
record: record,
61-
isInitialSubmission: isInitialSubmission,
60+
record,
61+
isInitialSubmission,
6262
}),
6363
};
6464

@@ -178,7 +178,7 @@ CommunitySelectionSearch.propTypes = {
178178
searchApi: PropTypes.object.isRequired,
179179
}),
180180
}),
181-
record: PropTypes.object.isRequired,
181+
record: PropTypes.object,
182182
isInitialSubmission: PropTypes.bool,
183183
CommunityListItem: PropTypes.elementType,
184184
pagination: PropTypes.bool,
@@ -192,6 +192,7 @@ CommunitySelectionSearch.defaultProps = {
192192
myCommunitiesEnabled: true,
193193
autofocus: true,
194194
CommunityListItem: CommunityListItem,
195+
record: null,
195196
apiConfigs: {
196197
allCommunities: {
197198
initialQueryState: { size: 5, page: 1, sortBy: "bestmatch" },
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// This file is part of InvenioRdmRecords
2+
// Copyright (C) 2026 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+
import React, { useCallback, useEffect, useState } from "react";
7+
import PropTypes from "prop-types";
8+
import { sendEnableDisableRequest } from "./api";
9+
import { CommunitySelectionModalComponent } from "../src/deposit/components/CommunitySelectionModal/CommunitySelectionModal.js";
10+
import { i18next } from "@translations/invenio_rdm_records/i18next";
11+
12+
const getRepositoryItemContainer = (repoSwitchElement) => {
13+
let currentEl = repoSwitchElement.parentElement;
14+
while (!currentEl.classList.contains("repository-item") || currentEl === null) {
15+
currentEl = currentEl.parentElement;
16+
}
17+
return currentEl;
18+
};
19+
20+
export const VCSCommunitiesApp = ({ communityRequiredToPublish }) => {
21+
const [selectedRepoSwitch, setSelectedRepoSwitch] = useState(null);
22+
23+
useEffect(() => {
24+
const repoSwitchElements = document.getElementsByClassName(
25+
"repo-switch-with-communities"
26+
);
27+
28+
const toggleListener = (event) => {
29+
if (communityRequiredToPublish && event.target.checked) {
30+
// If the instance requires communities, we need to ask the user's community of choice
31+
// before sending the request to enable the repo.
32+
setSelectedRepoSwitch(event.target);
33+
return;
34+
}
35+
36+
sendEnableDisableRequest(
37+
event.target.checked,
38+
getRepositoryItemContainer(event.target)
39+
);
40+
};
41+
42+
for (const repoSwitchElement of repoSwitchElements) {
43+
repoSwitchElement.addEventListener("change", toggleListener);
44+
}
45+
46+
return () => {
47+
for (const repoSwitchElement of repoSwitchElements) {
48+
repoSwitchElement.removeEventListener("change", toggleListener);
49+
}
50+
};
51+
}, [communityRequiredToPublish]);
52+
53+
const onCommunitySelect = useCallback(
54+
(community) => {
55+
if (selectedRepoSwitch === null) return;
56+
sendEnableDisableRequest(
57+
true,
58+
getRepositoryItemContainer(selectedRepoSwitch),
59+
community.id
60+
);
61+
setSelectedRepoSwitch(null);
62+
},
63+
[selectedRepoSwitch]
64+
);
65+
66+
const onModalClose = useCallback(() => {
67+
if (selectedRepoSwitch === null) return;
68+
// Uncheck the box so the user can clearly see the repo wasn't enabled
69+
selectedRepoSwitch.checked = false;
70+
setSelectedRepoSwitch(null);
71+
}, [selectedRepoSwitch]);
72+
73+
if (!selectedRepoSwitch) return null;
74+
75+
return (
76+
<CommunitySelectionModalComponent
77+
modalOpen
78+
onCommunityChange={onCommunitySelect}
79+
modalHeader={i18next.t("Select a community for this repository's records")}
80+
onModalChange={onModalClose}
81+
handleClose={onModalClose}
82+
isInitialSubmission
83+
/>
84+
);
85+
};
86+
87+
VCSCommunitiesApp.propTypes = {
88+
communityRequiredToPublish: PropTypes.bool,
89+
};
90+
91+
VCSCommunitiesApp.defaultProps = {
92+
communityRequiredToPublish: false,
93+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// This file is part of InvenioRdmRecords
2+
// Copyright (C) 2026 CERN.
3+
//
4+
// InvenioRDM 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+
import $ from "jquery";
7+
8+
function addResultMessage(element, color, icon, message) {
9+
element.classList.remove("hidden");
10+
element.classList.add(color);
11+
element.querySelector(`.icon`).className = `${icon} small icon`;
12+
element.querySelector(".content").textContent = message;
13+
}
14+
15+
// function from https://www.w3schools.com/js/js_cookies.asp
16+
// We're keeping this old method of accessing cookies for now since the modern async
17+
// CookieStore is not Baseline Widely Available (as of 2026-02, see https://developer.mozilla.org/en-US/docs/Web/API/CookieStore)
18+
function getCookie(cname) {
19+
let name = cname + "=";
20+
let decodedCookie = decodeURIComponent(document.cookie);
21+
let ca = decodedCookie.split(";");
22+
for (let i = 0; i < ca.length; i++) {
23+
let c = ca[i];
24+
while (c.charAt(0) === " ") {
25+
c = c.substring(1);
26+
}
27+
if (c.indexOf(name) === 0) {
28+
return c.substring(name.length, c.length);
29+
}
30+
}
31+
return "";
32+
}
33+
34+
const REQUEST_HEADERS = {
35+
"Content-Type": "application/json",
36+
"X-CSRFToken": getCookie("csrftoken"),
37+
};
38+
39+
export function sendEnableDisableRequest(checked, repo, communityId) {
40+
const input = repo.querySelector("input[data-repo-id]");
41+
const repoId = input.dataset["repoId"];
42+
const provider = input.dataset["provider"];
43+
const switchMessage = repo.querySelector(".repo-switch-message");
44+
45+
let url;
46+
if (checked === true) {
47+
url = `/api/user/vcs/${provider}/repositories/${repoId}/enable`;
48+
if (communityId) {
49+
url += `?community_id=${communityId}`;
50+
}
51+
} else if (checked === false) {
52+
url = `/api/user/vcs/${provider}/repositories/${repoId}/disable`;
53+
}
54+
55+
const request = new Request(url, {
56+
method: "POST",
57+
headers: REQUEST_HEADERS,
58+
});
59+
60+
sendRequest(request);
61+
62+
async function sendRequest(request) {
63+
try {
64+
const response = await fetch(request);
65+
if (response.ok) {
66+
addResultMessage(
67+
switchMessage,
68+
"positive",
69+
"checkmark",
70+
"Repository synced successfully. Please reload the page."
71+
);
72+
setTimeout(function () {
73+
switchMessage.classList.add("hidden");
74+
}, 10000);
75+
} else {
76+
addResultMessage(
77+
switchMessage,
78+
"negative",
79+
"cancel",
80+
`Request failed with status code: ${response.status}`
81+
);
82+
setTimeout(function () {
83+
switchMessage.classList.add("hidden");
84+
}, 5000);
85+
}
86+
} catch (error) {
87+
addResultMessage(
88+
switchMessage,
89+
"negative",
90+
"cancel",
91+
`There has been a problem: ${error}`
92+
);
93+
setTimeout(function () {
94+
switchMessage.classList.add("hidden");
95+
}, 7000);
96+
}
97+
}
98+
}
99+
100+
// DOI badge modal
101+
$(".doi-badge-modal").modal({
102+
selector: {
103+
close: ".close.button",
104+
},
105+
onShow: function () {
106+
const modalId = $(this).attr("id");
107+
const $modalTrigger = $(`#${modalId}-trigger`);
108+
$modalTrigger.attr("aria-expanded", true);
109+
},
110+
onHide: function () {
111+
const modalId = $(this).attr("id");
112+
const $modalTrigger = $(`#${modalId}-trigger`);
113+
$modalTrigger.attr("aria-expanded", false);
114+
},
115+
});
116+
117+
$(".doi-modal-trigger").on("click", function (event) {
118+
const modalId = $(event.target).attr("aria-controls");
119+
$(`#${modalId}.doi-badge-modal`).modal("show");
120+
});
121+
122+
$(".doi-modal-trigger").on("keydown", function (event) {
123+
if (event.key === "Enter") {
124+
const modalId = $(event.target).attr("aria-controls");
125+
$(`#${modalId}.doi-badge-modal`).modal("show");
126+
}
127+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// This file is part of InvenioRdmRecords
2+
// Copyright (C) 2026 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 React from "react";
8+
import ReactDOM from "react-dom";
9+
import "./api";
10+
import { VCSCommunitiesApp } from "./VCSCommunitiesApp";
11+
import { OverridableContext, overrideStore } from "react-overridable";
12+
13+
const domContainer = document.getElementById("invenio-vcs-communities-app");
14+
const communityRequiredToPublish =
15+
domContainer.dataset["communityRequiredToPublish"] === "True";
16+
const overriddenComponents = overrideStore.getAll();
17+
18+
if (domContainer) {
19+
ReactDOM.render(
20+
<OverridableContext.Provider value={overriddenComponents}>
21+
<VCSCommunitiesApp communityRequiredToPublish={communityRequiredToPublish} />
22+
</OverridableContext.Provider>,
23+
domContainer
24+
);
25+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{# -*- coding: utf-8 -*-
2+
3+
This file is part of Invenio.
4+
Copyright (C) 2026 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+
{% from "semantic-ui/invenio_formatter/macros/badges.html" import badges_formats_list %}
11+
12+
{%- macro doi_badge(doi, doi_url, provider_id, provider) %}
13+
{%- block doi_badge scoped %}
14+
{% set image_url = url_for('invenio_vcs_badge.index', provider=provider, repo_provider_id=provider_id, _external=True) %}
15+
<img
16+
role="button"
17+
tabindex="0"
18+
id="modal-{{ provider_id }}-trigger"
19+
aria-controls="modal-{{ provider_id }}"
20+
aria-expanded="false"
21+
class="doi-modal-trigger block m-0"
22+
src="{{ image_url }}"
23+
alt="DOI: {{ doi }}"
24+
/>
25+
26+
<div
27+
id="modal-{{ provider_id }}"
28+
role="dialog"
29+
aria-modal="true"
30+
class="ui modal segments fade doi-badge-modal"
31+
>
32+
<div class="ui segment header">
33+
<h2>{{ _("DOI Badge") }}</h2>
34+
</div>
35+
36+
<div class="ui segment content">
37+
<small>
38+
{{ _("This badge points to the latest released version of your repository. If you want a DOI badge for a specific release, please follow the DOI link for one of the specific releases and view the badge from its record.") }}
39+
</small>
40+
</div>
41+
42+
<div class="ui segment content">
43+
<h3 class="ui small header">{{ _("DOI") }}</h3>
44+
<pre>{{ doi }}</pre>
45+
46+
{{ badges_formats_list(image_url, doi_url) }}
47+
</div>
48+
49+
<div class="ui segment actions">
50+
<button class="ui close button">
51+
{{ _("Close") }}
52+
</button>
53+
</div>
54+
</div>
55+
{%- endblock %}
56+
{%- endmacro %}

0 commit comments

Comments
 (0)