@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.
- package/.eslintrc +32 -0
- package/.github/workflows/CODEOWNERS +8 -0
- package/.github/workflows/pr-validation.yml +44 -0
- package/.github/workflows/release.yml +64 -0
- package/.prettierrc +6 -0
- package/.stripesclirc +4 -0
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +4 -0
- package/LICENSE +201 -0
- package/README.md +16 -0
- package/administrator-documentation/roles-and-permissions.md +65 -0
- package/administrator-documentation/track-settings-admin-guide-sketch.md +192 -0
- package/administrator-documentation/using-the-application.md +192 -0
- package/icons/app.png +0 -0
- package/icons/app.svg +1 -0
- package/icons/playButton.png +0 -0
- package/icons/profilePicThumbnail.png +0 -0
- package/jest.config.js +10 -0
- package/module-descriptor.json +75 -0
- package/output/service-worker.js +0 -0
- package/package.json +146 -0
- package/src/components/incidents/ColumnChooser.js +37 -0
- package/src/components/incidents/CreateMedia.js +132 -0
- package/src/components/incidents/CreatePane.js +1215 -0
- package/src/components/incidents/CreatePane.test.js +138 -0
- package/src/components/incidents/CreateReport.js +102 -0
- package/src/components/incidents/DetailsPane.js +1267 -0
- package/src/components/incidents/DetailsPane.test.js +150 -0
- package/src/components/incidents/EditPane.js +2334 -0
- package/src/components/incidents/EditPane.test.js +187 -0
- package/src/components/incidents/GetDetails.js +55 -0
- package/src/components/incidents/GetListDQLinkIncident.js +81 -0
- package/src/components/incidents/GetListDynamicQuery.js +66 -0
- package/src/components/incidents/GetLocations.js +57 -0
- package/src/components/incidents/GetMedia.js +98 -0
- package/src/components/incidents/GetName.js +111 -0
- package/src/components/incidents/GetNameCreatedBy.js +94 -0
- package/src/components/incidents/GetOrgLocaleSettings.js +61 -0
- package/src/components/incidents/GetPatronGroups.js +52 -0
- package/src/components/incidents/GetSelf.js +65 -0
- package/src/components/incidents/GetSummary.js +110 -0
- package/src/components/incidents/IncidentTypeCard.js +53 -0
- package/src/components/incidents/IncidentTypeCard.test.js +133 -0
- package/src/components/incidents/IncidentsPaneset.js +810 -0
- package/src/components/incidents/IncidentsPaneset.test.js +128 -0
- package/src/components/incidents/LinkedIncident.js +86 -0
- package/src/components/incidents/ModalAddMedia.js +262 -0
- package/src/components/incidents/ModalAddMedia.test.js +97 -0
- package/src/components/incidents/ModalAttentionDecOfService.js +111 -0
- package/src/components/incidents/ModalCustomWitness.js +469 -0
- package/src/components/incidents/ModalCustomWitness.test.js +147 -0
- package/src/components/incidents/ModalCustomerDetails.js +480 -0
- package/src/components/incidents/ModalCustomerDetails.test.js +116 -0
- package/src/components/incidents/ModalDescribeCustomer.js +361 -0
- package/src/components/incidents/ModalDescribeCustomer.test.js +156 -0
- package/src/components/incidents/ModalDirtyFormWarn.js +62 -0
- package/src/components/incidents/ModalLinkIncident.js +1213 -0
- package/src/components/incidents/ModalLinkIncidentStyle.css +32 -0
- package/src/components/incidents/ModalSelectIncidentTypes.js +178 -0
- package/src/components/incidents/ModalSelectIncidentTypes.test.js +273 -0
- package/src/components/incidents/ModalSelectKnownCustomer.js +395 -0
- package/src/components/incidents/ModalSelectWitness.js +406 -0
- package/src/components/incidents/ModalSelectWitness.test.js +308 -0
- package/src/components/incidents/ModalStyle.css +44 -0
- package/src/components/incidents/ModalTrespass.js +741 -0
- package/src/components/incidents/ModalViewCustomerDetails.js +241 -0
- package/src/components/incidents/ModalViewMedia.js +86 -0
- package/src/components/incidents/ModalViewTrespass.js +210 -0
- package/src/components/incidents/ResultsPane.js +437 -0
- package/src/components/incidents/ResultsPane.test.js +120 -0
- package/src/components/incidents/SearchCustomerOrWitness.js +108 -0
- package/src/components/incidents/Thumbnail.js +72 -0
- package/src/components/incidents/ThumbnailMarkRemoval.js +38 -0
- package/src/components/incidents/ThumbnailSkeleton.js +30 -0
- package/src/components/incidents/ThumbnailStyles.js +49 -0
- package/src/components/incidents/ThumbnailTempPreSave.js +71 -0
- package/src/components/incidents/UpdateReport.js +84 -0
- package/src/components/incidents/__snapshots__/CreatePane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/DetailsPane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/EditPane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/IncidentTypeCard.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/IncidentsPaneset.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalAddMedia.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalCustomerDetails.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalSelectWitness.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ResultsPane.test.js.snap +3 -0
- package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.css +5 -0
- package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.js +51 -0
- package/src/components/incidents/helpers/ProfilePicture/isAValidURL.js +3 -0
- package/src/components/incidents/helpers/ProfilePicture/useProfilePicture.js +127 -0
- package/src/components/incidents/helpers/buildQueryString.js +28 -0
- package/src/components/incidents/helpers/cleanFormValues.js +53 -0
- package/src/components/incidents/helpers/computeEditedCustomers.js +124 -0
- package/src/components/incidents/helpers/convertDateIgnoringTZ.js +8 -0
- package/src/components/incidents/helpers/convertUTCISOToLocalePrettyTime.js +15 -0
- package/src/components/incidents/helpers/convertUTCISOToPrettyDate.js +19 -0
- package/src/components/incidents/helpers/decodeParamsToForm.js +20 -0
- package/src/components/incidents/helpers/deepNormalizeForComparison.js +39 -0
- package/src/components/incidents/helpers/extractFilterString.js +12 -0
- package/src/components/incidents/helpers/formatDateAndTimeToUTCISO.js +14 -0
- package/src/components/incidents/helpers/formatDateToUTCISO.js +14 -0
- package/src/components/incidents/helpers/formatTimeToUTCISO.js +28 -0
- package/src/components/incidents/helpers/getCurrentTime.js +20 -0
- package/src/components/incidents/helpers/getTodayDate.js +12 -0
- package/src/components/incidents/helpers/handlebarsHelpers.js +148 -0
- package/src/components/incidents/helpers/hasFormChangedAtCreate.js +50 -0
- package/src/components/incidents/helpers/hasTopLevelChangeAffectedDeclaration.js +90 -0
- package/src/components/incidents/helpers/hasTopLevelFormChanged.js +111 -0
- package/src/components/incidents/helpers/identifyCurrentTrespassDocs.js +109 -0
- package/src/components/incidents/helpers/isSameHtml.js +13 -0
- package/src/components/incidents/helpers/isValidDateFormat.js +14 -0
- package/src/components/incidents/helpers/isValidTimeInput.js +11 -0
- package/src/components/incidents/helpers/isValidUTCTimeFormat.js +14 -0
- package/src/components/incidents/helpers/parseMMDDYYYY.js +7 -0
- package/src/components/incidents/helpers/parseQueryString.js +16 -0
- package/src/components/incidents/helpers/sortTrespassDocuments.js +44 -0
- package/src/components/incidents/helpers/stripHTML.js +11 -0
- package/src/components/incidents/helpers/trespassDocUtils.js +197 -0
- package/src/components/incidents/helpers/validateTrespassDetails.js +37 -0
- package/src/components/incidents/usePersistedColModalLink.js +70 -0
- package/src/components/incidents/usePersistedColumns.js +70 -0
- package/src/components/incidents/usePersistedSort.js +23 -0
- package/src/components/incidents/usePersistedSortModalLink.js +23 -0
- package/src/contexts/IncidentContext.js +433 -0
- package/src/index.js +61 -0
- package/src/routes/Application.js +13 -0
- package/src/settings/GetIncidentCategories.js +56 -0
- package/src/settings/GetIncidentTypesDetails.js +88 -0
- package/src/settings/GetIncidentTypesIds.js +74 -0
- package/src/settings/GetLocationsInService.js +54 -0
- package/src/settings/GetSingleCustomLocationDetails.js +60 -0
- package/src/settings/GetSingleIncidentTypeDetails.js +60 -0
- package/src/settings/GetTrespassReasons.js +67 -0
- package/src/settings/GetTrespassTemplates.js +51 -0
- package/src/settings/IncidentCategoriesPane.js +285 -0
- package/src/settings/IncidentCategoriesPane.test.js +229 -0
- package/src/settings/IncidentTypeDetailsPane.js +215 -0
- package/src/settings/IncidentTypeDetailsPane.test.js +220 -0
- package/src/settings/IncidentTypeEditPane.js +211 -0
- package/src/settings/IncidentTypeEditPane.test.js +170 -0
- package/src/settings/IncidentTypesPaneset.js +167 -0
- package/src/settings/IncidentTypesPaneset.test.js +124 -0
- package/src/settings/LocationInServiceEditPane.js +320 -0
- package/src/settings/LocationsPaneset.js +415 -0
- package/src/settings/LocationsPaneset.test.js +106 -0
- package/src/settings/ModalDeleteCategory.js +47 -0
- package/src/settings/ModalDeleteIncidentType.js +49 -0
- package/src/settings/ModalDeleteLocationInService.js +49 -0
- package/src/settings/ModalDeleteTrespassReason.js +49 -0
- package/src/settings/ModalPreviewTrespassDoc.js +65 -0
- package/src/settings/ModalTrespassDocTokens.js +83 -0
- package/src/settings/NewIncidentTypePane.js +182 -0
- package/src/settings/PutIncidentType.js +60 -0
- package/src/settings/PutLocationsInService.js +52 -0
- package/src/settings/PutTrespassReasons.js +61 -0
- package/src/settings/PutTrespassTemplate.js +50 -0
- package/src/settings/TrespassDoc.css +17 -0
- package/src/settings/TrespassDocDetailsPane.js +215 -0
- package/src/settings/TrespassDocEditPane.js +538 -0
- package/src/settings/TrespassDocPaneset.js +581 -0
- package/src/settings/TrespassReasonDetailsPane.js +171 -0
- package/src/settings/TrespassReasonEditPane.js +221 -0
- package/src/settings/TrespassReasonsPaneset.js +282 -0
- package/src/settings/__snapshots__/IncidentCategoriesPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypeDetailsPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypeEditPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypesPaneset.test.js.snap +3 -0
- package/src/settings/__snapshots__/LocationsPaneset.test.js.snap +3 -0
- package/src/settings/data/exampleJSON.json +92 -0
- package/src/settings/data/templateTokens.js +396 -0
- package/src/settings/helpers/alphabetize.js +18 -0
- package/src/settings/helpers/getCategoryTitleById.js +13 -0
- package/src/settings/helpers/makeId.js +15 -0
- package/src/settings/index.js +48 -0
- package/stripes.config.js +10 -0
- package/test/jest/__mock__/index.js +8 -0
- package/test/jest/__mock__/intl.mock.js +27 -0
- package/test/jest/__mock__/stripes.mock.js +26 -0
- package/test/jest/__mock__/stripesComponents.mock.js +151 -0
- package/test/jest/__mock__/stripesConfig.mock.js +1 -0
- package/test/jest/__mock__/stripesCore.mock.js +9 -0
- package/test/jest/__mock__/stripesIcon.mock.js +5 -0
- package/test/jest/__mock__/stripesSmartComponents.mock.js +7 -0
- package/test/jest/__mock__/stripesUtils.mock.js +3 -0
- package/test/jest/eslintrc.js +12 -0
- package/test/jest/setupFiles.js +5 -0
- package/translations/ui-security-incident/en_US.json +542 -0
- 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
|
+
});
|