@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "8.13.4",
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.4",
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": "4ec142f1c856aa669b232b12250c800a19ab9ace"
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 submitDisabled = !template;
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(validateContent(template), _.isEmpty);
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
- content: payload?.data?.error,
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 };