@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,415 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { FormattedMessage } from 'react-intl';
3
+ import { useHistory, Switch, Route } from 'react-router-dom';
4
+ import {
5
+ AccordionSet,
6
+ Accordion,
7
+ Button,
8
+ Col,
9
+ Icon,
10
+ Label,
11
+ List,
12
+ MessageBanner,
13
+ MultiColumnList,
14
+ Pane,
15
+ Paneset,
16
+ Row,
17
+ Select,
18
+ TextArea,
19
+ TextField
20
+ } from '@folio/stripes/components';
21
+ import GetLocations from '../components/incidents/GetLocations';
22
+ import { useIncidents } from '../contexts/IncidentContext';
23
+ import GetLocationsInService from './GetLocationsInService';
24
+ import makeId from './helpers/makeId';
25
+ import PutLocationsInService from './PutLocationsInService';
26
+ import LocationInServiceEditPane from './LocationInServiceEditPane';
27
+
28
+ const LocationsPaneset = () => {
29
+ const history = useHistory();
30
+ const {
31
+ locations,
32
+ locationsInService,
33
+ setLocationsInService
34
+ } = useIncidents();
35
+ const [locationsDataOptions, setLocationsDataOptions] = useState([]);
36
+ const [selectValue, setSelectValue] = useState('');
37
+ const [formattedData, setFormattedData] = useState(null);
38
+ const [show, setShow] = useState(false);
39
+ const [showErrorBanner, setShowErrorBanner] = useState(false);
40
+ const [isButtonDisabled, setIsButtonDisabled] = useState(false);
41
+ const [endOfListTotal, setEndOfListTotal] = useState(0);
42
+
43
+ const [customLocationFormData, setCustomLocationFormData] = useState({
44
+ id: '',
45
+ location: '',
46
+ subLocations: []
47
+ });
48
+
49
+ const [newSubLocation, setNewSubLocation] = useState({
50
+ name: '',
51
+ description: '',
52
+ });
53
+
54
+ const handleMessageBannerEntered = () => {
55
+ setTimeout(() => {
56
+ setShow(false)
57
+ setShowErrorBanner(false)
58
+ }, 2800)
59
+ };
60
+
61
+ const handleRemoveSubLocation = (index) => {
62
+ const updatedSubLocations = customLocationFormData.subLocations.filter(
63
+ (_, idx) => idx !== index
64
+ );
65
+ setCustomLocationFormData({ ...customLocationFormData, subLocations: updatedSubLocations });
66
+ };
67
+
68
+ const handleAddSubLocation = () => {
69
+ if (newSubLocation && newSubLocation.name.trim() !== '') {
70
+ const updatedSubLocations = [...customLocationFormData.subLocations, newSubLocation];
71
+ setCustomLocationFormData({ ...customLocationFormData, subLocations: updatedSubLocations });
72
+ setTimeout(() => {
73
+ setNewSubLocation({ name: '', description: '' })
74
+ }, 200);
75
+ }
76
+ };
77
+
78
+ const handleNameInputChange = (e) => {
79
+ setNewSubLocation({
80
+ ...newSubLocation,
81
+ name: e.target.value
82
+ });
83
+ };
84
+
85
+ const handleDescriptionInputChange = (e) => {
86
+ setNewSubLocation({
87
+ ...newSubLocation,
88
+ description: e.target.value
89
+ });
90
+ };
91
+
92
+ useEffect(() => {
93
+ if (locationsInService) {
94
+ setEndOfListTotal(locationsInService.length);
95
+ };
96
+ }, [locationsInService])
97
+
98
+ const items = customLocationFormData.subLocations.map((loc) => {
99
+ return `${loc.name} - ${loc.description}`;
100
+ });
101
+
102
+ const itemFormatter = (item, index) => {
103
+ return (
104
+ <li key={index}>
105
+ {item}
106
+ <button
107
+ onClick={() => handleRemoveSubLocation(index)}
108
+ type="button"
109
+ aria-label={<FormattedMessage
110
+ id="settings.locations-itemFormatter-aria-label"
111
+ values={{ item }}/>
112
+ }
113
+ >
114
+ <Icon icon="trash" size="medium" />
115
+ </button>
116
+ </li>
117
+ );
118
+ };
119
+
120
+ // const resultsFormatter = {
121
+ // subLocations: (item) => {
122
+ // const zonesCount = item.subLocations.length > 0 ?
123
+ // item.subLocations.length : 0
124
+ // return zonesCount
125
+ // }
126
+ // };
127
+
128
+ const resultsFormatter = {
129
+ subLocations: (item) => Array.isArray(item?.subLocations)
130
+ ? item.subLocations.length
131
+ : 0,
132
+ };
133
+
134
+ // filter, format and set locations for <Select /> dataOptions
135
+ useEffect(() => {
136
+ const selectPlaceholder = {
137
+ value: '',
138
+ label: <FormattedMessage
139
+ id="settings.selectComponent.placeholder.select-location"/>
140
+ };
141
+ const locationsInServiceSet = new Set(
142
+ locationsInService.map(locInService => locInService.location)
143
+ );
144
+ const filteredLocations = locations.filter(
145
+ loc => !locationsInServiceSet.has(loc)
146
+ );
147
+ const formatLocations = filteredLocations.map((loc) => {
148
+ return { value: loc, label: loc }
149
+ });
150
+ setLocationsDataOptions([
151
+ { ...selectPlaceholder },
152
+ ...formatLocations
153
+ ]);
154
+
155
+ }, [locations, locationsInService]);
156
+
157
+ const handleChange = (event) => {
158
+ setSelectValue(event.target.value)
159
+ };
160
+
161
+ const makeLocationObj = (nameString) => {
162
+ return {
163
+ id: makeId(nameString),
164
+ location: nameString,
165
+ subLocations: []
166
+ }
167
+ };
168
+
169
+ const handleShowDetailsAsEditTest = (event, row) => {
170
+ const id = row.id;
171
+ history.push(`/settings/incidents/locations/${id}/edit`);
172
+ };
173
+ const handleCloseDetails = () => {
174
+ history.push(`/settings/incidents/locations`);
175
+ };
176
+ const handleShowEdit = (id) => {
177
+ history.push(`/settings/incidents/locations/${id}/edit`);
178
+ };
179
+ const handleCancelEdit = () => {
180
+ history.push(`/settings/incidents/locations`);
181
+ };
182
+ const handleCloseEdit = () => {
183
+ console.log("[STEP ?] handleCloseEdit RAN")
184
+ history.push(`/settings/incidents/locations`);
185
+ };
186
+
187
+ const handleAddToService = () => {
188
+ let updatedLocationsInService = [];
189
+ const newInService = makeLocationObj(selectValue)
190
+ const isDuplicate = locationsInService.some((loc) => loc.id === newInService.id);
191
+ if (!isDuplicate) {
192
+ updatedLocationsInService = [...locationsInService, newInService];
193
+ setLocationsInService(updatedLocationsInService); // optimistic instant ui update
194
+ }
195
+
196
+ const readyFormattedData = {
197
+ value: { locationsInService: updatedLocationsInService }
198
+ };
199
+ setFormattedData({ data: readyFormattedData })
200
+ setSelectValue('')
201
+ setTimeout(() => setFormattedData(null), 1000);
202
+ };
203
+
204
+ const isOptInSelectPresent = (e) => {
205
+ if (e) e.preventDefault();
206
+ const isSelectValid = selectValue !== '';
207
+ return isSelectValid;
208
+ };
209
+
210
+ const normalizeLocationName = (name) => {
211
+ return name.toLowerCase().replace(/[\s\-_.,'"]/g, '');
212
+ };
213
+
214
+ const handleLocationInput = (event) => {
215
+ const input = event.target.value;
216
+ const normalizedInput = normalizeLocationName(input);
217
+ const isNotNameUnique = locationsInService.some(loc => normalizeLocationName(loc.location) === normalizedInput);
218
+ const isEmpty = input.trim() === '';
219
+ const shouldDisable = isEmpty || isNotNameUnique;
220
+ setCustomLocationFormData(prevState => ({
221
+ ...prevState,
222
+ location: input
223
+ }));
224
+ setShowErrorBanner(isNotNameUnique);
225
+ setIsButtonDisabled(shouldDisable);
226
+ };
227
+
228
+ useEffect(() => {
229
+ if (customLocationFormData.location === '') {
230
+ setIsButtonDisabled(true);
231
+ }
232
+ }, [])
233
+
234
+ const handleAddCustomToService = () => {
235
+ let updatedLocationsInService = [];
236
+ const newInService = {
237
+ ...customLocationFormData,
238
+ id: makeId(customLocationFormData.location)
239
+ }
240
+ const isDuplicate = locationsInService.some((loc) => loc.id === newInService.id);
241
+ if (!isDuplicate) {
242
+ updatedLocationsInService = [...locationsInService, newInService];
243
+ }
244
+ // setLocationsInService(updatedLocationsInService); // optimistic instant ui update
245
+ const readyFormattedData = {
246
+ value: { locationsInService: updatedLocationsInService }
247
+ };
248
+ setFormattedData({ data: readyFormattedData})
249
+ setCustomLocationFormData({
250
+ id: '',
251
+ location: '',
252
+ subLocations: [],
253
+ })
254
+ setTimeout(() => setFormattedData(null), 1000);
255
+ };
256
+
257
+ return (
258
+ <Paneset>
259
+ <Pane
260
+ defaultWidth='fill'
261
+ paneTitle={
262
+ <FormattedMessage id="settings.locations.paneTitle"/>
263
+ }>
264
+ <GetLocations />
265
+ <GetLocationsInService />
266
+ {formattedData &&
267
+ <PutLocationsInService
268
+ data={formattedData}
269
+ setShow={setShow}
270
+ />}
271
+ <AccordionSet>
272
+ <Accordion label={<FormattedMessage id="settings.accordion.service-locations"/>}>
273
+ <Row style={{ marginTop: '25px'}}>
274
+ <Col xs={6}>
275
+ <Select
276
+ dataOptions={locationsDataOptions}
277
+ onChange={handleChange}
278
+ value={selectValue}
279
+ />
280
+ </Col>
281
+ <Col xs={3}>
282
+ <Button
283
+ disabled={!isOptInSelectPresent()}
284
+ buttonStyle='primary'
285
+ onClick={handleAddToService}
286
+ >
287
+ <FormattedMessage id="settings.button.save-loc-to-service"/>
288
+ </Button>
289
+ </Col>
290
+ </Row>
291
+
292
+ <Row>
293
+ <Col xs={10}>
294
+ <div style={{ height: '300px', width: 'auto' }}>
295
+ <MultiColumnList
296
+ autosize
297
+ // virtualize
298
+ // totalCount={endOfListTotal}
299
+ contentData={locationsInService}
300
+ formatter={resultsFormatter}
301
+ visibleColumns={['location', 'subLocations']}
302
+ columnMapping={{
303
+ location: <FormattedMessage id="settings.locations.column-mapping.location"/>,
304
+ subLocations: <FormattedMessage id="settings.locations.column-mapping.subLocations"/>
305
+ }}
306
+ onRowClick={handleShowDetailsAsEditTest}
307
+ isEmptyMessage={<FormattedMessage id="settings.locations.MCL.is-empty-message"/>}
308
+ />
309
+ </div>
310
+ </Col>
311
+ </Row>
312
+
313
+ <Row style={{ marginTop: '25px', marginBottom: '25px' }}>
314
+ <Col xs={8}>
315
+ <div style={{ minHeight: '50px' }}>
316
+ <MessageBanner
317
+ onEntered={() => handleMessageBannerEntered()}
318
+ type="success"
319
+ show={show}>
320
+ <FormattedMessage id="settings.locations.message-banner.success"/>
321
+ </MessageBanner>
322
+ </div>
323
+ </Col>
324
+ </Row>
325
+ </Accordion>
326
+
327
+ <Accordion label={<FormattedMessage id="settings.locations.accordion.custom-locations"/>} >
328
+ <Row style={{ marginTop: '20px' }}>
329
+ <Col xs={6}>
330
+ <Label id='label-name' style={{ marginBottom: '12px'}}>
331
+ <FormattedMessage id="settings.label.name"/>
332
+ </Label>
333
+ <TextField
334
+ aria-labelledby="label-name"
335
+ value={customLocationFormData.location}
336
+ onChange={handleLocationInput}
337
+ />
338
+ </Col>
339
+ <Col xs={6} style={{ marginTop: '20px' }}>
340
+ <div>
341
+ <MessageBanner
342
+ onEntered={() => handleMessageBannerEntered()}
343
+ type="error"
344
+ show={showErrorBanner}>
345
+ <FormattedMessage id="settings.locations-zones-messageBanner-error-msg"/>
346
+ </MessageBanner>
347
+ </div>
348
+ </Col>
349
+ </Row>
350
+
351
+ <Row style={{ marginTop: "20px"}}>
352
+ <Col xs={4}>
353
+ <Label id='label-zones'>
354
+ <FormattedMessage id="settings.label.zones"/>
355
+ </Label>
356
+ <List
357
+ aria-labelledby="label-zones"
358
+ items={items}
359
+ itemFormatter={itemFormatter}
360
+ />
361
+ <div>
362
+ <TextField
363
+ placeholder='Zone name'
364
+ value={newSubLocation.name}
365
+ onChange={handleNameInputChange}
366
+ />
367
+ <TextArea
368
+ placeholder='Zone description'
369
+ value={newSubLocation.description}
370
+ onChange={handleDescriptionInputChange}
371
+ style={{ marginTop: "20px" }}
372
+ />
373
+ <Button
374
+ style={{ marginTop: "20px"}}
375
+ onClick={handleAddSubLocation}
376
+ >
377
+ <FormattedMessage id="settings.button.add-zone"/>
378
+ </Button>
379
+ </div>
380
+ </Col>
381
+ </Row>
382
+
383
+ <Row style={{ marginTop: '10px' }}>
384
+ <Col xs={6}>
385
+ <Button
386
+ buttonStyle='primary'
387
+ onClick={handleAddCustomToService}
388
+ disabled={isButtonDisabled}
389
+ >
390
+ <FormattedMessage id="settings.button.save-new-custom-loc"/>
391
+ </Button>
392
+ </Col>
393
+ </Row>
394
+ </Accordion>
395
+ </AccordionSet>
396
+ </Pane>
397
+
398
+ <Switch>
399
+ <Route
400
+ exact
401
+ path="/settings/incidents/locations/:id/edit"
402
+ render={(props) => (
403
+ <LocationInServiceEditPane
404
+ handleCancelEdit={handleCancelEdit}
405
+ handleCloseEdit={handleCloseEdit}
406
+ {...props}
407
+
408
+ />
409
+ )}/>
410
+ </Switch>
411
+ </Paneset>
412
+ );
413
+ }
414
+
415
+ export default LocationsPaneset;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * LocationsPaneset.test.js
3
+ * Snapshot + smoke tests for the LocationsPaneset settings container.
4
+ */
5
+ import React from 'react';
6
+ import { act } from 'react-dom/test-utils';
7
+ import { createRoot } from 'react-dom/client';
8
+ import LocationsPaneset from './LocationsPaneset';
9
+
10
+ /* ------------------------------------------------------------------ *
11
+ * 1. external-library mocks
12
+ * ------------------------------------------------------------------ */
13
+ jest.mock('react-intl', () => ({
14
+ useIntl : () => ({ formatMessage: ({ id }) => id }),
15
+ FormattedMessage : (p) => <span>{p.id}</span>,
16
+ }));
17
+
18
+ jest.mock('react-router-dom', () => ({
19
+ useHistory : () => ({ push: jest.fn(), replace: jest.fn() }),
20
+ useLocation: () => ({ pathname: '/settings/incidents/locations', search: '' }),
21
+ // Keep <Switch> / <Route> simple so routing doesn’t affect snapshots.
22
+ Switch : (p) => <div>{p.children}</div>,
23
+ Route : () => null,
24
+ }));
25
+
26
+ jest.mock('@folio/stripes/components', () => {
27
+ const React = require('react');
28
+ const mk = (tag) => (p) => React.createElement(tag, p, p.children);
29
+ return {
30
+ Accordion : mk('div'),
31
+ AccordionSet : mk('div'),
32
+ Button : mk('button'),
33
+ Col : mk('div'),
34
+ Icon : (p) => <span {...p}>{p.icon}</span>,
35
+ Label : mk('label'),
36
+ List : (p) => <ul>{(p.items || []).map((it, i) => <li key={i}>{p.itemFormatter ? p.itemFormatter(it, i) : it}</li>)}</ul>,
37
+ MessageBanner : mk('div'),
38
+ MultiColumnList : mk('div'),
39
+ Pane : mk('div'),
40
+ Paneset : mk('div'),
41
+ Row : mk('div'),
42
+ Select : mk('select'),
43
+ TextArea : mk('textarea'),
44
+ TextField : mk('input'),
45
+ };
46
+ });
47
+
48
+ /* ------------------------------------------------------------------ *
49
+ * 2. child-component mocks
50
+ * ------------------------------------------------------------------ */
51
+ jest
52
+ .mock('../components/incidents/GetLocations', () => () => <div>Mock GetLocations</div>)
53
+ .mock('./GetLocationsInService', () => () => <div>Mock GetLocationsInService</div>)
54
+ .mock('./PutLocationsInService', () => (p) => <div>Mock PutLocationsInService {p?.data ? '(with data)' : ''}</div>)
55
+ .mock('./LocationInServiceEditPane', () => () => <div>Mock LocationInServiceEditPane</div>);
56
+
57
+ /* ------------------------------------------------------------------ *
58
+ * 3. helpers (only if they could affect render)
59
+ * ------------------------------------------------------------------ */
60
+ jest.mock('./helpers/makeId', () => () => 'mocked-id');
61
+
62
+ /* ------------------------------------------------------------------ *
63
+ * 4. IncidentContext mock
64
+ * ------------------------------------------------------------------ */
65
+ jest.mock('../contexts/IncidentContext', () => {
66
+ const context = {
67
+ // source lists (available locations not yet “in service”)
68
+ locations: ['Central', 'Shadle', 'Hillyard', 'Liberty Park'],
69
+ // persisted/managed “in service” list
70
+ locationsInService: [
71
+ { id: 'central', location: 'Central', subLocations: [] },
72
+ ],
73
+ setLocationsInService: jest.fn(),
74
+ };
75
+ return { useIncidents: () => context };
76
+ });
77
+
78
+ /* ------------------------------------------------------------------ *
79
+ * 5. DOM setup / teardown
80
+ * ------------------------------------------------------------------ */
81
+ let container, root;
82
+ beforeEach(() => {
83
+ container = document.createElement('div');
84
+ document.body.appendChild(container);
85
+ root = createRoot(container);
86
+ });
87
+ afterEach(() => {
88
+ root.unmount();
89
+ document.body.removeChild(container);
90
+ container = null;
91
+ });
92
+
93
+ /* ------------------------------------------------------------------ *
94
+ * 6. tests
95
+ * ------------------------------------------------------------------ */
96
+ it('renders without crashing (snapshot)', async () => {
97
+ await act(async () => { root.render(<LocationsPaneset />); });
98
+ expect(container.innerHTML).toMatchSnapshot();
99
+ });
100
+
101
+ it('mounts key child components', async () => {
102
+ await act(async () => { root.render(<LocationsPaneset />); });
103
+ const txt = container.textContent;
104
+ expect(txt).toContain('Mock GetLocations');
105
+ expect(txt).toContain('Mock GetLocationsInService');
106
+ });
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { Button, Modal, ModalFooter } from '@folio/stripes/components';
5
+
6
+ const ModalDeleteCategory = ({ isOpen, onClose, onConfirm }) => {
7
+ if (!isOpen) {
8
+ return null;
9
+ }
10
+
11
+ const footer = (
12
+ <ModalFooter>
13
+ <Button onClick={onConfirm} buttonStyle="primary">
14
+ <FormattedMessage id="settings.categories-delete-button" />
15
+ </Button>
16
+ <Button onClick={onClose}>
17
+ <FormattedMessage id="settings.categories-cancel-button" />
18
+ </Button>
19
+ </ModalFooter>
20
+ );
21
+
22
+ return (
23
+ <Modal
24
+ open={isOpen}
25
+ dismissible
26
+ closeOnBackgroundClick
27
+ label={<FormattedMessage id="settings.categories.modal-delete-label" />}
28
+ size="small"
29
+ onClose={onClose}
30
+ footer={footer}
31
+ >
32
+ <section>
33
+ <p>
34
+ <FormattedMessage id="settings.categories-delete-warn" />
35
+ </p>
36
+ </section>
37
+ </Modal>
38
+ );
39
+ };
40
+
41
+ ModalDeleteCategory.propTypes = {
42
+ isOpen: PropTypes.func.isRequired,
43
+ onClose: PropTypes.func.isRequired,
44
+ onConfirm: PropTypes.func.isRequired,
45
+ };
46
+
47
+ export default ModalDeleteCategory;
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { Button, Modal, ModalFooter } from '@folio/stripes/components';
5
+
6
+ const ModalDeleteIncidentType = ({ isOpen, onClose, onConfirm }) => {
7
+ if (!isOpen) {
8
+ return null;
9
+ }
10
+
11
+ const footer = (
12
+ <ModalFooter>
13
+ <Button onClick={onConfirm} buttonStyle="warning">
14
+ <FormattedMessage id="delete-button"/>
15
+ </Button>
16
+ <Button onClick={onClose}>
17
+ <FormattedMessage id="cancel-button"/>
18
+ </Button>
19
+ </ModalFooter>
20
+ );
21
+
22
+ return (
23
+ <Modal
24
+ open={isOpen}
25
+ dismissible
26
+ closeOnBackgroundClick
27
+ label={
28
+ <FormattedMessage id="settings.incident-types-modal-delete-label" />
29
+ }
30
+ size="small"
31
+ onClose={onClose}
32
+ footer={footer}
33
+ >
34
+ <section>
35
+ <p>
36
+ <FormattedMessage id="settings.incident-types-delete-warn" />
37
+ </p>
38
+ </section>
39
+ </Modal>
40
+ );
41
+ };
42
+
43
+ ModalDeleteIncidentType.propTypes = {
44
+ isOpen: PropTypes.func.isRequired,
45
+ onClose: PropTypes.func.isRequired,
46
+ onConfirm: PropTypes.func.isRequired,
47
+ };
48
+
49
+ export default ModalDeleteIncidentType;
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { Button, Modal, ModalFooter } from '@folio/stripes/components';
5
+
6
+ const ModalDeleteLocationInService = ({ isOpen, onClose, onConfirm }) => {
7
+ if (!isOpen) {
8
+ return null;
9
+ }
10
+
11
+ const footer = (
12
+ <ModalFooter>
13
+ <Button onClick={onConfirm} buttonStyle="warning">
14
+ <FormattedMessage id="delete-button" />
15
+ </Button>
16
+ <Button onClick={onClose}>
17
+ <FormattedMessage id="cancel-button" />
18
+ </Button>
19
+ </ModalFooter>
20
+ );
21
+
22
+ return (
23
+ <Modal
24
+ open={isOpen}
25
+ dismissible
26
+ closeOnBackgroundClick
27
+ label={
28
+ <FormattedMessage id="settings.custom-location-modal-delete-label" />
29
+ }
30
+ size="small"
31
+ onClose={onClose}
32
+ footer={footer}
33
+ >
34
+ <section>
35
+ <p>
36
+ <FormattedMessage id="settings.button.delete-warn" />
37
+ </p>
38
+ </section>
39
+ </Modal>
40
+ );
41
+ };
42
+
43
+ ModalDeleteLocationInService.propTypes = {
44
+ isOpen: PropTypes.func.isRequired,
45
+ onClose: PropTypes.func.isRequired,
46
+ onConfirm: PropTypes.func.isRequired,
47
+ };
48
+
49
+ export default ModalDeleteLocationInService;