Skip to content

Commit

Permalink
UX/UI: Update job applications filters
Browse files Browse the repository at this point in the history
The new UI has drowdown filters, which do not open a menu when there are
no available filters (e.g. no applications, so no departments). When a
filter is not available, it is hidden from the UI.

Filters are now located in two places:
- quick access from the top bar to the most frequently used filters
- an offcanvas toggled by “Tous les filtres”

There are two forms:
- an HTMX form for the quick access, that reloads the offcanvas form
  along with the results. That keep both forms in sync.
- a regular form, that triggers a page reload, after filters were
  selected from the offcanvas.
In order to keep the state between both forms, hidden fields are added
to each, for the fields they do not show.
  • Loading branch information
hellodeloo authored and francoisfreitag committed Jun 26, 2024
1 parent ea7bd8c commit c15d0d3
Show file tree
Hide file tree
Showing 31 changed files with 1,186 additions and 711 deletions.
10 changes: 5 additions & 5 deletions itou/static/css/itou.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,6 @@
margin-left: 2px;
}

.c-aside-filters .select2-container--bootstrap-5 {
width: auto !important;
display: block !important;
}

/*
Force the display of `.invalid-feedback` for Duet Date Picker with Bootstrap 4.
See also `duet_date_picker.html`.
Expand Down Expand Up @@ -466,3 +461,8 @@ an input field being invalid, generating an uncontrolled red box-shadow. */
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}

/* Fix for form.company and form.job_seeker width */
.w-lg-400px .select2-selection__rendered {
white-space: nowrap !important;
}
46 changes: 39 additions & 7 deletions itou/static/js/htmx_dropdown_filter.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
htmx.onLoad((target) => {
(function() {
const filtersContentId = "offcanvasApplyFiltersContent";
const filtersContent = document.getElementById(filtersContentId);

function fieldHasValue(container) {
return (
container.querySelector('input:checked:not([value=""])')
|| container.querySelector('input[value]:not([type=checkbox]):not([type=radio]):not([value=""])')
|| container.querySelector('duet-date-picker:not([value=""])')
|| container.querySelector("select > option:not([value=''])[selected]")
);
}

function toggleHasSelectedItem() {
const dropdown = this.closest('.dropdown');
this.classList.toggle('has-selected-item', dropdown.querySelector('input:checked:not([value=""])'));
this.classList.toggle('has-selected-item', fieldHasValue(dropdown));
}

function toggleCollapse(sidebar) {
Array.from(sidebar.querySelectorAll(".collapsed")).forEach((collapse) => {
const collapseTarget = document.querySelector(collapse.dataset.bsTarget);
if (fieldHasValue(collapseTarget)) {
bootstrap.Collapse.getOrCreateInstance(collapseTarget, { delay: 0 }).show();
}
});
}

function onLoad(target) {
target.querySelectorAll('.btn-dropdown-filter.dropdown-toggle').forEach((dropdownFilter) => {
dropdownFilter.addEventListener('hide.bs.dropdown', toggleHasSelectedItem);
toggleHasSelectedItem.call(dropdownFilter);
});
if (target.id === filtersContentId) {
toggleCollapse(target);
}
}

if (filtersContent) {
toggleCollapse(filtersContent);
}
target.querySelectorAll('.btn-dropdown-filter.dropdown-toggle').forEach((dropdownFilter) => {
dropdownFilter.addEventListener('hide.bs.dropdown', toggleHasSelectedItem);
toggleHasSelectedItem.call(dropdownFilter);
});
});
htmx.onLoad(onLoad);
})();
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{% load django_bootstrap5 %}


{% if filters_form.criteria %}
<hr>
<fieldset>
<legend>Critères administratifs déclarés</legend>
{% bootstrap_field filters_form.criteria wrapper_class="" %}
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapseCriteria" type="button" aria-expanded="false" aria-controls="collapseCriteria">
Critères administratifs déclarés
</button>
</legend>
<div class="mt-3 collapse" id="collapseCriteria">{% bootstrap_field filters_form.criteria %}</div>
</fieldset>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

<hr>
<fieldset>
<legend>Date d'envoi de la candidature</legend>
{% bootstrap_field filters_form.start_date %}
{% bootstrap_field filters_form.end_date %}
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapseDate" type="button" aria-expanded="false" aria-controls="collapseDate">
Date d'envoi de la candidature
</button>
</legend>
<div class="mt-3 collapse" id="collapseDate">
{% bootstrap_field filters_form.start_date %}
{% bootstrap_field filters_form.end_date %}
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
{% load django_bootstrap5 %}

{% if filters_form.departments %}
<hr>
<fieldset>
{% bootstrap_field filters_form.departments %}
</fieldset>
{% if filters_form.departments and filters_form.fields.departments.choices %}
{% if btn_dropdown_filter|default:False %}
<div class="dropdown">
<button type="button" class="btn btn-dropdown-filter dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
{{ filters_form.departments.label | capfirst }}
</button>
<ul class="dropdown-menu">
{% for choice in filters_form.departments %}
<li class="dropdown-item">
<div class="form-check">
<input id="{{ choice.id_for_label }}-top" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}-top" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<hr>
<fieldset>
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed"
data-bs-toggle="collapse"
data-bs-target="#collapseDepartments"
type="button"
aria-expanded="false"
aria-controls="collapseDepartments">{{ filters_form.departments.label | capfirst }}</button>
</legend>
<div class="my-3 collapse" id="collapseDepartments">
<ul>
{% for choice in filters_form.departments %}
<li>
<div class="form-check">
<input id="{{ choice.id_for_label }}" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
</fieldset>
{% endif %}
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
{% if filters_form.eligibility_validated %}
<hr>
<fieldset>
<legend>Éligibilité IAE</legend>
{% bootstrap_field filters_form.eligibility_validated %}
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed"
data-bs-toggle="collapse"
data-bs-target="#collapseEligibilityIAE"
type="button"
aria-expanded="false"
aria-controls="collapseEligibilityIAE">
{{ filters_form.eligibility_validated.label | capfirst }}
</button>
</legend>
<div class="mt-3 collapse" id="collapseEligibilityIAE">{% bootstrap_field filters_form.eligibility_validated %}</div>
</fieldset>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
{% if filters_form.job_seekers %}
<hr>
<fieldset>
{% bootstrap_field filters_form.job_seekers %}
<legend>
<button class="btn has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapseJobSeekers" type="button" aria-expanded="false" aria-controls="collapseJobSeekers">
{{ filters_form.job_seekers.label | capfirst }}
</button>
</legend>
<div class="mt-3 collapse" id="collapseJobSeekers">
{% bootstrap_field filters_form.job_seekers show_label=False %}
</div>
</fieldset>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<form>
{% comment %}
Do not reload the entire offcanvas with HTMX. Otherwise, STR :
1. Select a filter from the top bar (one with btn_dropdown_filter=True)
2. Quickly (before HTMX results are loaded), open the offcanvas by pressing « Tous les filtres »
Results load, offcanvas panel is oob-replaced by the response content, and initially not visible, leaving only
the viewable backdrop and no filter offcanvas.
{% endcomment %}
<div class="c-offcanvas-filters offcanvas offcanvas-end" tabindex="-1" aria-labelledby="offcanvasApplyFiltersLabel" id="offcanvasApplyFilters">
<div class="offcanvas-header">
<h4 class="mb-0 btn-ico" id="offcanvasApplyFiltersLabel">
<i class="ri-sound-module-fill font-weight-medium" aria-hidden="true"></i>
<span>Filtrer</span>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Fermer"></button>
</div>
{% include "apply/includes/job_applications_filters/offcanvas_body.html" %}
{% include "apply/includes/job_applications_filters/offcanvas_footer.html" %}
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="offcanvas-body" id="offcanvasApplyFiltersContent"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% include "apply/includes/job_applications_filters/status.html" %}
{% if request.user.is_employer or request.user.is_prescriber %}
{% include "apply/includes/job_applications_filters/selected_jobs.html" %}
{% include "apply/includes/job_applications_filters/departments.html" %}
{% include "apply/includes/job_applications_filters/sender.html" %}
{% include "apply/includes/job_applications_filters/criteria.html" %}
{% include "apply/includes/job_applications_filters/eligibility_validated.html" %}
{% include "apply/includes/job_applications_filters/pass.html" %}
{% comment %}
Do not render job_seekers:
- The field is meant to quickly find a job seeker and does not make
much sense combined with others.
- It uses a select2 widget. Overriding the id attribute so that the
field present twice on the page (in the top bar and side bar) is
challenging.
{% endcomment %}
{% for job_seeker_id in filters_form.job_seekers.value %}
<input type="hidden" name="{{ filters_form.job_seekers.name }}" value="{{ job_seeker_id }}">
{% endfor %}
{% endif %}
{% include "apply/includes/job_applications_filters/dates.html" %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="offcanvas-footer gap-3" id="offcanvasApplyFiltersButtons"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
<button class="btn btn-block btn-primary">Voir</button>
{% include "apply/includes/list_reset_filters.html" %}
</div>
14 changes: 11 additions & 3 deletions itou/templates/apply/includes/job_applications_filters/pass.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
{% if filters_form.pass_iae_suspended %}
<hr>
<fieldset>
<legend>Statut du PASS IAE</legend>
{% bootstrap_field filters_form.pass_iae_active wrapper_class="mb-2" %}
{% bootstrap_field filters_form.pass_iae_suspended wrapper_class="" %}
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapsePassIAE" type="button" aria-expanded="false" aria-controls="collapsePassIAE">
Statut du PASS IAE
</button>
</legend>
<div class="my-3 collapse" id="collapsePassIAE">
<ul>
<li>{% bootstrap_field filters_form.pass_iae_active wrapper_class="" %}</li>
<li>{% bootstrap_field filters_form.pass_iae_suspended wrapper_class="" %}</li>
</ul>
</div>
</fieldset>
{% endif %}
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
{% load django_bootstrap5 %}

{% if filters_form.selected_jobs %}
<hr>
<fieldset>
{% bootstrap_field filters_form.selected_jobs %}
</fieldset>
{% if filters_form.selected_jobs and filters_form.selected_jobs.field.choices %}
{% if btn_dropdown_filter|default:False %}
<div class="dropdown">
<button type="button" class="btn btn-dropdown-filter dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
{{ filters_form.selected_jobs.label | capfirst }}
</button>
<ul class="dropdown-menu">
{% for choice in filters_form.selected_jobs %}
<li class="dropdown-item">
<div class="form-check">
<input id="{{ choice.id_for_label }}-top" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}-top" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<hr>
<fieldset>
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed"
data-bs-toggle="collapse"
data-bs-target="#collapseSelectedJob"
type="button"
aria-expanded="false"
aria-controls="collapseSelectedJob">{{ filters_form.selected_jobs.label | capfirst }}</button>
</legend>
<div class="my-3 collapse" id="collapseSelectedJob">
<ul>
{% for choice in filters_form.selected_jobs %}
<li>
<div class="form-check">
<input id="{{ choice.id_for_label }}" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
</fieldset>
{% endif %}
{% endif %}
27 changes: 16 additions & 11 deletions itou/templates/apply/includes/job_applications_filters/sender.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
{% if filters_form.senders or filters_form.sender_prescriber_organizations or filters_form.sender_companies %}
<hr>
<fieldset>
<legend>Émetteur</legend>
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapseSenders" type="button" aria-expanded="false" aria-controls="collapseSenders">
Émetteur
</button>
</legend>
<div class="mt-3 collapse" id="collapseSenders">
{% if filters_form.senders %}
{% bootstrap_field filters_form.senders %}
{% endif %}

{% if filters_form.senders %}
{% bootstrap_field filters_form.senders %}
{% endif %}
{% if filters_form.sender_prescriber_organizations %}
{% bootstrap_field filters_form.sender_prescriber_organizations %}
{% endif %}

{% if filters_form.sender_prescriber_organizations %}
{% bootstrap_field filters_form.sender_prescriber_organizations %}
{% endif %}

{% if filters_form.sender_companies %}
{% bootstrap_field filters_form.sender_companies %}
{% endif %}
{% if filters_form.sender_companies %}
{% bootstrap_field filters_form.sender_companies %}
{% endif %}
</div>
</fieldset>
{% endif %}
50 changes: 37 additions & 13 deletions itou/templates/apply/includes/job_applications_filters/status.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
<fieldset>
<legend>Statut candidature</legend>
<ul>
{% for choice in filters_form.states %}
<li>
<div class="form-check">
<input id="{{ choice.id_for_label }}" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}" {% if choice.data.selected %}checked=""{% endif %}>
<label for="{{ choice.id_for_label }}" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</fieldset>
{% if btn_dropdown_filter|default:False %}
<div class="dropdown">
<button type="button" class="btn btn-dropdown-filter dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Statut
</button>
<ul class="dropdown-menu">
{% for choice in filters_form.states %}
<li class="dropdown-item">
<div class="form-check">
<input id="{{ choice.id_for_label }}-top" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}-top" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<fieldset>
<legend>
<button class="btn btn-outline-transparent has-collapse-caret collapsed" data-bs-toggle="collapse" data-bs-target="#collapseStates" type="button" aria-expanded="false" aria-controls="collapseStates">
Statut
</button>
</legend>
<div class="my-3 collapse" id="collapseStates">
<ul>
{% for choice in filters_form.states %}
<li>
<div class="form-check">
<input id="{{ choice.id_for_label }}" class="form-check-input" name="{{ choice.data.name }}" type="checkbox" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
<label for="{{ choice.id_for_label }}" class="form-check-label">{{ choice.choice_label }}</label>
</div>
</li>
{% endfor %}
</ul>
</div>
</fieldset>
{% endif %}
Loading

0 comments on commit c15d0d3

Please sign in to comment.