Improvements to the timeslot and schedule editors (move new toggles to modals, handle overflowing session names, fix timeslot editor scrolling, add buttons to quickly create single timeslot, accept trailing slash on edit URL)

- Legacy-Id: 19449
This commit is contained in:
Jennifer Richards 2021-10-21 14:59:02 +00:00
parent 446ac7a47e
commit 5cbe402036
8 changed files with 185 additions and 76 deletions

View file

@ -54,7 +54,7 @@ type_ietf_only_patterns = [
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules),
url(r'^agenda/new/$', views.new_meeting_schedule),
url(r'^timeslots/edit$', views.edit_timeslots),
url(r'^timeslots/edit/?$', views.edit_timeslots),
url(r'^timeslot/new$', views.create_timeslot),
url(r'^timeslot/(?P<slot_id>\d+)/edit$', views.edit_timeslot),
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),

View file

@ -1239,6 +1239,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
.edit-meeting-schedule .session .session-label {
flex-grow: 1;
margin-left: 0.1em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-meeting-schedule .session .session-label .bof-tag {
@ -1333,20 +1336,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
.edit-meeting-schedule .scheduling-panel .preferences {
margin: 0.5em 0;
display: flex;
align-items: flex-start;
}
.edit-meeting-schedule .scheduling-panel .preferences > div {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.edit-meeting-schedule .scheduling-panel .preferences > div > span {
.edit-meeting-schedule .scheduling-panel .preferences > span {
margin-top: 0;
margin-right: 1em;
white-space: nowrap;
}
.edit-meeting-schedule .sort-unassigned select {
@ -1354,17 +1348,20 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
display: inline-block;
}
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body {
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > div {
margin-bottom: 1.5em;
}
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots {
/*column-count: 3;*/
display: flex;
flex-flow: row wrap;
}
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > * {
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots > * {
margin-right: 1.5em;
}
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body label {
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots label {
display: block;
font-weight: normal;
}

View file

@ -738,13 +738,13 @@ jQuery(document).ready(function () {
timeslots.not(checked.join(",")).addClass('hidden-timeslot-type');
}
if (timeSlotTypeInputs.length > 0) {
timeSlotTypeInputs.on("click", updateTimeSlotTypeToggling);
timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling);
updateTimeSlotTypeToggling();
}
// Toggling session purposes
let sessionPurposeInputs = content.find('.session-purpose-toggles input');
function updateSessionPurposeToggling() {
function updateSessionPurposeToggling(evt) {
let checked = [];
sessionPurposeInputs.filter(":checked").each(function () {
checked.push(".purpose-" + this.value);
@ -754,12 +754,24 @@ jQuery(document).ready(function () {
sessions.not(checked.join(",")).addClass('hidden-purpose');
}
if (sessionPurposeInputs.length > 0) {
sessionPurposeInputs.on("click", updateSessionPurposeToggling);
sessionPurposeInputs.on("change", updateSessionPurposeToggling);
updateSessionPurposeToggling();
content.find('#session-toggles-modal .select-all').get(0).addEventListener(
'click',
function() {
sessionPurposeInputs.prop('checked', true);
updateSessionPurposeToggling();
});
content.find('#session-toggles-modal .clear-all').get(0).addEventListener(
'click',
function() {
sessionPurposeInputs.prop('checked', false);
updateSessionPurposeToggling();
});
}
// toggling visible timeslots
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input");
function updateTimeslotGroupToggling() {
let checked = [];
timeslotGroupInputs.filter(":checked").each(function () {

View file

@ -0,0 +1,43 @@
// Copyright The IETF Trust 2021, All Rights Reserved
/* global URLSearchParams */
(function() {
'use strict';
function initialize() {
const form = document.getElementById('timeslot-form');
if (!form) {
return;
}
const params = new URLSearchParams(document.location.search);
const day = params.get('day');
const date = params.get('date');
const location = params.get('location');
const time = params.get('time');
const duration = params.get('duration');
if (day) {
const inp = form.querySelector('#id_days input[value="' + day +'"]');
if (inp) {
inp.checked = true;
} else if (date) {
const date_field = form.querySelector('#id_other_date');
date_field.value = date;
}
}
if (location) {
const inp = form.querySelector('#id_locations input[value="' + location + '"]');
inp.checked=true;
}
if (time) {
const inp = form.querySelector('input#id_time');
inp.value = time;
}
if (duration) {
const inp = form.querySelector('input#id_duration');
inp.value = duration;
}
}
window.addEventListener('load', initialize);
})();

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2021, All Rights Reserved #}
{% load origin %}
{% load origin static %}
{% load bootstrap3 %}
{% block pagehead %}
@ -12,7 +12,7 @@
{% block content %}
{% origin %}
<h1>Create timeslot for {{meeting}}</h1>
<form method="post">
<form id="timeslot-form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
@ -23,5 +23,6 @@
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/meeting/create_timeslot.js' %}"></script>
{{ form.media.js }}
{% endblock %}

View file

@ -175,48 +175,27 @@
</div>
<div class="preferences">
<div>
<span class="sort-unassigned">
Sort unassigned:
<select name="sort_unassigned" class="form-control">
<option value="name" selected="selected">By name</option>
<option value="parent">By area</option>
<option value="duration">By duration</option>
<option value="comments">Special requests</option>
</select>
</span>
<span class="sort-unassigned">
Sort unassigned:
<select name="sort_unassigned" class="form-control">
<option value="name" selected="selected">By name</option>
<option value="parent">By area</option>
<option value="duration">By duration</option>
<option value="comments">Special requests</option>
</select>
</span>
<span class="timeslot-group-toggles">
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</span>
</div>
<span class="toggle-inputs session-parent-toggles">
Show:
{% for p in session_parents %}
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
{% endfor %}
</span>
<div>
<span class="toggle-inputs session-parent-toggles">
Show:
{% for p in session_parents %}
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
{% endfor %}
</span>
{% if session_purposes|length > 1 %}
<span class="toggle-inputs session-purpose-toggles">
Purpose:
{% for purpose in session_purposes %}
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{{ purpose.slug }}"> {{ purpose }}</label>
{% endfor %}
</span>
{% endif %}
{% if timeslot_types|length > 1 %}
<span class="toggle-inputs timeslot-type-toggles">
Type:
{% for type in timeslot_types %}
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
{% endfor %}
</span>
{% endif %}
</div>
{% if session_purposes|length > 1 %}
<button class="btn btn-default" data-toggle="modal" data-target="#session-toggles-modal"><input type="checkbox" checked="checked" disabled> Sessions</button>
{% endif %}
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</div>
</div>
@ -235,14 +214,52 @@
</div>
<div class="modal-body">
{% for day, t_groups in timeslot_groups %}
<div>
<div><strong>{{ day|date:"M. d" }}</strong></div>
{% for start, end, key in t_groups %}
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
<div class="individual-timeslots">
{% for day, t_groups in timeslot_groups %}
<div>
<div><strong>{{ day|date:"M. d" }}</strong></div>
{% for start, end, key in t_groups %}
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="timeslots-by-type timeslot-type-toggles">
Type:
{% for type in timeslot_types %}
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="session-toggles-modal" class="modal" role="dialog" aria-labelledby="session-toggles-modal-title">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title" id="session-toggles-modal-title">Displayed sessions</h4>
</div>
<div class="modal-body">
<div class="session-purpose-toggles">
{% for purpose in session_purposes %}
<div>
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{{ purpose.slug }}"> {{ purpose }}</label>
</div>
{% endfor %}
<button type="button" class="btn btn-default select-all">Select all</button>
<button type="button" class="btn btn-default clear-all">Clear all</button>
</div>
</div>
<div class="modal-footer">

View file

@ -1,15 +1,33 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load agenda_custom_tags %}
{% load agenda_custom_tags misc_filters %}
{% block title %}IETF {{ meeting.number }} Meeting Agenda: Timeslots and Room Availability{% endblock %}
{% block morecss %}
.tstable { width: 100%;}
{% comment %}
Put scrollbars on the editor table. Requires fixed height so the scroll bars appear on the div, not the page body.
Note that 100vh is viewport height. Using that minus 25rem seems to leave space for the page header/footer.
{% endcomment %}
.timeslot-edit { overflow: auto; height: max(30rem, calc(100vh - 25rem));}
.tstable { width: 100%; border-collapse: separate; } {# "separate" to ensure sticky cells keep their borders #}
.tstable thead { position: sticky; top: 0; z-index: 2; background-color: white;}
.tstable th:first-child, .tstable td:first-child {
background-color: white; {# needs to match the lighter of the striped-table colors! #}
position: sticky;
left: 0;
z-index: 1.5; {# render above other cells / borders but below thead (z-index 2, above) #}
}
.tstable tbody > tr:nth-of-type(odd) > th:first-child {
background-color: rgb(249, 249, 249); {# needs to match the darker of the striped-table colors! #}
}
.tstable th { white-space: nowrap;}
.tstable td { white-space: nowrap;}
.capacity { font-size:80%; font-weight: normal;}
a.new-timeslot-link { color: lightgray; font-size: large;}
{% endblock %}
{% block content %}
@ -90,15 +108,21 @@
<a href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}">Create a timeslot.</a>
</td>
{% else %}
{% for day in time_slices %}
{% for slice in date_slices|lookup:day %}{% with cell_ts=ts_list.popleft %}
{% for day in time_slices %}{% with slots=slot_slices|lookup:day %}
{% for slice in date_slices|lookup:day %}{% with cell_ts=ts_list.popleft slot=slots|index:forloop.counter0 %}
<td class="tscell {% if cell_ts|length > 1 %}timeslot-collision {% endif %}{% for ts in cell_ts %}tstype_{{ ts.type.slug }} {% endfor %}">
{% for ts in cell_ts %}
{% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments only %}
{% endfor %}
{% if cell_ts %}
{% for ts in cell_ts %}
{% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments only %}
{% endfor %}
{% endif %}
<a class="new-timeslot-link {% if cell_ts %}hidden{% endif %}"
href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}?day={{ day.toordinal }}&date={{ day|date:"Y-m-d" }}&location={{ room.pk }}&time={{ slot.time|date:'H:i' }}&duration={{ slot.duration }}">
<span class="fa fa-plus-square"></span>
</a>
{% endwith %}{% endfor %}
</td>
{% endfor %}
{% endwith %}{% endfor %}
{% endif %}
</tr>
{% endfor %}
@ -378,7 +402,12 @@
.error(function(jqXHR, textStatus) {
displayError('Error deleting timeslot: ' + jqXHR.responseText)
})
.done(function () {timeslotElts.forEach(tse => tse.parentNode.removeChild(tse))})
.done(function () {timeslotElts.forEach(
tse => {
tse.closest('td.tscell').querySelector('.new-timeslot-link').classList.remove('hidden');
tse.parentNode.removeChild(tse);
}
)})
.always(function () {deleteModal.modal('hide')});
}

View file

@ -60,3 +60,13 @@ def keep_only(items, arg):
present and truthy. The attribute can be an int or a string.
"""
return [item for item, value in zip(items, list_extract(items, arg)) if value]
@register.filter
def index(container, index):
"""Retrieve item from an indexable container
Usage: {{ item_list|index:forloop.counter0 }}
Returns the index corresponding to the loop counter. (For a literal value,
just use the {{ item_list.0 }} syntax.)
"""
return container[index]