@truedat/df 7.12.5 → 7.12.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 +4 -4
- package/src/components/DynamicFieldValue.js +1 -1
- package/src/components/DynamicFormViewer.js +4 -3
- package/src/components/DynamicFormWithTranslations.js +3 -3
- package/src/components/EditableDynamicFieldValue.js +1 -1
- package/src/components/FieldViewerValue.js +44 -3
- package/src/components/__tests__/FieldViewerValue.spec.js +10 -6
- package/src/components/__tests__/__snapshots__/FieldViewerValue.spec.js.snap +53 -0
- package/src/components/widgets/DynamicField.js +6 -62
- package/src/components/widgets/DynamicTableField.js +150 -0
- package/src/components/widgets/FieldByWidget.js +63 -0
- package/src/components/widgets/StandardDropdown.js +2 -2
- package/src/components/widgets/StringField.js +2 -1
- package/src/components/widgets/__tests__/DynamicField.spec.js +10 -1
- package/src/components/widgets/__tests__/DynamicTableField.spec.js +257 -0
- package/src/components/widgets/__tests__/__snapshots__/DynamicField.spec.js.snap +97 -0
- package/src/components/widgets/__tests__/__snapshots__/DynamicTableField.spec.js.snap +114 -0
- package/src/templates/components/templateForm/ActiveGroupForm.js +5 -16
- package/src/templates/components/templateForm/FieldDefinition.js +158 -0
- package/src/templates/components/templateForm/FieldForm.js +32 -135
- package/src/templates/components/templateForm/TableValuesForm.js +258 -0
- package/src/templates/components/templateForm/TemplateForm.js +43 -26
- package/src/templates/components/templateForm/ValuesConfiguration.js +67 -0
- package/src/templates/components/templateForm/ValuesField.js +60 -96
- package/src/templates/components/templateForm/ValuesListForm.js +1 -3
- package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +227 -0
- package/src/templates/components/templateForm/__tests__/TableValuesForm.spec.js +215 -0
- package/src/templates/components/templateForm/__tests__/ValuesField.spec.js +28 -83
- package/src/templates/components/templateForm/__tests__/__snapshots__/ActiveGroupForm.spec.js.snap +17 -0
- package/src/templates/components/templateForm/__tests__/__snapshots__/FieldDefinition.spec.js.snap +443 -0
- package/src/templates/components/templateForm/__tests__/__snapshots__/FieldForm.spec.js.snap +51 -0
- package/src/templates/components/templateForm/__tests__/__snapshots__/TemplateForm.spec.js.snap +17 -0
- package/src/templates/components/templateForm/__tests__/__snapshots__/ValuesField.spec.js.snap +61 -387
- package/src/templates/components/templateForm/contentValidation.js +22 -3
- package/src/templates/components/templateForm/valueDefinitions.js +16 -2
- package/src/templates/components/templateForm/widgetDefinitions.js +28 -2
- package/src/templates/utils/__tests__/validateContent.spec.js +6 -6
- package/src/templates/utils/applyTemplate.js +72 -23
- package/src/templates/utils/filterValues.js +3 -3
- package/src/templates/utils/parseFieldOptions.js +73 -58
- package/src/templates/utils/parseGroups.js +47 -48
- package/src/templates/utils/validateContent.js +70 -25
|
@@ -3,20 +3,16 @@ import { useEffect } from "react";
|
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { Form, Segment } from "semantic-ui-react";
|
|
5
5
|
import { useIntl } from "react-intl";
|
|
6
|
+
import { hasAiSuggestions, valueSegment, getKeyType } from "./valueDefinitions";
|
|
6
7
|
import DefaultValue from "./DefaultValue";
|
|
7
8
|
import ValuesSelector from "./ValuesSelector";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
const searchableEnabledScopes = ["bg", "dq", "ie", "ri", "dd"];
|
|
9
|
+
import ValuesConfiguration from "./ValuesConfiguration";
|
|
11
10
|
|
|
12
11
|
export const ValuesField = ({
|
|
13
12
|
defaultField,
|
|
14
13
|
field,
|
|
15
|
-
fieldType,
|
|
16
|
-
keyType,
|
|
17
14
|
name,
|
|
18
15
|
onChange,
|
|
19
|
-
onSelectionChange,
|
|
20
16
|
subscribableField,
|
|
21
17
|
values,
|
|
22
18
|
fieldNamePrefix,
|
|
@@ -25,9 +21,10 @@ export const ValuesField = ({
|
|
|
25
21
|
const { formatMessage } = useIntl();
|
|
26
22
|
const { ai_suggestion, editable = true, searchable = true } = field;
|
|
27
23
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
const fieldType = field?.type;
|
|
25
|
+
const keyType = getKeyType(field);
|
|
26
|
+
const hasAiSuggestion = hasAiSuggestions(fieldType, keyType);
|
|
27
|
+
|
|
31
28
|
useEffect(() => {
|
|
32
29
|
if (!hasAiSuggestion)
|
|
33
30
|
onChange(null, {
|
|
@@ -36,6 +33,19 @@ export const ValuesField = ({
|
|
|
36
33
|
});
|
|
37
34
|
}, [hasAiSuggestion]);
|
|
38
35
|
|
|
36
|
+
const changeValue = (name, value) =>
|
|
37
|
+
value == "null"
|
|
38
|
+
? onChange(null, { name, value: null })
|
|
39
|
+
: onChange(null, {
|
|
40
|
+
name,
|
|
41
|
+
value: { [value]: null },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const onSelectionChange = (e, { value }) => {
|
|
45
|
+
onChange(e, { name: subscribableField, value: false });
|
|
46
|
+
changeValue(name, value);
|
|
47
|
+
};
|
|
48
|
+
|
|
39
49
|
const valueTypes = (options) =>
|
|
40
50
|
_.map(({ id, value }) => ({
|
|
41
51
|
key: value || "null",
|
|
@@ -46,105 +56,59 @@ export const ValuesField = ({
|
|
|
46
56
|
return field.cardinality == "0"
|
|
47
57
|
? null
|
|
48
58
|
: valueSegment(values, keyType, fieldType) && (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
defaultField={defaultField}
|
|
65
|
-
defaultValue={field?.default}
|
|
66
|
-
name={name}
|
|
67
|
-
formatMessage={formatMessage}
|
|
68
|
-
onChange={onChange}
|
|
69
|
-
type={keyType}
|
|
70
|
-
values={_.path(`values.${keyType}`)(field)}
|
|
71
|
-
/>
|
|
72
|
-
) : null}
|
|
73
|
-
<Form.Group size="small" widths="equal">
|
|
74
|
-
<Form.Field>
|
|
75
|
-
<Form.Checkbox
|
|
76
|
-
name={`${fieldNamePrefix}.editable`}
|
|
77
|
-
label={formatMessage({
|
|
78
|
-
id: "template.field.editable",
|
|
79
|
-
defaultMessage: "Editable",
|
|
80
|
-
})}
|
|
81
|
-
checked={editable}
|
|
82
|
-
onChange={(e, { name, checked: value }) =>
|
|
83
|
-
onChange(null, { name, value })
|
|
84
|
-
}
|
|
85
|
-
/>
|
|
86
|
-
</Form.Field>
|
|
87
|
-
{hasAiSuggestion ? (
|
|
88
|
-
<Form.Field>
|
|
89
|
-
<Form.Checkbox
|
|
90
|
-
name={`${fieldNamePrefix}.ai_suggestion`}
|
|
91
|
-
label={formatMessage({
|
|
92
|
-
id: "template.field.ai_suggestion",
|
|
93
|
-
defaultMessage: "AI Suggestions",
|
|
94
|
-
})}
|
|
95
|
-
checked={ai_suggestion}
|
|
96
|
-
onChange={(_e, { name, checked: value }) =>
|
|
97
|
-
onChange(null, { name, value })
|
|
98
|
-
}
|
|
99
|
-
/>
|
|
100
|
-
</Form.Field>
|
|
101
|
-
) : null}
|
|
102
|
-
{_.includes(scope)(searchableEnabledScopes) ? (
|
|
103
|
-
<Form.Field>
|
|
104
|
-
<Form.Checkbox
|
|
105
|
-
name={`${fieldNamePrefix}.searchable`}
|
|
106
|
-
label={formatMessage({
|
|
107
|
-
id: "template.field.searchable",
|
|
108
|
-
defaultMessage: "Sercheable",
|
|
109
|
-
})}
|
|
110
|
-
checked={searchable}
|
|
111
|
-
onChange={(_e, { name, checked: value }) =>
|
|
112
|
-
onChange(null, { name, value })
|
|
113
|
-
}
|
|
114
|
-
/>
|
|
115
|
-
</Form.Field>
|
|
116
|
-
) : null}
|
|
117
|
-
{_.includes(keyType)(["fixed", "fixed_tuple"]) ? (
|
|
118
|
-
<Form.Checkbox
|
|
119
|
-
name={subscribableField}
|
|
120
|
-
label={formatMessage({ id: "template.field.subscribable" })}
|
|
121
|
-
checked={_.prop("subscribable")(field) || false}
|
|
122
|
-
onChange={(_e, { name, checked: value }) =>
|
|
123
|
-
onChange(null, { name, value })
|
|
124
|
-
}
|
|
125
|
-
/>
|
|
126
|
-
) : null}
|
|
127
|
-
</Form.Group>
|
|
128
|
-
<DefaultValue
|
|
59
|
+
<Segment>
|
|
60
|
+
{_.size(values) > 1 ? (
|
|
61
|
+
<Form.Dropdown
|
|
62
|
+
fluid
|
|
63
|
+
selection
|
|
64
|
+
label={formatMessage({ id: "template.field.values" })}
|
|
65
|
+
onChange={onSelectionChange}
|
|
66
|
+
value={keyType || "null"}
|
|
67
|
+
required
|
|
68
|
+
options={valueTypes(values)}
|
|
69
|
+
/>
|
|
70
|
+
) : null}
|
|
71
|
+
{keyType ? (
|
|
72
|
+
<ValuesSelector
|
|
73
|
+
cardinality={field?.cardinality}
|
|
129
74
|
defaultField={defaultField}
|
|
130
|
-
|
|
131
|
-
|
|
75
|
+
defaultValue={field?.default}
|
|
76
|
+
name={name}
|
|
132
77
|
formatMessage={formatMessage}
|
|
133
78
|
onChange={onChange}
|
|
134
79
|
type={keyType}
|
|
80
|
+
values={_.path(`values.${keyType}`)(field)}
|
|
135
81
|
/>
|
|
136
|
-
|
|
137
|
-
|
|
82
|
+
) : null}
|
|
83
|
+
<ValuesConfiguration
|
|
84
|
+
aiSuggestion={ai_suggestion}
|
|
85
|
+
editable={editable}
|
|
86
|
+
keyType={keyType}
|
|
87
|
+
onChange={onChange}
|
|
88
|
+
searchable={searchable}
|
|
89
|
+
fieldNamePrefix={fieldNamePrefix}
|
|
90
|
+
hasAiSuggestion={hasAiSuggestion}
|
|
91
|
+
subscribableField={subscribableField}
|
|
92
|
+
subscribable={field?.subscribable}
|
|
93
|
+
scope={scope}
|
|
94
|
+
/>
|
|
95
|
+
<DefaultValue
|
|
96
|
+
defaultField={defaultField}
|
|
97
|
+
field={field}
|
|
98
|
+
fieldType={fieldType}
|
|
99
|
+
formatMessage={formatMessage}
|
|
100
|
+
onChange={onChange}
|
|
101
|
+
type={keyType}
|
|
102
|
+
/>
|
|
103
|
+
</Segment>
|
|
104
|
+
);
|
|
138
105
|
};
|
|
139
106
|
|
|
140
107
|
ValuesField.propTypes = {
|
|
141
108
|
defaultField: PropTypes.string,
|
|
142
|
-
keyType: PropTypes.string,
|
|
143
109
|
field: PropTypes.object,
|
|
144
|
-
fieldType: PropTypes.string,
|
|
145
110
|
name: PropTypes.string,
|
|
146
111
|
onChange: PropTypes.func.isRequired,
|
|
147
|
-
onSelectionChange: PropTypes.func,
|
|
148
112
|
subscribableField: PropTypes.string,
|
|
149
113
|
values: PropTypes.array,
|
|
150
114
|
fieldNamePrefix: PropTypes.string,
|
|
@@ -92,9 +92,6 @@ export class ValuesListForm extends Component {
|
|
|
92
92
|
handleMoveDown = (key) => this.swapElements(key, key + 1);
|
|
93
93
|
|
|
94
94
|
renderListContent = (value) => {
|
|
95
|
-
const {
|
|
96
|
-
intl: { formatMessage },
|
|
97
|
-
} = this.props;
|
|
98
95
|
if (this.props.type == "fixed") {
|
|
99
96
|
return <List.Content>{value}</List.Content>;
|
|
100
97
|
} else if (this.props.type == "table_columns") {
|
|
@@ -121,6 +118,7 @@ export class ValuesListForm extends Component {
|
|
|
121
118
|
values,
|
|
122
119
|
type,
|
|
123
120
|
} = this.props;
|
|
121
|
+
|
|
124
122
|
const { error } = this.state;
|
|
125
123
|
const isNullOrEmpty = (value) =>
|
|
126
124
|
!_.isNumber(value) && (!value || _.isEmpty(value));
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// FieldDefinition.realDefs.test.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
4
|
+
import { within } from "@testing-library/react";
|
|
5
|
+
import userEvent from "@testing-library/user-event";
|
|
6
|
+
import FieldDefinition from "../FieldDefinition";
|
|
7
|
+
|
|
8
|
+
const mockFormatMessage = jest.fn(({ id }) => id);
|
|
9
|
+
|
|
10
|
+
jest.mock("react-intl", () => ({
|
|
11
|
+
...jest.requireActual("react-intl"),
|
|
12
|
+
useIntl: () => ({
|
|
13
|
+
formatMessage: mockFormatMessage,
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const user = userEvent.setup({ delay: null });
|
|
18
|
+
|
|
19
|
+
describe("FieldDefinition", () => {
|
|
20
|
+
const baseProps = {
|
|
21
|
+
fieldNamePrefix: "field",
|
|
22
|
+
defaultField: "field.default",
|
|
23
|
+
valueName: "field.value",
|
|
24
|
+
subscribableField: "field.subscribable",
|
|
25
|
+
widget: "string",
|
|
26
|
+
type: "string",
|
|
27
|
+
cardinality: "?",
|
|
28
|
+
onChange: jest.fn(),
|
|
29
|
+
allowedTypes: [],
|
|
30
|
+
allowedWidgets: [],
|
|
31
|
+
mandatory: {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("renders dropdowns with initial values", async () => {
|
|
39
|
+
const rendered = render(<FieldDefinition {...baseProps} onChange={jest.fn()} />);
|
|
40
|
+
waitForLoad(rendered);
|
|
41
|
+
expect(rendered.container).toMatchSnapshot();
|
|
42
|
+
const dropdowns = rendered.getAllByRole("listbox");
|
|
43
|
+
expect(dropdowns.length).toBe(3);
|
|
44
|
+
|
|
45
|
+
const widgetDropdown = dropdowns[0];
|
|
46
|
+
expect(widgetDropdown).toHaveAttribute("name", "field.widget");
|
|
47
|
+
const selectedWidget = within(widgetDropdown).getByRole("option", { name: "Text Input" });
|
|
48
|
+
expect(selectedWidget).toHaveAttribute("aria-checked", "true");
|
|
49
|
+
|
|
50
|
+
const typeDropdown = dropdowns[1];
|
|
51
|
+
expect(typeDropdown).toHaveAttribute("name", "field.type");
|
|
52
|
+
const selectedType = within(typeDropdown).getByRole("option", { name: "template.field.type.string" });
|
|
53
|
+
expect(selectedType).toHaveAttribute("aria-checked", "true");
|
|
54
|
+
|
|
55
|
+
const cardinalityDropdown = dropdowns[2];
|
|
56
|
+
expect(cardinalityDropdown).toHaveAttribute("name", "field.cardinality");
|
|
57
|
+
const selectedCardinality = within(cardinalityDropdown).getByRole("option", { name: "template.field.cardinality.?" });
|
|
58
|
+
expect(selectedCardinality).toHaveAttribute("aria-checked", "true");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("handleWidgetChange: sets valueName to null (via getValues), resets default, sets subscribable=false, then emits type change", async () => {
|
|
62
|
+
const onChange = jest.fn();
|
|
63
|
+
const rendered = render(<FieldDefinition {...baseProps} onChange={onChange} />);
|
|
64
|
+
waitForLoad(rendered);
|
|
65
|
+
|
|
66
|
+
const numberOpt = within(rendered.getAllByRole("listbox")[0]).getByRole("option", { name: "Number" });
|
|
67
|
+
// Change type; for widget 'string' the only type is 'string', so switch the widget first to 'number'
|
|
68
|
+
await user.click(numberOpt);
|
|
69
|
+
|
|
70
|
+
// 1) reset value
|
|
71
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
72
|
+
onChange.mock.calls.length - 3,
|
|
73
|
+
null,
|
|
74
|
+
{ name: "field.value", value: null }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// // 2) reset defaultField
|
|
78
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
79
|
+
onChange.mock.calls.length - 2,
|
|
80
|
+
expect.anything(),
|
|
81
|
+
{ name: "field.default", value: { value: "", origin: "default" } }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// // 3) set subscribable=false
|
|
85
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
86
|
+
onChange.mock.calls.length - 1,
|
|
87
|
+
expect.anything(),
|
|
88
|
+
{ name: "field.subscribable", value: false }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// // 4) finally, the actual type widget event payload
|
|
92
|
+
expect(onChange).toHaveBeenLastCalledWith(
|
|
93
|
+
expect.anything(),
|
|
94
|
+
expect.objectContaining({ name: "field.widget", value: "number" })
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("handleTypeChange: recomputes default value via real getValues, resets default, sets subscribable=false, and emits widget change", async () => {
|
|
99
|
+
const onChange = jest.fn();
|
|
100
|
+
const rendered = render(<FieldDefinition {...{ ...baseProps, widget: "dropdown", type: "string" }} onChange={onChange} />);
|
|
101
|
+
waitForLoad(rendered);
|
|
102
|
+
|
|
103
|
+
// Switch Type from 'string' to 'domain'
|
|
104
|
+
const domainOpt = within(rendered.getAllByRole("listbox")[1]).getByRole("option", { name: "template.field.type.domain" });
|
|
105
|
+
await user.click(domainOpt);
|
|
106
|
+
|
|
107
|
+
// The first call in handleTypeChange is changeValue(valueName, ...) -> null
|
|
108
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
109
|
+
1,
|
|
110
|
+
null,
|
|
111
|
+
{ name: "field.value", value: null }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// 2) default reset
|
|
115
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
116
|
+
2,
|
|
117
|
+
expect.anything(),
|
|
118
|
+
{ name: "field.default", value: { value: "", origin: "default" } }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// 3) subscribable=false
|
|
122
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
123
|
+
3,
|
|
124
|
+
expect.anything(),
|
|
125
|
+
{ name: "field.subscribable", value: false }
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// 4) widget change payload
|
|
129
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
130
|
+
4,
|
|
131
|
+
expect.anything(),
|
|
132
|
+
expect.objectContaining({ name: "field.type", value: "domain" })
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("useEffect: auto-fixes invalid type/cardinality for a widget (real WIDGETS)", () => {
|
|
137
|
+
const onChange = jest.fn();
|
|
138
|
+
|
|
139
|
+
// For widget='number', valid types: ['integer','float']; cardinalities: ['?','1']
|
|
140
|
+
// Provide invalid type 'string' and invalid cardinality '+'
|
|
141
|
+
const rendered = render(<FieldDefinition {...baseProps}
|
|
142
|
+
onChange={onChange}
|
|
143
|
+
widget="number"
|
|
144
|
+
type="string"
|
|
145
|
+
cardinality="+" />);
|
|
146
|
+
|
|
147
|
+
waitForLoad(rendered);
|
|
148
|
+
|
|
149
|
+
// The effect should fire once and fix both:
|
|
150
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
151
|
+
null,
|
|
152
|
+
{ name: "field.type", value: "integer" } // first in WIDGETS types for 'number'
|
|
153
|
+
);
|
|
154
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
155
|
+
null,
|
|
156
|
+
{ name: "field.cardinality", value: "?" } // first in WIDGETS cardinalities for 'number'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("handleCardinalityChange: selecting '1' (or '+') clears mandatory when it's non-empty", async () => {
|
|
161
|
+
const onChange = jest.fn();
|
|
162
|
+
|
|
163
|
+
const rendered = render(
|
|
164
|
+
<FieldDefinition
|
|
165
|
+
{...baseProps}
|
|
166
|
+
onChange={onChange}
|
|
167
|
+
widget="string"
|
|
168
|
+
type="string"
|
|
169
|
+
cardinality="?"
|
|
170
|
+
mandatory={{ some: "value" }}
|
|
171
|
+
/>);
|
|
172
|
+
|
|
173
|
+
waitForLoad(rendered);
|
|
174
|
+
|
|
175
|
+
// Select exactly-one '1'
|
|
176
|
+
const cardOpt = within(rendered.getAllByRole("listbox")[2]).getByRole("option", { name: "template.field.cardinality.1" });
|
|
177
|
+
await user.click(cardOpt);
|
|
178
|
+
|
|
179
|
+
// First: direct change emission
|
|
180
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
181
|
+
1,
|
|
182
|
+
expect.anything(),
|
|
183
|
+
expect.objectContaining({ name: "field.cardinality", value: "1" })
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Then: clear mandatory because value ∈ ['1','+'] and mandatory was non-empty
|
|
187
|
+
expect(onChange).toHaveBeenNthCalledWith(
|
|
188
|
+
2,
|
|
189
|
+
expect.anything(),
|
|
190
|
+
{ name: "field.mandatory", value: null }
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("allowedWidgets & allowedTypes filter visible options (using real option generators)", () => {
|
|
195
|
+
const onChange = jest.fn();
|
|
196
|
+
|
|
197
|
+
const rendered = render(
|
|
198
|
+
<FieldDefinition
|
|
199
|
+
{...baseProps}
|
|
200
|
+
onChange={onChange}
|
|
201
|
+
allowedWidgets={["number"]}
|
|
202
|
+
allowedTypes={["float"]}
|
|
203
|
+
widget="number"
|
|
204
|
+
type="float"
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
waitForLoad(rendered);
|
|
209
|
+
|
|
210
|
+
const dropdowns = rendered.getAllByRole("listbox");
|
|
211
|
+
expect(dropdowns.length).toBe(3);
|
|
212
|
+
|
|
213
|
+
const widgetDropdown = dropdowns[0];
|
|
214
|
+
expect(widgetDropdown).toHaveAttribute("name", "field.widget");
|
|
215
|
+
const widgetOptions = within(widgetDropdown).getAllByRole("option");
|
|
216
|
+
expect(widgetOptions.length).toBe(1);
|
|
217
|
+
expect(widgetOptions[0]).toHaveAttribute("aria-checked", "true");
|
|
218
|
+
expect(widgetOptions[0]).toHaveTextContent("Number");
|
|
219
|
+
|
|
220
|
+
const typeDropdown = dropdowns[1];
|
|
221
|
+
expect(typeDropdown).toHaveAttribute("name", "field.type");
|
|
222
|
+
const typeOptions = within(typeDropdown).getAllByRole("option");
|
|
223
|
+
expect(typeOptions.length).toBe(1);
|
|
224
|
+
expect(typeOptions[0]).toHaveAttribute("aria-checked", "true");
|
|
225
|
+
expect(typeOptions[0]).toHaveTextContent("float");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { within } from "@testing-library/react";
|
|
3
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import TableValuesForm from "../TableValuesForm";
|
|
6
|
+
|
|
7
|
+
const mockFormatMessage = jest.fn(({ id }) => id);
|
|
8
|
+
|
|
9
|
+
jest.mock("react-intl", () => ({
|
|
10
|
+
...jest.requireActual("react-intl"),
|
|
11
|
+
useIntl: () => ({
|
|
12
|
+
formatMessage: mockFormatMessage,
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// --- Stub child components to isolate TableValuesForm behavior ---
|
|
17
|
+
jest.mock("../FieldDefinition", () => () => <div data-testid="FieldDefinition" />);
|
|
18
|
+
jest.mock("../ValuesField", () => () => <div data-testid="ValuesField" />);
|
|
19
|
+
jest.mock("../ValuesConfiguration", () => () => <div data-testid="ValuesConfiguration" />);
|
|
20
|
+
|
|
21
|
+
const user = userEvent.setup({ delay: null });
|
|
22
|
+
|
|
23
|
+
const values = [
|
|
24
|
+
{ name: "colA", widget: "string", type: "string", errors: {} },
|
|
25
|
+
{ name: "colB", widget: "string", type: "string", errors: { nameDuplicated: true, hasErrors: true } },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const baseProps = {
|
|
29
|
+
name: "myField",
|
|
30
|
+
type: "table_columns", // used as the key in the onChange payload
|
|
31
|
+
values,
|
|
32
|
+
onChange: jest.fn(),
|
|
33
|
+
scope: "scope-x",
|
|
34
|
+
field: { values: { table_columns: values } },
|
|
35
|
+
fieldNamePrefix: "myPrefix",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
describe("TableValuesForm", () => {
|
|
40
|
+
test("renders an Accordion with a panel per value, showing name and duplicated label", () => {
|
|
41
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
42
|
+
waitForLoad(rendered);
|
|
43
|
+
|
|
44
|
+
// Two labels for two columns
|
|
45
|
+
expect(rendered.getByText("colA")).toBeInTheDocument();
|
|
46
|
+
expect(rendered.getByText("colB")).toBeInTheDocument();
|
|
47
|
+
|
|
48
|
+
// Duplicated name label for the second column only
|
|
49
|
+
// The label text comes from formatMessage
|
|
50
|
+
expect(rendered.getByText("template.form.validation.name_duplicated")).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("clicking a panel title toggles it active and reveals its content", async () => {
|
|
54
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
55
|
+
waitForLoad(rendered);
|
|
56
|
+
|
|
57
|
+
// Click on "colA" title to expand
|
|
58
|
+
await user.click(rendered.getByText("colA"));
|
|
59
|
+
|
|
60
|
+
// Content should render the stubs under the active panel
|
|
61
|
+
// (we don't assert which panel holds it; just that they are present)
|
|
62
|
+
expect(rendered.getAllByTestId("FieldDefinition").length).toBeGreaterThan(0);
|
|
63
|
+
expect(rendered.getAllByTestId("ValuesField").length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("move down moves the selected column forward and calls onChange with swapped array", async () => {
|
|
67
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
68
|
+
waitForLoad(rendered);
|
|
69
|
+
|
|
70
|
+
// Expand panel 0 (colA)
|
|
71
|
+
await user.click(rendered.getByText("colA"));
|
|
72
|
+
|
|
73
|
+
// In the panel content, there is a Button.Group with three buttons (up, down, delete)
|
|
74
|
+
// We'll grab them by role=button within the expanded panel area.
|
|
75
|
+
// Since Semantic UI doesn't attach specific names by default to icon-only buttons,
|
|
76
|
+
// we'll pick by order: [up, down, delete].
|
|
77
|
+
const activePanel = rendered.getByText("colA").closest(".title")?.nextElementSibling;
|
|
78
|
+
expect(activePanel).toBeTruthy();
|
|
79
|
+
|
|
80
|
+
const buttons = within(activePanel).getAllByRole("button");
|
|
81
|
+
// up is disabled for index 0; down should move colA to index 1
|
|
82
|
+
const [, downButton] = buttons;
|
|
83
|
+
await user.click(downButton);
|
|
84
|
+
|
|
85
|
+
// Expect onChange called with swapped values for key "table_columns"
|
|
86
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(
|
|
87
|
+
null,
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
name: "myField",
|
|
90
|
+
value: {
|
|
91
|
+
table_columns: [
|
|
92
|
+
expect.objectContaining({ name: "colB" }),
|
|
93
|
+
expect.objectContaining({ name: "colA" }),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("move up moves the selected column backward and calls onChange with swapped array", async () => {
|
|
101
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
102
|
+
waitForLoad(rendered);
|
|
103
|
+
|
|
104
|
+
// Expand panel 1 (colB)
|
|
105
|
+
await user.click(rendered.getByText("colB"));
|
|
106
|
+
|
|
107
|
+
// Get the three buttons from this panel (up, down, delete)
|
|
108
|
+
const activePanel = rendered.getByText("colB").closest(".title")?.nextElementSibling;
|
|
109
|
+
expect(activePanel).toBeTruthy();
|
|
110
|
+
|
|
111
|
+
const buttons = within(activePanel).getAllByRole("button");
|
|
112
|
+
const [upButton] = buttons; // first button = up
|
|
113
|
+
await user.click(upButton);
|
|
114
|
+
|
|
115
|
+
// Expect swap: colB moves to index 0
|
|
116
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(
|
|
117
|
+
null,
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
name: "myField",
|
|
120
|
+
value: {
|
|
121
|
+
table_columns: [
|
|
122
|
+
expect.objectContaining({ name: "colB" }),
|
|
123
|
+
expect.objectContaining({ name: "colA" }),
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("delete removes the selected column and calls onChange with shortened array", async () => {
|
|
131
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
132
|
+
waitForLoad(rendered);
|
|
133
|
+
|
|
134
|
+
// Expand panel 0 (colA)
|
|
135
|
+
await user.click(rendered.getByText("colA"));
|
|
136
|
+
|
|
137
|
+
const activePanel = rendered.getByText("colA").closest(".title")?.nextElementSibling;
|
|
138
|
+
const buttons = within(activePanel).getAllByRole("button");
|
|
139
|
+
const deleteButton = buttons[2]; // third button
|
|
140
|
+
|
|
141
|
+
await user.click(deleteButton);
|
|
142
|
+
|
|
143
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(
|
|
144
|
+
null,
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
name: "myField",
|
|
147
|
+
value: {
|
|
148
|
+
table_columns: [
|
|
149
|
+
expect.objectContaining({ name: "colB" }), // only colB remains
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("add column via Enter in input appends a default field with given name", async () => {
|
|
157
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
158
|
+
waitForLoad(rendered);
|
|
159
|
+
|
|
160
|
+
// Input is at the bottom with an icon. We'll type and press Enter.
|
|
161
|
+
const input = rendered.getByRole("textbox");
|
|
162
|
+
await user.type(input, "newCol{enter}");
|
|
163
|
+
|
|
164
|
+
// The payload includes the new element at the end.
|
|
165
|
+
// It uses defaultFieldDefinition + overrides: name, label set to 'myField', ai_suggestion=false
|
|
166
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(
|
|
167
|
+
null,
|
|
168
|
+
expect.objectContaining({
|
|
169
|
+
name: "myField",
|
|
170
|
+
value: {
|
|
171
|
+
table_columns: expect.arrayContaining([
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
name: "newCol",
|
|
174
|
+
ai_suggestion: false,
|
|
175
|
+
label: "newCol",
|
|
176
|
+
}),
|
|
177
|
+
]),
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("add column via clicking the add icon triggers same append behavior", async () => {
|
|
184
|
+
const rendered = render(<TableValuesForm {...baseProps} />);
|
|
185
|
+
waitForLoad(rendered);
|
|
186
|
+
|
|
187
|
+
// Type value first so state.fieldValue has content
|
|
188
|
+
const input = rendered.getByRole("textbox");
|
|
189
|
+
await user.type(input, "anotherCol");
|
|
190
|
+
|
|
191
|
+
// Click the icon (it's rendered inside the input as a clickable Icon)
|
|
192
|
+
// Find by role button might not exist; the icon has role="img" with class "add circle".
|
|
193
|
+
// We'll click it via its container: the input's parent .input has an i.icon inside.
|
|
194
|
+
const inputWrapper = input.closest(".input");
|
|
195
|
+
const addIcon = inputWrapper?.querySelector(".icon");
|
|
196
|
+
expect(addIcon).toBeTruthy();
|
|
197
|
+
await user.click(addIcon);
|
|
198
|
+
|
|
199
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(
|
|
200
|
+
null,
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
name: "myField",
|
|
203
|
+
value: {
|
|
204
|
+
table_columns: expect.arrayContaining([
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
name: "anotherCol",
|
|
207
|
+
ai_suggestion: false,
|
|
208
|
+
label: "anotherCol",
|
|
209
|
+
}),
|
|
210
|
+
]),
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|