@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,469 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { FormattedMessage } from 'react-intl';
3
+ import {
4
+ Button,
5
+ Col,
6
+ Modal,
7
+ Pane,
8
+ Paneset,
9
+ MessageBanner,
10
+ ModalFooter,
11
+ Row,
12
+ TextField,
13
+ } from '@folio/stripes/components';
14
+ import css from './ModalStyle.css';
15
+ import { v4 as uuidv4 } from 'uuid';
16
+ import { useIncidents } from '../../contexts/IncidentContext';
17
+
18
+
19
+ const ModalCustomWitness = ({
20
+ custWitViewObj,
21
+ custWitEditID,
22
+ custWitEditObj,
23
+ setCustWitEditObj,
24
+ setFormData,
25
+ context,
26
+ }) => {
27
+ const {
28
+ isModalCustomWitness,
29
+ closeModalCustomWitness,
30
+ setSelectedWitnesses,
31
+ selectedWitnesses
32
+ } = useIncidents();
33
+
34
+ const [customWitForm, setCustomWitForm] = useState({
35
+ firstName: '',
36
+ lastName: '',
37
+ role: '',
38
+ phone: '',
39
+ email: ''
40
+ });
41
+
42
+ // context of edit custom witness via CreatePane (selectedWitnesses)
43
+ useEffect(() => {
44
+ if (custWitEditID) {
45
+ const witnessToEdit = selectedWitnesses.find(wit => wit.id === custWitEditID);
46
+ if (witnessToEdit) {
47
+ setCustomWitForm({
48
+ firstName: witnessToEdit.firstName || '',
49
+ lastName: witnessToEdit.lastName || '',
50
+ role: witnessToEdit.role || '',
51
+ phone: witnessToEdit.phone || '',
52
+ email: witnessToEdit.email || ''
53
+ });
54
+ }
55
+ }
56
+ }, [custWitEditID, selectedWitnesses]);
57
+
58
+ // context of view custom witness
59
+ useEffect(() => {
60
+ if (custWitViewObj) {
61
+ setCustomWitForm({
62
+ firstName: custWitViewObj.firstName || '',
63
+ lastName: custWitViewObj.lastName || '',
64
+ role: custWitViewObj.role || '',
65
+ phone: custWitViewObj.phone || '',
66
+ email: custWitViewObj.email || ''
67
+ });
68
+ }
69
+ }, [custWitViewObj]);
70
+
71
+ // context of edit custom witness
72
+ useEffect(() => {
73
+ if (custWitEditObj) {
74
+ setCustomWitForm({
75
+ id: custWitEditObj.id || '',
76
+ firstName: custWitEditObj.firstName || '',
77
+ lastName: custWitEditObj.lastName || '',
78
+ role: custWitEditObj.role || '',
79
+ phone: custWitEditObj.phone || '',
80
+ email: custWitEditObj.email || '',
81
+ isCustom: true
82
+ });
83
+ }
84
+ }, [custWitEditObj]);
85
+
86
+ const [showPhoneError, setShowPhoneError] = useState(false);
87
+ const [showEmailError, setShowEmailError] = useState(false);
88
+
89
+ const handleChange = (event) => {
90
+ const { name, value } = event.target;
91
+ setCustomWitForm((prev) => ({
92
+ ...prev,
93
+ [name]: value,
94
+ }));
95
+ };
96
+
97
+ const handleCancel = () => {
98
+ setCustomWitForm({
99
+ firstName: '',
100
+ lastName: '',
101
+ role: '',
102
+ phone: '',
103
+ email: ''
104
+ });
105
+ // setCustWitEditID();
106
+ closeModalCustomWitness();
107
+ };
108
+
109
+ const handleCancelDetails = () => {
110
+ closeModalCustomWitness();
111
+ };
112
+
113
+ const handleOnEntered =() => {
114
+ setTimeout(() => {
115
+ setShowPhoneError(false);
116
+ setShowEmailError(false);
117
+ }, 4000)
118
+ };
119
+
120
+ const validatePhoneNumber = (phone) => {
121
+ // allow: 1234567890, 123-456-7890, 123 456 7890,
122
+ // (123)456-7890, (123) 456-7890, 11234567890,
123
+ // +11234567890, +1 123 456 7890, +1-123-456-7890
124
+ // not allow: too few digits, extra chars, leading zero country code,
125
+ // incorrect placement of parens, more than 4 digits in last section
126
+ const phoneRegex = /^(\+?[1-9]{1,2})?(\s|-)?\(?\d{3}\)?(\s|-)?\d{3}(\s|-)?\d{4}$/;
127
+ return phoneRegex.test(phone)
128
+ };
129
+
130
+ const validateEmail = (email) => {
131
+ // ensure local is one or more chars that are not space or @,
132
+ // ensure exactly one @,
133
+ // ensure domain one or more chars not space or @,
134
+ // ensure literal '.',
135
+ // ensure TLD 2-6 chars
136
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,6}$/;
137
+ return emailRegex.test(email);
138
+ };
139
+
140
+ const isFormDataPresent = () => {
141
+ const isFirstNameValid = customWitForm.firstName && customWitForm.firstName !== '';
142
+ const isLastNameValid = customWitForm.lastName && customWitForm.lastName !== '';
143
+ return isFirstNameValid && isLastNameValid;
144
+ };
145
+
146
+ // handle edit for uninstantiated (is local state) custom witness via CreatePane or EditPane
147
+ const handleSave = () => {
148
+ const phoneNumber = customWitForm.phone.trim();
149
+ const email = customWitForm.email.trim();
150
+ let hasError = false;
151
+ if (!validatePhoneNumber(phoneNumber) && phoneNumber !== '') {
152
+ setShowPhoneError(true)
153
+ hasError = true;
154
+ } else {
155
+ setShowPhoneError(false);
156
+ };
157
+ if (!validateEmail(email) && email !== '') {
158
+ setShowEmailError(true)
159
+ hasError = true;
160
+ } else {
161
+ setShowEmailError(false);
162
+ };
163
+ if (hasError) return;
164
+
165
+ const readyCustomWitness = {
166
+ id: custWitEditID || uuidv4(),
167
+ isCustom: true,
168
+ firstName: customWitForm.firstName.trim(),
169
+ lastName: customWitForm.lastName.trim(),
170
+ ...(customWitForm.role.trim() ? { role: customWitForm.role.trim() } : {}),
171
+ ...(customWitForm.phone.trim() ? { phone: customWitForm.phone.trim() } : {}),
172
+ ...(customWitForm.email.trim() ? { email: customWitForm.email.trim() } : {}),
173
+ };
174
+ setSelectedWitnesses(prevData => {
175
+ if (custWitEditID) {
176
+ return prevData.map(wit => wit.id === custWitEditID ?
177
+ readyCustomWitness : wit);
178
+ } else {
179
+ return [...prevData, readyCustomWitness];
180
+ }
181
+ });
182
+ setCustomWitForm({
183
+ firstName: '',
184
+ lastName: '',
185
+ role: '',
186
+ phone: '',
187
+ email: ''
188
+ });
189
+ closeModalCustomWitness();
190
+ };
191
+
192
+ // handle add new
193
+ const handleAddNewAtEdit = () => {
194
+ const phoneNumber = customWitForm.phone.trim();
195
+ const email = customWitForm.email.trim();
196
+ let hasError = false;
197
+ if (!validatePhoneNumber(phoneNumber) && phoneNumber !== '') {
198
+ setShowPhoneError(true)
199
+ hasError = true;
200
+ } else {
201
+ setShowPhoneError(false);
202
+ };
203
+ if (!validateEmail(email) && email !== '') {
204
+ setShowEmailError(true)
205
+ hasError = true;
206
+ } else {
207
+ setShowEmailError(false);
208
+ };
209
+ if (hasError) return;
210
+
211
+ setSelectedWitnesses(prevData => {
212
+ return [
213
+ ...prevData,
214
+ {
215
+ id: uuidv4(),
216
+ isCustom: true,
217
+ firstName: customWitForm.firstName.trim(),
218
+ lastName: customWitForm.lastName.trim(),
219
+ ...(customWitForm.role.trim() ? { role: customWitForm.role.trim() } : {}),
220
+ ...(customWitForm.phone.trim() ? { phone: customWitForm.phone.trim() } : {}),
221
+ ...(customWitForm.email.trim() ? { email: customWitForm.email.trim() } : {}),
222
+ }
223
+ ]
224
+ });
225
+ setCustomWitForm({
226
+ firstName: '',
227
+ lastName: '',
228
+ role: '',
229
+ phone: '',
230
+ email: ''
231
+ });
232
+ closeModalCustomWitness();
233
+ };
234
+
235
+ // handle edit an instantiated custom witness via edit pane
236
+ const handleSaveEditInstantiated = () => {
237
+ const phoneNumber = customWitForm.phone;
238
+ const email = customWitForm.email;
239
+ let hasError = false;
240
+
241
+ if (!validatePhoneNumber(phoneNumber) && phoneNumber !== '') {
242
+ setShowPhoneError(true)
243
+ hasError = true;
244
+ } else {
245
+ setShowPhoneError(false);
246
+ };
247
+
248
+ if (!validateEmail(email) && email !== '') {
249
+ setShowEmailError(true)
250
+ hasError = true;
251
+ } else {
252
+ setShowEmailError(false);
253
+ };
254
+
255
+ if (hasError) return;
256
+
257
+ setFormData(prevFormData => ({
258
+ ...prevFormData,
259
+ incidentWitnesses: prevFormData.incidentWitnesses.map(wit => wit.id === customWitForm.id ? {
260
+ ...customWitForm,
261
+ isCustom: true,
262
+ id: customWitForm.id,
263
+ firstName: customWitForm.firstName.trim(),
264
+ lastName: customWitForm.lastName.trim(),
265
+ ...(customWitForm.role.trim() ? { role: customWitForm.role.trim() } : {}),
266
+ ...(customWitForm.phone.trim() ? { phone: customWitForm.phone.trim() } : {}),
267
+ ...(customWitForm.email.trim() ? { email: customWitForm.email.trim() } : {}),
268
+
269
+ } : wit)
270
+ }));
271
+ setCustomWitForm({
272
+ firstName: '',
273
+ lastName: '',
274
+ role: '',
275
+ phone: '',
276
+ email: ''
277
+ });
278
+ setCustWitEditObj({}); // set to empty to resolve edit an unsaved custom wit
279
+ closeModalCustomWitness();
280
+ };
281
+
282
+ const footer = (
283
+ <ModalFooter>
284
+ <Button
285
+ onClick={
286
+ context === 'editSavedCustomWitness' ? handleSaveEditInstantiated
287
+ : context === 'addCustomWitAtEdit' ? handleAddNewAtEdit
288
+ : handleSave}
289
+ buttonStyle="primary"
290
+ disabled={!isFormDataPresent()}
291
+ marginBottom0
292
+ >
293
+ <FormattedMessage id="close-continue-button" />
294
+ </Button>
295
+ <Button onClick={handleCancel}>
296
+ <FormattedMessage id="cancel-button" />
297
+ </Button>
298
+ </ModalFooter>
299
+ );
300
+
301
+ const footerDetails = (
302
+ <ModalFooter>
303
+ <Button onClick={handleCancel}>
304
+ <FormattedMessage id="close-button" />
305
+ </Button>
306
+ </ModalFooter>
307
+ );
308
+
309
+ const rowStyle = { marginTop: '10px', marginLeft: '20px'};
310
+
311
+ if (!isModalCustomWitness) {
312
+ return null;
313
+ };
314
+
315
+ if (custWitViewObj) {
316
+ return (
317
+ <Modal
318
+ style={{ minHeight: '550px' }}
319
+ open
320
+ dismissible
321
+ closeOnBackgroundClick
322
+ label={<FormattedMessage id="modal-custom-witness-details-paneTitle" />}
323
+ size="medium"
324
+ onClose={handleCancelDetails}
325
+ footer={footerDetails}
326
+ contentClass={css.modalContent}
327
+ >
328
+ <Paneset>
329
+ <Pane defaultWidth='fill'>
330
+ <Row style={rowStyle}>
331
+ <Col xs={6}>
332
+ <strong>
333
+ <FormattedMessage id="modal-custom-witness-firstName-details"/>
334
+ </strong> {custWitViewObj.firstName}
335
+ </Col>
336
+ </Row>
337
+ <Row style={rowStyle}>
338
+ <Col xs={6}>
339
+ <strong>
340
+ <FormattedMessage id="modal-custom-witness-lastName-details"/>
341
+ </strong> {custWitViewObj.lastName}
342
+ </Col>
343
+ </Row>
344
+ <Row style={rowStyle}>
345
+ <Col xs={6}>
346
+ <strong>
347
+ <FormattedMessage id="modal-custom-witness-role-organization-details"/>
348
+ </strong> {custWitViewObj.role}
349
+ </Col>
350
+ </Row>
351
+ <Row style={rowStyle}>
352
+ <Col xs={6}>
353
+ <strong>
354
+ <FormattedMessage id="modal-custom-witness-phone-details"/>
355
+ </strong> {custWitViewObj.phone}
356
+ </Col>
357
+ </Row>
358
+ <Row style={rowStyle}>
359
+ <Col xs={6}>
360
+ <strong>
361
+ <FormattedMessage id="modal-custom-witness-email-details"/>
362
+ </strong> {custWitViewObj.email}
363
+ </Col>
364
+ </Row>
365
+ </Pane>
366
+ </Paneset>
367
+ </Modal>
368
+ );
369
+ };
370
+
371
+ return (
372
+ <Modal
373
+ style={{
374
+ minHeight: '550px', // minimum height when the browser is full size
375
+ height: '80%', // allows modal to grow/shrink based on content
376
+ maxHeight: '300vh', // modal will never exceed 90% of viewport height
377
+ maxWidth: '300vw', // modal width responsive to viewport width
378
+ width: '60%' // modal width adjusts based on content and window size
379
+ }}
380
+ open
381
+ dismissible
382
+ closeOnBackgroundClick
383
+ label={<FormattedMessage id="modal-custom-witness-details-paneTitle" />}
384
+ size="large"
385
+ onClose={handleCancel}
386
+ footer={footer}
387
+ contentClass={css.modalContent}
388
+ >
389
+ <Paneset>
390
+ <Pane>
391
+ <Row style={rowStyle}>
392
+ <Col xs={3}>
393
+ <TextField
394
+ required
395
+ onChange={handleChange}
396
+ value={customWitForm.firstName}
397
+ name="firstName"
398
+ label={<FormattedMessage id="modal-custom-witness-firstName-label"/>}
399
+ // <FormattedMessage id="" />
400
+ />
401
+ </Col>
402
+ </Row>
403
+ <Row style={rowStyle}>
404
+ <Col xs={3}>
405
+ <TextField
406
+ required
407
+ onChange={handleChange}
408
+ value={customWitForm.lastName}
409
+ name="lastName"
410
+ label={<FormattedMessage id="modal-custom-witness-lastName-label"/>}
411
+ />
412
+ </Col>
413
+ </Row>
414
+ <Row style={rowStyle}>
415
+ <Col xs={3}>
416
+ <TextField
417
+ onChange={handleChange}
418
+ value={customWitForm.role}
419
+ name="role"
420
+ label={<FormattedMessage id="modal-custom-witness-role-organization-label"/>}
421
+ />
422
+ </Col>
423
+ </Row>
424
+ <Row style={rowStyle}>
425
+ <Col xs={3}>
426
+ <TextField
427
+ onChange={handleChange}
428
+ value={customWitForm.phone}
429
+ name="phone"
430
+ label={<FormattedMessage id="modal-custom-witness-phone-label"/>}
431
+ />
432
+ </Col>
433
+ <Col xs={3} style={{ marginTop: '18px'}}>
434
+ <MessageBanner
435
+ onEntered={handleOnEntered}
436
+ show={showPhoneError}
437
+ type='error'
438
+ >
439
+ <FormattedMessage id="modal-custom-witness-error-phone"/>
440
+ </MessageBanner>
441
+ </Col>
442
+ </Row>
443
+
444
+ <Row style={rowStyle}>
445
+ <Col xs={3}>
446
+ <TextField
447
+ onChange={handleChange}
448
+ value={customWitForm.email}
449
+ name="email"
450
+ label={<FormattedMessage id="modal-custom-witness-email-label"/>}
451
+ />
452
+ </Col>
453
+ <Col xs={3} style={{ marginTop: '18px'}}>
454
+ <MessageBanner
455
+ onEntered={handleOnEntered}
456
+ show={showEmailError}
457
+ type='error'
458
+ >
459
+ <FormattedMessage id="modal-custom-witness-error-email"/>
460
+ </MessageBanner>
461
+ </Col>
462
+ </Row>
463
+ </Pane>
464
+ </Paneset>
465
+ </Modal>
466
+ );
467
+ };
468
+
469
+ export default ModalCustomWitness;
@@ -0,0 +1,147 @@
1
+ // Define the mutable IncidentContext mock (allowed because its name begins with "mock")
2
+ const mockIncidentContext = {
3
+ isModalCustomWitness: true, // default: modal open for tests
4
+ closeModalCustomWitness: jest.fn(),
5
+ setSelectedWitnesses: jest.fn(),
6
+ selectedWitnesses: [],
7
+ };
8
+
9
+ // --- Mocks for the Incident Context ---
10
+ jest.mock('../../contexts/IncidentContext', () => ({
11
+ useIncidents: () => mockIncidentContext,
12
+ }));
13
+
14
+ import React from 'react';
15
+ import ReactDOM from 'react-dom';
16
+ import { act } from 'react-dom/test-utils';
17
+ import ModalCustomWitness from './ModalCustomWitness';
18
+
19
+ // --- Suppress known nonfatal warnings ---
20
+ const originalConsoleError = console.error;
21
+ beforeEach(() => {
22
+ console.error = (...args) => {
23
+ if (
24
+ typeof args[0] === 'string' &&
25
+ (args[0].includes('ReactDOM.render is no longer supported') ||
26
+ args[0].includes('Invalid hook call') ||
27
+ args[0].includes('does not recognize the') ||
28
+ args[0].includes('Unknown event handler property') ||
29
+ args[0].includes('contentClass') ||
30
+ args[0].includes('defaultWidth'))
31
+ ) {
32
+ return;
33
+ }
34
+ originalConsoleError(...args);
35
+ };
36
+ });
37
+ afterEach(() => {
38
+ console.error = originalConsoleError;
39
+ });
40
+
41
+ // --- Mocks for External Dependencies ---
42
+
43
+ // Mock react-intl so that FormattedMessage simply renders its id.
44
+ jest.mock('react-intl', () => ({
45
+ FormattedMessage: (props) => <span>{props.id}</span>,
46
+ }));
47
+
48
+ // Update our mock for @folio/stripes/components so that TextField renders its label.
49
+ jest.mock('@folio/stripes/components', () => {
50
+ const React = require('react');
51
+ return {
52
+ Button: (props) => <button {...props}>{props.children}</button>,
53
+ Col: (props) => <div {...props}>{props.children}</div>,
54
+ Modal: (props) => <div {...props}>{props.children}</div>,
55
+ Paneset: (props) => <div {...props}>{props.children}</div>,
56
+ Pane: (props) => <div {...props}>{props.children}</div>,
57
+ ModalFooter: (props) => <div {...props}>{props.children}</div>,
58
+ Row: (props) => <div {...props}>{props.children}</div>,
59
+ // Modified TextField: renders a label and an input.
60
+ // Inside your jest.mock('@folio/stripes/components', ...) block:
61
+ TextField: (props) => {
62
+ // If props.label is an object and has a props.id (from FormattedMessage),
63
+ // then use that for rendering.
64
+ let labelText = '';
65
+ if (props.label) {
66
+ if (typeof props.label === 'object' && props.label.props && props.label.props.id) {
67
+ labelText = props.label.props.id;
68
+ } else {
69
+ labelText = props.label;
70
+ }
71
+ }
72
+ return (
73
+ <div>
74
+ {labelText && <label>{labelText}</label>}
75
+ <input type="text" {...props} />
76
+ </div>
77
+ );
78
+ },
79
+ // Render MessageBanner only if props.show is truthy.
80
+ MessageBanner: (props) => (props.show ? <div {...props}>{props.children}</div> : null),
81
+ };
82
+ });
83
+
84
+ // Mock the CSS module.
85
+ jest.mock('./ModalStyle.css', () => ({}));
86
+
87
+ // --- Test Setup ---
88
+ let container = null;
89
+ beforeEach(() => {
90
+ container = document.createElement('div');
91
+ document.body.appendChild(container);
92
+ });
93
+ afterEach(() => {
94
+ ReactDOM.unmountComponentAtNode(container);
95
+ container.remove();
96
+ container = null;
97
+ });
98
+
99
+ // --- Test Cases ---
100
+ describe('ModalCustomWitness', () => {
101
+ test('returns null when modal is closed', () => {
102
+ mockIncidentContext.isModalCustomWitness = false;
103
+ act(() => {
104
+ ReactDOM.render(<ModalCustomWitness />, container);
105
+ });
106
+ expect(container.innerHTML).toBe('');
107
+ });
108
+
109
+ test('renders view mode when custWitViewObj is provided', () => {
110
+ mockIncidentContext.isModalCustomWitness = true;
111
+ const custWitViewObj = {
112
+ firstName: 'Alice',
113
+ lastName: 'Smith',
114
+ role: 'Manager',
115
+ phone: '123-456-7890',
116
+ email: 'alice@example.com',
117
+ };
118
+ act(() => {
119
+ ReactDOM.render(
120
+ <ModalCustomWitness custWitViewObj={custWitViewObj} />,
121
+ container
122
+ );
123
+ });
124
+ const text = container.textContent;
125
+ // Expect the view mode to render detail labels and witness data.
126
+ expect(text).toMatch(/modal-custom-witness-firstName-details/);
127
+ expect(text).toMatch(/modal-custom-witness-lastName-details/);
128
+ expect(text).toMatch(/Alice/);
129
+ });
130
+
131
+ test('renders edit/add form when no custWitViewObj is provided', () => {
132
+ mockIncidentContext.isModalCustomWitness = true;
133
+ act(() => {
134
+ ReactDOM.render(
135
+ <ModalCustomWitness context="edit" />,
136
+ container
137
+ );
138
+ });
139
+ const text = container.textContent;
140
+ // Now the label should be rendered by our updated TextField mock.
141
+ expect(text).toMatch(/modal-custom-witness-firstName-label/);
142
+ expect(text).toMatch(/modal-custom-witness-lastName-label/);
143
+ });
144
+ });
145
+
146
+
147
+