@griddo/ax 11.15.8 → 11.15.9-rc.0
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 +2 -2
- package/src/__tests__/components/Fields/ArrayFieldGroup/ArrayFieldGroup.test.tsx +120 -240
- package/src/components/Fields/ArrayFieldGroup/ArrayFieldInline/index.tsx +24 -45
- package/src/components/Fields/ArrayFieldGroup/ArrayFieldItem/index.tsx +25 -45
- package/src/components/Fields/ArrayFieldGroup/helpers.ts +72 -0
- package/src/components/Fields/ArrayFieldGroup/index.tsx +42 -24
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "11.15.
|
|
4
|
+
"version": "11.15.9-rc.0",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
|
|
7
7
|
"Diego M. Béjar <diego.bejar@secuoyas.com>",
|
|
@@ -219,5 +219,5 @@
|
|
|
219
219
|
"publishConfig": {
|
|
220
220
|
"access": "public"
|
|
221
221
|
},
|
|
222
|
-
"gitHead": "
|
|
222
|
+
"gitHead": "a7cc8a0153f803596fab93ecbe74a37c59da3cf3"
|
|
223
223
|
}
|
|
@@ -1,192 +1,103 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import ArrayFieldGroup, { IProps } from "@ax/components/Fields/ArrayFieldGroup";
|
|
4
|
-
import { ThemeProvider } from "styled-components";
|
|
1
|
+
import ArrayFieldGroup, { type IProps } from "@ax/components/Fields/ArrayFieldGroup";
|
|
2
|
+
import FieldsBehavior from "@ax/components/FieldsBehavior";
|
|
5
3
|
import { parseTheme } from "@ax/helpers";
|
|
6
4
|
import globalTheme from "@ax/themes/theme.json";
|
|
7
|
-
import { render, cleanup, screen, fireEvent, act } from "../../../../../config/jest/test-utils";
|
|
8
|
-
import { mock } from "jest-mock-extended";
|
|
9
|
-
import FieldsBehavior from "@ax/components/FieldsBehavior";
|
|
10
5
|
|
|
11
|
-
|
|
6
|
+
import { mock } from "jest-mock-extended";
|
|
7
|
+
import configureStore from "redux-mock-store";
|
|
8
|
+
import { ThemeProvider } from "styled-components";
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
import { act, cleanup, fireEvent, render, screen } from "../../../../../config/jest/test-utils";
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
afterEach(cleanup);
|
|
16
13
|
|
|
17
14
|
const middlewares: any = [];
|
|
18
15
|
const mockStore = configureStore(middlewares);
|
|
16
|
+
const store = mockStore({});
|
|
19
17
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await act(async () => {
|
|
26
|
-
render(
|
|
27
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
28
|
-
<ArrayFieldGroup {...defaultProps} />
|
|
29
|
-
</ThemeProvider>,
|
|
30
|
-
{ store },
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(screen.getByTestId("button-line-inverse")).toBeTruthy();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("should render the component with ArrayFieldInline", async () => {
|
|
38
|
-
defaultProps.value = [
|
|
18
|
+
const sampleValue = [
|
|
19
|
+
{
|
|
20
|
+
id: "b71c3180-1cf2-4f69-8248-5fcb6e579377",
|
|
21
|
+
semTitle: "dsa",
|
|
22
|
+
subjects: [
|
|
39
23
|
{
|
|
40
|
-
id: "
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
id: "3ae4be00-9216-422d-a841-0448e305af08",
|
|
45
|
-
subjTeachers: {
|
|
46
|
-
mode: "manual",
|
|
47
|
-
fixed: [4476, 4425],
|
|
48
|
-
},
|
|
49
|
-
subjTitle: "as",
|
|
50
|
-
subjType: "das",
|
|
51
|
-
},
|
|
52
|
-
],
|
|
24
|
+
id: "3ae4be00-9216-422d-a841-0448e305af08",
|
|
25
|
+
subjTeachers: { mode: "manual", fixed: [4476, 4425] },
|
|
26
|
+
subjTitle: "as",
|
|
27
|
+
subjType: "das",
|
|
53
28
|
},
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fieldType: "TextField",
|
|
91
|
-
field: {
|
|
92
|
-
title: "Title",
|
|
93
|
-
type: "TextField",
|
|
94
|
-
key: "semTitle",
|
|
95
|
-
mandatory: "true",
|
|
96
|
-
isTitle: true,
|
|
97
|
-
},
|
|
98
|
-
title: "Title",
|
|
99
|
-
type: "TextField",
|
|
100
|
-
mandatory: "true",
|
|
101
|
-
isTitle: true,
|
|
102
|
-
theme: "default-theme",
|
|
103
|
-
};
|
|
104
|
-
defaultProps.innerFields = [<FieldsBehavior {...fieldProps2} key={1} />];
|
|
105
|
-
|
|
106
|
-
// rendering without innerFields in props
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Builds a fresh props object per test so mutations don't leak between cases.
|
|
34
|
+
const makeProps = (overrides: Partial<IProps> = {}): IProps => {
|
|
35
|
+
const props = mock<IProps>();
|
|
36
|
+
props.selectedContent = { component: "" };
|
|
37
|
+
props.value = sampleValue;
|
|
38
|
+
props.arrayType = "inline";
|
|
39
|
+
props.innerFields = [];
|
|
40
|
+
for (const [key, val] of Object.entries(overrides)) {
|
|
41
|
+
(props as any)[key] = val;
|
|
42
|
+
}
|
|
43
|
+
return props;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const makeField = (overrides: Record<string, unknown> = {}) => {
|
|
47
|
+
const fieldProps = {
|
|
48
|
+
objKey: "semTitle",
|
|
49
|
+
fieldType: "TextField",
|
|
50
|
+
value: "dsa",
|
|
51
|
+
innerFields: [],
|
|
52
|
+
field: { title: "Title", type: "TextField", key: "semTitle", mandatory: true, isTitle: true },
|
|
53
|
+
title: "Title",
|
|
54
|
+
type: "TextField",
|
|
55
|
+
mandatory: true,
|
|
56
|
+
isTitle: true,
|
|
57
|
+
theme: "default-theme",
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
return <FieldsBehavior {...fieldProps} key={1} />;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const renderGroup = async (props: IProps) => {
|
|
64
|
+
await act(async () => {
|
|
107
65
|
render(
|
|
108
66
|
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
109
|
-
<ArrayFieldGroup {...
|
|
67
|
+
<ArrayFieldGroup {...props} />
|
|
110
68
|
</ThemeProvider>,
|
|
111
69
|
{ store },
|
|
112
70
|
);
|
|
113
71
|
});
|
|
72
|
+
};
|
|
114
73
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
id: "b71c3180-1cf2-4f69-8248-5fcb6e579377",
|
|
119
|
-
semTitle: "dsa",
|
|
120
|
-
subjects: [
|
|
121
|
-
{
|
|
122
|
-
id: "3ae4be00-9216-422d-a841-0448e305af08",
|
|
123
|
-
subjTeachers: {
|
|
124
|
-
mode: "manual",
|
|
125
|
-
fixed: [4476, 4425],
|
|
126
|
-
},
|
|
127
|
-
subjTitle: "as",
|
|
128
|
-
subjType: "das",
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
];
|
|
133
|
-
const fieldProps = {
|
|
134
|
-
objKey: "semTitle",
|
|
135
|
-
fieldType: "TextField",
|
|
136
|
-
innerFields: [],
|
|
137
|
-
field: {
|
|
138
|
-
title: "Title",
|
|
139
|
-
type: "TextField",
|
|
140
|
-
key: "semTitle",
|
|
141
|
-
mandatory: "true",
|
|
142
|
-
isTitle: false,
|
|
143
|
-
},
|
|
144
|
-
title: "Title",
|
|
145
|
-
type: "TextField",
|
|
146
|
-
mandatory: "true",
|
|
147
|
-
isTitle: false,
|
|
148
|
-
theme: "default-theme",
|
|
149
|
-
};
|
|
74
|
+
describe("ArrayFieldGroup component rendering", () => {
|
|
75
|
+
it("should render the add button when there are no items", async () => {
|
|
76
|
+
await renderGroup(makeProps({ value: [] }));
|
|
150
77
|
|
|
151
|
-
|
|
152
|
-
|
|
78
|
+
expect(screen.getByTestId("button-line-inverse")).toBeTruthy();
|
|
79
|
+
});
|
|
153
80
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
157
|
-
<ArrayFieldGroup {...defaultProps} />
|
|
158
|
-
</ThemeProvider>,
|
|
159
|
-
{ store },
|
|
160
|
-
);
|
|
161
|
-
});
|
|
81
|
+
it("should render an ArrayFieldInline per item when arrayType is inline", async () => {
|
|
82
|
+
await renderGroup(makeProps({ arrayType: "inline", innerFields: [makeField()] }));
|
|
162
83
|
|
|
163
84
|
expect(screen.getByTestId("button-line-inverse")).toBeTruthy();
|
|
164
|
-
expect(screen.getByTestId("
|
|
85
|
+
expect(screen.getByTestId("arrayFieldInline")).toBeTruthy();
|
|
86
|
+
});
|
|
165
87
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
fieldType: "TextField",
|
|
169
|
-
field: {
|
|
170
|
-
title: "A Title",
|
|
171
|
-
type: "TextField",
|
|
172
|
-
key: "semTitle",
|
|
173
|
-
mandatory: "true",
|
|
174
|
-
isTitle: true,
|
|
175
|
-
},
|
|
176
|
-
title: "A Title",
|
|
177
|
-
type: "TextField",
|
|
178
|
-
mandatory: "true",
|
|
179
|
-
isTitle: true,
|
|
180
|
-
theme: "default-theme",
|
|
181
|
-
};
|
|
182
|
-
defaultProps.innerFields = [<FieldsBehavior {...fieldProps2} key={1} />];
|
|
88
|
+
it("should render an ArrayFieldItem per item when arrayType is item", async () => {
|
|
89
|
+
await renderGroup(makeProps({ arrayType: "item", innerFields: [makeField({ isTitle: false })] }));
|
|
183
90
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
91
|
+
expect(screen.getByTestId("button-line-inverse")).toBeTruthy();
|
|
92
|
+
expect(screen.getByTestId("arrayFieldItem")).toBeTruthy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should use the field marked as isTitle as the item title", async () => {
|
|
96
|
+
await renderGroup(
|
|
97
|
+
makeProps({
|
|
98
|
+
arrayType: "item",
|
|
99
|
+
innerFields: [makeField({ title: "A Title", field: { key: "semTitle", isTitle: true } })],
|
|
100
|
+
}),
|
|
190
101
|
);
|
|
191
102
|
|
|
192
103
|
expect(screen.getByText("A Title")).toBeTruthy();
|
|
@@ -194,91 +105,60 @@ describe("ArrayFieldGroup component rendering", () => {
|
|
|
194
105
|
});
|
|
195
106
|
|
|
196
107
|
describe("ArrayFieldGroup component events", () => {
|
|
197
|
-
it("should call
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
id: "b71c3180-1cf2-4f69-8248-5fcb6e579377",
|
|
201
|
-
semTitle: "dsa",
|
|
202
|
-
subjects: [
|
|
203
|
-
{
|
|
204
|
-
id: "3ae4be00-9216-422d-a841-0448e305af08",
|
|
205
|
-
subjTeachers: {
|
|
206
|
-
mode: "manual",
|
|
207
|
-
fixed: [4476, 4425],
|
|
208
|
-
},
|
|
209
|
-
subjTitle: "as",
|
|
210
|
-
subjType: "das",
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
},
|
|
214
|
-
];
|
|
108
|
+
it("should call onChange when clicking the add button", async () => {
|
|
109
|
+
const props = makeProps();
|
|
110
|
+
const onChangeMock = props.onChange as jest.MockedFunction<IProps["onChange"]>;
|
|
215
111
|
|
|
216
|
-
|
|
112
|
+
await renderGroup(props);
|
|
217
113
|
|
|
218
|
-
await act(async () => {
|
|
219
|
-
render(
|
|
220
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
221
|
-
<ArrayFieldGroup {...defaultProps} />
|
|
222
|
-
</ThemeProvider>,
|
|
223
|
-
{ store },
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
expect(screen.getByTestId("button-line-inverse")).toBeTruthy();
|
|
228
114
|
fireEvent.click(screen.getByTestId("button-line-inverse"));
|
|
229
115
|
expect(onChangeMock).toHaveBeenCalled();
|
|
230
116
|
});
|
|
231
117
|
|
|
232
|
-
it("should call
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
id: "b71c3180-1cf2-4f69-8248-5fcb6e579377",
|
|
236
|
-
semTitle: "dsa",
|
|
237
|
-
subjects: [
|
|
238
|
-
{
|
|
239
|
-
id: "3ae4be00-9216-422d-a841-0448e305af08",
|
|
240
|
-
subjTeachers: {
|
|
241
|
-
mode: "manual",
|
|
242
|
-
fixed: [4476, 4425],
|
|
243
|
-
},
|
|
244
|
-
subjTitle: "as",
|
|
245
|
-
subjType: "das",
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
},
|
|
249
|
-
];
|
|
250
|
-
const fieldProps = {
|
|
251
|
-
objKey: "semTitle",
|
|
252
|
-
fieldType: "TextField",
|
|
253
|
-
innerFields: [],
|
|
254
|
-
field: {
|
|
255
|
-
title: "Title",
|
|
256
|
-
type: "TextField",
|
|
257
|
-
key: "semTitle",
|
|
258
|
-
mandatory: "true",
|
|
259
|
-
isTitle: true,
|
|
260
|
-
},
|
|
261
|
-
title: "Title",
|
|
262
|
-
type: "TextField",
|
|
263
|
-
mandatory: "true",
|
|
264
|
-
isTitle: true,
|
|
265
|
-
theme: "default-theme",
|
|
266
|
-
};
|
|
267
|
-
defaultProps.innerFields = [<FieldsBehavior {...fieldProps} key={1} />];
|
|
268
|
-
defaultProps.arrayType = "inline";
|
|
269
|
-
const onChangeMock = defaultProps.onChange as jest.MockedFunction<(value: Record<string, unknown>[]) => void>;
|
|
118
|
+
it("should call onChange when deleting an inline item", async () => {
|
|
119
|
+
const props = makeProps({ arrayType: "inline", innerFields: [makeField()] });
|
|
120
|
+
const onChangeMock = props.onChange as jest.MockedFunction<IProps["onChange"]>;
|
|
270
121
|
|
|
271
|
-
await
|
|
272
|
-
render(
|
|
273
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
274
|
-
<ArrayFieldGroup {...defaultProps} />
|
|
275
|
-
</ThemeProvider>,
|
|
276
|
-
{ store },
|
|
277
|
-
);
|
|
278
|
-
});
|
|
122
|
+
await renderGroup(props);
|
|
279
123
|
|
|
280
124
|
expect(screen.getByTestId("arrayFieldInline")).toBeTruthy();
|
|
281
125
|
fireEvent.click(screen.getByTestId("icon-action-component"));
|
|
282
126
|
expect(onChangeMock).toHaveBeenCalled();
|
|
283
127
|
});
|
|
284
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("ArrayFieldGroup maxItems constraint", () => {
|
|
131
|
+
it("should keep the add button enabled with no tooltip when maxItems is undefined", async () => {
|
|
132
|
+
await renderGroup(makeProps({ value: sampleValue, maxItems: undefined }));
|
|
133
|
+
|
|
134
|
+
const button = screen.getByTestId("button-line-inverse") as HTMLButtonElement;
|
|
135
|
+
expect(button.disabled).toBe(false);
|
|
136
|
+
expect(screen.queryByTestId("tooltip-component")).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should keep the add button enabled while items are below maxItems", async () => {
|
|
140
|
+
await renderGroup(makeProps({ value: sampleValue, maxItems: 2 }));
|
|
141
|
+
|
|
142
|
+
const button = screen.getByTestId("button-line-inverse") as HTMLButtonElement;
|
|
143
|
+
expect(button.disabled).toBe(false);
|
|
144
|
+
expect(screen.queryByTestId("tooltip-component")).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should disable the add button and show a tooltip once maxItems is reached", async () => {
|
|
148
|
+
await renderGroup(makeProps({ value: sampleValue, maxItems: 1 }));
|
|
149
|
+
|
|
150
|
+
const button = screen.getByTestId("button-line-inverse") as HTMLButtonElement;
|
|
151
|
+
expect(button.disabled).toBe(true);
|
|
152
|
+
expect(screen.getByTestId("tooltip-component")).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should not call onChange when the add button is clicked at maxItems", async () => {
|
|
156
|
+
const props = makeProps({ value: sampleValue, maxItems: 1 });
|
|
157
|
+
const onChangeMock = props.onChange as jest.MockedFunction<IProps["onChange"]>;
|
|
158
|
+
|
|
159
|
+
await renderGroup(props);
|
|
160
|
+
|
|
161
|
+
fireEvent.click(screen.getByTestId("button-line-inverse"));
|
|
162
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import { connect } from "react-redux";
|
|
3
3
|
|
|
4
4
|
import { IconAction } from "@ax/components";
|
|
5
|
-
import { ISite } from "@ax/types";
|
|
6
5
|
import { pageEditorActions } from "@ax/containers/PageEditor";
|
|
7
|
-
import {
|
|
6
|
+
import type { ISite } from "@ax/types";
|
|
7
|
+
|
|
8
|
+
import { getArrayFields } from "../helpers";
|
|
8
9
|
|
|
9
10
|
import * as S from "./style";
|
|
10
11
|
|
|
12
|
+
const getInlineKey = (field: any): string => field.props.objKey;
|
|
13
|
+
|
|
11
14
|
const ArrayFieldInline = (props: IProps): JSX.Element => {
|
|
12
15
|
const {
|
|
13
16
|
fields,
|
|
@@ -25,46 +28,22 @@ const ArrayFieldInline = (props: IProps): JSX.Element => {
|
|
|
25
28
|
|
|
26
29
|
const deleteItem = () => handleDelete(index);
|
|
27
30
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const hideField = () => {
|
|
46
|
-
const isContainer = field.props.type === "ComponentContainer";
|
|
47
|
-
|
|
48
|
-
const nullValue = isContainer && item[key] ? getNullValue(item[key], hasMultipleOptions) : null;
|
|
49
|
-
onChange({ [key]: nullValue });
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
...field,
|
|
54
|
-
props: {
|
|
55
|
-
...field.props,
|
|
56
|
-
value: item[key],
|
|
57
|
-
objKey: key,
|
|
58
|
-
onChange: handleChange,
|
|
59
|
-
innerFields,
|
|
60
|
-
site,
|
|
61
|
-
actions: actionsField,
|
|
62
|
-
hideField,
|
|
63
|
-
},
|
|
64
|
-
key,
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
};
|
|
31
|
+
const renderedFields = useMemo(
|
|
32
|
+
() =>
|
|
33
|
+
getArrayFields({
|
|
34
|
+
fields,
|
|
35
|
+
item,
|
|
36
|
+
index,
|
|
37
|
+
parentKey,
|
|
38
|
+
site,
|
|
39
|
+
actions,
|
|
40
|
+
onChange,
|
|
41
|
+
addComponentInArray,
|
|
42
|
+
replaceModuleInArray,
|
|
43
|
+
getKey: getInlineKey,
|
|
44
|
+
}),
|
|
45
|
+
[fields, item, index, parentKey, site, actions, onChange, addComponentInArray, replaceModuleInArray],
|
|
46
|
+
);
|
|
68
47
|
|
|
69
48
|
return (
|
|
70
49
|
<S.Wrapper data-testid="arrayFieldInline">
|
|
@@ -72,7 +51,7 @@ const ArrayFieldInline = (props: IProps): JSX.Element => {
|
|
|
72
51
|
<S.IconWrapper>
|
|
73
52
|
<IconAction icon="delete" onClick={deleteItem} size="s" disabled={disabled} />
|
|
74
53
|
</S.IconWrapper>
|
|
75
|
-
{
|
|
54
|
+
{renderedFields}
|
|
76
55
|
</S.Content>
|
|
77
56
|
</S.Wrapper>
|
|
78
57
|
);
|
|
@@ -82,7 +61,7 @@ interface IProps {
|
|
|
82
61
|
fields: any[];
|
|
83
62
|
item: Record<string, unknown>;
|
|
84
63
|
index: number;
|
|
85
|
-
onChange: (value: Record<string, unknown
|
|
64
|
+
onChange: (value: Record<string, unknown>, index: number) => void;
|
|
86
65
|
handleDelete: (index: number) => void;
|
|
87
66
|
site: ISite | null;
|
|
88
67
|
disabled?: boolean;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import { connect } from "react-redux";
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import { pageEditorActions } from "@ax/containers/PageEditor";
|
|
5
|
-
import {
|
|
5
|
+
import type { ISite } from "@ax/types";
|
|
6
|
+
|
|
7
|
+
import { getArrayFields } from "../helpers";
|
|
6
8
|
|
|
7
9
|
import * as S from "./style";
|
|
8
10
|
|
|
11
|
+
const getItemKey = (field: any): string => field.key;
|
|
12
|
+
|
|
9
13
|
const ArrayFieldItem = (props: IProps): JSX.Element => {
|
|
10
14
|
const {
|
|
11
15
|
fields,
|
|
@@ -46,46 +50,22 @@ const ArrayFieldItem = (props: IProps): JSX.Element => {
|
|
|
46
50
|
return `${name} ${index + 1}`;
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const hideField = () => {
|
|
67
|
-
const isContainer = field.props.type === "ComponentContainer";
|
|
68
|
-
|
|
69
|
-
const nullValue = isContainer && item[key] ? getNullValue(item[key], hasMultipleOptions) : null;
|
|
70
|
-
onChange({ [key]: nullValue });
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
...field,
|
|
75
|
-
props: {
|
|
76
|
-
...field.props,
|
|
77
|
-
value: item[key],
|
|
78
|
-
objKey: key,
|
|
79
|
-
onChange: handleChange,
|
|
80
|
-
innerFields,
|
|
81
|
-
site,
|
|
82
|
-
actions: actionsField,
|
|
83
|
-
hideField,
|
|
84
|
-
},
|
|
85
|
-
key,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
};
|
|
53
|
+
const renderedFields = useMemo(
|
|
54
|
+
() =>
|
|
55
|
+
getArrayFields({
|
|
56
|
+
fields,
|
|
57
|
+
item,
|
|
58
|
+
index,
|
|
59
|
+
parentKey,
|
|
60
|
+
site,
|
|
61
|
+
actions,
|
|
62
|
+
onChange,
|
|
63
|
+
addComponentInArray,
|
|
64
|
+
replaceModuleInArray,
|
|
65
|
+
getKey: getItemKey,
|
|
66
|
+
}),
|
|
67
|
+
[fields, item, index, parentKey, site, actions, onChange, addComponentInArray, replaceModuleInArray],
|
|
68
|
+
);
|
|
89
69
|
|
|
90
70
|
return (
|
|
91
71
|
<S.Wrapper data-testid="arrayFieldItem" isOpen={isOpen === index}>
|
|
@@ -93,7 +73,7 @@ const ArrayFieldItem = (props: IProps): JSX.Element => {
|
|
|
93
73
|
{getItemTitle(fields)}
|
|
94
74
|
<S.StyledActionMenu icon="more" options={menuOptions} />
|
|
95
75
|
</S.Title>
|
|
96
|
-
<S.Content isOpen={isOpen === index}>{
|
|
76
|
+
<S.Content isOpen={isOpen === index}>{renderedFields}</S.Content>
|
|
97
77
|
</S.Wrapper>
|
|
98
78
|
);
|
|
99
79
|
};
|
|
@@ -103,7 +83,7 @@ interface IProps {
|
|
|
103
83
|
fields: any[];
|
|
104
84
|
item: Record<string, unknown>;
|
|
105
85
|
index: number;
|
|
106
|
-
onChange: (value: Record<string, unknown
|
|
86
|
+
onChange: (value: Record<string, unknown>, index: number) => void;
|
|
107
87
|
handleDelete: (index: number) => void;
|
|
108
88
|
isOpen: number | null;
|
|
109
89
|
setIsOpen: (index: number | null) => void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getNullValue } from "@ax/helpers";
|
|
2
|
+
import type { ISite } from "@ax/types";
|
|
3
|
+
|
|
4
|
+
interface IGetArrayFieldsParams {
|
|
5
|
+
fields: any[];
|
|
6
|
+
item: Record<string, unknown>;
|
|
7
|
+
index: number;
|
|
8
|
+
parentKey: string;
|
|
9
|
+
site: ISite | null;
|
|
10
|
+
actions: any;
|
|
11
|
+
onChange: (value: Record<string, unknown>, index: number) => void;
|
|
12
|
+
addComponentInArray: (
|
|
13
|
+
type: string,
|
|
14
|
+
key: string,
|
|
15
|
+
parentKey: string,
|
|
16
|
+
index: number,
|
|
17
|
+
isMultiple: boolean,
|
|
18
|
+
) => void;
|
|
19
|
+
replaceModuleInArray: (module: string, parent: any, key: string, parentKey: string, index: number) => void;
|
|
20
|
+
/** Each array variant resolves the field key differently (props.objKey vs field.key). */
|
|
21
|
+
getKey: (field: any) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds the inner field descriptors for an array item. Shared by ArrayFieldInline
|
|
26
|
+
* and ArrayFieldItem so the transformation lives in a single place.
|
|
27
|
+
*/
|
|
28
|
+
export const getArrayFields = (params: IGetArrayFieldsParams): any[] => {
|
|
29
|
+
const { fields, item, index, parentKey, site, actions, onChange, addComponentInArray, replaceModuleInArray, getKey } =
|
|
30
|
+
params;
|
|
31
|
+
|
|
32
|
+
return fields.map((field: any) => {
|
|
33
|
+
const key = getKey(field);
|
|
34
|
+
|
|
35
|
+
const handleChange = (newValue: any) => onChange({ [key]: newValue }, index);
|
|
36
|
+
|
|
37
|
+
const innerFields = field.props.innerFields
|
|
38
|
+
? getArrayFields({ ...params, fields: field.props.innerFields })
|
|
39
|
+
: undefined;
|
|
40
|
+
const hasMultipleOptions = field.props.whiteList && field.props.whiteList.length > 1;
|
|
41
|
+
|
|
42
|
+
const actionsField = {
|
|
43
|
+
...actions,
|
|
44
|
+
addComponentAction: (component: any, key: string) =>
|
|
45
|
+
addComponentInArray(component, key, parentKey, index, hasMultipleOptions),
|
|
46
|
+
replaceModuleAction: (module: string, parent: any, key: string) =>
|
|
47
|
+
replaceModuleInArray(module, parent, key, parentKey, index),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const hideField = () => {
|
|
51
|
+
const isContainer = field.props.type === "ComponentContainer";
|
|
52
|
+
|
|
53
|
+
const nullValue = isContainer && item[key] ? getNullValue(item[key], hasMultipleOptions) : null;
|
|
54
|
+
onChange({ [key]: nullValue }, index);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...field,
|
|
59
|
+
props: {
|
|
60
|
+
...field.props,
|
|
61
|
+
value: item[key],
|
|
62
|
+
objKey: key,
|
|
63
|
+
onChange: handleChange,
|
|
64
|
+
innerFields,
|
|
65
|
+
site,
|
|
66
|
+
actions: actionsField,
|
|
67
|
+
hideField,
|
|
68
|
+
},
|
|
69
|
+
key,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import { Button, FieldsDivider } from "@ax/components";
|
|
3
|
+
import { Button, FieldsDivider, Tooltip } from "@ax/components";
|
|
4
4
|
import { getDefaultSchema } from "@ax/helpers";
|
|
5
5
|
import type { ISite } from "@ax/types";
|
|
6
6
|
|
|
@@ -25,11 +25,19 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
25
25
|
objKey,
|
|
26
26
|
selectedContent,
|
|
27
27
|
actions,
|
|
28
|
+
maxItems,
|
|
28
29
|
} = props;
|
|
29
30
|
|
|
30
31
|
const [items, setItems] = useState<any[]>([]);
|
|
31
32
|
const [isOpen, setIsOpen] = useState<number | null>(null);
|
|
32
33
|
|
|
34
|
+
// Keeps the latest items accessible from stable callbacks without
|
|
35
|
+
// re-creating them on every change (which would break child memoization).
|
|
36
|
+
const itemsRef = useRef(items);
|
|
37
|
+
itemsRef.current = items;
|
|
38
|
+
|
|
39
|
+
const hasMaxItems = !!maxItems && items.length >= maxItems;
|
|
40
|
+
|
|
33
41
|
// biome-ignore lint/correctness/useExhaustiveDependencies: false positive
|
|
34
42
|
useEffect(() => {
|
|
35
43
|
const initialValue = value ? Object.values(value) : [];
|
|
@@ -40,6 +48,8 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
40
48
|
}, [editorID]);
|
|
41
49
|
|
|
42
50
|
const handleClick = () => {
|
|
51
|
+
if (hasMaxItems) return;
|
|
52
|
+
|
|
43
53
|
let newComponent = { id: uuidv4() };
|
|
44
54
|
if (selectedContent) {
|
|
45
55
|
const defaultValues = getDefaultSchema(selectedContent.component);
|
|
@@ -49,28 +59,34 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
49
59
|
: newComponent;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
const newItems = [...
|
|
62
|
+
const newItems = [...itemsRef.current, newComponent];
|
|
53
63
|
setItems(newItems);
|
|
54
64
|
onChange(newItems);
|
|
55
65
|
setIsOpen(newItems.length - 1);
|
|
56
66
|
};
|
|
57
67
|
|
|
58
|
-
const handleChange = (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const handleChange = useCallback(
|
|
69
|
+
(newValue: Record<string, unknown>, index: number) => {
|
|
70
|
+
const updatedItems = [...itemsRef.current];
|
|
71
|
+
updatedItems[index] = {
|
|
72
|
+
...updatedItems[index],
|
|
73
|
+
...newValue,
|
|
74
|
+
};
|
|
75
|
+
setItems(updatedItems);
|
|
76
|
+
onChange(updatedItems);
|
|
77
|
+
},
|
|
78
|
+
[onChange],
|
|
79
|
+
);
|
|
67
80
|
|
|
68
|
-
const handleDelete = (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
const handleDelete = useCallback(
|
|
82
|
+
(index: number) => {
|
|
83
|
+
const updatedItems = [...itemsRef.current];
|
|
84
|
+
updatedItems.splice(index, 1);
|
|
85
|
+
setItems(updatedItems);
|
|
86
|
+
onChange(updatedItems);
|
|
87
|
+
},
|
|
88
|
+
[onChange],
|
|
89
|
+
);
|
|
74
90
|
|
|
75
91
|
const DividerComponent = () => (divider ? <FieldsDivider data={divider} /> : null);
|
|
76
92
|
|
|
@@ -79,14 +95,13 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
79
95
|
<DividerComponent />
|
|
80
96
|
<div>
|
|
81
97
|
{items?.map((item, index) => {
|
|
82
|
-
const handleFieldChange = (newValue: Record<string, unknown>) => handleChange(newValue, index);
|
|
83
98
|
return arrayType === "inline" ? (
|
|
84
99
|
<ArrayFieldInline
|
|
85
100
|
key={item.id}
|
|
86
101
|
fields={innerFields}
|
|
87
102
|
item={item}
|
|
88
103
|
index={index}
|
|
89
|
-
onChange={
|
|
104
|
+
onChange={handleChange}
|
|
90
105
|
handleDelete={handleDelete}
|
|
91
106
|
site={site}
|
|
92
107
|
disabled={disabled}
|
|
@@ -100,7 +115,7 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
100
115
|
fields={innerFields}
|
|
101
116
|
item={item}
|
|
102
117
|
index={index}
|
|
103
|
-
onChange={
|
|
118
|
+
onChange={handleChange}
|
|
104
119
|
handleDelete={handleDelete}
|
|
105
120
|
isOpen={isOpen}
|
|
106
121
|
setIsOpen={setIsOpen}
|
|
@@ -113,9 +128,11 @@ const ArrayFieldGroup = (props: IProps): JSX.Element => {
|
|
|
113
128
|
})}
|
|
114
129
|
</div>
|
|
115
130
|
<S.ButtonWrapper>
|
|
116
|
-
<
|
|
117
|
-
{
|
|
118
|
-
|
|
131
|
+
<Tooltip content={hasMaxItems ? "Item limit reached" : undefined}>
|
|
132
|
+
<Button type="button" onClick={handleClick} buttonStyle="line" disabled={disabled || hasMaxItems}>
|
|
133
|
+
{`Add ${name}`}
|
|
134
|
+
</Button>
|
|
135
|
+
</Tooltip>
|
|
119
136
|
</S.ButtonWrapper>
|
|
120
137
|
</div>
|
|
121
138
|
);
|
|
@@ -134,6 +151,7 @@ export interface IProps {
|
|
|
134
151
|
objKey: string;
|
|
135
152
|
selectedContent: any;
|
|
136
153
|
actions: any;
|
|
154
|
+
maxItems?: number;
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
export default ArrayFieldGroup;
|