@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,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;
|