Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/templates filter macro #1877

Merged
merged 48 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2dfa554
feat(template-filter-macro): adds a macro to enable filtering of the …
andrewleith Jun 25, 2024
ac842c2
feat(styleguide): adds an example of how the template filter macro co…
andrewleith Jun 25, 2024
8d64f00
chore: add more fullsome docs
andrewleith Jun 25, 2024
3dfe3d2
feat(template-filter): allow passing of codesets
andrewleith Jun 25, 2024
8a1967f
style(template-filter): add suggestions from pr review
andrewleith Jun 25, 2024
907f0a2
Apply suggestions from code review
andrewleith Jun 25, 2024
6f9af0f
move template filter to templates.html and split css out of the macro
amazingphilippe Jul 10, 2024
341945b
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 17, 2024
00c9bcd
feat: include `template_caetgory` on the class used by the Templates …
andrewleith Jul 17, 2024
8c50e90
feat: expose template types and categories so the filters can be popu…
andrewleith Jul 17, 2024
bdd2af8
refactor: simplify template-filter macro
andrewleith Jul 17, 2024
e5143ea
feat: implement template-filter macro on the templates screen
andrewleith Jul 17, 2024
0774eff
chore: regen resources
andrewleith Jul 17, 2024
e1bf1dd
chore: formatting
andrewleith Jul 17, 2024
fa4517b
chore: formatting
andrewleith Jul 17, 2024
2c436bf
fix: re-hide hidden items when filters reset back to all
andrewleith Jul 18, 2024
5e1824f
a11y: various fixes
andrewleith Jul 18, 2024
7cf45c6
fix: get list of template types for template filter
andrewleith Jul 18, 2024
1981e0f
test: add testids
andrewleith Jul 18, 2024
ac41be5
test: add ui tests
andrewleith Jul 18, 2024
678cb82
fix: update incorrect template category name in test
andrewleith Jul 18, 2024
e873ce6
chore: remove unused import
andrewleith Jul 19, 2024
6ce839c
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 19, 2024
07a2a8a
feat: add action for filtering and use it in the test
andrewleith Jul 19, 2024
e418d97
a11y: remove aria_labelledby on fieldset
andrewleith Jul 19, 2024
78c6308
fix: run all template filter tests in both english and french
andrewleith Jul 19, 2024
df53dcd
Update app/templates/components/template-filter.html
andrewleith Jul 22, 2024
cb05cca
Update app/templates/components/template-filter.html
andrewleith Jul 22, 2024
625e3d7
Update app/templates/components/template-filter.html
andrewleith Jul 22, 2024
c24e6f3
Apply suggestions from code review
andrewleith Jul 22, 2024
5642083
fix: reset the sticky footer buttons when filtering
andrewleith Jul 22, 2024
dcf33d7
fix: remove pill navigation
andrewleith Jul 22, 2024
7ef072e
fix: make tests more reliable and not depend on language strings
andrewleith Jul 22, 2024
a0ad29c
a11y: use a nav element for filtering
andrewleith Jul 22, 2024
50109c2
chore: fix failing tests
andrewleith Jul 23, 2024
5d8e060
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 23, 2024
1826fdd
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 23, 2024
4a68e6a
Merge branch 'main' into feat/templates-filter-macro
whabanks Jul 23, 2024
f52a981
chore: update translations
andrewleith Jul 23, 2024
5fc84c3
fix: only show dev message to admins; edit doc strings
andrewleith Jul 23, 2024
a939061
chore: move js into its own file
andrewleith Jul 23, 2024
f0518f9
feat: Put FF around functionality
andrewleith Jul 23, 2024
296e2e8
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 23, 2024
5e075a6
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 23, 2024
247204f
chore: fix tests
andrewleith Jul 23, 2024
cf03821
chore: formatting
andrewleith Jul 23, 2024
5044e2c
chore: fix tests
andrewleith Jul 23, 2024
f026c17
Merge branch 'main' into feat/templates-filter-macro
andrewleith Jul 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
andrewleith marked this conversation as resolved.
Show resolved Hide resolved
@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}
),
Comment on lines +324 to +327
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lists are passed into the template-filter macro to use as the filters.

template_category_name_col=template_category_name_col,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to know which template category field to show, based on the current app language. There may be a cleaner way to do this.

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the template_category into the TemplateList that is used when showing the list of available tempaltes.



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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this docstring so much!

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">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove pl gutter here to keep things aligned.

Suggested change
<nav class="flex p-0 gap-gutter pl-gutter mt-2" aria-label="{{ _('Filter by template type and category') }}" data-testid="filter-content">
<nav class="flex p-0 gap-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
Loading