@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,53 @@
|
|
|
1
|
+
|
|
2
|
+
// intlDTFResolvedOptionsTZ = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
3
|
+
const cleanFormValues = (formParamsObject, orgTZ, intlDTFResolvedOptionsTZ) => {
|
|
4
|
+
const trimmedTerm = formParamsObject.term?.trim();
|
|
5
|
+
const cleanedSearchParams = Object.keys(formParamsObject).reduce((acc, key) => {
|
|
6
|
+
const value = formParamsObject[key];
|
|
7
|
+
|
|
8
|
+
// includeSuppressed should only be sent when it is true
|
|
9
|
+
if (key === 'includeSuppressed' && value !== true) {
|
|
10
|
+
return acc; // skip when false or undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if ((key === 'currentTrespass' || key === 'expiredTrespass') && value !== true) {
|
|
14
|
+
return acc; //skip this key
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// handle searchType when it is 'created-by' or 'witnessed-by'
|
|
18
|
+
// is case of use UI Select for those keys and move them to its own key outside of searchType
|
|
19
|
+
// so single source of truth (UI has options for searchType Select OR single text field filter)
|
|
20
|
+
if (key === 'searchType' && value === 'created-by' && trimmedTerm) {
|
|
21
|
+
acc['createdBy'] = trimmedTerm;
|
|
22
|
+
return acc; //skip adding 'searchType' and 'term'
|
|
23
|
+
} else if (key === 'searchType' && value === 'witnessed-by' && trimmedTerm) {
|
|
24
|
+
acc['witnessedBy'] = trimmedTerm;
|
|
25
|
+
return acc; //skip adding 'searchType' and 'term'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// check for searchType without term
|
|
29
|
+
if (key === 'searchType' && !trimmedTerm) {
|
|
30
|
+
return acc; //skip adding 'searchType' without 'term'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// if value is array and empty, skip it (locationValue, incidentTypeId)
|
|
34
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
35
|
+
return acc;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// add only non-empty, non-null, non-undefined values
|
|
39
|
+
if (value !== '' && value !== null && value !== undefined) {
|
|
40
|
+
acc[key] = key === 'term' ? trimmedTerm : value;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return acc;
|
|
44
|
+
}, {});
|
|
45
|
+
|
|
46
|
+
if (cleanedSearchParams.startDate && cleanedSearchParams.endDate) {
|
|
47
|
+
cleanedSearchParams.timezone = orgTZ || intlDTFResolvedOptionsTZ;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return cleanedSearchParams;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default cleanFormValues;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isSameHtml } from './isSameHtml.js';
|
|
2
|
+
import deepNormalizeForComparison from './deepNormalizeForComparison.js';
|
|
3
|
+
|
|
4
|
+
// ***** helpers *****
|
|
5
|
+
const stripUIOnlyKeys = (cust = {}) => {
|
|
6
|
+
const {
|
|
7
|
+
associatedFirstName,
|
|
8
|
+
associatedLastName,
|
|
9
|
+
...rest
|
|
10
|
+
} = cust || {};
|
|
11
|
+
return rest;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const deepMergeWithFallback = (fallback, curr) => {
|
|
15
|
+
if (Array.isArray(fallback) || Array.isArray(curr)) {
|
|
16
|
+
return Array.isArray(curr) ? curr : fallback;
|
|
17
|
+
}
|
|
18
|
+
if (fallback && typeof fallback === 'object' && curr && typeof curr === 'object') {
|
|
19
|
+
const out = { ...fallback };
|
|
20
|
+
for (const k of Object.keys(curr)) {
|
|
21
|
+
out[k] = deepMergeWithFallback(fallback[k], curr[k]);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return curr !== undefined ? curr : fallback;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// remove empty/whitespace-only keys from details, and text-normalize description
|
|
29
|
+
const normalizeCustomerForComparison = (cust) => {
|
|
30
|
+
const {
|
|
31
|
+
details,
|
|
32
|
+
description = '',
|
|
33
|
+
...rest
|
|
34
|
+
} = stripUIOnlyKeys(cust);
|
|
35
|
+
|
|
36
|
+
const canonTrespass = (() => {
|
|
37
|
+
const t = rest?.trespass;
|
|
38
|
+
if (!t) return undefined;
|
|
39
|
+
|
|
40
|
+
const raw = Array.isArray(t.exclusionOrTrespassBasedOn)
|
|
41
|
+
? t.exclusionOrTrespassBasedOn
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
const ids = raw
|
|
45
|
+
.map(e =>
|
|
46
|
+
typeof e === 'string'
|
|
47
|
+
? e
|
|
48
|
+
: (e?.id ?? e?.reasonId ?? e?.value ?? '')
|
|
49
|
+
)
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
|
|
52
|
+
// treat as set for meaningful change
|
|
53
|
+
const uniqSortedIds = Array.from(new Set(ids)).sort();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...t,
|
|
57
|
+
exclusionOrTrespassBasedOn: uniqSortedIds,
|
|
58
|
+
};
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
// strip whitespace-only or empty-string keys from details
|
|
62
|
+
const cleanedDetails = Object.entries(details || {}).reduce((acc, [k, v]) => {
|
|
63
|
+
const trimmed = typeof v === 'string' ? v.trim() : v;
|
|
64
|
+
if (trimmed !== '' && trimmed !== null && trimmed !== undefined) {
|
|
65
|
+
acc[k] = trimmed;
|
|
66
|
+
}
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
const hasRealDetails = Object.keys(cleanedDetails).length > 0;
|
|
70
|
+
|
|
71
|
+
const cleaned = {
|
|
72
|
+
...rest,
|
|
73
|
+
...(hasRealDetails ? { details: cleanedDetails } : {}),
|
|
74
|
+
...(canonTrespass ? { trespass: canonTrespass } : {}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// IMPORTANT: we keep description for HTML semantic equality first;
|
|
78
|
+
// only if it’s not empty do we store a text-normalized version for deep compare.
|
|
79
|
+
if (description && !isSameHtml(description, '')) {
|
|
80
|
+
const text = typeof window !== 'undefined'
|
|
81
|
+
? new DOMParser().parseFromString(description, 'text/html').body.textContent.trim()
|
|
82
|
+
: description; // SSR safety fallback
|
|
83
|
+
cleaned.description = text;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return deepNormalizeForComparison(cleaned);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// ***** main *****
|
|
91
|
+
const computeEditedCustomers = (initialFormData, allCustomers) => {
|
|
92
|
+
const edited = new Set();
|
|
93
|
+
|
|
94
|
+
for (const init of (initialFormData.customers || [])) {
|
|
95
|
+
const curr = (allCustomers || []).find(c => c.id === init.id);
|
|
96
|
+
if (!curr) {
|
|
97
|
+
// removed entirely => top-level change; not a per-customer edit here
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// first, check HTML semantic equality for description (cheap early exit)
|
|
102
|
+
const initDesc = stripUIOnlyKeys(init).description;
|
|
103
|
+
const currDesc = stripUIOnlyKeys(curr).description;
|
|
104
|
+
if (!isSameHtml(initDesc, currDesc)) {
|
|
105
|
+
edited.add(init.id);
|
|
106
|
+
continue; // already edited; no need to do deeper compare
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// merge current over initial so missing fields fall back
|
|
110
|
+
const merged = deepMergeWithFallback(init, curr);
|
|
111
|
+
|
|
112
|
+
// normalize both sides the same way (strip UI keys, clean details/description, normalize)
|
|
113
|
+
const normInit = normalizeCustomerForComparison(init);
|
|
114
|
+
const normCurr = normalizeCustomerForComparison(merged);
|
|
115
|
+
|
|
116
|
+
if (JSON.stringify(normInit) !== JSON.stringify(normCurr)) {
|
|
117
|
+
edited.add(init.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return edited;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default computeEditedCustomers;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const convertUTCISOToLocalePrettyTime = (isoString) => {
|
|
4
|
+
const date = new Date(isoString);
|
|
5
|
+
if (isNaN(date)) {
|
|
6
|
+
return `Invalid ISO string`
|
|
7
|
+
}
|
|
8
|
+
return date.toLocaleTimeString('en-US', {
|
|
9
|
+
hour: 'numeric',
|
|
10
|
+
minute: '2-digit',
|
|
11
|
+
hour12: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default convertUTCISOToLocalePrettyTime;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
const convertUTCISOToPrettyDate = (dateStr) => {
|
|
3
|
+
if (!dateStr || typeof dateStr !== 'string') return '';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const date = new Date(dateStr);
|
|
7
|
+
if (isNaN(date.getTime())) return '';
|
|
8
|
+
|
|
9
|
+
return date.toLocaleDateString(undefined, {
|
|
10
|
+
year: 'numeric',
|
|
11
|
+
month: '2-digit',
|
|
12
|
+
day: '2-digit'
|
|
13
|
+
});
|
|
14
|
+
} catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default convertUTCISOToPrettyDate;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
const decodeParamsToForm = (obj) => ({
|
|
3
|
+
searchType: obj.searchType || 'keyword',
|
|
4
|
+
term: obj.term || '',
|
|
5
|
+
locationValue: obj.locationValue ?
|
|
6
|
+
obj.locationValue.split(',') : [],
|
|
7
|
+
incidentTypeId: obj.incidentTypeId ?
|
|
8
|
+
obj.incidentTypeId.split(',') : [],
|
|
9
|
+
witnessedBy: obj.witnessedBy || '',
|
|
10
|
+
createdBy: obj.createdBy || '',
|
|
11
|
+
startDate: obj.startDate || '',
|
|
12
|
+
endDate: obj.endDate || '',
|
|
13
|
+
currentTrespass: obj.currentTrespass === 'true',
|
|
14
|
+
expiredTrespass: obj.expiredTrespass === 'true',
|
|
15
|
+
staffSuppress: obj.staffSuppress || 'non',
|
|
16
|
+
sort: obj.sort || '',
|
|
17
|
+
dir: obj.dir || ''
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default decodeParamsToForm;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const deepNormalizeForComparison = (obj) => {
|
|
4
|
+
const DATE_KEYS = new Set(['dateOfOccurrence', 'endDateOfTrespass', 'date']);
|
|
5
|
+
|
|
6
|
+
const toLocalYMD = (d) => {
|
|
7
|
+
if (!d || typeof d !== 'string') return d;
|
|
8
|
+
const md = d.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
9
|
+
if (md) {
|
|
10
|
+
const [ , mm, dd, yyyy ] = md;
|
|
11
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
12
|
+
}
|
|
13
|
+
const iso = d.match(/^\d{4}-\d{2}-\d{2}T/);
|
|
14
|
+
if (iso) {
|
|
15
|
+
const date = new Date(d);
|
|
16
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
17
|
+
}
|
|
18
|
+
return d.trim();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(obj)) {
|
|
22
|
+
return obj.map(deepNormalizeForComparison);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (obj && typeof obj === 'object') {
|
|
26
|
+
return Object.entries(obj)
|
|
27
|
+
.filter(([_, v]) => v !== undefined && v !== null && v !== '')
|
|
28
|
+
.sort(([a], [b]) => a.localeCompare(b)) // sort object keys
|
|
29
|
+
.reduce((acc, [k, v]) => {
|
|
30
|
+
acc[k] = typeof v === 'string'
|
|
31
|
+
? (DATE_KEYS.has(k) ? toLocalYMD(v.trim()) : v.trim())
|
|
32
|
+
: deepNormalizeForComparison(v);
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
}
|
|
36
|
+
return obj;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default deepNormalizeForComparison;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
import buildQueryString from "./buildQueryString";
|
|
3
|
+
|
|
4
|
+
const extractFilterString = (params = {}) => {
|
|
5
|
+
// return valid URL if no params
|
|
6
|
+
if (Object.keys(params).length === 0) return "";
|
|
7
|
+
|
|
8
|
+
const { limit, offset, ...filters } = params;
|
|
9
|
+
return buildQueryString(filters);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default extractFilterString;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- concatenate 'mm/dd/yyyy' string and 'HH:MM AM/PM' string and format
|
|
3
|
+
to UTC ISO format
|
|
4
|
+
@param {string} dateString - string set by DatePicker component
|
|
5
|
+
@param {string} timeString - string set by TimePicker component
|
|
6
|
+
@returns {string} ISO formatted date string, or null if input is not present
|
|
7
|
+
*/
|
|
8
|
+
const formatDateAndTimeToUTCISO = (dateString, timeString) => {
|
|
9
|
+
if (!dateString || !timeString) return null;
|
|
10
|
+
const utcDateTime = new Date(`${dateString} ${timeString}`).toISOString();
|
|
11
|
+
return utcDateTime
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default formatDateAndTimeToUTCISO;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- format 'mm/dd/yyyy' (local date) string to UTC ISO format date/time at start of day, with optional add offset in seconds
|
|
3
|
+
@param {string} dateString - string set by DatePicker component
|
|
4
|
+
@param {number} offsetInSeconds - Optional - defaults to 0
|
|
5
|
+
@returns {string|null} ISO formatted date string as local machine date, or null if input is not present (EX: If in Pacific Time and dateString = '01/02/2025', the return is '2025-01-02T08:00:00.000Z' or with optional offsetInSeconds ('01/02/2025', 1) the return is '2025-01-02T08:00:01.000Z')
|
|
6
|
+
*/
|
|
7
|
+
const formatDateToUTCISO = (dateString, offsetInSeconds = 0) => {
|
|
8
|
+
if (!dateString) return null;
|
|
9
|
+
const dateObj = new Date(dateString); // interprets as midnight in local tz
|
|
10
|
+
dateObj.setHours(0, 0, offsetInSeconds, 0); // force local time to midnight, offset seconds
|
|
11
|
+
return dateObj.toISOString(); // convert internal epoch time to UTC ISO
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default formatDateToUTCISO;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- format timeString to to UTC ISO w/ associated 'dateOfIncident'
|
|
3
|
+
as date value the string
|
|
4
|
+
@param {string} dateOfIncidentString - user inputted value from Datepicker
|
|
5
|
+
component fed via formData
|
|
6
|
+
(Ex: "10/30/2024")
|
|
7
|
+
@param {string} timeString - user inputted value from Timepicker component
|
|
8
|
+
(Ex: "9:33 AM")
|
|
9
|
+
@returns {string} UTC ISO formatted datetime string, or null if input is not present
|
|
10
|
+
(Ex: '2024-10-16T17:27:00.000Z')
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const formatTimeToUTCISO = (dateOfIncidentString, timeString) => {
|
|
14
|
+
if (!dateOfIncidentString || !timeString) {
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// check if timeString is already in ISO
|
|
19
|
+
if (!isNaN(Date.parse(timeString))) {
|
|
20
|
+
console.log("timeString: ", timeString)
|
|
21
|
+
return timeString;
|
|
22
|
+
}
|
|
23
|
+
const utcDateTime = new Date(`${dateOfIncidentString} ${timeString}`).toISOString();
|
|
24
|
+
// console.log("utcDateTime: ", utcDateTime)
|
|
25
|
+
return utcDateTime;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default formatTimeToUTCISO;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- @returns {string} 'hh:mm AM/PM' to populate in TimePicker component on initial render
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const getCurrentTime = () => {
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const hours = now.getHours();
|
|
8
|
+
const minutes = now.getMinutes();
|
|
9
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
10
|
+
const formattedHours = hours % 12 || 12; // 12-hour format
|
|
11
|
+
// console.log(`hours before format: ${hours} - formattedHours: ${formattedHours}`)
|
|
12
|
+
const formattedMinutes = minutes.toString().padStart(2, '0');
|
|
13
|
+
const currentTime = `${formattedHours}:${formattedMinutes} ${ampm}`
|
|
14
|
+
// return `${formattedHours}:${formattedMinutes} ${ampm}`
|
|
15
|
+
// console.log("@getCurrentTime - currentTime: ", currentTime)
|
|
16
|
+
return currentTime;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// console.log(getCurrentTime());
|
|
20
|
+
export default getCurrentTime;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- @returns {string} 'mm/dd/yyyy' to populate in DatePicker component on initial render
|
|
3
|
+
*/
|
|
4
|
+
const getTodayDate = () => {
|
|
5
|
+
const today = new Date();
|
|
6
|
+
const day = String(today.getDate()).padStart(2, '0');
|
|
7
|
+
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const year = today.getFullYear();
|
|
9
|
+
return `${month}/${day}/${year}`;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default getTodayDate;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { decode } from 'html-entities';
|
|
3
|
+
import { FormattedMessage } from 'react-intl';
|
|
4
|
+
import Handlebars from 'handlebars';
|
|
5
|
+
import convertUTCISOToPrettyDate from './convertUTCISOToPrettyDate';
|
|
6
|
+
|
|
7
|
+
export function registerHandlebarsHelpers({
|
|
8
|
+
locationDataOptions,
|
|
9
|
+
trespassReasons,
|
|
10
|
+
self,
|
|
11
|
+
triggerDocumentError
|
|
12
|
+
}) {
|
|
13
|
+
|
|
14
|
+
const reasonById = new Map((trespassReasons ?? []).map(tr => [tr.id, tr.reason]));
|
|
15
|
+
if (!Handlebars.helpers.resolveTrespassReasons) {
|
|
16
|
+
// no 'path', Handlebars handles scope on this one
|
|
17
|
+
// token -> {{resolveTrespassReasons customer.trespass.exclusionOrTrespassBasedOn}}
|
|
18
|
+
Handlebars.registerHelper('resolveTrespassReasons', function(list) {
|
|
19
|
+
try {
|
|
20
|
+
if (!Array.isArray(list) || list.length === 0) return '';
|
|
21
|
+
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const reasons = [];
|
|
24
|
+
|
|
25
|
+
for (const entry of list) {
|
|
26
|
+
// handle for prev set up / strays after migration - entry is a string id
|
|
27
|
+
if (typeof entry === 'string') {
|
|
28
|
+
const r = reasonById.get(entry);
|
|
29
|
+
if (r && !seen.has(r)) { reasons.push(r); seen.add(r); }
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// current - entry is an object
|
|
34
|
+
if (entry && typeof entry === 'object') {
|
|
35
|
+
const id = entry.id ?? entry.reasonId ?? entry.value;
|
|
36
|
+
const r = entry.reason ?? (id ? reasonById.get(id) : undefined);
|
|
37
|
+
if (r && !seen.has(r)) { reasons.push(r); seen.add(r); }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return reasons.join(', ');
|
|
42
|
+
|
|
43
|
+
} catch (error) {
|
|
44
|
+
triggerDocumentError?.(<FormattedMessage id="generate-trespass.error-doc-incTypes" values={{ error: error.message }} />);
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!Handlebars.helpers.resolveIncTypes) {
|
|
50
|
+
// token -> {{resolveIncTypes "incident.incidentTypes.title"}}
|
|
51
|
+
Handlebars.registerHelper('resolveIncTypes', function(path) {
|
|
52
|
+
try {
|
|
53
|
+
const keys = path.split('.');
|
|
54
|
+
const propertyKey = keys.pop();
|
|
55
|
+
let arrayValue = this;
|
|
56
|
+
for (const key of keys) arrayValue = arrayValue?.[key];
|
|
57
|
+
if (!Array.isArray(arrayValue)) return '';
|
|
58
|
+
return arrayValue
|
|
59
|
+
.map(item => item?.[propertyKey] || '')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(', ');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
triggerDocumentError?.(<FormattedMessage id="generate-trespass.error-doc-incTypes" values={{ error: error.message }} />);
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Handlebars.helpers.formatLocation) {
|
|
70
|
+
// tokens ->
|
|
71
|
+
// {{formatLocation incident.incidentLocation}}
|
|
72
|
+
// {{formatLocation customer.trespass.declarationOfService.placeSigned}}
|
|
73
|
+
Handlebars.registerHelper('formatLocation', function(locationIdString) {
|
|
74
|
+
try {
|
|
75
|
+
const matched = locationDataOptions.find(loc => loc.value === locationIdString);
|
|
76
|
+
return matched?.label || locationIdString || '';
|
|
77
|
+
} catch (error) {
|
|
78
|
+
triggerDocumentError?.(<FormattedMessage id="generate-trespass.error-doc-locationIdString" values={{ error: error.message }} />);
|
|
79
|
+
return locationIdString || '';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!Handlebars.helpers.formatDate) {
|
|
85
|
+
// tokens ->
|
|
86
|
+
// {{formatDate (resolve "customer.trespass.dateOfOccurrence")}}
|
|
87
|
+
// {{formatDate (resolve "customer.trespass.declarationOfService.date")}}
|
|
88
|
+
// {{formatDate (resolve "customer.trespass.endDateOfTrespass")}}
|
|
89
|
+
Handlebars.registerHelper('formatDate', function(dateString) {
|
|
90
|
+
try {
|
|
91
|
+
return convertUTCISOToPrettyDate(dateString);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
triggerDocumentError(<FormattedMessage id="generate-trespass.error-doc-formatDate" values={{ dateString, error: error.message }} />);
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!Handlebars.helpers.resolve) {
|
|
100
|
+
// runs on customer keys and nested keys
|
|
101
|
+
Handlebars.registerHelper('resolve', function(path, options) {
|
|
102
|
+
try {
|
|
103
|
+
const keys = path.split('.');
|
|
104
|
+
let value = this;
|
|
105
|
+
for (const key of keys) value = value?.[key];
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
const prop = options.hash?.property;
|
|
108
|
+
return prop
|
|
109
|
+
? value.map(item => item?.[prop] || '').join(', ')
|
|
110
|
+
: value.map(item => JSON.stringify(item)).join(', ');
|
|
111
|
+
}
|
|
112
|
+
return value ?? '';
|
|
113
|
+
} catch (error) {
|
|
114
|
+
triggerDocumentError(<FormattedMessage id="generate-trespass.error-doc-resolve" values={{ path, error: error.message }} />);
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!Handlebars.helpers.trespassDescriptionPlain) {
|
|
121
|
+
// token -> {{{trespassDescriptionPlain}}}
|
|
122
|
+
Handlebars.registerHelper('trespassDescriptionPlain', function () {
|
|
123
|
+
try {
|
|
124
|
+
const html = this?.customer?.trespass?.description?.trim() || this?.customer?.trespass?.descriptionOfOccurrence?.trim() || '';
|
|
125
|
+
|
|
126
|
+
// preserve HTML blocks
|
|
127
|
+
const raw = decode(DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }));
|
|
128
|
+
// return raw;
|
|
129
|
+
return new Handlebars.SafeString(raw);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
triggerDocumentError(<FormattedMessage id="generate-trespass.error-doc-descriptionPlainFallback" values={{ error: error.message }} />);
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Handlebars.helpers.declarationOfServiceIssuedBy) {
|
|
138
|
+
// token -> {{declarationOfServiceIssuedBy}} (is the logged in FOLIO user 'self')
|
|
139
|
+
Handlebars.registerHelper('declarationOfServiceIssuedBy', function () {
|
|
140
|
+
try {
|
|
141
|
+
return `${self?.lastName || ''}, ${self?.firstName || ''}` || 'User not found';
|
|
142
|
+
} catch (error) {
|
|
143
|
+
triggerDocumentError(<FormattedMessage id="generate-trespass.error-doc-declarationOfServiceIssuedBy" values={{ error: error.message }} />);
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
const hasFormChangedAtCreate = ({
|
|
3
|
+
formData,
|
|
4
|
+
initialFormData,
|
|
5
|
+
selectedCustomers,
|
|
6
|
+
selectedWitnesses,
|
|
7
|
+
editorWasTouched,
|
|
8
|
+
}) => {
|
|
9
|
+
// ignore these keys as they are populated on render
|
|
10
|
+
const keysToIgnore = ['dateOfIncident', 'timeOfIncident', 'id'];
|
|
11
|
+
|
|
12
|
+
if (selectedCustomers.length > 0) return true;
|
|
13
|
+
if (selectedWitnesses.length > 0) return true;
|
|
14
|
+
if (editorWasTouched) {
|
|
15
|
+
// we check for was touched instead of formData was mutated b/c of our onBlur pattern to mitigate
|
|
16
|
+
// react-quill's cleanup <-> setState feedback loop.
|
|
17
|
+
// i.e. user inputs to Editor, doesn't make onBlur and clicks directly to 'cancel' the form
|
|
18
|
+
// would not be considered 'dirty' as no update to formData. This editorWasTouch ref handles that case.
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const key in formData) {
|
|
23
|
+
if (keysToIgnore.includes(key)) continue;
|
|
24
|
+
|
|
25
|
+
const original = initialFormData[key] ?? null;
|
|
26
|
+
const current = formData[key] ?? null;
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(original) && Array.isArray(current)) {
|
|
29
|
+
if (original.length !== current.length) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (JSON.stringify(original) !== JSON.stringify(current)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
} else if (typeof original === 'string' || typeof current === 'string') {
|
|
37
|
+
if ((original || '').trim() !== (current || '').trim()) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
if (JSON.stringify(original) !== JSON.stringify(current)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default hasFormChangedAtCreate;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
import { isSameHtml } from './isSameHtml.js';
|
|
3
|
+
import deepNormalizeForComparison from './deepNormalizeForComparison.js';
|
|
4
|
+
|
|
5
|
+
// intentionally does not consider:
|
|
6
|
+
// selectedCustomers, customers add/remove, staffSuppressed,
|
|
7
|
+
// isApproximateTime, media add/remove -> these should not force global 'Update declaration'
|
|
8
|
+
const hasTopLevelChangeAffectedDeclaration = (
|
|
9
|
+
initial,
|
|
10
|
+
current,
|
|
11
|
+
selectedWitnesses
|
|
12
|
+
) => {
|
|
13
|
+
const simpleKeys = [
|
|
14
|
+
'customerNa',
|
|
15
|
+
'incidentLocation',
|
|
16
|
+
'subLocation',
|
|
17
|
+
'dateTimeOfIncident',
|
|
18
|
+
'timeOfIncident'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// return true if any changes to simple keys
|
|
22
|
+
for (const key of simpleKeys) {
|
|
23
|
+
if (current[key] !== initial[key]) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// return true if any changes to top-level detailedDescriptionOfIncident
|
|
29
|
+
if (!isSameHtml(current.detailedDescriptionOfIncident, initial.detailedDescriptionOfIncident)) {
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// return true if any new witnesses selected for saving
|
|
34
|
+
if (selectedWitnesses.length > 0) {
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getSortedIds = arr => (arr || []).map(i => i.id).sort();
|
|
39
|
+
|
|
40
|
+
// return true if relevant item is removed in UI
|
|
41
|
+
if (
|
|
42
|
+
JSON.stringify(getSortedIds(current.incidentTypes)) !== JSON.stringify(getSortedIds(initial.incidentTypes)) ||
|
|
43
|
+
JSON.stringify(getSortedIds(current.incidentWitnesses)) !== JSON.stringify(getSortedIds(initial.incidentWitnesses)) ||
|
|
44
|
+
JSON.stringify(getSortedIds(current.attachments)) !== JSON.stringify(getSortedIds(initial.attachments))
|
|
45
|
+
) {
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const normalizedWitness = (wit) => {
|
|
50
|
+
const {
|
|
51
|
+
id,
|
|
52
|
+
isCustom,
|
|
53
|
+
firstName = '',
|
|
54
|
+
lastName = '',
|
|
55
|
+
role = '',
|
|
56
|
+
phone = '',
|
|
57
|
+
email = '',
|
|
58
|
+
} = wit;
|
|
59
|
+
|
|
60
|
+
return deepNormalizeForComparison({
|
|
61
|
+
id,
|
|
62
|
+
isCustom,
|
|
63
|
+
firstName: firstName.trim(),
|
|
64
|
+
lastName: lastName.trim(),
|
|
65
|
+
...(role.trim() && { role: role.trim() }),
|
|
66
|
+
...(phone.trim() && { phone: phone.trim() }),
|
|
67
|
+
...(email.trim() && { email: email.trim() }),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const initialCustomWitnesses = (initial.incidentWitnesses || []).filter(w => w.isCustom);
|
|
72
|
+
const currentCustomWitnesses = (current.incidentWitnesses || []).filter(w => w.isCustom);
|
|
73
|
+
|
|
74
|
+
// check for any modified custom witness
|
|
75
|
+
for (const initWit of initialCustomWitnesses) {
|
|
76
|
+
const currWit = currentCustomWitnesses.find(w => w.id === initWit.id);
|
|
77
|
+
if (!currWit) return true;
|
|
78
|
+
|
|
79
|
+
const normInit = normalizedWitness(initWit);
|
|
80
|
+
const normCurr = normalizedWitness(currWit);
|
|
81
|
+
|
|
82
|
+
if (JSON.stringify(normInit) !== JSON.stringify(normCurr)) {
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return false; // no changes
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default hasTopLevelChangeAffectedDeclaration;
|