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,306 @@
+# 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-origin-url"
+ title: Software origin URL
+ intro: |
+ Here you can find the URL of the archived software origin.
+ Following that link will always bring you back to the code in the HEAD branch
+ as captured by the latest Software Heritage visit.
+ position: bottom
+
+ - element: "#swh-go-to-origin"
+ title: Visit software origin
+ intro: |
+ You can visit the software origin URL where source code was captured from
+ by following that link.
+ position: bottom
+
+ - element: "#swh-origin-visit"
+ title: Software Heritage origin visit date
+ intro: |
+ Here you can find the date when Software Heritage captured the source code of
+ that origin.
+ Following that link will always bring you back to the code in the HEAD branch
+ as captured by that visit.
+ position: bottom
+
+ - 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
+ for the currently selected Software Heritage visit.
+ 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:
+
+ You need to select the line number before proceeding to
next step.
+
+ You need to select the line numbers range from ${lineNumberStart} + to ${lineNumberEnd} before proceeding to next step. +
`); + } + break; + } + } + return canGoNext; + } + previousElement = targetElement; + 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.setOptions({ + 'exitOnOverlayClick': false, + '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(() => { + enableScrolling(); // re-enable page scrolling when tour is complete + 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 + disableScrolling(); // disable page scrolling with mouse or keyboard while tour runs. + tour.start(); + window.scrollTo(0, 0); + }, 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/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js --- a/assets/src/bundles/webapp/code-highlighting.js +++ b/assets/src/bundles/webapp/code-highlighting.js @@ -7,35 +7,38 @@ import {removeUrlFragment} from 'utils/functions'; -export async function highlightCode(showLineNumbers = true) { +// keep track of the first highlighted line +let firstHighlightedLine = null; +// highlighting color +const lineHighlightColor = 'rgb(193, 255, 193)'; - await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); +// function to highlight a line +export function highlightLine(i, firstHighlighted = false) { + const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`); + lineTd.css('background-color', lineHighlightColor); + if (firstHighlighted) { + firstHighlightedLine = i; + } + return lineTd; +} - // keep track of the first highlighted line - let firstHighlightedLine = null; - // highlighting color - const lineHighlightColor = 'rgb(193, 255, 193)'; +// function to reset highlighting +export function resetHighlightedLines() { + firstHighlightedLine = null; + $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); +} - // function to highlight a line - function highlightLine(i) { - const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`); - lineTd.css('background-color', lineHighlightColor); - return lineTd; +export function scrollToLine(lineDomElt) { + if ($(lineDomElt).closest('.swh-content').length > 0) { + $('html, body').animate({ + scrollTop: $(lineDomElt).offset().top - 70 + }, 500); } +} - // function to reset highlighting - function resetHighlightedLines() { - firstHighlightedLine = null; - $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); - } +export async function highlightCode(showLineNumbers = true) { - function scrollToLine(lineDomElt) { - if ($(lineDomElt).closest('.swh-content').length > 0) { - $('html, body').animate({ - scrollTop: $(lineDomElt).offset().top - 70 - }, 500); - } - } + await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // function to highlight lines based on a url fragment // in the form '#Lx' or '#Lx-Ly' 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,123 @@ +/** + * 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 = (stopAtTitle = null) => { + cy.get('.introjs-nextbutton').then($button => { + const buttonText = $button.text(); + const headerText = $button.parent().siblings('.introjs-tooltip-header').text(); + if (buttonText === 'Next' && headerText.slice(0, -1) !== stopAtTitle) { + cy.get('.introjs-nextbutton') + .click({force: true}) + .then(() => { + cy.get('.introjs-tooltip').should('be.visible'); + clickNextStepButtons(stopAtTitle); + }); + } + }); + }; + + 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 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); + + clickNextStepButtons('Highlight a source code line'); + + 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="11"]') + .click(); + // check move to next step is allowed + cy.get('.introjs-nextbutton') + .click(); + cy.get('.introjs-tooltip-header') + .should('not.have.text', headerText); + }); + + 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="17"]') + .click({shiftKey: true}); + // 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.44.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 %} -