Skip to content

Commit

Permalink
Feat/472297 add sorting police user list (#348)
Browse files Browse the repository at this point in the history
* style: 472297 - Increase width of the table

* feat: 472297 - Add autocomplete field for police user list

* feat: 472297 - Update get to handle URL objects

* feat: 472297 - Add filtering and sorting to getUsers

* feat: 472297 - GET /admin/users/police/list filtering

* feat: 472297 - Sorting columns on /admin/users/police/list

* chore: patch v0.96.46

* chore: patch v0.96.47

* refactor: 472297 - Using deconstruction for query params /admin/users/police/index

* feat: 472297 - Fix refactor issues

* feat: 472297 - Fix failing tests

* feat: 472297 - Fix failing tests2

---------

Co-authored-by: Chris Cole <whitewaterdesign>
  • Loading branch information
whitewaterdesign authored Nov 15, 2024
1 parent c6c5f2f commit 54da169
Show file tree
Hide file tree
Showing 22 changed files with 672 additions and 150 deletions.
5 changes: 3 additions & 2 deletions app/api/ddi-index-api/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ const { ApiErrorFailure } = require('../../errors/api-error-failure')

const baseUrl = config.ddiIndexApi.baseUrl

const get = async (endpoint, user) => {
const get = async (endpointOrUrl, user) => {
const url = endpointOrUrl instanceof URL ? endpointOrUrl.toString() : `${baseUrl}/${endpointOrUrl}`
const options = user?.username ? { json: true, headers: addHeaders(user) } : { json: true }
const { payload } = await wreck.get(`${baseUrl}/${endpoint}`, options)
const { payload } = await wreck.get(url, options)

return payload
}
Expand Down
8 changes: 6 additions & 2 deletions app/api/ddi-index-api/police-forces.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ const policeForcesEndpoint = 'police-forces'
* @typedef PoliceForceRequest
* @property {string} name
*/

/**
* @typedef PoliceForceDto
* @property {number} id
* @property {string} name
*/
/**
* @param user
* @return {Promise<[{name: string, id: number}]>}
* @return {Promise<PoliceForceDto[]>}
*/
const getPoliceForces = async (user) => {
const payload = await get(policeForcesEndpoint, user)
Expand Down
63 changes: 53 additions & 10 deletions app/api/ddi-index-api/users.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const { get, boomRequest, callDelete, post } = require('./base')
const { ApiErrorFailure } = require('../../errors/api-error-failure')
const { ApiConflictError } = require('../../errors/api-conflict-error')
const config = require('../../config')
const { sort } = require('../../constants/api')

const userEndpoint = 'user'
const usersEndpoint = 'users'

/**
* @typedef UserAccount
* @typedef UserAccountDto
* @property {number} id
* @property {string} username
* @property {number} policeForceId
Expand All @@ -19,21 +21,62 @@ const usersEndpoint = 'users'
*/

/**
* @typedef UserAccount
* @property {number} id
* @property {string} username
* @property {number} policeForceId
* @property {string} policeForce
* @property {boolean|Date} accepted
* @property {boolean|Date} activated
* @property {boolean|Date} lastLogin
* @property {boolean|Date} createdAt
*/

/**
* @typedef GetUserOptions
* @property {{ policeForceId?: number }} [filter]
* @property {{
* username?: 'ASC'|'DESC';
* policeForce?: 'ASC'|'DESC';
* indexAccess?: boolean;
* }} [sort]
*/

/**
* @param {GetUserOptions} options
* @param callingUser
* @return {Promise<{ users: UserAccount[], count: number }>}
* @return {Promise<{ users: UserAccount[]; count: number }>}
*/
const getUsers = async (callingUser) => {
const getUsers = async (options, callingUser) => {
const url = new URL(`${usersEndpoint}`, config.ddiIndexApi.baseUrl)

if (!isNaN(options.filter?.policeForceId)) {
url.searchParams.append('policeForceId', `${options.filter.policeForceId}`)
}

let sortOrder

if (options.sort?.username !== undefined) {
url.searchParams.append('sortKey', 'username')
sortOrder = options.sort.username
} else if (options.sort?.policeForce !== undefined) {
url.searchParams.append('sortKey', 'policeForce')
sortOrder = options.sort.policeForce
} else if (options.sort?.indexAccess !== undefined) {
url.searchParams.append('sortKey', 'activated')
url.searchParams.append('activated', `${options.sort.indexAccess}`)
}

if ([sort.ASC, sort.DESC].includes(sortOrder)) {
url.searchParams.append('sortOrder', sortOrder)
}
/**
* @type {{
* users: {
* id: number,
* username: string,
* active: boolean,
* police_force_id?: number
* }[]
* users: UserAccountDto[];
* count: number;
* }}
*/
const payload = await get(usersEndpoint, callingUser)
const payload = await get(url, callingUser)

return {
users: payload.users.map(user => {
Expand Down
4 changes: 3 additions & 1 deletion app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const schema = Joi.object({
})
})

const ddiApiBaseUrl = process.env.DDI_EVENTS_BASE_URL

// Build config
const config = {
serviceName: process.env.SERVICE_NAME,
Expand All @@ -65,7 +67,7 @@ const config = {
baseUrl: process.env.DDI_API_BASE_URL
},
ddiEventsApi: {
baseUrl: process.env.DDI_EVENTS_BASE_URL
baseUrl: ddiApiBaseUrl
},
osPlacesApi: {
baseUrl: process.env.OS_PLACES_API_BASE_URL,
Expand Down
6 changes: 6 additions & 0 deletions app/constants/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
sort: {
ASC: 'ASC',
DESC: 'DESC'
}
}
91 changes: 80 additions & 11 deletions app/models/admin/users/police/user-list.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
const { errorPusherDefault } = require('../../../../lib/error-helpers')
const { routes } = require('../../../../constants/admin')
const { forms } = require('../../../../constants/forms')
const { getAriaSortBuilder, columnLinkBuilder } = require('../../../sorting')

/**
* @param {{
* column: 'status'
* order: 'ASC'|'DESC'
* }} sort
* @param {string} column
* @return {string}
*/
const getAriaSort = getAriaSortBuilder('email')

/**
* @param {{
* column: 'joinedExemptionScheme'
* order: 'ASC'|'DESC'
* }} sort
* @param {string} column
* @return {string}
*/
const columnLink = columnLinkBuilder('email')
/**
* @typedef AddOrRemoveDetails
* @property {string} [optionText]
Expand All @@ -9,7 +30,16 @@ const { routes } = require('../../../../constants/admin')
*/

/**
* @param {{ users: UserAccount[], count: number }} details
* @param {{
* users: UserAccount[];
* count: number;
* policeForce?: string;
* policeForces: PoliceForceDto[];
* sort: {
* order: string;
* column: string;
* }
* }} details
* @param {{ policeForce?: string; sort?: unknown }} options
* @param [backNav]
* @param [errors]
Expand All @@ -19,33 +49,71 @@ function ViewModel (details, options, backNav, errors) {
const tableHeadings = [
{
label: 'Email address',
// link: columnLink(sort, undefined),
// ariaSort: getAriaSort(sort, undefined),
name: 'emailAddress'
link: columnLink(details.sort, undefined),
ariaSort: getAriaSort(details.sort, undefined),
name: 'email'
},
{
label: 'Police force',
// link: columnLink(sort, 'indexNumber'),
// ariaSort: getAriaSort(sort, 'indexNumber'),
link: columnLink(details.sort, 'policeForce'),
ariaSort: getAriaSort(details.sort, 'policeForce'),
name: 'policeForce'
},
{
label: 'Index access',
// link: columnLink(sort, 'dateOfBirth'),
// ariaSort: getAriaSort(sort, 'dateOfBirth'),
link: columnLink(details.sort, 'indexAccess'),
ariaSort: getAriaSort(details.sort, 'indexAccess'),
name: 'indexAccess'
}
]

/**
*
* @type {GovukButton}
*/
const submit = {
preventDoubleClick: true,
text: 'Select police force',
classes: 'govuk-!-margin-bottom-8'
}

/**
* @type {AccessibleAutocompleteItem[]}
*/
const items = details.policeForces.map(item => {
return {
text: item.name,
value: item.id
}
})

/**
* @type {AccessibleAutocomplete}
*/
const policeForce = {
formGroup: {
classes: 'govuk-!-width-one-third'
},
label: {
text: 'Officers by police force',
classes: 'govuk-label govuk-body'
},
id: 'policeForce',
name: 'policeForce',
value: details.policeForce ?? '',
placeholder: 'Start typing to select a police force',
items: [{ text: '', value: null }, { text: 'All police forces', value: -1 }, ...items],
autocomplete: forms.preventAutocomplete
}

this.model = {
backLink: backNav?.backLink || routes.index.get,
fieldset: {
legend: {
text: 'Police officers with access to the Index',
isPageHeading: true,
classes: 'govuk-fieldset__legend--l govuk-!-margin-bottom-5'
},
classes: 'govuk-!-margin-bottom-8'
}
},
tableHeadings,
policeOfficers: details.users.map(user => ({
Expand All @@ -54,7 +122,8 @@ function ViewModel (details, options, backNav, errors) {
indexAccess: user.accepted && user.activated ? 'Yes' : 'Invite sent'
})),
count: details.count,
policeForce: options.policeForce,
policeForce,
submit,
sort: {
column: 'email',
order: 'ASC',
Expand Down
75 changes: 72 additions & 3 deletions app/models/builders/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
* @properties {string} [text] - Text to add after all checkbox items. If html is provided, the text option will be ignored.
* @properties {string} [html] - HTML to add after all checkbox items. If html is provided, the text option will be ignored.
**/

/**
* @typedef StandardComponent
* @property {string} [classes]
* @property {HTMLAttributes} [attributes]
*/
/**
* @typedef FormGroupObject
* @property {string} classes - Classes to add to the form group (for example to show error state for the whole group).
Expand All @@ -28,8 +32,12 @@

/**
* @typedef LabelComponent
* @properties {string} classes - Classes to add to the label tag.
* @properties {HTMLAttributes} attributes - HTML attributes (for example data attributes) to add to the label tag.
* @properties {string} [text] - Required. If html is set, this is not required. Text to use within the hint. If html is provided, the text option will be ignored.
* @properties {string} [html] - Required. If text is set, this is not required. HTML to use within the hint. If html is provided, the text option will be ignored.
* @property {string} [for]
* @property {boolean} [isPageHeading]
* @properties {string} [classes] - Classes to add to the label tag.
* @properties {HTMLAttributes} [attributes] - HTML attributes (for example data attributes) to add to the label tag.
**/

/**
Expand Down Expand Up @@ -226,3 +234,64 @@
* @property {boolean} preventDoubleClick - Prevent accidental double clicks on submit buttons from submitting forms multiple times.
* @property {boolean} isStartButton - Use for the main call to action on your service’s start page.
* @property {string} id- */

/**
* @typedef PrefixComponent
* @property {string} [text]
* @property {string} [html]
* @property {string} [classes]
* @property {string} [attributes]
*/

/**
* @typedef GovukInput
* @property {string} id
* @property {string} name
* @property {LabelComponent} label
* @property {HintComponent} [hint]
* @property {ErrorMessageComponent} [errorMessage]
* @property {string} [value]
* @property {string} [type] - default "text"
* @property {string} [inputmode]
* @property {boolean} [disabled]
* @property {string} [describedBy] - aria-describedby
* @property {PrefixComponent} [prefix]
* @property {PrefixComponent} [suffix]
* @property {FormGroupObject} [formGroup]
* @property {string} [classes]
* @property {string} [autocomplete]
* @property {string} [pattern] - Regex
* @property {boolean} [spellcheck]
* @property {string} [autocapitalize]
* @property {StandardComponent} [inputWrapper]
* @property {string} [attributes]
*/
/**
* @typedef AccessibleAutocompleteItem
* @property {string} text
* @property {string} value
*/
/**
* @typedef AccessibleAutocomplete
* @property {string} id
* @property {string} name
* @property {LabelComponent} label
* @property {AccessibleAutocompleteItem[]} items
* @property {HintComponent} [hint]
* @property {ErrorMessageComponent} [errorMessage]
* @property {string} [value]
* @property {string} [type] - default "text"
* @property {string} [inputmode]
* @property {boolean} [disabled]
* @property {string} [describedBy] - aria-describedby
* @property {PrefixComponent} [prefix]
* @property {PrefixComponent} [suffix]
* @property {FormGroupObject} [formGroup]
* @property {string} [classes]
* @property {string} [autocomplete]
* @property {string} [pattern] - Regex
* @property {boolean} [spellcheck]
* @property {string} [autocapitalize]
* @property {StandardComponent} [inputWrapper]
* @property {string} [attributes]
*/
1 change: 1 addition & 0 deletions app/plugins/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const routes = [].concat(
require('../routes/admin/users/police/index'),
require('../routes/admin/users/police/add'),
require('../routes/admin/users/police/remove'),
require('../routes/admin/users/police/list.js'),
require('../routes/admin/external-events'),
require('../routes/admin/audit/audit-query-type'),
require('../routes/admin/audit/audit-query-details'),
Expand Down
Loading

0 comments on commit 54da169

Please sign in to comment.