@truedat/dd 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/StructureNotesEdit.js +8 -2
- package/src/components/SystemForm.js +10 -1
- package/src/components/__tests__/StructureNotesEdit.spec.js +105 -1
- package/src/components/__tests__/SystemForm.dateRestrictions.spec.js +102 -0
- package/src/reducers/__tests__/structureNoteErrorMessage.spec.js +78 -0
- package/src/reducers/ddMessage.js +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "8.13.
|
|
3
|
+
"version": "8.13.6",
|
|
4
4
|
"description": "Truedat Web Data Dictionary",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@testing-library/jest-dom": "^6.6.3",
|
|
55
55
|
"@testing-library/react": "^16.3.0",
|
|
56
56
|
"@testing-library/user-event": "^14.6.1",
|
|
57
|
-
"@truedat/test": "8.13.
|
|
57
|
+
"@truedat/test": "8.13.6",
|
|
58
58
|
"identity-obj-proxy": "^3.0.0",
|
|
59
59
|
"jest": "^29.7.0",
|
|
60
60
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"svg-pan-zoom": "^3.6.2",
|
|
91
91
|
"swr": "^2.3.3"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "c0943c8cf31e82a81fdca24d68ef57406fe5bcbc"
|
|
94
94
|
}
|
|
@@ -6,7 +6,7 @@ import { Button, Form, Segment } from "semantic-ui-react";
|
|
|
6
6
|
import { useIntl } from "react-intl";
|
|
7
7
|
import HistoryBackButton from "@truedat/core/components/HistoryBackButton";
|
|
8
8
|
import { selectTemplate, selectDomains } from "@truedat/df/routines";
|
|
9
|
-
import { applyTemplate } from "@truedat/df/utils";
|
|
9
|
+
import { applyTemplate, validateContent } from "@truedat/df/utils";
|
|
10
10
|
import SuggestionsWidget from "@truedat/ai/components/suggestions/SuggestionsWidget";
|
|
11
11
|
import { doStructureNoteAction } from "../routines";
|
|
12
12
|
import { getLatestDfContent } from "../selectors/getSortedStructureNotes";
|
|
@@ -38,6 +38,7 @@ export const StructureNotesEdit = ({
|
|
|
38
38
|
latestDfContent,
|
|
39
39
|
}) => {
|
|
40
40
|
const [content, setContent] = useState(latestDfContent);
|
|
41
|
+
const [originalContent] = useState(latestDfContent);
|
|
41
42
|
const explicitEmptiesRef = useRef({});
|
|
42
43
|
const isModification = !_.isEmpty(latestDfContent);
|
|
43
44
|
const initializedRef = useRef(false);
|
|
@@ -56,7 +57,11 @@ export const StructureNotesEdit = ({
|
|
|
56
57
|
|
|
57
58
|
const { formatMessage, locale } = useIntl();
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
+
const hasDateRestrictionErrors = validateContent(
|
|
61
|
+
template,
|
|
62
|
+
originalContent,
|
|
63
|
+
)(content).some(({ error }) => error === "date out of range");
|
|
64
|
+
const submitDisabled = !template || hasDateRestrictionErrors;
|
|
60
65
|
|
|
61
66
|
const handleFormChange = (newContent) => {
|
|
62
67
|
const clearedFields = _.flow(
|
|
@@ -128,6 +133,7 @@ export const StructureNotesEdit = ({
|
|
|
128
133
|
<DynamicForm
|
|
129
134
|
onChange={handleFormChange}
|
|
130
135
|
content={content}
|
|
136
|
+
originalContent={originalContent}
|
|
131
137
|
isModification={isModification}
|
|
132
138
|
/>
|
|
133
139
|
<Button
|
|
@@ -31,9 +31,17 @@ export const SystemForm = ({
|
|
|
31
31
|
}) => {
|
|
32
32
|
const { formatMessage } = useIntl();
|
|
33
33
|
const [system, setSystem] = useState(editSystem || {});
|
|
34
|
+
const [originalContent] = useState(() =>
|
|
35
|
+
_.isEmpty(editSystem)
|
|
36
|
+
? undefined
|
|
37
|
+
: _.cloneDeep(editSystem.dynamic_content || {}),
|
|
38
|
+
);
|
|
34
39
|
|
|
35
40
|
const isNonEmpty = _.flow(_.trim, _.negate(_.isEmpty));
|
|
36
|
-
const isValidContent = _.flow(
|
|
41
|
+
const isValidContent = _.flow(
|
|
42
|
+
validateContent(template, originalContent),
|
|
43
|
+
_.isEmpty,
|
|
44
|
+
);
|
|
37
45
|
const isValid = _.conforms({
|
|
38
46
|
name: isNonEmpty,
|
|
39
47
|
external_id: isNonEmpty,
|
|
@@ -105,6 +113,7 @@ export const SystemForm = ({
|
|
|
105
113
|
template={template}
|
|
106
114
|
onChange={handleContentChange}
|
|
107
115
|
content={dynamic_content || {}}
|
|
116
|
+
originalContent={originalContent}
|
|
108
117
|
/>
|
|
109
118
|
)}
|
|
110
119
|
<div className="actions">
|
|
@@ -12,12 +12,14 @@ jest.mock("@truedat/ai/components/suggestions/SuggestionsWidget", () => ({
|
|
|
12
12
|
|
|
13
13
|
let dynamicFormOnChange;
|
|
14
14
|
let dynamicFormContent;
|
|
15
|
+
let dynamicFormOriginalContent;
|
|
15
16
|
|
|
16
17
|
jest.mock("@truedat/df/components/DynamicForm", () => ({
|
|
17
18
|
__esModule: true,
|
|
18
|
-
default: ({ onChange, content }) => {
|
|
19
|
+
default: ({ onChange, content, originalContent }) => {
|
|
19
20
|
dynamicFormOnChange = onChange;
|
|
20
21
|
dynamicFormContent = content;
|
|
22
|
+
dynamicFormOriginalContent = originalContent;
|
|
21
23
|
return <div data-testid="dynamic-form" />;
|
|
22
24
|
},
|
|
23
25
|
}));
|
|
@@ -27,6 +29,108 @@ describe("<StructureNotesEdit />", () => {
|
|
|
27
29
|
jest.clearAllMocks();
|
|
28
30
|
dynamicFormOnChange = undefined;
|
|
29
31
|
dynamicFormContent = undefined;
|
|
32
|
+
dynamicFormOriginalContent = undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("date restrictions when editing", () => {
|
|
36
|
+
const template = {
|
|
37
|
+
id: 2,
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
name: "dates",
|
|
41
|
+
fields: [
|
|
42
|
+
{
|
|
43
|
+
name: "fecha",
|
|
44
|
+
type: "date",
|
|
45
|
+
restrictions: { resolved: { min: null, max: "2026-10-15" } },
|
|
46
|
+
},
|
|
47
|
+
{ name: "description", type: "string" },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const latestDfContent = {
|
|
53
|
+
fecha: { value: "2026-10-16", origin: "user" },
|
|
54
|
+
description: { value: "Original", origin: "user" },
|
|
55
|
+
};
|
|
56
|
+
const props = {
|
|
57
|
+
structure: { id: 1, domain_ids: [1] },
|
|
58
|
+
template,
|
|
59
|
+
applyTemplate: applyTemplate(template),
|
|
60
|
+
latestDfContent,
|
|
61
|
+
selectTemplate: jest.fn(),
|
|
62
|
+
selectDomains: jest.fn(),
|
|
63
|
+
doStructureNoteAction: jest.fn(),
|
|
64
|
+
structureNotes: { structureNotes: [] },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it("keeps Save enabled for an unchanged out-of-range date", async () => {
|
|
68
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
69
|
+
await waitForLoad(rendered);
|
|
70
|
+
|
|
71
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("passes persisted originals to the widgets", async () => {
|
|
75
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
76
|
+
await waitForLoad(rendered);
|
|
77
|
+
|
|
78
|
+
expect(dynamicFormOriginalContent).toEqual(latestDfContent);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps Save enabled when only another field is edited", async () => {
|
|
82
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
83
|
+
await waitForLoad(rendered);
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
dynamicFormOnChange({
|
|
87
|
+
...latestDfContent,
|
|
88
|
+
description: { value: "Updated", origin: "user" },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("disables Save when the date changes to another invalid value", async () => {
|
|
96
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
97
|
+
await waitForLoad(rendered);
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
dynamicFormOnChange({
|
|
101
|
+
...latestDfContent,
|
|
102
|
+
fecha: { value: "2026-10-17", origin: "user" },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(await rendered.findByText(/save/i)).toBeDisabled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("restores the exemption when the original date is restored", async () => {
|
|
110
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
111
|
+
await waitForLoad(rendered);
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
dynamicFormOnChange({
|
|
115
|
+
...latestDfContent,
|
|
116
|
+
fecha: { value: "2026-10-17", origin: "user" },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
await act(async () => dynamicFormOnChange({ ...latestDfContent }));
|
|
120
|
+
|
|
121
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("still validates dates on creation", async () => {
|
|
125
|
+
const rendered = render(
|
|
126
|
+
<StructureNotesEdit {...props} latestDfContent={{}} />,
|
|
127
|
+
);
|
|
128
|
+
await waitForLoad(rendered);
|
|
129
|
+
|
|
130
|
+
await act(async () => dynamicFormOnChange(latestDfContent));
|
|
131
|
+
|
|
132
|
+
expect(await rendered.findByText(/save/i)).toBeDisabled();
|
|
133
|
+
});
|
|
30
134
|
});
|
|
31
135
|
|
|
32
136
|
it("matches the latest snapshot", async () => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { act } from "@testing-library/react";
|
|
2
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
3
|
+
import DynamicForm from "@truedat/df/components/DynamicForm";
|
|
4
|
+
import { SystemForm } from "../SystemForm";
|
|
5
|
+
|
|
6
|
+
jest.mock("@truedat/df/components/DynamicForm", () => jest.fn(() => null));
|
|
7
|
+
jest.mock("@truedat/df/templates/components/TemplateLoader", () => () => null);
|
|
8
|
+
|
|
9
|
+
const content = { date: { value: "2025-01-01", origin: "user" } };
|
|
10
|
+
const template = {
|
|
11
|
+
name: "system",
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
name: "dates",
|
|
15
|
+
fields: [
|
|
16
|
+
{
|
|
17
|
+
name: "date",
|
|
18
|
+
type: "date",
|
|
19
|
+
cardinality: "?",
|
|
20
|
+
restrictions: { max_date: { mode: "static", value: "2024-01-01" } },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
const system = {
|
|
27
|
+
id: 1,
|
|
28
|
+
name: "System",
|
|
29
|
+
external_id: "system",
|
|
30
|
+
dynamic_content: content,
|
|
31
|
+
};
|
|
32
|
+
const formProps = () => DynamicForm.mock.calls.at(-1)[0];
|
|
33
|
+
|
|
34
|
+
describe("<SystemForm /> date restrictions", () => {
|
|
35
|
+
beforeEach(() => jest.clearAllMocks());
|
|
36
|
+
|
|
37
|
+
it("passes persisted content to the dynamic form", async () => {
|
|
38
|
+
const rendered = render(
|
|
39
|
+
<SystemForm
|
|
40
|
+
system={system}
|
|
41
|
+
template={template}
|
|
42
|
+
createSystem={jest.fn()}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
await waitForLoad(rendered);
|
|
46
|
+
expect(formProps().originalContent).toEqual(content);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("allows unchanged persisted dates outside the current range", async () => {
|
|
50
|
+
const rendered = render(
|
|
51
|
+
<SystemForm
|
|
52
|
+
system={system}
|
|
53
|
+
template={template}
|
|
54
|
+
createSystem={jest.fn()}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
await waitForLoad(rendered);
|
|
58
|
+
expect(
|
|
59
|
+
rendered.getByRole("button", { name: "actions.update" }),
|
|
60
|
+
).toBeEnabled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects changed dates outside the current range", async () => {
|
|
64
|
+
const rendered = render(
|
|
65
|
+
<SystemForm
|
|
66
|
+
system={system}
|
|
67
|
+
template={template}
|
|
68
|
+
createSystem={jest.fn()}
|
|
69
|
+
/>,
|
|
70
|
+
);
|
|
71
|
+
await waitForLoad(rendered);
|
|
72
|
+
act(() =>
|
|
73
|
+
formProps().onChange({ date: { value: "2025-02-01", origin: "user" } }),
|
|
74
|
+
);
|
|
75
|
+
expect(
|
|
76
|
+
rendered.getByRole("button", { name: "actions.update" }),
|
|
77
|
+
).toBeDisabled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("keeps the edit baseline after content changes", async () => {
|
|
81
|
+
const rendered = render(
|
|
82
|
+
<SystemForm
|
|
83
|
+
system={system}
|
|
84
|
+
template={template}
|
|
85
|
+
createSystem={jest.fn()}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
await waitForLoad(rendered);
|
|
89
|
+
act(() =>
|
|
90
|
+
formProps().onChange({ date: { value: "2023-01-01", origin: "user" } }),
|
|
91
|
+
);
|
|
92
|
+
expect(formProps().originalContent).toEqual(content);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does not supply an original for creation", async () => {
|
|
96
|
+
const rendered = render(
|
|
97
|
+
<SystemForm template={template} createSystem={jest.fn()} />,
|
|
98
|
+
);
|
|
99
|
+
await waitForLoad(rendered);
|
|
100
|
+
expect(formProps().originalContent).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { doStructureNoteAction } from "../../routines";
|
|
2
|
+
import { ddMessage, initialState } from "../ddMessage";
|
|
3
|
+
|
|
4
|
+
describe("reducers: ddMessage on a failed structure note action", () => {
|
|
5
|
+
it("surfaces the backend's field error", () => {
|
|
6
|
+
const state = ddMessage(initialState, {
|
|
7
|
+
type: doStructureNoteAction.FAILURE,
|
|
8
|
+
payload: {
|
|
9
|
+
status: 422,
|
|
10
|
+
data: {
|
|
11
|
+
errors: {
|
|
12
|
+
fecha: ["2026-06-30 is not within the allowed range"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(state.error).toBe(true);
|
|
19
|
+
expect(state.header).toBe("alert.doStructureNoteAction.failed.header");
|
|
20
|
+
expect(state.content).toBe("2026-06-30 is not within the allowed range");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("names every field that failed", () => {
|
|
24
|
+
const state = ddMessage(initialState, {
|
|
25
|
+
type: doStructureNoteAction.FAILURE,
|
|
26
|
+
payload: {
|
|
27
|
+
status: 422,
|
|
28
|
+
data: {
|
|
29
|
+
errors: {
|
|
30
|
+
fecha: ["2026-06-30 is not within the allowed range"],
|
|
31
|
+
nombre: ["can't be blank"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(state.content).toContain(
|
|
38
|
+
"2026-06-30 is not within the allowed range",
|
|
39
|
+
);
|
|
40
|
+
expect(state.content).toContain("can't be blank");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("still falls back to the header when the payload carries no errors", () => {
|
|
44
|
+
const state = ddMessage(initialState, {
|
|
45
|
+
type: doStructureNoteAction.FAILURE,
|
|
46
|
+
payload: { status: 500, data: {} },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(state.error).toBe(true);
|
|
50
|
+
expect(state.header).toBe("alert.doStructureNoteAction.failed.header");
|
|
51
|
+
expect(state.content).toBeFalsy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps a non-validation error message when the backend sends one", () => {
|
|
55
|
+
const state = ddMessage(initialState, {
|
|
56
|
+
type: doStructureNoteAction.FAILURE,
|
|
57
|
+
payload: { status: 400, data: { error: "structure note is locked" } },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(state.content).toBe("structure note is locked");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("passes structured date errors to the shared Alert", () => {
|
|
64
|
+
const error = {
|
|
65
|
+
name: "structureNote.error.errors.date_restrictions.out_of_range",
|
|
66
|
+
field: "fecha max fija",
|
|
67
|
+
values: { limit: "2026-10-15" },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const state = ddMessage(initialState, {
|
|
71
|
+
type: doStructureNoteAction.FAILURE,
|
|
72
|
+
payload: { status: 422, data: { errors: [error] } },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(state.errorList).toEqual([error]);
|
|
76
|
+
expect(state.content).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -132,11 +132,26 @@ const ddMessage = (state = initialState, { type, payload }) => {
|
|
|
132
132
|
error: true,
|
|
133
133
|
header: "alert.doStructureNoteAction.failed.header",
|
|
134
134
|
icon: "attention",
|
|
135
|
-
|
|
135
|
+
...noteActionErrorMessage(payload),
|
|
136
136
|
};
|
|
137
137
|
default:
|
|
138
138
|
return state;
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
const noteActionErrorMessage = (payload) => {
|
|
143
|
+
const errors = payload?.data?.errors;
|
|
144
|
+
|
|
145
|
+
if (Array.isArray(errors)) {
|
|
146
|
+
return { errorList: errors };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (errors && typeof errors === "object") {
|
|
150
|
+
const messages = Object.values(errors).flat().filter(Boolean);
|
|
151
|
+
if (messages.length) return { content: messages.join(" ") };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { content: payload?.data?.error };
|
|
155
|
+
};
|
|
156
|
+
|
|
142
157
|
export { ddMessage };
|