@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,94 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { IncidentContext } from '../../contexts/IncidentContext';
5
+
6
+ class GetNameCreatedBy extends React.Component {
7
+ static contextType = IncidentContext;
8
+ static manifest = Object.freeze({
9
+ customer: {
10
+ type: 'okapi',
11
+ accumulate: true,
12
+ throwErrors: false // handle 4xx/5xx errors on our own
13
+ },
14
+ });
15
+
16
+ static propTypes = {
17
+ context: PropTypes.string,
18
+ uuid: PropTypes.string,
19
+ customer: PropTypes.shape({
20
+ record: PropTypes.object,
21
+ }),
22
+ handleGetCreatedByName: PropTypes.func,
23
+ mutator: PropTypes.shape({
24
+ customer: PropTypes.shape({
25
+ GET: PropTypes.func.isRequired,
26
+ }).isRequired,
27
+ }).isRequired,
28
+ };
29
+
30
+ componentDidMount() {
31
+ this.fetchNameCreatedBy(this.props.uuid);
32
+ };
33
+
34
+ componentDidUpdate(prevProps) {
35
+ if (this.props.uuid !== prevProps.uuid) {
36
+ this.fetchNameCreatedBy(this.props.uuid);
37
+ }
38
+ };
39
+
40
+ fetchNameCreatedBy(paramsId) {
41
+ const idToFetch = paramsId || this.props.uuid
42
+
43
+ if (idToFetch !== '') {
44
+ this.props.mutator.customer
45
+ .GET({ path: `users/${this.props.uuid}` })
46
+ .then((response) => {
47
+ if (response && response.httpStatus === 404) {
48
+ console.log(`@GetNameCreatedBy: user for uuid="${this.props.uuid}" not found.`);
49
+ return;
50
+ };
51
+
52
+ if (response && response.id) {
53
+ const refinedRecord = {
54
+ id: response.id,
55
+ barcode: response.barcode,
56
+ firstName: response.personal.firstName,
57
+ lastName: response.personal.lastName,
58
+ };
59
+ this.props.handleGetCreatedByName(refinedRecord);
60
+ };
61
+ })
62
+ .catch((error) => {
63
+ // if stripes-connect rejects the promise
64
+ if (error?.httpStatus === 404) {
65
+ console.log(`@GetNameCreatedBy: user for uuid="${this.props.uuid}" not found in.`);
66
+ this.props.handleMissingUsers(this.props.uuid)
67
+ return;
68
+ }
69
+ console.error('@GetNameCreatedBy: unhandled error fetching user:', error);
70
+ })
71
+ } else {
72
+ console.log('@fetchNameCreatedBy - something went wrong - no data');
73
+ };
74
+ };
75
+
76
+ render() {
77
+ return null;
78
+ };
79
+ };
80
+
81
+ GetNameCreatedBy.contextType = IncidentContext;
82
+ GetNameCreatedBy.propTypes = {
83
+ uuid: PropTypes.string,
84
+ customer: PropTypes.shape({
85
+ record: PropTypes.object,
86
+ }),
87
+ mutator: PropTypes.shape({
88
+ customer: PropTypes.shape({
89
+ GET: PropTypes.func.isRequired,
90
+ }).isRequired,
91
+ }).isRequired,
92
+ };
93
+
94
+ export default stripesConnect(GetNameCreatedBy, '@spokane-folio/security-incident');
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { IncidentContext } from '../../contexts/IncidentContext';
5
+
6
+
7
+
8
+ class GetOrgLocaleSettings extends React.Component {
9
+ static contextType = IncidentContext;
10
+ static manifest = Object.freeze({
11
+ localeSettings: {
12
+ type: 'okapi',
13
+ path: 'configurations/entries?query=module==ORG and configName==localeSettings and enabled=true'
14
+ },
15
+ });
16
+
17
+ static propTypes = {
18
+ localeSettings: PropTypes.shape({
19
+ records: PropTypes.arrayOf(PropTypes.object),
20
+ }),
21
+ resources: PropTypes.object.isRequired
22
+ };
23
+
24
+ componentDidMount() {
25
+ this.fetchOrgTimezone();
26
+ }
27
+
28
+ componentDidUpdate(prevProps) {
29
+ this.fetchOrgTimezone(prevProps);
30
+ }
31
+
32
+ fetchOrgTimezone(prevProps = {}) {
33
+ const currentResources = this.props.resources || {};
34
+ const prevResources = prevProps.resources || {};
35
+ const currentLocaleSettings = currentResources.localeSettings;
36
+ const prevLocaleSettings = prevResources.localeSettings;
37
+ if (currentLocaleSettings && currentLocaleSettings !== prevLocaleSettings) {
38
+ try {
39
+ const localeSettingsRecords = currentLocaleSettings.records;
40
+ if (localeSettingsRecords && localeSettingsRecords.length > 0) {
41
+ const orgTimezone = JSON.parse(localeSettingsRecords[0].configs[0].value).timezone;
42
+ this.context.setOrganizationTimezone(orgTimezone);
43
+ }
44
+ } catch (error) {
45
+ // placeholder
46
+ console.log('@fetchOrgTimezone - error parsing: ', error.message);
47
+ }
48
+ } else {
49
+ // no new loaded data yet. prev logged here, but removed to reduce noise in console
50
+ return;
51
+ };
52
+ };
53
+
54
+ render() {
55
+ return <></>;
56
+ };
57
+ };
58
+
59
+ GetOrgLocaleSettings.contextType = IncidentContext;
60
+
61
+ export default stripesConnect(GetOrgLocaleSettings, '@spokane-folio/security-incident');
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { IncidentContext } from '../../contexts/IncidentContext';
5
+
6
+ class GetPatronGroups extends React.Component {
7
+ static contextType = IncidentContext;
8
+
9
+ static manifest = Object.freeze({
10
+ patronGroups: {
11
+ type: 'okapi',
12
+ accumulate: true,
13
+ },
14
+ });
15
+
16
+ static propTypes = {
17
+ context: PropTypes.string,
18
+ uuid: PropTypes.string,
19
+ patronGroups: PropTypes.shape({
20
+ records: PropTypes.object,
21
+ }),
22
+ mutator: PropTypes.shape({
23
+ patronGroups: PropTypes.shape({
24
+ GET: PropTypes.func.isRequired,
25
+ }).isRequired,
26
+ }).isRequired,
27
+ };
28
+
29
+ componentDidMount() {
30
+ this.fetchPatronGroups();
31
+ };
32
+
33
+ fetchPatronGroups() {
34
+ this.props.mutator.patronGroups
35
+ .GET({ path: `groups?query=cql.allRecords=1%20sortby%20group&limit=2000`})
36
+ .then((records) => {
37
+ const titlesAndIds = records.usergroups.map((pg) => {
38
+ return {
39
+ id: pg.id,
40
+ group: pg.group
41
+ }
42
+ });
43
+ this.props.setPatronGroups(titlesAndIds)
44
+ })
45
+ };
46
+
47
+ render() {
48
+ return null;
49
+ };
50
+ };
51
+
52
+ export default stripesConnect(GetPatronGroups, '@spokane-folio/security-incident');
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { IncidentContext } from '../../contexts/IncidentContext';
5
+
6
+ class GetSelf extends React.Component {
7
+ static contextType = IncidentContext;
8
+
9
+ static manifest = Object.freeze({
10
+ self: {
11
+ type: 'okapi',
12
+ accumulate: true,
13
+ },
14
+ });
15
+
16
+ static propTypes = {
17
+ self: PropTypes.shape({
18
+ records: PropTypes.object,
19
+ }),
20
+ mutator: PropTypes.shape({
21
+ self: PropTypes.shape({
22
+ GET: PropTypes.func.isRequired,
23
+ }).isRequired,
24
+ }).isRequired,
25
+ };
26
+
27
+ componentDidMount() {
28
+ this.fetchSelf();
29
+ }
30
+
31
+ componentDidUpdate(prevProps) {
32
+ if (this.props.self !== prevProps.self) {
33
+ this.fetchSelf();
34
+ }
35
+ }
36
+
37
+ fetchSelf() {
38
+ this.props.mutator.self
39
+ .GET({ path: `bl-users/_self` })
40
+ .then((records) => {
41
+ const self = records.user;
42
+ const refinedSelf = {
43
+ id: self.id,
44
+ lastName: self.personal.lastName,
45
+ firstName: self.personal.firstName,
46
+ barcode: self.barcode ? self.barcode : '',
47
+ };
48
+ this.context.setSelf(refinedSelf);
49
+ })
50
+ .catch((error) => {
51
+ console.log(
52
+ '@fetchSelf - something went wrong - no data. Error: ',
53
+ JSON.stringify(error, null, 2)
54
+ );
55
+ });
56
+ }
57
+
58
+ render() {
59
+ return <></>;
60
+ }
61
+ }
62
+
63
+ GetSelf.contextType = IncidentContext;
64
+
65
+ export default stripesConnect(GetSelf, '@spokane-folio/security-incident');
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { stripesConnect } from '@folio/stripes/core';
4
+ import { getHeaderWithCredentials } from '@folio/stripes/util';
5
+ import { IncidentContext } from '../../contexts/IncidentContext';
6
+
7
+ class GetSummary extends React.Component {
8
+ static contextType = IncidentContext;
9
+
10
+ static propTypes = {
11
+ stripes: PropTypes.object.isRequired,
12
+ ids: PropTypes.arrayOf(PropTypes.string).isRequired,
13
+ onResult: PropTypes.func.isRequired,
14
+ };
15
+
16
+ constructor(props) {
17
+ super(props);
18
+ this.okapiURL = props.stripes.okapi.url;
19
+ this._isMounted = false;
20
+ this._lastIdsSignature = this.makeIdsSignature(props.ids);
21
+ this._abortController = null;
22
+ }
23
+
24
+ componentDidMount() {
25
+ this._isMounted = true;
26
+ this.fetchIncidentSummaries(this.props.ids);
27
+ }
28
+
29
+ componentDidUpdate(prevProps) {
30
+ const prevSig = this.makeIdsSignature(prevProps.ids);
31
+ const nextSig = this.makeIdsSignature(this.props.ids);
32
+
33
+ if (prevSig !== nextSig) {
34
+ this._lastIdsSignature = nextSig;
35
+ this.fetchIncidentSummaries(this.props.ids);
36
+ }
37
+ }
38
+
39
+ componentWillUnmount() {
40
+ this._isMounted = false;
41
+ if (this._abortController) this._abortController.abort();
42
+ }
43
+
44
+ makeIdsSignature = (idsInput) => {
45
+ // normalize -> sort unique array -> JSON string
46
+ const arr = Array.isArray(idsInput) ? idsInput : Array.from(idsInput || []);
47
+ const uniqSorted = Array.from(new Set(arr)).sort();
48
+ return JSON.stringify(uniqSorted);
49
+ };
50
+
51
+ fetchIncidentSummaries = async (idsInput) => {
52
+ const ids = Array.isArray(idsInput) ? idsInput : Array.from(idsInput || []);
53
+ if (!ids.length) {
54
+ this.props.onResult([]);
55
+ return;
56
+ }
57
+
58
+ // cancel any in-flight batch
59
+ if (this._abortController) this._abortController.abort();
60
+ this._abortController = new AbortController();
61
+
62
+ const credsObj = getHeaderWithCredentials(this.props.stripes.okapi);
63
+ const headers = { ...credsObj.headers };
64
+
65
+ try {
66
+ // TODO set open for loading spinner on linkedTo UI list
67
+
68
+ const fetchOne = async (id) => {
69
+ const resp = await fetch(`${this.okapiURL}/incidents/${id}`, {
70
+ ...credsObj,
71
+ headers,
72
+ signal: this._abortController.signal,
73
+ });
74
+ if (!resp.ok) throw new Error(`GET /incidents/${id} -> ${resp.status}`);
75
+ const json = await resp.json();
76
+
77
+ // prefer server-provided preview; fallback builds a minimal preview
78
+ const customers =
79
+ (json.previewForLinkedToUI?.customers) ||
80
+ (json.customers || []).map(c => c.customerNameLabel || '').filter(Boolean);
81
+
82
+ return {
83
+ id: json.previewForLinkedToUI?.id || json.id,
84
+ createdDate:
85
+ json.previewForLinkedToUI?.createdDate ||
86
+ json.metadata?.createdDate ||
87
+ null,
88
+ customers,
89
+ };
90
+ };
91
+
92
+ const summaries = await Promise.all(ids.map(fetchOne));
93
+
94
+ if (this._isMounted) this.props.onResult(summaries);
95
+ } catch (err) {
96
+ if (err.name !== 'AbortError') {
97
+ console.error('GetSummary: fetch failed', err);
98
+ if (this._isMounted) this.props.onResult([]);
99
+ }
100
+ } finally {
101
+ // TODO set close for loading spinner on linkedTo UI list
102
+ }
103
+ };
104
+
105
+ render() {
106
+ return null;
107
+ };
108
+ };
109
+
110
+ export default stripesConnect(GetSummary, '@spokane-folio/security-incident');
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Card, Button, Icon } from '@folio/stripes/components';
4
+
5
+ const IncidentTypeCard = ({
6
+ handleTypeToggle,
7
+ id,
8
+ category_id,
9
+ title,
10
+ description,
11
+ isSelected,
12
+ }) => {
13
+ const typeData = {
14
+ id,
15
+ title,
16
+ category_id,
17
+ description,
18
+ };
19
+
20
+ const handleClick = () => {
21
+ handleTypeToggle(typeData);
22
+ };
23
+
24
+ return (
25
+ <Card
26
+ style={{ height: '210px', overflow: 'auto' }}
27
+ roundedBorder
28
+ headerStart={<b>{title}</b>}
29
+ headerEnd={
30
+ <Button
31
+ buttonStyle={isSelected ? 'success' : 'primary'}
32
+ style={{ marginTop: '10px' }}
33
+ onClick={handleClick}
34
+ >
35
+ {isSelected ? <Icon icon="check-circle" size="large" /> : 'Add'}
36
+ </Button>
37
+ }
38
+ >
39
+ {description}
40
+ </Card>
41
+ );
42
+ };
43
+
44
+ IncidentTypeCard.propTypes = {
45
+ handleTypeToggle: PropTypes.func.isRequired,
46
+ id: PropTypes.string.isRequired,
47
+ category_id: PropTypes.string.isRequired,
48
+ title: PropTypes.string.isRequired,
49
+ description: PropTypes.string.isRequired,
50
+ isSelected: PropTypes.bool.isRequired,
51
+ };
52
+
53
+ export default IncidentTypeCard;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * IncidentTypeCard.test.js
3
+ * Snapshot + smoke + basic interaction tests for IncidentTypeCard.
4
+ */
5
+ import React, { act } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+ import IncidentTypeCard from './IncidentTypeCard';
8
+
9
+ /* ------------------------------------------------------------------ *
10
+ * stripes/components mock
11
+ * ------------------------------------------------------------------ */
12
+ jest.mock('@folio/stripes/components', () => {
13
+ const React = require('react');
14
+
15
+ // Render Card's headerStart, headerEnd, and children so they appear in the DOM
16
+ const Card = (p) => (
17
+ <div data-test-card>
18
+ <div data-slot="headerStart">{p.headerStart}</div>
19
+ <div data-slot="headerEnd">{p.headerEnd}</div>
20
+ <div data-slot="body">{p.children}</div>
21
+ </div>
22
+ );
23
+
24
+ // Strip unknown DOM props like buttonStyle to avoid warnings
25
+ const Button = ({ buttonStyle, ...rest }) => <button {...rest}>{rest.children}</button>;
26
+
27
+ // Show the icon name in the DOM for assertions
28
+ const Icon = ({ icon, ...rest }) => <span {...rest}>{icon}</span>;
29
+
30
+ return { Card, Button, Icon };
31
+ });
32
+
33
+ /* ------------------------------------------------------------------ *
34
+ * DOM setup / teardown
35
+ * ------------------------------------------------------------------ */
36
+ let container, root;
37
+ beforeEach(() => {
38
+ container = document.createElement('div');
39
+ document.body.appendChild(container);
40
+ root = createRoot(container);
41
+ });
42
+ afterEach(async () => {
43
+ await act(async () => {
44
+ root.unmount();
45
+ });
46
+ document.body.removeChild(container);
47
+ container = null;
48
+ });
49
+
50
+ /* ------------------------------------------------------------------ *
51
+ * helpers
52
+ * ------------------------------------------------------------------ */
53
+ const findButton = (rootEl) => rootEl.querySelector('button');
54
+
55
+ /* ------------------------------------------------------------------ *
56
+ * fixtures
57
+ * ------------------------------------------------------------------ */
58
+ const baseProps = {
59
+ handleTypeToggle: jest.fn(),
60
+ id: 't1',
61
+ category_id: 'cat-1',
62
+ title: 'Type 1 - Disorderly',
63
+ description: 'A description of the incident type.',
64
+ };
65
+
66
+ /* ------------------------------------------------------------------ *
67
+ * tests
68
+ * ------------------------------------------------------------------ */
69
+ it('renders without crashing (snapshot)', async () => {
70
+ await act(async () => {
71
+ root.render(<IncidentTypeCard {...baseProps} isSelected={false} />);
72
+ });
73
+ expect(container.innerHTML).toMatchSnapshot();
74
+ });
75
+
76
+ it('shows "Add" when not selected', async () => {
77
+ await act(async () => {
78
+ root.render(<IncidentTypeCard {...baseProps} isSelected={false} />);
79
+ });
80
+
81
+ // title in headerStart
82
+ expect(container.textContent).toContain('Type 1 - Disorderly');
83
+ // description in body
84
+ expect(container.textContent).toContain('A description of the incident type.');
85
+
86
+ // Button should say "Add"
87
+ const btn = findButton(container);
88
+ expect(btn).toBeTruthy();
89
+ expect(btn.textContent).toContain('Add');
90
+ // Should not show the check icon name
91
+ expect(container.textContent).not.toContain('check-circle');
92
+ });
93
+
94
+ it('shows a check icon when selected', async () => {
95
+ await act(async () => {
96
+ root.render(<IncidentTypeCard {...baseProps} isSelected={true} />);
97
+ });
98
+
99
+ const btn = findButton(container);
100
+ expect(btn).toBeTruthy();
101
+ // Our Icon mock renders its `icon` prop as text
102
+ expect(btn.textContent).toContain('check-circle');
103
+ // Should not show "Add"
104
+ expect(btn.textContent).not.toContain('Add');
105
+ });
106
+
107
+ it('calls handleTypeToggle with the correct type data when clicked', async () => {
108
+ const handleTypeToggle = jest.fn();
109
+ await act(async () => {
110
+ root.render(
111
+ <IncidentTypeCard
112
+ {...baseProps}
113
+ handleTypeToggle={handleTypeToggle}
114
+ isSelected={false}
115
+ />
116
+ );
117
+ });
118
+
119
+ const btn = findButton(container);
120
+ expect(btn).toBeTruthy();
121
+
122
+ await act(async () => {
123
+ btn.click();
124
+ });
125
+
126
+ expect(handleTypeToggle).toHaveBeenCalledTimes(1);
127
+ expect(handleTypeToggle).toHaveBeenCalledWith({
128
+ id: 't1',
129
+ title: 'Type 1 - Disorderly',
130
+ category_id: 'cat-1',
131
+ description: 'A description of the incident type.',
132
+ });
133
+ });