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,155 @@ +# 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 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 did 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: "#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 the dedicated + buttons on the bottom-right. + 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,132 @@ +/** + * 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/memononen/libtess2`, + 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, .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 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,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/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() + .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 %} -
+