@spokane-folio/security-incident 1.0.28

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 (188) hide show
  1. package/.eslintrc +32 -0
  2. package/.github/workflows/CODEOWNERS +8 -0
  3. package/.github/workflows/pr-validation.yml +44 -0
  4. package/.github/workflows/release.yml +64 -0
  5. package/.prettierrc +6 -0
  6. package/.stripesclirc +4 -0
  7. package/CHANGELOG.md +8 -0
  8. package/CONTRIBUTING.md +4 -0
  9. package/LICENSE +201 -0
  10. package/README.md +16 -0
  11. package/administrator-documentation/roles-and-permissions.md +65 -0
  12. package/administrator-documentation/track-settings-admin-guide-sketch.md +192 -0
  13. package/administrator-documentation/using-the-application.md +192 -0
  14. package/icons/app.png +0 -0
  15. package/icons/app.svg +1 -0
  16. package/icons/playButton.png +0 -0
  17. package/icons/profilePicThumbnail.png +0 -0
  18. package/jest.config.js +10 -0
  19. package/module-descriptor.json +75 -0
  20. package/output/service-worker.js +0 -0
  21. package/package.json +146 -0
  22. package/src/components/incidents/ColumnChooser.js +37 -0
  23. package/src/components/incidents/CreateMedia.js +132 -0
  24. package/src/components/incidents/CreatePane.js +1215 -0
  25. package/src/components/incidents/CreatePane.test.js +138 -0
  26. package/src/components/incidents/CreateReport.js +102 -0
  27. package/src/components/incidents/DetailsPane.js +1267 -0
  28. package/src/components/incidents/DetailsPane.test.js +150 -0
  29. package/src/components/incidents/EditPane.js +2334 -0
  30. package/src/components/incidents/EditPane.test.js +187 -0
  31. package/src/components/incidents/GetDetails.js +55 -0
  32. package/src/components/incidents/GetListDQLinkIncident.js +81 -0
  33. package/src/components/incidents/GetListDynamicQuery.js +66 -0
  34. package/src/components/incidents/GetLocations.js +57 -0
  35. package/src/components/incidents/GetMedia.js +98 -0
  36. package/src/components/incidents/GetName.js +111 -0
  37. package/src/components/incidents/GetNameCreatedBy.js +94 -0
  38. package/src/components/incidents/GetOrgLocaleSettings.js +61 -0
  39. package/src/components/incidents/GetPatronGroups.js +52 -0
  40. package/src/components/incidents/GetSelf.js +65 -0
  41. package/src/components/incidents/GetSummary.js +110 -0
  42. package/src/components/incidents/IncidentTypeCard.js +53 -0
  43. package/src/components/incidents/IncidentTypeCard.test.js +133 -0
  44. package/src/components/incidents/IncidentsPaneset.js +810 -0
  45. package/src/components/incidents/IncidentsPaneset.test.js +128 -0
  46. package/src/components/incidents/LinkedIncident.js +86 -0
  47. package/src/components/incidents/ModalAddMedia.js +262 -0
  48. package/src/components/incidents/ModalAddMedia.test.js +97 -0
  49. package/src/components/incidents/ModalAttentionDecOfService.js +111 -0
  50. package/src/components/incidents/ModalCustomWitness.js +469 -0
  51. package/src/components/incidents/ModalCustomWitness.test.js +147 -0
  52. package/src/components/incidents/ModalCustomerDetails.js +480 -0
  53. package/src/components/incidents/ModalCustomerDetails.test.js +116 -0
  54. package/src/components/incidents/ModalDescribeCustomer.js +361 -0
  55. package/src/components/incidents/ModalDescribeCustomer.test.js +156 -0
  56. package/src/components/incidents/ModalDirtyFormWarn.js +62 -0
  57. package/src/components/incidents/ModalLinkIncident.js +1213 -0
  58. package/src/components/incidents/ModalLinkIncidentStyle.css +32 -0
  59. package/src/components/incidents/ModalSelectIncidentTypes.js +178 -0
  60. package/src/components/incidents/ModalSelectIncidentTypes.test.js +273 -0
  61. package/src/components/incidents/ModalSelectKnownCustomer.js +395 -0
  62. package/src/components/incidents/ModalSelectWitness.js +406 -0
  63. package/src/components/incidents/ModalSelectWitness.test.js +308 -0
  64. package/src/components/incidents/ModalStyle.css +44 -0
  65. package/src/components/incidents/ModalTrespass.js +741 -0
  66. package/src/components/incidents/ModalViewCustomerDetails.js +241 -0
  67. package/src/components/incidents/ModalViewMedia.js +86 -0
  68. package/src/components/incidents/ModalViewTrespass.js +210 -0
  69. package/src/components/incidents/ResultsPane.js +437 -0
  70. package/src/components/incidents/ResultsPane.test.js +120 -0
  71. package/src/components/incidents/SearchCustomerOrWitness.js +108 -0
  72. package/src/components/incidents/Thumbnail.js +72 -0
  73. package/src/components/incidents/ThumbnailMarkRemoval.js +38 -0
  74. package/src/components/incidents/ThumbnailSkeleton.js +30 -0
  75. package/src/components/incidents/ThumbnailStyles.js +49 -0
  76. package/src/components/incidents/ThumbnailTempPreSave.js +71 -0
  77. package/src/components/incidents/UpdateReport.js +84 -0
  78. package/src/components/incidents/__snapshots__/CreatePane.test.js.snap +3 -0
  79. package/src/components/incidents/__snapshots__/DetailsPane.test.js.snap +3 -0
  80. package/src/components/incidents/__snapshots__/EditPane.test.js.snap +3 -0
  81. package/src/components/incidents/__snapshots__/IncidentTypeCard.test.js.snap +3 -0
  82. package/src/components/incidents/__snapshots__/IncidentsPaneset.test.js.snap +3 -0
  83. package/src/components/incidents/__snapshots__/ModalAddMedia.test.js.snap +3 -0
  84. package/src/components/incidents/__snapshots__/ModalCustomerDetails.test.js.snap +3 -0
  85. package/src/components/incidents/__snapshots__/ModalSelectWitness.test.js.snap +3 -0
  86. package/src/components/incidents/__snapshots__/ResultsPane.test.js.snap +3 -0
  87. package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.css +5 -0
  88. package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.js +51 -0
  89. package/src/components/incidents/helpers/ProfilePicture/isAValidURL.js +3 -0
  90. package/src/components/incidents/helpers/ProfilePicture/useProfilePicture.js +127 -0
  91. package/src/components/incidents/helpers/buildQueryString.js +28 -0
  92. package/src/components/incidents/helpers/cleanFormValues.js +53 -0
  93. package/src/components/incidents/helpers/computeEditedCustomers.js +124 -0
  94. package/src/components/incidents/helpers/convertDateIgnoringTZ.js +8 -0
  95. package/src/components/incidents/helpers/convertUTCISOToLocalePrettyTime.js +15 -0
  96. package/src/components/incidents/helpers/convertUTCISOToPrettyDate.js +19 -0
  97. package/src/components/incidents/helpers/decodeParamsToForm.js +20 -0
  98. package/src/components/incidents/helpers/deepNormalizeForComparison.js +39 -0
  99. package/src/components/incidents/helpers/extractFilterString.js +12 -0
  100. package/src/components/incidents/helpers/formatDateAndTimeToUTCISO.js +14 -0
  101. package/src/components/incidents/helpers/formatDateToUTCISO.js +14 -0
  102. package/src/components/incidents/helpers/formatTimeToUTCISO.js +28 -0
  103. package/src/components/incidents/helpers/getCurrentTime.js +20 -0
  104. package/src/components/incidents/helpers/getTodayDate.js +12 -0
  105. package/src/components/incidents/helpers/handlebarsHelpers.js +148 -0
  106. package/src/components/incidents/helpers/hasFormChangedAtCreate.js +50 -0
  107. package/src/components/incidents/helpers/hasTopLevelChangeAffectedDeclaration.js +90 -0
  108. package/src/components/incidents/helpers/hasTopLevelFormChanged.js +111 -0
  109. package/src/components/incidents/helpers/identifyCurrentTrespassDocs.js +109 -0
  110. package/src/components/incidents/helpers/isSameHtml.js +13 -0
  111. package/src/components/incidents/helpers/isValidDateFormat.js +14 -0
  112. package/src/components/incidents/helpers/isValidTimeInput.js +11 -0
  113. package/src/components/incidents/helpers/isValidUTCTimeFormat.js +14 -0
  114. package/src/components/incidents/helpers/parseMMDDYYYY.js +7 -0
  115. package/src/components/incidents/helpers/parseQueryString.js +16 -0
  116. package/src/components/incidents/helpers/sortTrespassDocuments.js +44 -0
  117. package/src/components/incidents/helpers/stripHTML.js +11 -0
  118. package/src/components/incidents/helpers/trespassDocUtils.js +197 -0
  119. package/src/components/incidents/helpers/validateTrespassDetails.js +37 -0
  120. package/src/components/incidents/usePersistedColModalLink.js +70 -0
  121. package/src/components/incidents/usePersistedColumns.js +70 -0
  122. package/src/components/incidents/usePersistedSort.js +23 -0
  123. package/src/components/incidents/usePersistedSortModalLink.js +23 -0
  124. package/src/contexts/IncidentContext.js +433 -0
  125. package/src/index.js +61 -0
  126. package/src/routes/Application.js +13 -0
  127. package/src/settings/GetIncidentCategories.js +56 -0
  128. package/src/settings/GetIncidentTypesDetails.js +88 -0
  129. package/src/settings/GetIncidentTypesIds.js +74 -0
  130. package/src/settings/GetLocationsInService.js +54 -0
  131. package/src/settings/GetSingleCustomLocationDetails.js +60 -0
  132. package/src/settings/GetSingleIncidentTypeDetails.js +60 -0
  133. package/src/settings/GetTrespassReasons.js +67 -0
  134. package/src/settings/GetTrespassTemplates.js +51 -0
  135. package/src/settings/IncidentCategoriesPane.js +285 -0
  136. package/src/settings/IncidentCategoriesPane.test.js +229 -0
  137. package/src/settings/IncidentTypeDetailsPane.js +215 -0
  138. package/src/settings/IncidentTypeDetailsPane.test.js +220 -0
  139. package/src/settings/IncidentTypeEditPane.js +211 -0
  140. package/src/settings/IncidentTypeEditPane.test.js +170 -0
  141. package/src/settings/IncidentTypesPaneset.js +167 -0
  142. package/src/settings/IncidentTypesPaneset.test.js +124 -0
  143. package/src/settings/LocationInServiceEditPane.js +320 -0
  144. package/src/settings/LocationsPaneset.js +415 -0
  145. package/src/settings/LocationsPaneset.test.js +106 -0
  146. package/src/settings/ModalDeleteCategory.js +47 -0
  147. package/src/settings/ModalDeleteIncidentType.js +49 -0
  148. package/src/settings/ModalDeleteLocationInService.js +49 -0
  149. package/src/settings/ModalDeleteTrespassReason.js +49 -0
  150. package/src/settings/ModalPreviewTrespassDoc.js +65 -0
  151. package/src/settings/ModalTrespassDocTokens.js +83 -0
  152. package/src/settings/NewIncidentTypePane.js +182 -0
  153. package/src/settings/PutIncidentType.js +60 -0
  154. package/src/settings/PutLocationsInService.js +52 -0
  155. package/src/settings/PutTrespassReasons.js +61 -0
  156. package/src/settings/PutTrespassTemplate.js +50 -0
  157. package/src/settings/TrespassDoc.css +17 -0
  158. package/src/settings/TrespassDocDetailsPane.js +215 -0
  159. package/src/settings/TrespassDocEditPane.js +538 -0
  160. package/src/settings/TrespassDocPaneset.js +581 -0
  161. package/src/settings/TrespassReasonDetailsPane.js +171 -0
  162. package/src/settings/TrespassReasonEditPane.js +221 -0
  163. package/src/settings/TrespassReasonsPaneset.js +282 -0
  164. package/src/settings/__snapshots__/IncidentCategoriesPane.test.js.snap +3 -0
  165. package/src/settings/__snapshots__/IncidentTypeDetailsPane.test.js.snap +3 -0
  166. package/src/settings/__snapshots__/IncidentTypeEditPane.test.js.snap +3 -0
  167. package/src/settings/__snapshots__/IncidentTypesPaneset.test.js.snap +3 -0
  168. package/src/settings/__snapshots__/LocationsPaneset.test.js.snap +3 -0
  169. package/src/settings/data/exampleJSON.json +92 -0
  170. package/src/settings/data/templateTokens.js +396 -0
  171. package/src/settings/helpers/alphabetize.js +18 -0
  172. package/src/settings/helpers/getCategoryTitleById.js +13 -0
  173. package/src/settings/helpers/makeId.js +15 -0
  174. package/src/settings/index.js +48 -0
  175. package/stripes.config.js +10 -0
  176. package/test/jest/__mock__/index.js +8 -0
  177. package/test/jest/__mock__/intl.mock.js +27 -0
  178. package/test/jest/__mock__/stripes.mock.js +26 -0
  179. package/test/jest/__mock__/stripesComponents.mock.js +151 -0
  180. package/test/jest/__mock__/stripesConfig.mock.js +1 -0
  181. package/test/jest/__mock__/stripesCore.mock.js +9 -0
  182. package/test/jest/__mock__/stripesIcon.mock.js +5 -0
  183. package/test/jest/__mock__/stripesSmartComponents.mock.js +7 -0
  184. package/test/jest/__mock__/stripesUtils.mock.js +3 -0
  185. package/test/jest/eslintrc.js +12 -0
  186. package/test/jest/setupFiles.js +5 -0
  187. package/translations/ui-security-incident/en_US.json +542 -0
  188. package/ui-module-acceptance-criteria.md +34 -0
@@ -0,0 +1,437 @@
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { useIntl, FormattedMessage } from 'react-intl';
3
+ import { useHistory, useLocation } from 'react-router-dom';
4
+ import { useStripes } from '@folio/stripes/core';
5
+ import {
6
+ Icon,
7
+ Pane,
8
+ MultiColumnList,
9
+ Button,
10
+ PaneHeader,
11
+ LoadingPane,
12
+ } from '@folio/stripes/components';
13
+ import { useIncidents } from '../../contexts/IncidentContext';
14
+ import GetIncidentTypesDetails from '../../settings/GetIncidentTypesDetails';
15
+ import convertUTCISOToPrettyDate from './helpers/convertUTCISOToPrettyDate';
16
+ import ColumnChooser from './ColumnChooser.js';
17
+ import usePersistedColumns from './usePersistedColumns.js';
18
+ import usePersistedSort from './usePersistedSort.js';
19
+ import buildQueryString from './helpers/buildQueryString.js';
20
+
21
+ const ResultsPane = ({
22
+ handlePagination,
23
+ ...props
24
+ } ) => {
25
+ const stripes = useStripes();
26
+ const history = useHistory();
27
+ const location = useLocation();
28
+ const intl = useIntl();
29
+
30
+ const {
31
+ openDetailsPane,
32
+ openCreatePane,
33
+ isLoadingSearch,
34
+ incidentsList,
35
+ limit,
36
+ offset,
37
+ totalResults,
38
+ appliedSearchParams
39
+ } = useIncidents();
40
+
41
+ const {
42
+ sortColumn,
43
+ sortDirection,
44
+ setSortColumn,
45
+ setSortDirection
46
+ } = usePersistedSort();
47
+
48
+ const [sortedData, setSortedData] = useState([]); // final data prop for MCL
49
+
50
+
51
+ const handleCreateReport = () => {
52
+ openCreatePane();
53
+ history.push(`/incidents/create`);
54
+ };
55
+
56
+ const handleViewDetails = (event, row) => {
57
+ const id = row.id;
58
+ openDetailsPane(id);
59
+ sessionStorage.setItem('lastTrackListRoute', location.pathname + location.search);
60
+ history.push(`/incidents/${id}${location.search}`);
61
+ };
62
+
63
+ const customCameraSvg = (props) => (
64
+ <svg
65
+ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32" {...props}>
66
+ <g clipPath="url(#a)">
67
+ <path fill="#fff" d="M0 0h24v24H0z"/><path stroke="#000" strokeLinecap="round" strokeLinejoin="round" d="M3 8a1 1 0 0 1 1-1h4.5l1-3h5l1 3H20a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8Z"/>
68
+ <circle cx="12" cy="13" r="3" stroke="#000" strokeLinejoin="round"/>
69
+ </g>
70
+ <defs>
71
+ <clipPath id="a">
72
+ <path fill="#fff" d="M0 0h24v24H0z"/></clipPath>
73
+ </defs>
74
+ </svg>
75
+ );
76
+
77
+ const inlineIconStyle = {
78
+ display: 'inline-flex',
79
+ alignItems: 'center',
80
+ verticalAlign: 'middle',
81
+ lineHeight: '1.2',
82
+ gap: '4px',
83
+ marginLeft: '8px'
84
+ }
85
+
86
+ const resultsFormatter = {
87
+ customers: (item) => {
88
+ const isStaffSuppressed = item.staffSuppressed && item.staffSuppressed === true;
89
+ // if report has attachments of image or video, returns true
90
+ const hasImageOrVideo = item.attachments?.some((att) => {
91
+ return att.contentType.startsWith('image/') || att.contentType.startsWith('video/') || false
92
+ });
93
+ // if report has non trespass related document, returns true
94
+ const hasNonTrespassPDF = item.attachments?.some((att) => att.contentType === 'application/pdf' && !att.id?.toLowerCase()?.includes('trespass')) || false;
95
+
96
+ const customerNameList = item.customers?.map((cust, index) => {
97
+ const nameLabel = cust.customerNameLabel;
98
+ const custDescSnippet = cust.customerDescSnippet;
99
+ const trespassServed =
100
+ cust.trespass && cust.trespass.declarationOfService;
101
+ const endDateOfTrespass = cust.trespass?.endDateOfTrespass ?? null;
102
+ const isTrespassExpired =
103
+ endDateOfTrespass && new Date(endDateOfTrespass) < Date.now();
104
+
105
+ return (
106
+ <li key={index}>
107
+ {nameLabel ? nameLabel : custDescSnippet}
108
+
109
+ {/* render green check for has been served 'in hand' */}
110
+ {trespassServed && !isTrespassExpired ? (
111
+ <span style={{
112
+ ...inlineIconStyle,
113
+ color: 'green'
114
+ }}>
115
+ <Icon icon="check-circle"/>
116
+ </span>
117
+ ) : (
118
+ null
119
+ )}
120
+ {/* render X if currently trespassed, else trespass expired */}
121
+ {cust.trespass && !isTrespassExpired ? (
122
+ <span style={{
123
+ ...inlineIconStyle,
124
+ color: 'red'
125
+ }}>
126
+ <Icon
127
+ icon="times-circle-solid" />
128
+ {endDateOfTrespass ?
129
+ convertUTCISOToPrettyDate(endDateOfTrespass)
130
+ : <FormattedMessage id="results-pane.customers-formatter-trespass-no-end-date"/>}
131
+ </span>
132
+ ) : cust.trespass && isTrespassExpired ? (
133
+ <span style={{ marginLeft: '10px' }}>
134
+ <FormattedMessage id="results-pane.customers-formatter-trespass-expired"/>
135
+ </span>
136
+ ) : (
137
+ null
138
+ )}
139
+ {/* render icon if report has attachment(s) of image or video or non-trespass pdf*/}
140
+ {hasImageOrVideo || hasNonTrespassPDF ? (
141
+ <span style={{ ...inlineIconStyle, marginTop: '3px', }}>
142
+ <Icon
143
+ size='large'
144
+ iconClassName='cameraIcon'
145
+ icon={customCameraSvg}
146
+ aria-label='camera icon'
147
+ />
148
+ </span>
149
+ ) : (
150
+ null
151
+ )}
152
+ {isStaffSuppressed ? (
153
+ <span
154
+ style={{
155
+ display: 'inline-block',
156
+ marginBottom: '4px',
157
+ marginLeft: '3px',
158
+ verticalAlign: 'middle'
159
+ }}>
160
+ <Icon size='small' icon='exclamation-circle' status='warn'>
161
+ </Icon>
162
+ </span>)
163
+ : (
164
+ null
165
+ )}
166
+ </li>
167
+ );
168
+ });
169
+ return <ul style={{ margin: '0' }}>
170
+ {customerNameList?.length > 0 ?
171
+ customerNameList
172
+ : (
173
+ <li>
174
+ <FormattedMessage
175
+ id="results-pane.customers-formatter-no-associated-customers"
176
+ />
177
+ {hasImageOrVideo || hasNonTrespassPDF ? (
178
+ <span style={{ ...inlineIconStyle, marginTop: '3px', }}>
179
+ <Icon
180
+ size='large'
181
+ iconClassName='cameraIcon'
182
+ icon={customCameraSvg}
183
+ aria-label='camera icon'
184
+ />
185
+ </span>
186
+ ) : (
187
+ null
188
+ )}
189
+ {isStaffSuppressed ? (
190
+ <span
191
+ style={{
192
+ display: 'inline-block',
193
+ marginBottom: '2px',
194
+ marginLeft: '3px',
195
+ verticalAlign: 'middle'
196
+
197
+ }}>
198
+ <Icon size='small' icon='exclamation-circle' status='warn'>
199
+ </Icon>
200
+ </span>)
201
+ : (
202
+ null
203
+ )}
204
+ </li>
205
+ )
206
+ }
207
+ </ul>;
208
+ },
209
+ incidentLocation: (item) => {
210
+ return item.incidentLocationLabel ? item.incidentLocationLabel : item.incidentLocation
211
+ },
212
+ dateOfIncident: (item) => {
213
+ const readableDate = convertUTCISOToPrettyDate(item.dateTimeOfIncident);
214
+ return readableDate;
215
+ },
216
+ endDateOfTrespass: (item) => {
217
+ const readableDate = item.convertUTCISOToPrettyDate(endDateOfTrespass);
218
+ return readableDate;
219
+ },
220
+ incidentTypes: (item) => {
221
+ const typeList = item.incidentTypesLabels.map((type, index) => (
222
+ <li key={index} style={{ listStyleType: 'none', padding: '5px 0' }}>
223
+ {type}
224
+ </li>
225
+ ));
226
+
227
+ return <ul style={{ padding: '0', margin: '0' }}>{typeList}</ul>;
228
+ },
229
+ incidentWitnesses: (item) => {
230
+ const staffInvolvedNamesAssociatedKeys = item.incidentWitnesses?.map((wit) => {
231
+ const witnessNameLabel = wit.witnessNameLabel;
232
+ return (
233
+ <li key={wit.id} style={{ listStyleType: 'none', padding: '5px 0' }}>
234
+ {`${witnessNameLabel}`}
235
+ </li>
236
+ );
237
+ });
238
+
239
+ return <ul style={{ padding: '0', margin: '0' }}>
240
+ {staffInvolvedNamesAssociatedKeys}
241
+ </ul>
242
+ },
243
+ createdBy: (item) => {
244
+ return item.createdByLabel ? item.createdByLabel
245
+ : `${item.createdBy?.lastName}, ${item.createdBy?.firstName}`
246
+ },
247
+ trespassExpirationDates: (item) => {
248
+ const { customers = [] } = item;
249
+
250
+ if (customers.length === 0) {
251
+ return <FormattedMessage id="no-customers" />
252
+ }
253
+
254
+ const endDates = customers
255
+ .map(cust => cust.trespass?.endDateOfTrespass)
256
+ .filter(Boolean);
257
+
258
+ if (endDates.length === 0) {
259
+ return <FormattedMessage id="no-trespass" />;
260
+ }
261
+
262
+ return (
263
+ <ul style={{ paddingLeft: 20, margin: 0 }}>
264
+ {endDates.map((date, idx) => (
265
+ <li key={idx}>{convertUTCISOToPrettyDate(date)}</li>
266
+ ))}
267
+ </ul>
268
+ );
269
+ }
270
+ };
271
+
272
+ const columnWidths = {
273
+ customers: '375px',
274
+ incidentLocation: '175px',
275
+ dateOfIncident: '110px',
276
+ incidentTypes: '175px'
277
+ };
278
+
279
+ const resultCount = intl.formatMessage(
280
+ { id: `results-pane.paneSubTitle` },
281
+ { count: totalResults }
282
+ );
283
+
284
+ const columnsList = [
285
+ 'customers',
286
+ 'incidentLocation',
287
+ 'dateOfIncident',
288
+ 'incidentTypes',
289
+ 'incidentWitnesses',
290
+ 'createdBy',
291
+ 'trespassExpirationDates'
292
+ ];
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+ const fixedColumns = ['customers'];
301
+
302
+ const toggleableColumns = useMemo(() => [
303
+ 'incidentLocation',
304
+ 'dateOfIncident',
305
+ 'incidentTypes',
306
+ 'incidentWitnesses',
307
+ 'createdBy',
308
+ 'trespassExpirationDates'
309
+ ], []);
310
+
311
+ const allColumns = [...fixedColumns, ...toggleableColumns];
312
+
313
+ // const possibleColumns = useMemo(() => [...columnsList]);
314
+ const sortableFields = [...allColumns];
315
+
316
+ const columnLabels = {
317
+ customers: <FormattedMessage id="column-mapping.name" />,
318
+ incidentLocation: <FormattedMessage id="column-mapping.incidentLocation" />,
319
+ dateOfIncident: <FormattedMessage id="column-mapping.dateOfIncident" />,
320
+ incidentTypes: <FormattedMessage id="column-mapping.incidentTypes" />,
321
+ incidentWitnesses: <FormattedMessage id="column-mapping.witnessedBy" />,
322
+ createdBy: <FormattedMessage id="column-mapping.createdBy" />,
323
+ trespassExpirationDates: <FormattedMessage id="column-mapping.trespassExpirationDates" />
324
+ };
325
+
326
+ const [visibleToggleable, toggleColumn] = usePersistedColumns(toggleableColumns);
327
+
328
+ const visibleColumns = [...fixedColumns, ...visibleToggleable];
329
+
330
+ const actionMenu = ({ onToggle }) => (
331
+ <>
332
+ {stripes.hasPerm('ui-security-incident.edit') ?
333
+ <Button
334
+ style={{ marginTop: '10px' }}
335
+ buttonStyle="primary"
336
+ onClick={handleCreateReport}
337
+ >
338
+ <FormattedMessage id="results-pane.create-report-button" />
339
+ </Button>
340
+ : null
341
+ }
342
+ <ColumnChooser
343
+ possibleColumns={toggleableColumns}
344
+ visibleColumns={visibleToggleable}
345
+ toggleColumn={toggleColumn}
346
+ columnLabels={columnLabels}
347
+ />
348
+ </>
349
+ );
350
+
351
+ const renderHeader = (renderProps) => (
352
+ <PaneHeader
353
+ {...renderProps}
354
+ paneTitle={<FormattedMessage id="results-pane.paneTitle" />}
355
+ paneSub={resultCount}
356
+ actionMenu={actionMenu}
357
+ />
358
+ );
359
+
360
+ const handleSort = (_e, { name }) => {
361
+ const nextCol = name;
362
+ const nextDir =
363
+ name === sortColumn && sortDirection === 'asc' ? 'desc' : 'asc';
364
+
365
+ setSortColumn(nextCol);
366
+ setSortDirection(nextDir);
367
+
368
+ const qs = buildQueryString(
369
+ { ...appliedSearchParams, limit, offset: 0 }, // always restart at offset 0
370
+ nextCol,
371
+ nextDir
372
+ );
373
+ history.push(`/incidents?${qs}`)
374
+ };
375
+
376
+ useEffect(() => {
377
+ setSortedData(incidentsList);
378
+ }, [incidentsList]);
379
+
380
+ return (
381
+ isLoadingSearch ? (
382
+ <LoadingPane defaultWidth="fill" paneTitle="Loading results..." />
383
+ ) : <Pane
384
+ paneTitle={<FormattedMessage id="results-pane.paneTitle"/>}
385
+ id="results-pane"
386
+ defaultWidth="75%"
387
+ {...props}
388
+ renderHeader={renderHeader}
389
+ >
390
+ <GetIncidentTypesDetails context='incidents'/>
391
+
392
+ <div style={{ height: '80vh', width: 'auto' }}>
393
+ <MultiColumnList
394
+ autosize
395
+ virtualize
396
+ showSortIndicator
397
+ sortableFields={sortableFields}
398
+ onHeaderClick={handleSort}
399
+ sortedColumn={sortColumn}
400
+ sortDirection={sortDirection === 'desc' ? 'descending' : 'ascending'}
401
+ contentData={sortedData}
402
+ pageAmount={limit}
403
+ totalCount={totalResults}
404
+ pagingOffset={offset}
405
+ pagingType='prev-next'
406
+ pagingCanGoNext={offset + limit < totalResults}
407
+ pagingCanGoPrevious={offset > 0}
408
+ onNeedMoreData={(askAmount, index) => handlePagination(askAmount, index)}
409
+ formatter={resultsFormatter}
410
+ visibleColumns={visibleColumns}
411
+ columnMapping={{
412
+ customers: <FormattedMessage id="column-mapping.name" />,
413
+ incidentLocation:
414
+ <FormattedMessage id="column-mapping.incidentLocation" />
415
+ ,
416
+ dateOfIncident:
417
+ <FormattedMessage id="column-mapping.dateOfIncident" />
418
+ ,
419
+ incidentTypes:
420
+ <FormattedMessage id="column-mapping.incidentTypes" />
421
+ ,
422
+ incidentWitnesses:
423
+ <FormattedMessage id="column-mapping.witnessedBy" />,
424
+ createdBy:
425
+ <FormattedMessage id="column-mapping.createdBy" />,
426
+ trespassExpirationDates:
427
+ <FormattedMessage id="column-mapping.trespassExpirationDates" />,
428
+ }}
429
+ columnWidths={columnWidths}
430
+ onRowClick={handleViewDetails}
431
+ />
432
+ </div>
433
+ </Pane>
434
+ );
435
+ };
436
+
437
+ export default ResultsPane;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ResultsPane.test.js
3
+ * Snapshot + smoke tests for the ResultsPane container.
4
+ */
5
+ import React from 'react';
6
+ import { act } from 'react-dom/test-utils';
7
+ import { createRoot } from 'react-dom/client';
8
+ import ResultsPane from './ResultsPane';
9
+
10
+ /* ------------------------------------------------------------------ *
11
+ 1 stripes core + UI widget mocks
12
+ * ------------------------------------------------------------------ */
13
+ jest.mock('@folio/stripes/core', () => ({
14
+ useStripes: () => ({ hasPerm: jest.fn(() => true) }),
15
+ stripesConnect: (Comp) => Comp,
16
+ }));
17
+
18
+ jest.mock('@folio/stripes/components', () => {
19
+ const React = require('react');
20
+ const make = (tag) => (p) => React.createElement(tag, p, p.children);
21
+
22
+ /* render actionMenu so ColumnChooser text appears */
23
+ const PaneHeader = (props) => {
24
+ const menu = props.actionMenu ? props.actionMenu({ onToggle: jest.fn() }) : null;
25
+ return React.createElement('div', props, menu, props.children);
26
+ };
27
+
28
+ return {
29
+ Icon: (p) => <span {...p}>{p.icon}</span>,
30
+ Pane: make('div'),
31
+ PaneHeader,
32
+ PaneMenu: make('div'),
33
+ MultiColumnList: make('div'),
34
+ Button: make('button'),
35
+ LoadingPane: make('div'),
36
+ };
37
+ });
38
+
39
+ /* ------------------------------------------------------------------ *
40
+ 2 nested components & hooks
41
+ * ------------------------------------------------------------------ */
42
+ jest
43
+ .mock('../../settings/GetIncidentTypesDetails', () => () => <div>Mock GetIncidentTypesDetails</div>)
44
+ .mock('../../settings/GetLocationsInService', () => () => <div>Mock GetLocationsInService</div>)
45
+ .mock('./GetName', () => () => <div>Mock GetName</div>)
46
+ .mock('./ColumnChooser', () => () => <div>Mock ColumnChooser</div>);
47
+
48
+ jest.mock('./usePersistedColumns', () => () => [['customers', 'incidentLocation'], jest.fn()]);
49
+ jest.mock('./usePersistedSort', () => () => ({
50
+ sortColumn: '',
51
+ sortDirection: 'asc',
52
+ setSortColumn: jest.fn(),
53
+ setSortDirection: jest.fn(),
54
+ }));
55
+
56
+ jest.mock('./helpers/buildQueryString', () => jest.fn(() => 'mockedQueryString'));
57
+ jest.mock('./helpers/convertUTCISOToPrettyDate', () => jest.fn((d) => d));
58
+
59
+ /* ------------------------------------------------------------------ *
60
+ * 3 router + IncidentContext
61
+ * ------------------------------------------------------------------ */
62
+ const stableIncidentsList = []; // avoid new [] on each render
63
+ const stableAppliedFilters = {};
64
+
65
+ jest.mock('react-router-dom', () => ({
66
+ useLocation: () => ({ pathname: '/incidents', search: '' }),
67
+ useHistory: () => ({ push: jest.fn(), replace: jest.fn() }),
68
+ }));
69
+
70
+ jest.mock('../../contexts/IncidentContext', () => ({
71
+ useIncidents: () => ({
72
+ openDetailsPane: jest.fn(),
73
+ openCreatePane: jest.fn(),
74
+ isLoadingSearch: false,
75
+ incidentsList: stableIncidentsList,
76
+ appliedSearchParams: stableAppliedFilters,
77
+ incidentTypesList: [],
78
+ locationsInService: [],
79
+ limit: 20,
80
+ offset: 0,
81
+ totalResults: 0,
82
+ }),
83
+ }));
84
+
85
+ /* ------------------------------------------------------------------ *
86
+ 4 DOM setup / teardown (React-18 createRoot)
87
+ * ------------------------------------------------------------------ */
88
+ let container;
89
+ let root;
90
+ beforeEach(() => {
91
+ container = document.createElement('div');
92
+ document.body.appendChild(container);
93
+ root = createRoot(container);
94
+ });
95
+
96
+ afterEach(() => {
97
+ root.unmount();
98
+ document.body.removeChild(container);
99
+ container = null;
100
+ });
101
+
102
+ /* ------------------------------------------------------------------ *
103
+ 5 Tests
104
+ * ------------------------------------------------------------------ */
105
+ it('renders without crashing (snapshot)', () => {
106
+ act(() => {
107
+ root.render(<ResultsPane />);
108
+ });
109
+ expect(container.innerHTML).toMatchSnapshot();
110
+ });
111
+
112
+ it('mounts key child components', () => {
113
+ act(() => {
114
+ root.render(<ResultsPane />);
115
+ });
116
+ expect(container.textContent).toEqual(
117
+ expect.stringContaining('Mock GetIncidentTypesDetails')
118
+ );
119
+ });
120
+
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { IncidentContext } from '../../contexts/IncidentContext';
5
+
6
+ class SearchCustomerOrWitness extends React.Component {
7
+ static contextType = IncidentContext;
8
+
9
+ static manifest = Object.freeze({
10
+ customer: {
11
+ type: 'okapi',
12
+ accumulate: true,
13
+ },
14
+ });
15
+
16
+ static propTypes = {
17
+ term: PropTypes.string,
18
+ customer: PropTypes.shape({
19
+ records: PropTypes.object,
20
+ }),
21
+ mutator: PropTypes.shape({
22
+ customer: PropTypes.shape({
23
+ GET: PropTypes.func.isRequired,
24
+ }).isRequired,
25
+ }).isRequired,
26
+ };
27
+
28
+ componentDidMount() {
29
+ this.fetchCustomer();
30
+ }
31
+
32
+ componentDidUpdate(prevProps) {
33
+ if (this.props.term !== prevProps.term) {
34
+ this.fetchCustomer(this.props.term);
35
+ }
36
+ }
37
+
38
+ fetchCustomer() {
39
+ if (this.props.term) {
40
+ this.context.openLoadingSearch();
41
+ let cleanedInput = this.props.term.replace(/[^\w\s]/g, '');
42
+ const terms = cleanedInput.split(/\s+/).filter(Boolean);
43
+ let completeQuery = ''
44
+
45
+ const searchFields = [
46
+ 'personal.firstName',
47
+ 'personal.lastName',
48
+ 'personal.middleName',
49
+ 'barcode'
50
+ ];
51
+
52
+ const termQueries = terms.map((term) => {
53
+ const encoded = encodeURIComponent(term);
54
+ const orExpressions = searchFields
55
+ .map((field) => `${field}="${encoded}*"`)
56
+ .join(' or ');
57
+ return `(${orExpressions})`;
58
+ });
59
+
60
+ completeQuery = termQueries.join(' and ');
61
+
62
+ const finalQuery = `(${completeQuery}) sortby personal.lastName personal.firstName`;
63
+ // console.log('@SearchCustomerOrWitness - finalQuery: ', finalQuery);
64
+
65
+ this.props.mutator.customer
66
+ .GET({ path: `users?query=${finalQuery}&limit=1000` })
67
+ .then((records) => {
68
+ const list = records.users;
69
+ const refinedList = list.map((user) => {
70
+ return {
71
+ id: user.id,
72
+ barcode: user.barcode,
73
+ firstName: user.personal.firstName,
74
+ lastName: user.personal.lastName,
75
+ middleName: user.personal.middleName,
76
+ active: user.active,
77
+ profilePicLinkOrUUID: user.personal.profilePictureLink ? user.personal.profilePictureLink : '',
78
+ patronGroup: user.patronGroup
79
+ };
80
+ });
81
+ // console.log("@SearchCustomerOrWitness - refinedList: ", JSON.stringify(refinedList, null, 2))
82
+ this.context.setCustomers(refinedList);
83
+ this.context.closeLoadingSearch();
84
+ });
85
+ } else {
86
+ console.log('@fetchCustomer - something went wrong - no data');
87
+ }
88
+ };
89
+
90
+ render() {
91
+ return <></>;
92
+ };
93
+ };
94
+
95
+ SearchCustomerOrWitness.contextType = IncidentContext;
96
+ SearchCustomerOrWitness.propTypes = {
97
+ term: PropTypes.string,
98
+ customer: PropTypes.shape({
99
+ records: PropTypes.object,
100
+ }),
101
+ mutator: PropTypes.shape({
102
+ customer: PropTypes.shape({
103
+ GET: PropTypes.func.isRequired,
104
+ }).isRequired,
105
+ }).isRequired,
106
+ };
107
+
108
+ export default stripesConnect(SearchCustomerOrWitness, '@spokane-folio/security-incident');