@ministryofjustice/hmpps-connect-dps-shared-items 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +34 -11
  2. package/dist/addressAutosuggest/osPlacesAddressService.d.ts +14 -0
  3. package/dist/addressAutosuggest/osPlacesAddressService.test.d.ts +1 -0
  4. package/dist/addressAutosuggest/osPlacesApiClient.d.ts +13 -0
  5. package/dist/addressAutosuggest/osPlacesApiClient.test.d.ts +1 -0
  6. package/dist/addressAutosuggest/testMocks/osPlacesAddressQueryResponse.d.ts +12 -0
  7. package/dist/addressAutosuggest/testMocks/osPlacesApiClientMock.d.ts +2 -0
  8. package/dist/assets/dps/components/address-autosuggest/_address-autosuggest.scss +98 -0
  9. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.js +43 -0
  10. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.js +90 -0
  11. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.js +374 -0
  12. package/dist/assets/dps/components/address-autosuggest/index.js +10 -0
  13. package/dist/assets/dps/components/address-autosuggest/macro.njk +61 -0
  14. package/dist/assets/js/all.js +5 -0
  15. package/dist/assets/scss/all.scss +2 -1
  16. package/dist/index.cjs +128 -0
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +169 -2
  19. package/dist/index.esm.js +127 -1
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/types/public/ConnectDpsComponentLogger.d.ts +2 -0
  22. package/dist/types/public/addressAutosuggest/index.d.ts +2 -0
  23. package/dist/types/public/addressAutosuggest/osAddress.d.ts +13 -0
  24. package/dist/types/public/addressAutosuggest/osPlaces.d.ts +124 -0
  25. package/dist/utils/utils.d.ts +8 -0
  26. package/package.json +10 -2
  27. package/readme/publishing.md +3 -3
package/README.md CHANGED
@@ -1,11 +1,17 @@
1
- # hmpps-connect-dps-shared-items-package
2
- A library for sharing dps components across applications
1
+ # hmpps-connect-dps-shared-items
2
+
3
+ A library for sharing DPS Components across applications.
3
4
 
4
5
  ## Contents
5
6
 
6
- 1. [Publishing to NPM](readme/publishing.md)
7
+ 1. [Using the library](#using-the-library)
8
+ 2. [Library components](#library-components)
9
+ 3. [For library developers](#for-library-developers)
10
+
7
11
 
8
- ## Installation
12
+ ## Using the library
13
+
14
+ ### Installation
9
15
 
10
16
  To install the package, run the following command:
11
17
 
@@ -13,7 +19,7 @@ To install the package, run the following command:
13
19
  npm install @ministryofjustice/hmpps-connect-dps-shared-items
14
20
  ```
15
21
 
16
- ## Nunjucks macro usage
22
+ ### Nunjucks macro usage
17
23
 
18
24
  Add the `hmpps-connect-dps-shared-items` assets directory to nunjucks configuration in your application:
19
25
 
@@ -30,7 +36,14 @@ Add the `hmpps-connect-dps-shared-items` assets directory to nunjucks configurat
30
36
  },
31
37
  )
32
38
  ```
33
- Include the package scss within the all.scss file. You can either import all:
39
+
40
+ then import the component into your nunkucks file:
41
+ ```js
42
+ {% from "dps/components/alert-flag/macro.njk" import alertFlag %}
43
+ ```
44
+
45
+ ### Styling
46
+ Include the package scss within the `index.scss` file. You can either import all:
34
47
  ```scss
35
48
  @import 'node_modules/@ministryofjustice/hmpps-connect-dps-shared-items/dist/assets/scss/all';
36
49
  ```
@@ -38,11 +51,21 @@ or import the specific component scss you are using, e.g:
38
51
  ```scss
39
52
  @import 'node_modules/@ministryofjustice/hmpps-connect-dps-shared-items/dist/assets/dps/components/alrt-flag/alert-flag';
40
53
  ```
41
- import the component into your nunkucks file:
42
- ```javascript
43
- {% from "dps/components/alert-flag/macro.njk" import alertFlag %}
54
+
55
+ ### Client side javascript
56
+
57
+ In your client side javascript, include:
58
+ ```js
59
+ import * as connectDps from '@ministryofjustice/hmpps-connect-dps-shared-items/dist/assets/js/all'
60
+
61
+ connectDps.initAll()
44
62
  ```
45
63
 
46
- ## Components:
64
+ ## Library Components:
65
+
66
+ 1. [Alert Flag](readme/components/alertFlag/alert-flag.md)
67
+ 2. [Address Autosuggest](readme/components/addressAutosuggest/address-autosuggest.md)
68
+
69
+ ## For Library Developers
47
70
 
48
- 1. [Alert flag](readme/components/alert-flag.md)
71
+ 1. [Publishing to NPM](readme/publishing.md)
@@ -0,0 +1,14 @@
1
+ import { OsAddress } from '../types/public/addressAutosuggest/osAddress';
2
+ import OsPlacesApiClient from './osPlacesApiClient';
3
+ import { ConnectDpsComponentLogger } from '../types/public/ConnectDpsComponentLogger';
4
+ export default class OsPlacesAddressService {
5
+ private readonly logger;
6
+ private readonly osPlacesApiClient;
7
+ constructor(logger: ConnectDpsComponentLogger, osPlacesApiClient: OsPlacesApiClient);
8
+ getAddressesMatchingQuery(searchQuery: string, sanitisePostcode?: boolean): Promise<OsAddress[]>;
9
+ getAddressByUprn(uprn: string): Promise<OsAddress | null>;
10
+ sanitiseUkPostcode(stringContainingPostcode: string): string;
11
+ private handleResponse;
12
+ private toOsAddress;
13
+ private formatAddressString;
14
+ }
@@ -0,0 +1,13 @@
1
+ import { ApiConfig } from '@ministryofjustice/hmpps-rest-client';
2
+ import { OsPlacesQueryResponse } from '../types/public/addressAutosuggest/osPlaces';
3
+ import { ConnectDpsComponentLogger } from '../types/public/ConnectDpsComponentLogger';
4
+ export default class OsPlacesApiClient {
5
+ private readonly logger;
6
+ private readonly config;
7
+ constructor(logger: ConnectDpsComponentLogger, config: ApiConfig & {
8
+ apiKey: string;
9
+ });
10
+ getAddressesByFreeTextQuery(freeTextQuery: string): Promise<OsPlacesQueryResponse>;
11
+ getAddressesByUprn(uprn: string): Promise<OsPlacesQueryResponse>;
12
+ get<T>(path: string, query: Record<string, string>): Promise<T>;
13
+ }
@@ -0,0 +1,12 @@
1
+ import { OsPlacesQueryResponse } from '../../types/public/addressAutosuggest/osPlaces';
2
+ export declare const mockOsPlacesAddressQueryResponse: OsPlacesQueryResponse;
3
+ export declare const mockOsPlacesAddressQueryEmptyResponse: OsPlacesQueryResponse;
4
+ export declare const mockOsPlacesAddressQuerySingleResponse: OsPlacesQueryResponse;
5
+ export declare const mockOsPlacesInvalidApiKey: {
6
+ fault: {
7
+ faultstring: string;
8
+ detail: {
9
+ errorcode: string;
10
+ };
11
+ };
12
+ };
@@ -0,0 +1,2 @@
1
+ import OsPlacesApiClient from '../osPlacesApiClient';
2
+ export declare const osPlacesApiClientMock: () => OsPlacesApiClient;
@@ -0,0 +1,98 @@
1
+ @import 'govuk-frontend/dist/govuk/helpers/colour';
2
+ @import 'govuk-frontend/dist/govuk/settings/colours-applied';
3
+
4
+ .hmpps-address-autosuggest {
5
+ position: relative;
6
+ max-width: 630px;
7
+
8
+ .govuk-form-group {
9
+ margin-bottom: 10px;
10
+ }
11
+
12
+ &__combobox {
13
+ border-radius: 3px;
14
+ display: inline-block;
15
+ }
16
+
17
+ &__results {
18
+ border: 1px solid $govuk-border-colour;
19
+ border-radius: 3px;
20
+ display: none;
21
+ margin: 0 0 0;
22
+ overflow: hidden;
23
+ padding: 0;
24
+ width: 100%;
25
+ }
26
+
27
+ &__results-title {
28
+ background: govuk-colour("light-grey");
29
+ border-bottom: 1px solid $govuk-border-colour;
30
+ padding: 0.25rem 0.5rem;
31
+ }
32
+
33
+ &__listbox {
34
+ background: govuk-colour("white");
35
+ list-style: none;
36
+ margin: 0;
37
+ padding: 0;
38
+ &:focus {
39
+ outline: none;
40
+ }
41
+ }
42
+
43
+ &__option {
44
+ cursor: pointer;
45
+ margin: 0;
46
+ outline: none;
47
+ padding: 0.25rem 0.5rem;
48
+
49
+ &:not(:last-child) {
50
+ border-bottom: 1px solid $govuk-border-colour;
51
+ }
52
+
53
+ &--focused:not(&--no-results) {
54
+ background: #1d70b8;
55
+ border-color: #1d70b8;
56
+ color: govuk-colour("white");
57
+ }
58
+
59
+ &:active:not(&--no-results) {
60
+ background: var(--ons-color-focus);
61
+ color: govuk-colour("black");
62
+ }
63
+
64
+ &--no-results {
65
+ background: govuk-colour("light-grey");
66
+ cursor: not-allowed;
67
+ padding: 0.25rem 0.5rem;
68
+ }
69
+ }
70
+
71
+ &__error {
72
+ margin-top: 5px;
73
+ }
74
+
75
+ &:not(&--initialised) & {
76
+ &__instructions,
77
+ &__listbox,
78
+ &__status {
79
+ display: none;
80
+ }
81
+ }
82
+
83
+ &--has-results & {
84
+ &__results {
85
+ display: block;
86
+ }
87
+ }
88
+
89
+ &--header {
90
+ .hmpps-address-autosuggest__results {
91
+ border: 0;
92
+ box-shadow: 0 0 5px 0 rgb(34 34 34 / 60%);
93
+ left: 0;
94
+ position: absolute;
95
+ z-index: 10;
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,43 @@
1
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/js/abortable-fetch.js
2
+
3
+ export const FetchStatus = {
4
+ unsent: 'UNSENT',
5
+ loading: 'LOADING',
6
+ done: 'DONE',
7
+ }
8
+
9
+ export const abortError = 'AbortError'
10
+ export const abortTimeout = 'AbortTimeout'
11
+
12
+ class AbortableFetch {
13
+ constructor(url) {
14
+ this.url = url
15
+ this.controller = new window.AbortController()
16
+ this.status = FetchStatus.unsent
17
+ }
18
+
19
+ async send() {
20
+ this.status = FetchStatus.loading
21
+ try {
22
+ setTimeout(() => {
23
+ this.controller.abort(abortTimeout)
24
+ }, 5000)
25
+ const response = await window.fetch(this.url, { signal: this.controller.signal })
26
+
27
+ if (!(+response.status === 200)) {
28
+ const error = new Error(`API error: ${response?.status ?? 500}`)
29
+ error.response = response
30
+ throw error
31
+ }
32
+ return response
33
+ } finally {
34
+ this.status = FetchStatus.done
35
+ }
36
+ }
37
+
38
+ abort() {
39
+ this.controller.abort()
40
+ }
41
+ }
42
+
43
+ export const abortableFetch = (url, options) => new AbortableFetch(url, options)
@@ -0,0 +1,90 @@
1
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/address-input/autosuggest.address.js
2
+ import { AutosuggestUi, NoResults } from './autosuggest-ui'
3
+ import { abortableFetch, abortError, FetchStatus } from './abortable-fetch'
4
+
5
+ const classUPRN = 'hmpps-js-uprn'
6
+ const classInputContainer = 'hmpps-address-autosuggest'
7
+ const classInput = 'hmpps-js-autosuggest-input'
8
+
9
+ export class AutosuggestAddress {
10
+ constructor(context) {
11
+ this.context = context
12
+ this.input = context.querySelector(`.${classInput}`)
13
+ this.container = context.querySelector(`.${classInputContainer}`)
14
+ this.uprn = context.querySelector(`.${classUPRN}`)
15
+ this.findUrl = this.container.getAttribute('data-find-url')
16
+
17
+ // State:
18
+ this.fetch = null
19
+
20
+ // Initialise autosuggest:
21
+ this.autosuggest = new AutosuggestUi({
22
+ context: this.container,
23
+ onSelect: this.onAddressSelect.bind(this),
24
+ suggestionFunction: this.suggestAddresses.bind(this),
25
+ })
26
+
27
+ // Check API status:
28
+ this.checkAPIStatus()
29
+ }
30
+
31
+ async checkAPIStatus() {
32
+ this.fetch = abortableFetch(`${this.findUrl}/SW1H9AJ`)
33
+
34
+ try {
35
+ const response = await this.fetch.send()
36
+ const status = response && (await response.json()).status
37
+ if (status !== 200) {
38
+ this.autosuggest.handleNoResults(NoResults.server_error)
39
+ }
40
+ } catch (error) {
41
+ if (error.name !== abortError) {
42
+ this.autosuggest.handleNoResults(NoResults.server_error)
43
+ }
44
+ }
45
+ }
46
+
47
+ async suggestAddresses(query) {
48
+ if (this.fetch && this.fetch.status !== FetchStatus.done) {
49
+ this.fetch.abort()
50
+ }
51
+
52
+ // Reset any previously selected result:
53
+ this.uprn.value = ''
54
+
55
+ return await this.findAddress(query?.replace(/[^A-Z0-9 ]/gi, ''))
56
+ }
57
+
58
+ async findAddress(query) {
59
+ this.fetch = abortableFetch(`${this.findUrl}/${query}`)
60
+ const data = await this.fetch.send()
61
+ const response = await data.json()
62
+ const status = response.status
63
+ const addresses = response.results
64
+ return this.mapFindResults(addresses, status)
65
+ }
66
+
67
+ mapFindResults(results, status) {
68
+ if (!results) return { status }
69
+
70
+ return {
71
+ results: this.addressMapping(results),
72
+ status: status,
73
+ }
74
+ }
75
+
76
+ addressMapping(addresses) {
77
+ if (!addresses?.length) return []
78
+
79
+ return addresses.map(address => ({ uprn: address.uprn, displayText: address.addressString }))
80
+ }
81
+
82
+ async onAddressSelect(selectedResult) {
83
+ if (this.fetch && this.fetch.status !== FetchStatus.done) {
84
+ this.fetch.abort()
85
+ }
86
+
87
+ this.autosuggest.input.value = selectedResult.displayText
88
+ this.uprn.value = selectedResult.uprn
89
+ }
90
+ }