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 =
`
The "save code now" request has been accepted and will be processed as soon as possible.
`;
let saveRequestPendingAlert =
`
The "save code now" request has been put in pending state and may be accepted for processing after manual review.
`;
let saveRequestRejectedAlert =
`
The "save code now" request has been rejected because the reCAPTCHA could not be validated or the provided origin url is blacklisted.
`;
$('#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);
+}