@truedat/core 8.13.4 → 8.13.6
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/package.json +3 -3
- package/src/components/Alert.js +12 -8
- package/src/components/GlossaryMenu.js +4 -4
- package/src/components/TemplateSelector.js +29 -3
- package/src/components/UploadJobDetails.js +1 -1
- package/src/components/UploadJobParser.js +109 -13
- package/src/components/UploadJobScope.js +9 -0
- package/src/components/UploadJobSummary.js +2 -1
- package/src/components/UploadJobs.js +1 -0
- package/src/components/__tests__/AlertDateRestrictionError.spec.js +87 -0
- package/src/components/__tests__/TemplateSelector.spec.js +19 -2
- package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +10 -0
- package/src/components/__tests__/UploadJobParser.spec.js +302 -0
- package/src/components/__tests__/UploadJobScope.spec.js +17 -0
- package/src/components/__tests__/UploadJobSummary.spec.js +27 -0
- package/src/components/__tests__/UploadJobs.spec.js +15 -0
- package/src/components/__tests__/__snapshots__/GlossaryMenu.spec.js.snap +1 -1
- package/src/components/__tests__/__snapshots__/SideMenu.spec.js.snap +1 -1
- package/src/components/__tests__/__snapshots__/UploadJobBreadcrumbs.spec.js.snap +16 -0
- package/src/components/index.js +2 -1
- package/src/routes.js +4 -2
- package/src/services/__tests__/messageObjectErrors.spec.js +77 -0
- package/src/services/message.js +26 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "8.13.
|
|
3
|
+
"version": "8.13.6",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@testing-library/jest-dom": "^6.6.3",
|
|
58
58
|
"@testing-library/react": "^16.3.0",
|
|
59
59
|
"@testing-library/user-event": "^14.6.1",
|
|
60
|
-
"@truedat/test": "8.13.
|
|
60
|
+
"@truedat/test": "8.13.6",
|
|
61
61
|
"identity-obj-proxy": "^3.0.0",
|
|
62
62
|
"jest": "^29.7.0",
|
|
63
63
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"swr": "^2.3.3",
|
|
100
100
|
"turndown": "^7.2.2"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "c0943c8cf31e82a81fdca24d68ef57406fe5bcbc"
|
|
103
103
|
}
|
package/src/components/Alert.js
CHANGED
|
@@ -11,10 +11,14 @@ import { AlertExporter } from "./AlertExporter";
|
|
|
11
11
|
const existingList = (list) => list && !_.isEmpty(list);
|
|
12
12
|
|
|
13
13
|
const safeFormat = (formatMessage) => (descriptor, values) => {
|
|
14
|
+
const fallback = descriptor.defaultMessage ?? descriptor.id;
|
|
15
|
+
|
|
14
16
|
try {
|
|
15
|
-
|
|
17
|
+
const formatted = formatMessage(descriptor, values);
|
|
18
|
+
|
|
19
|
+
return formatted === descriptor.id ? fallback : formatted;
|
|
16
20
|
} catch {
|
|
17
|
-
return
|
|
21
|
+
return fallback;
|
|
18
22
|
}
|
|
19
23
|
};
|
|
20
24
|
|
|
@@ -34,14 +38,14 @@ const formatErrorMessages = (error, formatMessage) => {
|
|
|
34
38
|
});
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
if (_.
|
|
38
|
-
|
|
39
|
-
{ id: error.name
|
|
40
|
-
{ text: error.code },
|
|
41
|
+
if (_.has("name")(error)) {
|
|
42
|
+
const message = safe(
|
|
43
|
+
{ id: error.name },
|
|
44
|
+
{ text: error.code, field: error.field, ...(error.values || {}) },
|
|
41
45
|
);
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
return error.field ? `${error.field}: ${message}` : message;
|
|
48
|
+
}
|
|
45
49
|
|
|
46
50
|
return formatMessage({
|
|
47
51
|
id: "error.unknown",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
CONCEPTS_DEPRECATED,
|
|
16
16
|
CONCEPT_LINKS_MANAGEMENT,
|
|
17
17
|
CONCEPT_LINKS_APPROVALS,
|
|
18
|
-
|
|
18
|
+
CONCEPTS_UPLOAD_JOBS,
|
|
19
19
|
CONCEPTS_SUBSCOPE,
|
|
20
20
|
CONCEPTS_SIDEMENU_SUBSCOPE,
|
|
21
21
|
CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
|
|
@@ -101,9 +101,9 @@ function buildMainConceptsSubmenuItems(
|
|
|
101
101
|
};
|
|
102
102
|
const manageUploads = {
|
|
103
103
|
name: "concepts_upload_events",
|
|
104
|
-
routes: [
|
|
104
|
+
routes: [CONCEPTS_UPLOAD_JOBS],
|
|
105
105
|
groups: ["business_glossary_management"],
|
|
106
|
-
isActive: () => pathname ===
|
|
106
|
+
isActive: () => pathname === CONCEPTS_UPLOAD_JOBS,
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
return [
|
|
@@ -171,7 +171,7 @@ function buildManagementSubmenuItems() {
|
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
name: "concepts_upload_events",
|
|
174
|
-
routes: [
|
|
174
|
+
routes: [CONCEPTS_UPLOAD_JOBS],
|
|
175
175
|
groups: ["business_glossary_management"],
|
|
176
176
|
},
|
|
177
177
|
];
|
|
@@ -2,7 +2,7 @@ import _ from "lodash/fp";
|
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
3
|
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
|
-
import { Form, Label } from "semantic-ui-react";
|
|
5
|
+
import { Button, Form, Label, Message } from "semantic-ui-react";
|
|
6
6
|
import { useIntl, FormattedMessage } from "react-intl";
|
|
7
7
|
import { useQuery } from "@apollo/client";
|
|
8
8
|
import { accentInsensitivePathOrder } from "../services/sort";
|
|
@@ -112,13 +112,37 @@ TemplateSelector.propTypes = {
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
const emptyTemplates = [];
|
|
115
|
+
|
|
116
|
+
export const TemplateLoadError = ({ onRetry }) => (
|
|
117
|
+
<Message negative compact>
|
|
118
|
+
<Message.Content>
|
|
119
|
+
<FormattedMessage
|
|
120
|
+
id="template.selector.load_error"
|
|
121
|
+
defaultMessage="Templates could not be loaded. Check your access and try again."
|
|
122
|
+
/>
|
|
123
|
+
{onRetry ? (
|
|
124
|
+
<Button size="small" onClick={onRetry}>
|
|
125
|
+
<FormattedMessage
|
|
126
|
+
id="template.selector.retry"
|
|
127
|
+
defaultMessage="Retry"
|
|
128
|
+
/>
|
|
129
|
+
</Button>
|
|
130
|
+
) : null}
|
|
131
|
+
</Message.Content>
|
|
132
|
+
</Message>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
TemplateLoadError.propTypes = {
|
|
136
|
+
onRetry: PropTypes.func,
|
|
137
|
+
};
|
|
138
|
+
|
|
115
139
|
export const TemplateSelectorLoader = ({
|
|
116
140
|
scope,
|
|
117
141
|
onLoad,
|
|
118
142
|
domainIds,
|
|
119
143
|
...props
|
|
120
144
|
}) => {
|
|
121
|
-
const { loading, error, data } = useQuery(TEMPLATES_QUERY, {
|
|
145
|
+
const { loading, error, data, refetch } = useQuery(TEMPLATES_QUERY, {
|
|
122
146
|
fetchPolicy: "cache-and-network",
|
|
123
147
|
variables: { scope, domainIds },
|
|
124
148
|
});
|
|
@@ -131,7 +155,9 @@ export const TemplateSelectorLoader = ({
|
|
|
131
155
|
}
|
|
132
156
|
}, [data, onLoad]);
|
|
133
157
|
|
|
134
|
-
if (error)
|
|
158
|
+
if (error && !loading) {
|
|
159
|
+
return <TemplateLoadError onRetry={() => refetch()} />;
|
|
160
|
+
}
|
|
135
161
|
|
|
136
162
|
const templates = data?.templates || emptyTemplates;
|
|
137
163
|
return (
|
|
@@ -168,7 +168,7 @@ export const ChangesDetail = ({ changes, changeDetails, scope }) => {
|
|
|
168
168
|
const detailsId = useId();
|
|
169
169
|
const details = changeDetails ?? changes;
|
|
170
170
|
|
|
171
|
-
if (!details) return null;
|
|
171
|
+
if (!details || _.isEmpty(details)) return null;
|
|
172
172
|
|
|
173
173
|
const changesList = flattenChanges(details);
|
|
174
174
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
|
+
import { Link } from "react-router";
|
|
3
4
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
5
|
import Label from "semantic-ui-react/dist/commonjs/elements/Label";
|
|
5
6
|
import RuleImplementationLink from "@truedat/dq/components/RuleImplementationLink";
|
|
6
7
|
import StructureNoteLink from "@truedat/dd/components/StructureNoteLink";
|
|
8
|
+
import { linkTo } from "../routes";
|
|
7
9
|
import { ChangesDetail, ErrorDetailsDisclosure } from "./UploadJobDetails";
|
|
8
10
|
import { classifyUploadJobErrorMessage } from "./UploadJobSummary";
|
|
9
11
|
|
|
@@ -45,7 +47,13 @@ export const ResponseCell = ({
|
|
|
45
47
|
);
|
|
46
48
|
}
|
|
47
49
|
case "ERROR":
|
|
48
|
-
return
|
|
50
|
+
return (
|
|
51
|
+
<ErrorDetail
|
|
52
|
+
response={response}
|
|
53
|
+
showOrigin={showOrigin}
|
|
54
|
+
scope={scope}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
49
57
|
case "COMPLETED":
|
|
50
58
|
return <CompletedDetail response={response} />;
|
|
51
59
|
case "INFO":
|
|
@@ -69,6 +77,19 @@ export const ResponseCell = ({
|
|
|
69
77
|
}
|
|
70
78
|
};
|
|
71
79
|
|
|
80
|
+
const ConceptLink = ({ business_concept_id, id, name }) => (
|
|
81
|
+
<Link to={linkTo.CONCEPT_VERSION({ business_concept_id, id })}>{name}</Link>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
ConceptLink.propTypes = {
|
|
85
|
+
business_concept_id: PropTypes.oneOfType([
|
|
86
|
+
PropTypes.number,
|
|
87
|
+
PropTypes.string,
|
|
88
|
+
]),
|
|
89
|
+
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
90
|
+
name: PropTypes.string,
|
|
91
|
+
};
|
|
92
|
+
|
|
72
93
|
export const ResultEntityCell = ({ response, status }) => {
|
|
73
94
|
if (!response?.details || !["INFO", "ERROR"].includes(status)) return null;
|
|
74
95
|
|
|
@@ -78,6 +99,8 @@ export const ResultEntityCell = ({ response, status }) => {
|
|
|
78
99
|
|
|
79
100
|
return response.details.implementation_key != null ? (
|
|
80
101
|
<RuleImplementationLink {...response.details} />
|
|
102
|
+
) : response.details.business_concept_id != null ? (
|
|
103
|
+
<ConceptLink {...response.details} />
|
|
81
104
|
) : status === "INFO" &&
|
|
82
105
|
(response.details.data_structure_id != null ||
|
|
83
106
|
response.details.external_id != null) ? (
|
|
@@ -113,6 +136,11 @@ ResultEntityCell.propTypes = {
|
|
|
113
136
|
PropTypes.number,
|
|
114
137
|
PropTypes.string,
|
|
115
138
|
]),
|
|
139
|
+
business_concept_id: PropTypes.oneOfType([
|
|
140
|
+
PropTypes.number,
|
|
141
|
+
PropTypes.string,
|
|
142
|
+
]),
|
|
143
|
+
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
116
144
|
name: PropTypes.string,
|
|
117
145
|
}),
|
|
118
146
|
}),
|
|
@@ -187,7 +215,7 @@ const externalIdDetailValue = (details) =>
|
|
|
187
215
|
(details?.external_id ?? "")
|
|
188
216
|
);
|
|
189
217
|
|
|
190
|
-
const ErrorDetail = ({ response, showOrigin }) => {
|
|
218
|
+
const ErrorDetail = ({ response, showOrigin, scope }) => {
|
|
191
219
|
const { formatMessage } = useIntl();
|
|
192
220
|
|
|
193
221
|
if (!response || !response.type) {
|
|
@@ -196,6 +224,31 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
196
224
|
const { type } = response;
|
|
197
225
|
const messagePrefix = "uploadJob.parser.detail";
|
|
198
226
|
|
|
227
|
+
const translatedFieldError = (error, details) => {
|
|
228
|
+
const rawMessageId = error?.name || error?.message;
|
|
229
|
+
const resourcePrefix =
|
|
230
|
+
scope === "concepts"
|
|
231
|
+
? "concept.error.content."
|
|
232
|
+
: details?.implementation_key
|
|
233
|
+
? "implementation.error."
|
|
234
|
+
: details?.rule_name
|
|
235
|
+
? "rule.error."
|
|
236
|
+
: details?.external_id
|
|
237
|
+
? "structureNote.error."
|
|
238
|
+
: "";
|
|
239
|
+
const messageId = error?.name
|
|
240
|
+
? rawMessageId.startsWith(resourcePrefix) || !resourcePrefix
|
|
241
|
+
? rawMessageId
|
|
242
|
+
: `${resourcePrefix}${rawMessageId}`
|
|
243
|
+
: rawMessageId;
|
|
244
|
+
if (!messageId) return "";
|
|
245
|
+
|
|
246
|
+
return formatMessage(
|
|
247
|
+
{ id: messageId, defaultMessage: error?.message || messageId },
|
|
248
|
+
{ field: error?.field, ...(error?.values || {}) },
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
199
252
|
if (!showOrigin && type === "data_structure_not_found") {
|
|
200
253
|
return (
|
|
201
254
|
<span className="upload-job-simple-error">
|
|
@@ -204,6 +257,51 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
204
257
|
);
|
|
205
258
|
}
|
|
206
259
|
|
|
260
|
+
if (!showOrigin && type === "name_not_available") {
|
|
261
|
+
const name = response.details?.name;
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<span className="upload-job-simple-error">
|
|
265
|
+
<FormattedMessage id="uploadJob.parser.error.name_not_available" />
|
|
266
|
+
{name != null ? `: ${name}` : null}
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!showOrigin && type === "unexpected_error") {
|
|
272
|
+
const { type: errorType } = classifyUploadJobErrorMessage(
|
|
273
|
+
response.details?.message,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (errorType === "unexpected_error") {
|
|
277
|
+
return (
|
|
278
|
+
<span className="upload-job-simple-error">
|
|
279
|
+
<FormattedMessage id="uploadJob.parser.error.unexpected_error" />
|
|
280
|
+
</span>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const validationErrorPairs = (errors, details) => {
|
|
286
|
+
if (_.isArray(errors)) {
|
|
287
|
+
return errors.map((e) => [
|
|
288
|
+
e?.field ?? formatMessage({ id: `${messagePrefix}.message` }),
|
|
289
|
+
translatedFieldError(e, details),
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (_.isObject(errors)) {
|
|
294
|
+
return Object.entries(errors).flatMap(([field, messages]) =>
|
|
295
|
+
(_.isArray(messages) ? messages : [messages]).map((message) => [
|
|
296
|
+
field,
|
|
297
|
+
message ?? "",
|
|
298
|
+
]),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null;
|
|
303
|
+
};
|
|
304
|
+
|
|
207
305
|
const detailBuilders = {
|
|
208
306
|
missing_required_headers: (details) => [
|
|
209
307
|
[
|
|
@@ -276,10 +374,9 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
276
374
|
[null, externalIdDetailValue(details)],
|
|
277
375
|
].filter(([, v]) => v !== undefined && v !== ""),
|
|
278
376
|
field_validation_error: (details) =>
|
|
279
|
-
details?.errors
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
]) ?? [[null, externalIdDetailValue(details)]],
|
|
377
|
+
validationErrorPairs(details?.errors, details) ?? [
|
|
378
|
+
[null, externalIdDetailValue(details)],
|
|
379
|
+
],
|
|
283
380
|
unprocessable_entity: (details) => [[null, externalIdDetailValue(details)]],
|
|
284
381
|
structure_note_creation_error: (details) => {
|
|
285
382
|
const reason = details?.reason ?? details?.error;
|
|
@@ -289,12 +386,10 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
289
386
|
? `structures.uploadStructures.failed.note.${reason}`
|
|
290
387
|
: null;
|
|
291
388
|
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
])
|
|
297
|
-
: [
|
|
389
|
+
const validationPairs = validationErrorPairs(details?.errors, details);
|
|
390
|
+
|
|
391
|
+
const errorPairs = _.isEmpty(validationPairs)
|
|
392
|
+
? [
|
|
298
393
|
[
|
|
299
394
|
formatMessage({ id: `${messagePrefix}.reason` }),
|
|
300
395
|
reasonMessageId
|
|
@@ -304,7 +399,8 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
304
399
|
})
|
|
305
400
|
: String(reason ?? ""),
|
|
306
401
|
],
|
|
307
|
-
]
|
|
402
|
+
]
|
|
403
|
+
: validationPairs;
|
|
308
404
|
|
|
309
405
|
const externalIdPair = details?.external_id
|
|
310
406
|
? [[null, externalIdDetailValue(details)]]
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CONCEPTS_UPLOAD_JOBS,
|
|
2
3
|
IMPLEMENTATIONS_UPLOAD_JOBS,
|
|
3
4
|
STRUCTURE_NOTES_UPLOAD_JOBS,
|
|
4
5
|
linkTo,
|
|
5
6
|
} from "@truedat/core/routes";
|
|
6
7
|
|
|
7
8
|
export const uploadJobScopes = {
|
|
9
|
+
concepts: {
|
|
10
|
+
listPath: CONCEPTS_UPLOAD_JOBS,
|
|
11
|
+
detailPath: (id) => linkTo.CONCEPTS_UPLOAD_JOB({ id }),
|
|
12
|
+
listHeaderId: "uploadJobs.concepts.header",
|
|
13
|
+
detailHeaderId: "uploadJob.concepts.header",
|
|
14
|
+
entityColumnMessageId: "uploadJob.results.column.concept",
|
|
15
|
+
fieldMessagePrefix: "concepts.props",
|
|
16
|
+
},
|
|
8
17
|
implementations: {
|
|
9
18
|
listPath: IMPLEMENTATIONS_UPLOAD_JOBS,
|
|
10
19
|
detailPath: (id) => linkTo.IMPLEMENTATIONS_UPLOAD_JOB({ id }),
|
|
@@ -52,7 +52,8 @@ export const classifyUploadJobErrorMessage = (message) => {
|
|
|
52
52
|
const isTechnicalError =
|
|
53
53
|
/^%[A-Z][A-Za-z0-9_.]*\{/.test(normalizedMessage) ||
|
|
54
54
|
/^\{:/.test(normalizedMessage) ||
|
|
55
|
-
/^\*\* \(/.test(normalizedMessage)
|
|
55
|
+
/^\*\* \(/.test(normalizedMessage) ||
|
|
56
|
+
/^protocol \S+ not implemented/.test(normalizedMessage);
|
|
56
57
|
|
|
57
58
|
return isTechnicalError
|
|
58
59
|
? { type: "unexpected_error" }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
|
|
2
|
+
import { waitForLoad } from "@truedat/test/waitForLoad";
|
|
3
|
+
import { Alert } from "../Alert";
|
|
4
|
+
|
|
5
|
+
const mockFormatMessage = jest.fn(({ id }) => id);
|
|
6
|
+
|
|
7
|
+
jest.mock("react-intl", () => ({
|
|
8
|
+
...jest.requireActual("react-intl"),
|
|
9
|
+
useIntl: () => ({ formatMessage: mockFormatMessage }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
beforeEach(() => mockFormatMessage.mockClear());
|
|
13
|
+
|
|
14
|
+
const renderError = async (error) => {
|
|
15
|
+
const rendered = render(
|
|
16
|
+
<Alert
|
|
17
|
+
dismissAlert={jest.fn()}
|
|
18
|
+
message={{
|
|
19
|
+
error: true,
|
|
20
|
+
header: "header.id",
|
|
21
|
+
icon: "attention",
|
|
22
|
+
text: "text",
|
|
23
|
+
errorList: [error],
|
|
24
|
+
}}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
await waitForLoad(rendered);
|
|
28
|
+
return rendered;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("a validation error with values", () => {
|
|
32
|
+
it("shows the message and the field it came from", async () => {
|
|
33
|
+
const rendered = await renderError({
|
|
34
|
+
code: "undefined",
|
|
35
|
+
name: "concept.error.content.errors.date_restrictions.out_of_range",
|
|
36
|
+
field: "fecha max fija",
|
|
37
|
+
values: { limit: "2026-06-30" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const text = rendered.container.textContent;
|
|
41
|
+
|
|
42
|
+
expect(text).toContain("fecha max fija");
|
|
43
|
+
expect(text).toContain(
|
|
44
|
+
"concept.error.content.errors.date_restrictions.out_of_range",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("passes the field and limit as interpolation parameters", async () => {
|
|
49
|
+
await renderError({
|
|
50
|
+
code: "undefined",
|
|
51
|
+
name: "concept.error.content.errors.date_restrictions.out_of_range",
|
|
52
|
+
field: "fecha max fija",
|
|
53
|
+
values: { limit: "2026-06-30" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(mockFormatMessage).toHaveBeenCalledWith(
|
|
57
|
+
{ id: "concept.error.content.errors.date_restrictions.out_of_range" },
|
|
58
|
+
{
|
|
59
|
+
text: "undefined",
|
|
60
|
+
field: "fecha max fija",
|
|
61
|
+
limit: "2026-06-30",
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to the key when there are no values", async () => {
|
|
67
|
+
const rendered = await renderError({
|
|
68
|
+
code: "undefined",
|
|
69
|
+
name: "concept.error.content.name.is.invalid",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
rendered.getByText("concept.error.content.name.is.invalid"),
|
|
74
|
+
).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("keeps showing a plain error with a code and no field", async () => {
|
|
78
|
+
const rendered = await renderError({
|
|
79
|
+
code: "some_code",
|
|
80
|
+
name: "createDomain.error.name.unique",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
rendered.getByText("createDomain.error.name.unique"),
|
|
85
|
+
).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -62,14 +62,31 @@ describe("<TemplateSelector />", () => {
|
|
|
62
62
|
expect(rendered.container).toMatchSnapshot();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("
|
|
65
|
+
it("shows an actionable message if templates query returns an error", async () => {
|
|
66
66
|
const renderOpts = { mocks: [errorTemplateMock(variables)] };
|
|
67
67
|
const rendered = render(
|
|
68
68
|
<TemplateSelector {...props} required />,
|
|
69
69
|
renderOpts,
|
|
70
70
|
);
|
|
71
71
|
await waitForLoad(rendered);
|
|
72
|
-
expect(
|
|
72
|
+
expect(
|
|
73
|
+
rendered.getByText(/templates could not be loaded/i),
|
|
74
|
+
).toBeInTheDocument();
|
|
75
|
+
expect(
|
|
76
|
+
rendered.getByRole("button", { name: /retry/i }),
|
|
77
|
+
).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("retries a failed templates query", async () => {
|
|
81
|
+
const renderOpts = {
|
|
82
|
+
mocks: [errorTemplateMock(variables), multipleTemplatesMock(variables)],
|
|
83
|
+
};
|
|
84
|
+
const rendered = render(<TemplateSelector {...props} />, renderOpts);
|
|
85
|
+
const user = userEvent.setup({ delay: null });
|
|
86
|
+
|
|
87
|
+
await user.click(await rendered.findByRole("button", { name: /retry/i }));
|
|
88
|
+
|
|
89
|
+
expect(await rendered.findByText(/template1/i)).toBeInTheDocument();
|
|
73
90
|
});
|
|
74
91
|
|
|
75
92
|
it("calls onChange when template is selected", async () => {
|
|
@@ -37,4 +37,14 @@ describe("ImplementationUploadJobBreadcrumbs", () => {
|
|
|
37
37
|
expect(rendered.getByText(filename)).toBeInTheDocument();
|
|
38
38
|
expect(rendered.container).toMatchSnapshot();
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it("renders breadcrumbs for concepts scope", async () => {
|
|
42
|
+
const rendered = render(<UploadJobBreadcrumbs scope="concepts" />);
|
|
43
|
+
await waitForLoad(rendered);
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
rendered.getByText(/uploadJobs.concepts.header/i),
|
|
47
|
+
).toBeInTheDocument();
|
|
48
|
+
expect(rendered.container).toMatchSnapshot();
|
|
49
|
+
});
|
|
40
50
|
});
|
|
@@ -32,6 +32,10 @@ jest.mock("@truedat/dd/components/StructureNoteLink", () => ({
|
|
|
32
32
|
);
|
|
33
33
|
},
|
|
34
34
|
}));
|
|
35
|
+
jest.mock("react-router", () => ({
|
|
36
|
+
...jest.requireActual("react-router"),
|
|
37
|
+
Link: ({ to, children }) => <a href={to}>{children}</a>,
|
|
38
|
+
}));
|
|
35
39
|
|
|
36
40
|
const changeMessages = {
|
|
37
41
|
en: {
|
|
@@ -50,6 +54,8 @@ const changeMessages = {
|
|
|
50
54
|
"uploadJob.parser.error.field_validation_error": "Field validation error",
|
|
51
55
|
"uploadJob.parser.error.data_structure_not_found":
|
|
52
56
|
"Data structure not found",
|
|
57
|
+
"uploadJob.parser.error.unexpected_error": "Unexpected error",
|
|
58
|
+
"uploadJob.parser.error.name_not_available": "Name not available",
|
|
53
59
|
"uploadJob.parser.detail.message": "Message",
|
|
54
60
|
},
|
|
55
61
|
};
|
|
@@ -99,6 +105,98 @@ describe("<ResultEntityCell />", () => {
|
|
|
99
105
|
);
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
it("renders INFO concept links from row details", () => {
|
|
109
|
+
const rendered = render(
|
|
110
|
+
<ResultEntityCell
|
|
111
|
+
status="INFO"
|
|
112
|
+
response={{
|
|
113
|
+
details: {
|
|
114
|
+
id: 5,
|
|
115
|
+
business_concept_id: 3,
|
|
116
|
+
name: "My Concept",
|
|
117
|
+
},
|
|
118
|
+
}}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
|
|
122
|
+
"href",
|
|
123
|
+
"/concepts/3/versions/5",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("renders ERROR concept links from row details", () => {
|
|
128
|
+
const rendered = render(
|
|
129
|
+
<ResultEntityCell
|
|
130
|
+
status="ERROR"
|
|
131
|
+
response={{
|
|
132
|
+
type: "name_not_available",
|
|
133
|
+
details: {
|
|
134
|
+
id: 5,
|
|
135
|
+
business_concept_id: 3,
|
|
136
|
+
name: "My Concept",
|
|
137
|
+
},
|
|
138
|
+
}}
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
|
|
142
|
+
"href",
|
|
143
|
+
"/concepts/3/versions/5",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("renders concept links for unexpected_error rows with entity data", () => {
|
|
148
|
+
const rendered = render(
|
|
149
|
+
<ResultEntityCell
|
|
150
|
+
status="ERROR"
|
|
151
|
+
response={{
|
|
152
|
+
type: "unexpected_error",
|
|
153
|
+
details: {
|
|
154
|
+
id: 5,
|
|
155
|
+
business_concept_id: 3,
|
|
156
|
+
name: "My Concept",
|
|
157
|
+
},
|
|
158
|
+
}}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
|
|
162
|
+
"href",
|
|
163
|
+
"/concepts/3/versions/5",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders concept links for unchanged rows", () => {
|
|
168
|
+
const rendered = render(
|
|
169
|
+
<ResultEntityCell
|
|
170
|
+
status="INFO"
|
|
171
|
+
response={{
|
|
172
|
+
type: "unchanged",
|
|
173
|
+
details: {
|
|
174
|
+
id: 5,
|
|
175
|
+
business_concept_id: 3,
|
|
176
|
+
name: "My Concept",
|
|
177
|
+
},
|
|
178
|
+
}}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
|
|
182
|
+
"href",
|
|
183
|
+
"/concepts/3/versions/5",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("renders no entity for historical unchanged rows without entity data", () => {
|
|
188
|
+
const rendered = render(
|
|
189
|
+
<ResultEntityCell
|
|
190
|
+
status="INFO"
|
|
191
|
+
response={{
|
|
192
|
+
type: "unchanged",
|
|
193
|
+
details: { row_number: 3, sheet: "date template" },
|
|
194
|
+
}}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
expect(rendered.container).toBeEmptyDOMElement();
|
|
198
|
+
});
|
|
199
|
+
|
|
102
200
|
it("renders implementation refs and preferred structure labels with external ids", () => {
|
|
103
201
|
const implementation = render(
|
|
104
202
|
<ResultEntityCell
|
|
@@ -512,6 +610,128 @@ describe("<ResponseCell />", () => {
|
|
|
512
610
|
expect(rendered.getByText(/Already exists/i)).toBeInTheDocument();
|
|
513
611
|
});
|
|
514
612
|
|
|
613
|
+
it("translates named field errors and interpolates their field and values", () => {
|
|
614
|
+
const rendered = render(
|
|
615
|
+
<ResponseCell
|
|
616
|
+
response={{
|
|
617
|
+
type: "field_validation_error",
|
|
618
|
+
details: {
|
|
619
|
+
implementation_key: "implementation-key",
|
|
620
|
+
errors: [
|
|
621
|
+
{
|
|
622
|
+
name: "errors.date_restrictions.out_of_range",
|
|
623
|
+
message: "Invalid value",
|
|
624
|
+
field: "threshold",
|
|
625
|
+
values: { min: 10, max: 20 },
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
},
|
|
629
|
+
}}
|
|
630
|
+
status="ERROR"
|
|
631
|
+
scope="concepts"
|
|
632
|
+
/>,
|
|
633
|
+
{
|
|
634
|
+
messages: {
|
|
635
|
+
en: {
|
|
636
|
+
"concept.error.content.errors.date_restrictions.out_of_range":
|
|
637
|
+
"{field} must be between {min} and {max}",
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
expect(
|
|
644
|
+
rendered.getByText("threshold must be between 10 and 20"),
|
|
645
|
+
).toBeInTheDocument();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("keeps field errors with only a message compatible", () => {
|
|
649
|
+
const rendered = render(
|
|
650
|
+
<ResponseCell
|
|
651
|
+
response={{
|
|
652
|
+
type: "field_validation_error",
|
|
653
|
+
details: {
|
|
654
|
+
errors: [{ field: "name", message: "Required" }],
|
|
655
|
+
},
|
|
656
|
+
}}
|
|
657
|
+
status="ERROR"
|
|
658
|
+
/>,
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
expect(rendered.getByText("Required")).toBeInTheDocument();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("translates named implementation errors and interpolates their values", () => {
|
|
665
|
+
const rendered = render(
|
|
666
|
+
<ResponseCell
|
|
667
|
+
response={{
|
|
668
|
+
type: "field_validation_error",
|
|
669
|
+
details: {
|
|
670
|
+
implementation_key: "implementation-key",
|
|
671
|
+
errors: [
|
|
672
|
+
{
|
|
673
|
+
name: "value.out_of_range",
|
|
674
|
+
message: "Invalid value",
|
|
675
|
+
field: "threshold",
|
|
676
|
+
values: { min: 10, max: 20 },
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
}}
|
|
681
|
+
status="ERROR"
|
|
682
|
+
/>,
|
|
683
|
+
{
|
|
684
|
+
messages: {
|
|
685
|
+
en: {
|
|
686
|
+
"implementation.error.value.out_of_range":
|
|
687
|
+
"{field} must be between {min} and {max}",
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
expect(
|
|
694
|
+
rendered.getByText("threshold must be between 10 and 20"),
|
|
695
|
+
).toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("renders ERROR status with field_validation_error details as an object", () => {
|
|
699
|
+
const response = {
|
|
700
|
+
type: "field_validation_error",
|
|
701
|
+
details: {
|
|
702
|
+
id: 693,
|
|
703
|
+
name: "prueba2",
|
|
704
|
+
sheet: "termino",
|
|
705
|
+
errors: { adjuntos: ["is invalid"] },
|
|
706
|
+
row_number: 3,
|
|
707
|
+
business_concept_id: 337,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
const rendered = render(
|
|
711
|
+
<ResponseCell response={response} status="ERROR" />,
|
|
712
|
+
);
|
|
713
|
+
expect(
|
|
714
|
+
rendered.getByText(/uploadJob.parser.error.field_validation_error/i),
|
|
715
|
+
).toBeInTheDocument();
|
|
716
|
+
expect(rendered.getByText(/adjuntos/i)).toBeInTheDocument();
|
|
717
|
+
expect(rendered.getByText(/is invalid/i)).toBeInTheDocument();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("renders ERROR status with field_validation_error object details with multiple messages", () => {
|
|
721
|
+
const response = {
|
|
722
|
+
type: "field_validation_error",
|
|
723
|
+
details: {
|
|
724
|
+
errors: { name: ["Required", "Too long"], type: "is invalid" },
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
const rendered = render(
|
|
728
|
+
<ResponseCell response={response} status="ERROR" />,
|
|
729
|
+
);
|
|
730
|
+
expect(rendered.getByText(/Required/i)).toBeInTheDocument();
|
|
731
|
+
expect(rendered.getByText(/Too long/i)).toBeInTheDocument();
|
|
732
|
+
expect(rendered.getByText(/is invalid/i)).toBeInTheDocument();
|
|
733
|
+
});
|
|
734
|
+
|
|
515
735
|
it("renders ERROR status with deprecated_implementation details", () => {
|
|
516
736
|
const response = {
|
|
517
737
|
type: "deprecated_implementation",
|
|
@@ -706,6 +926,27 @@ describe("<ResponseCell />", () => {
|
|
|
706
926
|
});
|
|
707
927
|
});
|
|
708
928
|
|
|
929
|
+
it("renders no changes disclosure when change_details is empty", () => {
|
|
930
|
+
const rendered = render(
|
|
931
|
+
<ResponseCell
|
|
932
|
+
response={{
|
|
933
|
+
type: "updated",
|
|
934
|
+
details: {
|
|
935
|
+
change_details: {},
|
|
936
|
+
},
|
|
937
|
+
}}
|
|
938
|
+
status="INFO"
|
|
939
|
+
/>,
|
|
940
|
+
{
|
|
941
|
+
messages: changeMessages,
|
|
942
|
+
},
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
expect(
|
|
946
|
+
rendered.queryByRole("button", { name: /modified fields?/i }),
|
|
947
|
+
).not.toBeInTheDocument();
|
|
948
|
+
});
|
|
949
|
+
|
|
709
950
|
it("renders boolean, null and zero change values without losing their values", async () => {
|
|
710
951
|
const user = userEvent.setup({ delay: null });
|
|
711
952
|
const rendered = render(
|
|
@@ -850,6 +1091,67 @@ describe("<ResultDetailCell /> errors", () => {
|
|
|
850
1091
|
expect(rendered.queryByText("unknown-note")).not.toBeInTheDocument();
|
|
851
1092
|
});
|
|
852
1093
|
|
|
1094
|
+
it("renders unexpected_error with empty details as a generic message only", () => {
|
|
1095
|
+
const rendered = render(
|
|
1096
|
+
<ResultDetailCell
|
|
1097
|
+
response={{ type: "unexpected_error", details: {} }}
|
|
1098
|
+
status="ERROR"
|
|
1099
|
+
/>,
|
|
1100
|
+
{ messages: changeMessages },
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
expect(rendered.getByText("Unexpected error")).toBeInTheDocument();
|
|
1104
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
1105
|
+
expect(
|
|
1106
|
+
rendered.queryByText(/protocol String\.Chars/),
|
|
1107
|
+
).not.toBeInTheDocument();
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it("renders unexpected_error events with technical messages as a generic message only", () => {
|
|
1111
|
+
const technicalMessage =
|
|
1112
|
+
"protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n";
|
|
1113
|
+
|
|
1114
|
+
const rendered = render(
|
|
1115
|
+
<ResultDetailCell
|
|
1116
|
+
response={{
|
|
1117
|
+
type: "unexpected_error",
|
|
1118
|
+
details: { message: technicalMessage },
|
|
1119
|
+
}}
|
|
1120
|
+
status="ERROR"
|
|
1121
|
+
/>,
|
|
1122
|
+
{ messages: changeMessages },
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
expect(rendered.getByText("Unexpected error")).toBeInTheDocument();
|
|
1126
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
1127
|
+
expect(
|
|
1128
|
+
rendered.queryByText(/protocol String\.Chars/),
|
|
1129
|
+
).not.toBeInTheDocument();
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it("renders name_not_available with the conflicting name as a simple message", () => {
|
|
1133
|
+
const rendered = render(
|
|
1134
|
+
<ResultDetailCell
|
|
1135
|
+
response={{
|
|
1136
|
+
type: "name_not_available",
|
|
1137
|
+
details: {
|
|
1138
|
+
name: "TEST CREACION",
|
|
1139
|
+
row_number: 3,
|
|
1140
|
+
sheet: "date template",
|
|
1141
|
+
},
|
|
1142
|
+
}}
|
|
1143
|
+
status="ERROR"
|
|
1144
|
+
/>,
|
|
1145
|
+
{ messages: changeMessages },
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
expect(
|
|
1149
|
+
rendered.getByText("Name not available: TEST CREACION"),
|
|
1150
|
+
).toBeInTheDocument();
|
|
1151
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
1152
|
+
expect(rendered.queryByText(/row_number/)).not.toBeInTheDocument();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
853
1155
|
it("uses the singular error count for one field", () => {
|
|
854
1156
|
const rendered = render(
|
|
855
1157
|
<ResultDetailCell
|
|
@@ -25,6 +25,23 @@ describe("Upload job scopes", () => {
|
|
|
25
25
|
);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
it("defines concepts upload routes and presentation metadata", () => {
|
|
29
|
+
expect(uploadJobScopes.concepts.listPath).toBe("/concepts/uploadJobs");
|
|
30
|
+
expect(uploadJobScopes.concepts.listHeaderId).toBe(
|
|
31
|
+
"uploadJobs.concepts.header",
|
|
32
|
+
);
|
|
33
|
+
expect(uploadJobScopes.concepts.detailHeaderId).toBe(
|
|
34
|
+
"uploadJob.concepts.header",
|
|
35
|
+
);
|
|
36
|
+
expect(uploadJobScopes.concepts.detailPath(12)).toBe(
|
|
37
|
+
"/concepts/uploadJobs/12",
|
|
38
|
+
);
|
|
39
|
+
expect(uploadJobScopes.concepts.entityColumnMessageId).toBe(
|
|
40
|
+
"uploadJob.results.column.concept",
|
|
41
|
+
);
|
|
42
|
+
expect(uploadJobScopes.concepts.fieldMessagePrefix).toBe("concepts.props");
|
|
43
|
+
});
|
|
44
|
+
|
|
28
45
|
it("falls back to implementation semantics for unknown scopes", () => {
|
|
29
46
|
expect(getUploadJobScope("other")).toMatchObject({
|
|
30
47
|
listPath: null,
|
|
@@ -130,6 +130,33 @@ describe("<UploadJobSummary />", () => {
|
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("classifies protocol not implemented messages as unexpected errors", () => {
|
|
134
|
+
expect(
|
|
135
|
+
classifyUploadJobErrorMessage(
|
|
136
|
+
"protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n",
|
|
137
|
+
),
|
|
138
|
+
).toEqual({ type: "unexpected_error" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("replaces protocol failure details with an unexpected error", () => {
|
|
142
|
+
const technicalMessage =
|
|
143
|
+
"protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n";
|
|
144
|
+
const rendered = render(
|
|
145
|
+
<UploadJobSummary
|
|
146
|
+
events={[{ status: "FAILED", response: { message: technicalMessage } }]}
|
|
147
|
+
latestStatus="FAILED"
|
|
148
|
+
/>,
|
|
149
|
+
{ messages: summaryMessages },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(rendered.getByLabelText("Summary")).toHaveTextContent(
|
|
153
|
+
"Error processing the file: Unexpected error",
|
|
154
|
+
);
|
|
155
|
+
expect(rendered.getByLabelText("Summary")).not.toHaveTextContent(
|
|
156
|
+
"protocol String.Chars not implemented",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
133
160
|
it("renders a human failure message", () => {
|
|
134
161
|
const rendered = render(
|
|
135
162
|
<UploadJobSummary
|
|
@@ -189,4 +189,19 @@ describe("<UploadJobs />", () => {
|
|
|
189
189
|
expect(rendered.getByText(/completed/i)).toBeInTheDocument();
|
|
190
190
|
expect(rendered.getByText(/success/i)).toBeInTheDocument();
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
it("renders concepts scope header", async () => {
|
|
194
|
+
useUploadJobs.mockReturnValue({
|
|
195
|
+
loading: false,
|
|
196
|
+
data: {
|
|
197
|
+
data: [],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const rendered = render(<UploadJobs scope="concepts" />);
|
|
202
|
+
await waitForLoad(rendered);
|
|
203
|
+
|
|
204
|
+
const elements = rendered.getAllByText(/uploadJobs.concepts.header/i);
|
|
205
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
206
|
+
});
|
|
192
207
|
});
|
|
@@ -132,7 +132,7 @@ exports[`<GlossaryMenu /> matches the latest snapshot, finds fixed and subscopes
|
|
|
132
132
|
aria-checked="false"
|
|
133
133
|
class="item"
|
|
134
134
|
data-discover="true"
|
|
135
|
-
href="/concepts/
|
|
135
|
+
href="/concepts/uploadJobs"
|
|
136
136
|
name="concepts_upload_events"
|
|
137
137
|
role="option"
|
|
138
138
|
>
|
|
@@ -125,7 +125,7 @@ exports[`<SideMenu /> matches the latest snapshot 1`] = `
|
|
|
125
125
|
aria-checked="false"
|
|
126
126
|
class="item"
|
|
127
127
|
data-discover="true"
|
|
128
|
-
href="/concepts/
|
|
128
|
+
href="/concepts/uploadJobs"
|
|
129
129
|
name="concepts_upload_events"
|
|
130
130
|
role="option"
|
|
131
131
|
>
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
+
exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs for concepts scope 1`] = `
|
|
4
|
+
<div>
|
|
5
|
+
<div
|
|
6
|
+
class="ui breadcrumb"
|
|
7
|
+
>
|
|
8
|
+
<a
|
|
9
|
+
class="section"
|
|
10
|
+
data-discover="true"
|
|
11
|
+
href="/concepts/uploadJobs"
|
|
12
|
+
>
|
|
13
|
+
uploadJobs.concepts.header
|
|
14
|
+
</a>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
`;
|
|
18
|
+
|
|
3
19
|
exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs with filename 1`] = `
|
|
4
20
|
<div>
|
|
5
21
|
<div
|
package/src/components/index.js
CHANGED
|
@@ -55,7 +55,7 @@ import SideMenu from "./SideMenu";
|
|
|
55
55
|
import StructureFilterItem from "./StructureFilterItem";
|
|
56
56
|
import Submenu from "./Submenu";
|
|
57
57
|
import TaxonomyMenu from "./TaxonomyMenu";
|
|
58
|
-
import TemplateSelector from "./TemplateSelector";
|
|
58
|
+
import TemplateSelector, { TemplateLoadError } from "./TemplateSelector";
|
|
59
59
|
import TreeSelector from "./TreeSelector";
|
|
60
60
|
import Unauthorized from "./Unauthorized";
|
|
61
61
|
import UploadModal from "./UploadModal";
|
|
@@ -119,6 +119,7 @@ export {
|
|
|
119
119
|
StructureFilterItem,
|
|
120
120
|
Submenu,
|
|
121
121
|
TaxonomyMenu,
|
|
122
|
+
TemplateLoadError,
|
|
122
123
|
TemplateSelector,
|
|
123
124
|
TreeSelector,
|
|
124
125
|
TrueModal,
|
package/src/routes.js
CHANGED
|
@@ -14,8 +14,8 @@ export const BUCKET_VIEW_CONFIG_EDIT = "/bucketViewConfigs/:id/edit";
|
|
|
14
14
|
export const BUCKET_VIEW_CONFIG_NEW = "/bucketViewConfigs/new";
|
|
15
15
|
export const CONCEPTS = "/concepts";
|
|
16
16
|
export const CONCEPTS_BULK_UPDATE = "/concepts/bulk_update";
|
|
17
|
-
export const
|
|
18
|
-
|
|
17
|
+
export const CONCEPTS_UPLOAD_JOBS = "/concepts/uploadJobs";
|
|
18
|
+
export const CONCEPTS_UPLOAD_JOB = "/concepts/uploadJobs/:id";
|
|
19
19
|
export const CONCEPTS_NEW = "/concepts/new";
|
|
20
20
|
export const CONCEPTS_PENDING = "/concepts/pending";
|
|
21
21
|
export const CONCEPTS_DEPRECATED = "/concepts/deprecated";
|
|
@@ -313,6 +313,8 @@ const routes = {
|
|
|
313
313
|
BUCKET_VIEW_CONFIGS,
|
|
314
314
|
CONCEPTS,
|
|
315
315
|
CONCEPTS_BULK_UPDATE,
|
|
316
|
+
CONCEPTS_UPLOAD_JOBS,
|
|
317
|
+
CONCEPTS_UPLOAD_JOB,
|
|
316
318
|
CONCEPT_LINKS_APPROVALS,
|
|
317
319
|
CONCEPT_LINKS_APPROVALS_RESULTS,
|
|
318
320
|
CONCEPTS_NEW,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { toErrorList, phoenixFailure } from "../message";
|
|
2
|
+
|
|
3
|
+
describe("errors whose entries are objects", () => {
|
|
4
|
+
it("does not stringify an object into the key", () => {
|
|
5
|
+
const [error] = toErrorList("checkoutGrantRequest.error")({
|
|
6
|
+
requests: [{ data_structure_id: 42 }],
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
expect(error.name).not.toContain("[object Object]");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("falls back to the key itself when the object carries no message", () => {
|
|
13
|
+
const [error] = toErrorList("checkoutGrantRequest.error")({
|
|
14
|
+
requests: [{ data_structure_id: 42 }],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(error.name).toBe("checkoutGrantRequest.error.requests");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("uses a message when the object carries one", () => {
|
|
21
|
+
const [error] = toErrorList("checkoutGrantRequest.error")({
|
|
22
|
+
requests: [{ message: "user_id is required" }],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(error.name).toBe(
|
|
26
|
+
"checkoutGrantRequest.error.requests.user_id is required",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("keeps plain string entries working as before", () => {
|
|
31
|
+
const errors = toErrorList("checkoutGrantRequest.error")({
|
|
32
|
+
user_id: ["can't be blank"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(errors).toEqual([
|
|
36
|
+
{ name: "checkoutGrantRequest.error.user_id.can't be blank" },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("a phoenix failure with an object entry does not leak it either", () => {
|
|
41
|
+
const failure = phoenixFailure("CHECKOUT_GRANT_REQUEST/FAILURE", {
|
|
42
|
+
requests: [{ data_structure_id: 42 }],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(JSON.stringify(failure)).not.toContain("[object Object]");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("both changeset error shapes", () => {
|
|
50
|
+
it("passes through a list the back end already normalised", () => {
|
|
51
|
+
const failure = phoenixFailure("RULE_CREATE/FAILURE", [
|
|
52
|
+
{
|
|
53
|
+
name: "rule.error.errors.date_restrictions.out_of_range",
|
|
54
|
+
field: "fecha max fija",
|
|
55
|
+
values: { limit: "2026-06-30" },
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
expect(failure.errorList).toEqual([
|
|
60
|
+
{
|
|
61
|
+
name: "rule.error.errors.date_restrictions.out_of_range",
|
|
62
|
+
field: "fecha max fija",
|
|
63
|
+
values: { limit: "2026-06-30" },
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("still builds keys from the older object shape", () => {
|
|
69
|
+
const failure = phoenixFailure("RULE_CREATE/FAILURE", {
|
|
70
|
+
name: ["can't be blank"],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(failure.errorList).toEqual([
|
|
74
|
+
{ name: "ruleCreate.error.name.can't be blank" },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/services/message.js
CHANGED
|
@@ -4,7 +4,9 @@ const isErrorsFailure = (status, data) =>
|
|
|
4
4
|
status === 422 && _.conformsTo({ errors: _.isArray })(data);
|
|
5
5
|
|
|
6
6
|
const isPhoenixFailure = (status, data) =>
|
|
7
|
-
status === 422 &&
|
|
7
|
+
status === 422 &&
|
|
8
|
+
(_.conformsTo({ errors: _.isObject })(data) ||
|
|
9
|
+
_.conformsTo({ errors: _.isArray })(data));
|
|
8
10
|
|
|
9
11
|
const isDefaultFailure = (status) => {
|
|
10
12
|
return !!status;
|
|
@@ -79,14 +81,32 @@ export const graphQLErrors = (payload) => {
|
|
|
79
81
|
};
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
_.
|
|
84
|
+
const errorKeyPart = (entry) => {
|
|
85
|
+
if (_.isString(entry)) return entry;
|
|
86
|
+
if (_.isObject(entry)) return entry.message || "";
|
|
87
|
+
return String(entry ?? "");
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const toErrorList = (prefix) => (errors) => {
|
|
91
|
+
if (_.isArray(errors)) {
|
|
92
|
+
return errors.map((error) => ({
|
|
93
|
+
name: error.name,
|
|
94
|
+
...(error.field ? { field: error.field } : {}),
|
|
95
|
+
...(_.isEmpty(error.values) ? {} : { values: error.values }),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return _.flow(
|
|
84
100
|
_.toPairs,
|
|
85
|
-
_.flatMap(([field,
|
|
86
|
-
_.map((
|
|
101
|
+
_.flatMap(([field, errorList]) =>
|
|
102
|
+
_.map((entry) => {
|
|
103
|
+
const part = errorKeyPart(entry);
|
|
104
|
+
return part ? `${prefix}.${field}.${part}` : `${prefix}.${field}`;
|
|
105
|
+
})(errorList),
|
|
87
106
|
),
|
|
88
107
|
_.map((name) => ({ name })),
|
|
89
|
-
);
|
|
108
|
+
)(errors);
|
|
109
|
+
};
|
|
90
110
|
|
|
91
111
|
export const phoenixFailure = (type, errors) => {
|
|
92
112
|
const action = _.flow(_.replace("/FAILURE", ""), _.camelCase)(type);
|