Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const CommunityListItem = ({ result, record, isInitialSubmission }) => {
const itemSelected = getChosenCommunity()?.id === result.id;
const userMembership = userCommunitiesMemberships[result["id"]];
const invalidPermissionLevel =
record.access.record === "public" && result.access.visibility === "restricted";
record?.access.record === "public" && result.access.visibility === "restricted";
const canSubmitRecord = result.ui.permissions.can_submit_record;
const hasTheme = get(result, "theme.enabled");
const dedicatedUpload = isInitialSubmission && hasTheme;
Expand Down Expand Up @@ -126,10 +126,11 @@ export const CommunityListItem = ({ result, record, isInitialSubmission }) => {

CommunityListItem.propTypes = {
result: PropTypes.object.isRequired,
record: PropTypes.object.isRequired,
record: PropTypes.object,
isInitialSubmission: PropTypes.bool,
};

CommunityListItem.defaultProps = {
isInitialSubmission: true,
record: null,
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
this.contextValue = {
setLocalCommunity: this.setCommunity,
getChosenCommunity: this.getChosenCommunity,
userCommunitiesMemberships,
userCommunitiesMemberships: userCommunitiesMemberships ?? {},
displaySelected,
};
}
Expand Down Expand Up @@ -117,15 +117,15 @@
chosenCommunity: PropTypes.object,
onCommunityChange: PropTypes.func.isRequired,
trigger: PropTypes.object,
userCommunitiesMemberships: PropTypes.object.isRequired,
userCommunitiesMemberships: PropTypes.object,

Check warning on line 120 in invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/components/CommunitySelectionModal/CommunitySelectionModal.js

View workflow job for this annotation

GitHub Actions / JS / Tests (22.x)

propType "userCommunitiesMemberships" is not required, but has no corresponding defaultProps declaration
extraContentComponents: PropTypes.node,
modalHeader: PropTypes.string,
onModalChange: PropTypes.func,
displaySelected: PropTypes.bool,
modalOpen: PropTypes.bool,
apiConfigs: PropTypes.object,
handleClose: PropTypes.func.isRequired,
record: PropTypes.object.isRequired,
record: PropTypes.object,
isInitialSubmission: PropTypes.bool,
};

Expand All @@ -139,6 +139,7 @@
trigger: undefined,
apiConfigs: undefined,
isInitialSubmission: true,
record: null,
};

const mapStateToProps = (state) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export class CommunitySelectionSearch extends Component {
const searchApi = new InvenioSearchApi(selectedSearchApi);
const overriddenComponents = {
[`${selectedAppId}.ResultsList.item`]: parametrize(CommunityListItem, {
record: record,
isInitialSubmission: isInitialSubmission,
record,
isInitialSubmission,
}),
};

Expand Down Expand Up @@ -178,7 +178,7 @@ CommunitySelectionSearch.propTypes = {
searchApi: PropTypes.object.isRequired,
}),
}),
record: PropTypes.object.isRequired,
record: PropTypes.object,
isInitialSubmission: PropTypes.bool,
CommunityListItem: PropTypes.elementType,
pagination: PropTypes.bool,
Expand All @@ -192,6 +192,7 @@ CommunitySelectionSearch.defaultProps = {
myCommunitiesEnabled: true,
autofocus: true,
CommunityListItem: CommunityListItem,
record: null,
apiConfigs: {
allCommunities: {
initialQueryState: { size: 5, page: 1, sortBy: "bestmatch" },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// This file is part of InvenioRdmRecords
// Copyright (C) 2026 CERN.
//
// 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 React, { useCallback, useEffect, useState } from "react";
import PropTypes from "prop-types";
import { sendEnableDisableRequest } from "./api";
import { CommunitySelectionModalComponent } from "../src/deposit/components/CommunitySelectionModal/CommunitySelectionModal.js";
import { i18next } from "@translations/invenio_rdm_records/i18next";

const getRepositoryItemContainer = (repoSwitchElement) => {
let currentEl = repoSwitchElement.parentElement;
while (!currentEl.classList.contains("repository-item") || currentEl === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason for having this loop?

Copy link
Copy Markdown
Member Author

@palkerecsenyi palkerecsenyi Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This JS can be rendered in multiple pages. On some pages the .repository-item element is directly 'above' (parent) the repoSwitchElement, but in other pages it is 3 or 4 elements above it. Instead of hardcoding (e.g. repoSwitchElement.parentElement.parentElement.parentElement...) which could break if we change the template structure even slightly, I chose to use a loop to repeatedly go upwards until we find it. The repoSwitchElement is always rendered inside a .repository-item element, and there's the currentEl === null in case we somehow reach the <html> at the top, so this will never be an infinite loop.

currentEl = currentEl.parentElement;
}
return currentEl;
};

export const VCSCommunitiesApp = ({ communityRequiredToPublish }) => {
const [selectedRepoSwitch, setSelectedRepoSwitch] = useState(null);

useEffect(() => {
const repoSwitchElements = document.getElementsByClassName(
"repo-switch-with-communities"
);

const toggleListener = (event) => {
if (communityRequiredToPublish && event.target.checked) {
// If the instance requires communities, we need to ask the user's community of choice
// before sending the request to enable the repo.
setSelectedRepoSwitch(event.target);
return;
}

sendEnableDisableRequest(
event.target.checked,
getRepositoryItemContainer(event.target)
);
};

for (const repoSwitchElement of repoSwitchElements) {
repoSwitchElement.addEventListener("change", toggleListener);
}

return () => {
for (const repoSwitchElement of repoSwitchElements) {
repoSwitchElement.removeEventListener("change", toggleListener);
}
};
}, [communityRequiredToPublish]);

const onCommunitySelect = useCallback(
(community) => {
if (selectedRepoSwitch === null) return;
sendEnableDisableRequest(
true,
getRepositoryItemContainer(selectedRepoSwitch),
community.id
);
setSelectedRepoSwitch(null);
},
[selectedRepoSwitch]
);

const onModalClose = useCallback(() => {
if (selectedRepoSwitch === null) return;
// Uncheck the box so the user can clearly see the repo wasn't enabled
selectedRepoSwitch.checked = false;
setSelectedRepoSwitch(null);
}, [selectedRepoSwitch]);

if (!selectedRepoSwitch) return null;

return (
<CommunitySelectionModalComponent
modalOpen
onCommunityChange={onCommunitySelect}
modalHeader={i18next.t("Select a community for this repository's records")}
onModalChange={onModalClose}
handleClose={onModalClose}
isInitialSubmission
/>
);
};

VCSCommunitiesApp.propTypes = {
communityRequiredToPublish: PropTypes.bool,
};

VCSCommunitiesApp.defaultProps = {
communityRequiredToPublish: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// This file is part of InvenioRdmRecords
// Copyright (C) 2026 CERN.
//
// InvenioRDM 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 $ from "jquery";

function addResultMessage(element, color, icon, message) {
element.classList.remove("hidden");
element.classList.add(color);
element.querySelector(`.icon`).className = `${icon} small icon`;
element.querySelector(".content").textContent = message;
}

// function from https://www.w3schools.com/js/js_cookies.asp
// We're keeping this old method of accessing cookies for now since the modern async
// CookieStore is not Baseline Widely Available (as of 2026-02, see https://developer.mozilla.org/en-US/docs/Web/API/CookieStore)
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

const REQUEST_HEADERS = {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"),
};

export function sendEnableDisableRequest(checked, repo, communityId) {
const input = repo.querySelector("input[data-repo-id]");
const repoId = input.dataset["repoId"];
const provider = input.dataset["provider"];
const switchMessage = repo.querySelector(".repo-switch-message");

let url;
if (checked === true) {
url = `/api/user/vcs/${provider}/repositories/${repoId}/enable`;
if (communityId) {
url += `?community_id=${communityId}`;
}
} else if (checked === false) {
url = `/api/user/vcs/${provider}/repositories/${repoId}/disable`;
}

const request = new Request(url, {
method: "POST",
headers: REQUEST_HEADERS,
});

sendRequest(request);

async function sendRequest(request) {
try {
const response = await fetch(request);
if (response.ok) {
addResultMessage(
switchMessage,
"positive",
"checkmark",
"Repository synced successfully. Please reload the page."
);
setTimeout(function () {
switchMessage.classList.add("hidden");
}, 10000);
} else {
addResultMessage(
switchMessage,
"negative",
"cancel",
`Request failed with status code: ${response.status}`
);
setTimeout(function () {
switchMessage.classList.add("hidden");
}, 5000);
}
} catch (error) {
addResultMessage(
switchMessage,
"negative",
"cancel",
`There has been a problem: ${error}`
);
setTimeout(function () {
switchMessage.classList.add("hidden");
}, 7000);
}
}
}

// DOI badge modal
$(".doi-badge-modal").modal({
selector: {
close: ".close.button",
},
onShow: function () {
const modalId = $(this).attr("id");
const $modalTrigger = $(`#${modalId}-trigger`);
$modalTrigger.attr("aria-expanded", true);
},
onHide: function () {
const modalId = $(this).attr("id");
const $modalTrigger = $(`#${modalId}-trigger`);
$modalTrigger.attr("aria-expanded", false);
},
});

$(".doi-modal-trigger").on("click", function (event) {
const modalId = $(event.target).attr("aria-controls");
$(`#${modalId}.doi-badge-modal`).modal("show");
});

$(".doi-modal-trigger").on("keydown", function (event) {
if (event.key === "Enter") {
const modalId = $(event.target).attr("aria-controls");
$(`#${modalId}.doi-badge-modal`).modal("show");
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This file is part of InvenioRdmRecords
// Copyright (C) 2026 CERN.
//
// 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 React from "react";
import ReactDOM from "react-dom";
import "./api";
import { VCSCommunitiesApp } from "./VCSCommunitiesApp";
import { OverridableContext, overrideStore } from "react-overridable";

const domContainer = document.getElementById("invenio-vcs-communities-app");
const communityRequiredToPublish =
domContainer.dataset["communityRequiredToPublish"] === "True";
const overriddenComponents = overrideStore.getAll();

if (domContainer) {
ReactDOM.render(
<OverridableContext.Provider value={overriddenComponents}>
<VCSCommunitiesApp communityRequiredToPublish={communityRequiredToPublish} />
</OverridableContext.Provider>,
domContainer
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{# -*- coding: utf-8 -*-

This file is part of Invenio.
Copyright (C) 2026 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.
#}

{% from "semantic-ui/invenio_formatter/macros/badges.html" import badges_formats_list %}

{%- macro doi_badge(doi, doi_url, provider_id, provider) %}
{%- block doi_badge scoped %}
{% set image_url = url_for('invenio_vcs_badge.index', provider=provider, repo_provider_id=provider_id, _external=True) %}
<img
role="button"
tabindex="0"
id="modal-{{ provider_id }}-trigger"
aria-controls="modal-{{ provider_id }}"
aria-expanded="false"
class="doi-modal-trigger block m-0"
src="{{ image_url }}"
alt="DOI: {{ doi }}"
/>

<div
id="modal-{{ provider_id }}"
role="dialog"
aria-modal="true"
class="ui modal segments fade doi-badge-modal"
>
<div class="ui segment header">
<h2>{{ _("DOI Badge") }}</h2>
</div>

<div class="ui segment content">
<small>
{{ _("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.") }}
</small>
</div>

<div class="ui segment content">
<h3 class="ui small header">{{ _("DOI") }}</h3>
<pre>{{ doi }}</pre>

{{ badges_formats_list(image_url, doi_url) }}
</div>

<div class="ui segment actions">
<button class="ui close button">
{{ _("Close") }}
</button>
</div>
</div>
{%- endblock %}
{%- endmacro %}
Loading
Loading