diff --git a/app/assets/javascripts/collapsibleCheckboxes.js b/app/assets/javascripts/collapsibleCheckboxes.js index 11c14a2e4..0333b8dc3 100644 --- a/app/assets/javascripts/collapsibleCheckboxes.js +++ b/app/assets/javascripts/collapsibleCheckboxes.js @@ -5,7 +5,7 @@ function Summary (module) { this.module = module; - this.$el = module.$formGroup.find('.selection-summary'); + this.$el = module.$formGroup.find('.selection-summary').first(); this.fieldLabel = module.fieldLabel; this.total = module.total; this.addContent(); @@ -25,6 +25,7 @@ if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); } this.$el.append(this.$text); + this.module.$formGroup.find('.govuk-hint').remove(); }; Summary.prototype.update = function(selection) { let template; @@ -86,12 +87,13 @@ .focus(); }; CollapsibleCheckboxes.prototype.start = function(component) { - this.$formGroup = $(component); - this.$fieldset = this.$formGroup.find('fieldset'); + this.$component = $(component); + this.$formGroup = this.$component.find('.govuk-form-group').first(); + this.$fieldset = this.$formGroup.find('fieldset').first(); this.$checkboxes = this.$fieldset.find('input[type=checkbox]'); - this.fieldLabel = this.$formGroup.data('fieldLabel'); + this.fieldLabel = this.$component.data('fieldLabel'); this.total = this.$checkboxes.length; - this.legendText = this.$fieldset.find('legend').text().trim(); + this.legendText = this.$fieldset.find('legend').first().text().trim(); this.expanded = false; this.addHeadingHideLegend(); @@ -113,7 +115,7 @@ }; CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; }; CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() { - const headingLevel = this.$formGroup.data('heading-level') || '2'; + const headingLevel = this.$component.data('heading-level') || '2'; this.$heading = $(`${this.legendText}`); this.$fieldset.before(this.$heading); diff --git a/app/assets/stylesheets/components/checkboxes.scss b/app/assets/stylesheets/components/checkboxes.scss index 3986c65bf..094c37d7a 100644 --- a/app/assets/stylesheets/components/checkboxes.scss +++ b/app/assets/stylesheets/components/checkboxes.scss @@ -1,3 +1,7 @@ +// Taken from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/_checkboxes.scss +$govuk-touch-target-size: 44px; +$govuk-checkboxes-size: 40px; + .selection-summary { .selection-summary__text { @@ -65,6 +69,30 @@ } +.govuk-form-group--nested { + + $border-thickness: $govuk-touch-target-size - $govuk-checkboxes-size; + $border-indent: $govuk-touch-target-size / 2; + + position: relative; + + // To equalise the spacing between the line and the top/bottom of + // the radio + margin-top: govuk-spacing(1) + ($border-thickness / 2); + margin-bottom: govuk-spacing(1) * -1; + padding-left: govuk-spacing(2) + 2; + + &:before { + content: ""; + position: absolute; + bottom: 0; + left: $border-indent * -1; + width: $border-thickness; + height: 100%; + background: $govuk-border-colour; + } +} + .selection-content { margin-bottom: govuk-spacing(4); diff --git a/app/assets/stylesheets/components/message.scss b/app/assets/stylesheets/components/message.scss index 3d63ff455..d783f7d5b 100644 --- a/app/assets/stylesheets/components/message.scss +++ b/app/assets/stylesheets/components/message.scss @@ -24,10 +24,13 @@ } } +$govuk-checkboxes-size: 40px; +$govuk-checkboxes-label-padding-left-right: govuk-spacing(3); +$message-type-bottom-spacing: govuk-spacing(4); + .message { &-name { - @include bold-24; margin: 0; a { @@ -63,37 +66,20 @@ } &-type { - color: $secondary-text-colour; - margin: 0 0 govuk-spacing(4) 0; + color: $govuk-secondary-text-colour; + margin: 0 0 $message-type-bottom-spacing 0; + padding-left: 0; pointer-events: none; } } -#template-list { - - margin-top: govuk-spacing(6); - - &.top-gutter-5px { - margin-top: 5px; - } - -} - .template-list { &-item { &-with-checkbox { - - position: relative; - padding-left: govuk-spacing(9); - - .multiple-choice { - position: absolute; - left: 0; - } - + padding-left: $govuk-checkboxes-size + $govuk-checkboxes-label-padding-left-right; } &-hidden-by-default { @@ -123,6 +109,29 @@ } + &-label { + position: absolute; + left: 0; + width: $govuk-checkboxes-size + $govuk-checkboxes-label-padding-left-right; + height: 100%; + padding: 0; + } + + // Fix for GOVUK Frontend selector with high precendence + // https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/hint/_hint.scss + &-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl)+.template-list-item-hint { + margin-bottom: $message-type-bottom-spacing; + } + + &-hint { + padding-left: 0; + } + + } + + &-folder, + &-template { + @include govuk-font($size: 24, $weight: bold, $line-height: 1.25); } &-folder { diff --git a/app/assets/stylesheets/govuk-frontend/_all.scss b/app/assets/stylesheets/govuk-frontend/_all.scss index 9b8755367..6776fb95a 100644 --- a/app/assets/stylesheets/govuk-frontend/_all.scss +++ b/app/assets/stylesheets/govuk-frontend/_all.scss @@ -26,6 +26,7 @@ $govuk-assets-path: "/static/"; @import 'components/button/_button'; @import 'components/details/_details'; @import 'components/radios/_radios'; +@import 'components/checkboxes/_checkboxes'; @import "utilities/all"; @import "overrides/all"; diff --git a/app/main/forms.py b/app/main/forms.py index 66c506976..569553c37 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from itertools import chain import pytz -from flask import request +from flask import Markup, render_template, request from flask_login import current_user from flask_wtf import FlaskForm as Form from flask_wtf.file import FileAllowed @@ -17,6 +17,7 @@ from notifications_utils.recipients import ( normalise_phone_number, validate_phone_number, ) +from werkzeug.utils import cached_property from wtforms import ( BooleanField, DateField, @@ -37,7 +38,6 @@ from wtforms import ( ) from wtforms.fields.html5 import EmailField, SearchField, TelField from wtforms.validators import URL, DataRequired, Length, Optional, Regexp -from wtforms.widgets import CheckboxInput, ListWidget from app import format_thousands from app.main.validators import ( @@ -117,11 +117,6 @@ def get_next_days_until(until): ] -class MultiCheckboxField(SelectMultipleField): - widget = ListWidget(prefix_label=False) - option_widget = CheckboxInput() - - class RadioField(WTFormsRadioField): def __init__( @@ -322,13 +317,16 @@ class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField): class NestedFieldMixin: + def children(self): + # start map with root option as a single child entry child_map = {None: [option for option in self if option.data == self.NONE_OPTION_VALUE]} # add entries for all other children for option in self: + # assign all options with a NONE_OPTION_VALUE (not always None) to the None key if option.data == self.NONE_OPTION_VALUE: child_ids = [ folder['id'] for folder in self.all_template_folders @@ -344,6 +342,47 @@ class NestedFieldMixin: return child_map + # to be used as the only version of .children once radios are converted + @cached_property + def _children(self): + return self.children() + + def get_items_from_options(self, field): + items = [] + + for option in self._children[None]: + item = self.get_item_from_option(option) + if option.data in self._children: + item['children'] = self.render_children(field.name, option.label.text, self._children[option.data]) + items.append(item) + + return items + + def render_children(self, name, label, options): + params = { + "name": name, + "fieldset": { + "legend": { + "text": label, + "classes": "govuk-visually-hidden" + } + }, + "formGroup": { + "classes": "govuk-form-group--nested" + }, + "asList": True, + "items": [] + } + for option in options: + item = self.get_item_from_option(option) + + if len(self._children[option.data]): + item['children'] = self.render_children(name, option.label.text, self._children[option.data]) + + params['items'].append(item) + + return render_template('forms/fields/checkboxes/template.njk', params=params) + class NestedRadioField(RadioFieldWithNoneOption, NestedFieldMixin): pass @@ -492,11 +531,239 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm): auth_type = HiddenField('auth_type', validators=[DataRequired()]) +class govukCheckboxesMixin: + + def extend_params(self, params, extensions): + items = None + param_items = len(params['items']) if 'items' in params else 0 + + # split items off from params to make it a pure dict + if 'items' in extensions: + items = extensions['items'] + del extensions['items'] + + # merge dicts + params.update(extensions) + + # merge items + if items: + if 'items' not in params: + params['items'] = items + else: + for idx, _item in enumerate(items): + if idx >= param_items: + params['items'].append(items[idx]) + else: + params['items'][idx].update(items[idx]) + + +class govukCheckboxField(govukCheckboxesMixin, BooleanField): + + def __init__(self, label='', validators=None, param_extensions=None, **kwargs): + super(govukCheckboxField, self).__init__(label, validators, false_values=None, **kwargs) + self.param_extensions = param_extensions + + # self.__call__ renders the HTML for the field by: + # 1. delegating to self.meta.render_field which + # 2. calls field.widget + # this bypasses that by making self.widget a method with the same interface as widget.__call__ + def widget(self, field, param_extensions=None, **kwargs): + + # error messages + error_message = None + if field.errors: + error_message = { + "attributes": { + "data-module": "track-error", + "data-error-type": field.errors[0], + "data-error-label": field.name + }, + "text": " ".join(field.errors).strip() + } + + params = { + 'name': field.name, + 'errorMessage': error_message, + 'items': [ + { + "name": field.name, + "id": field.id, + "text": field.label.text, + "value": 'y', + "checked": field.data + } + ] + + } + + # extend default params with any sent in during instantiation + if self.param_extensions: + self.extend_params(params, self.param_extensions) + + # add any sent in though use in templates + if param_extensions: + self.extend_params(params, param_extensions) + + return Markup( + render_template('forms/fields/checkboxes/macro.njk', params=params)) + + +# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6 +class govukCheckboxesField(govukCheckboxesMixin, SelectMultipleField): + + render_as_list = False + + def __init__(self, label='', validators=None, param_extensions=None, **kwargs): + super(govukCheckboxesField, self).__init__(label, validators, **kwargs) + self.param_extensions = param_extensions + + def get_item_from_option(self, option): + return { + "name": option.name, + "id": option.id, + "text": option.label.text, + "value": str(option.data), # to protect against non-string types like uuids + "checked": option.checked + } + + def get_items_from_options(self, field): + return [self.get_item_from_option(option) for option in field] + + def extend_params(self, params, extensions): + items = None + param_items = len(params['items']) if 'items' in params else 0 + + # split items off from params to make it a pure dict + if 'items' in extensions: + items = extensions['items'] + del extensions['items'] + + # merge dicts + params.update(extensions) + + # merge items + if items: + if 'items' not in params: + params['items'] = items + else: + for idx, _item in enumerate(items): + if idx >= param_items: + params['items'].append(items[idx]) + else: + params['items'][idx].update(items[idx]) + + # self.__call__ renders the HTML for the field by: + # 1. delegating to self.meta.render_field which + # 2. calls field.widget + # this bypasses that by making self.widget a method with the same interface as widget.__call__ + def widget(self, field, param_extensions=None, **kwargs): + + # error messages + error_message = None + if field.errors: + error_message = { + "attributes": { + "data-module": "track-error", + "data-error-type": field.errors[0], + "data-error-label": field.name + }, + "text": " ".join(field.errors).strip() + } + + # returns either a list or a hierarchy of lists + # depending on how get_items_from_options is implemented + items = self.get_items_from_options(field) + + params = { + 'name': field.name, + "fieldset": { + "attributes": {"id": field.name}, + "legend": { + "text": field.label.text, + "classes": "govuk-fieldset__legend--s" + } + }, + "asList": self.render_as_list, + 'errorMessage': error_message, + 'items': items + } + + # extend default params with any sent in during instantiation + if self.param_extensions: + self.extend_params(params, self.param_extensions) + + # add any sent in though use in templates + if param_extensions: + self.extend_params(params, param_extensions) + + return Markup( + render_template('forms/fields/checkboxes/macro.njk', params=params)) + + +# Extends fields using the govukCheckboxesField interface to wrap their render in HTML needed by the collapsible JS +class govukCollapsibleCheckboxesMixin: + def __init__(self, label='', validators=None, field_label='', param_extensions=None, **kwargs): + + super(govukCollapsibleCheckboxesMixin, self).__init__(label, validators, param_extensions, **kwargs) + self.field_label = field_label + + def widget(self, field, **kwargs): + + # add a blank hint to act as an ARIA live-region + if self.param_extensions is not None: + self.param_extensions.update( + {"hint": {"html": "
"}}) + else: + self.param_extensions = \ + {"hint": {"html": "
"}} + + # wrap the checkboxes HTML in the HTML needed by the collapisble JS + return Markup( + f'
' + f' {super(govukCollapsibleCheckboxesMixin, self).widget(field, **kwargs)}' + f'
' + ) + + +class govukCollapsibleCheckboxesField(govukCollapsibleCheckboxesMixin, govukCheckboxesField): + pass + + +# govukCollapsibleCheckboxesMixin adds an ARIA live-region to the hint and wraps the render in HTML needed by the +# collapsible JS +# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items +class govukCollapsibleNestedCheckboxesField(govukCollapsibleCheckboxesMixin, NestedFieldMixin, govukCheckboxesField): + NONE_OPTION_VALUE = None + render_as_list = True + + +# guard against data entries that aren't a role in permissions +def filter_by_permissions(valuelist): + if valuelist is None: + return None + else: + return [entry for entry in valuelist if any(entry in role for role in permissions)] + + +# guard against data entries that aren't a role in broadcast_permissions +def filter_by_broadcast_permissions(valuelist): + if valuelist is None: + return None + else: + return [entry for entry in valuelist if any(entry in role for role in broadcast_permissions)] + + +# Included to support both versions of how user permissions are handled in permissions forms +# Remove when changeover to new version (permissions_field) is complete PermissionsAbstract = type("PermissionsAbstract", (StripWhitespaceForm,), { permission: BooleanField(label) for permission, label in permissions }) +# Included to support both versions of how user permissions are handled in permissions forms +# Remove when changeover to new version (permissions_field) is complete BroadcastPermissionsAbstract = type("BroadcastPermissionsAbstract", (StripWhitespaceForm,), { permission: BooleanField(label) for permission, label in broadcast_permissions }) @@ -512,7 +779,9 @@ class BasePermissionsForm(StripWhitespaceForm): (item['id'], item['name']) for item in ([{'name': 'Templates', 'id': None}] + all_template_folders) ] - folder_permissions = NestedCheckboxesField('Folders this team member can see') + folder_permissions = govukCollapsibleNestedCheckboxesField( + 'Folders this team member can see', + field_label='folder') login_authentication = RadioField( 'Sign in using', @@ -524,10 +793,30 @@ class BasePermissionsForm(StripWhitespaceForm): validators=[DataRequired()] ) + permissions_field = govukCheckboxesField( + 'Permssions', + filters=[filter_by_permissions], + choices=[ + (value, label) for value, label in permissions + ], + param_extensions={ + "hint": {"text": "All team members can see sent messages."} + } + ) + + # Modified to support both versions of how user permissions are handled in permissions forms + # Remove when changeover to new version (permissions_field) is complete @property def permissions(self): - return {field.id for field in self.permissions_fields if field.data is True} + permissions_field_data = set(self.permissions_field.data) + permissions_fields_data = {field.id for field in self.permissions_fields if field.data is True} + if len(permissions_field_data) == 0 and len(permissions_fields_data) != 0: + return permissions_fields_data + else: + return permissions_field_data + # Included to support both versions of how user permissions are handled in permissions forms + # Remove when changeover to new version (permissions_field) is complete @property def permissions_fields(self): return ( @@ -540,19 +829,34 @@ class BasePermissionsForm(StripWhitespaceForm): return cls( **kwargs, **{ - role: user.has_permission_for_service(service_id, role) - for role in roles.keys() + "permissions_field": [ + role for role in roles.keys() if user.has_permission_for_service(service_id, role)] }, login_authentication=user.auth_type ) +# Included to support both versions of how user permissions are handled in permissions forms +# Remove when changeover to new version (permissions_field) is complete class PermissionsForm(PermissionsAbstract, BasePermissionsForm): pass +# Included to support both versions of how user permissions are handled in permissions forms +# Remove when changeover to new version (permissions_field) is complete class BroadcastPermissionsForm(BroadcastPermissionsAbstract, BasePermissionsForm): + permissions_field = govukCheckboxesField( + 'Permssions', + choices=[ + (value, label) for value, label in broadcast_permissions + ], + filters=[filter_by_broadcast_permissions], + param_extensions={ + "hint": {"text": "All team members can see sent messages."} + } + ) + @property def permissions(self): return {'view_activity'} | super().permissions @@ -1155,7 +1459,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm): class ServiceReplyToEmailForm(StripWhitespaceForm): email_address = email_address(label='Reply-to email address', gov_user=False) - is_default = BooleanField("Make this email address the default") + is_default = govukCheckboxField("Make this email address the default") class ServiceSmsSenderForm(StripWhitespaceForm): @@ -1169,11 +1473,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm): DoesNotStartWithDoubleZero(), ] ) - is_default = BooleanField("Make this text message sender the default") + is_default = govukCheckboxField("Make this text message sender the default") class ServiceEditInboundNumberForm(StripWhitespaceForm): - is_default = BooleanField("Make this text message sender the default") + is_default = govukCheckboxField("Make this text message sender the default") class ServiceLetterContactBlockForm(StripWhitespaceForm): @@ -1183,7 +1487,7 @@ class ServiceLetterContactBlockForm(StripWhitespaceForm): NoCommasInPlaceHolders() ] ) - is_default = BooleanField("Set as your default address") + is_default = govukCheckboxField("Set as your default address") def validate_letter_contact_block(self, field): line_count = field.data.strip().count('\n') @@ -1349,7 +1653,7 @@ class GuestList(StripWhitespaceForm): class DateFilterForm(StripWhitespaceForm): start_date = DateField("Start Date", [validators.optional()]) end_date = DateField("End Date", [validators.optional()]) - include_from_test_key = BooleanField("Include test keys", default="checked", false_values={"N"}) + include_from_test_key = govukCheckboxField("Include test keys") class RequiredDateFilterForm(StripWhitespaceForm): @@ -1661,7 +1965,9 @@ class TemplateFolderForm(StripWhitespaceForm): (item.id, item.name) for item in all_service_users ] - users_with_permission = MultiCheckboxField('Team members who can see this folder') + users_with_permission = govukCollapsibleCheckboxesField( + 'Team members who can see this folder', + field_label='folder') name = StringField('Folder name', validators=[DataRequired(message='Cannot be empty')]) @@ -1766,9 +2072,18 @@ class TemplateAndFoldersSelectionForm(Form): return self.move_to_new_folder_name.data return None - templates_and_folders = MultiCheckboxField('Choose templates or folders', validators=[ - required_for_ops('move-to-new-folder', 'move-to-existing-folder') - ]) + templates_and_folders = govukCheckboxesField( + 'Choose templates or folders', + validators=[required_for_ops('move-to-new-folder', 'move-to-existing-folder')], + choices=[], # added to keep order of arguments, added properly in __init__ + param_extensions={ + "fieldset": { + "legend": { + "classes": "govuk-visually-hidden" + } + } + } + ) # if no default set, it is set to None, which process_data transforms to '__NONE__' # this means '__NONE__' (self.ALL_TEMPLATES option) is selected when no form data has been submitted # set default to empty string so process_data method doesn't perform any transformation @@ -1867,11 +2182,13 @@ class AcceptAgreementForm(StripWhitespaceForm): class BroadcastAreaForm(StripWhitespaceForm): - areas = MultiCheckboxField('Choose areas to broadcast to') + areas = govukCheckboxesField('Choose areas to broadcast to') def __init__(self, choices, *args, **kwargs): super().__init__(*args, **kwargs) self.areas.choices = choices + self.areas.render_as_list = True + self.areas.param_extensions = {'fieldset': {'legend': {'classes': 'govuk-visually-hidden'}}} @classmethod def from_library(cls, library): diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index 53cca7132..ed85cad74 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -168,9 +168,11 @@ def platform_admin_services(): # Default to True if the user hasn’t done any filtering, # otherwise respect their choice form.include_from_test_key.data = True + + include_from_test_key = form.include_from_test_key.data api_args = {'detailed': True, 'only_active': False, # specifically DO get inactive services - 'include_from_test_key': form.include_from_test_key.data, + 'include_from_test_key': include_from_test_key, } if form.start_date.data: @@ -184,7 +186,7 @@ def platform_admin_services(): return render_template( 'views/platform-admin/services.html', - include_from_test_key=form.include_from_test_key.data, + include_from_test_key=include_from_test_key, form=form, services=list(format_stats_by_service(services)), page_title='{} services'.format( diff --git a/app/templates/components/checkbox.html b/app/templates/components/checkbox.html deleted file mode 100644 index 3647c4f1c..000000000 --- a/app/templates/components/checkbox.html +++ /dev/null @@ -1,46 +0,0 @@ -{% from "components/select-input.html" import select_nested, select %} - -{% macro checkbox( - field, - hint=False, - width='2-3' -) %} -
- {{ checkbox_input(field.id, field.name, field.data) }} - -
-{% endmacro %} - - -{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %} - {{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style, input="checkbox") }} -{% endmacro %} - - -{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}) %} - {{ select(field, hint, disable, option_hints, hide_legend, collapsible_opts, input="checkbox") }} -{% endmacro %} - - -{% macro checkbox_input(id, name, data=None, value="y") %} - -{% endmacro %} - -{% macro unlabelled_checkbox(id, name, data=None, value="y") %} -
- {{ checkbox_input(id, name, data, value) }} - -
-{% endmacro %} diff --git a/app/templates/forms/fields/checkboxes/macro.njk b/app/templates/forms/fields/checkboxes/macro.njk new file mode 100644 index 000000000..c91452053 --- /dev/null +++ b/app/templates/forms/fields/checkboxes/macro.njk @@ -0,0 +1,4 @@ +{%- macro govukCheckboxes(params) %} + {%- include "./template.njk" -%} +{%- endmacro %} +{{ govukCheckboxes(params) }} diff --git a/app/templates/forms/fields/checkboxes/template.njk b/app/templates/forms/fields/checkboxes/template.njk new file mode 100644 index 000000000..ddd83e88c --- /dev/null +++ b/app/templates/forms/fields/checkboxes/template.njk @@ -0,0 +1,136 @@ +{% from "components/error-message/macro.njk" import govukErrorMessage -%} +{% from "components/fieldset/macro.njk" import govukFieldset %} +{% from "components/hint/macro.njk" import govukHint %} +{% from "components/label/macro.njk" import govukLabel %} + + +{#- Copied from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/template.njk + Changes: + - `classes` option added to `item` allow custom classes on the `.govuk-checkboxes__item` element + - `classes` option added to `item.hint` allow custom classes on the `.govuk-hint` element (added to GOVUK Frontend in v3.5.0 - remove when we update) + - `asList` option added the root `params` object to allow setting of the `.govuk-checkboxes` and `.govuk-checkboxes__item` element types + - `children` option added to `item` allowing the sending in of prerendered child checkboxes (allowing the creation of tree structures through recursion) -#} +{#- If an id 'prefix' is not passed, fall back to using the name attribute + instead. We need this for error messages and hints as well -#} +{% set idPrefix = params.idPrefix if params.idPrefix else params.name %} + +{#- a record of other elements that we need to associate with the input using + aria-describedby – for example hints or error messages -#} +{% set describedBy = params.describedBy if params.describedBy else "" %} +{% if params.fieldset.describedBy %} + {% set describedBy = params.fieldset.describedBy %} +{% endif %} + +{#- set the types of element used for the checkboxes and their group based on + whether asList is set -#} +{% if params.asList %} + {% set groupElement = 'ul' %} + {% set groupItemElement = 'li' %} +{% else %} + {% set groupElement = 'div' %} + {% set groupItemElement = 'div' %} +{% endif %} + +{% set isConditional = false %} +{% for item in params.items %} + {% if item.conditional %} + {% set isConditional = true %} + {% endif %} +{% endfor %} + +{#- fieldset is false by default -#} +{% set hasFieldset = true if params.fieldset else false %} + +{#- Capture the HTML so we can optionally nest it in a fieldset -#} +{% set innerHtml %} +{% if params.hint %} + {% set hintId = idPrefix + '-hint' %} + {% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %} + {{ govukHint({ + id: hintId, + classes: params.hint.classes, + attributes: params.hint.attributes, + html: params.hint.html, + text: params.hint.text + }) | indent(2) | trim }} +{% endif %} +{% if params.errorMessage %} + {% set errorId = idPrefix + '-error' %} + {% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %} + {{ govukErrorMessage({ + id: errorId, + classes: params.errorMessage.classes, + attributes: params.errorMessage.attributes, + html: params.errorMessage.html, + text: params.errorMessage.text, + visuallyHiddenText: params.errorMessage.visuallyHiddenText + }) | indent(2) | trim }} +{% endif %} + <{{ groupElement }} class="govuk-checkboxes {%- if params.classes %} {{ params.classes }}{% endif %}" + {%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %} + {%- if isConditional %} data-module="checkboxes"{% endif -%}> + {% for item in params.items %} + {% set id = item.id if item.id else idPrefix + "-" + loop.index %} + {% set name = item.name if item.name else params.name %} + {% set conditionalId = "conditional-" + id %} + {% set hasHint = true if item.hint.text or item.hint.html %} + {% set itemHintId = id + "-item-hint" if hasHint else "" %} + {% set itemDescribedBy = describedBy if not hasFieldset else "" %} + {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %} + <{{ groupItemElement }} class="govuk-checkboxes__item {%- if item.classes %} {{ item.classes }}{% endif %}"> + {%- if item.before %}{{ item.before }}{% endif -%} + + {{ govukLabel({ + html: item.html, + text: item.text, + classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes), + attributes: item.label.attributes, + for: id + }) | indent(6) | trim }} + {%- if hasHint %} + {{ govukHint({ + id: itemHintId, + classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes), + attributes: item.hint.attributes, + html: item.hint.html, + text: item.hint.text + }) | indent(6) | trim }} + {%- endif %} + {%- if item.children %} + {{ item.children | safe }} + {%- endif %} + {% if params.asList and item.conditional %} +
+ {{ item.conditional.html | safe }} +
+ {% endif %} + {%- if item.after %}{{ item.after }}{% endif -%} + + {% if not params.asList and item.conditional %} +
+ {{ item.conditional.html | safe }} +
+ {% endif %} + {% endfor %} + +{% endset -%} + +
+{% if params.fieldset %} + {% call govukFieldset({ + describedBy: describedBy, + classes: params.fieldset.classes, + attributes: params.fieldset.attributes, + legend: params.fieldset.legend + }) %} + {{ innerHtml | trim | safe }} + {% endcall %} +{% else %} + {{ innerHtml | trim | safe }} +{% endif %} +
diff --git a/app/templates/views/broadcast/areas.html b/app/templates/views/broadcast/areas.html index 5020b587c..9ed0a38e8 100644 --- a/app/templates/views/broadcast/areas.html +++ b/app/templates/views/broadcast/areas.html @@ -1,6 +1,5 @@ {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import sticky_page_footer %} -{% from "components/checkbox.html" import checkboxes %} {% from "components/form.html" import form_wrapper %} {% from "components/live-search.html" import live_search %} @@ -17,10 +16,10 @@ back_link=url_for('.choose_broadcast_library', service_id=current_service.id, broadcast_message_id=broadcast_message.id), )}} - {{ live_search(target_selector='.multiple-choice', show=show_search_form, form=search_form, label='Search by name') }} + {{ live_search(target_selector='.govuk-checkboxes__item', show=show_search_form, form=search_form, label='Search by name') }} {% call form_wrapper() %} - {{ checkboxes(form.areas, hide_legend=True) }} + {{ form.areas }} {{ sticky_page_footer('Add to broadcast') }} {% endcall %} diff --git a/app/templates/views/invite-user.html b/app/templates/views/invite-user.html index 6ffb0ba04..11ae6a050 100644 --- a/app/templates/views/invite-user.html +++ b/app/templates/views/invite-user.html @@ -1,5 +1,4 @@ {% extends "withnav_template.html" %} -{% from "components/checkbox.html" import checkbox %} {% from "components/textbox.html" import textbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} diff --git a/app/templates/views/manage-users/permissions.html b/app/templates/views/manage-users/permissions.html index e6ee7637d..77189dcb0 100644 --- a/app/templates/views/manage-users/permissions.html +++ b/app/templates/views/manage-users/permissions.html @@ -1,20 +1,9 @@ -{% from "components/checkbox.html" import checkbox, checkboxes_nested %} {% from "components/radios.html" import radio, radios, conditional_radio_panel %} -
- - Permissions - - - All team members can see sent messages. - - {% for field in form.permissions_fields %} - {{ checkbox(field) }} - {% endfor %} -
+{{ form.permissions_field }} {% if form.folder_permissions.all_template_folders %} - {{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children(), hide_legend=True, collapsible_opts={ 'field': 'folder' }) }} + {{ form.folder_permissions }} {% elif user and user.platform_admin %}

Platform admin users can access all template folders. diff --git a/app/templates/views/platform-admin/_base_template.html b/app/templates/views/platform-admin/_base_template.html index 5d5c13743..c3dd1e1e1 100644 --- a/app/templates/views/platform-admin/_base_template.html +++ b/app/templates/views/platform-admin/_base_template.html @@ -1,6 +1,5 @@ {% extends "withoutnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-footer.html" import page_footer %} diff --git a/app/templates/views/platform-admin/services.html b/app/templates/views/platform-admin/services.html index a0ba6b671..2329b077f 100644 --- a/app/templates/views/platform-admin/services.html +++ b/app/templates/views/platform-admin/services.html @@ -1,6 +1,5 @@ {% extends "views/platform-admin/_base_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-footer.html" import page_footer %} {% from "components/big-number.html" import big_number, big_number_with_status %} {% from "components/message-count-label.html" import message_count_label %} @@ -70,8 +69,7 @@ {% call form_wrapper(method="get") %} {{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }} {{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }} - {{ checkbox(form.include_from_test_key) }} -
+ {{ form.include_from_test_key }} {{ govukButton({ "text": "Filter" }) }} {% endcall %} {% endset %} diff --git a/app/templates/views/service-settings/email-reply-to/_verify-updates.html b/app/templates/views/service-settings/email-reply-to/_verify-updates.html index b86f1bf6f..1a48fa1fc 100644 --- a/app/templates/views/service-settings/email-reply-to/_verify-updates.html +++ b/app/templates/views/service-settings/email-reply-to/_verify-updates.html @@ -1,6 +1,5 @@ {% from "components/banner.html" import banner, banner_wrapper %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} {% from "components/button/macro.njk" import govukButton %} @@ -55,9 +54,7 @@ hint='This should be a shared inbox managed by your team, not your own email address' ) }} {% if not first_email_address and not existing_is_default %} -

- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% endif %} {{ page_footer('Try again') }} {% endcall %} diff --git a/app/templates/views/service-settings/email-reply-to/add.html b/app/templates/views/service-settings/email-reply-to/add.html index 65135fe26..a9b9774d6 100644 --- a/app/templates/views/service-settings/email-reply-to/add.html +++ b/app/templates/views/service-settings/email-reply-to/add.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -24,9 +23,7 @@ safe_error_message=True ) }} {% if not first_email_address %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% endif %} {{ page_footer('Add') }} {% endcall %} diff --git a/app/templates/views/service-settings/email-reply-to/edit.html b/app/templates/views/service-settings/email-reply-to/edit.html index cdf7e66d9..2d675fb1f 100644 --- a/app/templates/views/service-settings/email-reply-to/edit.html +++ b/app/templates/views/service-settings/email-reply-to/edit.html @@ -1,7 +1,6 @@ {% extends "withnav_template.html" %} {% from "components/banner.html" import banner_wrapper %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -29,9 +28,7 @@

{{ page_footer('Save') }} {% else %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {{ page_footer( 'Save', delete_link=url_for('.service_confirm_delete_email_reply_to', service_id=current_service.id, reply_to_email_id=reply_to_email_address_id), diff --git a/app/templates/views/service-settings/email-reply-to/verify.html b/app/templates/views/service-settings/email-reply-to/verify.html index 00d4c8c10..407f693c5 100644 --- a/app/templates/views/service-settings/email-reply-to/verify.html +++ b/app/templates/views/service-settings/email-reply-to/verify.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} diff --git a/app/templates/views/service-settings/letter-contact/add.html b/app/templates/views/service-settings/letter-contact/add.html index 46f10b0da..ab1ba67b7 100644 --- a/app/templates/views/service-settings/letter-contact/add.html +++ b/app/templates/views/service-settings/letter-contact/add.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -27,9 +26,7 @@ highlight_placeholders=True ) }} {% if not first_contact_block %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% endif %} {{ page_footer('Add') }} {% endcall %} diff --git a/app/templates/views/service-settings/letter-contact/edit.html b/app/templates/views/service-settings/letter-contact/edit.html index cb1a63b87..a4be56d40 100644 --- a/app/templates/views/service-settings/letter-contact/edit.html +++ b/app/templates/views/service-settings/letter-contact/edit.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -31,9 +30,7 @@ This is currently your default address for {{ current_service.name }}.

{% else %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% endif %} {{ page_footer( diff --git a/app/templates/views/service-settings/sms-sender/add.html b/app/templates/views/service-settings/sms-sender/add.html index 950d5059f..1bf80bacb 100644 --- a/app/templates/views/service-settings/sms-sender/add.html +++ b/app/templates/views/service-settings/sms-sender/add.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -23,9 +22,7 @@ hint='Up to 11 characters, letters, numbers and spaces only' ) }} {% if not first_sms_sender %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% endif %} {{ page_footer('Save') }} {% endcall %} diff --git a/app/templates/views/service-settings/sms-sender/edit.html b/app/templates/views/service-settings/sms-sender/edit.html index fa98466c7..67889fcd2 100644 --- a/app/templates/views/service-settings/sms-sender/edit.html +++ b/app/templates/views/service-settings/sms-sender/edit.html @@ -1,7 +1,6 @@ {% extends "withnav_template.html" %} {% from "components/banner.html" import banner_wrapper %} {% from "components/textbox.html" import textbox %} -{% from "components/checkbox.html" import checkbox %} {% from "components/page-header.html" import page_header %} {% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} @@ -35,9 +34,7 @@

{{ page_footer('Save') }} {% else %} -
- {{ checkbox(form.is_default) }} -
+ {{ form.is_default }} {% if inbound_number %} {{ page_footer('Save') }} {% else %} diff --git a/app/templates/views/support/form.html b/app/templates/views/support/form.html index 4c4098d6b..4909aa991 100644 --- a/app/templates/views/support/form.html +++ b/app/templates/views/support/form.html @@ -1,5 +1,4 @@ {% extends "withoutnav_template.html" %} -{% from "components/checkbox.html" import checkbox %} {% from "components/textbox.html" import textbox %} {% from "components/page-footer.html" import sticky_page_footer %} {% from "components/page-header.html" import page_header %} diff --git a/app/templates/views/templates/_template_list.html b/app/templates/views/templates/_template_list.html index 5bcb03501..f17fcfb07 100644 --- a/app/templates/views/templates/_template_list.html +++ b/app/templates/views/templates/_template_list.html @@ -1,13 +1,15 @@ -{% from "components/checkbox.html" import unlabelled_checkbox %} {% from "components/message-count-label.html" import folder_contents_count, message_count_label %} -{% macro format_item_name(name) -%} +{% macro format_item_name(name, separators=True) -%} {%- if name is string -%} {{- name -}} {%- else -%} {%- for part in name -%} - {{- format_item_name(part) -}} - {%- if not loop.last %} {% endif -%} + {{- format_item_name(part, separators) -}} + {%- if not loop.last -%} + {%- if separators %} + {%- else %} {% endif -%} + {% endif -%} {%- endfor -%} {% endif %} {%- endmacro %} @@ -21,37 +23,69 @@ {% endif %}

{% else %} -