diff --git a/swh/web/assets/src/bundles/browse/origin-save.js b/swh/web/assets/src/bundles/browse/origin-save.js index 9444b3c8..bf6e8224 100644 --- a/swh/web/assets/src/bundles/browse/origin-save.js +++ b/swh/web/assets/src/bundles/browse/origin-save.js @@ -1,196 +1,205 @@ /** * Copyright (C) 2018 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 {handleFetchError, csrfPost, isGitRepoUrl} from 'utils/functions'; +import {handleFetchError, csrfPost, isGitRepoUrl, removeUrlFragment} from 'utils/functions'; import {validate} from 'validate.js'; let saveRequestsTable; export function initOriginSave() { $(document).ready(() => { $.fn.dataTable.ext.errMode = 'throw'; fetch(Urls.browse_origin_save_types_list()) .then(response => response.json()) .then(data => { for (let originType of data) { $('#swh-input-origin-type').append(``); } }); saveRequestsTable = $('#swh-origin-save-requests').DataTable({ serverSide: true, ajax: Urls.browse_origin_save_requests_list('all'), columns: [ { data: 'save_request_date', name: 'request_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { data: 'origin_type', name: 'origin_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { return `${data}`; } return data; } }, { data: 'save_request_status', name: 'status' }, { data: 'save_task_status', name: 'loading_task_status', render: (data, type, row) => { if (data === 'succeed') { let browseOriginUrl = Urls.browse_origin(row.origin_url); if (row.visit_date) { browseOriginUrl += `visit/${row.visit_date}/`; } return `${data}`; } return data; } } ], scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']] }); $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => { saveRequestsTable.draw(); + window.location.hash = '#requests'; + }); + + $('#swh-origin-save-request-create-tab').on('shown.bs.tab', () => { + removeUrlFragment(); }); let saveRequestAcceptedAlert = ``; let saveRequestPendingAlert = ``; let saveRequestRejectedAlert = ``; $('#swh-save-origin-form').submit(event => { event.preventDefault(); event.stopPropagation(); $('.alert').alert('close'); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); let originType = $('#swh-input-origin-type').val(); let originUrl = $('#swh-input-origin-url').val(); let addSaveOriginRequestUrl = Urls.browse_origin_save_request(originType, originUrl); let grecaptchaData = {'g-recaptcha-response': grecaptcha.getResponse()}; let headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' }; let body = JSON.stringify(grecaptchaData); csrfPost(addSaveOriginRequestUrl, headers, body) .then(handleFetchError) .then(response => response.json()) .then(data => { if (data.save_request_status === 'accepted') { $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert); } else { $('#swh-origin-save-request-status').html(saveRequestPendingAlert); } grecaptcha.reset(); }) .catch(response => { if (response.status === 403) { $('#swh-origin-save-request-status').css('color', 'red'); $('#swh-origin-save-request-status').html(saveRequestRejectedAlert); } grecaptcha.reset(); }); } else { $(event.target).addClass('was-validated'); } }); $('#swh-show-origin-save-requests-list').on('click', (event) => { event.preventDefault(); $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); }); $('#swh-input-origin-url').on('input', function(event) { let originUrl = $(this).val().trim(); $(this).val(originUrl); $('#swh-input-origin-type option').each(function() { let val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); } }); }); + if (window.location.hash === '#requests') { + $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); + } + }); } export function validateSaveOriginUrl(input) { let validUrl = validate({website: input.value}, { website: { url: { schemes: ['http', 'https', 'svn', 'git'] } } }) === undefined; let originType = $('#swh-input-origin-type').val(); if (originType === 'git' && validUrl) { // additional checks for well known code hosting providers let githubIdx = input.value.indexOf('://github.com'); let gitlabIdx = input.value.indexOf('://gitlab.'); let gitSfIdx = input.value.indexOf('://git.code.sf.net'); let bitbucketIdx = input.value.indexOf('://bitbucket.org'); if (githubIdx !== -1 && githubIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'github.com'); } else if (gitlabIdx !== -1 && gitlabIdx <= 5) { let startIdx = gitlabIdx + 3; let idx = input.value.indexOf('/', startIdx); if (idx !== -1) { let gitlabDomain = input.value.substr(startIdx, idx - startIdx); // GitLab repo url needs to be suffixed by '.git' in order to be successfully loaded validUrl = isGitRepoUrl(input.value, gitlabDomain) && input.value.endsWith('.git'); } else { validUrl = false; } } else if (gitSfIdx !== -1 && gitSfIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'git.code.sf.net/p'); } else if (bitbucketIdx !== -1 && bitbucketIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'bitbucket.org'); } } if (validUrl) { input.setCustomValidity(''); } else { input.setCustomValidity('The origin url is not valid or does not reference a code repository'); } } diff --git a/swh/web/assets/src/bundles/webapp/code-highlighting.js b/swh/web/assets/src/bundles/webapp/code-highlighting.js index c43951df..1502bc02 100644 --- a/swh/web/assets/src/bundles/webapp/code-highlighting.js +++ b/swh/web/assets/src/bundles/webapp/code-highlighting.js @@ -1,113 +1,111 @@ /** * Copyright (C) 2018 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 {removeUrlFragment} from 'utils/functions'; + export async function highlightCode(showLineNumbers = true) { await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // keep track of the first highlighted line let firstHighlightedLine = null; // highlighting color let lineHighlightColor = 'rgb(193, 255, 193)'; // function to highlight a line function highlightLine(i) { let lineTd = $(`.swh-content div[data-line-number="${i}"]`).parent().parent(); lineTd.css('background-color', lineHighlightColor); return lineTd; } - function removeHash() { - history.replaceState('', document.title, window.location.pathname + window.location.search); - } - // function to reset highlighting function resetHighlightedLines() { firstHighlightedLine = null; $('.swh-content tr').css('background-color', 'inherit'); } function scrollToLine(lineDomElt) { if ($(lineDomElt).closest('.swh-content').length > 0) { $('html, body').animate({ scrollTop: $(lineDomElt).offset().top - 70 }, 500); } } // function to highlight lines based on a url fragment // in the form '#Lx' or '#Lx-Ly' function parseUrlFragmentForLinesToHighlight() { let lines = []; let linesRegexp = new RegExp(/L(\d+)/g); let line = linesRegexp.exec(window.location.hash); while (line) { lines.push(parseInt(line[1])); line = linesRegexp.exec(window.location.hash); } resetHighlightedLines(); if (lines.length === 1) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); } else if (lines[0] < lines[lines.length - 1]) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) { highlightLine(i); } } } $(document).ready(() => { // highlight code and add line numbers $('code').each((i, block) => { hljs.highlightBlock(block); if (showLineNumbers) { hljs.lineNumbersBlock(block); } }); if (!showLineNumbers) { return; } // click handler to dynamically highlight line(s) // when the user clicks on a line number (lines range // can also be highlighted while holding the shift key) $('body').click(evt => { if (evt.target.classList.contains('hljs-ln-n')) { let line = parseInt($(evt.target).data('line-number')); if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) { let firstLine = firstHighlightedLine; resetHighlightedLines(); for (let i = firstLine; i <= line; ++i) { highlightLine(i); } firstHighlightedLine = firstLine; window.location.hash = `#L${firstLine}-L${line}`; } else { resetHighlightedLines(); highlightLine(line); window.location.hash = `#L${line}`; scrollToLine(evt.target); } } else if ($(evt.target).closest('.hljs').length) { resetHighlightedLines(); - removeHash(); + removeUrlFragment(); } }); // update lines highlighting when the url fragment changes $(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight()); // schedule lines highlighting if any as hljs.lineNumbersBlock() is async setTimeout(() => { parseUrlFragmentForLinesToHighlight(); }); }); } diff --git a/swh/web/assets/src/utils/functions.js b/swh/web/assets/src/utils/functions.js index 945b1734..c8ce8bd1 100644 --- a/swh/web/assets/src/utils/functions.js +++ b/swh/web/assets/src/utils/functions.js @@ -1,45 +1,49 @@ /** * Copyright (C) 2018 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 */ // utility functions export function handleFetchError(response) { if (!response.ok) { throw response; } return response; } export function handleFetchErrors(responses) { for (let i = 0; i < responses.length; ++i) { if (!responses[i].ok) { throw responses[i]; } } return responses; } export function staticAsset(asset) { return `${__STATIC__}${asset}`; } export function csrfPost(url, headers = {}, body = null) { headers['X-CSRFToken'] = Cookies.get('csrftoken'); return fetch(url, { credentials: 'include', headers: headers, method: 'POST', body: body }); } export function isGitRepoUrl(url, domain) { let endOfPattern = '\\/[\\w\\.-]+\\/?(?!=.git)(?:\\.git(?:\\/?|\\#[\\w\\.\\-_]+)?)?$'; let pattern = `(?:git|https?|git@)(?:\\:\\/\\/)?${domain}[/|:][A-Za-z0-9-]+?` + endOfPattern; let re = new RegExp(pattern); return re.test(url); }; + +export function removeUrlFragment() { + history.replaceState('', document.title, window.location.pathname + window.location.search); +}