Allows for the hostname to contain multilevel subdomain and still access the same operator server. Consistent with changes made to operator and origin userscript.
733 lines
23 KiB
JavaScript
733 lines
23 KiB
JavaScript
// ==UserScript==
|
|
// @name OSRT Staging Move Drag-n-Drop
|
|
// @namespace openSUSE/openSUSE-release-tools
|
|
// @version 0.2.0
|
|
// @description Provide staging request moving interface on staging dashboard.
|
|
// @author Jimmy Berry
|
|
// @match */project/staging_projects/*
|
|
// @require https://code.jquery.com/jquery-3.3.1.min.js
|
|
// @require https://raw.githubusercontent.com/p34eu/selectables/master/selectables.js
|
|
// @grant none
|
|
// ==/UserScript==
|
|
|
|
// Uses a combination of two sources:
|
|
// - https://www.sitepoint.com/accessible-drag-drop/ (modified slightly)
|
|
// - https://github.com/p34eu/selectables (used directly with abuse of modifier key option)
|
|
|
|
(function()
|
|
{
|
|
// Exclude not usable browsers.
|
|
if (!document.querySelectorAll || !('draggable' in document.createElement('span'))) {
|
|
return;
|
|
}
|
|
|
|
// Ensure user is logged in.
|
|
if (!document.querySelector('#link-to-user-home')) {
|
|
return;
|
|
}
|
|
|
|
// Add explanation of trigger shortcut to legend box.
|
|
var explanation = document.createElement('div');
|
|
explanation.id = 'osrt-explanation';
|
|
explanation.innerText = 'enter move mode';
|
|
explanation.setAttribute('title', 'ctrl + m');
|
|
explanation.onclick = function() {
|
|
initMoveInterface();
|
|
this.onclick = null;
|
|
};
|
|
document.querySelector('#legends').appendChild(explanation);
|
|
|
|
window.onkeyup = function(e) {
|
|
if (e.keyCode == 77 && e.ctrlKey) {
|
|
initMoveInterface();
|
|
}
|
|
}
|
|
|
|
// Include CSS immediately for explanation.
|
|
$('head').append(`<style>
|
|
#osrt-explanation
|
|
{
|
|
padding: 10px;
|
|
background-color: #d9b200;
|
|
color: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#osrt-explanation.osrt-active
|
|
{
|
|
cursor: default;
|
|
}
|
|
|
|
#osrt-summary
|
|
{
|
|
position: sticky;
|
|
z-index: 10000;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%%;
|
|
height: 2em;
|
|
padding-top: 0.7em;
|
|
text-align: center;
|
|
background-color: white;
|
|
}
|
|
|
|
#osrt-summary button
|
|
{
|
|
margin-left: 10px;
|
|
}
|
|
|
|
#osrt-summary progress
|
|
{
|
|
display: none;
|
|
}
|
|
|
|
#osrt-summary.osrt-progress progress
|
|
{
|
|
display: inline;
|
|
}
|
|
|
|
#osrt-summary.osrt-progress span,
|
|
#osrt-summary.osrt-progress button,
|
|
#osrt-summary.osrt-success button
|
|
{
|
|
display: none;
|
|
}
|
|
|
|
#osrt-summary.osrt-failed
|
|
{
|
|
background-color: red;
|
|
color: white;
|
|
}
|
|
|
|
#osrt-summary.osrt-failed span
|
|
{
|
|
display: inline;
|
|
}
|
|
|
|
#osrt-summary.osrt-failed progress
|
|
{
|
|
display: none;
|
|
}
|
|
|
|
#osrt-summary.osrt-success
|
|
{
|
|
background-color: green;
|
|
color: white;
|
|
}
|
|
|
|
/* drop target state */
|
|
[data-draggable="target"][aria-dropeffect="move"]
|
|
{
|
|
border-color: #68b;
|
|
}
|
|
|
|
[data-draggable="target"][aria-dropeffect="move"]:focus,
|
|
[data-draggable="target"][aria-dropeffect="move"].dragover
|
|
{
|
|
box-shadow:0 0 0 1px #fff, 0 0 0 3px #68b;
|
|
}
|
|
|
|
[data-draggable="item"]:focus
|
|
{
|
|
box-shadow: 0 0 0 2px #68b, inset 0 0 0 1px #ddd;
|
|
}
|
|
|
|
[data-draggable="item"][aria-grabbed="true"],
|
|
.color-legend .selected
|
|
{
|
|
background: #8ad;
|
|
color: #fff;
|
|
}
|
|
|
|
.color-legend .moved,
|
|
.osrt-moved
|
|
{
|
|
background: repeating-linear-gradient(
|
|
45deg,
|
|
#606dbc,
|
|
#606dbc 10px,
|
|
#465298 10px,
|
|
#465298 20px
|
|
);
|
|
}
|
|
</style>`);
|
|
|
|
})();
|
|
|
|
var initMoveInterface = function() {
|
|
// Update explanation text and add new legend entries.
|
|
function addLegend(type)
|
|
{
|
|
var listItem = document.createElement('li');
|
|
var span = document.createElement('span');
|
|
span.classList.add(type.toLowerCase());
|
|
listItem.appendChild(span);
|
|
listItem.appendChild(document.createTextNode(type));
|
|
document.querySelector('ul.color-legend').appendChild(listItem);
|
|
}
|
|
|
|
addLegend('Moved');
|
|
addLegend('Selected');
|
|
|
|
var explanation = document.querySelector('#osrt-explanation');
|
|
explanation.innerText = 'drag box around requests or ctrl/shift + click requests to select and drag a request to another staging.';
|
|
explanation.setAttribute('title', 'move mode activated');
|
|
explanation.classList.add('osrt-active');
|
|
|
|
// @resource will not work since served without proper MIME type.
|
|
$.get('https://raw.githubusercontent.com/p34eu/selectables/master/selectables.css', function(data, status) {
|
|
$('head').append('<style>' + data + '</style>');
|
|
});
|
|
|
|
// Mark the drag targets and draggable items.
|
|
// Preferable to use the tr element as target, but mouse events not handled properly.
|
|
// Avoid making expand/collapse links selectable and avoid forcing them to be expanded.
|
|
// The pointer-events changes only seem to work properly from script.
|
|
$('table.staging-dashboard td').attr('data-draggable', 'target');
|
|
$('table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse))').attr('data-draggable', 'item').css('pointer-events', 'all');
|
|
|
|
// Disable mouse events on the request links as that makes it nearly impossible to drag them.
|
|
$('table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse)) a').css('pointer-events', 'none');
|
|
|
|
// Configure selectables to play nice with drag-n-drop code.
|
|
new Selectables({
|
|
elements: 'ul.packages-list li[data-draggable="item"]',
|
|
zone: 'body',
|
|
start: function (e) {
|
|
e.osrtContinue = (e.target.getAttribute('data-draggable') != 'item' &&
|
|
e.target.tagName != 'A' &&
|
|
e.target.tagName != 'LABEL' &&
|
|
!e.target.id.startsWith('osrt-'));
|
|
},
|
|
// Abuse key option by setting the value in start callback whic is run
|
|
// first and the value determines if drag selection is started.
|
|
key: 'osrtContinue',
|
|
onSelect: function (e) {
|
|
addSelection(e);
|
|
},
|
|
onDeselect: function (e) {
|
|
removeSelection(e);
|
|
}
|
|
});
|
|
|
|
function getStaging(item)
|
|
{
|
|
var parent;
|
|
if (item.tagName == 'TD') {
|
|
parent = item.parentElement;
|
|
} else {
|
|
parent = item.parentElement.parentElement.parentElement;
|
|
}
|
|
if (item.parentElement.classList.contains('staging_collapsible')) {
|
|
// An additional layer since in hidden container.
|
|
parent = parent.parentElement;
|
|
}
|
|
return parent.querySelector('div.letter a').innerText;
|
|
}
|
|
|
|
var summary = {};
|
|
function updateSummary()
|
|
{
|
|
var summaryElement = document.querySelector('div#osrt-summary');
|
|
if (!summaryElement) {
|
|
summaryElement = document.createElement('div');
|
|
summaryElement.id = 'osrt-summary';
|
|
summaryElement.appendChild(document.createElement('span'))
|
|
|
|
var button = document.createElement('button');
|
|
button.innerText = 'Apply';
|
|
button.onclick = applyChanges;
|
|
summaryElement.appendChild(button);
|
|
|
|
summaryElement.appendChild(document.createElement('progress'))
|
|
document.body.appendChild(summaryElement);
|
|
}
|
|
|
|
var elements = document.querySelectorAll('.osrt-moved');
|
|
summary = {};
|
|
var staging;
|
|
for (var i = 0; i < elements.length; i++) {
|
|
staging = getStaging(elements[i]);
|
|
if (!isNaN(staging)) {
|
|
staging = 'adi:' + staging;
|
|
}
|
|
if (!(staging in summary)) {
|
|
summary[staging] = [];
|
|
}
|
|
summary[staging].push(elements[i].children[0].innerText.trim());
|
|
}
|
|
|
|
summaryElement.children[0].innerText = elements.length + ' request(s) to move affecting ' + Object.keys(summary).length + ' stagings(s)';
|
|
summaryElement.children[2].setAttribute('max', elements.length);
|
|
}
|
|
|
|
function applyChanges()
|
|
{
|
|
var summaryElement = document.querySelector('div#osrt-summary');
|
|
summaryElement.classList.add('osrt-progress');
|
|
|
|
var user = document.querySelector('#link-to-user-home').innerText.trim();
|
|
var pathParts = window.location.pathname.split('/');
|
|
var project = pathParts[pathParts.length - 1];
|
|
|
|
var data = JSON.stringify({'user': user, 'project': project, 'move': true, 'selection': summary});
|
|
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
|
|
var subdomain = domain_parent.endsWith('suse.de') ? 'tortuga' : 'operator';
|
|
var url = 'https://' + subdomain + '.' + domain_parent + '/staging/select';
|
|
$.post({url: url, data: data, crossDomain: true, xhrFields: {withCredentials: true},
|
|
success: applyChangesSuccess}).fail(applyChangesFailed);
|
|
}
|
|
|
|
function applyChangesSuccess(data)
|
|
{
|
|
// Could provide link to this in UI.
|
|
console.log(data);
|
|
|
|
var summaryElement = document.querySelector('div#osrt-summary');
|
|
summaryElement.classList.add('osrt-complete');
|
|
if (data.trim().endsWith('failed')) {
|
|
applyChangesFailed();
|
|
return;
|
|
}
|
|
|
|
var expected = summaryElement.children[2].getAttribute('max');
|
|
if ((data.match(/\(\d+\/\d+\)/g) || []).length == expected) {
|
|
summaryElement.children[0].innerText = 'Moved ' + expected + ' request(s).';
|
|
summaryElement.children[2].setAttribute('value', expected);
|
|
summaryElement.classList.add('osrt-success');
|
|
summaryElement.classList.remove('osrt-progress');
|
|
|
|
// Could reset UI in a more elegant way.
|
|
reloadShortly();
|
|
return;
|
|
}
|
|
|
|
applyChangesFailed();
|
|
}
|
|
|
|
function applyChangesFailed()
|
|
{
|
|
var summaryElement = document.querySelector('div#osrt-summary');
|
|
summaryElement.children[0].innerText = 'Failed to move requests.';
|
|
summaryElement.classList.add('osrt-failed');
|
|
}
|
|
|
|
function reloadShortly()
|
|
{
|
|
setTimeout(function() { window.location.reload(); }, 3000);
|
|
}
|
|
|
|
//get the collection of draggable targets and add their draggable attribute
|
|
for(var
|
|
targets = document.querySelectorAll('[data-draggable="target"]'),
|
|
len = targets.length,
|
|
i = 0; i < len; i ++)
|
|
{
|
|
targets[i].setAttribute('aria-dropeffect', 'none');
|
|
}
|
|
|
|
//get the collection of draggable items and add their draggable attributes
|
|
for(var
|
|
items = document.querySelectorAll('[data-draggable="item"]'),
|
|
len = items.length,
|
|
i = 0; i < len; i ++)
|
|
{
|
|
items[i].setAttribute('draggable', 'true');
|
|
items[i].setAttribute('aria-grabbed', 'false');
|
|
items[i].setAttribute('tabindex', '0');
|
|
|
|
// OSRT modification: keep track of original staging.
|
|
items[i].setAttribute('data-staging-origin', getStaging(items[i]));
|
|
}
|
|
|
|
//dictionary for storing the selections data
|
|
//comprising an array of the currently selected items
|
|
//a reference to the selected items' owning container
|
|
//and a refernce to the current drop target container
|
|
var selections =
|
|
{
|
|
items : [],
|
|
owner : null,
|
|
droptarget : null
|
|
};
|
|
|
|
//function for selecting an item
|
|
function addSelection(item)
|
|
{
|
|
//if the owner reference is still null, set it to this item's parent
|
|
//so that further selection is only allowed within the same container
|
|
if(!selections.owner)
|
|
{
|
|
selections.owner = item.parentNode;
|
|
}
|
|
|
|
//or if that's already happened then compare it with this item's parent
|
|
//and if they're not the same container, return to prevent selection
|
|
else if(selections.owner != item.parentNode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//set this item's grabbed state
|
|
item.setAttribute('aria-grabbed', 'true');
|
|
|
|
//add it to the items array
|
|
selections.items.push(item);
|
|
}
|
|
|
|
//function for unselecting an item
|
|
function removeSelection(item)
|
|
{
|
|
//reset this item's grabbed state
|
|
item.setAttribute('aria-grabbed', 'false');
|
|
|
|
//then find and remove this item from the existing items array
|
|
for(var len = selections.items.length, i = 0; i < len; i ++)
|
|
{
|
|
if(selections.items[i] == item)
|
|
{
|
|
selections.items.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//function for resetting all selections
|
|
function clearSelections()
|
|
{
|
|
//if we have any selected items
|
|
if(selections.items.length)
|
|
{
|
|
//reset the owner reference
|
|
selections.owner = null;
|
|
|
|
//reset the grabbed state on every selected item
|
|
for(var len = selections.items.length, i = 0; i < len; i ++)
|
|
{
|
|
selections.items[i].setAttribute('aria-grabbed', 'false');
|
|
}
|
|
|
|
//then reset the items array
|
|
selections.items = [];
|
|
}
|
|
}
|
|
|
|
//shorctut function for testing whether a selection modifier is pressed
|
|
function hasModifier(e)
|
|
{
|
|
return (e.ctrlKey || e.metaKey || e.shiftKey);
|
|
}
|
|
|
|
//function for applying dropeffect to the target containers
|
|
function addDropeffects()
|
|
{
|
|
//apply aria-dropeffect and tabindex to all targets apart from the owner
|
|
for(var len = targets.length, i = 0; i < len; i ++)
|
|
{
|
|
if
|
|
(
|
|
targets[i] != selections.owner
|
|
&&
|
|
targets[i].getAttribute('aria-dropeffect') == 'none'
|
|
)
|
|
{
|
|
targets[i].setAttribute('aria-dropeffect', 'move');
|
|
targets[i].setAttribute('tabindex', '0');
|
|
}
|
|
}
|
|
|
|
//remove aria-grabbed and tabindex from all items inside those containers
|
|
for(var len = items.length, i = 0; i < len; i ++)
|
|
{
|
|
if
|
|
(
|
|
items[i].parentNode != selections.owner
|
|
&&
|
|
items[i].getAttribute('aria-grabbed')
|
|
)
|
|
{
|
|
items[i].removeAttribute('aria-grabbed');
|
|
items[i].removeAttribute('tabindex');
|
|
}
|
|
}
|
|
}
|
|
|
|
//function for removing dropeffect from the target containers
|
|
function clearDropeffects()
|
|
{
|
|
//if we have any selected items
|
|
if(selections.items.length)
|
|
{
|
|
//reset aria-dropeffect and remove tabindex from all targets
|
|
for(var len = targets.length, i = 0; i < len; i ++)
|
|
{
|
|
if(targets[i].getAttribute('aria-dropeffect') != 'none')
|
|
{
|
|
targets[i].setAttribute('aria-dropeffect', 'none');
|
|
targets[i].removeAttribute('tabindex');
|
|
}
|
|
}
|
|
|
|
//restore aria-grabbed and tabindex to all selectable items
|
|
//without changing the grabbed value of any existing selected items
|
|
for(var len = items.length, i = 0; i < len; i ++)
|
|
{
|
|
if(!items[i].getAttribute('aria-grabbed'))
|
|
{
|
|
items[i].setAttribute('aria-grabbed', 'false');
|
|
items[i].setAttribute('tabindex', '0');
|
|
}
|
|
else if(items[i].getAttribute('aria-grabbed') == 'true')
|
|
{
|
|
items[i].setAttribute('tabindex', '0');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//shortcut function for identifying an event element's target container
|
|
function getContainer(element)
|
|
{
|
|
do
|
|
{
|
|
if(element.nodeType == 1 && element.getAttribute('aria-dropeffect'))
|
|
{
|
|
return element;
|
|
}
|
|
}
|
|
while(element = element.parentNode);
|
|
|
|
return null;
|
|
}
|
|
|
|
//mousedown event to implement single selection
|
|
document.addEventListener('mousedown', function(e)
|
|
{
|
|
//if the element is a draggable item
|
|
if(e.target.getAttribute('draggable'))
|
|
{
|
|
//clear dropeffect from the target containers
|
|
clearDropeffects();
|
|
|
|
//if the multiple selection modifier is not pressed
|
|
//and the item's grabbed state is currently false
|
|
if
|
|
(
|
|
!hasModifier(e)
|
|
&&
|
|
e.target.getAttribute('aria-grabbed') == 'false'
|
|
)
|
|
{
|
|
//clear all existing selections
|
|
clearSelections();
|
|
|
|
//then add this new selection
|
|
addSelection(e.target);
|
|
}
|
|
}
|
|
|
|
//else [if the element is anything else]
|
|
//and the selection modifier is not pressed
|
|
else if(!hasModifier(e))
|
|
{
|
|
//clear dropeffect from the target containers
|
|
clearDropeffects();
|
|
|
|
//clear all existing selections
|
|
clearSelections();
|
|
}
|
|
|
|
//else [if the element is anything else and the modifier is pressed]
|
|
else
|
|
{
|
|
//clear dropeffect from the target containers
|
|
clearDropeffects();
|
|
}
|
|
|
|
}, false);
|
|
|
|
//mouseup event to implement multiple selection
|
|
document.addEventListener('mouseup', function(e)
|
|
{
|
|
//if the element is a draggable item
|
|
//and the multipler selection modifier is pressed
|
|
if(e.target.getAttribute('draggable') && hasModifier(e))
|
|
{
|
|
//if the item's grabbed state is currently true
|
|
if(e.target.getAttribute('aria-grabbed') == 'true')
|
|
{
|
|
//unselect this item
|
|
removeSelection(e.target);
|
|
|
|
//if that was the only selected item
|
|
//then reset the owner container reference
|
|
if(!selections.items.length)
|
|
{
|
|
selections.owner = null;
|
|
}
|
|
}
|
|
|
|
//else [if the item's grabbed state is false]
|
|
else
|
|
{
|
|
//add this additional selection
|
|
addSelection(e.target);
|
|
}
|
|
}
|
|
|
|
}, false);
|
|
|
|
//dragstart event to initiate mouse dragging
|
|
document.addEventListener('dragstart', function(e)
|
|
{
|
|
//if the element's parent is not the owner, then block this event
|
|
if(selections.owner != e.target.parentNode)
|
|
{
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
//[else] if the multiple selection modifier is pressed
|
|
//and the item's grabbed state is currently false
|
|
if
|
|
(
|
|
hasModifier(e)
|
|
&&
|
|
e.target.getAttribute('aria-grabbed') == 'false'
|
|
)
|
|
{
|
|
//add this additional selection
|
|
addSelection(e.target);
|
|
}
|
|
|
|
//we don't need the transfer data, but we have to define something
|
|
//otherwise the drop action won't work at all in firefox
|
|
//most browsers support the proper mime-type syntax, eg. "text/plain"
|
|
//but we have to use this incorrect syntax for the benefit of IE10+
|
|
e.dataTransfer.setData('text', '');
|
|
|
|
//apply dropeffect to the target containers
|
|
addDropeffects();
|
|
|
|
}, false);
|
|
|
|
//related variable is needed to maintain a reference to the
|
|
//dragleave's relatedTarget, since it doesn't have e.relatedTarget
|
|
var related = null;
|
|
|
|
//dragenter event to set that variable
|
|
document.addEventListener('dragenter', function(e)
|
|
{
|
|
related = e.target;
|
|
|
|
}, false);
|
|
|
|
//dragleave event to maintain target highlighting using that variable
|
|
document.addEventListener('dragleave', function(e)
|
|
{
|
|
//get a drop target reference from the relatedTarget
|
|
var droptarget = getContainer(related);
|
|
|
|
//if the target is the owner then it's not a valid drop target
|
|
if(droptarget == selections.owner)
|
|
{
|
|
droptarget = null;
|
|
}
|
|
|
|
//if the drop target is different from the last stored reference
|
|
//(or we have one of those references but not the other one)
|
|
if(droptarget != selections.droptarget)
|
|
{
|
|
//if we have a saved reference, clear its existing dragover class
|
|
if(selections.droptarget)
|
|
{
|
|
selections.droptarget.className =
|
|
selections.droptarget.className.replace(/ dragover/g, '');
|
|
}
|
|
|
|
//apply the dragover class to the new drop target reference
|
|
if(droptarget)
|
|
{
|
|
droptarget.className += ' dragover';
|
|
}
|
|
|
|
//then save that reference for next time
|
|
selections.droptarget = droptarget;
|
|
}
|
|
|
|
}, false);
|
|
|
|
//dragover event to allow the drag by preventing its default
|
|
document.addEventListener('dragover', function(e)
|
|
{
|
|
//if we have any selected items, allow them to be dragged
|
|
if(selections.items.length)
|
|
{
|
|
e.preventDefault();
|
|
}
|
|
|
|
}, false);
|
|
|
|
//dragend event to implement items being validly dropped into targets,
|
|
//or invalidly dropped elsewhere, and to clean-up the interface either way
|
|
document.addEventListener('dragend', function(e)
|
|
{
|
|
//if we have a valid drop target reference
|
|
//(which implies that we have some selected items)
|
|
if(selections.droptarget)
|
|
{
|
|
// OSRT modification: only move if location is changing.
|
|
if (getStaging(selections.droptarget) == getStaging(selections.items[0])) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// OSRT modification: place requests back in package list.
|
|
var target = selections.droptarget.parentElement.querySelector('ul.packages-list');
|
|
|
|
//append the selected items to the end of the target container
|
|
for(var len = selections.items.length, i = 0; i < len; i ++)
|
|
{
|
|
// OSRT modification: place in package list and determine if moved from origin.
|
|
// selections.droptarget.appendChild(selections.items[i]);
|
|
target.appendChild(selections.items[i]);
|
|
if (getStaging(selections.items[i]) != selections.items[i].getAttribute('data-staging-origin'))
|
|
{
|
|
selections.items[i].classList.add('osrt-moved');
|
|
}
|
|
else {
|
|
selections.items[i].classList.remove('osrt-moved');
|
|
}
|
|
}
|
|
|
|
// OSRT modification: after drag update overall summary of moves.
|
|
updateSummary();
|
|
|
|
//prevent default to allow the action
|
|
e.preventDefault();
|
|
}
|
|
|
|
//if we have any selected items
|
|
if(selections.items.length)
|
|
{
|
|
//clear dropeffect from the target containers
|
|
clearDropeffects();
|
|
|
|
//if we have a valid drop target reference
|
|
if(selections.droptarget)
|
|
{
|
|
//reset the selections array
|
|
clearSelections();
|
|
|
|
//reset the target's dragover class
|
|
selections.droptarget.className =
|
|
selections.droptarget.className.replace(/ dragover/g, '');
|
|
|
|
//reset the target reference
|
|
selections.droptarget = null;
|
|
}
|
|
}
|
|
|
|
}, false);
|
|
};
|