Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rules for Condition Groups #3737

Open
wants to merge 11 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/admin/src/plugins/formBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import editorFieldNumber from "@webiny/app-form-builder/admin/plugins/editor/for
import editorFieldRadioButtons from "@webiny/app-form-builder/admin/plugins/editor/formFields/radioButtons";
import editorFieldCheckboxes from "@webiny/app-form-builder/admin/plugins/editor/formFields/checkboxes";
import editorFieldDateTime from "@webiny/app-form-builder/admin/plugins/editor/formFields/dateTime";
import editorFieldConditionGroup from "@webiny/app-form-builder/admin/plugins/editor/formFields/conditionGroup";
import editorFieldFirstName from "@webiny/app-form-builder/admin/plugins/editor/formFields/contact/firstName";
import editorFieldLastName from "@webiny/app-form-builder/admin/plugins/editor/formFields/contact/lastName";
import editorFieldEmail from "@webiny/app-form-builder/admin/plugins/editor/formFields/contact/email";
Expand Down Expand Up @@ -84,6 +85,7 @@ export default [
editorFieldRadioButtons,
editorFieldCheckboxes,
editorFieldDateTime,
editorFieldConditionGroup,
editorFieldFirstName,
editorFieldLastName,
editorFieldEmail,
Expand Down
19 changes: 17 additions & 2 deletions apps/theme/layouts/forms/DefaultFormLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Form } from "@webiny/form";
import { FormLayoutComponent } from "@webiny/app-form-builder/types";
import styled from "@emotion/styled";
Expand Down Expand Up @@ -48,6 +48,8 @@ const DefaultFormLayout: FormLayoutComponent = ({
submit,
goToNextStep,
goToPreviousStep,
validateStepConditions,
setFormState,
isLastStep,
isFirstStep,
isMultiStepForm,
Expand All @@ -65,6 +67,12 @@ const DefaultFormLayout: FormLayoutComponent = ({
// Is the form successfully submitted?
const [formSuccess, setFormSuccess] = useState(false);

const [defData, setDefData] = useState(getDefaultValues());

useEffect(() => {
setDefData(getDefaultValues());
}, [formData.fields]);

// All form fields - an array of rows where each row is an array that contain fields.
const fields = getFields(currentStepIndex);

Expand All @@ -89,7 +97,14 @@ const DefaultFormLayout: FormLayoutComponent = ({
return (
/* "onSubmit" callback gets triggered once all the fields are valid. */
/* We also pass the default values for all fields via the getDefaultValues callback. */
<Form onSubmit={submitForm} data={getDefaultValues()}>
<Form
onSubmit={submitForm}
data={defData}
onChange={data => {
validateStepConditions(data, currentStepIndex);
setFormState(data);
}}
>
{({ submit }) => (
<Wrapper>
{isMultiStepForm && <StepTitle>{currentStep?.title}</StepTitle>}
Expand Down
2 changes: 2 additions & 0 deletions apps/theme/layouts/forms/DefaultFormLayout/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const Field = (props: FieldProps) => {
return <HiddenField {...props} />;
case "datetime":
return <DateTimeField {...props} />;
case "condition-group":
return null;
default:
return <span>Cannot render field.</span>;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/api-form-builder/src/plugins/crud/forms.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => {
steps: [
{
title: "Step 1",
layout: []
layout: [],
rules: []
}
],
settings: await new models.FormSettingsModel().toJSON(),
Expand Down
3 changes: 2 additions & 1 deletion packages/api-form-builder/src/plugins/crud/forms.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const FormStepsModel = withFields({
value: {},
instanceOf: withFields({
title: string(),
layout: object({ value: [] })
layout: object({ value: [] }),
rules: object({ value: [] })
})()
})
})();
Expand Down
44 changes: 44 additions & 0 deletions packages/api-form-builder/src/plugins/graphql/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ const plugin: GraphQLSchemaPlugin<FormBuilderContext> = {
input FbFormStepInput {
title: String
layout: [[String]]
rules: [FbFormRuleInput]
}

input FbFormRuleInput {
title: String
action: FbFormRuleActionInput
matchAll: Boolean
id: String
conditions: [FbFormConditionInput]
isValid: Boolean
}

input FbFormRuleActionInput {
type: String
value: String
}

input FbFormConditionInput {
id: String
fieldName: String
filterType: String
filterValue: String
}

input FbFieldOptionsInput {
Expand All @@ -79,6 +101,28 @@ const plugin: GraphQLSchemaPlugin<FormBuilderContext> = {
type FbFormStepType {
title: String
layout: [[String]]
rules: [FbFormRuleType]
}

type FbFormRuleType {
title: String
action: FbFormRuleActionType
matchAll: Boolean
id: String
conditions: [FbFormConditionType]
isValid: Boolean
}

type FbFormRuleActionType {
type: String
value: String
}

type FbFormConditionType {
id: String
fieldName: String
filterType: String
filterValue: String
}

type FbFormFieldType {
Expand Down
22 changes: 22 additions & 0 deletions packages/api-form-builder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,30 @@ interface FbSubmissionMeta {
interface FbFormStep {
title: string;
layout: string[][];
rules: FbFormRule[];
}

export type FbFormRuleAction = {
type: string;
value: string;
};

export type FbFormRule = {
action: FbFormRuleAction;
matchAll: boolean;
id: string;
title: string;
conditions: FbFormCondition[];
isValid: boolean;
};

export type FbFormCondition = {
id: string;
fieldName: string;
filterType: string;
filterValue: string;
};

interface FbFormFieldValidator {
name: string;
message: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { FbFormModelField, FbFormModel, FbFormModelFieldsLayout, FbFormStep } fr
interface Params {
field: FbFormModelField;
data: FbFormModel;
targetStepId: string;
containerType?: "step" | "conditionGroup";
containerId: string;
}
export default ({ field, data, targetStepId }: Params): FbFormModel => {
export default ({ field, data, containerType, containerId }: Params): FbFormModel => {
// Remove the field from fields list...
const fieldIndex = data.fields.findIndex(item => item._id === field._id);
data.fields.splice(fieldIndex, 1);
Expand All @@ -18,10 +19,13 @@ export default ({ field, data, targetStepId }: Params): FbFormModel => {

// ...and rebuild the layout object.
const layout: FbFormModelFieldsLayout = [];
const targetStepLayout = data.steps.find(s => s.id === targetStepId) as FbFormStep;
const destinationContainerLayout =
containerType === "conditionGroup"
? (data.fields.find(f => f._id === containerId)?.settings as FbFormStep)
: (data.steps.find(step => step.id === containerId) as FbFormStep);
let currentRowIndex = 0;

targetStepLayout.layout.forEach(row => {
destinationContainerLayout.layout.forEach(row => {
row.forEach(fieldId => {
const field = data.fields.find(item => item._id === fieldId);
if (!field) {
Expand All @@ -36,6 +40,6 @@ export default ({ field, data, targetStepId }: Params): FbFormModel => {
layout[currentRowIndex] && layout[currentRowIndex].length && currentRowIndex++;
});

targetStepLayout.layout = layout;
destinationContainerLayout.layout = layout;
return data;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FbFormModel, FbFormStep, DropDestination, DropSource } from "~/types";

interface ContainerLayoutParams {
data: FbFormModel;
destination: DropDestination;
source: DropSource;
}
/*
This is a helper function that gets layout from the container based on its type.
* If "source" or "destination" is Condition Group then it would take layout from the settings of the Condition Group field.
* If "source" or "destination" is Step that it would take layout from the step itself.
*/
export default (params: ContainerLayoutParams) => {
const { data, destination, source } = params;

const sourceContainer =
source.containerType === "conditionGroup"
? (data.fields.find(field => field._id === source.containerId)?.settings as FbFormStep)
: (data.steps.find(step => step.id === source.containerId) as FbFormStep);

const destinationContainer =
destination.containerType === "conditionGroup"
? (data.fields.find(field => field._id === destination.containerId)
?.settings as FbFormStep)
: (data.steps.find(step => step.id === destination.containerId) as FbFormStep);

return {
sourceContainer,
destinationContainer
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FbFormModelField, FbFormStep, FbFormModel } from "~/types";

import { deleteField } from "./index";

interface DeleteConditionGroupParams {
data: FbFormModel;
formStep: FbFormStep;
stepFields: FbFormModelField[];
conditionGroup: FbFormModelField;
conditionGroupFields: FbFormModelField[];
}
// When we delete condition group we also need to delete fields inside of it,
// because those fields belong directly (they are being stored in the setting of the condition group) to the condition group and not the step.
export default (params: DeleteConditionGroupParams) => {
const { data, formStep, stepFields, conditionGroup, conditionGroupFields } = params;

const deleteConditionGroup = () => {
const layout = stepFields.map(field => {
if (field._id === conditionGroup._id) {
deleteField({
field,
data,
containerType: "step",
containerId: formStep.id
});
return;
} else {
return field;
}
});

return layout;
};

const deleteConditionGroupFields = () => {
const layout = conditionGroupFields.map(field => {
if (!conditionGroup._id) {
return;
}
deleteField({
field,
data,
containerType: "conditionGroup",
containerId: conditionGroup._id
});
});

return layout;
};
deleteConditionGroupFields();

deleteConditionGroup();
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ export default ({ data, field, target, source, destination }: HandleMoveField) =
if (source.containerId === destination.containerId) {
// This condition should cover:
// 1. When we move field in scope of one Step;
// 2. When we move field in scope of one Condition Group (Condition Group yet to be implemented).
// 2. When we move field in scope of one Condition Group.
moveField({
field,
data,
target,
destination
destination,
source
});
} else {
// This condition should cover:
// 1. When we move field in scope of two different Steps;
// 2. When we move field in scope of two different Condition Groups (Condition Group yet to be implemented).
// 2. When we move field in scope of two different Condition Groups.
moveFieldBetween({
data,
field,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { FbFormModelField, FieldIdType, FieldLayoutPositionType, FbFormStep } from "~/types";
import { FbFormModelField, FieldIdType, FieldLayoutPositionType } from "~/types";

interface GetFieldPositionResult extends Omit<FieldLayoutPositionType, "index"> {
index: number;
}
interface GetFieldPositionParams {
field: FbFormModelField | FieldIdType;
data: FbFormStep;
layout: string[][];
}

export default ({ field, data }: GetFieldPositionParams): GetFieldPositionResult | null => {
export default ({ field, layout }: GetFieldPositionParams): GetFieldPositionResult | null => {
const id = typeof field === "string" ? field : field._id;
for (let rowIndex = 0; rowIndex < data.layout.length; rowIndex++) {
const row = data.layout[rowIndex];
for (let rowIndex = 0; rowIndex < layout.length; rowIndex++) {
const row = layout[rowIndex];
for (let fieldIndex = 0; fieldIndex < row.length; fieldIndex++) {
if (row[fieldIndex] !== id) {
continue;
Expand Down