Bootstrap 5 & remove jQuery (#661)

* build: move frontend assets to Bootstrap 5.3 and Tom Select

* feat: migrate templates and scripts to Bootstrap 5, dropping jQuery

Templates
- Rename Bootstrap 4 classes and data attributes (data-bs-*, ms-/me-/ps-/pe-,
  text-start/end, form-group -> mb-3, input-group wrappers removed, badges,
  custom controls -> form-check/form-select, media -> d-flex, btn-close, ...)
- Dark mode now uses Bootstrap 5.3 colour modes (data-bs-theme on <html>),
  set from <head> to avoid a flash of the light theme
- Remove the Bootstrap 4 /bootstrap/ demo page
- Contextual table classes (table-success etc.) still work as plain
  backgrounds for the rigboard key

JavaScript
- Remove jQuery: src.js/interaction.js and every inline template script are
  rewritten in plain JS on top of small helpers (helpers.js, bundled with
  Bootstrap into jpop.js so inline scripts can use them)
- Replace bootstrap-select/ajax-bootstrap-select with Tom Select. The remote
  search now sends the `q` parameter and reads `text`, which is what the
  API actually uses (the old picker sent `term` and read `label`)
- HTML fragments are fetched with an X-Requested-With header so views keep
  rendering their modal variants, and fetched scripts are re-executed
- Remove the Bootstrap 4 plugin script includes

* fix: stop rigboard header buttons stretching to full width under Bootstrap 5

* fix: restore dark-mode brand colours and contextual table colours under Bootstrap 5

* fix: brighten dark-mode links and make coloured card outlines visible

* fix: calendar layout and grid borders, navbar search button width, guard missing modal links

* fix: readable text in dark-mode Tom Select inputs and placeholders, restore form-control column widths

* build: split the Dockerfile into cached stages with prod and dev targets

* deploy: make compose.yml development-only with live reload, and document it

* fix: keep badge text colour for links inside badges

* fix: restore table-bordered grid lines in dark mode

* fix: include Bootstrap's progress styles and keep progress labels readable
This commit is contained in:
Joe Banks
2026-10-02 16:28:11 +01:00
committed by GitHub
parent 08171767a0
commit a29384a314
124 changed files with 1538 additions and 4137 deletions

View File

@@ -6,11 +6,11 @@
{% block content %}
<div class="row">
<div class="col-12 text-right my-3">
<div class="col-12 text-end my-3">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
class="d-none d-sm-inline">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
</div>
@@ -73,14 +73,14 @@
{% include 'partials/crew_list.html' with event=object.event %}
</div>
<div class="col-12 text-right mt-4">
<div class="col-12 text-end mt-4">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
class="d-none d-sm-inline">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<div class="col-12 text-right">
<div class="col-12 text-end">
{% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div>
{% endblock %}

View File

@@ -18,7 +18,6 @@
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
{% endblock %}
{% block content %}
@@ -44,7 +43,7 @@
<dt class="col-4">Event Size</dt>
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<div class="form-group form-row">
<div class="mb-3 row g-2">
<label for="{{ form.date.id_for_label }}"
class="col-4 col-form-label">{{ form.date.label }}</label>
{% if not form.date.value %}
@@ -53,10 +52,10 @@
{% render_field form.date class+="form-control col-8" %}
{% endif %}
</div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<div class="mb-3 row g-2" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% endif %}
@@ -90,7 +89,7 @@
</div>
</div>
<div class="row mt-3">
<div class="col-sm-12 text-right">
<div class="col-sm-12 text-end">
{% button 'submit' %}
</div>
</div>

View File

@@ -17,9 +17,7 @@
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %}
{% block content %}
@@ -29,11 +27,11 @@
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% if not is_ajax and request.user.pk == event.mic.pk %}
<div class="form-group">
<div class="mb-3">
<label for="{{ form.person.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
<div class="col-sm-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="px-0 ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if form.instance.person %}
<option value="{{ form.instance.person.pk }}" selected="selected">{{ form.instance.person.name }}</option>
{% else %}
@@ -47,16 +45,16 @@
value="{{request.user.pk}}"/>
{% endif %}
{% csrf_token %}
<div class="form-group">
<div class="mb-3">
<label for="{{ form.time.id_for_label }}"
class="col-sm-4 col-form-label">Start Time</label>
<div class="col-sm-8">
{% render_field form.time class+="form-control" %}
</div>
</div>
<div class="form-group">
<div class="mb-3">
<label for="{{ form.role.id_for_label }}" class="col col-form-label">Role</label>
<div class="row pl-3">
<div class="row ps-3">
<div class="col-md-6 col-sm-12">
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_role').value='MIC'">MIC</button>
<button type="button" class="btn btn-danger" onclick="document.getElementById('id_role').value='Power MIC'">Power MIC</button>
@@ -67,9 +65,9 @@
</div>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<label for="{{ form.vehicle.id_for_label }}" class="col col-form-label">Vehicle (if applicable)</label>
<div class="row pl-3">
<div class="row ps-3">
<div class="col-md-6 col-sm-12">
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_vehicle').value='Virgil'"><span class="fas fa-truck-moving"></span> Virgil</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('id_vehicle').value='Virgil + Erms'"><span class="fas fa-trailer"></span><span class="fas fa-truck-moving"></span> Virgil + Erms</button>
@@ -80,7 +78,7 @@
</div>
</div>
{% if edit or manual %}
<div class="form-group">
<div class="mb-3">
<label for="{{ form.end_time.id_for_label }}"
class="col-sm-4 col-form-label">End Time</label>
<div class="col-sm-8">
@@ -90,7 +88,7 @@
{% endif %}
{% if not is_ajax %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
<div class="col-sm-12 text-end">
{% button 'submit' %}
</div>
</div>
@@ -100,7 +98,7 @@
{% endblock %}
{% block footer %}
<div class="col-sm-12 text-right pr-0">
<div class="col-sm-12 text-end pe-0">
<button type="submit" class="btn btn-primary" title="Save" form="checkin"
><span class="fas fa-save align-middle"></span> <span class="d-none d-sm-inline align-middle">Save</span></button>
</div>

View File

@@ -42,7 +42,7 @@
<br/>
{% endfor %}
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create</span></a>
class="d-none d-sm-inline">Create</span></a>
</td>
</tr>
{% empty %}

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col-12 text-right my-3">
<div class="col-12 text-end my-3">
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
@@ -164,13 +164,13 @@
{% endif %}
</div>
</div>
<div class="col-12 text-right">
<div class="col-12 text-end">
{% button 'print' 'pt_print' object.pk %}
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
</div>
<div class="col-12 text-right">
<div class="col-12 text-end">
{% include 'partials/last_edited.html' with target="powertestrecord_history" %}
</div>
{% endblock %}

View File

@@ -17,8 +17,6 @@
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %}
{% block content %}
@@ -48,19 +46,19 @@
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<hr>
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<div class="mb-3 row g-2" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="ts-select col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<div class="mb-3 row g-2" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="ts-select col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% endif %}
@@ -190,7 +188,7 @@
</div>
{% endif %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
<div class="col-sm-12 text-end">
{% button 'submit' %}
</div>
</div>

View File

@@ -165,7 +165,7 @@
</div>
</div>
</div>
<div class="col-12 text-right">
<div class="col-12 text-end">
{% button 'print' 'ra_print' object.pk %}
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>

View File

@@ -13,27 +13,35 @@
{% endblock %}
{% block js %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
function parseBool(str) {
return str.toLowerCase() == 'true';
}
$('input[type=radio][name=big_power]').change(function() {
$('#{{ form.power_mic.id_for_label }}').prop('required', parseBool(this.value));
function onRadioChange(names, handler) {
names.forEach(function (name) {
document.querySelectorAll('input[type=radio][name=' + name + ']').forEach(function (radio) {
radio.addEventListener('change', handler);
});
});
}
onRadioChange(['big_power'], function() {
document.getElementById('{{ form.power_mic.id_for_label }}').required = parseBool(this.value);
});
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
//$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
onRadioChange(['outside', 'generators', 'other_companies_power', 'nonstandard_equipment_power', 'multiple_electrical_environments'], function() {
document.getElementById('{{ form.power_notes.id_for_label }}').required = parseBool(this.value);
});
$('input[type=radio][name=special_structures]').change(function() {
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
$('label[for={{ form.persons_responsible_structures.id_for_label }}]').prop('hidden', !parseBool(this.value));
onRadioChange(['special_structures'], function() {
var field = document.getElementById('{{ form.persons_responsible_structures.id_for_label }}');
field.hidden = !parseBool(this.value);
field.required = parseBool(this.value);
document.querySelector('label[for={{ form.persons_responsible_structures.id_for_label }}]').hidden = !parseBool(this.value);
});
$('input[type=radio][name=suspended_structures]').change(function() {
$('#{{ form.rigging_plan.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
$('label[for={{ form.rigging_plan.id_for_label }}]').prop('hidden', !parseBool(this.value));
onRadioChange(['suspended_structures'], function() {
var field = document.getElementById('{{ form.rigging_plan.id_for_label }}');
field.hidden = !parseBool(this.value);
field.required = parseBool(this.value);
document.querySelector('label[for={{ form.rigging_plan.id_for_label }}]').hidden = !parseBool(this.value);
});
</script>
{% endblock %}
@@ -42,13 +50,13 @@
<div class="col">
{% if form.errors %}
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<dl>
{% with form|nice_errors as qq %}
{% for error_name,desc in qq.items %}
{% if error_name == 'General form errors' %}
{{ form.non_field_errors.0|safe }}
<div class="text-right">
<div class="text-end">
{% render_field form.supervisor_consulted class+="form-check-input" form="form" %}<label class="form-check-label" for="{{ form.supervisor_consulted.id_for_label}}">Please confirm you've done so.</label><br>
<button class="btn btn-primary mt-2" type="submit" form="form"><span class="fas fa-thumbs-up"></span> Confirm</button>
</div>
@@ -98,7 +106,7 @@
<label for="{{ form.power_mic.id_for_label }}"
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
<div class="col-6">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if power_mic %}
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
{% endif %}
@@ -174,7 +182,7 @@
</div>
</div>
<div class="row mt-3">
<div class="col-sm-12 text-right">
<div class="col-sm-12 text-end">
<div class="btn-group">
<button type="submit" class="btn btn-primary" title="Save"><i
class="fas fa-save"></i> Save