@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,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Button } from '@folio/stripes/components';
|
|
4
|
+
import {
|
|
5
|
+
thumbnailContainerStyle,
|
|
6
|
+
thumbnailTextStyle,
|
|
7
|
+
buttonContainerStyle,
|
|
8
|
+
thumbnailStyle,
|
|
9
|
+
buttonStyle
|
|
10
|
+
} from './ThumbnailStyles';
|
|
11
|
+
import playButton from '../../../icons/playButton.png';
|
|
12
|
+
|
|
13
|
+
import { FormattedMessage } from 'react-intl';
|
|
14
|
+
const Thumbnail = React.memo(({
|
|
15
|
+
handler,
|
|
16
|
+
mediaId,
|
|
17
|
+
src,
|
|
18
|
+
alt,
|
|
19
|
+
imageDescription,
|
|
20
|
+
handleMarkForRemoval,
|
|
21
|
+
context
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div style={thumbnailContainerStyle}>
|
|
27
|
+
<button onClick={handler} type="button">
|
|
28
|
+
{src === 'isVideo' ? (
|
|
29
|
+
<img
|
|
30
|
+
src={playButton}
|
|
31
|
+
alt={alt}
|
|
32
|
+
style={thumbnailStyle}
|
|
33
|
+
/>
|
|
34
|
+
) : (<img
|
|
35
|
+
src={src}
|
|
36
|
+
alt={alt}
|
|
37
|
+
style={thumbnailStyle}
|
|
38
|
+
/>)}
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<div style={thumbnailTextStyle}>
|
|
42
|
+
<p>{imageDescription}</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{context === 'details' ?
|
|
46
|
+
<></> : (<div style={buttonContainerStyle}>
|
|
47
|
+
<Button
|
|
48
|
+
buttonStyle='default'
|
|
49
|
+
style={buttonStyle}
|
|
50
|
+
onClick={() => handleMarkForRemoval(mediaId)}
|
|
51
|
+
type="button"
|
|
52
|
+
aria-label={`Remove ${imageDescription}`}
|
|
53
|
+
>
|
|
54
|
+
<FormattedMessage id="remove-button" />
|
|
55
|
+
</Button>
|
|
56
|
+
</div>)}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
Thumbnail.propTypes = {
|
|
64
|
+
handler: PropTypes.func.isRequired,
|
|
65
|
+
src: PropTypes.string.isRequired,
|
|
66
|
+
alt: PropTypes.string.isRequired,
|
|
67
|
+
imageDescription: PropTypes.string.isRequired,
|
|
68
|
+
style: PropTypes.object.isRequired,
|
|
69
|
+
contentType: PropTypes.string.isRequired
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default Thumbnail;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FormattedMessage } from 'react-intl';
|
|
3
|
+
import { Icon, Button } from '@folio/stripes/components'
|
|
4
|
+
import {
|
|
5
|
+
thumbnailContainerStyle,
|
|
6
|
+
thumbnailTextStyle,
|
|
7
|
+
buttonContainerStyle,
|
|
8
|
+
thumbnailStyle,
|
|
9
|
+
buttonStyle
|
|
10
|
+
} from './ThumbnailStyles';
|
|
11
|
+
const ThumbnailMarkRemoval = ({ handleUndo, undoId }) => {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<div style={thumbnailContainerStyle}>
|
|
15
|
+
|
|
16
|
+
<div style={thumbnailStyle}>
|
|
17
|
+
<Icon icon='dash'></Icon>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div style={thumbnailTextStyle}>
|
|
21
|
+
<p><FormattedMessage id="media-marked-for-removal"/></p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div style={buttonContainerStyle}>
|
|
25
|
+
<Button
|
|
26
|
+
buttonStyle='default'
|
|
27
|
+
style={buttonStyle}
|
|
28
|
+
onClick={() => handleUndo(undoId)}
|
|
29
|
+
>
|
|
30
|
+
<FormattedMessage id="undo-button"/>
|
|
31
|
+
</Button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default ThumbnailMarkRemoval;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Loading } from '@folio/stripes/components';
|
|
4
|
+
import {
|
|
5
|
+
thumbnailContainerStyle,
|
|
6
|
+
buttonStyle
|
|
7
|
+
} from './ThumbnailStyles';
|
|
8
|
+
const ThumbnailSkeleton = () => {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div style={thumbnailContainerStyle}>
|
|
12
|
+
|
|
13
|
+
<div style={{ marginTop: '45px'}}>
|
|
14
|
+
<Loading size ='large'/>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div style={{ marginTop: '50px'}}>
|
|
18
|
+
<Loading size ='medium'/>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div style={{ marginTop: '40px'}}>
|
|
22
|
+
<Loading size ='medium' style={buttonStyle}/>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default ThumbnailSkeleton;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
export const thumbnailContainerStyle = {
|
|
3
|
+
width: '120px', // Ensure enough space for padding and internal content
|
|
4
|
+
borderRadius: '4px',
|
|
5
|
+
margin: '10px',
|
|
6
|
+
textAlign: 'center',
|
|
7
|
+
// border: '1px solid #ccc',
|
|
8
|
+
boxSizing: 'border-box', // Ensure padding is included within the width
|
|
9
|
+
padding: '10px',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column', // Stack elements vertically
|
|
12
|
+
alignItems: 'center' // Center content within the container
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const thumbnailStyle = {
|
|
16
|
+
width: '100px',
|
|
17
|
+
height: 'auto',
|
|
18
|
+
objectFit: 'cover',
|
|
19
|
+
maxWidth: '100%', // Ensure it stays within the container
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const thumbnailTextStyle = {
|
|
23
|
+
maxWidth: '100px',
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
height: '4.4em', // 2 lines of height
|
|
26
|
+
margin: '5px 0',
|
|
27
|
+
textAlign: 'center',
|
|
28
|
+
lineHeight: '1.2em', // Each line is 1.2 times the font size
|
|
29
|
+
whiteSpace: 'normal',
|
|
30
|
+
display: 'block', // Ensure block-level for multi-line text
|
|
31
|
+
textOverflow: 'ellipsis', // Add ellipsis if the text exceeds 2 lines
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const buttonContainerStyle = {
|
|
35
|
+
marginTop: '10px',
|
|
36
|
+
textAlign: 'center',
|
|
37
|
+
width: '100%', // Ensure the button spans the width of the container
|
|
38
|
+
display: 'flex',
|
|
39
|
+
justifyContent: 'center', // Center the button horizontally
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const buttonStyle = {
|
|
43
|
+
marginTop: '-10px',
|
|
44
|
+
maxWidth: '100%', // Ensure the button doesn't exceed container width
|
|
45
|
+
padding: '5px 10px', // Add some padding for visual appeal
|
|
46
|
+
whiteSpace: 'nowrap', // Prevent text from wrapping to the next line
|
|
47
|
+
overflow: 'hidden', // Hide any overflowed content
|
|
48
|
+
textOverflow: 'ellipsis', // Show an ellipsis if the text is too long
|
|
49
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Button, Icon } from '@folio/stripes/components';
|
|
4
|
+
import {
|
|
5
|
+
thumbnailContainerStyle,
|
|
6
|
+
thumbnailTextStyle,
|
|
7
|
+
buttonContainerStyle,
|
|
8
|
+
thumbnailStyle,
|
|
9
|
+
buttonStyle
|
|
10
|
+
} from './ThumbnailStyles';
|
|
11
|
+
import { FormattedMessage } from 'react-intl';
|
|
12
|
+
const ThumbnailTempPreSave = React.memo(({
|
|
13
|
+
mediaId,
|
|
14
|
+
src,
|
|
15
|
+
alt,
|
|
16
|
+
imageDescription,
|
|
17
|
+
handleRemoveUnsavedMedia,
|
|
18
|
+
handleRemoveUnsavedMediaCreate,
|
|
19
|
+
context
|
|
20
|
+
}) => {
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<div style={thumbnailContainerStyle}>
|
|
25
|
+
{src === 'isVideo' ? (
|
|
26
|
+
<div style={thumbnailStyle}>
|
|
27
|
+
<Icon icon='play' size='large'></Icon>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
: src === 'isPdf' ? (
|
|
31
|
+
<div style={thumbnailStyle}>
|
|
32
|
+
<Icon icon='report' size='large'></Icon>
|
|
33
|
+
</div>
|
|
34
|
+
) :(<img
|
|
35
|
+
src={src}
|
|
36
|
+
alt={alt}
|
|
37
|
+
style={thumbnailStyle}
|
|
38
|
+
/>)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
<div style={thumbnailTextStyle}>
|
|
42
|
+
<p>{imageDescription}</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div style={buttonContainerStyle}>
|
|
46
|
+
<Button
|
|
47
|
+
buttonStyle='default'
|
|
48
|
+
style={buttonStyle}
|
|
49
|
+
onClick={context === 'create' ?
|
|
50
|
+
() => handleRemoveUnsavedMediaCreate(mediaId)
|
|
51
|
+
: () => handleRemoveUnsavedMedia(mediaId)}
|
|
52
|
+
type="button"
|
|
53
|
+
aria-label={`Remove ${imageDescription}`}
|
|
54
|
+
>
|
|
55
|
+
<FormattedMessage id="remove-button" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ThumbnailTempPreSave.propTypes = {
|
|
64
|
+
src: PropTypes.string.isRequired,
|
|
65
|
+
alt: PropTypes.string.isRequired,
|
|
66
|
+
imageDescription: PropTypes.string.isRequired,
|
|
67
|
+
style: PropTypes.object.isRequired,
|
|
68
|
+
handleRemoveUnsavedMedia: PropTypes.func,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default ThumbnailTempPreSave;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 UpdateReport extends React.Component {
|
|
7
|
+
static contextType = IncidentContext;
|
|
8
|
+
|
|
9
|
+
static manifest = Object.freeze({
|
|
10
|
+
incident: {
|
|
11
|
+
type: 'okapi',
|
|
12
|
+
records: 'incidents',
|
|
13
|
+
PUT: {
|
|
14
|
+
path: 'incidents/!{id}',
|
|
15
|
+
},
|
|
16
|
+
accumulate: true,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// console.log("manifest _resourceData:", JSON.stringify(_resourceData, null, 2));
|
|
21
|
+
|
|
22
|
+
static propTypes = {
|
|
23
|
+
id: PropTypes.string,
|
|
24
|
+
data: PropTypes.object,
|
|
25
|
+
mutator: PropTypes.shape({
|
|
26
|
+
incident: PropTypes.shape({
|
|
27
|
+
PUT: PropTypes.func.isRequired,
|
|
28
|
+
}).isRequired,
|
|
29
|
+
}).isRequired,
|
|
30
|
+
handleCloseEdit: PropTypes.func.isRequired,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
componentDidUpdate(prevProps) {
|
|
34
|
+
if (this.props.data !== prevProps.data && this.props.id) {
|
|
35
|
+
this.updateReport(this.props.id, this.props.data);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updateReport = (id, data) => {
|
|
40
|
+
// handle update report w/ attachments
|
|
41
|
+
if (this.context.attachmentsData && this.context.attachmentsData.length > 0) {
|
|
42
|
+
console.log("EDIT HAS ATTACHMENTS RAN")
|
|
43
|
+
this.props.mutator.incident
|
|
44
|
+
.PUT(data)
|
|
45
|
+
.then(() => {
|
|
46
|
+
this.context.setIdForMediaCreate(this.props.id);
|
|
47
|
+
this.context.setFormDataArrayForMediaCreate(this.context.attachmentsData);
|
|
48
|
+
})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
// this.context.setUseGetList(false);
|
|
51
|
+
console.error(
|
|
52
|
+
'@updateReport - WITH Attachments - Error occurred: ',
|
|
53
|
+
error
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
// handle update report w/out attachments
|
|
58
|
+
this.props.mutator.incident
|
|
59
|
+
.PUT(data)
|
|
60
|
+
.then(() => {
|
|
61
|
+
this.context.setIsLoadingDetails(false);
|
|
62
|
+
this.context.setIsUpdatingReport(false)
|
|
63
|
+
this.props.handleCloseEdit();
|
|
64
|
+
// this.context.setUseGetList(false);
|
|
65
|
+
// console.log('update successful');
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
// this.context.setUseGetList(false);
|
|
69
|
+
console.error(
|
|
70
|
+
'@updateReport - NO Attachments - Error occurred: ',
|
|
71
|
+
error
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
render() {
|
|
78
|
+
return <></>;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
UpdateReport.contextType = IncidentContext;
|
|
83
|
+
|
|
84
|
+
export default stripesConnect(UpdateReport, '@spokane-folio/security-incident');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`CreatePane renders correctly (snapshot test) 1`] = `"<div defaultwidth="100%" panetitle="[object Object]" footer="[object Object]"><div>Mock ModalDescribeCustomer</div><div>Mock ModalSelectKnownCustomer</div><div>Mock ModalSelectWitness</div><div>Mock GetSelf</div><div>Mock ModalAddMedia</div><div>Mock ModalCustomWitness</div><div>Mock GetTrespassTemplates</div><div>Mock GetLocations</div><div>Mock GetLocationsInService</div><div>Mock GetIncidentTypesDetails</div><div>Mock ModalSelectIncidentTypes</div><div>Mock CreateReport</div><div><button></button><div label="[object Object]"><div><div xs="2" style="margin-top: 10px; margin-left: 10px; margin-bottom: 10px;"><div label="[object Object]" name="customerNa"></div></div></div><div><div xs="3"><button style="margin-top: 10px;"><span>select-add-known-customer-button</span></button></div></div><div><div xs="3" style="margin-top: 10px;"><button><span>describe-add-unknown-customer-button</span></button></div></div><div><div xs="9"><label style="margin-top: 5px;" tag="h2"><b>customers-list-label</b></label><ul liststyle="bullets" label="customers-list-label" items="" isemptymessage="[object Object]"></ul></div></div></div><div label="[object Object]"><div><div xs="3"><select required="" label="[object Object]" name="incidentLocation" dataoptions="[object Object]"></select></div><div xs="3"><select label="[object Object]" name="subLocation" dataoptions=""></select></div></div><div style="margin-top: 25px;"><div xs="3"><input type="date" required="" name="dateOfIncident" label="[object Object]" value="01/01/2020"></div><div xs="2"><input type="time" required="" name="timeOfIncident" label="[object Object]" value="12:00"></div><div xs="2" style="margin-top: 25px;"><div label="Approximate time" name="isApproximateTime"></div></div></div><div><div xs="4"><button style="margin-top: 15px;"><span>create-pane.select-add-incident-type-button</span></button></div></div><div><div xs="4" style="padding-left: 20px;"><label style="margin-top: 5px;" tag="h2"><b>incident-types-list-label</b></label><ul liststyle="bullets" label="incident-types-list-label" items="" isemptymessage="[object Object]"></ul></div></div><div style="margin-top: 25px;"><div xs="6"><textarea required="" label="[object Object]"></textarea></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>select-add-witness-button</span></button></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>add-self-witness-button</span></button></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>add-custom-witness-button</span></button></div></div><div><div xs="6" style="padding-top: 20px;"><label style="margin-top: 5px;" tag="h2"><span>witnesses-list-label</span></label><ul liststyle="bullets" label="witnesses-list-label" items="" isemptymessage="[object Object]"></ul></div></div></div><div label="[object Object]"><div style="margin: 25px;"><div xs="1" style="visibility: hidden;"></div></div><div style="margin-top: 25px;"><div xs="2"><button><span>add-media-button</span></button></div></div></div></div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`renders without crashing (snapshot) 1`] = `"<div>Mock GetDetails</div><div>Mock UpdateReport</div><div panetitle="[object Object]" defaultwidth="fill" footer="[object Object]"><div>Mock GetTrespassTemplates</div><div>Mock GetTrespassReasons</div><div>Mock GetSummary</div><div>Mock ModalCustomWitness</div><div>Mock GetLocationsInService</div><div>Mock GetIncidentTypesDetails</div><div>Mock ModalAddMedia</div><div>Mock ModalDescribeCustomer</div><div>Mock ModalSelectKnownCustomer</div><div>Mock ModalSelectWitness</div><div>Mock ModalSelectIncidentTypes</div><div>Mock GetSelf</div><div><div xs="12"><div><div type="error"><span>message-banner.error-missing-users-404</span></div></div></div></div><div><button></button><div label="[object Object]"><div headinglevel="4" createdby="[object Object]" lastupdatedby="[object Object]"></div><div><div xs="6"><div label="[object Object]" name="staffSuppressed"></div></div></div></div><div label="[object Object]"><div><div xs="2" style="margin-top: 10px; margin-left: 10px; margin-bottom: 10px;"><div label="[object Object]" name="customerNa"></div></div></div><div><div xs="3"><button style="margin-top: 25px;"><span>select-add-known-customer-button</span></button></div></div><div><div xs="3"><button style="margin-top: 25px;"><span>describe-add-unknown-customer-button</span></button></div></div><div><div xs="6"><label style="margin-top: 5px;" tag="h2" id="customer-list-label"><b>customers-list-label</b></label></div></div><div><div xs="12"><ul></ul></div></div></div><div label="[object Object]"><div><div xs="3"><select required="" label="[object Object]" name="incidentLocation" dataoptions="[object Object]"></select></div><div xs="3"><select label="[object Object]" name="subLocation" dataoptions="[object Object]"></select></div></div><div><div xs="3"><input required="" name="dateTimeOfIncident" label="[object Object]" value="2020-01-01T12:00:00Z"></div><div xs="2"><input required="" name="timeOfIncident" label="[object Object]" value="2020-01-01T12:00:00Z"></div><div xs="2" style="margin-top: 25px;"><div label="Approximate time" name="isApproximateTime"></div></div></div><div><div xs="4"><button style="margin-top: 15px;"><span>edit-pane.select-add-incident-type-button</span></button></div></div><div><div xs="8" style="padding-left: 20px;"><label style="margin-top: 5px;" tag="h2"><b>incident-types-list-label</b></label><ul></ul></div></div><div style="margin-top: 25px;"><div xs="6"><textarea required="" label="[object Object]" modules="[object Object]">desc</textarea></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>select-add-witness-button</span></button></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>add-self-witness-button</span></button></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>add-custom-witness-button</span></button></div></div><div><div xs="2" style="padding-top: 25px;"><button><span>link-to-button</span></button></div></div><div><div xs="8"><label style="margin-top: 5px;" tag="h2"><span>witnesses-list-label</span></label><ul></ul></div></div></div><div label="[object Object]"><div></div><div style="margin: 25px;"><div xs="1" style="visibility: hidden;"></div></div><div style="margin: 25px;"><div xs="1" style="visibility: hidden;"></div></div><div style="margin: 25px;"><div xs="1" style="visibility: hidden;"></div></div><div style="margin-top: 25px;"><div xs="2"><button><span>add-media-button</span></button></div></div></div><div label="[object Object]"><div></div><div><div xs="1" style="visibility: hidden;"></div></div></div></div></div>)"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`renders without crashing (snapshot) 1`] = `"<div data-test-card="true"><div data-slot="headerStart"><b>Type 1 - Disorderly</b></div><div data-slot="headerEnd"><button style="margin-top: 10px;">Add</button></div><div data-slot="body">A description of the incident type.</div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`renders without crashing (snapshot) 1`] = `"<div><div panetitle="[object Object]" defaultwidth="25%"><div>Mock GetIncidentTypesDetails</div><div>Mock GetLocationsInService</div><div>Mock GetLocations</div><div>Mock GetOrgLocaleSettings</div><div style="margin-top: 35px;"><div xs="10"><h2 margin="medium" tag="h2" id="searchField-label">search-pane.searchField-h2-label</h2></div></div><div><div xs="12"><input aria-labelledby="searchField-label" name="term" searchableindexes="[object Object],[object Object],[object Object],[object Object],[object Object]" value=""><div style="margin-top: 45px;"><div xs="10"><h2 margin="medium" tag="h2">search-pane.filters-h2-label</h2></div></div><div label="[object Object]"><div><div xs="10"><div value="" items="" menustyle="[object Object]"></div></div></div><div><div xs="12" style="margin-left: 10px;"><div style="max-height: 125px; overflow-x: clip; overflow-y: auto; margin-top: 8px;"></div><div style="margin-top: 2px;"></div></div></div></div><div label="[object Object]"><div><div xs="10"><div value="" items="" menustyle="[object Object]"></div></div></div><div><div xs="12" style="margin-left: 10px;"><div style="max-height: 125px; overflow-x: clip; overflow-y: auto; margin-top: 8px;"></div><div style="margin-top: 2px;"></div></div></div></div><hr><div style="margin-top: 25px;"><div xs="8"><input label="[object Object]" value=""></div></div><div><div xs="8"><input label="[object Object]" value=""></div></div></div></div><div><div xs="10" style="margin-top: 12px;"><input label="[object Object]" name="startDate" value=""><input label="[object Object]" name="endDate" value=""></div></div><div><div xs="12"><h2 tag="h2" style="margin-top: 15px;">search-pane.trespass-status-label</h2></div></div><div style="margin-top: -15px;"><div xs="12"><input type="radio" label="[object Object]" checked=""></div></div><div><div xs="12"><input type="radio" label="[object Object]"></div></div><div><div xs="12"><input type="radio" label="[object Object]"></div></div><div style="margin-top: 20px;"><div xs="12"><div label="[object Object]"><div><div xs="12"><input type="radio" name="staffSuppress" label="[object Object]" value="non" checked=""></div></div><div><div xs="12"><input type="radio" name="staffSuppress" label="[object Object]" value="suppressed"></div></div><div><div xs="12"><input type="radio" name="staffSuppress" label="[object Object]" value="all"></div></div></div></div></div><div><div xs="12"><button style="margin-top: 23px;" buttonstyle="primary">search-button</button></div></div><div><div xs="12"><button style="background-color: rgb(222, 221, 217);" buttonstyle="disabled"><span icon="times-circle-solid">times-circle-solid</span></button></div></div></div><div>Mock ResultsPane</div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ModalAddMedia renders correctly when open (snapshot test) 1`] = `"<div style="min-height: 550px; height: 80%; max-height: 300vh; max-width: 300vw; width: 60%;" open="" label="[object Object]" footer="[object Object]"><div><div defaultwidth="fill"><div><div xs="5"><input type="text" name="description" label="[object Object]" value=""></div></div><div><div xs="6"><input type="file"></div></div><div style="margin-top: 25px;"><div xs="8"><div type="error"><span>modal-add-media-error-msg-default</span></div></div></div></div></div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ModalCustomerDetails renders correctly when open (snapshot test) 1`] = `"<div style="min-height: 250px; height: 80%; max-height: 300vh; max-width: 300vw; width: 70%;" open="" label="[object Object]" footer="[object Object]" contentclass="modalContent"><div><div defaultwidth="100%"><div><div label="[object Object]"><div><div xs="3"><input type="text" name="firstName" label="[object Object]" value="John"></div></div><div><div xs="3"><input type="text" name="lastName" label="[object Object]" value="Doe"></div></div><div><div xs="6"><textarea label="[object Object]" modules="[object Object]" required="">Test description</textarea></div></div></div><div label="[object Object]"><div><div xs="2"><input type="text" label="[object Object]" name="sex" value=""></div><div xs="2"><input type="text" label="[object Object]" name="race" value=""></div><div xs="2"><input type="text" label="[object Object]" name="height" value=""></div></div><div><div xs="2"><input type="text" label="[object Object]" name="weight" value=""></div><div xs="2"><input type="text" label="[object Object]" name="hair" value=""></div><div xs="2"><input type="text" label="[object Object]" name="eyes" value=""></div></div><div><div xs="3"><input type="date" label="[object Object]" name="dateOfBirth" value=""></div></div><div><div xs="4"><input type="text" label="[object Object]" name="streetAddress" value=""></div><div xs="2"><input type="text" label="[object Object]" name="city" value=""></div></div><div style="margin-bottom: 100px;"><div xs="2"><input type="text" label="[object Object]" name="state" value=""></div><div xs="2"><input type="text" label="[object Object]" name="zipcode" value=""></div></div></div></div></div></div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`renders open modal with search + results (snapshot) 1`] = `"<div data-modal="true"><div data-modal-body="true"><div>Mock GetPatronGroups</div><div class="modalBody"><div data-paneset="true"><div data-pane="true"><div><input placeholder="Name or barcode"><button><span>search-button</span></button></div></div><div data-pane="true"><div data-pane-header="true">modal-select-witness.results-pane.paneSubTitle</div><div><div class="mclContainer"><table data-mcl="true"><tbody><tr><td data-col="name">Public, Jane Q</td><td data-col="active"><p><span>modal-select-customer.resultsFormatter-active</span></p></td><td data-col="patronGroup">Staff</td><td data-col="barcode">111</td><td data-col="profilePicLinkOrUUID"><div style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center;"><div style="width: 100px; height: 100px; display: flex; align-items: center; justify-content: center;"><div data-profile-picture="true">uuid-111</div></div></div></td><td data-col="id"><button>Add</button></td></tr><tr><td data-col="name">Doe, John</td><td data-col="active"><p><span>modal-select-customer.resultsFormatter-inactive</span></p></td><td data-col="patronGroup">Visitor</td><td data-col="barcode">222</td><td data-col="profilePicLinkOrUUID"><div style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center;"><div style="width: 100px; height: 100px; display: flex; align-items: center; justify-content: center;"><div data-profile-picture="true">uuid-222</div></div></div></td><td data-col="id"><button>Add</button></td></tr></tbody></table></div></div></div></div></div></div><div data-modal-footer="true"><button id="close-continue-button"><span>close-continue-button</span></button><button><span>cancel-button</span></button></div></div>"`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`renders without crashing (snapshot) 1`] = `"<div panetitle="[object Object]" id="results-pane" defaultwidth="75%"><div>Mock GetIncidentTypesDetails</div><div style="height: 80vh; width: auto;"><div sortablefields="customers,incidentLocation,dateOfIncident,incidentTypes,incidentWitnesses,createdBy,trespassExpirationDates" sortedcolumn="" sortdirection="ascending" contentdata="" pageamount="20" totalcount="0" pagingoffset="0" pagingtype="prev-next" formatter="[object Object]" visiblecolumns="customers,incidentLocation" columnmapping="[object Object]" columnwidths="[object Object]"></div></div></div>"`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
ProfilePicture - a Track app vendored and slightly scoped down copy
|
|
3
|
+
of the ProfilePicture as seen in the @folio/stripes-smart-components v10.0.2.
|
|
4
|
+
*/
|
|
5
|
+
import { useIntl } from 'react-intl';
|
|
6
|
+
import PropTypes from 'prop-types';
|
|
7
|
+
import { Img } from 'react-image';
|
|
8
|
+
import { Loading } from '@folio/stripes/components';
|
|
9
|
+
import { isAValidURL } from './isAValidURL';
|
|
10
|
+
import profilePicThumbnail from '../../../../../icons/profilePicThumbnail.png';
|
|
11
|
+
import useProfilePicture from './useProfilePicture';
|
|
12
|
+
import css from './ProfilePicture.css';
|
|
13
|
+
|
|
14
|
+
const ProfilePicture = ({ profilePictureLink, croppedLocalImage }) => {
|
|
15
|
+
/*
|
|
16
|
+
croppedLocalImage is not used in Track (read only for ProfilePicture)
|
|
17
|
+
keeping prop and logic path for API parity so potential future comparisons or upgrades remain predictable.
|
|
18
|
+
*/
|
|
19
|
+
const intl = useIntl();
|
|
20
|
+
|
|
21
|
+
const { isFetching, profilePictureData } = useProfilePicture({ profilePictureId: profilePictureLink });
|
|
22
|
+
|
|
23
|
+
const hasProfilePicture = Boolean(croppedLocalImage) || Boolean(profilePictureLink);
|
|
24
|
+
|
|
25
|
+
const isProfilePictureLinkAURL = hasProfilePicture && isAValidURL(profilePictureLink);
|
|
26
|
+
|
|
27
|
+
const profilePictureSrc = croppedLocalImage || (isProfilePictureLinkAURL ? profilePictureLink : 'data:;base64,' + profilePictureData);
|
|
28
|
+
|
|
29
|
+
const imgSrc = !hasProfilePicture ? profilePicThumbnail : profilePictureSrc;
|
|
30
|
+
|
|
31
|
+
if (isFetching) {
|
|
32
|
+
return <span data-testid="profile-picture-loader"> <Loading /> </span>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Img
|
|
37
|
+
data-testid="profile-picture"
|
|
38
|
+
className={css.profilePlaceholder}
|
|
39
|
+
alt={intl.formatMessage({ id: 'ui-users.information.profilePicture' })}
|
|
40
|
+
src={imgSrc}
|
|
41
|
+
loader={<Loading />}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
ProfilePicture.propTypes = {
|
|
47
|
+
profilePictureLink: PropTypes.string,
|
|
48
|
+
croppedLocalImage: PropTypes.string,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default ProfilePicture;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { useStripes } from '@folio/stripes/core';
|
|
3
|
+
|
|
4
|
+
// lightweight uuid check
|
|
5
|
+
const looksLikeUUID = (s) =>
|
|
6
|
+
typeof s === 'string' &&
|
|
7
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s);
|
|
8
|
+
|
|
9
|
+
const API_BASE = 'users/profile-picture';
|
|
10
|
+
|
|
11
|
+
// session cache
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* fetch profile picture for a given UUID.
|
|
16
|
+
* returns { isFetching, isLoading, profilePictureData (base64), error, reload }
|
|
17
|
+
*
|
|
18
|
+
* - returns base64 only; <ProfilePicture /> caller builds
|
|
19
|
+
* the data: URI at its profilePictureSrc
|
|
20
|
+
*
|
|
21
|
+
* - 404 is treated as "no picture", not an error (returns empty string).
|
|
22
|
+
* - uses Okapi headers from stripes context by default; can be overridden via options -> Track app doesn't make use of these options, but keeping as a zero cost potential future alignment with FOLIO upstream
|
|
23
|
+
*/
|
|
24
|
+
export default function useProfilePicture(
|
|
25
|
+
{ profilePictureId },
|
|
26
|
+
options = {} // { baseUrl, tenant, token, enabled, disableCache }
|
|
27
|
+
) {
|
|
28
|
+
const stripes = typeof useStripes === 'function' ? useStripes() : null;
|
|
29
|
+
|
|
30
|
+
const okapiUrl = options.baseUrl ?? stripes?.okapi?.url;
|
|
31
|
+
const okapiTenant = options.tenant ?? stripes?.okapi?.tenant;
|
|
32
|
+
const okapiToken = options.token ?? stripes?.okapi?.token;
|
|
33
|
+
const disableCache = Boolean(options.disableCache);
|
|
34
|
+
|
|
35
|
+
const enabled =
|
|
36
|
+
(options.enabled ?? true) &&
|
|
37
|
+
Boolean(profilePictureId) &&
|
|
38
|
+
looksLikeUUID(profilePictureId) &&
|
|
39
|
+
Boolean(okapiUrl) &&
|
|
40
|
+
Boolean(okapiTenant);
|
|
41
|
+
|
|
42
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
43
|
+
const [profilePictureData, setProfilePictureData] = useState(
|
|
44
|
+
(!disableCache && cache.get(profilePictureId)) || ''
|
|
45
|
+
);
|
|
46
|
+
const [error, setError] = useState(null);
|
|
47
|
+
|
|
48
|
+
// bump this to force re-fetch
|
|
49
|
+
const [nonce, setNonce] = useState(0);
|
|
50
|
+
const abortRef = useRef(null);
|
|
51
|
+
|
|
52
|
+
const reload = useCallback(() => {
|
|
53
|
+
if (!disableCache) cache.delete(profilePictureId);
|
|
54
|
+
setNonce((n) => n + 1);
|
|
55
|
+
}, [profilePictureId, disableCache]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!enabled) {
|
|
59
|
+
setIsFetching(false);
|
|
60
|
+
setError(null);
|
|
61
|
+
// don't wipe data; allows graceful fallback
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!disableCache && cache.has(profilePictureId)) {
|
|
66
|
+
setProfilePictureData(cache.get(profilePictureId));
|
|
67
|
+
setIsFetching(false);
|
|
68
|
+
setError(null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setIsFetching(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
|
|
75
|
+
// cancel any in-flight request
|
|
76
|
+
if (abortRef.current) abortRef.current.abort();
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
abortRef.current = controller;
|
|
79
|
+
|
|
80
|
+
const url = `${okapiUrl.replace(/\/$/, '')}/${API_BASE}/${profilePictureId}`;
|
|
81
|
+
|
|
82
|
+
fetch(url, {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
headers: {
|
|
85
|
+
'X-Okapi-Tenant': okapiTenant,
|
|
86
|
+
...(okapiToken ? { 'X-Okapi-Token': okapiToken } : {}),
|
|
87
|
+
'Accept': 'application/json',
|
|
88
|
+
},
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
})
|
|
91
|
+
.then(async (res) => {
|
|
92
|
+
if (res.status === 404) return null; // no picture set
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const text = await res.text().catch(() => '');
|
|
95
|
+
throw new Error(`GET ${API_BASE}/${profilePictureId} failed: ${res.status} ${text}`);
|
|
96
|
+
}
|
|
97
|
+
return res.json();
|
|
98
|
+
})
|
|
99
|
+
.then((json) => {
|
|
100
|
+
if (!json) {
|
|
101
|
+
setProfilePictureData('');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// payload key in Users: profile_picture_blob
|
|
105
|
+
const b64 = json.profile_picture_blob ?? '';
|
|
106
|
+
const val = typeof b64 === 'string' ? b64 : '';
|
|
107
|
+
if (!disableCache) cache.set(profilePictureId, val);
|
|
108
|
+
setProfilePictureData(val);
|
|
109
|
+
})
|
|
110
|
+
.catch((e) => {
|
|
111
|
+
if (e.name !== 'AbortError') setError(e);
|
|
112
|
+
})
|
|
113
|
+
.finally(() => setIsFetching(false));
|
|
114
|
+
|
|
115
|
+
return () => controller.abort();
|
|
116
|
+
}, [
|
|
117
|
+
enabled,
|
|
118
|
+
okapiUrl,
|
|
119
|
+
okapiTenant,
|
|
120
|
+
okapiToken,
|
|
121
|
+
profilePictureId,
|
|
122
|
+
disableCache,
|
|
123
|
+
nonce, // triggers reload
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
return { isFetching, isLoading: isFetching, profilePictureData, error, reload };
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default function buildQueryString(
|
|
2
|
+
filtersOrFull = {},
|
|
3
|
+
sort = '',
|
|
4
|
+
dir = 'asc'
|
|
5
|
+
) {
|
|
6
|
+
// support old signature: buildQueryString({ a:1, b:2 })
|
|
7
|
+
const filters = typeof filtersOrFull === 'object' && !Array.isArray(filtersOrFull)
|
|
8
|
+
? filtersOrFull
|
|
9
|
+
: {};
|
|
10
|
+
|
|
11
|
+
const p = new URLSearchParams();
|
|
12
|
+
|
|
13
|
+
// canonical order
|
|
14
|
+
Object.keys(filters).sort().forEach(k => {
|
|
15
|
+
const v = filters[k];
|
|
16
|
+
if (v !== undefined && v !== null && v !== '') p.set(k, v);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// if (sort) p.set('sort', sort);
|
|
20
|
+
// if (dir) p.set('dir', dir);
|
|
21
|
+
|
|
22
|
+
if (sort !== '') {
|
|
23
|
+
p.set('sort', sort); // only when column name is supplied
|
|
24
|
+
if (dir) p.set('dir', dir) // then only add dir
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return p.toString();
|
|
28
|
+
}
|