@defra/flood-webchat 0.0.1-beta.9 → 1.0.2
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.
- package/README.md +6 -0
- package/dist/client.js +1 -329
- package/dist/server.js +1 -286
- package/main.scss +13 -8
- package/package.json +29 -22
- package/src/client/components/availability/availability.jsx +94 -65
- package/src/client/components/availability/availability.scss +20 -32
- package/src/client/components/chat/chat.jsx +239 -0
- package/src/client/components/chat/chat.scss +258 -0
- package/src/client/components/errorSummary/error-summary.jsx +34 -0
- package/src/client/components/live-region.jsx +55 -0
- package/src/client/components/message/message.jsx +21 -0
- package/src/client/components/panel/panel-footer.jsx +9 -0
- package/src/client/components/panel/panel-header.jsx +75 -0
- package/src/client/components/panel/panel.jsx +163 -0
- package/src/client/components/panel/panel.scss +112 -0
- package/src/client/components/screens/end-chat.jsx +77 -0
- package/src/client/components/screens/feedback.jsx +65 -0
- package/src/client/components/screens/pre-chat.jsx +62 -0
- package/src/client/components/screens/request-chat.jsx +183 -0
- package/src/client/components/screens/settings.jsx +97 -0
- package/src/client/components/screens/unavailable.jsx +23 -0
- package/src/client/components/skip-link.jsx +42 -0
- package/src/client/hooks/useFocusedElements.js +80 -0
- package/src/client/hooks/useTextareaAutosize.js +13 -0
- package/src/client/index.jsx +18 -1
- package/src/client/lib/agent-status-headline.js +17 -0
- package/src/client/lib/check-availability.js +1 -0
- package/src/client/lib/history.js +13 -0
- package/src/client/lib/message-notification.js +30 -0
- package/src/client/lib/transform-messages.js +47 -0
- package/src/client/scss/_objects.scss +33 -0
- package/src/client/scss/_settings.scss +101 -0
- package/src/client/scss/_tools.scss +33 -0
- package/src/client/scss/_utilities.scss +25 -0
- package/src/client/store/AppProvider.jsx +183 -0
- package/src/client/store/actions-map.js +152 -0
- package/src/client/store/constants.js +4 -0
- package/src/client/store/reducer.js +31 -0
- package/src/client/store/useApp.js +5 -0
- package/src/client/store/useChatSdk.js +37 -0
- package/src/server/lib/client.js +2 -2
- package/src/server/lib/utils.js +16 -8
- package/webpack.config.mjs +0 -2
- package/webpack.prod.mjs +7 -0
- package/dist/client.js.map +0 -1
- package/dist/server.js.map +0 -1
- package/src/client/lib/external-stores.js +0 -4
- package/src/client/lib/external-sync-store.js +0 -42
package/dist/server.js
CHANGED
|
@@ -1,286 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/******/ var __webpack_modules__ = ({
|
|
3
|
-
|
|
4
|
-
/***/ "./src/server/index.js":
|
|
5
|
-
/*!*****************************!*\
|
|
6
|
-
!*** ./src/server/index.js ***!
|
|
7
|
-
\*****************************/
|
|
8
|
-
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
|
|
9
|
-
|
|
10
|
-
const {
|
|
11
|
-
authenticate,
|
|
12
|
-
getApiBaseUrl,
|
|
13
|
-
getIsOpen,
|
|
14
|
-
getActivity
|
|
15
|
-
} = __webpack_require__(/*! ./lib/client.js */ "./src/server/lib/client.js");
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Returns webchat availability
|
|
19
|
-
* @param options {object}
|
|
20
|
-
* @param options.clientId {string}
|
|
21
|
-
* @param options.clientSecret {string}
|
|
22
|
-
* @param options.accessKey {string}
|
|
23
|
-
* @param options.accessSecret {string}
|
|
24
|
-
* @param options.skillEndpoint {string}
|
|
25
|
-
* @param options.hoursEndpoint {string}
|
|
26
|
-
* @param options.authenticationUri {string}
|
|
27
|
-
* @param options.wellKnownUri {string}
|
|
28
|
-
* @param options.maxQueueCount {string}
|
|
29
|
-
* @returns {Promise<{date: Date, availability: (string)}>}
|
|
30
|
-
*/
|
|
31
|
-
module.exports = async function getAvailability({
|
|
32
|
-
clientId,
|
|
33
|
-
clientSecret,
|
|
34
|
-
accessKey,
|
|
35
|
-
accessSecret,
|
|
36
|
-
maxQueueCount,
|
|
37
|
-
skillEndpoint,
|
|
38
|
-
hoursEndpoint,
|
|
39
|
-
wellKnownUri = 'https://cxone.niceincontact.com/.well-known/cxone-configuration',
|
|
40
|
-
authenticationUri = 'https://cxone.niceincontact.com/auth/token'
|
|
41
|
-
}) {
|
|
42
|
-
const authorisation = 'Basic ' + Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64');
|
|
43
|
-
|
|
44
|
-
// Cache authentication and re-authenticate when needed (lasts 1 hour?)
|
|
45
|
-
const {
|
|
46
|
-
tenantId,
|
|
47
|
-
token,
|
|
48
|
-
tokenType
|
|
49
|
-
} = await authenticate({
|
|
50
|
-
authenticationUri,
|
|
51
|
-
authorisation,
|
|
52
|
-
accessKey,
|
|
53
|
-
accessSecret
|
|
54
|
-
});
|
|
55
|
-
const apiBaseUrl = await getApiBaseUrl({
|
|
56
|
-
wellKnownUri,
|
|
57
|
-
tenantId
|
|
58
|
-
});
|
|
59
|
-
const [{
|
|
60
|
-
hasCapacity,
|
|
61
|
-
hasAgentsAvailable
|
|
62
|
-
}, isOpen] = await Promise.all([getActivity({
|
|
63
|
-
baseUrl: apiBaseUrl,
|
|
64
|
-
tokenType,
|
|
65
|
-
token,
|
|
66
|
-
skillEndpoint,
|
|
67
|
-
maxQueueCount
|
|
68
|
-
}), getIsOpen({
|
|
69
|
-
baseUrl: apiBaseUrl,
|
|
70
|
-
token,
|
|
71
|
-
tokenType,
|
|
72
|
-
hoursEndpoint
|
|
73
|
-
})]);
|
|
74
|
-
const isAvailable = isOpen && hasAgentsAvailable && hasCapacity;
|
|
75
|
-
const isExistingOnly = isOpen && hasAgentsAvailable && !hasCapacity;
|
|
76
|
-
let availability = 'UNAVAILABLE';
|
|
77
|
-
if (isAvailable) {
|
|
78
|
-
availability = 'AVAILABLE';
|
|
79
|
-
} else if (isExistingOnly) {
|
|
80
|
-
availability = 'EXISTING';
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
date: new Date(),
|
|
84
|
-
availability
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/***/ }),
|
|
89
|
-
|
|
90
|
-
/***/ "./src/server/lib/client.js":
|
|
91
|
-
/*!**********************************!*\
|
|
92
|
-
!*** ./src/server/lib/client.js ***!
|
|
93
|
-
\**********************************/
|
|
94
|
-
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
|
|
95
|
-
|
|
96
|
-
const querystring = __webpack_require__(/*! querystring */ "querystring");
|
|
97
|
-
const axios = __webpack_require__(/*! axios */ "axios");
|
|
98
|
-
const jwtdecode = __webpack_require__(/*! jwt-decode */ "jwt-decode");
|
|
99
|
-
const {
|
|
100
|
-
isWithinHours
|
|
101
|
-
} = __webpack_require__(/*! ./utils.js */ "./src/server/lib/utils.js");
|
|
102
|
-
const contentType = 'application/x-www-form-urlencoded';
|
|
103
|
-
const authenticate = async ({
|
|
104
|
-
authenticationUri,
|
|
105
|
-
authorisation,
|
|
106
|
-
accessKey,
|
|
107
|
-
accessSecret
|
|
108
|
-
}) => {
|
|
109
|
-
const url = new URL(authenticationUri);
|
|
110
|
-
const body = querystring.stringify({
|
|
111
|
-
grant_type: 'password',
|
|
112
|
-
username: accessKey,
|
|
113
|
-
password: accessSecret
|
|
114
|
-
});
|
|
115
|
-
const auth = await axios.post(url.href, body, {
|
|
116
|
-
signal: AbortSignal.timeout(3000),
|
|
117
|
-
headers: {
|
|
118
|
-
Host: url.host,
|
|
119
|
-
'Content-Type': contentType,
|
|
120
|
-
Authorization: authorisation
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
return {
|
|
124
|
-
token: auth.data.access_token,
|
|
125
|
-
tokenType: auth.data.token_type,
|
|
126
|
-
tenantId: jwtdecode(auth.data.id_token)?.tenantId
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
const getApiBaseUrl = async ({
|
|
130
|
-
wellKnownUri,
|
|
131
|
-
tenantId
|
|
132
|
-
}) => {
|
|
133
|
-
const url = new URL(wellKnownUri);
|
|
134
|
-
url.searchParams.set('tenantId', tenantId);
|
|
135
|
-
const {
|
|
136
|
-
data
|
|
137
|
-
} = await axios.get(url.href, {
|
|
138
|
-
headers: {
|
|
139
|
-
Host: url.host
|
|
140
|
-
},
|
|
141
|
-
signal: AbortSignal.timeout(3000)
|
|
142
|
-
});
|
|
143
|
-
return data.api_endpoint;
|
|
144
|
-
};
|
|
145
|
-
const getActivity = async ({
|
|
146
|
-
tokenType,
|
|
147
|
-
token,
|
|
148
|
-
baseUrl,
|
|
149
|
-
skillEndpoint,
|
|
150
|
-
maxQueueCount
|
|
151
|
-
}) => {
|
|
152
|
-
const url = new URL(skillEndpoint, baseUrl);
|
|
153
|
-
const skill = await axios.get(url.href, {
|
|
154
|
-
signal: AbortSignal.timeout(3000),
|
|
155
|
-
headers: {
|
|
156
|
-
Host: url.host,
|
|
157
|
-
Authorization: `${tokenType} ${token}`,
|
|
158
|
-
'Content-Type': contentType
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
const activity = skill.data.skillActivity[0];
|
|
162
|
-
return {
|
|
163
|
-
hasCapacity: activity.queueCount < maxQueueCount,
|
|
164
|
-
hasAgentsAvailable: activity.agentsAvailable >= 1
|
|
165
|
-
};
|
|
166
|
-
};
|
|
167
|
-
const getIsOpen = async ({
|
|
168
|
-
baseUrl,
|
|
169
|
-
token,
|
|
170
|
-
tokenType,
|
|
171
|
-
hoursEndpoint
|
|
172
|
-
}) => {
|
|
173
|
-
const url = new URL(hoursEndpoint, baseUrl);
|
|
174
|
-
const hours = await axios.get(url.href, {
|
|
175
|
-
signal: AbortSignal.timeout(3000),
|
|
176
|
-
headers: {
|
|
177
|
-
Host: url.host,
|
|
178
|
-
Authorization: `${tokenType} ${token}`,
|
|
179
|
-
'Content-Type': contentType
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
const days = hours.data.resultSet.hoursOfOperationProfiles[0].days;
|
|
183
|
-
return isWithinHours(days);
|
|
184
|
-
};
|
|
185
|
-
module.exports = {
|
|
186
|
-
authenticate,
|
|
187
|
-
getApiBaseUrl,
|
|
188
|
-
getIsOpen,
|
|
189
|
-
getActivity
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
/***/ }),
|
|
193
|
-
|
|
194
|
-
/***/ "./src/server/lib/utils.js":
|
|
195
|
-
/*!*********************************!*\
|
|
196
|
-
!*** ./src/server/lib/utils.js ***!
|
|
197
|
-
\*********************************/
|
|
198
|
-
/***/ ((module) => {
|
|
199
|
-
|
|
200
|
-
const isWithinHours = (days, date = null) => {
|
|
201
|
-
const now = date ? new Date(date) : new Date();
|
|
202
|
-
const name = now.toLocaleDateString('en-GB', {
|
|
203
|
-
weekday: 'long'
|
|
204
|
-
});
|
|
205
|
-
const day = days.find(d => d.day.toLowerCase() === name.toLowerCase());
|
|
206
|
-
const dateItems = now.toLocaleDateString('en-GB').split('/');
|
|
207
|
-
const open = `${dateItems[2]}-${dateItems[1]}-${dateItems[0]}T${day.openTime}`;
|
|
208
|
-
const close = `${dateItems[2]}-${dateItems[1]}-${dateItems[0]}T${day.closeTime}`;
|
|
209
|
-
return now.getTime() >= Date.parse(open) && now.getTime() <= Date.parse(close);
|
|
210
|
-
};
|
|
211
|
-
module.exports = {
|
|
212
|
-
isWithinHours
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/***/ }),
|
|
216
|
-
|
|
217
|
-
/***/ "axios":
|
|
218
|
-
/*!************************!*\
|
|
219
|
-
!*** external "axios" ***!
|
|
220
|
-
\************************/
|
|
221
|
-
/***/ ((module) => {
|
|
222
|
-
|
|
223
|
-
"use strict";
|
|
224
|
-
module.exports = require("axios");
|
|
225
|
-
|
|
226
|
-
/***/ }),
|
|
227
|
-
|
|
228
|
-
/***/ "jwt-decode":
|
|
229
|
-
/*!*****************************!*\
|
|
230
|
-
!*** external "jwt-decode" ***!
|
|
231
|
-
\*****************************/
|
|
232
|
-
/***/ ((module) => {
|
|
233
|
-
|
|
234
|
-
"use strict";
|
|
235
|
-
module.exports = require("jwt-decode");
|
|
236
|
-
|
|
237
|
-
/***/ }),
|
|
238
|
-
|
|
239
|
-
/***/ "querystring":
|
|
240
|
-
/*!******************************!*\
|
|
241
|
-
!*** external "querystring" ***!
|
|
242
|
-
\******************************/
|
|
243
|
-
/***/ ((module) => {
|
|
244
|
-
|
|
245
|
-
"use strict";
|
|
246
|
-
module.exports = require("querystring");
|
|
247
|
-
|
|
248
|
-
/***/ })
|
|
249
|
-
|
|
250
|
-
/******/ });
|
|
251
|
-
/************************************************************************/
|
|
252
|
-
/******/ // The module cache
|
|
253
|
-
/******/ var __webpack_module_cache__ = {};
|
|
254
|
-
/******/
|
|
255
|
-
/******/ // The require function
|
|
256
|
-
/******/ function __webpack_require__(moduleId) {
|
|
257
|
-
/******/ // Check if module is in cache
|
|
258
|
-
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
259
|
-
/******/ if (cachedModule !== undefined) {
|
|
260
|
-
/******/ return cachedModule.exports;
|
|
261
|
-
/******/ }
|
|
262
|
-
/******/ // Create a new module (and put it into the cache)
|
|
263
|
-
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
264
|
-
/******/ // no module.id needed
|
|
265
|
-
/******/ // no module.loaded needed
|
|
266
|
-
/******/ exports: {}
|
|
267
|
-
/******/ };
|
|
268
|
-
/******/
|
|
269
|
-
/******/ // Execute the module function
|
|
270
|
-
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
271
|
-
/******/
|
|
272
|
-
/******/ // Return the exports of the module
|
|
273
|
-
/******/ return module.exports;
|
|
274
|
-
/******/ }
|
|
275
|
-
/******/
|
|
276
|
-
/************************************************************************/
|
|
277
|
-
/******/
|
|
278
|
-
/******/ // startup
|
|
279
|
-
/******/ // Load entry module and return exports
|
|
280
|
-
/******/ // This entry module is referenced by other modules so it can't be inlined
|
|
281
|
-
/******/ var __webpack_exports__ = __webpack_require__("./src/server/index.js");
|
|
282
|
-
/******/ module.exports = __webpack_exports__;
|
|
283
|
-
/******/
|
|
284
|
-
/******/ })()
|
|
285
|
-
;
|
|
286
|
-
//# sourceMappingURL=server.js.map
|
|
1
|
+
(()=>{var e={525:(e,t,n)=>{const{authenticate:o,getApiBaseUrl:a,getIsOpen:i,getActivity:s}=n(398);e.exports=async function({clientId:e,clientSecret:t,accessKey:n,accessSecret:r,maxQueueCount:c,skillEndpoint:u,hoursEndpoint:l,wellKnownUri:p="https://cxone.niceincontact.com/.well-known/cxone-configuration",authenticationUri:d="https://cxone.niceincontact.com/auth/token"}){const h="Basic "+Buffer.from(`${encodeURIComponent(e)}:${encodeURIComponent(t)}`).toString("base64"),{tenantId:m,token:y,tokenType:w}=await o({authenticationUri:d,authorisation:h,accessKey:n,accessSecret:r}),g=await a({wellKnownUri:p,tenantId:m}),[{hasCapacity:A,hasAgentsAvailable:k},f]=await Promise.all([s({baseUrl:g,tokenType:w,token:y,skillEndpoint:u,maxQueueCount:c}),i({baseUrl:g,token:y,tokenType:w,hoursEndpoint:l})]);let x="UNAVAILABLE";return f&&k&&A?x="AVAILABLE":f&&k&&!A&&(x="EXISTING"),{date:new Date,availability:x}}},398:(e,t,n)=>{const o=n(45),a=n(938),i=n(158).decode,{isWithinHours:s}=n(792),r="application/x-www-form-urlencoded";e.exports={authenticate:async({authenticationUri:e,authorisation:t,accessKey:n,accessSecret:s})=>{const c=new URL(e),u=o.stringify({grant_type:"password",username:n,password:s}),l=await a.post(c.href,u,{signal:AbortSignal.timeout(3e3),headers:{Host:c.host,"Content-Type":r,Authorization:t}});return{token:l.data.access_token,tokenType:l.data.token_type,tenantId:i(l.data.id_token)?.tenantId}},getApiBaseUrl:async({wellKnownUri:e,tenantId:t})=>{const n=new URL(e);n.searchParams.set("tenantId",t);const{data:o}=await a.get(n.href,{headers:{Host:n.host},signal:AbortSignal.timeout(3e3)});return o.api_endpoint},getIsOpen:async({baseUrl:e,token:t,tokenType:n,hoursEndpoint:o})=>{const i=new URL(o,e),c=(await a.get(i.href,{signal:AbortSignal.timeout(3e3),headers:{Host:i.host,Authorization:`${n} ${t}`,"Content-Type":r}})).data.resultSet.hoursOfOperationProfiles[0].days;return s(c)},getActivity:async({tokenType:e,token:t,baseUrl:n,skillEndpoint:o,maxQueueCount:i})=>{const s=new URL(o,n),c=(await a.get(s.href,{signal:AbortSignal.timeout(3e3),headers:{Host:s.host,Authorization:`${e} ${t}`,"Content-Type":r}})).data.skillActivity[0];return{hasCapacity:c.queueCount<i,hasAgentsAvailable:c.agentsAvailable>=1}}}},792:(e,t,n)=>{const{DateTime:o}=n(168),a=e=>Number(e.split(":")[0]);e.exports={isWithinHours:(e,t)=>{const n=t?o.fromISO(t):o.local();n.setZone("Europe/London");const i=t?new Date(t):new Date,s=i.toLocaleDateString("en-GB",{weekday:"long"}),r=i.toLocaleDateString("en-GB").split("/"),c=e.find((e=>e.day===s)),u=o.local(Number(r[2]),Number(r[1]),Number(r[0]),a(c.openTime)),l=o.local(Number(r[2]),Number(r[1]),Number(r[0]),a(c.closeTime));return n.diff(u).milliseconds>=0&&n.diff(l).milliseconds<=0}}},938:e=>{"use strict";e.exports=require("axios")},158:e=>{"use strict";e.exports=require("jwt-decode")},168:e=>{"use strict";e.exports=require("luxon")},45:e=>{"use strict";e.exports=require("querystring")}},t={},n=function n(o){var a=t[o];if(void 0!==a)return a.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}(525);module.exports=n})();
|
package/main.scss
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
// Required GOVUK Styles
|
|
3
|
-
@import "govuk-frontend/govuk/core/typography";
|
|
4
|
-
@import "govuk-frontend/govuk/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@import "./src/client/components/availability/availability";
|
|
3
|
+
@import "govuk-frontend/dist/govuk/core/typography";
|
|
4
|
+
@import "govuk-frontend/dist/govuk/core/links";
|
|
5
|
+
@import "govuk-frontend/dist/govuk/utilities/visually-hidden";
|
|
6
|
+
@import "govuk-frontend/dist/govuk/components/button/button";
|
|
8
7
|
|
|
8
|
+
// (ITCSS)
|
|
9
|
+
@import "./src/client/scss/settings";
|
|
10
|
+
@import "./src/client/scss/tools";
|
|
11
|
+
@import "./src/client/scss/objects";
|
|
12
|
+
@import "./src/client/scss/utilities";
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
// React components
|
|
15
|
+
@import "./src/client/components/availability/availability";
|
|
16
|
+
@import "./src/client/components/panel/panel";
|
|
17
|
+
@import "./src/client/components/chat/chat";
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/flood-webchat",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"browser": "dist/client.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "npm run lint && npm run unit-test",
|
|
9
9
|
"dev": "(cd demo && webpack server)",
|
|
10
|
-
"build": "webpack",
|
|
10
|
+
"build": "webpack --config webpack.prod.mjs",
|
|
11
11
|
"unit-test": "jest",
|
|
12
12
|
"lint": "npm run lint:js && npm run lint:scss",
|
|
13
13
|
"lint:fix": "npm run lint:js -- --fix && npm run lint:scss -- --fix",
|
|
14
14
|
"lint:js": "standard",
|
|
15
15
|
"lint:scss": "stylelint \"**/*.scss\"",
|
|
16
|
-
"prepare": "
|
|
16
|
+
"prepare": "husky install"
|
|
17
17
|
},
|
|
18
18
|
"standard": {
|
|
19
19
|
"ignore": [
|
|
@@ -27,41 +27,48 @@
|
|
|
27
27
|
"author": "DEFRA",
|
|
28
28
|
"license": "OGL-UK-3.0",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@babel/core": "^7.
|
|
31
|
-
"@babel/preset-env": "^7.
|
|
32
|
-
"@babel/preset-react": "^7.
|
|
33
|
-
"@testing-library/
|
|
34
|
-
"@testing-library/
|
|
35
|
-
"@
|
|
30
|
+
"@babel/core": "^7.24.6",
|
|
31
|
+
"@babel/preset-env": "^7.24.6",
|
|
32
|
+
"@babel/preset-react": "^7.24.6",
|
|
33
|
+
"@testing-library/jest-dom": "^6.4.5",
|
|
34
|
+
"@testing-library/react": "^15.0.7",
|
|
35
|
+
"@testing-library/user-event": "^14.5.2",
|
|
36
|
+
"@types/jest": "^29.5.12",
|
|
36
37
|
"babel-jest": "^29.6.4",
|
|
37
38
|
"babel-loader": "^9.1.3",
|
|
38
|
-
"core-js": "^
|
|
39
|
-
"css-loader": "^
|
|
40
|
-
"dotenv": "^16.
|
|
39
|
+
"core-js": "^3.37.1",
|
|
40
|
+
"css-loader": "^7.1.2",
|
|
41
|
+
"dotenv": "^16.4.5",
|
|
41
42
|
"husky": "^8.0.3",
|
|
42
|
-
"jest": "^29.
|
|
43
|
-
"jest-environment-jsdom": "^29.
|
|
43
|
+
"jest": "^29.7.0",
|
|
44
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
44
45
|
"mini-css-extract-plugin": "^2.7.6",
|
|
45
46
|
"node-cache": "^5.1.2",
|
|
46
47
|
"node-sass": "^9.0.0",
|
|
47
48
|
"sass": "^1.66.1",
|
|
48
|
-
"sass-loader": "^
|
|
49
|
+
"sass-loader": "^14.2.1",
|
|
49
50
|
"standard": "^17.1.0",
|
|
50
|
-
"stylelint": "^15.
|
|
51
|
+
"stylelint": "^15.11.0",
|
|
51
52
|
"stylelint-config-gds": "^1.0.0",
|
|
52
|
-
"webpack": "^5.
|
|
53
|
+
"webpack": "^5.91.0",
|
|
53
54
|
"webpack-cli": "^5.1.4",
|
|
54
|
-
"webpack-dev-server": "^
|
|
55
|
+
"webpack-dev-server": "^5.0.4",
|
|
56
|
+
"webpack-merge": "^5.10.0",
|
|
55
57
|
"webpack-node-externals": "^3.0.0"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
|
-
"govuk-frontend": "^4.0
|
|
60
|
+
"govuk-frontend": "^5.4.0"
|
|
59
61
|
},
|
|
60
62
|
"dependencies": {
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
+
"@nice-devone/nice-cxone-chat-web-sdk": "^1.18.0",
|
|
64
|
+
"axios": "^1.7.2",
|
|
65
|
+
"html-react-parser": "^5.1.10",
|
|
66
|
+
"jwt-decode": "^4.0.0",
|
|
67
|
+
"lodash.debounce": "^4.0.8",
|
|
68
|
+
"luxon": "^3.4.3",
|
|
63
69
|
"querystring": "^0.2.1",
|
|
64
70
|
"react": "^18.2.0",
|
|
65
|
-
"react-dom": "^18.2.0"
|
|
71
|
+
"react-dom": "^18.2.0",
|
|
72
|
+
"uuid": "^9.0.1"
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -1,70 +1,121 @@
|
|
|
1
|
-
import React, { useEffect, useRef
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
2
3
|
import { classnames } from '../../lib/classnames'
|
|
3
|
-
import {
|
|
4
|
+
import { Panel } from '../panel/panel.jsx'
|
|
5
|
+
import { SkipLink } from '../skip-link.jsx'
|
|
6
|
+
|
|
7
|
+
import { useApp } from '../../store/useApp'
|
|
8
|
+
import { historyPushState, historyReplaceState } from '../../lib/history.js'
|
|
9
|
+
import { LiveRegion } from '../live-region.jsx'
|
|
10
|
+
|
|
11
|
+
export function Availability () {
|
|
12
|
+
const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
|
|
4
13
|
|
|
5
|
-
export function Availability (props) {
|
|
6
|
-
const [isOpen, setOpen] = useWebchatOpenState()
|
|
7
|
-
const [isFixed, setFixed] = useState(false)
|
|
8
14
|
const buttonRef = useRef()
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
|
|
16
|
+
const showUnseenCount = unseenCount > 0 && !isChatOpen
|
|
17
|
+
|
|
18
|
+
const messageText = unseenCount > 1 ? 'new messages' : 'new message'
|
|
19
|
+
|
|
20
|
+
const onClick = e => {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
setUnseenCount(0)
|
|
23
|
+
setChatVisibility(!isChatOpen)
|
|
24
|
+
setInstigatorId(e.target.id)
|
|
25
|
+
|
|
26
|
+
if (!isChatOpen) {
|
|
27
|
+
historyPushState()
|
|
28
|
+
} else {
|
|
29
|
+
historyReplaceState()
|
|
30
|
+
}
|
|
11
31
|
}
|
|
32
|
+
|
|
12
33
|
const onKeyDown = event => {
|
|
13
34
|
if (event.key === ' ') {
|
|
14
35
|
event.preventDefault()
|
|
15
36
|
}
|
|
16
37
|
}
|
|
38
|
+
|
|
17
39
|
const onKeyUp = event => {
|
|
18
40
|
if (event.key === ' ') {
|
|
19
|
-
|
|
41
|
+
setUnseenCount(0)
|
|
42
|
+
setChatVisibility(!isChatOpen)
|
|
20
43
|
}
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
const intersectionCallback = entries => {
|
|
24
|
-
const [entry] = entries
|
|
25
|
-
const isBelowFold = !entry.isIntersecting && entry.boundingClientRect.top > 0
|
|
26
|
-
setFixed(!isOpen && isBelowFold)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
46
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
33
|
-
const parentElement = buttonRef.current?.parentElement
|
|
34
|
-
if (parentElement) {
|
|
35
|
-
observer.observe(parentElement)
|
|
47
|
+
if (showUnseenCount) {
|
|
48
|
+
setLiveRegionText(`Floodline webchat - ${unseenCount} ${messageText}`)
|
|
36
49
|
}
|
|
50
|
+
|
|
37
51
|
return () => {
|
|
38
|
-
|
|
39
|
-
observer.unobserve(parentElement)
|
|
40
|
-
}
|
|
52
|
+
setLiveRegionText()
|
|
41
53
|
}
|
|
42
|
-
}, [
|
|
54
|
+
}, [unseenCount, isChatOpen])
|
|
43
55
|
|
|
44
56
|
useEffect(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const onScroll = () => {
|
|
58
|
+
if (!threadId) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (availability !== 'AVAILABLE') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rect = buttonRef.current.parentElement.getBoundingClientRect()
|
|
67
|
+
|
|
68
|
+
const isBelowFold = rect.top + 35 > (window.innerHeight || document.documentElement.clientHeight)
|
|
69
|
+
const isFixed = !isChatOpen && isBelowFold
|
|
70
|
+
|
|
71
|
+
document.documentElement.classList.toggle('wc-u-scroll-padding', isFixed)
|
|
72
|
+
document.body.classList.toggle('wc-u-scroll-padding', isFixed)
|
|
73
|
+
buttonRef.current.classList.toggle('wc-availability--fixed', isFixed)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
document.addEventListener('scroll', onScroll)
|
|
77
|
+
onScroll()
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
document.removeEventListener('scroll', onScroll)
|
|
81
|
+
}
|
|
82
|
+
}, [threadId, isChatOpen, availability])
|
|
48
83
|
|
|
49
|
-
switch (
|
|
84
|
+
switch (availability) {
|
|
50
85
|
case 'AVAILABLE':
|
|
51
86
|
return (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
<>
|
|
88
|
+
<div
|
|
89
|
+
className={classnames('wc-availability', isChatOpen && 'wc-open')}
|
|
90
|
+
ref={buttonRef}
|
|
91
|
+
>
|
|
92
|
+
<div className='wc-availability__inner'>
|
|
93
|
+
<a
|
|
94
|
+
id='webchat-start-chat-link'
|
|
95
|
+
className='wc-availability__link'
|
|
96
|
+
href='#webchat'
|
|
97
|
+
draggable='false'
|
|
98
|
+
onClick={onClick}
|
|
99
|
+
onKeyUp={onKeyUp}
|
|
100
|
+
onKeyDown={onKeyDown}
|
|
101
|
+
role='button'
|
|
102
|
+
data-module='govuk-button'
|
|
103
|
+
>
|
|
104
|
+
{!threadId ? <>Start chat</> : <>Show chat</>}
|
|
105
|
+
{showUnseenCount
|
|
106
|
+
? (
|
|
107
|
+
<span className='wc-availability__unseen'>
|
|
108
|
+
{unseenCount}
|
|
109
|
+
<span className='govuk-visually-hidden'> {messageText}</span>
|
|
110
|
+
</span>
|
|
111
|
+
)
|
|
112
|
+
: null}
|
|
113
|
+
</a>
|
|
114
|
+
</div>
|
|
66
115
|
</div>
|
|
67
|
-
|
|
116
|
+
{!isChatOpen ? <LiveRegion /> : <Panel />}
|
|
117
|
+
<SkipLink />
|
|
118
|
+
</>
|
|
68
119
|
)
|
|
69
120
|
case 'EXISTING':
|
|
70
121
|
case 'UNAVAILABLE':
|
|
@@ -77,25 +128,3 @@ export function Availability (props) {
|
|
|
77
128
|
)
|
|
78
129
|
}
|
|
79
130
|
}
|
|
80
|
-
|
|
81
|
-
function AvailabilityContent () {
|
|
82
|
-
const [thread] = useMessageThread()
|
|
83
|
-
const unreadMessageCount = thread.filter(message => !message.read).length
|
|
84
|
-
if (!thread.length) {
|
|
85
|
-
return (
|
|
86
|
-
<>
|
|
87
|
-
Start Chat
|
|
88
|
-
</>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
return (
|
|
92
|
-
<>
|
|
93
|
-
Show Chat {!!unreadMessageCount && (
|
|
94
|
-
<>
|
|
95
|
-
<span className='wc-availability__unseen'>{unreadMessageCount}</span>
|
|
96
|
-
<span className='govuk-visually-hidden'> {unreadMessageCount === 1 ? 'new message' : 'new messages'}</span>
|
|
97
|
-
</>
|
|
98
|
-
)}
|
|
99
|
-
</>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// stylelint-disable selector-max-id
|
|
2
|
+
#wc-availability {
|
|
3
|
+
margin-bottom: govuk-spacing(4);
|
|
4
|
+
}
|
|
5
|
+
// stylelint-enable selector-max-id
|
|
1
6
|
|
|
2
7
|
.wc-availability__inner {
|
|
3
8
|
max-width: 960px;
|
|
@@ -12,6 +17,10 @@
|
|
|
12
17
|
right: 0;
|
|
13
18
|
padding-left: 15px;
|
|
14
19
|
padding-right: 15px;
|
|
20
|
+
border: 1px solid transparent;
|
|
21
|
+
@media (forced-colors: active) {
|
|
22
|
+
border-color: currentColor;
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
@include mq ($from: 'tablet') {
|
|
17
26
|
padding-left: 30px;
|
|
@@ -27,52 +36,31 @@
|
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
.wc-availability__link {
|
|
39
|
+
@extend %govuk-link;
|
|
40
|
+
display: inline-block;
|
|
30
41
|
@include govuk-font($size: 19);
|
|
31
|
-
position:relative;
|
|
32
|
-
display:inline-block;
|
|
33
42
|
margin-right: 5px;
|
|
34
|
-
margin-bottom: 0;
|
|
35
|
-
fill: govuk-colour('black');
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
&:visited:not(:hover):not(:focus) {
|
|
45
|
+
color: govuk-colour('blue');
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
.govuk-visually-hidden, .wc-availability__unseen {
|
|
42
49
|
pointer-events: none;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
&:hover {
|
|
46
|
-
color:
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
&:active {
|
|
50
|
-
color: govuk-colour('black');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
&:visited {
|
|
54
|
-
color: $govuk-link-colour;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
&:focus {
|
|
58
|
-
box-shadow: none;
|
|
59
|
-
background-color: transparent;
|
|
60
|
-
text-decoration: underline;
|
|
61
|
-
color: $govuk-focus-colour;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
&:hover:not(:focus):not(:active) {
|
|
65
|
-
color: $govuk-link-hover-colour;
|
|
66
|
-
|
|
67
|
-
.wc-availability__unseen {
|
|
68
|
-
background-color: govuk-colour('dark-blue');
|
|
69
|
-
}
|
|
52
|
+
&:hover:not(:focus):not(:active) .wc-availability__unseen {
|
|
53
|
+
background-color: govuk-colour('dark-blue');
|
|
70
54
|
}
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
.wc-availability__unseen {
|
|
74
58
|
display: inline-block;
|
|
75
59
|
position: relative;
|
|
60
|
+
border: 1px solid transparent;
|
|
61
|
+
@media (forced-colors: active) {
|
|
62
|
+
border-color: currentColor;
|
|
63
|
+
}
|
|
76
64
|
border-radius: 10px;
|
|
77
65
|
background-color: govuk-colour('black');
|
|
78
66
|
color: govuk-colour('white');
|
|
@@ -86,4 +74,4 @@
|
|
|
86
74
|
padding-left: 7px;
|
|
87
75
|
padding-right: 7px;
|
|
88
76
|
}
|
|
89
|
-
}
|
|
77
|
+
}
|