@truedat/core 8.13.5 → 8.13.7
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/TemplateSelector.js +29 -3
- package/src/components/UploadJobParser.js +37 -6
- 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__/UploadJobParser.spec.js +85 -0
- package/src/components/index.js +2 -1
- 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.7",
|
|
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.7",
|
|
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": "704dcfcba34b6094a368a93059de7f4937502f32"
|
|
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",
|
|
@@ -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 (
|
|
@@ -47,7 +47,13 @@ export const ResponseCell = ({
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
case "ERROR":
|
|
50
|
-
return
|
|
50
|
+
return (
|
|
51
|
+
<ErrorDetail
|
|
52
|
+
response={response}
|
|
53
|
+
showOrigin={showOrigin}
|
|
54
|
+
scope={scope}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
51
57
|
case "COMPLETED":
|
|
52
58
|
return <CompletedDetail response={response} />;
|
|
53
59
|
case "INFO":
|
|
@@ -209,7 +215,7 @@ const externalIdDetailValue = (details) =>
|
|
|
209
215
|
(details?.external_id ?? "")
|
|
210
216
|
);
|
|
211
217
|
|
|
212
|
-
const ErrorDetail = ({ response, showOrigin }) => {
|
|
218
|
+
const ErrorDetail = ({ response, showOrigin, scope }) => {
|
|
213
219
|
const { formatMessage } = useIntl();
|
|
214
220
|
|
|
215
221
|
if (!response || !response.type) {
|
|
@@ -218,6 +224,31 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
218
224
|
const { type } = response;
|
|
219
225
|
const messagePrefix = "uploadJob.parser.detail";
|
|
220
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
|
+
|
|
221
252
|
if (!showOrigin && type === "data_structure_not_found") {
|
|
222
253
|
return (
|
|
223
254
|
<span className="upload-job-simple-error">
|
|
@@ -251,11 +282,11 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
251
282
|
}
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
const validationErrorPairs = (errors) => {
|
|
285
|
+
const validationErrorPairs = (errors, details) => {
|
|
255
286
|
if (_.isArray(errors)) {
|
|
256
287
|
return errors.map((e) => [
|
|
257
288
|
e?.field ?? formatMessage({ id: `${messagePrefix}.message` }),
|
|
258
|
-
e
|
|
289
|
+
translatedFieldError(e, details),
|
|
259
290
|
]);
|
|
260
291
|
}
|
|
261
292
|
|
|
@@ -343,7 +374,7 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
343
374
|
[null, externalIdDetailValue(details)],
|
|
344
375
|
].filter(([, v]) => v !== undefined && v !== ""),
|
|
345
376
|
field_validation_error: (details) =>
|
|
346
|
-
validationErrorPairs(details?.errors) ?? [
|
|
377
|
+
validationErrorPairs(details?.errors, details) ?? [
|
|
347
378
|
[null, externalIdDetailValue(details)],
|
|
348
379
|
],
|
|
349
380
|
unprocessable_entity: (details) => [[null, externalIdDetailValue(details)]],
|
|
@@ -355,7 +386,7 @@ const ErrorDetail = ({ response, showOrigin }) => {
|
|
|
355
386
|
? `structures.uploadStructures.failed.note.${reason}`
|
|
356
387
|
: null;
|
|
357
388
|
|
|
358
|
-
const validationPairs = validationErrorPairs(details?.errors);
|
|
389
|
+
const validationPairs = validationErrorPairs(details?.errors, details);
|
|
359
390
|
|
|
360
391
|
const errorPairs = _.isEmpty(validationPairs)
|
|
361
392
|
? [
|
|
@@ -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 () => {
|
|
@@ -610,6 +610,91 @@ describe("<ResponseCell />", () => {
|
|
|
610
610
|
expect(rendered.getByText(/Already exists/i)).toBeInTheDocument();
|
|
611
611
|
});
|
|
612
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
|
+
|
|
613
698
|
it("renders ERROR status with field_validation_error details as an object", () => {
|
|
614
699
|
const response = {
|
|
615
700
|
type: "field_validation_error",
|
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,
|
|
@@ -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);
|