@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,285 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { FormattedMessage } from 'react-intl';
5
+ import {
6
+ Button,
7
+ Col,
8
+ MultiColumnList,
9
+ Pane,
10
+ PaneHeader,
11
+ Row,
12
+ TextField,
13
+ } from '@folio/stripes/components';
14
+ import makeId from './helpers/makeId';
15
+ import GetIncidentCategories from './GetIncidentCategories';
16
+ import ModalDeleteCategory from './ModalDeleteCategory';
17
+ import { IncidentContext } from '../contexts/IncidentContext';
18
+
19
+ class IncidentCategoriesPane extends React.Component {
20
+ static contextType = IncidentContext;
21
+ static manifest = Object.freeze({
22
+ incidentCategory: {
23
+ type: 'okapi',
24
+ path: 'incidents/configurations/incident-categories',
25
+ PUT: {
26
+ path: `incidents/configurations/incident-categories`,
27
+ },
28
+ accumulate: true,
29
+ },
30
+ });
31
+
32
+ static propTypes = {
33
+ data: PropTypes.string,
34
+ mutator: PropTypes.shape({
35
+ incidentCategory: PropTypes.shape({
36
+ PUT: PropTypes.func.isRequired,
37
+ }).isRequired,
38
+ }).isRequired,
39
+ };
40
+
41
+ constructor(props) {
42
+ super(props);
43
+ this.state = {
44
+ editableData: [], // local copy for edit in mcl
45
+ editRow: null,
46
+ isClickDelete: false,
47
+ toDeleteId: null,
48
+ prevContextCategories: null // local tracker for comparison
49
+ };
50
+ }
51
+
52
+ // move to setting 'setState editableData' with this.context.incidentCategories
53
+ // handleFetchedCategories = (data) => {
54
+ // this.setState({ editableData: data });
55
+ // };
56
+
57
+ componentDidMount() {
58
+ const { incidentCategories } = this.context;
59
+ if (incidentCategories?.length) {
60
+ this.setState({
61
+ editableData: incidentCategories,
62
+ prevContextCategories: incidentCategories // hold what is used
63
+ })
64
+ }
65
+ }
66
+
67
+ componentDidUpdate() {
68
+ const { incidentCategories } = this.context;
69
+ if (incidentCategories !== this.state.prevContextCategories) {
70
+ this.setState({
71
+ editableData: incidentCategories,
72
+ prevContextCategories: incidentCategories // hold what is used
73
+ })
74
+ }
75
+ }
76
+
77
+ handleTitleChange = (newTitle, id) => {
78
+ this.setState((prevState) => ({
79
+ editableData: prevState.editableData.map((item) =>
80
+ item.id === id ? { ...item, title: newTitle } : item
81
+ ),
82
+ }));
83
+ };
84
+
85
+ handleEdit = (id) => {
86
+ this.setState({ editRow: id });
87
+ };
88
+
89
+ preventDuplicateId = (itemTitle) => {
90
+ const existingIds = new Set(this.state.editableData.map(cat => cat.id));
91
+ const baseId = makeId(itemTitle);
92
+ if (!existingIds.has(baseId)) return baseId;
93
+
94
+ // else increment made id value "-1", "-2", ... until gap
95
+ let i = 1;
96
+ let candidate;
97
+ do {
98
+ candidate = `${baseId}-${i}`;
99
+ i += 1;
100
+ } while (existingIds.has(candidate));
101
+
102
+ return candidate
103
+ };
104
+
105
+ handleSave = () => {
106
+ const isNewCategory = this.state.editRow === -1;
107
+ let updatedCategories = [...this.state.editableData];
108
+
109
+ if (isNewCategory) {
110
+ updatedCategories = updatedCategories.map((item) =>
111
+ item.id === -1 ? { ...item, id: this.preventDuplicateId(item.title) } : item
112
+ );
113
+ };
114
+
115
+ console.log("@handleSave - updatedCategories --> ", JSON.stringify(updatedCategories, null, 2))
116
+
117
+ const formattedReadyData = {
118
+ data: {
119
+ value: {
120
+ categories: updatedCategories
121
+ }
122
+ }
123
+ };
124
+
125
+ console.log("formattedReadyData --> ", JSON.stringify(formattedReadyData, null, 2))
126
+
127
+ this.props.mutator.incidentCategory
128
+ .PUT(formattedReadyData)
129
+ .then((response) => {
130
+ console.log('update successful - response: ', JSON.stringify(response, null,2));
131
+ })
132
+ .catch((error) => {
133
+ console.error('@IncidentCategoriesPane error updating: ', error);
134
+ });
135
+ this.setState({ editRow: null });
136
+ };
137
+
138
+ handleCancelEdit = () => {
139
+ this.setState((prevState) => {
140
+ const updatedCategories = prevState.editableData.filter(
141
+ (item) => item.id !== -1
142
+ );
143
+ return {
144
+ editableData: updatedCategories,
145
+ // reset editRow to null for both cancel edit and cancel new
146
+ editRow: null,
147
+ };
148
+ });
149
+ };
150
+
151
+ handleNew = () => {
152
+ const newCategory = {
153
+ id: -1,
154
+ title: '',
155
+ };
156
+ this.setState((prevState) => ({
157
+ editableData: [newCategory, ...prevState.editableData],
158
+ editRow: -1,
159
+ }));
160
+ };
161
+
162
+ handleShowModal = (id) => {
163
+ this.setState({ isClickDelete: true, toDeleteId: id });
164
+ };
165
+
166
+ handleCloseModal = () => {
167
+ this.setState({ isClickDelete: false, toDeleteId: null });
168
+ };
169
+
170
+ handleDelete = () => {
171
+ const { toDeleteId } = this.state;
172
+ if (toDeleteId) {
173
+ this.setState(
174
+ (prevState) => {
175
+ const updatedCategories = prevState.editableData.filter(
176
+ (item) => item.id !== toDeleteId
177
+ );
178
+ return { editableData: updatedCategories };
179
+ },
180
+ () => {
181
+ const readyFormattedData = {
182
+ data: {
183
+ value: {
184
+ categories: this.state.editableData,
185
+ }
186
+ }
187
+ };
188
+ this.props.mutator.incidentCategory
189
+ .PUT(readyFormattedData)
190
+ .then(() => {
191
+ console.log('removal and update successful');
192
+ })
193
+ .catch((error) => {
194
+ console.error('error in updating after removal: ', error);
195
+ });
196
+ this.handleCloseModal();
197
+ }
198
+ );
199
+ }
200
+ };
201
+
202
+ render() {
203
+ const { editableData, editRow, isClickDelete } = this.state;
204
+ const resultsFormatter = {
205
+ title: (item) =>
206
+ editRow === item.id ? (
207
+ <TextField
208
+ value={editableData.find((cat) => cat.id === item.id).title}
209
+ onChange={(e) => this.handleTitleChange(e.target.value, item.id)}
210
+ />
211
+ ) : (
212
+ item.title
213
+ ),
214
+ id: (item) => (
215
+ <div>
216
+ {editRow === item.id ? (
217
+ <div>
218
+ <Button onClick={this.handleCancelEdit}>
219
+ <FormattedMessage id="settings.categories-cancel-button" />
220
+ </Button>
221
+ <Button onClick={this.handleSave}>
222
+ <FormattedMessage id="settings.categories-save-button" />
223
+ </Button>
224
+ </div>
225
+ ) : (
226
+ <div>
227
+ <Button onClick={() => this.handleEdit(item.id)}>
228
+ <FormattedMessage id="edit-button" />
229
+ </Button>
230
+ <Button onClick={() => this.handleShowModal(item.id)}>
231
+ <FormattedMessage id="settings.categories-delete-button" />
232
+ </Button>
233
+ </div>
234
+ )}
235
+ </div>
236
+ ),
237
+ };
238
+
239
+ const columnWidths = {
240
+ title: '225px'
241
+ }
242
+
243
+ return (
244
+ <Pane
245
+ paneTitle="Incident categories"
246
+ defaultWidth="fill"
247
+ renderHeader={(renderProps) => <PaneHeader {...renderProps} />}
248
+ >
249
+ <Row>
250
+ <Col xs={10}>
251
+ <Button buttonStyle="primary" onClick={this.handleNew}>
252
+ <FormattedMessage id="settings.categories-new-button" />
253
+ </Button>
254
+ </Col>
255
+ </Row>
256
+ <MultiColumnList
257
+ contentData={editableData}
258
+ formatter={resultsFormatter}
259
+ visibleColumns={['title', 'id']}
260
+ columnWidths={columnWidths}
261
+ isEmptyMessage={<FormattedMessage id="settings.categories.mcl-isEmptyMsg" />}
262
+ columnMapping={{
263
+ title:
264
+ <FormattedMessage id="settings.categories.column-mapping-title" />
265
+ ,
266
+ id:
267
+ <FormattedMessage id="settings.categories.column-mapping-actions" />
268
+ ,
269
+ }}
270
+ />
271
+ <GetIncidentCategories />
272
+ {isClickDelete && (
273
+ <ModalDeleteCategory
274
+ isOpen={isClickDelete}
275
+ onClose={this.handleCloseModal}
276
+ onConfirm={this.handleDelete}
277
+ />
278
+ )}
279
+ </Pane>
280
+ );
281
+ }
282
+ }
283
+ IncidentCategoriesPane.contextType = IncidentContext;
284
+
285
+ export default stripesConnect(IncidentCategoriesPane);
@@ -0,0 +1,229 @@
1
+ /**
2
+ * IncidentCategoriesPane.test.js
3
+ * Snapshot + smoke + basic behavior tests for the IncidentCategoriesPane.
4
+ */
5
+ import React, { act } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+ import IncidentCategoriesPane from './IncidentCategoriesPane';
8
+
9
+ /* ------------------------------------------------------------------ *
10
+ * 1) stripes/core + intl mocks
11
+ * ------------------------------------------------------------------ */
12
+ jest.mock('@folio/stripes/core', () => ({
13
+ // Make stripesConnect a no-op so we can pass mutator manually
14
+ stripesConnect: (C) => C,
15
+ }));
16
+
17
+ jest.mock('react-intl', () => ({
18
+ useIntl : () => ({ formatMessage: ({ id }) => id }),
19
+ FormattedMessage : (p) => <span>{p.id}</span>,
20
+ }));
21
+
22
+ /* ------------------------------------------------------------------ *
23
+ * 2) stripes/components mock
24
+ * ------------------------------------------------------------------ */
25
+ jest.mock('@folio/stripes/components', () => {
26
+ const React = require('react');
27
+ const mk = (tag) => (p) => React.createElement(tag, p, p.children);
28
+
29
+ // Render header and children so everything shows up in DOM
30
+ const Pane = (p) => (
31
+ <div>
32
+ {typeof p.renderHeader === 'function' ? p.renderHeader({}) : p.renderHeader}
33
+ {p.children}
34
+ {p.footer}
35
+ </div>
36
+ );
37
+
38
+ // Minimal MultiColumnList that applies the provided formatter
39
+ const MultiColumnList = (p) => {
40
+ const items = p.contentData || [];
41
+ const fmt = p.formatter || {};
42
+ return (
43
+ <ul>
44
+ {items.map((item, i) => (
45
+ <li key={item.id ?? i}>
46
+ <div data-col="title">{fmt.title ? fmt.title(item) : item.title}</div>
47
+ <div data-col="id">{fmt.id ? fmt.id(item) : item.id}</div>
48
+ </li>
49
+ ))}
50
+ </ul>
51
+ );
52
+ };
53
+
54
+ // Strip unknown DOM props to avoid warnings
55
+ const Button = ({ buttonStyle, ...rest }) => <button {...rest}>{rest.children}</button>;
56
+
57
+ return {
58
+ Button,
59
+ Col : mk('div'),
60
+ MultiColumnList,
61
+ Pane,
62
+ PaneHeader : mk('div'),
63
+ Row : mk('div'),
64
+ TextField : (p) => <input {...p} />,
65
+ };
66
+ });
67
+
68
+ /* ------------------------------------------------------------------ *
69
+ * 3) child-component + helper mocks
70
+ * ------------------------------------------------------------------ */
71
+ jest.mock('./GetIncidentCategories', () => () => (
72
+ <div>Mock GetIncidentCategories</div>
73
+ ));
74
+
75
+ // Ensure deterministic IDs when adding a new category
76
+ jest.mock('./helpers/makeId', () => jest.fn(() => 'new-cat'));
77
+
78
+ // Simple delete modal that exposes confirm/close actions
79
+ jest.mock('./ModalDeleteCategory', () => (p) =>
80
+ p.isOpen ? (
81
+ <div>
82
+ <div>Mock ModalDeleteCategory</div>
83
+ <button onClick={p.onConfirm}>confirm-delete</button>
84
+ <button onClick={p.onClose}>close-modal</button>
85
+ </div>
86
+ ) : null
87
+ );
88
+
89
+ /* ------------------------------------------------------------------ *
90
+ * 4) IncidentContext mock (class component uses contextType)
91
+ * ------------------------------------------------------------------ */
92
+ jest.mock('../contexts/IncidentContext', () => {
93
+ const React = require('react');
94
+ return {
95
+ IncidentContext: React.createContext({
96
+ incidentCategories: [
97
+ { id: 'cat-1', title: 'Behavior' },
98
+ { id: 'cat-2', title: 'Property' },
99
+ ],
100
+ }),
101
+ };
102
+ });
103
+
104
+ /* ------------------------------------------------------------------ *
105
+ * 5) DOM setup / teardown
106
+ * ------------------------------------------------------------------ */
107
+ let container, root;
108
+ beforeEach(() => {
109
+ container = document.createElement('div');
110
+ document.body.appendChild(container);
111
+ root = createRoot(container);
112
+ });
113
+ afterEach(async () => {
114
+ // Wrap unmount in act to avoid warnings
115
+ await act(async () => {
116
+ root.unmount();
117
+ });
118
+ document.body.removeChild(container);
119
+ container = null;
120
+ });
121
+
122
+ /* ------------------------------------------------------------------ *
123
+ * 6) helpers
124
+ * ------------------------------------------------------------------ */
125
+ const findButtonByText = (rootEl, text) =>
126
+ Array.from(rootEl.querySelectorAll('button')).find((b) =>
127
+ (b.textContent || '').includes(text)
128
+ );
129
+
130
+ // Reliable flush (micro + macro) to ensure class setState fully commits
131
+ const flushAll = async () => {
132
+ await act(async () => { await Promise.resolve(); }); // microtask
133
+ await act(async () => { await new Promise(r => setTimeout(r, 0)); }); // macrotask
134
+ };
135
+
136
+ /* ------------------------------------------------------------------ *
137
+ * 7) tests
138
+ * ------------------------------------------------------------------ */
139
+ it('renders without crashing (snapshot)', async () => {
140
+ const mutator = { incidentCategory: { PUT: jest.fn().mockResolvedValue({}) } };
141
+ await act(async () => {
142
+ root.render(<IncidentCategoriesPane mutator={mutator} />);
143
+ });
144
+ await flushAll();
145
+ expect(container.innerHTML).toMatchSnapshot();
146
+ });
147
+
148
+ it('mounts key child components', async () => {
149
+ const mutator = { incidentCategory: { PUT: jest.fn().mockResolvedValue({}) } };
150
+ await act(async () => {
151
+ root.render(<IncidentCategoriesPane mutator={mutator} />);
152
+ });
153
+ await flushAll();
154
+ expect(container.textContent).toContain('Mock GetIncidentCategories');
155
+ });
156
+
157
+ it('adds a new category and saves (calls PUT with new list)', async () => {
158
+ const put = jest.fn().mockResolvedValue({});
159
+ const mutator = { incidentCategory: { PUT: put } };
160
+
161
+ await act(async () => {
162
+ root.render(<IncidentCategoriesPane mutator={mutator} />);
163
+ });
164
+ await flushAll();
165
+
166
+ // Click "New"
167
+ const newBtn = findButtonByText(container, 'settings.categories-new-button');
168
+ expect(newBtn).toBeTruthy();
169
+ await act(async () => { newBtn.click(); });
170
+ await flushAll();
171
+
172
+ // The edit row input should appear (id = -1)
173
+ const input = container.querySelector('input');
174
+ expect(input).toBeTruthy();
175
+
176
+ // Type a title for the new category (best-effort – jsdom+class comps can be finicky)
177
+ await act(async () => {
178
+ input.value = 'New Category';
179
+ input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
180
+ input.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
181
+ });
182
+ await flushAll();
183
+
184
+ // Click "Save" within the actions cell for the edit row
185
+ const saveBtn = findButtonByText(container, 'settings.categories-save-button');
186
+ expect(saveBtn).toBeTruthy();
187
+
188
+ await act(async () => { saveBtn.click(); });
189
+ await flushAll();
190
+
191
+ expect(put).toHaveBeenCalledTimes(1);
192
+ const payload = put.mock.calls[0][0];
193
+ const cats = payload?.data?.value?.categories || [];
194
+ expect(Array.isArray(cats)).toBe(true);
195
+
196
+ // Core behavior: a new item with deterministic ID is persisted.
197
+ // (Avoid asserting the title string due to controlled-input flakiness in jsdom.)
198
+ const newItem = cats.find((c) => c.id === 'new-cat');
199
+ expect(!!newItem).toBe(true);
200
+ });
201
+
202
+ it('deletes a category via the modal and calls PUT with the reduced list', async () => {
203
+ const put = jest.fn().mockResolvedValue({});
204
+ const mutator = { incidentCategory: { PUT: put } };
205
+
206
+ await act(async () => {
207
+ root.render(<IncidentCategoriesPane mutator={mutator} />);
208
+ });
209
+ await flushAll();
210
+
211
+ // Click the first "Delete" button (for cat-1)
212
+ const delBtn = findButtonByText(container, 'settings.categories-delete-button');
213
+ expect(delBtn).toBeTruthy();
214
+ await act(async () => { delBtn.click(); });
215
+ await flushAll();
216
+
217
+ // Modal should render; confirm
218
+ const confirm = findButtonByText(container, 'confirm-delete');
219
+ expect(confirm).toBeTruthy();
220
+ await act(async () => { confirm.click(); });
221
+ await flushAll();
222
+
223
+ expect(put).toHaveBeenCalled();
224
+ const payload = put.mock.calls[put.mock.calls.length - 1][0];
225
+ const cats = payload?.data?.value?.categories || [];
226
+ // cat-1 should be removed; cat-2 should remain
227
+ expect(cats.some((c) => c.id === 'cat-1')).toBe(false);
228
+ expect(cats.some((c) => c.id === 'cat-2')).toBe(true);
229
+ });