Skip to content

Commit

Permalink
Feat/templates filter macro (#1877)
Browse files Browse the repository at this point in the history
* feat(template-filter-macro): adds a macro to enable filtering of the template list

* feat(styleguide): adds an example of how the template filter macro could be used.  The templates list will need to be marked up with some new data attributes in order to make use of this functionality.

* chore: add more fullsome docs

* feat(template-filter): allow passing of codesets

* style(template-filter): add suggestions from pr review

* Apply suggestions from code review

Co-authored-by: Philippe Caron <[email protected]>

* move template filter to templates.html and split css out of the macro

* feat: include `template_caetgory` on the class used by the Templates apge

* feat: expose template types and categories so the filters can be populated

* refactor: simplify template-filter macro

* feat: implement template-filter macro on the templates screen

* chore: regen resources

* chore: formatting

* chore: formatting

* fix: re-hide hidden items when filters reset back to all

* a11y: various fixes
- fix aria-describedbys
- fix duplicate ids
- fix invalid heading structure
- remove unneccessary and broken labelledby

* fix: get list of template types for template filter

* test: add testids

* test: add ui tests

* fix: update incorrect template category name in test

* chore: remove unused import

* feat: add action for filtering and use it in the test

* a11y: remove aria_labelledby on fieldset

* fix: run all template filter tests in both english and french

* Update app/templates/components/template-filter.html

Co-authored-by: Philippe Caron <[email protected]>

* Update app/templates/components/template-filter.html

Co-authored-by: Philippe Caron <[email protected]>

* Update app/templates/components/template-filter.html

Co-authored-by: Philippe Caron <[email protected]>

* Apply suggestions from code review

Co-authored-by: Philippe Caron <[email protected]>

* fix: reset the sticky footer buttons when filtering

* fix: remove pill navigation

* fix: make tests more reliable and not depend on language strings

* a11y: use a nav element for filtering

* chore: fix failing tests

* chore: update translations

* fix: only show dev message to admins; edit doc strings

* chore: move js into its own file

* feat: Put FF around functionality

* chore: fix tests

* chore: formatting

* chore: fix tests

---------

Co-authored-by: Philippe Caron <[email protected]>
Co-authored-by: William B <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent 01ad65d commit b6b9043
Show file tree
Hide file tree
Showing 24 changed files with 758 additions and 190 deletions.
89 changes: 89 additions & 0 deletions app/assets/javascripts/templateFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Template filters
*
* This JS enhances the templates page by adding a filter to enable the user to filter the templates by type and category.
**/
(function () {
const templateFilter = document.querySelector(".template-filter");
const rowSelector = templateFilter.dataset.rowSelector;

// Function to initialize event listeners on filter links
function initializeFilterLinks() {
document.querySelectorAll(".filter-list a").forEach((link) => {
link.addEventListener("click", handleFilterClick);
});
}

// Handle click events on filter links
function handleFilterClick(event) {
event.preventDefault();
const clickedLink = event.target;
const filterGroup = clickedLink.closest(".filter-list");

// Remove 'active' class and aria-current=true from all links in the current filter group
filterGroup
.querySelectorAll("a")
.forEach((link) => link.classList.remove("active"));
filterGroup
.querySelectorAll("a")
.forEach((link) => link.removeAttribute("aria-current"));

// Add 'active' class to the clicked link
clickedLink.classList.add("active");
clickedLink.setAttribute("aria-current", "true");

// Apply filters based on the active selections
applyFilters();
}

// Collect active filters and apply them to the rows
function applyFilters() {
const activeFilters = collectActiveFilters();
filterRows(activeFilters);
}

// Collect active filters from the UI
function collectActiveFilters() {
const activeFilters = [];
templateFilter.querySelectorAll(".filter-list").forEach((filterGroup) => {
const activeLink = filterGroup.querySelector(".active");
activeFilters.push({
target: filterGroup.dataset.target,
value: activeLink.dataset.target,
});
});
return activeFilters;
}

// Apply active filters to rows, showing or hiding them as necessary
function filterRows(activeFilters) {
document.querySelectorAll(rowSelector).forEach((row) => {
const resetFilter = activeFilters.every(
(filter) => filter.value === "all",
);
const shouldShow = activeFilters.every((filter) => {
return (
filter.value === "all" ||
row.getAttribute(filter.target) === filter.value
);
});

if (resetFilter) {
row.style.display = "";
} else {
if (shouldShow) {
row.style.display = "grid";
} else {
row.style.display = "none";
}
}
});
// reset the sticky footer buttons as the height of the list may have changed
if ("stickAtBottomWhenScrolling" in GOVUK) {
GOVUK.stickAtBottomWhenScrolling.recalculate();
}
}

// Initialize the script
initializeFilterLinks();
})();
1 change: 1 addition & 0 deletions app/assets/javascripts/templateFilters.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/assets/stylesheets/index.css

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions app/assets/stylesheets/tailwind/components/filter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@layer components {
/*! purgecss start ignore */

.template-filter .filter-list:nth-child(n + 2) {
@apply pl-gutter border-l-4 border-gray-border;
}

.template-filter .filter-heading {
@apply font-medium text-small text-gray-700 mb-2;
}

.template-filter .filter-item {
@apply block p-2;
}

.template-filter .filter-item.active:not(:focus) {
@apply bg-blue-700 text-white;
}

/*! purgecss end ignore */
}
14 changes: 8 additions & 6 deletions app/assets/stylesheets/tailwind/components/message.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
@layer components {
/*! purgecss start ignore */
.message-name {
grid-column: span 2;
@apply m-0 leading-tight font-bold text-title;
}
.message-name a {
margin-bottom: -30px;
@apply pb-12;
}
.message-name a:hover {
@apply text-blue-lightblue;
}
Expand All @@ -25,8 +22,8 @@
@apply border-blue;
}

.message-type {
@apply m-0 mb-8 pointer-events-none text-gray-grey1;
.message-meta {
@apply text-small pointer-events-none text-gray-700;
}

#template-list {
Expand All @@ -36,6 +33,11 @@
@apply mt-2;
}

.template-list-item {
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
@apply grid gap-x-gutter gap-y-2 pb-gutterHalf mb-gutterHalf border-b border-gray-300 items-baseline;
}

.template-list-item-with-checkbox {
@apply relative pl-24;
}
Expand Down
5 changes: 2 additions & 3 deletions app/assets/stylesheets/tailwind/elements/details.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
}

details summary {
padding-left: 25px;
border-color: theme("colors.blue.default");
@apply mb-gutterHalf underline inline-block text-blue cursor-pointer relative;
@apply pl-gutter mb-gutterHalf underline inline-block text-blue cursor-pointer relative;
}
details summary:before {
content: "";
Expand Down Expand Up @@ -52,7 +51,7 @@
}

details [id^="details-content"] {
padding-left: 25px;
box-shadow: inset 5px 0 0 theme("colors.gray.border");
@apply pl-gutter;
}
}
1 change: 1 addition & 0 deletions app/assets/stylesheets/tailwind/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@import "./components/big-number.css";
@import "./components/textbox.css";
@import "./components/file-upload.css";
@import "./components/filter.css";
@import "./components/browse-list.css";
@import "./components/email_sms_message.css";
@import "./components/buttons.css";
Expand Down
13 changes: 12 additions & 1 deletion app/main/views/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@
from app.models.enum.template_categories import DefaultTemplateCategories
from app.models.enum.template_process_types import TemplateProcessTypes
from app.models.service import Service
from app.models.template_list import TemplateList, TemplateLists
from app.models.template_list import (
TEMPLATE_TYPES_NO_LETTER,
TemplateList,
TemplateLists,
)
from app.template_previews import TemplatePreview, get_page_count_for_letter
from app.utils import (
email_or_sms_not_enabled,
Expand Down Expand Up @@ -309,12 +313,19 @@ def choose_template(service_id, template_type="all", template_folder_id=None):

sending_view = request.args.get("view") == "sending"

template_category_name_col = "name_en" if get_current_locale(current_app) == "en" else "name_fr"

return render_template(
"views/templates/choose.html",
current_template_folder_id=template_folder_id,
current_template_folder=current_service.get_template_folder_path(template_folder_id)[-1],
template_folder_path=current_service.get_template_folder_path(template_folder_id),
template_list=template_list,
template_types=list(TEMPLATE_TYPES_NO_LETTER.values()),
template_categories=list(
{template.template_category[template_category_name_col] for template in template_list if template.template_category}
),
template_category_name_col=template_category_name_col,
show_search_box=current_service.count_of_templates_and_folders > 7,
show_template_nav=(current_service.has_multiple_template_types and (len(current_service.all_templates) > 2)),
sending_view=sending_view,
Expand Down
14 changes: 9 additions & 5 deletions app/models/template_list.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from flask_babel import _
from flask_babel import lazy_gettext as _l

TEMPLATE_TYPES = {
"email": _l("Email template"),
"sms": _l("Text message template"),
"letter": _l("Letter template"),
}
TEMPLATE_TYPES_NO_LETTER = filtered_template_types = {key: value for key, value in TEMPLATE_TYPES.items() if key != "letter"}


class TemplateList:
def __init__(
Expand Down Expand Up @@ -99,6 +106,7 @@ def __init__(
self.id = template_or_folder["id"]
self.name = template_or_folder["name"]
self.ancestors = ancestors
self.template_category = template_or_folder.get("template_category", None)


class TemplateListTemplate(TemplateListItem):
Expand All @@ -112,11 +120,7 @@ def __init__(
):
super().__init__(template, ancestors)
self.service_id = service_id
self.hint = {
"email": _l("Email template"),
"sms": _l("Text message template"),
"letter": _l("Letter template"),
}.get(template["template_type"])
self.hint = TEMPLATE_TYPES.get(template["template_type"])


class TemplateListFolder(TemplateListItem):
Expand Down
57 changes: 57 additions & 0 deletions app/templates/components/template-filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{#
This macro creates a filterable template list based on specified criteria.
The macro requires three parameters:
- row_selector: A CSS selector for the rows to filter, i.e '.template-row'
- notification_types: list of notification types, i.e ['Email', 'SMS']
- template_categories: list of template categories and text to display, i.e ['Status Update', 'Password Reset']
- notification_type_data_attribue: The data attribute to filter by notification type, i.e 'data-notification-type'
- template_category_data_attribute: The data attribute to filter by template category, i.e 'data-template-category'

NOTE: the data attributes need to be present on the rows indicated by `row-selector` for the filtering to work. Example
of a complete row:
```
<div class="template-row" data-notification-type="Email" data-template-category="Status Update">
```
#}
{% macro template_filter(row_selector, notification_types, template_categories, notification_type_data_attribue, template_category_data_attribute) %}
{# Ensure all required parameters are provided #}
{% if not row_selector or not notification_type_data_attribue or not template_category_data_attribute %}
{% if current_user.platform_admin %}
<div class="text-red font-bold my-4">
Missing required parameters: <code>row_selector</code>, <code>notification_type_attribute</code>, and <code>template_category_attribute</code>. Please verify your code and try again.
</div>
{% endif %}
{% else %}
{# Main filter container with data attributes for dynamic filtering #}
<div class="template-filter" data-row-selector="{{ row_selector }}">
<details data-testid="filter">
<summary data-testid="filter-toggle">{{ _("Apply filters") }}</summary>
<nav class="flex p-0 gap-gutter pl-gutter mt-2" aria-label="{{ _('Filter by template type and category') }}" data-testid="filter-content">
{# Filter group for notification types #}
<div class="filter-list" data-target="{{ notification_type_data_attribue }}">
<h2 class="filter-heading">{{ _("Type") }}</h2>
<div class="space-y-1" data-testid="filter-types">
<a href="#" class="filter-item active" data-target="all" data-testid="filter-type-all">{{ _("All") }}</a>
{% for notification_type in notification_types %}
<a href="#" class="filter-item" data-target="{{ notification_type }}">{{ notification_type }}</a>
{% endfor %}
</div>
</div>
{# Filter group for template categories #}
<div class="filter-list" data-target="{{ template_category_data_attribute }}">
<h2 class="filter-heading">{{ _("Category") }}</h2>
<div class="space-y-1" data-testid="filter-categories">
<a href="#" class="filter-item active" data-target="all" data-testid="filter-category-all">{{ _("All") }}</a>
{# Loop through template categories and create a filter link for each #}
{% for template_category in template_categories %}
<a href="#" class="filter-item" data-target="{{ template_category }}">{{ template_category }}</a>
{% endfor %}
</div>
</div>
</nav>
</details>
</div>

<script nonce="{{ request_nonce }}" src="{{ asset_url('javascripts/templateFilters.min.js') }}"></script>
{% endif %}
{% endmacro %}
2 changes: 1 addition & 1 deletion app/templates/views/edit-email-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h2 class="heading-medium">{{ _('Template category') }}</h2>
{% endif %}

{% if current_user.platform_admin %}
{{ radios(form.process_type, hint=_('This is only manageable by platform admins')) }}
{{ radios(form.process_type, hint=_('This is only manageable by platform admins'), use_aria_labelledby=false) }}
{% endif %}
{{ sticky_page_footer_two_submit_buttons(_('Save'), "save", _("Preview"), "preview") }}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/views/edit-sms-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ <h2 class="heading-medium">{{ _('Template category') }}</h2>
{% endif %}

{% if current_user.platform_admin %}
{{ radios(form.process_type, hint=_('This is only manageable by platform admins')) }}
{{ radios(form.process_type, hint=_('This is only manageable by platform admins'), use_aria_labelledby=false) }}
{% endif %}
{{ sticky_page_footer_two_submit_buttons(_('Save'), "save", _("Preview"), "preview") }}
</div>
Expand Down
Loading

0 comments on commit b6b9043

Please sign in to comment.