@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,128 @@
1
+ // IncidentsPaneset.test.js
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom';
4
+ import { act } from 'react-dom/test-utils';
5
+ import IncidentsPaneset from './IncidentsPaneset';
6
+
7
+ /* ------------------------------------------------------------------ *
8
+ * 1 mock externals
9
+ * ------------------------------------------------------------------ */
10
+ jest.mock('@folio/stripes/core', () => ({
11
+ stripesConnect: (Comp) => Comp,
12
+ }));
13
+
14
+ jest.mock('@folio/stripes/components', () => {
15
+ const React = require('react');
16
+ const make = (tag) => (p) => React.createElement(tag, p, p.children);
17
+ return {
18
+ Accordion: make('div'),
19
+ AutoSuggest: make('div'),
20
+ Checkbox: make('div'),
21
+ Paneset: make('div'),
22
+ Pane: make('div'),
23
+ SearchField: make('input'),
24
+ Button: make('button'),
25
+ Headline: make('h2'),
26
+ Datepicker: make('input'),
27
+ Row: make('div'),
28
+ Col: make('div'),
29
+ Icon: (p) => <span {...p}>{p.icon}</span>,
30
+ TextField: make('input'),
31
+ RadioButton: (p) => <input type="radio" {...p} />,
32
+ };
33
+ });
34
+
35
+ /* ------------------------------------------------------------------ *
36
+ * 2 mock nested panes and utils
37
+ * ------------------------------------------------------------------ */
38
+ jest
39
+ .mock('../../settings/GetLocationsInService', () => () => <div>Mock GetLocationsInService</div>)
40
+ .mock('./GetListDynamicQuery', () => () => <div>Mock GetListDynamicQuery</div>)
41
+ .mock('./GetLocations', () => () => <div>Mock GetLocations</div>)
42
+ .mock('../../settings/GetIncidentTypesDetails', () => () => <div>Mock GetIncidentTypesDetails</div>)
43
+ .mock('./GetOrgLocaleSettings', () => () => <div>Mock GetOrgLocaleSettings</div>)
44
+ .mock('./ResultsPane', () => () => <div>Mock ResultsPane</div>)
45
+ .mock('./DetailsPane', () => () => <div>Mock DetailsPane</div>)
46
+ .mock('./EditPane', () => () => <div>Mock EditPane</div>)
47
+ .mock('./CreatePane', () => () => <div>Mock CreatePane</div>);
48
+
49
+ jest.mock('./usePersistedSort', () => () => ({
50
+ sortColumn: '',
51
+ sortDirection: 'asc',
52
+ setSortColumn: jest.fn(),
53
+ setSortDirection: jest.fn(),
54
+ }));
55
+
56
+ jest.mock('./helpers/buildQueryString', () => jest.fn(() => 'mockedQueryString'));
57
+
58
+ /* ------------------------------------------------------------------ *
59
+ * 3 router and context
60
+ * ------------------------------------------------------------------ */
61
+ jest.mock('react-router-dom', () => ({
62
+ useLocation: () => ({ pathname: '/incidents', search: '' }),
63
+ useHistory: () => ({ push: jest.fn(), replace: jest.fn() }),
64
+ useParams: () => ({}),
65
+ }));
66
+
67
+ jest.mock('../../contexts/IncidentContext', () => ({
68
+ useIncidents: () => ({
69
+ /* pane toggles */
70
+ isDetailsPaneOpen: false,
71
+ isEditPaneOpen: false,
72
+ isCreatePaneOpen: false,
73
+ /* data / helpers */
74
+ incidentTypesNamesIdsList: [],
75
+ locationsInService: [],
76
+ organizationTimezone: 'America/Chicago',
77
+ /* query-string & paging */
78
+ queryString: '',
79
+ setQueryString: jest.fn(),
80
+ appliedSearchParams: {},
81
+ setAppliedSearchParams: jest.fn(),
82
+ limit: 20,
83
+ setLimit: jest.fn(),
84
+ offset: 0,
85
+ setOffset: jest.fn(),
86
+ /* misc */
87
+ setIncidentsList: jest.fn(),
88
+ setTotalResults: jest.fn(),
89
+ }),
90
+ }));
91
+
92
+ /* ------------------------------------------------------------------ *
93
+ * 4 test harness setup / teardown *
94
+ * ------------------------------------------------------------------ */
95
+ let container;
96
+ beforeEach(() => {
97
+ container = document.createElement('div');
98
+ document.body.appendChild(container);
99
+ });
100
+
101
+ afterEach(() => {
102
+ document.body.removeChild(container);
103
+ container = null;
104
+ });
105
+
106
+ /* ------------------------------------------------------------------ *
107
+ * 5 test cases *
108
+ * ------------------------------------------------------------------ */
109
+ it('renders without crashing (snapshot)', () => {
110
+ act(() => {
111
+ ReactDOM.render(<IncidentsPaneset />, container);
112
+ });
113
+ expect(container.innerHTML).toMatchSnapshot();
114
+ });
115
+
116
+ it('mounts key child panes', () => {
117
+ act(() => {
118
+ ReactDOM.render(<IncidentsPaneset />, container);
119
+ });
120
+ // Smoke-test that one or two top-level mocked panes are present
121
+ expect(container.textContent).toEqual(
122
+ expect.stringContaining('Mock GetLocationsInService')
123
+ );
124
+ expect(container.textContent).toEqual(
125
+ expect.stringContaining('Mock ResultsPane')
126
+ );
127
+ });
128
+
@@ -0,0 +1,86 @@
1
+ import convertUTCISOToPrettyDate from './helpers/convertUTCISOToPrettyDate';
2
+ import { Icon } from '@folio/stripes/components';
3
+ import { useIntl, FormattedMessage } from 'react-intl';
4
+ const containerStyle = { padding: '2px 0' };
5
+
6
+ const rowStyle = {
7
+ display: 'flex',
8
+ alignItems: 'flex-start',
9
+ gap: '8px',
10
+ };
11
+
12
+ const linkBlockStyle = {
13
+ display: 'block',
14
+ textDecoration: 'none',
15
+ color: 'rgb(0,0,238)',
16
+ fontWeight: 'bold',
17
+ };
18
+
19
+ const dateStyle = {
20
+ margin: 0,
21
+ lineHeight: 1.2,
22
+ };
23
+
24
+ const listStyle = {
25
+ margin: '6px 0 0 1.5rem',
26
+ padding: 0,
27
+ listStyleType: 'disc',
28
+ listStylePosition: 'outside',
29
+ };
30
+
31
+ const LinkedIncident = ({ summaryObj, onDelete, renderContext = 'create-edit' }) => {
32
+ const intl = useIntl();
33
+ const ariaLabel = intl.formatMessage({
34
+ id: 'linked-incident.aria-label-report-details-view'
35
+ });
36
+
37
+ const customers = summaryObj.customers ?? [];
38
+ const created = summaryObj.createdDate;
39
+
40
+ const handleIconKeyDown = (e) => {
41
+ if (e.key === 'Enter' || e.key === ' ') {
42
+ e.preventDefault();
43
+ onDelete?.(summaryObj.id);
44
+ }
45
+ };
46
+
47
+ return (
48
+ <div style={containerStyle}>
49
+ <div style={rowStyle}>
50
+ {renderContext !== 'details' && (
51
+ <button
52
+ style={{}}
53
+ onClick={() => onDelete?.(summaryObj.id)}
54
+ type="button"
55
+ aria-label="Remove linked incident"
56
+ onKeyDown={handleIconKeyDown}
57
+ >
58
+ <Icon icon="trash" size="medium" />
59
+ </button>
60
+ )}
61
+
62
+ <a
63
+ href={`/incidents/${summaryObj.id}`}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ aria-label={ariaLabel}
67
+ style={linkBlockStyle}
68
+ >
69
+ <p style={dateStyle}>
70
+ {convertUTCISOToPrettyDate(created)}
71
+ </p>
72
+
73
+ {customers.length > 0 && (
74
+ <ul style={listStyle}>
75
+ {customers.map((cust, idx) => (
76
+ <li key={`${summaryObj.id}-${idx}`}>{cust}</li>
77
+ ))}
78
+ </ul>
79
+ )}
80
+ </a>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export default LinkedIncident;
@@ -0,0 +1,262 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { FormattedMessage } from 'react-intl';
3
+ import {
4
+ Button,
5
+ Col,
6
+ MessageBanner,
7
+ Modal,
8
+ Pane,
9
+ Paneset,
10
+ ModalFooter,
11
+ Row,
12
+ TextField,
13
+ } from '@folio/stripes/components';
14
+ import css from './ModalStyle.css';
15
+ import { useIncidents } from '../../contexts/IncidentContext';
16
+ import { fileTypeFromBuffer } from 'file-type';
17
+
18
+ const ModalAddMedia = ({ handleAddMedia, handleAddMediaAtCreate, context }) => {
19
+ const fileInputRef = useRef(null);
20
+ const {
21
+ isModalMedia,
22
+ closeModalMedia,
23
+ setIdForMediaCreate,
24
+ setFormDataArrayForMediaCreate,
25
+ } = useIncidents();
26
+ const [showBanner, setShowBanner] = useState(false);
27
+ const [validFile, setValidFile] = useState(false);
28
+ const [errorMessage, setErrorMessage] = useState('');
29
+ const [mediaDataForm, setMediaDataForm] = useState({
30
+ description: '',
31
+ file: null,
32
+ contentType: '',
33
+ filePreviewUrl: null
34
+ });
35
+ const MAX_SIZE_IMAGE_OR_PDF = 10 * 1024 * 1024; // 10 MB
36
+ const MAX_SIZE_VIDEO = 100 * 1024 * 1024; // 100 MB
37
+
38
+ if (!isModalMedia) {
39
+ return null;
40
+ };
41
+
42
+ const resetFileState = () => {
43
+ setMediaDataForm((prev) => ({
44
+ ...prev,
45
+ description: '',
46
+ file: null,
47
+ contentType: '',
48
+ filePreviewUrl: null,
49
+ }));
50
+ if (fileInputRef.current) {
51
+ fileInputRef.current.value = null;
52
+ };
53
+ };
54
+
55
+ const handleChange = (event) => {
56
+ const { name, value } = event.target;
57
+ setMediaDataForm((prev) => ({
58
+ ...prev,
59
+ [name]: value,
60
+ }))
61
+ };
62
+
63
+ const isFileSizeValid = (mime, size) => {
64
+ // if video/ type allow up to 100MB, else 10MB
65
+ if (mime.startsWith('video/')) {
66
+ return size <= MAX_SIZE_VIDEO;
67
+ } else {
68
+ return size <= MAX_SIZE_IMAGE_OR_PDF
69
+ }
70
+ };
71
+
72
+ const bytesToMB = (bytes) => {
73
+ const megabytes = bytes / (1024 * 1024);
74
+ return megabytes.toFixed(1);
75
+ };
76
+
77
+ const handleFileChange = async (event) => {
78
+ const file = event.target.files[0];
79
+ if (!file) return;
80
+
81
+ setShowBanner(false);
82
+ setValidFile(false);
83
+
84
+ try {
85
+ // read small chunk of file for signature detection
86
+ const chunk = file.slice(0, 4100);
87
+ // read and return promise
88
+ const arrayBuffer = await chunk.arrayBuffer();
89
+ // make byte-level view of raw data
90
+ const buffer = new Uint8Array(arrayBuffer);
91
+ // get detected type
92
+ const detectedType = await fileTypeFromBuffer(buffer);
93
+
94
+ // return if !detectedType
95
+ if (!detectedType) {
96
+ setErrorMessage(<FormattedMessage id="modal-add-media-error-msg-unrecognized"/>);
97
+ setShowBanner(true);
98
+ resetFileState();
99
+ return;
100
+ };
101
+
102
+ const { ext, mime } = detectedType;
103
+ const validExtensions = ['pdf', 'mp4', 'mpeg', 'jpeg', 'jpg', 'png'];
104
+
105
+ // check allowed extensions
106
+ if (!validExtensions.includes(ext)) {
107
+ console.log(`The ext: ${ext}`)
108
+ setErrorMessage(<FormattedMessage
109
+ id="modal-add-media-error-msg-ext-not-allowed"
110
+ values={{ ext }}
111
+ />);
112
+ setShowBanner(true);
113
+ resetFileState();
114
+ return;
115
+ };
116
+
117
+ // check if file size is acceptable
118
+ if (!isFileSizeValid(mime, file.size)) {
119
+ const fileSize = bytesToMB(file.size);
120
+ setErrorMessage(<FormattedMessage
121
+ id="modal-add-media-error-msg-file-too-large"
122
+ values={{ mime, fileSize }}
123
+ />);
124
+ setShowBanner(true);
125
+ resetFileState();
126
+ return;
127
+ };
128
+
129
+ // work with valid file from here
130
+ setValidFile(true);
131
+
132
+ // make preview
133
+ let filePreviewUrl = null;
134
+ if (mime.startsWith('image/')) {
135
+ filePreviewUrl = URL.createObjectURL(file);
136
+ } else if (mime.startsWith('video/')) {
137
+ filePreviewUrl = 'isVideo';
138
+ } else if (mime === 'application/pdf') {
139
+ filePreviewUrl = 'isPdf'
140
+ };
141
+
142
+ // update state
143
+ setMediaDataForm((prev) => ({
144
+ ...prev,
145
+ file,
146
+ contentType: mime,
147
+ filePreviewUrl
148
+ }));
149
+ } catch (error) {
150
+ console.log('error while reading file - error: ', error.message);
151
+ const errorMsg = error.message;
152
+ setErrorMessage(<FormattedMessage
153
+ id="modal-add-media-error-msg-while-reading-file"
154
+ values={{ errorMsg }}
155
+ />)
156
+ setShowBanner(true);
157
+ resetFileState();
158
+ };
159
+ };
160
+
161
+ const handleCancel = () => {
162
+ setMediaDataForm({
163
+ description: '',
164
+ file: null,
165
+ contentType: ''
166
+ });
167
+ setIdForMediaCreate(null);
168
+ setFormDataArrayForMediaCreate(null);
169
+ setShowBanner(false);
170
+ setValidFile(false);
171
+ closeModalMedia();
172
+ };
173
+
174
+ const isFormDataPresent = () => {
175
+ const { description } = mediaDataForm;
176
+ return description.trim() !== '' && validFile
177
+ };
178
+
179
+ const handleSave = () => {
180
+ if (context === 'create') {
181
+ handleAddMediaAtCreate(mediaDataForm)
182
+ } else if (context === 'edit') {
183
+ handleAddMedia(mediaDataForm)
184
+ };
185
+ resetFileState();
186
+ setShowBanner(false);
187
+ setValidFile(false);
188
+ closeModalMedia();
189
+ };
190
+
191
+ const footer = (
192
+ <ModalFooter>
193
+ <Button
194
+ onClick={handleSave}
195
+ buttonStyle="primary"
196
+ disabled={!isFormDataPresent()}
197
+ marginBottom0
198
+ >
199
+ <FormattedMessage id="close-continue-button" />
200
+ </Button>
201
+ <Button onClick={handleCancel}>
202
+ <FormattedMessage id="cancel-button" />
203
+ </Button>
204
+ </ModalFooter>
205
+ );
206
+
207
+ return (
208
+ <Modal
209
+ style={{
210
+ minHeight: '550px',
211
+ height: '80%', // allows modal to grow/shrink based on content
212
+ maxHeight: '300vh', // modal will never exceed 90% of viewport height
213
+ maxWidth: '300vw', // modal width responsive to viewport width
214
+ width: '60%' // modal width adjusts based on content and window size
215
+ }}
216
+ open
217
+ dismissible
218
+ closeOnBackgroundClick
219
+ label={<FormattedMessage id="modal-add-media-label-add-media-file" />}
220
+ size="large"
221
+ onClose={handleCancel}
222
+ footer={footer}
223
+ contentClass={css.modalContent}
224
+ >
225
+ <Paneset>
226
+ <Pane
227
+ defaultWidth='fill'
228
+ >
229
+ <Row>
230
+ <Col xs={5}>
231
+ <TextField
232
+ onChange={handleChange}
233
+ value={mediaDataForm.description}
234
+ name="description"
235
+ label={
236
+ <FormattedMessage id="modal-add-media-textField-description-media-file" />
237
+ }
238
+ />
239
+ </Col>
240
+ </Row>
241
+ <Row>
242
+ <Col xs={6}>
243
+ <input type="file" onChange={handleFileChange} ref={fileInputRef}/>
244
+ </Col>
245
+ </Row>
246
+ <Row style={{ marginTop: '25px' }}>
247
+ <Col xs={8}>
248
+ <MessageBanner
249
+ dismissible
250
+ type='error'
251
+ show={showBanner}>
252
+ {errorMessage || <FormattedMessage id="modal-add-media-error-msg-default"/>}
253
+ </MessageBanner>
254
+ </Col>
255
+ </Row>
256
+ </Pane>
257
+ </Paneset>
258
+ </Modal>
259
+ );
260
+ };
261
+
262
+ export default ModalAddMedia;
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { act } from 'react-dom/test-utils';
4
+ import ModalAddMedia from './ModalAddMedia';
5
+
6
+ // --- Mocks for External Dependencies ---
7
+
8
+ // Mock react-intl so that FormattedMessage simply renders its id.
9
+ jest.mock('react-intl', () => ({
10
+ FormattedMessage: (props) => <span>{props.id}</span>,
11
+ }));
12
+
13
+ // Mock react-router-dom with stable params and history.
14
+ jest.mock('react-router-dom', () => ({
15
+ useParams: () => ({}),
16
+ useHistory: () => ({ push: jest.fn() }),
17
+ }));
18
+
19
+ // Mock @folio/stripes/components with minimal implementations.
20
+ jest.mock('@folio/stripes/components', () => {
21
+ const React = require('react');
22
+ return {
23
+ Button: (props) => <button {...props}>{props.children}</button>,
24
+ Col: (props) => <div {...props}>{props.children}</div>,
25
+ MessageBanner: (props) => <div {...props}>{props.children}</div>,
26
+ Modal: (props) => <div {...props}>{props.children}</div>,
27
+ Pane: (props) => <div {...props}>{props.children}</div>,
28
+ Paneset: (props) => <div {...props}>{props.children}</div>,
29
+ ModalFooter: (props) => <div {...props}>{props.children}</div>,
30
+ Row: (props) => <div {...props}>{props.children}</div>,
31
+ TextField: (props) => <input type="text" {...props} />,
32
+ };
33
+ });
34
+
35
+ // --- Virtual Mock for file-type ---
36
+ // Jest has trouble locating the file-type module (ESM-only), so we create a virtual module.
37
+ jest.mock(
38
+ 'file-type',
39
+ () => ({
40
+ fileTypeFromBuffer: jest.fn(() => Promise.resolve({ ext: 'jpg', mime: 'image/jpeg' })),
41
+ }),
42
+ { virtual: true }
43
+ );
44
+
45
+ // Mock the CSS module.
46
+ jest.mock('./ModalStyle.css', () => ({}));
47
+
48
+ // --- Mocks for the Incident Context ---
49
+ // Define a stable context so that the same object is returned on every call.
50
+ // This prevents useEffect dependencies from changing on every render.
51
+ jest.mock('../../contexts/IncidentContext', () => {
52
+ const stableMediaContext = {
53
+ isModalMedia: true, // default open for our rendering test
54
+ closeModalMedia: jest.fn(),
55
+ setIdForMediaCreate: jest.fn(),
56
+ setFormDataArrayForMediaCreate: jest.fn(),
57
+ };
58
+ return {
59
+ useIncidents: () => stableMediaContext,
60
+ };
61
+ });
62
+
63
+ // --- Test Setup ---
64
+ let container = null;
65
+ beforeEach(() => {
66
+ container = document.createElement('div');
67
+ document.body.appendChild(container);
68
+ });
69
+ afterEach(() => {
70
+ document.body.removeChild(container);
71
+ container = null;
72
+ });
73
+
74
+ // --- Test Cases ---
75
+
76
+ // Test case 1: When isModalMedia is true, the modal should render.
77
+ // We capture a snapshot to help detect unintended changes.
78
+ test('ModalAddMedia renders correctly when open (snapshot test)', () => {
79
+ act(() => {
80
+ ReactDOM.render(<ModalAddMedia context="edit" handleAddMedia={() => {}} />, container);
81
+ });
82
+ expect(container.innerHTML).toMatchSnapshot();
83
+ });
84
+
85
+ // Test case 2: When isModalMedia is false, the component should return null.
86
+ test('ModalAddMedia returns null when not open', () => {
87
+ // Get the stable context and set isModalMedia to false to simulate the modal being closed.
88
+ const { useIncidents } = require('../../contexts/IncidentContext');
89
+ const stableContext = useIncidents();
90
+ stableContext.isModalMedia = false;
91
+
92
+ act(() => {
93
+ ReactDOM.render(<ModalAddMedia context="edit" handleAddMedia={() => {}} />, container);
94
+ });
95
+ // When the modal is closed, the component returns null, so the container should be empty.
96
+ expect(container.innerHTML).toBe('');
97
+ });
@@ -0,0 +1,111 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useIntl, FormattedMessage } from 'react-intl';
3
+ import {
4
+ Button,
5
+ Col,
6
+ List,
7
+ Modal,
8
+ ModalFooter,
9
+ Row,
10
+ } from '@folio/stripes/components';
11
+ import css from './ModalStyle.css';
12
+
13
+ const ModalAttentionDecOfService = ({
14
+ onNo,
15
+ onYes,
16
+ missingIds,
17
+ allCustomers
18
+ }) => {
19
+ const intl = useIntl();
20
+
21
+ // compute list to render
22
+ const customersList = useMemo(() => allCustomers.filter((c) => missingIds.includes(c.id)),
23
+ [allCustomers, missingIds]
24
+ );
25
+ // console.log("customersList --> ", JSON.stringify(customersList, null, 2));
26
+
27
+ const itemFormatter = (cust) => {
28
+ const notAvailable = intl.formatMessage({ id: "unknown-name-placeholder" });
29
+ const firstName = cust.registered === false ?
30
+ cust.firstName || notAvailable : cust.associatedFirstName;
31
+ const lastName = cust.registered === false ?
32
+ cust.lastName || notAvailable : cust.associatedLastName;
33
+ const name = `${lastName}, ${firstName}`;
34
+ return (
35
+ <li>
36
+ {name}
37
+ </li>
38
+ )
39
+ };
40
+
41
+ const footer = (
42
+ <ModalFooter>
43
+ <Button
44
+ onClick={onYes}
45
+ buttonStyle="primary"
46
+ >
47
+ <FormattedMessage id="yes-button" />
48
+ </Button>
49
+ <Button
50
+ onClick={onNo}
51
+ >
52
+ <FormattedMessage id="no-button" />
53
+ </Button>
54
+ </ModalFooter>
55
+ );
56
+
57
+ return (
58
+ <Modal
59
+ style={{
60
+ maxHeight: '500vh',
61
+ maxWidth: '300vw',
62
+ width: '60%'
63
+ }}
64
+ open
65
+ closeOnBackgroundClick
66
+ label={<FormattedMessage id="modal-attention-declaration-of-service" />}
67
+ size="large"
68
+ footer={footer}
69
+ contentClass={css.modalContent}
70
+ >
71
+ <div
72
+ style={{
73
+ display: 'flex',
74
+ flexDirection: 'column',
75
+ gap: 16,
76
+ // Ensure inner flex children can shrink and scroll properly
77
+ minHeight: 0,
78
+ maxHeight: 'calc(80vh - 120px)' // headroom for modal header/footer
79
+ }}>
80
+ <Row style={{ marginTop: '25px' }}>
81
+ <Col
82
+ xs={12}
83
+ style={{ textAlign: 'center' }}
84
+ >
85
+ <FormattedMessage id="modal-attention-declaration-of-service-message" />
86
+ </Col>
87
+ </Row>
88
+
89
+ <Row>
90
+ <Col xs={12} >
91
+ <List
92
+ listStyle='bullet'
93
+ items={customersList}
94
+ itemFormatter={itemFormatter}
95
+ />
96
+ </Col>
97
+ </Row>
98
+
99
+ <Row style={{ marginBottom: '25px' }}>
100
+ <Col
101
+ xs={12}
102
+ style={{ textAlign: 'center' }}>
103
+ <FormattedMessage id="modal-attention-declaration-of-service-question" />
104
+ </Col>
105
+ </Row>
106
+ </div>
107
+ </Modal>
108
+ );
109
+ };
110
+
111
+ export default ModalAttentionDecOfService;