Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 95 additions & 114 deletions frontend/src/components/applyForm/FormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,131 +32,112 @@ export const FormFields = ({
uiSchema: UiSchema;
formContext?: RootBudgetFormContext;
}) => {
let renderedFields: JSX.Element[] = [];
let requiredFieldPaths = [];

try {
let acc: JSX.Element[] = [];
requiredFieldPaths = getRequiredProperties(schema);
} catch (e) {
console.error(e);
return (
<Alert data-testid="alert" type="error" heading="Error" headingLevel="h4">
Error rendering form
</Alert>
);
}

const requiredFieldPaths = getRequiredProperties(schema);
const buildFormTree = (
uiSchema: UiSchema,
parent: { label: string; name: string; description?: string } | null,
) => {
if (!Array.isArray(uiSchema)) {
throw new Error("ui schema element is not an array");
}

const buildFormTree = (
uiSchema:
| UiSchema
| {
children: UiSchema;
label: string;
name: string;
description?: string;
},
parent: { label: string; name: string; description?: string } | null,
) => {
if (
!Array.isArray(uiSchema) &&
typeof uiSchema === "object" &&
"children" in uiSchema
) {
buildFormTree(uiSchema.children, {
label: uiSchema.label,
name: uiSchema.name,
description: uiSchema.description,
// generate fields for all schema elements that are not children of a section
uiSchema.forEach((node) => {
if ("children" in node) {
// treat as section
buildFormTree(node.children as unknown as UiSchema, {
label: node.label,
name: node.name,
description: node.description,
});
} else if (Array.isArray(uiSchema)) {
uiSchema.forEach((node) => {
if ("children" in node) {
buildFormTree(node.children as unknown as UiSchema, {
label: node.label,
name: node.name,
description: node.description,
});
} else if (!parent && ("definition" in node || "schema" in node)) {
const requiredField = isFieldRequired(
(node.definition || node.schema.title || "") as string,
requiredFieldPaths,
);
const widgetConfig = getFieldConfig({
uiFieldObject: node,
formSchema: schema,
errors: errors ?? null,
formData,
requiredField,
});

const field = renderWidget({
type: widgetConfig.type,
props: { ...widgetConfig.props, formContext },
definition: node.definition,
});

if (field) {
acc = [
...acc,
<React.Fragment key={node.name}>{field}</React.Fragment>,
];
}
}
} else if (!("definition" in node || "schema" in node)) {
throw new Error("child field missing definition and schema");
} else if (!parent) {
// treat as valid non-child field
const requiredField = isFieldRequired(
(node.definition || node.schema.title || "") as string,
requiredFieldPaths,
);
const widgetConfig = getFieldConfig({
uiFieldObject: node,
formSchema: schema,
errors: errors ?? null,
formData,
requiredField,
});

if (parent) {
const childAcc: JSX.Element[] = [];
const keys: number[] = [];
const row = uiSchema.map((node) => {
if ("children" in node) {
acc.forEach((item, key) => {
if (item && item.key === `${node.name}-wrapper`) {
keys.push(key);
}
});
return null;
} else {
const requiredField = isFieldRequired(
(node.definition || node.schema.title || "") as string,
requiredFieldPaths,
);
const widgetConfig = getFieldConfig({
uiFieldObject: node,
formSchema: schema,
errors: errors ?? null,
formData,
requiredField,
});

return renderWidget({
type: widgetConfig.type,
props: { ...widgetConfig.props, formContext },
definition: node.definition,
});
}
});
const field = renderWidget({
type: widgetConfig.type,
props: { ...widgetConfig.props, formContext },
definition: node.definition,
});

if (keys.length) {
keys.forEach((key) => {
childAcc.push(acc[key]);
delete acc[key];
});
acc = [
...acc,
wrapSection({
label: parent.label,
fieldName: parent.name,
description: parent.description,
tree: <>{childAcc}</>,
}),
];
} else {
acc = [
...acc,
wrapSection({
label: parent.label,
fieldName: parent.name,
tree: <>{row}</>,
description: parent.description,
}),
];
}
if (field) {
renderedFields = [
...renderedFields,
<React.Fragment key={node.name}>{field}</React.Fragment>,
];
}
}
};
});

// if top level node is a section, the uiSchema passed will represent the section children,
// and the section definition will be in the parent. Fields will be rendered (rather than in the
// iteration above) and wrapped in a section here.
if (parent) {
const sectionFields = uiSchema.map((node) => {
// assume that any child fields of a section are defined fields, no support for sub sections
if (!("definition" in node || "schema" in node)) {
throw new Error("section child is not a defined field");
}

const requiredField = isFieldRequired(
(node.definition || node.schema.title || "") as string,
requiredFieldPaths,
);
const widgetConfig = getFieldConfig({
uiFieldObject: node,
formSchema: schema,
errors: errors ?? null,
formData,
requiredField,
});

return renderWidget({
type: widgetConfig.type,
props: { ...widgetConfig.props, formContext },
definition: node.definition,
});
});

renderedFields = [
...renderedFields,
wrapSection({
label: parent.label,
fieldName: parent.name,
sectionFields: <>{sectionFields}</>,
description: parent.description,
}),
];
}
};

try {
buildFormTree(uiSchema, null);
return acc;
return renderedFields;
} catch (e) {
console.error(e);
return (
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/applyForm/formDataToJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ const getFieldType = (
formSchema: RJSFSchema,
parentKey?: string,
): string => {
// this assumes that all elements of an array will have the same type
// for fields that represent array items in the form schema, we need to reference
// the "items" property of the field's schema definition. The form data key will
// include an index into the array - switching that out for "items" will allow us to
// point to the correct place in the form schema.
// needed to handle activity line items in the budget form
const keyWithArrayNotationStripped = currentKey.replace(
/\[\d+\]/g,
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/components/applyForm/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const formatValidationWarning = (
return validationWarningOverrides(message, fieldName, title);
};

// formats warning messages for more helpful display
const validationWarningOverrides = (
message: string,
fieldName: string,
Expand Down Expand Up @@ -439,12 +440,21 @@ export const getFieldConfig = ({
let value = "" as string | number | object | undefined;
let rawErrors: string[] | FormattedFormValidationWarning[] = [];

if (fieldType === "multiField" && definition && Array.isArray(definition)) {
fieldName = uiFieldObject.name ? uiFieldObject.name : "";
if (!fieldName) {
console.error("name misssing from multiField definition");
throw new Error("Could not build field");
if (fieldType === "multiField") {
if (!uiFieldObject.name) {
console.error("name missing from multiField schema");
throw new Error("Could not build multifield field due to missing name");
}
if (!definition || !Array.isArray(definition)) {
console.error(
"no multiField definition, or multifield definition not an array",
definition,
);
throw new Error(
"Could not build multifield field due to missing or malformed definition",
);
}
fieldName = uiFieldObject.name;
fieldSchema = definition
.map((def) => getSchemaObjectFromPointer(formSchema, def) as RJSFSchema)
.reduce((acc, schema) => ({ ...acc, ...schema }), {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import { widgetComponents } from "./Widgets";
export const wrapSection = ({
label,
fieldName,
tree,
sectionFields,
description,
}: {
label: string;
fieldName: string;
tree: JSX.Element | undefined;
sectionFields: JSX.Element | undefined;
description?: string;
}) => {
const uniqueKey = `${fieldName}-fieldset`;
Expand All @@ -29,7 +29,7 @@ export const wrapSection = ({
label={label}
description={description}
>
{tree}
{sectionFields}
</FieldsetWidget>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ describe("wrapSection", () => {
const props = {
label: "label",
fieldName: "fieldName",
tree: <span>hi</span>,
sectionFields: <span>hi</span>,
description: "description",
};
render(wrapSection(props));
expect(mockFieldsetWidget).toHaveBeenCalledWith({
label: props.label,
fieldName: props.fieldName,
description: props.description,
children: props.tree,
children: props.sectionFields,
});
});
});
Expand Down