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 %} -
+