@truedat/dd 8.12.4 → 8.12.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/api/__tests__/queries.spec.js +17 -8
- package/src/api/queries.js +1 -1
- package/src/components/StructureNoteActions.js +10 -1
- package/src/components/StructureNotes.js +42 -6
- package/src/components/StructureNotesEdit.js +38 -8
- package/src/components/__tests__/StructureNoteActions.spec.js +96 -0
- package/src/components/__tests__/StructureNotes.spec.js +170 -1
- package/src/components/__tests__/StructureNotesEdit.spec.js +185 -1
- package/src/components/__tests__/__snapshots__/StructureNoteActions.spec.js.snap +2 -1
- package/src/components/__tests__/__snapshots__/StructureNotesEdit.spec.js.snap +3 -0
- package/src/reducers/__tests__/ddMessage.spec.js +48 -0
- package/src/reducers/__tests__/structureRedirect.spec.js +22 -0
- package/src/reducers/ddMessage.js +32 -0
- package/src/reducers/structureRedirect.js +7 -2
- package/src/selectors/__tests__/messages.spec.js +14 -0
- package/src/selectors/messages.js +30 -0
- package/src/utils/__tests__/structureNoteActions.spec.js +196 -0
- package/src/utils/structureNoteActions.js +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.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.12.
|
|
57
|
+
"@truedat/test": "8.12.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": "fe7375c2d3c6ebdaf3dacd985f56433eda477712"
|
|
94
94
|
}
|
|
@@ -24,18 +24,27 @@ const linksField = (field) =>
|
|
|
24
24
|
(s) => s.kind === "Field" && s.name.value === "links",
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const expectResourceType = (field, value) => {
|
|
28
|
+
const resourceType = linksField(field).arguments.find(
|
|
29
|
+
(argument) => argument.name.value === "resource_type",
|
|
30
|
+
);
|
|
31
|
+
expect(resourceType.value.value).toBe(value);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const expectRelationLinks = (document, childrenResourceType) => {
|
|
28
35
|
const parents = fieldAtPath(document, PARENT_PATH);
|
|
29
36
|
const children = fieldAtPath(document, CHILD_PATH);
|
|
30
37
|
|
|
31
38
|
expect(selectionNames(parents)).toEqual(expect.arrayContaining(["links"]));
|
|
32
39
|
expect(selectionNames(children)).toEqual(expect.arrayContaining(["links"]));
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
expectResourceType(parents, "business_concept");
|
|
42
|
+
|
|
43
|
+
if (childrenResourceType) {
|
|
44
|
+
expectResourceType(children, childrenResourceType);
|
|
45
|
+
} else {
|
|
46
|
+
expect(linksField(children).arguments).toEqual([]);
|
|
47
|
+
}
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
describe("LEGACY_DATA_STRUCTURE_VERSION_QUERY", () => {
|
|
@@ -46,10 +55,10 @@ describe("LEGACY_DATA_STRUCTURE_VERSION_QUERY", () => {
|
|
|
46
55
|
|
|
47
56
|
describe("buildDSVQuery", () => {
|
|
48
57
|
it("requests concept links on parent and child structure relations", () => {
|
|
49
|
-
expectRelationLinks(buildDSVQuery(false));
|
|
58
|
+
expectRelationLinks(buildDSVQuery(false), "business_concept");
|
|
50
59
|
});
|
|
51
60
|
|
|
52
61
|
it("requests concept links on parent and child structure relations when loading metadata", () => {
|
|
53
|
-
expectRelationLinks(buildDSVQuery(true));
|
|
62
|
+
expectRelationLinks(buildDSVQuery(true), "business_concept");
|
|
54
63
|
});
|
|
55
64
|
});
|
package/src/api/queries.js
CHANGED
|
@@ -7,6 +7,7 @@ import { FormattedMessage } from "react-intl";
|
|
|
7
7
|
import { ConfirmModal, GroupActions } from "@truedat/core/components";
|
|
8
8
|
import { linkTo } from "@truedat/core/routes";
|
|
9
9
|
import { doStructureNoteAction } from "../routines";
|
|
10
|
+
import { canPerformAction } from "../utils/structureNoteActions";
|
|
10
11
|
|
|
11
12
|
const ModalDelete = ({
|
|
12
13
|
className,
|
|
@@ -67,6 +68,8 @@ export const StructureNoteActions = ({
|
|
|
67
68
|
doStructureNoteAction,
|
|
68
69
|
actions,
|
|
69
70
|
structure,
|
|
71
|
+
template,
|
|
72
|
+
content,
|
|
70
73
|
}) => {
|
|
71
74
|
const actionToIcon = {
|
|
72
75
|
deleted: "trash alternate outline",
|
|
@@ -88,6 +91,7 @@ export const StructureNoteActions = ({
|
|
|
88
91
|
_.toPairs,
|
|
89
92
|
_.reject(([action, _data]) => !_.get(action)(actionToIcon)),
|
|
90
93
|
_.map(([action, data]) => {
|
|
94
|
+
const allowed = canPerformAction(action, content, template);
|
|
91
95
|
const baseAction = {
|
|
92
96
|
key: action,
|
|
93
97
|
icon: _.get(action)(actionToIcon),
|
|
@@ -100,8 +104,11 @@ export const StructureNoteActions = ({
|
|
|
100
104
|
),
|
|
101
105
|
as: actionToAs(action),
|
|
102
106
|
to: actionToTo(action),
|
|
107
|
+
className: "td-structure-note-action",
|
|
108
|
+
disabled: !allowed,
|
|
109
|
+
...(!allowed ? { style: { opacity: 0.45 } } : {}),
|
|
103
110
|
onClick:
|
|
104
|
-
action === "edited" || withModal(action)
|
|
111
|
+
action === "edited" || withModal(action) || !allowed
|
|
105
112
|
? undefined
|
|
106
113
|
: () =>
|
|
107
114
|
doStructureNoteAction({
|
|
@@ -135,6 +142,8 @@ StructureNoteActions.propTypes = {
|
|
|
135
142
|
doStructureNoteAction: PropTypes.func,
|
|
136
143
|
actions: PropTypes.object.isRequired,
|
|
137
144
|
structure: PropTypes.object.isRequired,
|
|
145
|
+
template: PropTypes.object,
|
|
146
|
+
content: PropTypes.object,
|
|
138
147
|
};
|
|
139
148
|
|
|
140
149
|
export default connect(null, { doStructureNoteAction })(StructureNoteActions);
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import { lazy, useState } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
|
-
import {
|
|
4
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
5
5
|
import Moment from "react-moment";
|
|
6
6
|
import { connect } from "react-redux";
|
|
7
7
|
import { Link } from "react-router";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
Segment,
|
|
11
|
+
Message,
|
|
12
|
+
Loader,
|
|
13
|
+
Label,
|
|
14
|
+
Icon,
|
|
15
|
+
} from "semantic-ui-react";
|
|
9
16
|
import { linkTo } from "@truedat/core/routes";
|
|
10
17
|
import {
|
|
11
18
|
getActiveStructureNotes,
|
|
12
19
|
getVersionedStructureNotes,
|
|
13
20
|
} from "../selectors/getSortedStructureNotes";
|
|
21
|
+
import { getMissingMandatoryFields } from "../utils/structureNoteActions";
|
|
14
22
|
|
|
15
23
|
import StructureNoteActions from "./StructureNoteActions";
|
|
16
24
|
|
|
@@ -27,6 +35,10 @@ const StructureNote = ({ structureNote, template, structure, notesSize }) => {
|
|
|
27
35
|
structureNote?.status !== "published" ||
|
|
28
36
|
!_.isEmpty(actions) ||
|
|
29
37
|
notesSize > 1;
|
|
38
|
+
const noteContent =
|
|
39
|
+
structureNote?.dynamic_content || structureNote?.df_content || {};
|
|
40
|
+
const missingFields = getMissingMandatoryFields(noteContent, template);
|
|
41
|
+
const showMissingLabel = !_.isEmpty(missingFields);
|
|
30
42
|
return (
|
|
31
43
|
<Segment>
|
|
32
44
|
<div
|
|
@@ -59,6 +71,21 @@ const StructureNote = ({ structureNote, template, structure, notesSize }) => {
|
|
|
59
71
|
/>
|
|
60
72
|
</Label>
|
|
61
73
|
)}
|
|
74
|
+
{showMissingLabel && (
|
|
75
|
+
<Label
|
|
76
|
+
basic
|
|
77
|
+
color="orange"
|
|
78
|
+
size="tiny"
|
|
79
|
+
className="td-missing-mandatory-label"
|
|
80
|
+
>
|
|
81
|
+
<Icon name="warning sign" />
|
|
82
|
+
<FormattedMessage
|
|
83
|
+
id="structure.notes.actions.missing_mandatory"
|
|
84
|
+
defaultMessage="Missing required fields: {fields}"
|
|
85
|
+
values={{ fields: missingFields.join(", ") }}
|
|
86
|
+
/>
|
|
87
|
+
</Label>
|
|
88
|
+
)}
|
|
62
89
|
</>
|
|
63
90
|
)}
|
|
64
91
|
</div>
|
|
@@ -70,7 +97,12 @@ const StructureNote = ({ structureNote, template, structure, notesSize }) => {
|
|
|
70
97
|
/>
|
|
71
98
|
</Button>
|
|
72
99
|
)}
|
|
73
|
-
<StructureNoteActions
|
|
100
|
+
<StructureNoteActions
|
|
101
|
+
actions={actions}
|
|
102
|
+
structure={structure}
|
|
103
|
+
template={template}
|
|
104
|
+
content={noteContent}
|
|
105
|
+
/>
|
|
74
106
|
</div>
|
|
75
107
|
</div>
|
|
76
108
|
{!collapsed && (
|
|
@@ -99,6 +131,7 @@ export const StructureNotes = ({
|
|
|
99
131
|
template,
|
|
100
132
|
}) => {
|
|
101
133
|
const { formatMessage } = useIntl();
|
|
134
|
+
|
|
102
135
|
const [isShowingVersioned, setShowingVersioned] = useState(false);
|
|
103
136
|
const notesSize = _.size(activeStructureNotes);
|
|
104
137
|
|
|
@@ -163,6 +196,11 @@ StructureNotes.propTypes = {
|
|
|
163
196
|
template: PropTypes.object,
|
|
164
197
|
};
|
|
165
198
|
|
|
199
|
+
export const findTemplate = ({
|
|
200
|
+
templates,
|
|
201
|
+
structure: { structure_type } = {},
|
|
202
|
+
}) => _.find(_.propEq("id", _.prop("template_id")(structure_type)))(templates);
|
|
203
|
+
|
|
166
204
|
const mapStateToProps = (state) => {
|
|
167
205
|
const { templates, structure, structureNotes } = state;
|
|
168
206
|
return {
|
|
@@ -170,9 +208,7 @@ const mapStateToProps = (state) => {
|
|
|
170
208
|
versionedStructureNotes: getVersionedStructureNotes(state),
|
|
171
209
|
structureNotes,
|
|
172
210
|
structure,
|
|
173
|
-
template:
|
|
174
|
-
_.propEq("id", _.path("structure_type.template_id")(structure)),
|
|
175
|
-
)(templates),
|
|
211
|
+
template: findTemplate({ templates, structure }),
|
|
176
212
|
};
|
|
177
213
|
};
|
|
178
214
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import { lazy, useEffect, useState } from "react";
|
|
2
|
+
import { lazy, useEffect, useRef, useState } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { connect } from "react-redux";
|
|
5
5
|
import { Button, Form, Segment } from "semantic-ui-react";
|
|
6
6
|
import { useIntl } from "react-intl";
|
|
7
7
|
import { HistoryBackButton } from "@truedat/core/components";
|
|
8
8
|
import { selectTemplate, selectDomains } from "@truedat/df/routines";
|
|
9
|
-
import { applyTemplate
|
|
9
|
+
import { applyTemplate } 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";
|
|
@@ -17,6 +17,16 @@ const TemplateLoader = lazy(
|
|
|
17
17
|
() => import("@truedat/df/templates/components/TemplateLoader"),
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
+
const isInformed = _.flow(
|
|
21
|
+
_.cond([
|
|
22
|
+
[_.isArray, _.isEmpty],
|
|
23
|
+
[_.isObject, _.isEmpty],
|
|
24
|
+
[_.isString, _.isEmpty],
|
|
25
|
+
[_.stubTrue, _.isNil],
|
|
26
|
+
]),
|
|
27
|
+
(value) => !value,
|
|
28
|
+
);
|
|
29
|
+
|
|
20
30
|
export const StructureNotesEdit = ({
|
|
21
31
|
applyTemplate,
|
|
22
32
|
structure,
|
|
@@ -28,8 +38,9 @@ export const StructureNotesEdit = ({
|
|
|
28
38
|
latestDfContent,
|
|
29
39
|
}) => {
|
|
30
40
|
const [content, setContent] = useState(latestDfContent);
|
|
31
|
-
|
|
41
|
+
const explicitEmptiesRef = useRef({});
|
|
32
42
|
const isModification = !_.isEmpty(latestDfContent);
|
|
43
|
+
const initializedRef = useRef(false);
|
|
33
44
|
|
|
34
45
|
useEffect(() => {
|
|
35
46
|
selectDomains(structure.domain_ids);
|
|
@@ -37,18 +48,37 @@ export const StructureNotesEdit = ({
|
|
|
37
48
|
}, [template, structure, selectTemplate, selectDomains]);
|
|
38
49
|
|
|
39
50
|
useEffect(() => {
|
|
40
|
-
if (template && latestDfContent) {
|
|
51
|
+
if (!initializedRef.current && template && latestDfContent) {
|
|
52
|
+
initializedRef.current = true;
|
|
41
53
|
setContent(applyTemplate(latestDfContent, structure.domain_id));
|
|
42
54
|
}
|
|
43
55
|
}, [applyTemplate, template, latestDfContent, structure.domain_id]);
|
|
44
56
|
|
|
45
57
|
const { formatMessage, locale } = useIntl();
|
|
46
58
|
|
|
47
|
-
const submitDisabled =
|
|
48
|
-
|
|
59
|
+
const submitDisabled = !template;
|
|
60
|
+
|
|
61
|
+
const handleFormChange = (newContent) => {
|
|
62
|
+
const clearedFields = _.flow(
|
|
63
|
+
_.pickBy((value) => isInformed(value.value)),
|
|
64
|
+
_.omit(Object.keys(newContent)),
|
|
65
|
+
_.mapValues(() => ({ value: "", origin: "user" })),
|
|
66
|
+
)(content);
|
|
67
|
+
|
|
68
|
+
explicitEmptiesRef.current = {
|
|
69
|
+
..._.omit(
|
|
70
|
+
Object.keys(_.pickBy((value) => isInformed(value.value))(newContent)),
|
|
71
|
+
)(explicitEmptiesRef.current),
|
|
72
|
+
...clearedFields,
|
|
73
|
+
};
|
|
74
|
+
setContent(newContent);
|
|
75
|
+
};
|
|
49
76
|
|
|
50
77
|
const handleSubmit = () => {
|
|
51
|
-
const df_content =
|
|
78
|
+
const df_content = {
|
|
79
|
+
...applyTemplate(content, structure.domain_id),
|
|
80
|
+
...explicitEmptiesRef.current,
|
|
81
|
+
};
|
|
52
82
|
const draftNoteAction = _.flow(
|
|
53
83
|
_.get("structureNotes"),
|
|
54
84
|
_.find({ status: "draft" }),
|
|
@@ -96,7 +126,7 @@ export const StructureNotesEdit = ({
|
|
|
96
126
|
/>
|
|
97
127
|
<Form>
|
|
98
128
|
<DynamicForm
|
|
99
|
-
onChange={
|
|
129
|
+
onChange={handleFormChange}
|
|
100
130
|
content={content}
|
|
101
131
|
isModification={isModification}
|
|
102
132
|
/>
|
|
@@ -202,4 +202,100 @@ describe("<StructureNoteActions />", () => {
|
|
|
202
202
|
.querySelector(".clipboard.check.icon"),
|
|
203
203
|
).toBeInTheDocument();
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
it("disables 'pending_approval' when mandatory fields are missing", async () => {
|
|
207
|
+
const doStructureNoteAction = jest.fn();
|
|
208
|
+
const template = {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
name: "g1",
|
|
212
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
const props = {
|
|
217
|
+
doStructureNoteAction,
|
|
218
|
+
actions: {
|
|
219
|
+
pending_approval: {
|
|
220
|
+
href: "url",
|
|
221
|
+
input: { status: "pending_approval" },
|
|
222
|
+
method: "PATCH",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
structure: { id: 1 },
|
|
226
|
+
template,
|
|
227
|
+
content: {},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const rendered = render(<StructureNoteActions {...props} />);
|
|
231
|
+
await waitForLoad(rendered);
|
|
232
|
+
|
|
233
|
+
const item = (await rendered.findByText(/pending_approval/i)).closest(
|
|
234
|
+
".item",
|
|
235
|
+
);
|
|
236
|
+
expect(item).toHaveClass("disabled");
|
|
237
|
+
expect(doStructureNoteAction).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("disables 'published' when mandatory fields are missing", async () => {
|
|
241
|
+
const doStructureNoteAction = jest.fn();
|
|
242
|
+
const template = {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
name: "g1",
|
|
246
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
const props = {
|
|
251
|
+
doStructureNoteAction,
|
|
252
|
+
actions: {
|
|
253
|
+
published: {
|
|
254
|
+
href: "url",
|
|
255
|
+
input: { status: "published" },
|
|
256
|
+
method: "PATCH",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
structure: { id: 1 },
|
|
260
|
+
template,
|
|
261
|
+
content: {},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const rendered = render(<StructureNoteActions {...props} />);
|
|
265
|
+
await waitForLoad(rendered);
|
|
266
|
+
|
|
267
|
+
const item = (await rendered.findByText(/published/i)).closest(".item");
|
|
268
|
+
expect(item).toHaveClass("disabled");
|
|
269
|
+
expect(doStructureNoteAction).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("keeps 'edited' enabled when mandatory fields are missing", async () => {
|
|
273
|
+
const doStructureNoteAction = jest.fn();
|
|
274
|
+
const template = {
|
|
275
|
+
content: [
|
|
276
|
+
{
|
|
277
|
+
name: "g1",
|
|
278
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
const props = {
|
|
283
|
+
doStructureNoteAction,
|
|
284
|
+
actions: {
|
|
285
|
+
edited: {
|
|
286
|
+
href: "edit-url",
|
|
287
|
+
method: "GET",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
structure: { id: 1 },
|
|
291
|
+
template,
|
|
292
|
+
content: {},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const rendered = render(<StructureNoteActions {...props} />);
|
|
296
|
+
await waitForLoad(rendered);
|
|
297
|
+
|
|
298
|
+
const item = (await rendered.findByText(/edited/i)).closest(".item");
|
|
299
|
+
expect(item).not.toHaveClass("disabled");
|
|
300
|
+
});
|
|
205
301
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import userEvent from "@testing-library/user-event";
|
|
2
2
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
3
|
-
import { StructureNotes } from "../StructureNotes";
|
|
3
|
+
import { StructureNotes, findTemplate } from "../StructureNotes";
|
|
4
4
|
|
|
5
5
|
jest.mock("@truedat/df/components/DynamicFormViewer", () => {
|
|
6
6
|
const DynamicFormViewer = () => <div>DynamicFormViewer</div>;
|
|
@@ -111,4 +111,173 @@ describe("<StructureNotes />", () => {
|
|
|
111
111
|
);
|
|
112
112
|
expect(rendered.container).toMatchSnapshot();
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
it("disables publish/pending_approval when df_content has missing mandatory fields", async () => {
|
|
116
|
+
const props = {
|
|
117
|
+
structure: { id: 1 },
|
|
118
|
+
structureNotes: { _actions: {}, loading: false },
|
|
119
|
+
activeStructureNotes: [
|
|
120
|
+
{
|
|
121
|
+
id: 4,
|
|
122
|
+
status: "draft",
|
|
123
|
+
dynamic_content: null,
|
|
124
|
+
df_content: null,
|
|
125
|
+
version: "4",
|
|
126
|
+
_actions: {
|
|
127
|
+
pending_approval: {
|
|
128
|
+
href: "url",
|
|
129
|
+
input: { status: "pending_approval" },
|
|
130
|
+
method: "PATCH",
|
|
131
|
+
},
|
|
132
|
+
published: {
|
|
133
|
+
href: "url",
|
|
134
|
+
input: { status: "published" },
|
|
135
|
+
method: "PATCH",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
versionedStructureNotes: [],
|
|
141
|
+
template: {
|
|
142
|
+
id: 2,
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
name: "g1",
|
|
146
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
scope: "dd",
|
|
150
|
+
},
|
|
151
|
+
doStructureNoteAction: () => {},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const rendered = render(<StructureNotes {...props} />);
|
|
155
|
+
await waitForLoad(rendered);
|
|
156
|
+
|
|
157
|
+
const pendingItem = (
|
|
158
|
+
await rendered.findByText(/pending_approval/i)
|
|
159
|
+
).closest(".item");
|
|
160
|
+
expect(pendingItem).toHaveClass("disabled");
|
|
161
|
+
|
|
162
|
+
const publishedItem = (await rendered.findByText(/published/i)).closest(
|
|
163
|
+
".item",
|
|
164
|
+
);
|
|
165
|
+
expect(publishedItem).toHaveClass("disabled");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("shows missing mandatory label in header when draft has missing fields", async () => {
|
|
169
|
+
const props = {
|
|
170
|
+
structure: { id: 1 },
|
|
171
|
+
structureNotes: { _actions: {}, loading: false },
|
|
172
|
+
activeStructureNotes: [
|
|
173
|
+
{
|
|
174
|
+
id: 4,
|
|
175
|
+
status: "draft",
|
|
176
|
+
dynamic_content: null,
|
|
177
|
+
df_content: null,
|
|
178
|
+
version: "4",
|
|
179
|
+
_actions: {
|
|
180
|
+
published: {
|
|
181
|
+
href: "url",
|
|
182
|
+
input: { status: "published" },
|
|
183
|
+
method: "PATCH",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
versionedStructureNotes: [],
|
|
189
|
+
template: {
|
|
190
|
+
id: 2,
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
name: "g1",
|
|
194
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
scope: "dd",
|
|
198
|
+
},
|
|
199
|
+
doStructureNoteAction: () => {},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const rendered = render(<StructureNotes {...props} />);
|
|
203
|
+
await waitForLoad(rendered);
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
await rendered.findByText(/missing required fields/i),
|
|
207
|
+
).toBeInTheDocument();
|
|
208
|
+
expect(
|
|
209
|
+
rendered.container.querySelector(".td-missing-mandatory-label"),
|
|
210
|
+
).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("does not show missing mandatory label when all fields are complete", async () => {
|
|
214
|
+
const props = {
|
|
215
|
+
structure: { id: 1 },
|
|
216
|
+
structureNotes: { _actions: {}, loading: false },
|
|
217
|
+
activeStructureNotes: [
|
|
218
|
+
{
|
|
219
|
+
id: 4,
|
|
220
|
+
status: "draft",
|
|
221
|
+
dynamic_content: { required: { value: "ok", origin: "user" } },
|
|
222
|
+
df_content: { required: { value: "ok", origin: "user" } },
|
|
223
|
+
version: "4",
|
|
224
|
+
_actions: {
|
|
225
|
+
published: {
|
|
226
|
+
href: "url",
|
|
227
|
+
input: { status: "published" },
|
|
228
|
+
method: "PATCH",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
versionedStructureNotes: [],
|
|
234
|
+
template: {
|
|
235
|
+
id: 2,
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
name: "g1",
|
|
239
|
+
fields: [{ name: "required", type: "string", cardinality: "1" }],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
scope: "dd",
|
|
243
|
+
},
|
|
244
|
+
doStructureNoteAction: () => {},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const rendered = render(<StructureNotes {...props} />);
|
|
248
|
+
await waitForLoad(rendered);
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
rendered.queryByText(/missing required fields/i),
|
|
252
|
+
).not.toBeInTheDocument();
|
|
253
|
+
expect(
|
|
254
|
+
rendered.container.querySelector(".td-missing-mandatory-label"),
|
|
255
|
+
).not.toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("resolves the template from structure_type.template_id", () => {
|
|
259
|
+
const templates = [
|
|
260
|
+
{ id: 1, name: "other" },
|
|
261
|
+
{ id: 2, name: "Business Concept", content: [] },
|
|
262
|
+
];
|
|
263
|
+
expect(
|
|
264
|
+
findTemplate({
|
|
265
|
+
templates,
|
|
266
|
+
structure: { structure_type: { template_id: 2 } },
|
|
267
|
+
}),
|
|
268
|
+
).toEqual(templates[1]);
|
|
269
|
+
expect(
|
|
270
|
+
findTemplate({
|
|
271
|
+
templates,
|
|
272
|
+
structure: { structure_type: { template_id: 1 } },
|
|
273
|
+
}),
|
|
274
|
+
).toEqual(templates[0]);
|
|
275
|
+
expect(
|
|
276
|
+
findTemplate({
|
|
277
|
+
templates,
|
|
278
|
+
structure: { structure_type: { template: { id: 2 } } },
|
|
279
|
+
}),
|
|
280
|
+
).toBeUndefined();
|
|
281
|
+
expect(findTemplate({ templates, structure: {} })).toBeUndefined();
|
|
282
|
+
});
|
|
114
283
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import userEvent from "@testing-library/user-event";
|
|
2
|
-
import { waitFor } from "@testing-library/react";
|
|
2
|
+
import { act, waitFor } from "@testing-library/react";
|
|
3
3
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
4
4
|
import { applyTemplate } from "@truedat/df/utils";
|
|
5
5
|
import SuggestionsWidget from "@truedat/ai/components/suggestions/SuggestionsWidget";
|
|
@@ -10,9 +10,23 @@ jest.mock("@truedat/ai/components/suggestions/SuggestionsWidget", () => ({
|
|
|
10
10
|
default: jest.fn(() => null),
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
+
let dynamicFormOnChange;
|
|
14
|
+
let dynamicFormContent;
|
|
15
|
+
|
|
16
|
+
jest.mock("@truedat/df/components/DynamicForm", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: ({ onChange, content }) => {
|
|
19
|
+
dynamicFormOnChange = onChange;
|
|
20
|
+
dynamicFormContent = content;
|
|
21
|
+
return <div data-testid="dynamic-form" />;
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
13
25
|
describe("<StructureNotesEdit />", () => {
|
|
14
26
|
beforeEach(() => {
|
|
15
27
|
jest.clearAllMocks();
|
|
28
|
+
dynamicFormOnChange = undefined;
|
|
29
|
+
dynamicFormContent = undefined;
|
|
16
30
|
});
|
|
17
31
|
|
|
18
32
|
it("matches the latest snapshot", async () => {
|
|
@@ -121,4 +135,174 @@ describe("<StructureNotesEdit />", () => {
|
|
|
121
135
|
}),
|
|
122
136
|
);
|
|
123
137
|
});
|
|
138
|
+
|
|
139
|
+
it("preserves explicit empty fields when a field is cleared", async () => {
|
|
140
|
+
const doStructureNoteAction = jest.fn();
|
|
141
|
+
const template = {
|
|
142
|
+
id: 2,
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
name: "test-group",
|
|
146
|
+
fields: [
|
|
147
|
+
{
|
|
148
|
+
name: "foo",
|
|
149
|
+
type: "string",
|
|
150
|
+
values: null,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "bar",
|
|
154
|
+
type: "string",
|
|
155
|
+
values: null,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const props = {
|
|
163
|
+
structure: { id: 1 },
|
|
164
|
+
template,
|
|
165
|
+
doStructureNoteAction,
|
|
166
|
+
selectTemplate: () => {},
|
|
167
|
+
selectDomain: () => {},
|
|
168
|
+
structureNotes: {
|
|
169
|
+
structureNotes: [],
|
|
170
|
+
},
|
|
171
|
+
latestDfContent: {
|
|
172
|
+
foo: { value: "foo value", origin: "user" },
|
|
173
|
+
bar: { value: "bar value", origin: "user" },
|
|
174
|
+
},
|
|
175
|
+
applyTemplate: applyTemplate(template),
|
|
176
|
+
selectDomains: jest.fn(),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
180
|
+
await waitForLoad(rendered);
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
dynamicFormOnChange({
|
|
184
|
+
foo: { value: "foo value", origin: "user" },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const user = userEvent.setup({ delay: null });
|
|
189
|
+
await user.click(await rendered.findByText(/save/i));
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(doStructureNoteAction.mock.calls.length).toBe(1);
|
|
193
|
+
});
|
|
194
|
+
expect(doStructureNoteAction).toHaveBeenCalledWith(
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
action: {
|
|
197
|
+
input: {
|
|
198
|
+
df_content: {
|
|
199
|
+
foo: { value: "foo value", origin: "user" },
|
|
200
|
+
bar: { value: "", origin: "user" },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
dataStructureId: 1,
|
|
205
|
+
redirect: true,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not reset local content when latestDfContent changes)", async () => {
|
|
211
|
+
const doStructureNoteAction = jest.fn();
|
|
212
|
+
const template = {
|
|
213
|
+
id: 2,
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
name: "test-group",
|
|
217
|
+
fields: [
|
|
218
|
+
{
|
|
219
|
+
name: "foo",
|
|
220
|
+
type: "string",
|
|
221
|
+
values: null,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const props = {
|
|
229
|
+
structure: { id: 1 },
|
|
230
|
+
template,
|
|
231
|
+
doStructureNoteAction,
|
|
232
|
+
selectTemplate: () => {},
|
|
233
|
+
selectDomain: () => {},
|
|
234
|
+
structureNotes: {
|
|
235
|
+
structureNotes: [],
|
|
236
|
+
},
|
|
237
|
+
latestDfContent: { foo: { value: "saved value", origin: "user" } },
|
|
238
|
+
applyTemplate: applyTemplate(template),
|
|
239
|
+
selectDomains: jest.fn(),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
243
|
+
await waitForLoad(rendered);
|
|
244
|
+
|
|
245
|
+
await act(async () => {
|
|
246
|
+
dynamicFormOnChange({
|
|
247
|
+
foo: { value: "local edit", origin: "user" },
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
expect(dynamicFormContent).toEqual({
|
|
251
|
+
foo: { value: "local edit", origin: "user" },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const updatedProps = {
|
|
255
|
+
...props,
|
|
256
|
+
latestDfContent: { foo: { value: "saved value", origin: "user" } },
|
|
257
|
+
};
|
|
258
|
+
await act(async () => {
|
|
259
|
+
rendered.rerender(<StructureNotesEdit {...updatedProps} />);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(dynamicFormContent).toEqual({
|
|
263
|
+
foo: { value: "local edit", origin: "user" },
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("Save button is enabled when mandatory fields are missing", async () => {
|
|
268
|
+
const doStructureNoteAction = jest.fn();
|
|
269
|
+
const template = {
|
|
270
|
+
id: 2,
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
name: "test-group",
|
|
274
|
+
fields: [
|
|
275
|
+
{
|
|
276
|
+
name: "required_field",
|
|
277
|
+
type: "string",
|
|
278
|
+
cardinality: "1",
|
|
279
|
+
values: null,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const props = {
|
|
287
|
+
structure: { id: 1 },
|
|
288
|
+
template,
|
|
289
|
+
doStructureNoteAction,
|
|
290
|
+
selectTemplate: () => {},
|
|
291
|
+
selectDomain: () => {},
|
|
292
|
+
structureNotes: {
|
|
293
|
+
structureNotes: [],
|
|
294
|
+
},
|
|
295
|
+
latestDfContent: {},
|
|
296
|
+
applyTemplate: applyTemplate(template),
|
|
297
|
+
selectDomains: jest.fn(),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
301
|
+
await waitForLoad(rendered);
|
|
302
|
+
|
|
303
|
+
const saveButton = await rendered.findByRole("button", {
|
|
304
|
+
name: /actions\.save/i,
|
|
305
|
+
});
|
|
306
|
+
expect(saveButton).not.toBeDisabled();
|
|
307
|
+
});
|
|
124
308
|
});
|
|
@@ -21,8 +21,9 @@ exports[`<StructureNoteActions /> matches the latest snapshot 1`] = `
|
|
|
21
21
|
>
|
|
22
22
|
<div
|
|
23
23
|
aria-checked="false"
|
|
24
|
+
aria-disabled="false"
|
|
24
25
|
aria-selected="true"
|
|
25
|
-
class="selected item td-icon-text-control"
|
|
26
|
+
class="selected item td-structure-note-action td-icon-text-control"
|
|
26
27
|
role="option"
|
|
27
28
|
style="pointer-events: all;"
|
|
28
29
|
>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
bulkUpdateStructures,
|
|
2
3
|
createReferenceDataset,
|
|
3
4
|
deleteReferenceDataset,
|
|
4
5
|
updateReferenceDataset,
|
|
@@ -74,6 +75,53 @@ describe("reducers: ddMessage", () => {
|
|
|
74
75
|
});
|
|
75
76
|
});
|
|
76
77
|
|
|
78
|
+
it("should handle the bulkUpdateStructures.FAILURE action with error details", () => {
|
|
79
|
+
const ddMsg = ddMessage(fooState, {
|
|
80
|
+
type: bulkUpdateStructures.FAILURE,
|
|
81
|
+
payload: { status: 422, data: { error: { message: "some message" } } },
|
|
82
|
+
});
|
|
83
|
+
expect(ddMsg).toMatchObject({
|
|
84
|
+
error: true,
|
|
85
|
+
header: "structures.bulkUpdateStructures.failed.header",
|
|
86
|
+
content: "some message",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle bulkUpdateStructures.FAILURE with string payload (timeout)", () => {
|
|
91
|
+
const ddMsg = ddMessage(fooState, {
|
|
92
|
+
type: bulkUpdateStructures.FAILURE,
|
|
93
|
+
payload: "timeout of 0ms exceeded",
|
|
94
|
+
});
|
|
95
|
+
expect(ddMsg).toMatchObject({
|
|
96
|
+
error: true,
|
|
97
|
+
header: "structures.bulkUpdateStructures.failed.timeout.header",
|
|
98
|
+
content: "timeout of 0ms exceeded",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle bulkUpdateStructures.FAILURE with string payload (generic network error)", () => {
|
|
103
|
+
const ddMsg = ddMessage(fooState, {
|
|
104
|
+
type: bulkUpdateStructures.FAILURE,
|
|
105
|
+
payload: "Network Error",
|
|
106
|
+
});
|
|
107
|
+
expect(ddMsg).toMatchObject({
|
|
108
|
+
error: true,
|
|
109
|
+
header: "structures.bulkUpdateStructures.failed.header",
|
|
110
|
+
content: "Network Error",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should handle bulkUpdateStructures.FAILURE with 504 status as timeout", () => {
|
|
115
|
+
const ddMsg = ddMessage(fooState, {
|
|
116
|
+
type: bulkUpdateStructures.FAILURE,
|
|
117
|
+
payload: { status: 504, data: {} },
|
|
118
|
+
});
|
|
119
|
+
expect(ddMsg).toMatchObject({
|
|
120
|
+
error: true,
|
|
121
|
+
header: "structures.bulkUpdateStructures.failed.timeout.header",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
77
125
|
it("should handle the createReferenceDataset.SUCCESS action", () => {
|
|
78
126
|
expect(ddMessage(fooState, createReferenceDataset.success())).toMatchObject(
|
|
79
127
|
{ header: "alert.createReferenceDataset.success.header" },
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { clearRedirect } from "@truedat/core/routines";
|
|
2
|
+
import { STRUCTURES } from "@truedat/core/routes";
|
|
2
3
|
import {
|
|
3
4
|
createReferenceDataset,
|
|
4
5
|
deleteReferenceDataset,
|
|
5
6
|
updateReferenceDataset,
|
|
6
7
|
updateStructureType,
|
|
7
8
|
updateStructure,
|
|
9
|
+
bulkUpdateStructures,
|
|
8
10
|
} from "../../routines";
|
|
9
11
|
import { structureRedirect, initialState } from "../structureRedirect";
|
|
10
12
|
|
|
@@ -69,4 +71,24 @@ describe("reducers: structureRedirect", () => {
|
|
|
69
71
|
),
|
|
70
72
|
).toBe("/referenceDatasets/42");
|
|
71
73
|
});
|
|
74
|
+
|
|
75
|
+
it("should redirect to STRUCTURES when bulkUpdateStructures.SUCCESS has no errors", () => {
|
|
76
|
+
const payload = { data: { errors: [] } };
|
|
77
|
+
expect(
|
|
78
|
+
structureRedirect(fooState, {
|
|
79
|
+
type: bulkUpdateStructures.SUCCESS,
|
|
80
|
+
payload,
|
|
81
|
+
}),
|
|
82
|
+
).toBe(STRUCTURES);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should not redirect when bulkUpdateStructures.SUCCESS has errors", () => {
|
|
86
|
+
const payload = { data: { errors: [{ message: "err" }] } };
|
|
87
|
+
expect(
|
|
88
|
+
structureRedirect(fooState, {
|
|
89
|
+
type: bulkUpdateStructures.SUCCESS,
|
|
90
|
+
payload,
|
|
91
|
+
}),
|
|
92
|
+
).toBe(fooState);
|
|
93
|
+
});
|
|
72
94
|
});
|
|
@@ -19,6 +19,38 @@ const ddMessage = (state = initialState, { type, payload }) => {
|
|
|
19
19
|
return initialState;
|
|
20
20
|
case bulkUpdateStructures.SUCCESS:
|
|
21
21
|
return getUploadAlertMessage(payload);
|
|
22
|
+
case bulkUpdateStructures.FAILURE: {
|
|
23
|
+
const isTimeout =
|
|
24
|
+
typeof payload === "string"
|
|
25
|
+
? /timeout|timed out|ECONNABORTED/i.test(payload)
|
|
26
|
+
: payload?.status === 504 ||
|
|
27
|
+
/timeout/i.test(payload?.data?.error?.message || "");
|
|
28
|
+
|
|
29
|
+
if (isTimeout) {
|
|
30
|
+
return {
|
|
31
|
+
error: true,
|
|
32
|
+
header: "structures.bulkUpdateStructures.failed.timeout.header",
|
|
33
|
+
icon: "attention",
|
|
34
|
+
content:
|
|
35
|
+
typeof payload === "string"
|
|
36
|
+
? payload
|
|
37
|
+
: payload?.data?.error?.message,
|
|
38
|
+
initialMaxListItems: 5,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
error: true,
|
|
44
|
+
header: "structures.bulkUpdateStructures.failed.header",
|
|
45
|
+
icon: "attention",
|
|
46
|
+
content:
|
|
47
|
+
typeof payload === "string"
|
|
48
|
+
? payload
|
|
49
|
+
: payload?.data?.error?.message ||
|
|
50
|
+
"structures.bulkUpdateStructures.failed.generic.content",
|
|
51
|
+
initialMaxListItems: 5,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
22
54
|
case uploadStructuresDomains.SUCCESS: {
|
|
23
55
|
const messagesImp = _.has("errors")(payload.data)
|
|
24
56
|
? _.flow(
|
|
@@ -47,8 +47,13 @@ export const structureRedirect = (
|
|
|
47
47
|
return initialState;
|
|
48
48
|
case updateStructure.SUCCESS:
|
|
49
49
|
return payload?.redirect || state;
|
|
50
|
-
case bulkUpdateStructures.SUCCESS:
|
|
51
|
-
|
|
50
|
+
case bulkUpdateStructures.SUCCESS: {
|
|
51
|
+
const errors = payload?.data?.errors;
|
|
52
|
+
if (!errors || errors.length === 0) {
|
|
53
|
+
return STRUCTURES;
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
52
57
|
case createSystem.SUCCESS: {
|
|
53
58
|
const id = _.path("data.id")(payload);
|
|
54
59
|
return linkTo.SYSTEM_STRUCTURES({ id });
|
|
@@ -53,4 +53,18 @@ describe("selectors: getUploadAlertMessage", () => {
|
|
|
53
53
|
"structures.bulkUpdateStructures.failed.note.only_draft_are_editable",
|
|
54
54
|
);
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("returns uploadFailed with errors array when status is 422", () => {
|
|
58
|
+
const data = { errors: [{ message: "some_error" }] };
|
|
59
|
+
const result = getUploadAlertMessage({ status: 422, data });
|
|
60
|
+
expect(result.error).toBe(true);
|
|
61
|
+
expect(result.header).toBe("structures.bulkUpdateStructures.failed.header");
|
|
62
|
+
expect(result.messages.length).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns uploadFailed when status is 500", () => {
|
|
66
|
+
const result = getUploadAlertMessage({ status: 500, data: {} });
|
|
67
|
+
expect(result.error).toBe(true);
|
|
68
|
+
expect(result.header).toBe("structures.bulkUpdateStructures.failed.header");
|
|
69
|
+
});
|
|
56
70
|
});
|
|
@@ -48,6 +48,32 @@ const uploadAccepted = () => {
|
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
const uploadFailed = (data) => ({
|
|
52
|
+
error: true,
|
|
53
|
+
header: "structures.bulkUpdateStructures.failed.header",
|
|
54
|
+
icon: "attention",
|
|
55
|
+
color: "red",
|
|
56
|
+
text: "",
|
|
57
|
+
messages: _.has("errors")(data)
|
|
58
|
+
? _.flow(
|
|
59
|
+
_.propOr([], "errors"),
|
|
60
|
+
_.map((error) => ({
|
|
61
|
+
id: `structures.bulkUpdateStructures.failed.note.${error.message}`,
|
|
62
|
+
fields: {
|
|
63
|
+
row: error.row,
|
|
64
|
+
id: error.id,
|
|
65
|
+
field: error.field,
|
|
66
|
+
externalId: error.external_id,
|
|
67
|
+
message: error.message,
|
|
68
|
+
},
|
|
69
|
+
defaultMessage: "structures.bulkUploadStructures.success.errors",
|
|
70
|
+
})),
|
|
71
|
+
)(data)
|
|
72
|
+
: [],
|
|
73
|
+
fields: data?.errors ? { count_errors: data.errors.length } : {},
|
|
74
|
+
initialMaxListItems: 5,
|
|
75
|
+
});
|
|
76
|
+
|
|
51
77
|
export const getUploadAlertMessage = ({ status, data }) => {
|
|
52
78
|
if (status === 202) {
|
|
53
79
|
return uploadAccepted();
|
|
@@ -55,4 +81,8 @@ export const getUploadAlertMessage = ({ status, data }) => {
|
|
|
55
81
|
if (status === 200) {
|
|
56
82
|
return uploadWithErrors(data);
|
|
57
83
|
}
|
|
84
|
+
if (status >= 400) {
|
|
85
|
+
return uploadFailed(data || {});
|
|
86
|
+
}
|
|
87
|
+
return uploadAccepted();
|
|
58
88
|
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasMandatoryErrors,
|
|
3
|
+
canPerformAction,
|
|
4
|
+
getMissingMandatoryFields,
|
|
5
|
+
} from "../structureNoteActions";
|
|
6
|
+
|
|
7
|
+
describe("hasMandatoryErrors", () => {
|
|
8
|
+
const template = {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
name: "group",
|
|
12
|
+
fields: [
|
|
13
|
+
{
|
|
14
|
+
name: "required_field",
|
|
15
|
+
type: "string",
|
|
16
|
+
cardinality: "1",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "optional_field",
|
|
20
|
+
type: "string",
|
|
21
|
+
cardinality: "?",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
it("returns false when content is null/undefined", () => {
|
|
29
|
+
expect(hasMandatoryErrors(null, template)).toBe(false);
|
|
30
|
+
expect(hasMandatoryErrors(undefined, template)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns false when template is null/undefined", () => {
|
|
34
|
+
expect(hasMandatoryErrors({}, null)).toBe(false);
|
|
35
|
+
expect(hasMandatoryErrors({}, undefined)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns true when a mandatory field is missing", () => {
|
|
39
|
+
const content = { optional_field: { value: "x", origin: "user" } };
|
|
40
|
+
expect(hasMandatoryErrors(content, template)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns false when all mandatory fields are populated", () => {
|
|
44
|
+
const content = {
|
|
45
|
+
required_field: { value: "ok", origin: "user" },
|
|
46
|
+
optional_field: { value: "x", origin: "user" },
|
|
47
|
+
};
|
|
48
|
+
expect(hasMandatoryErrors(content, template)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns true when a mandatory field is present but empty", () => {
|
|
52
|
+
const content = {
|
|
53
|
+
required_field: { value: "", origin: "user" },
|
|
54
|
+
};
|
|
55
|
+
expect(hasMandatoryErrors(content, template)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns true when a mandatory field is null", () => {
|
|
59
|
+
const content = {
|
|
60
|
+
required_field: { value: null, origin: "user" },
|
|
61
|
+
};
|
|
62
|
+
expect(hasMandatoryErrors(content, template)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("getMissingMandatoryFields", () => {
|
|
67
|
+
const template = {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
name: "group",
|
|
71
|
+
fields: [
|
|
72
|
+
{
|
|
73
|
+
name: "required_field",
|
|
74
|
+
type: "string",
|
|
75
|
+
cardinality: "1",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "another_required",
|
|
79
|
+
type: "string",
|
|
80
|
+
cardinality: "+",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "optional_field",
|
|
84
|
+
type: "string",
|
|
85
|
+
cardinality: "?",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
it("returns empty array when content is null/undefined", () => {
|
|
93
|
+
expect(getMissingMandatoryFields(null, template)).toEqual([]);
|
|
94
|
+
expect(getMissingMandatoryFields(undefined, template)).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns empty array when template is null/undefined", () => {
|
|
98
|
+
expect(getMissingMandatoryFields({}, null)).toEqual([]);
|
|
99
|
+
expect(getMissingMandatoryFields({}, undefined)).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns list of missing field names", () => {
|
|
103
|
+
const content = { optional_field: { value: "x", origin: "user" } };
|
|
104
|
+
const missing = getMissingMandatoryFields(content, template);
|
|
105
|
+
expect(missing).toContain("required_field");
|
|
106
|
+
expect(missing).toContain("another_required");
|
|
107
|
+
expect(missing).toHaveLength(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns empty array when all mandatory fields are populated", () => {
|
|
111
|
+
const content = {
|
|
112
|
+
required_field: { value: "ok", origin: "user" },
|
|
113
|
+
another_required: { value: ["a"], origin: "user" },
|
|
114
|
+
optional_field: { value: "x", origin: "user" },
|
|
115
|
+
};
|
|
116
|
+
expect(getMissingMandatoryFields(content, template)).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("canPerformAction (TD-7093)", () => {
|
|
121
|
+
const template = {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
name: "group",
|
|
125
|
+
fields: [
|
|
126
|
+
{
|
|
127
|
+
name: "required_field",
|
|
128
|
+
type: "string",
|
|
129
|
+
cardinality: "1",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const fullContent = { required_field: { value: "ok", origin: "user" } };
|
|
137
|
+
const partialContent = { optional_field: { value: "x", origin: "user" } };
|
|
138
|
+
|
|
139
|
+
describe("publishing actions", () => {
|
|
140
|
+
it("returns false for 'published' when mandatory fields are missing", () => {
|
|
141
|
+
expect(canPerformAction("published", partialContent, template)).toBe(
|
|
142
|
+
false,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns true for 'published' when all mandatory fields are populated", () => {
|
|
147
|
+
expect(canPerformAction("published", fullContent, template)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns false for 'pending_approval' when mandatory fields are missing", () => {
|
|
151
|
+
expect(
|
|
152
|
+
canPerformAction("pending_approval", partialContent, template),
|
|
153
|
+
).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("returns true for 'pending_approval' when all mandatory fields are populated", () => {
|
|
157
|
+
expect(canPerformAction("pending_approval", fullContent, template)).toBe(
|
|
158
|
+
true,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("draft actions", () => {
|
|
164
|
+
it("always allows 'edited' even with partial content", () => {
|
|
165
|
+
expect(canPerformAction("edited", partialContent, template)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("always allows 'draft' even with partial content", () => {
|
|
169
|
+
expect(canPerformAction("draft", partialContent, template)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("always allows 'deleted' even with partial content", () => {
|
|
173
|
+
expect(canPerformAction("deleted", partialContent, template)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("always allows 'rejected' even with partial content", () => {
|
|
177
|
+
expect(canPerformAction("rejected", partialContent, template)).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("always allows 'deprecated' even with partial content", () => {
|
|
181
|
+
expect(canPerformAction("deprecated", partialContent, template)).toBe(
|
|
182
|
+
true,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("edge cases", () => {
|
|
188
|
+
it("returns true for publishing actions when template is missing (no validation possible)", () => {
|
|
189
|
+
expect(canPerformAction("published", partialContent, null)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns true for publishing actions when content is missing (no validation possible)", () => {
|
|
193
|
+
expect(canPerformAction("published", null, template)).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateContent } from "@truedat/df/utils";
|
|
2
|
+
|
|
3
|
+
export const getMissingMandatoryFields = (content, template) => {
|
|
4
|
+
if (!template || !content) return [];
|
|
5
|
+
const errors = validateContent(template)(content);
|
|
6
|
+
return Array.isArray(errors)
|
|
7
|
+
? errors
|
|
8
|
+
.filter((err) => err && err.error === "missing required" && err.field)
|
|
9
|
+
.map((err) => err.field)
|
|
10
|
+
: [];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const hasMandatoryErrors = (content, template) => {
|
|
14
|
+
return getMissingMandatoryFields(content, template).length > 0;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const canPerformAction = (action, content, template) => {
|
|
18
|
+
if (action === "pending_approval" || action === "published") {
|
|
19
|
+
return !hasMandatoryErrors(content, template);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|