diff --git a/assets/config/webpack.config.development.js b/assets/config/webpack.config.development.js
--- a/assets/config/webpack.config.development.js
+++ b/assets/config/webpack.config.development.js
@@ -344,6 +344,11 @@
outputPath: 'img/thirdParty/'
}
}]
+ },
+ {
+ test: /\.ya?ml$/,
+ type: 'json',
+ use: 'yaml-loader'
}
],
// tell webpack to not parse already minified files to speedup build process
diff --git a/assets/src/bundles/browse/swhid-utils.js b/assets/src/bundles/browse/swhid-utils.js
--- a/assets/src/bundles/browse/swhid-utils.js
+++ b/assets/src/bundles/browse/swhid-utils.js
@@ -85,9 +85,15 @@
$('#swh-identifiers').css('width', '1000px');
}
+ // prevent automatic closing of SWHIDs tab during guided tour
+ // as it is displayed programmatically
+ function clickScreenToCloseFilter() {
+ return $('.introjs-overlay').length > 0;
+ }
+
const tabSlideOptions = {
tabLocation: 'right',
- clickScreenToCloseFilters: ['.ui-slideouttab-panel', '.modal'],
+ clickScreenToCloseFilters: [clickScreenToCloseFilter, '.ui-slideouttab-panel', '.modal'],
offset: function() {
const width = $(window).width();
if (width < BREAKPOINT_SM) {
diff --git a/assets/src/bundles/guided_tour/guided-tour-steps.yaml b/assets/src/bundles/guided_tour/guided-tour-steps.yaml
new file mode 100644
--- /dev/null
+++ b/assets/src/bundles/guided_tour/guided-tour-steps.yaml
@@ -0,0 +1,244 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+homepage:
+ - title: Welcome to the guided tour !
+ intro: |
+ This guided tour will showcase Software Heritage web application
+ features in order to help you navigate into the archive
+
+ - title: Homepage
+ intro: |
+ This is the entry point of Software Heritage web application,
+ let's see what we can do from here.
+
+ - element: .swh-search-box
+ title: Search archived software origins
+ intro: |
+ An origin corresponds to a location from which a coherent set of
+ source codes has been obtained, like a git repository, a directory
+ containing tarballs, etc.
+ Software origins are identified by URLs (git clone URLs for instance).
+ That form enables to search for terms in the full set of archived software
+ origin URLs. You will be redirected to a dedicated interface displaying search
+ results. Clicking on an origin URL will then take you to the source code browsing
+ interface. If you enter a complete archived origin URL, you will be immediately
+ redirected to its source code browsing interface.
+
+ - element: .swh-origin-save-link
+ title: Save code now
+ intro: |
+ If you haven't found the software origin you were looking for, you can use the
+ Save Code Now interface to submit a save request that will be immediately processed.
+
+ - element: .swh-vault-link
+ title: Downloads from the vault
+ intro: |
+ Show the list of downloads you requested from the Software Heritage Vault
+ while browsing the archive.
+ Those downloads correspond to tarballs containing source directories
+ archived by Software Heritage.
+ That list of downloads is stored in your browser local storage so it
+ will be persistent across your visits.
+
+ - element: .swh-help-link
+ title: Launch guided tour
+ intro: Replay that guided tour.
+
+ - element: "#swh-login"
+ title: Login or register
+ intro: |
+ Come and join our users community with a Software Heritage account.
+ Click here and register in less than 30 seconds.
+ When authenticated, you can benefit from extended features like a higher
+ rate-limit quota for the Web API.
+ If you are already logged in, that link will take you to your user
+ profile interface where you can generate bearer token for Web API
+ authentication.
+
+ - element: "#swh-web-api-link"
+ title: Software Heritage Web API
+ intro: |
+ In the Software Heritage Web API documentation you will find the complete list
+ of endpoints and how to use each one with a detailed example.
+ Please note that the Web API can also be queried from your web browser
+ through a dedicated HTML interface displaying query results.
+
+ - title: Browsing source code of an archived software origin
+ intro: |
+ Come on in, let's introduce the Web UI to browse the content of an
+ archived software origin.
+
+browseOrigin:
+ - title: Browse source code of an archived software origin
+ intro: |
+ You just arrived into the first view of the archived source code of an origin.
+ The displayed source code files are taken from the most recent snapshot taken by
+ Software Heritage. By default, the content of the HEAD branch is displayed.
+ Continue your journey and dive deeper into the code and its development history.
+
+ - element: "#swh-browse-code-nav-link"
+ title: Browse source code
+ intro: |
+ Here you can browse the source code of a software origin.
+ Clicking on the Code tab will always bring you back to the code in the HEAD branch.
+ position: bottom
+
+ - element: "#swh-browse-snapshot-branches-nav-link"
+ title: Browse branches
+ intro: |
+ Here you can browse the list of branches for a software origin.
+ Links are offered to browse the source code contained in each branch.
+ position: bottom
+
+ - element: "#swh-browse-snapshot-releases-nav-link"
+ title: Browse releases
+ intro: |
+ Here you can browse the list of releases for a software origin.
+ Links are offered to browse the source code contained in each release.
+ Please note that for git origins, only annotated tags are considered as releases.
+ For non annotated git tags, you can browse them in the Branches tab.
+ position: bottom
+
+ - element: "#swh-browse-origin-visits-nav-link"
+ title: Browse origin visits
+ intro: |
+ Here you can find when Software Heritage captured the source code.
+ These visits are called snapshots and visualized in various ways: timeline,
+ calendar and simple list.
+ Like with a way-back machine, you can travel in time and see the code as it was
+ when crawled by Software Heritage.
+ position: bottom
+
+ - element: "#swh-branches-releases-dd"
+ title: Switch between branches and releases
+ intro: |
+ You can easily switch between different branches and releases using this dropdown.
+ position: bottom
+
+ - element: "#swh-breadcrumbs-container"
+ title: Current navigation path
+ intro: |
+ You can see here the current path you are taking in the code, which will make it
+ easier to navigate back.
+ position: bottom
+
+ - element: .swh-tr-link
+ title: Browse revisions history
+ intro: |
+ Display the list of revisions (aka commits) for the current branch in various
+ orderings. Links are offered to browse source code as it was in each revision.
+ The list of files changes introduced in each revision can also be computed and
+ the associated diffs displayed.
+ position: bottom
+
+ - element: .swh-vault-download
+ title: Download source code in an archive
+ intro: |
+ You can request the creation of an archive in .tar.gz format that will contain
+ the currently browsed directory.
+ You can follow the archive creation progress and download it once done by
+ visiting the Downloads page (link can be found in the left sidebar).
+ position: bottom
+
+ - element: "#swh-take-new-snashot"
+ title: Request to save origin again
+ intro: |
+ If the archived software origin currently browsed is not synchronized with its
+ upstream version (for instance when new commits have been issued), you can
+ explicitly request Software Heritage to take a new snapshot of it.
+ position: bottom
+
+ - element: "#swh-tip-revision"
+ title: Branch tip revision
+ intro: |
+ Here you can see the latest revision (commit) archived by Software Heritage
+ for the current branch.
+ position: bottom
+
+ - element: "#swhids-handle"
+ title: Display SWHIDs of browsed objects
+ intro: |
+ When clicking on this handle, a tab will be displayed containing Software Heritage
+ IDentifiers of currently browsed objects.
+ position: left
+
+ - element: "#swh-identifiers"
+ title: Get SWHIDs of browsed objects
+ intro: |
+ In that tab, you can get the SWHIDs of currently browsed objects.
+ Let's see what we can do from here.
+ position: left
+
+ - element: "#swhid-object-types"
+ title: Select archived object type
+ intro: |
+ Software Heritage computes identifiers for all archived objects whose type can be:
+
+ -
+ content: a (specific version of a) file stored in the archive.
+
+ -
+ directory: a set of named pointers to contents (file entries), directories
+ (directory entries) and revisions (revision entries).
+
+ -
+ release: a revision that has been marked as noteworthy with a specific name
+ (e.g., a version number).
+
+ -
+ revision: a point in time snapshot of the content of a directory, together
+ with associated development metadata (e.g., author, timestamp, log message, etc).
+
+ -
+ snapshot: the state of all visible branches during a specific visit of a
+ software origin
+
+
+ Based on the current context, you can get the SWHID of each browsed object in a
+ dedicated tab.
+ position: left
+
+ - element: .swh-badges
+ title: Software Heritage badges
+ intro: |
+ You can include Software Heritage badges in the README file of you code repository
+ to indicate its archival by Software Heritage.
+ Clicking on a badge will show you how to do so depending on your README format.
+
+ - element: .swhid
+ title: Software Heritage IDentifier (SWHID)
+ intro: |
+ Here you can find the SWHID of the selected object.
+ position: left
+
+ - element: "#swhid-options"
+ title: Add / remove qualifiers to SWHID
+ intro: |
+ Toggle the adding of qualifiers to the SWHID which adds extra information regarding
+ the context the object has been found.
+ position: bottom
+
+ - element: "#swhid-copy-buttons"
+ title: Copy SWHID for a given browsed object
+ intro: |
+ You can easily copy to clipboard a SWHID or its permalink using these dedicated
+ buttons.
+ position: bottom
+
+browseContent:
+ - element: .hljs-ln-numbers[data-line-number="1"]
+ title: Highlight a source code line
+ intro: |
+ Click on the line number to highlight the corresponding line of code,
+ then click on Next
+ position: left
+
+ - element: .hljs-ln-numbers[data-line-number="10"]
+ title: Highlight a range of source code lines,
+ intro: |
+ Hold Shift and click on the line number to highlight a range of
+ source code lines
+ position: left
diff --git a/assets/src/bundles/guided_tour/index.js b/assets/src/bundles/guided_tour/index.js
new file mode 100644
--- /dev/null
+++ b/assets/src/bundles/guided_tour/index.js
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2021 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU Affero General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+import * as introJs from 'intro.js';
+import 'intro.js/introjs.css';
+import './swh-introjs.css';
+import guidedTourSteps from './guided-tour-steps.yaml';
+import {disableScrolling, enableScrolling} from 'utils/scrolling';
+
+let guidedTour = [];
+let tour = null;
+
+// init guided tour configuration when page loads in order
+// to hack on it in cypress tests
+$(() => {
+ // tour is defined by an array of objects containing:
+ // - URL of page to run a tour
+ // - intro.js configuration with tour steps
+ // - optional intro.js callback function for tour interactivity
+ guidedTour = [
+ {
+ url: Urls.swh_web_homepage(),
+ introJsOptions: {
+ disableInteraction: true,
+ scrollToElement: false,
+ steps: guidedTourSteps.homepage
+ }
+ },
+ {
+ url: `${Urls.browse_origin_directory()}?origin_url=https://github.com/memononen/libtess2`,
+ introJsOptions: {
+ disableInteraction: true,
+ scrollToElement: false,
+ steps: guidedTourSteps.browseOrigin
+ },
+ onBeforeChange: function(targetElement) {
+ // open SWHIDs tab before its tour step
+ if (targetElement.id === 'swh-identifiers') {
+ if (!$('#swh-identifiers').tabSlideOut('isOpen')) {
+ $('.introjs-helperLayer, .introjs-tooltipReferenceLayer').hide();
+ $('#swh-identifiers').tabSlideOut('open');
+ setTimeout(() => {
+ $('.introjs-helperLayer, .introjs-tooltipReferenceLayer').show();
+ tour.nextStep();
+ }, 500);
+ return false;
+ }
+ }
+ return true;
+ }
+ },
+ {
+ url: `${Urls.browse_origin_content()}?origin_url=https://github.com/memononen/libtess2&path=README.md`,
+ introJsOptions: {
+ steps: guidedTourSteps.browseContent
+ },
+ onBeforeChange: function(targetElement) {
+ // forbid move to next step until user clicks on line numbers
+ if (targetElement.dataset.lineNumber === '10') {
+ const background = $('.hljs-ln-numbers[data-line-number="1"]').css('background-color');
+ return background !== 'rgba(0, 0, 0, 0)';
+ }
+ return true;
+ }
+ }
+ ];
+ // init guided tour on page if guided_tour query parameter is present
+ const searchParams = new URLSearchParams(window.location.search);
+ if (searchParams && searchParams.has('guided_tour')) {
+ initGuidedTour(parseInt(searchParams.get('guided_tour')));
+ }
+});
+
+export function getGuidedTour() {
+ return guidedTour;
+}
+
+export function guidedTourButtonClick(event) {
+ event.preventDefault();
+ initGuidedTour();
+}
+
+export function initGuidedTour(page = 0) {
+ if (page >= guidedTour.length) {
+ return;
+ }
+ const pageUrl = new URL(window.location.origin + guidedTour[page].url);
+ const currentUrl = new URL(window.location.href);
+ const guidedTourNext = currentUrl.searchParams.get('guided_tour_next');
+ currentUrl.searchParams.delete('guided_tour');
+ currentUrl.searchParams.delete('guided_tour_next');
+ const pageUrlStr = decodeURIComponent(pageUrl.toString());
+ const currentUrlStr = decodeURIComponent(currentUrl.toString());
+ if (currentUrlStr !== pageUrlStr) {
+ // go to guided tour page URL if current one does not match
+ pageUrl.searchParams.set('guided_tour', page);
+ if (page === 0) {
+ // user will be redirected to the page he was at the end of the tour
+ pageUrl.searchParams.set('guided_tour_next', currentUrlStr);
+ }
+ window.location = decodeURIComponent(pageUrl.toString());
+ } else {
+ // create intro.js guided tour and configure it
+ tour = introJs().setOptions(guidedTour[page].introJsOptions);
+ tour.setOption('showBullets', false);
+ if (page < guidedTour.length - 1) {
+ // if not on the last page of the tour, rename next button label
+ // and schedule next page loading when clicking on it
+ tour.setOption('doneLabel', 'Next page')
+ .onexit(() => {
+ // re-enable page scrolling when exiting tour
+ enableScrolling();
+ })
+ .oncomplete(() => {
+ const nextPageUrl = new URL(window.location.origin + guidedTour[page + 1].url);
+ nextPageUrl.searchParams.set('guided_tour', page + 1);
+ if (guidedTourNext) {
+ nextPageUrl.searchParams.set('guided_tour_next', guidedTourNext);
+ }
+ window.location.href = decodeURIComponent(nextPageUrl.toString());
+ });
+ } else {
+ tour.oncomplete(() => {
+ if (guidedTourNext) {
+ window.location.href = guidedTourNext;
+ }
+ });
+ }
+ if (guidedTour[page].hasOwnProperty('onBeforeChange')) {
+ tour.onbeforechange(guidedTour[page].onBeforeChange);
+ }
+ window.scrollTo(0, 0);
+ setTimeout(() => {
+ // run guided tour with a little delay to ensure every asynchronous operations
+ // after page load have been executed
+ disableScrolling(); // disable page scrolling with mouse or keyboard while tour runs.
+ tour.start();
+ }, 500);
+ }
+};
diff --git a/assets/src/bundles/guided_tour/swh-introjs.css b/assets/src/bundles/guided_tour/swh-introjs.css
new file mode 100644
--- /dev/null
+++ b/assets/src/bundles/guided_tour/swh-introjs.css
@@ -0,0 +1,18 @@
+/**
+ * Copyright (C) 2021 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU Affero General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+.introjs-tooltip {
+ min-width: 500px;
+}
+
+.introjs-tooltip.introjs-floating {
+ /* center tooltip not attached to a DOM element to the center of the screen */
+ position: fixed !important;
+ top: 50% !important;
+ margin: 0 auto !important;
+ transform: translate(-50%, -50%) !important;
+}
diff --git a/assets/src/utils/scrolling.js b/assets/src/utils/scrolling.js
new file mode 100644
--- /dev/null
+++ b/assets/src/utils/scrolling.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2021 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU Affero General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+// adapted from https://stackoverflow.com/questions/4770025/how-to-disable-scrolling-temporarily
+
+// up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36
+const keys = {38: 1, 40: 1, 32: 1, 33: 1, 34: 1, 35: 1, 36: 1};
+
+function preventDefault(e) {
+ e.preventDefault();
+}
+
+function preventDefaultForScrollKeys(e) {
+ if (keys[e.keyCode]) {
+ preventDefault(e);
+ return false;
+ }
+}
+
+// modern Chrome requires { passive: false } when adding event
+let supportsPassive = false;
+try {
+ window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
+ get: function() { supportsPassive = true; }
+ }));
+} catch (e) {}
+
+const wheelOpt = supportsPassive ? {passive: false} : false;
+const wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
+
+export function disableScrolling() {
+ window.addEventListener('DOMMouseScroll', preventDefault, false); // older FF
+ window.addEventListener(wheelEvent, preventDefault, wheelOpt); // modern desktop
+ window.addEventListener('touchmove', preventDefault, wheelOpt); // mobile
+ window.addEventListener('keydown', preventDefaultForScrollKeys, false);
+}
+
+export function enableScrolling() {
+ window.removeEventListener('DOMMouseScroll', preventDefault, false);
+ window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
+ window.removeEventListener('touchmove', preventDefault, wheelOpt);
+ window.removeEventListener('keydown', preventDefaultForScrollKeys, false);
+}
diff --git a/cypress/integration/guided-tour.spec.js b/cypress/integration/guided-tour.spec.js
new file mode 100644
--- /dev/null
+++ b/cypress/integration/guided-tour.spec.js
@@ -0,0 +1,103 @@
+/**
+ * Copyright (C) 2021 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU Affero General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+describe('Guided Tour Tests', function() {
+
+ // utility function to traverse all guided tour steps in a page
+ const clickNextStepButtons = () => {
+ cy.get('.introjs-nextbutton').then($button => {
+ const buttonText = $button.text();
+ if (buttonText === 'Next') {
+ cy.get('.introjs-nextbutton')
+ .click({force: true})
+ .then(() => {
+ cy.get('.introjs-tooltip').should('be.visible');
+ clickNextStepButtons();
+ });
+ }
+ });
+ };
+
+ it('should start UI guided tour when clicking on help button', function() {
+ cy.ambassadorLogin();
+ cy.visit('/');
+ cy.get('.swh-help-link')
+ .click();
+
+ cy.get('.introjs-tooltip')
+ .should('exist');
+ });
+
+ it('should change guided tour page after current page steps', function() {
+ cy.ambassadorLogin();
+ cy.visit('/');
+
+ cy.get('.swh-help-link')
+ .click();
+
+ cy.url().then(url => {
+ clickNextStepButtons();
+ cy.get('.introjs-nextbutton')
+ .should('have.text', 'Next page')
+ .click();
+ cy.url().should('not.eq', url);
+ });
+
+ });
+
+ it('should automatically open SWHIDs tab on second page of the guided tour', function() {
+ const guidedTourPageIndex = 1;
+ cy.ambassadorLogin();
+ cy.visit('/').window().then(win => {
+ const guidedTour = win.swh.guided_tour.getGuidedTour();
+ // jump to third guided tour page
+ cy.visit(guidedTour[guidedTourPageIndex].url);
+ cy.window().then(win => {
+ // SWHIDs tab should be closed when tour begins
+ cy.get('.ui-slideouttab-open').should('not.exist');
+ // init guided tour on the page
+ win.swh.guided_tour.initGuidedTour(guidedTourPageIndex);
+ clickNextStepButtons();
+ // SWHIDs tab should be opened when tour begins
+ cy.get('.ui-slideouttab-open').should('exist');
+ });
+ });
+ });
+
+ it('should stay at first step while line numbers not clicked on content view tour', function() {
+ const guidedTourPageIndex = 2;
+ cy.ambassadorLogin();
+ // jump to third guided tour page
+ cy.visit('/').window().then(win => {
+ const guidedTour = win.swh.guided_tour.getGuidedTour();
+ cy.visit(guidedTour[guidedTourPageIndex].url);
+ cy.window().then(win => {
+ // init guided tour on the page
+ win.swh.guided_tour.initGuidedTour(guidedTourPageIndex);
+
+ cy.get('.introjs-tooltip-header').then($header => {
+ const headerText = $header.text();
+ // user did not click yet on line numbers and should stay
+ // blocked on first step of the tour
+ cy.get('.introjs-nextbutton')
+ .click();
+ cy.get('.introjs-tooltip-header')
+ .should('have.text', headerText);
+ // click on line numbers
+ cy.get('.hljs-ln-numbers[data-line-number="1"]')
+ .click();
+ // check move to next step is allowed
+ cy.get('.introjs-nextbutton')
+ .click();
+ cy.get('.introjs-tooltip-header')
+ .should('not.have.text', headerText);
+
+ });
+ });
+ });
+ });
+});
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"highlightjs-line-numbers.js": "^2.8.0",
"html-encoder-decoder": "^1.3.9",
"iframe-resizer": "^4.3.2",
+ "intro.js": "^4.1.0",
"jquery": "^3.6.0",
"js-cookie": "^2.2.1",
"js-year-calendar": "^1.0.2",
@@ -114,7 +115,8 @@
"webpack": "^5.41.0",
"webpack-bundle-tracker": "^1.1.0",
"webpack-cli": "^4.7.2",
- "webpack-dev-server": "^3.11.2"
+ "webpack-dev-server": "^3.11.2",
+ "yaml-loader": "^0.6.0"
},
"resolutions": {
"jquery": "^3.6.0"
diff --git a/swh/web/templates/homepage.html b/swh/web/templates/homepage.html
--- a/swh/web/templates/homepage.html
+++ b/swh/web/templates/homepage.html
@@ -24,7 +24,7 @@
{% block content %}
-
+
Search
{% include "includes/origin-search-form.html" %}
@@ -32,7 +32,7 @@
... or check our
-
+
Web API
diff --git a/swh/web/templates/includes/revision-info.html b/swh/web/templates/includes/revision-info.html
--- a/swh/web/templates/includes/revision-info.html
+++ b/swh/web/templates/includes/revision-info.html
@@ -1,12 +1,12 @@
{% comment %}
-Copyright (C) 2020 The Software Heritage developers
+Copyright (C) 2020-2021 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
{% endcomment %}
{% if snapshot_context and snapshot_context.revision_id %}
-
+
diff --git a/swh/web/templates/includes/show-swhids.html b/swh/web/templates/includes/show-swhids.html
--- a/swh/web/templates/includes/show-swhids.html
+++ b/swh/web/templates/includes/show-swhids.html
@@ -1,5 +1,5 @@
{% comment %}
-Copyright (C) 2017-2020 The Software Heritage developers
+Copyright (C) 2017-2021 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
@@ -11,9 +11,9 @@
{% if swhids_info|length > 1 %}
-
Permalinks
+
Permalinks
{% else %}
-
Permalink
+
Permalink
{% endif %}
@@ -28,7 +28,7 @@
Select below a type of object currently browsed in order to display its associated SWHID and permalink.
-
+
{% for swhid_info in swhids_info %}
{% if forloop.first %}
-
@@ -55,16 +55,22 @@
{% endif %}
- {% if snapshot_context and snapshot_context.origin_info %}
-
- {% endif %}
+
+ {% if snapshot_context and snapshot_context.origin_info %}
+
+ {% endif %}
+ {% if swhid_info.object_id %}
+
+ {% endif %}
+
{% if swhid_info.object_id %}
-
-
{{ swhid_info.swhid }}
+
{{ swhid_info.swhid }}
{% endif %}
{% if swhid_info.swhid_with_context is not None %}
@@ -80,7 +86,7 @@
{% endif %}
-