datatracker/ietf/static/ietf/js/edit-meeting-schedule.js
Ole Laursen 45ed2c5a2c Add support in the new meeting schedule editor for making a tombstone
session when rescheduling a session after the schedule is made the
official meeting schedule.

Show both cancelled and rescheduled sessions as tombstones in the new
meeting schedule editor.

Add support for showing rescheduled tombstones in the meeting agenda
views.

Adjust the Secretariat session tool so that it's not possible to
(re)cancel cancelled or rescheduled tombstones.
 - Legacy-Id: 18108
2020-06-30 16:55:24 +00:00

446 lines
16 KiB
JavaScript

jQuery(document).ready(function () {
let content = jQuery(".edit-meeting-schedule");
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let sessions = content.find(".session").not(".tombstone");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
// hack to work around lack of position sticky support in old browsers, see https://caniuse.com/#feat=css-sticky
if (content.find(".scheduling-panel").css("position") != "sticky") {
content.find(".scheduling-panel").css("position", "fixed");
content.css("padding-bottom", "14em");
}
function findTimeslotsOverlapping(intervals) {
let res = [];
timeslots.each(function () {
var timeslot = jQuery(this);
let start = timeslot.data("start");
let end = timeslot.data("end");
for (let i = 0; i < intervals.length; ++i) {
if (end >= intervals[i][0] && intervals[i][1] >= start) {
res.push(timeslot);
break;
}
}
});
return res;
}
// selecting
function selectSessionElement(element) {
if (element) {
sessions.not(element).removeClass("selected");
jQuery(element).addClass("selected");
showConstraintHints(element);
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
sessionInfoContainer.html(jQuery(element).find(".session-info").html());
sessionInfoContainer.find("[data-original-title]").tooltip();
sessionInfoContainer.find(".time").text(jQuery(element).closest(".timeslot").data('scheduledatlabel'));
sessionInfoContainer.find(".other-session").each(function () {
let scheduledAt = sessions.filter("#session" + this.dataset.othersessionid).closest(".timeslot").data('scheduledatlabel');
let timeElement = jQuery(this).find(".time");
if (scheduledAt)
timeElement.text(timeElement.data("scheduled").replace("{time}", scheduledAt));
else
timeElement.text(timeElement.data("notscheduled"));
});
}
else {
sessions.removeClass("selected");
showConstraintHints();
content.find(".scheduling-panel .session-info-container").html("");
}
}
function showConstraintHints(selectedSession) {
let sessionId = selectedSession ? selectedSession.id.slice("session".length) : null;
// hints on the sessions
sessions.find(".constraints > span").each(function () {
if (!sessionId) {
jQuery(this).removeClass("would-violate-hint");
return;
}
let sessionIds = this.dataset.sessions;
if (!sessionIds)
return;
let wouldViolate = sessionIds.split(",").indexOf(sessionId) != -1;
jQuery(this).toggleClass("would-violate-hint", wouldViolate);
});
// hints on timeslots
timeslots.removeClass("would-violate-hint");
if (selectedSession) {
let intervals = [];
timeslots.filter(":has(.session .constraints > span.would-violate-hint)").each(function () {
intervals.push([this.dataset.start, this.dataset.end]);
});
let overlappingTimeslots = findTimeslotsOverlapping(intervals);
for (let i = 0; i < overlappingTimeslots.length; ++i)
overlappingTimeslots[i].addClass("would-violate-hint");
// check room sizes
let attendees = +selectedSession.dataset.attendees;
if (attendees) {
timeslots.not(".would-violate-hint").each(function () {
if (attendees > +jQuery(this).closest(".timeslots").data("roomcapacity"))
jQuery(this).addClass("would-violate-hint");
});
}
}
}
content.on("click", function (event) {
if (jQuery(event.target).is(".session-info-container") || jQuery(event.target).closest(".session-info-container").length > 0)
return;
selectSessionElement(null);
});
sessions.on("click", function (event) {
event.stopPropagation();
selectSessionElement(this);
});
if (ietfData.can_edit) {
// dragging
sessions.on("dragstart", function (event) {
event.originalEvent.dataTransfer.setData("text/plain", this.id);
jQuery(this).addClass("dragging");
selectSessionElement(this);
});
sessions.on("dragend", function () {
jQuery(this).removeClass("dragging");
});
sessions.prop('draggable', true);
// dropping
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
dropElements.on('dragenter', function (event) {
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
return;
event.preventDefault(); // default action is signalling that this is not a valid target
jQuery(this).parent().addClass("dropping");
});
dropElements.on('dragover', function (event) {
// we don't actually need this event, except we need to signal
// that this is a valid drop target, by cancelling the default
// action
event.preventDefault();
});
dropElements.on('dragleave', function (event) {
// skip dragleave events if they are to children
if (event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget))
return;
jQuery(this).parent().removeClass("dropping");
});
dropElements.on('drop', function (event) {
let dropElement = jQuery(this);
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") {
dropElement.parent().removeClass("dropping");
return;
}
let sessionElement = sessions.filter("#" + sessionId);
if (sessionElement.length == 0) {
dropElement.parent().removeClass("dropping");
return;
}
event.preventDefault(); // prevent opening as link
let dragParent = sessionElement.parent();
if (dragParent.is(this)) {
dropElement.parent().removeClass("dropping");
return;
}
let dropParent = dropElement.parent();
function failHandler(xhr, textStatus, error) {
dropElement.parent().removeClass("dropping");
console.log("xhr", xhr)
console.log("textstatus", textStatus)
console.log("error", error)
reportServerError(xhr, textStatus, error);
}
function done(response) {
dropElement.parent().removeClass("dropping");
if (!response.success) {
reportServerError(null, null, response);
return;
}
dropElement.append(sessionElement); // move element
if (response.tombstone)
dragParent.append(response.tombstone);
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
sortUnassigned();
}
if (dropParent.hasClass("unassigned-sessions")) {
jQuery.ajax({
url: ietfData.urls.assign,
method: "post",
timeout: 5 * 1000,
data: {
action: "unassign",
session: sessionId.slice("session".length)
}
}).fail(failHandler).done(done);
}
else {
jQuery.ajax({
url: ietfData.urls.assign,
method: "post",
data: {
action: "assign",
session: sessionId.slice("session".length),
timeslot: dropParent.attr("id").slice("timeslot".length)
},
timeout: 5 * 1000
}).fail(failHandler).done(done);
}
});
}
// hints for the current schedule
function updateSessionConstraintViolations() {
// do a sweep on sessions sorted by start time
let scheduledSessions = [];
sessions.each(function () {
let timeslot = jQuery(this).closest(".timeslot");
if (timeslot.length == 1)
scheduledSessions.push({
start: timeslot.data("start"),
end: timeslot.data("end"),
id: this.id.slice("session".length),
element: jQuery(this),
timeslot: timeslot.get(0)
});
});
scheduledSessions.sort(function (a, b) {
if (a.start < b.start)
return -1;
if (a.start > b.start)
return 1;
return 0;
});
let currentlyOpen = {};
let openedIndex = 0;
for (let i = 0; i < scheduledSessions.length; ++i) {
let s = scheduledSessions[i];
// prune
for (let sessionIdStr in currentlyOpen) {
if (currentlyOpen[sessionIdStr].end <= s.start)
delete currentlyOpen[sessionIdStr];
}
// expand
while (openedIndex < scheduledSessions.length && scheduledSessions[openedIndex].start < s.end) {
let toAdd = scheduledSessions[openedIndex];
currentlyOpen[toAdd.id] = toAdd;
++openedIndex;
}
// check for violated constraints
s.element.find(".constraints > span").each(function () {
let sessionIds = this.dataset.sessions;
let violated = sessionIds && sessionIds.split(",").filter(function (v) {
return (v != s.id
&& v in currentlyOpen
// ignore errors within the same timeslot
// under the assumption that the sessions
// in the timeslot happen sequentially
&& s.timeslot != currentlyOpen[v].timeslot);
}).length > 0;
jQuery(this).toggleClass("violated-hint", violated);
});
}
}
function updateTimeSlotDurationViolations() {
timeslots.each(function () {
let total = 0;
jQuery(this).find(".session").each(function () {
total += +jQuery(this).data("duration");
});
jQuery(this).toggleClass("overfull", total > +jQuery(this).data("duration"));
});
}
function updateAttendeesViolations() {
sessions.each(function () {
let roomCapacity = jQuery(this).closest(".timeslots").data("roomcapacity");
if (roomCapacity && this.dataset.attendees)
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
});
}
function updateCurrentSchedulingHints() {
updateSessionConstraintViolations();
updateAttendeesViolations();
updateTimeSlotDurationViolations();
}
updateCurrentSchedulingHints();
// sorting unassigned
function sortArrayWithKeyFunctions(array, keyFunctions) {
function compareArrays(a, b) {
for (let i = 1; i < a.length; ++i) {
let ai = a[i];
let bi = b[i];
if (ai > bi)
return 1;
else if (ai < bi)
return -1;
}
return 0;
}
let arrayWithSortKeys = array.map(function (a) {
let res = [a];
for (let i = 0; i < keyFunctions.length; ++i)
res.push(keyFunctions[i](a));
return res;
});
arrayWithSortKeys.sort(compareArrays);
return arrayWithSortKeys.map(function (l) {
return l[0];
});
}
function sortUnassigned() {
let sortBy = content.find("select[name=sort_unassigned]").val();
function extractId(e) {
return e.id.slice("session".length);
}
function extractName(e) {
return e.querySelector(".session-label").innerHTML;
}
function extractParent(e) {
return e.querySelector(".session-parent").innerHTML;
}
function extractDuration(e) {
return +e.dataset.duration;
}
function extractComments(e) {
return e.querySelector(".session-info .comments") ? 0 : 1;
}
let keyFunctions = [];
if (sortBy == "name")
keyFunctions = [extractName, extractDuration, extractId];
else if (sortBy == "parent")
keyFunctions = [extractParent, extractName, extractDuration, extractId];
else if (sortBy == "duration")
keyFunctions = [extractDuration, extractParent, extractName, extractId];
else if (sortBy == "comments")
keyFunctions = [extractComments, extractParent, extractName, extractDuration, extractId];
let unassignedSessionsContainer = content.find(".unassigned-sessions .drop-target");
let sortedSessions = sortArrayWithKeyFunctions(unassignedSessionsContainer.children(".session").toArray(), keyFunctions);
for (let i = 0; i < sortedSessions.length; ++i)
unassignedSessionsContainer.append(sortedSessions[i]);
}
content.find("select[name=sort_unassigned]").on("change click", function () {
sortUnassigned();
});
sortUnassigned();
// toggling visible sessions by session parents
let sessionParentInputs = content.find(".session-parent-toggles input");
function updateSessionParentToggling() {
let checked = [];
sessionParentInputs.filter(":checked").each(function () {
checked.push(".parent-" + this.value);
});
sessions.not(".untoggleable").filter(checked.join(",")).show();
sessions.not(".untoggleable").not(checked.join(",")).hide();
}
sessionParentInputs.on("click", updateSessionParentToggling);
updateSessionParentToggling();
// toggling visible timeslots
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
function updateTimeslotGroupToggling() {
let checked = [];
timeslotGroupInputs.filter(":checked").each(function () {
checked.push("." + this.value);
});
timeslots.filter(checked.join(",")).removeClass("hidden");
timeslots.not(checked.join(",")).addClass("hidden");
days.each(function () {
jQuery(this).toggle(jQuery(this).find(".timeslot:not(.hidden)").length > 0);
});
}
timeslotGroupInputs.on("click change", updateTimeslotGroupToggling);
updateTimeslotGroupToggling();
// session info
content.find(".session-info-container").on("mouseover", ".other-session", function (event) {
sessions.filter("#session" + this.dataset.othersessionid).addClass("highlight");
}).on("mouseleave", ".other-session", function (event) {
sessions.filter("#session" + this.dataset.othersessionid).removeClass("highlight");
});
});