Skip to content

Commit 75a91d9

Browse files
authored
Merge pull request #515 from zendesk/gianluca/GG-3785-conditional-fields-multi-level
Fixed conditional fields
2 parents fbed005 + f0e6f61 commit 75a91d9

File tree

9 files changed

+423
-61
lines changed

9 files changed

+423
-61
lines changed

assets/new-request-form-bundle.js

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ const config = {
22
coverageProvider: "v8",
33
testEnvironment: "jsdom",
44
preset: "rollup-jest",
5+
transform: {
6+
"^.+.tsx?$": ["ts-jest", {}],
7+
},
58
setupFilesAfterEnv: ["@testing-library/jest-dom"],
69
};
710

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"rollup-plugin-sass": "1.12.18",
9292
"sass": "1.58.3",
9393
"semantic-release": "19.0.5",
94+
"ts-jest": "^29.2.4",
9495
"typescript": "^5.1.6",
9596
"url-pattern": "1.0.3",
9697
"vinyl": "^3.0.0",

src/modules/new-request-form/NewRequestForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Alert } from "@zendeskgarden/react-notifications";
1313
import { useFormSubmit } from "./useFormSubmit";
1414
import { usePrefilledTicketFields } from "./usePrefilledTicketFields";
1515
import { Attachments } from "./fields/attachments/Attachments";
16-
import { useEndUserConditions } from "./useEndUserConditions";
16+
import { getVisibleFields } from "./getVisibleFields";
1717
import { DatePicker } from "./fields/DatePicker";
1818
import { CcField } from "./fields/cc-field/CcField";
1919
import { CreditCard } from "./fields/CreditCard";
@@ -108,7 +108,7 @@ export function NewRequestForm({
108108
prefilledOrganizationField
109109
);
110110
const [dueDateField, setDueDateField] = useState(prefilledDueDateField);
111-
const visibleFields = useEndUserConditions(ticketFields, end_user_conditions);
111+
const visibleFields = getVisibleFields(ticketFields, end_user_conditions);
112112
const { formRefCallback, handleSubmit } = useFormSubmit(ticketFields);
113113
const { t } = useTranslation();
114114

src/modules/new-request-form/data-types/Field.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface Field {
22
id: number;
33
name: string;
44
value?: string | string[] | boolean;
5-
error: string;
5+
error: string | null;
66
label: string;
77
required: boolean;
88
description: string;
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import type { EndUserCondition, Field } from "./data-types";
2+
import { getVisibleFields } from "./getVisibleFields";
3+
4+
const dropdownField: Field = {
5+
id: 123,
6+
name: "request[custom_fields][123]",
7+
required: false,
8+
error: null,
9+
label: "Dropdown Field",
10+
description: "",
11+
type: "tagger",
12+
options: [
13+
{ name: "One", value: "one" },
14+
{ name: "Two", value: "two" },
15+
],
16+
};
17+
18+
const secondDropdownField: Field = {
19+
id: 456,
20+
name: "request[custom_fields][456]",
21+
required: false,
22+
error: null,
23+
label: "Second Dropdown Field",
24+
description: "",
25+
type: "tagger",
26+
options: [
27+
{ name: "Three", value: "three" },
28+
{ name: "Four", value: "four" },
29+
],
30+
};
31+
32+
const textField: Field = {
33+
id: 789,
34+
name: "request[custom_fields][789]",
35+
required: false,
36+
error: null,
37+
label: "Text Field",
38+
description: "",
39+
type: "text",
40+
options: [],
41+
};
42+
43+
const integerField: Field = {
44+
id: 101,
45+
name: "request[custom_fields][101]",
46+
required: false,
47+
error: null,
48+
label: "Number Field",
49+
description: "",
50+
type: "number",
51+
options: [],
52+
};
53+
54+
describe("getVisibleFields", () => {
55+
it("should return the same fields if no end user conditions are provided", () => {
56+
const fields = [
57+
dropdownField,
58+
secondDropdownField,
59+
textField,
60+
integerField,
61+
];
62+
const endUserConditions: EndUserCondition[] = [];
63+
64+
const result = getVisibleFields(fields, endUserConditions);
65+
66+
expect(result).toEqual(fields);
67+
});
68+
69+
it("should show fields and set required if the value matches", () => {
70+
const fields = [
71+
{ ...dropdownField, value: "one" },
72+
{ ...secondDropdownField, value: "three" },
73+
{ ...textField, required: true },
74+
integerField,
75+
];
76+
const endUserConditions: EndUserCondition[] = [
77+
{
78+
parent_field_id: 123,
79+
parent_field_type: "tagger",
80+
value: "one",
81+
child_fields: [{ id: 456, is_required: true }],
82+
},
83+
{
84+
parent_field_id: 456,
85+
parent_field_type: "tagger",
86+
value: "three",
87+
child_fields: [{ id: 789, is_required: false }],
88+
},
89+
];
90+
91+
const result = getVisibleFields(fields, endUserConditions);
92+
93+
expect(result).toEqual([
94+
{ ...dropdownField, value: "one" },
95+
{ ...secondDropdownField, value: "three", required: true },
96+
textField,
97+
integerField,
98+
]);
99+
});
100+
101+
it("should hide fields if the value does not match", () => {
102+
const fields = [
103+
{ ...dropdownField, value: "one" },
104+
{ ...secondDropdownField, value: "three" },
105+
textField,
106+
integerField,
107+
];
108+
const endUserConditions: EndUserCondition[] = [
109+
{
110+
parent_field_id: 123,
111+
parent_field_type: "tagger",
112+
value: "two",
113+
child_fields: [{ id: 456, is_required: true }],
114+
},
115+
{
116+
parent_field_id: 456,
117+
parent_field_type: "tagger",
118+
value: "four",
119+
child_fields: [{ id: 789, is_required: false }],
120+
},
121+
];
122+
123+
const result = getVisibleFields(fields, endUserConditions);
124+
125+
expect(result).toEqual([{ ...dropdownField, value: "one" }, integerField]);
126+
});
127+
128+
it("should not change required if the field is not part of some condition", () => {
129+
const fields = [
130+
{ ...dropdownField, value: "one", required: true },
131+
{ ...secondDropdownField, value: "three" },
132+
{ ...textField, required: true },
133+
{ ...integerField, required: false },
134+
];
135+
const endUserConditions: EndUserCondition[] = [
136+
{
137+
parent_field_id: 123,
138+
parent_field_type: "tagger",
139+
value: "one",
140+
child_fields: [{ id: 456, is_required: true }],
141+
},
142+
];
143+
144+
const result = getVisibleFields(fields, endUserConditions);
145+
146+
expect(result).toEqual([
147+
{ ...dropdownField, value: "one", required: true },
148+
{ ...secondDropdownField, value: "three", required: true },
149+
{ ...textField, required: true },
150+
{ ...integerField, required: false },
151+
]);
152+
});
153+
154+
it("should hide fields with multi level conditions when the value on parent doesn't match", () => {
155+
const fields = [
156+
{ ...dropdownField, value: "one" },
157+
{ ...secondDropdownField, value: "three" },
158+
{ ...textField, value: "text" },
159+
integerField,
160+
];
161+
const endUserConditions: EndUserCondition[] = [
162+
{
163+
parent_field_id: 123,
164+
parent_field_type: "tagger",
165+
value: "two",
166+
child_fields: [
167+
{
168+
id: 456,
169+
is_required: true,
170+
},
171+
],
172+
},
173+
{
174+
parent_field_id: 456,
175+
parent_field_type: "tagger",
176+
value: "three",
177+
child_fields: [
178+
{
179+
id: 789,
180+
is_required: false,
181+
},
182+
],
183+
},
184+
{
185+
parent_field_id: 789,
186+
parent_field_type: "text",
187+
value: "text",
188+
child_fields: [
189+
{
190+
id: 101,
191+
is_required: true,
192+
},
193+
],
194+
},
195+
];
196+
197+
const result = getVisibleFields(fields, endUserConditions);
198+
199+
expect(result).toEqual([{ ...dropdownField, value: "one" }]);
200+
});
201+
202+
it("should not hide fields with multi level conditions when a second condition is met", () => {
203+
const fields = [
204+
{ ...dropdownField, value: "one" },
205+
{ ...secondDropdownField, value: "three" },
206+
{ ...textField, value: "text" },
207+
integerField,
208+
];
209+
210+
const endUserConditions: EndUserCondition[] = [
211+
{
212+
parent_field_id: 123,
213+
parent_field_type: "tagger",
214+
value: "two",
215+
child_fields: [
216+
{
217+
id: 456,
218+
is_required: true,
219+
},
220+
],
221+
},
222+
{
223+
parent_field_id: 456,
224+
parent_field_type: "tagger",
225+
value: "three",
226+
child_fields: [
227+
{
228+
id: 789,
229+
is_required: false,
230+
},
231+
],
232+
},
233+
{
234+
parent_field_id: 789,
235+
parent_field_type: "text",
236+
value: "text",
237+
child_fields: [
238+
{
239+
id: 101,
240+
is_required: true,
241+
},
242+
],
243+
},
244+
{
245+
parent_field_id: 123,
246+
parent_field_type: "tagger",
247+
value: "one",
248+
child_fields: [
249+
{
250+
id: 789,
251+
is_required: false,
252+
},
253+
{
254+
id: 101,
255+
is_required: true,
256+
},
257+
],
258+
},
259+
];
260+
261+
const result = getVisibleFields(fields, endUserConditions);
262+
263+
expect(result).toEqual([
264+
{ ...dropdownField, value: "one" },
265+
{ ...textField, value: "text" },
266+
{ ...integerField, required: true },
267+
]);
268+
});
269+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { EndUserCondition, Field } from "./data-types";
2+
3+
function getFieldConditions(
4+
fieldId: number,
5+
endUserConditions: EndUserCondition[]
6+
): EndUserCondition[] {
7+
return endUserConditions.filter((condition) => {
8+
return condition.child_fields.some((child) => child.id === fieldId);
9+
});
10+
}
11+
12+
function getAppliedConditions(
13+
fieldConditions: EndUserCondition[],
14+
allConditions: EndUserCondition[],
15+
fields: Field[]
16+
): EndUserCondition[] {
17+
return fieldConditions.filter((condition) => {
18+
const parentField = fields.find(
19+
(field) => field.id === condition.parent_field_id
20+
);
21+
22+
if (!parentField) {
23+
return false;
24+
}
25+
26+
const parentFieldConditions = getFieldConditions(
27+
parentField.id,
28+
allConditions
29+
);
30+
31+
// the condition is applied if the parent field value matches the condition value
32+
// and if the parent field has no conditions or if the parent field conditions are met
33+
return (
34+
parentField.value === condition.value &&
35+
(parentFieldConditions.length === 0 ||
36+
getAppliedConditions(parentFieldConditions, allConditions, fields)
37+
.length > 0)
38+
);
39+
});
40+
}
41+
42+
export function getVisibleFields(
43+
fields: Field[],
44+
endUserConditions: EndUserCondition[]
45+
): Field[] {
46+
if (endUserConditions.length === 0) {
47+
return fields;
48+
}
49+
50+
return fields.reduce((acc: Field[], field) => {
51+
const fieldConditions = getFieldConditions(field.id, endUserConditions);
52+
53+
if (fieldConditions.length === 0) {
54+
return [...acc, field];
55+
}
56+
57+
const appliedConditions = getAppliedConditions(
58+
fieldConditions,
59+
endUserConditions,
60+
fields
61+
);
62+
63+
if (appliedConditions.length > 0) {
64+
return [
65+
...acc,
66+
{
67+
...field,
68+
required: appliedConditions.some((condition) =>
69+
condition.child_fields.some(
70+
(child) => child.id == field.id && child.is_required
71+
)
72+
),
73+
},
74+
];
75+
}
76+
77+
return acc;
78+
}, []);
79+
}

0 commit comments

Comments
 (0)