-
Notifications
You must be signed in to change notification settings - Fork 115
feat(vcs): support for new VCS integration (frontend) #2283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/vcs/VCSCommunitiesApp.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
| 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, | ||
| }; | ||
127 changes: 127 additions & 0 deletions
127
invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/vcs/api.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| } | ||
| }); |
25 changes: 25 additions & 0 deletions
25
invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/vcs/index.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ); | ||
| } |
56 changes: 56 additions & 0 deletions
56
invenio_rdm_records/templates/semantic-ui/invenio_vcs/doi-badge.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 %} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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-itemelement is directly 'above' (parent) therepoSwitchElement, 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. TherepoSwitchElementis always rendered inside a.repository-itemelement, and there's thecurrentEl === nullin case we somehow reach the<html>at the top, so this will never be an infinite loop.