@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,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 defaultwidth="fill" panetitle="[object Object]"></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,5 @@
1
+ .profilePlaceholder {
2
+ max-width: 100px;
3
+ max-height: 100px;
4
+ object-fit: scale-down;
5
+ }
@@ -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,3 @@
1
+ export const isAValidURL = (str) => {
2
+ return URL.canParse(str);
3
+ };
@@ -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
+ }