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;
+ }
+
let 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,101 @@
+# 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-web-api-link"
+ title: Software Heritage Web API
+ intro: |
+ That link will take you to the Software Heritage Web API documentation.
+ You will find information about how to use it and description of endpoints.
+ Please note that the Web API can also be queried from your web browser
+ through a dedicated HTML interface displaying query results.
+
+ - element: .swh-vault-link
+ title: Downloads from the vault
+ intro: |
+ That link will take you to your list of downloads from the Software Heritage vault.
+
+ - element: .swh-origin-save-link
+ title: Save code now
+ intro: |
+ That link will take you to the Save code now interface.
+ It enables to submit software origins to save into the archive.
+
+ - element: .swh-help-link
+ title: Launch guided tour
+ intro: Replay that guided tour.
+
+ - title: Browsing source code of an archived software origin
+ intro: |
+ Clicking on Next page will take you to the source code browsing interface where
+ that guided tour will continue.
+
+browseOrigin:
+ - element: "#swh-browse-code-nav-link"
+ title: Browse source code
+ intro: In this tab, you can browse the source code of a software origin.
+ position: bottom
+
+ - element: "#swh-browse-snapshot-branches-nav-link"
+ title: Browse branches
+ intro: In this tab, you can browse the list of branches for a software origin.
+ position: bottom
+
+ - element: "#swh-browse-snapshot-releases-nav-link"
+ title: Browse releases
+ intro: In this tab, you can browse the list of releases for a software origin.
+ position: bottom
+
+ - element: "#swhids-handle"
+ title: Get 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: .swhid-ui
+ title: Copy SWHID for a given browsed object
+ intro: |
+ You can easily copy to clipboard a SWHID or its resolve URL
+ using that dedicated interface.
+ position: left
+
+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,134 @@
+/**
+ * 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';
+
+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/python/cpython`,
+ introJsOptions: {
+ disableInteraction: true,
+ scrollToElement: false,
+ steps: guidedTourSteps.browseOrigin
+ },
+ onBeforeChange: function(targetElement) {
+ // open SWHIDs tab before its tour step
+ if (targetElement.className.indexOf('swhid-ui') !== -1) {
+ if (!$('#swh-identifiers').tabSlideOut('isOpen')) {
+ $('.introjs-helperLayer').hide();
+ $('.introjs-tooltipReferenceLayer').hide();
+ $('#swh-identifiers').tabSlideOut('open');
+ setTimeout(() => {
+ $('.introjs-helperLayer').show();
+ $('.introjs-tooltipReferenceLayer').show();
+ tour.nextStep();
+ }, 500);
+ return false;
+ }
+ }
+ return true;
+ }
+ },
+ {
+ url: `${Urls.browse_origin_content()}?origin_url=https://github.com/python/cpython&path=setup.py`,
+ 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 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')
+ .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);
+ }
+ setTimeout(() => {
+ // run guided tour with a little delay to ensure every asynchronous operations
+ // after page load have been executed
+ 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,16 @@
+/**
+ * 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 {
+ transform: translate(-50%, -200%);
+ margin-left: unset !important;
+ margin-top: unset !important;
+}
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,87 @@
+/**
+ * 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() {
+
+ 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();
+
+ // 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()
+ .then(() => {
+ clickNextStepButtons();
+ });
+ }
+ });
+ };
+
+ cy.url().then(url => {
+ clickNextStepButtons();
+ cy.get('.introjs-nextbutton')
+ .should('have.text', 'Next page')
+ .click();
+ cy.url().should('not.eq', url);
+ });
+
+ });
+
+ it('should stay at first step while line numbers not clicked on content view tour', function() {
+ const guidedTourPageIndex = 2;
+ const origin = this.origin[0];
+ cy.ambassadorLogin();
+ // use a content url available in test archive to avoid 404
+ let url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}`;
+ url += `&path=${origin.content[0].path}`;
+ cy.visit(url);
+ cy.window().then(win => {
+ // override guided tour page url for the test to work
+ const guidedTour = win.swh.guided_tour.getGuidedTour();
+ guidedTour[guidedTourPageIndex].url = url;
+ // 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.39.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"
@@ -133,4 +135,4 @@
"engines": {
"node": ">=12.0.0"
}
-}
\ No newline at end of file
+}
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 %}
-