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

Move users index page to design system #2345

Merged
merged 41 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ad9a039
Move UserFilterHelper#two_step_abbr_tag -> UsersHelper
floehopper Sep 6, 2023
2722ff2
Rename UserFilterHelper#filtered_user_roles -> assignable_user_roles
floehopper Sep 6, 2023
9a2e75f
Move UserFilterHelper#assignable_user_roles -> UsersHelper
floehopper Sep 6, 2023
64c8ba9
Remove filter functionality from users index page
floehopper Sep 6, 2023
62eb2f6
Remove "Unlock account" button from users index page
floehopper Sep 6, 2023
2197050
Remove "Resend signup email" button from users index page
floehopper Sep 6, 2023
12c780e
Move users index page to use the Design System
floehopper Sep 6, 2023
6bb4e92
Remove redundant includes from UsersController#export
floehopper Sep 6, 2023
a5df618
Introduce UsersFilter on users index page
floehopper Sep 6, 2023
3c299f5
Improve test for exporting CSV of users
floehopper Sep 6, 2023
d1e0125
Add name/email filtering to users page
floehopper Sep 6, 2023
95dc4b6
Add a "Clear all filters" link
floehopper Sep 6, 2023
f38214b
Copy option-select component from finder-frontend
floehopper Sep 6, 2023
a7a30d2
Extract User#role_class method
floehopper Sep 6, 2023
f1b3c83
Encapsulate more of UserPolicy#can_manage? in User#can_manage?
floehopper Sep 6, 2023
1096452
Inline Role::Base#can_manage? into User#can_manage?
floehopper Sep 6, 2023
ff6a198
Add role filtering to users page
floehopper Sep 6, 2023
5ebfbce
Introduce User#manageable_organisations method
floehopper Sep 6, 2023
1e4750d
Add organisation filtering to users page
floehopper Sep 6, 2023
603c7d0
Add permission filtering to users page
floehopper Sep 6, 2023
91d5f8b
Add status filtering to users page
floehopper Sep 6, 2023
688e49e
Extract User#two_step_status & status constants
floehopper Sep 6, 2023
0c4697a
Add 2SV status filtering to users page
floehopper Sep 6, 2023
782840b
Move CSV export link below filter & style as link
floehopper Sep 6, 2023
8c22177
Move number of users into table caption
floehopper Sep 6, 2023
d0d7b81
Add horizontal lines to separate filters on users page
floehopper Sep 6, 2023
b026842
Add "Filter results" heading to sidebar on users page
floehopper Sep 6, 2023
301bba5
Avoid unpermitted parameter warnings in log
floehopper Sep 6, 2023
66e4022
Open option-select components if any options selected
floehopper Sep 6, 2023
deb4739
Put selected options first when there are a lot of options
floehopper Sep 6, 2023
50e22bd
Redirect legacy users filters on users index page
floehopper Sep 6, 2023
0df53af
Automatically submit users filter form
floehopper Sep 6, 2023
6de8f58
Remove unused context span in admin layout
floehopper Sep 6, 2023
c73b2b6
Move 3 buttons on users index page to top-right
floehopper Sep 6, 2023
8fa8faf
Make some buttons secondary on users index page
floehopper Sep 6, 2023
14d33ad
Use new table component on users index page
floehopper Sep 6, 2023
a6452a9
Remove "Organisation" & "Last sign-in" cols from users page
floehopper Sep 6, 2023
a4a7062
Increase max-width of govuk-width-container for all pages
floehopper Sep 6, 2023
b62d5af
Use custom breakpoint for table on users page
floehopper Sep 6, 2023
e22212c
Group tests in UserTest more logically
floehopper Sep 6, 2023
201e2b7
Add JS-enabled integration tests for /users page
chrisroos Sep 12, 2023
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
2 changes: 2 additions & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
//= link legacy_layout.css
//= link application.js
//= link legacy_layout.js

//= link components/_option-select.css
3 changes: 3 additions & 0 deletions app/assets/images/option-select/input-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//= require govuk_publishing_components/dependencies
//= require govuk_publishing_components/all_components
//= require_tree ./modules
//= require rails-ujs

// Components from this application
//= require_tree ./components
//= require rails-ujs
304 changes: 304 additions & 0 deletions app/assets/javascripts/components/option-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
/* This JavaScript provides two functional enhancements to option-select components:
1) A count that shows how many results have been checked in the option-container
2) Open/closing of the list of checkboxes
*/
function OptionSelect ($module) {
this.$optionSelect = $module
this.$options = this.$optionSelect.querySelectorAll("input[type='checkbox']")
this.$optionsContainer = this.$optionSelect.querySelector('.js-options-container')
this.$optionList = this.$optionsContainer.querySelector('.js-auto-height-inner')
this.$allCheckboxes = this.$optionsContainer.querySelectorAll('.govuk-checkboxes__item')
this.hasFilter = this.$optionSelect.getAttribute('data-filter-element') || ''

this.checkedCheckboxes = []
}

OptionSelect.prototype.init = function () {
if (this.hasFilter.length) {
var filterEl = document.createElement('div')
filterEl.innerHTML = this.hasFilter

var optionSelectFilter = document.createElement('div')
optionSelectFilter.classList.add('app-c-option-select__filter')
optionSelectFilter.innerHTML = filterEl.childNodes[0].nodeValue

this.$optionsContainer.parentNode.insertBefore(optionSelectFilter, this.$optionsContainer)

this.$filter = this.$optionSelect.querySelector('input[name="option-select-filter"]')
this.$filterCount = document.getElementById(this.$filter.getAttribute('aria-describedby'))
this.filterTextSingle = ' ' + this.$filterCount.getAttribute('data-single')
this.filterTextMultiple = ' ' + this.$filterCount.getAttribute('data-multiple')
this.filterTextSelected = ' ' + this.$filterCount.getAttribute('data-selected')
this.checkboxLabels = []
this.filterTimeout = 0

this.getAllCheckedCheckboxes()
for (var i = 0; i < this.$allCheckboxes.length; i++) {
this.checkboxLabels.push(this.cleanString(this.$allCheckboxes[i].textContent))
}

this.$filter.addEventListener('keyup', this.typeFilterText.bind(this))
}

// Attach listener to update checked count
this.$optionsContainer.querySelector('.gem-c-checkboxes__list').addEventListener('change', this.updateCheckedCount.bind(this))

// Replace div.container-head with a button
this.replaceHeadingSpanWithButton()

// Add js-collapsible class to parent for CSS
this.$optionSelect.classList.add('js-collapsible')

// Add open/close listeners
var button = this.$optionSelect.querySelector('.js-container-button')
button.addEventListener('click', this.toggleOptionSelect.bind(this))

var closedOnLoad = this.$optionSelect.getAttribute('data-closed-on-load')
var closedOnLoadMobile = this.$optionSelect.getAttribute('data-closed-on-load-mobile')

// By default the .filter-content container is hidden on mobile
// By checking if .filter-content is hidden, we are in mobile view given the current implementation
var isFacetsContentHidden = this.isFacetsContainerHidden()

// Check if the option select should be closed for mobile screen sizes
var closedForMobile = closedOnLoadMobile === 'true' && isFacetsContentHidden

// Always set the contain height to 200px for mobile screen sizes
if (closedForMobile) {
this.setContainerHeight(200)
}

if (closedOnLoad === 'true' || closedForMobile) {
this.close()
} else {
this.setupHeight()
}

var checkedString = this.checkedString()
if (checkedString) {
this.attachCheckedCounter(checkedString)
}
}

OptionSelect.prototype.typeFilterText = function (event) {
event.stopPropagation()
var ENTER_KEY = 13

if (event.keyCode !== ENTER_KEY) {
clearTimeout(this.filterTimeout)
this.filterTimeout = setTimeout(
function () { this.doFilter(this) }.bind(this),
300
)
} else {
event.preventDefault() // prevents finder forms from being submitted when user presses ENTER
}
}

OptionSelect.prototype.cleanString = function cleanString (text) {
text = text.replace(/&/g, 'and')
text = text.replace(/[’',:–-]/g, '') // remove punctuation characters
text = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape special characters
return text.trim().replace(/\s\s+/g, ' ').toLowerCase() // replace multiple spaces with one
}

OptionSelect.prototype.getAllCheckedCheckboxes = function getAllCheckedCheckboxes () {
this.checkedCheckboxes = []

for (var i = 0; i < this.$options.length; i++) {
if (this.$options[i].checked) {
this.checkedCheckboxes.push(i)
}
}
}

OptionSelect.prototype.doFilter = function doFilter (obj) {
var filterBy = obj.cleanString(obj.$filter.value)
var showCheckboxes = obj.checkedCheckboxes.slice()
var i = 0

for (i = 0; i < obj.$allCheckboxes.length; i++) {
if (showCheckboxes.indexOf(i) === -1 && obj.checkboxLabels[i].search(filterBy) !== -1) {
showCheckboxes.push(i)
}
}

for (i = 0; i < obj.$allCheckboxes.length; i++) {
obj.$allCheckboxes[i].style.display = 'none'
}

for (i = 0; i < showCheckboxes.length; i++) {
obj.$allCheckboxes[showCheckboxes[i]].style.display = 'block'
}

var lenChecked = obj.$optionsContainer.querySelectorAll('.govuk-checkboxes__input:checked').length
var len = showCheckboxes.length + lenChecked
var html = len + (len === 1 ? obj.filterTextSingle : obj.filterTextMultiple) + ', ' + lenChecked + obj.filterTextSelected
obj.$filterCount.innerHTML = html
}

OptionSelect.prototype.replaceHeadingSpanWithButton = function replaceHeadingSpanWithButton () {
/* Replace the span within the heading with a button element. This is based on feedback from Léonie Watson.
* The button has all of the accessibility hooks that are used by screen readers and etc.
* We do this in the JavaScript because if the JavaScript is not active then the button shouldn't
* be there as there is no JS to handle the click event.
*/
var containerHead = this.$optionSelect.querySelector('.js-container-button')
var jsContainerHeadHTML = containerHead.innerHTML

// Create button and replace the preexisting html with the button.
var button = document.createElement('button')
button.setAttribute('class', 'js-container-button app-c-option-select__title app-c-option-select__button')
// Add type button to override default type submit when this component is used within a form
button.setAttribute('type', 'button')
button.setAttribute('aria-expanded', true)
button.setAttribute('id', containerHead.getAttribute('id'))
button.setAttribute('aria-controls', this.$optionsContainer.getAttribute('id'))
button.innerHTML = jsContainerHeadHTML
containerHead.parentNode.replaceChild(button, containerHead)

// GA4 Accordion tracking. Relies on the ga4-finder-tracker setting the index first, so we wrap this in a custom event.
window.addEventListener('ga4-filter-indexes-added', function () {
if (window.GOVUK.analyticsGa4) {
if (window.GOVUK.analyticsGa4.Ga4FinderTracker) {
window.GOVUK.analyticsGa4.Ga4FinderTracker.addFilterButtonTracking(button, button.innerHTML)
}
}
})
}

OptionSelect.prototype.attachCheckedCounter = function attachCheckedCounter (checkedString) {
var element = document.createElement('div')
element.setAttribute('class', 'app-c-option-select__selected-counter js-selected-counter')
element.innerHTML = checkedString
this.$optionSelect.querySelector('.js-container-button').insertAdjacentElement('afterend', element)
}

OptionSelect.prototype.updateCheckedCount = function updateCheckedCount () {
var checkedString = this.checkedString()
var checkedStringElement = this.$optionSelect.querySelector('.js-selected-counter')

if (checkedString) {
if (checkedStringElement === null) {
this.attachCheckedCounter(checkedString)
} else {
checkedStringElement.textContent = checkedString
}
} else if (checkedStringElement) {
checkedStringElement.parentNode.removeChild(checkedStringElement)
}
}

OptionSelect.prototype.checkedString = function checkedString () {
this.getAllCheckedCheckboxes()
var count = this.checkedCheckboxes.length
var checkedString = false
if (count > 0) {
checkedString = count + ' selected'
}

return checkedString
}

OptionSelect.prototype.toggleOptionSelect = function toggleOptionSelect (e) {
if (this.isClosed()) {
this.open()
} else {
this.close()
}
e.preventDefault()
}

OptionSelect.prototype.open = function open () {
if (this.isClosed()) {
this.$optionSelect.querySelector('.js-container-button').setAttribute('aria-expanded', true)
this.$optionSelect.classList.remove('js-closed')
this.$optionSelect.classList.add('js-opened')
if (!this.$optionsContainer.style.height) {
this.setupHeight()
}
}
}

OptionSelect.prototype.close = function close () {
this.$optionSelect.classList.remove('js-opened')
this.$optionSelect.classList.add('js-closed')
this.$optionSelect.querySelector('.js-container-button').setAttribute('aria-expanded', false)
}

OptionSelect.prototype.isClosed = function isClosed () {
return this.$optionSelect.classList.contains('js-closed')
}

OptionSelect.prototype.setContainerHeight = function setContainerHeight (height) {
this.$optionsContainer.style.height = height + 'px'
}

OptionSelect.prototype.isCheckboxVisible = function isCheckboxVisible (option) {
var initialOptionContainerHeight = this.$optionsContainer.clientHeight
var optionListOffsetTop = this.$optionList.getBoundingClientRect().top
var distanceFromTopOfContainer = option.getBoundingClientRect().top - optionListOffsetTop
return distanceFromTopOfContainer < initialOptionContainerHeight
}

OptionSelect.prototype.getVisibleCheckboxes = function getVisibleCheckboxes () {
var visibleCheckboxes = []
for (var i = 0; i < this.$options.length; i++) {
if (this.isCheckboxVisible(this.$options[i])) {
visibleCheckboxes.push(this.$options[i])
}
}

// add an extra checkbox, if the label of the first is too long it collapses onto itself
if (this.$options[visibleCheckboxes.length]) {
visibleCheckboxes.push(this.$options[visibleCheckboxes.length])
}
return visibleCheckboxes
}

OptionSelect.prototype.isFacetsContainerHidden = function isFacetsContainerHidden () {
var facetsContent = this.$optionSelect.parentElement
var isFacetsContentHidden = false
// check whether this is hidden by progressive disclosure,
// because height calculations won't work
// would use offsetParent === null but for IE10+
if (facetsContent) {
isFacetsContentHidden = !(facetsContent.offsetWidth || facetsContent.offsetHeight || facetsContent.getClientRects().length)
}

return isFacetsContentHidden
}

OptionSelect.prototype.setupHeight = function setupHeight () {
var initialOptionContainerHeight = this.$optionsContainer.clientHeight
var height = this.$optionList.offsetHeight

var isFacetsContainerHidden = this.isFacetsContainerHidden()

if (isFacetsContainerHidden) {
initialOptionContainerHeight = 200
height = 200
}

// Resize if the list is only slightly bigger than its container
// If isFacetsContainerHidden is true, then 200 < 250
// And the container height is always set to 201px
if (height < initialOptionContainerHeight + 50) {
this.setContainerHeight(height + 1)
return
}

// Resize to cut last item cleanly in half
var visibleCheckboxes = this.getVisibleCheckboxes()

var lastVisibleCheckbox = visibleCheckboxes[visibleCheckboxes.length - 1]
var position = lastVisibleCheckbox.parentNode.offsetTop // parent element is relative
this.setContainerHeight(position + (lastVisibleCheckbox.clientHeight / 1.5))
}

Modules.OptionSelect = OptionSelect
})(window.GOVUK.Modules)
49 changes: 0 additions & 49 deletions app/assets/javascripts/legacy/modules/dropdown_filter.js

This file was deleted.

21 changes: 21 additions & 0 deletions app/assets/javascripts/modules/auto-submit-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
'use strict'

function AutoSubmitForm (module) {
this.module = module
this.module.ignore = this.module.getAttribute('data-auto-submit-ignore').split(',')
}

AutoSubmitForm.prototype.init = function () {
this.module.addEventListener('change', function (e) {
if (!this.module.ignore.includes(e.target.getAttribute('name'))) {
this.module.submit()
}
}.bind(this))
}

Modules.AutoSubmitForm = AutoSubmitForm
})(window.GOVUK.Modules)
Loading