Skip to content

Commit

Permalink
feat: exclusive 'Or' option in checklists (#4056)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Dec 18, 2024
1 parent ff6cc54 commit 2a7be85
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 86 deletions.
35 changes: 25 additions & 10 deletions editor.planx.uk/src/@planx/components/Checklist/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import { ModalFooter } from "ui/editor/ModalFooter";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import RichTextInput from "ui/editor/RichTextInput/RichTextInput";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";

import { parseBaseNodeData } from "../../shared";
import { Option, parseBaseNodeData } from "../../shared";
import { ICONS } from "../../shared/icons";
import type { Checklist } from "../model";
import { toggleExpandableChecklist } from "../model";
import { ChecklistProps } from "../types";
import { Options } from "./Options";

export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
export const ChecklistEditor: React.FC<ChecklistProps> = (props) => {
const type = TYPES.Checklist;

const formik = useFormik<Checklist>({
Expand All @@ -39,7 +40,7 @@ export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
: groupedOptions?.flatMap((group) => group.children);

const filteredOptions = (sourceOptions || []).filter(
(option) => option.data.text,
(option) => option.data.text
);

const processedOptions = filteredOptions.map((option) => ({
Expand All @@ -66,21 +67,33 @@ export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
}),
},
},
processedOptions,
processedOptions
);
} else {
alert(JSON.stringify({ type, ...values, options }, null, 2));
}
},
validate: ({ options, groupedOptions, ...values }) => {
validate: ({ options, groupedOptions, allRequired, ...values }) => {
const errors: FormikErrors<FormikValues> = {};

const exclusiveOptions: Option[] | undefined = options?.filter(
(option) => option.data.exclusive
);
if (allRequired && exclusiveOptions && exclusiveOptions.length > 0) {
errors.allRequired =
'Cannot configure exclusive "or" option alongside "all required" setting';
}
// Account for flat or expandable Checklist options
options =
options || groupedOptions?.map((group) => group.children)?.flat();
if (values.fn && !options?.some((option) => option.data.val)) {
errors.fn =
"At least one option must set a data value when the checklist has a data field";
}
if (exclusiveOptions && exclusiveOptions.length > 1) {
errors.options =
"There should be a maximum of one exclusive option configured";
}
return errors;
},
});
Expand Down Expand Up @@ -160,33 +173,35 @@ export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
onChange={() =>
formik.setFieldValue(
"allRequired",
!formik.values.allRequired,
!formik.values.allRequired
)
}
label="All required"
/>
</InputRow>

<InputRow>
<Switch
checked={formik.values.neverAutoAnswer}
onChange={() =>
formik.setFieldValue(
"neverAutoAnswer",
!formik.values.neverAutoAnswer,
!formik.values.neverAutoAnswer
)
}
label="Always put to user (forgo automation)"
/>
</InputRow>
</InputGroup>
</ModalSectionContent>

<Options formik={formik} />
<ErrorWrapper error={formik.errors.options}>
<Options formik={formik} />
</ErrorWrapper>
</ModalSection>

<ModalFooter formik={formik} />
</form>
);
};

export default ChecklistComponent;
export default ChecklistEditor;
72 changes: 61 additions & 11 deletions editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import Delete from "@mui/icons-material/Delete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import { BaseOptionsEditor } from "@planx/components/shared/BaseOptionsEditor";
import { hasFeatureFlag } from "lib/featureFlags";
import { partition } from "lodash";
import adjust from "ramda/src/adjust";
import compose from "ramda/src/compose";
import remove from "ramda/src/remove";
import React from "react";
import { FormikHookReturn } from "types";
import ListManager from "ui/editor/ListManager/ListManager";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";

Expand All @@ -17,6 +21,14 @@ import type { Group } from "../model";
import ChecklistOptionsEditor from "./OptionsEditor";

export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
const [exclusiveOptions, nonExclusiveOptions]: Option[][] = partition(
formik.values.options,
(option) => option.data.exclusive
);

const exclusiveOrOptionManagerShouldRender =
hasFeatureFlag("EXCLUSIVE_OR") && nonExclusiveOptions.length;

return (
<ModalSectionContent subtitle="Options">
{formik.values.groupedOptions ? (
Expand All @@ -42,7 +54,7 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
onClick={() => {
formik.setFieldValue(
`groupedOptions`,
remove(groupIndex, 1, formik.values.groupedOptions),
remove(groupIndex, 1, formik.values.groupedOptions)
);
}}
size="large"
Expand All @@ -57,7 +69,7 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
onChange={(newOptions) => {
formik.setFieldValue(
`groupedOptions[${groupIndex}].children`,
newOptions,
newOptions
);
}}
newValue={() =>
Expand All @@ -76,7 +88,7 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
showValueField: !!formik.values.fn,
onMoveToGroup: (
movedItemIndex: number,
moveToGroupIndex: number,
moveToGroupIndex: number
) => {
const item = groupedOption.children[movedItemIndex];
formik.setFieldValue(
Expand All @@ -87,27 +99,27 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
(option: Group<Option>) => ({
...option,
children: [...option.children, item],
}),
})
),
adjust(groupIndex, (option: Group<Option>) => ({
...option,
children: remove(
movedItemIndex,
1,
option.children,
option.children
),
})),
)(formik.values.groupedOptions),
}))
)(formik.values.groupedOptions)
);
},
groups: formik.values.groupedOptions.map(
(opt: Group<Option>) => opt.title,
(opt: Group<Option>) => opt.title
),
}}
/>
</Box>
</Box>
),
)
)}
<Box mt={1}>
<Button
Expand All @@ -128,9 +140,14 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
</Box>
) : (
<ListManager
values={formik.values.options || []}
values={nonExclusiveOptions || []}
onChange={(newOptions) => {
formik.setFieldValue("options", newOptions);
const newCombinedOptions =
newOptions.length === 0
? []
: [...exclusiveOptions, ...newOptions];

formik.setFieldValue("options", newCombinedOptions);
}}
newValueLabel="add new option"
newValue={() =>
Expand All @@ -146,6 +163,39 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => {
editorExtraProps={{ showValueField: !!formik.values.fn }}
/>
)}
{exclusiveOrOptionManagerShouldRender ? (
<Box mt={1}>
<ErrorWrapper error={formik.errors.allRequired as string}>
<ListManager
values={exclusiveOptions || []}
onChange={(newExclusiveOptions) => {
const newCombinedOptions = [
...nonExclusiveOptions,
...newExclusiveOptions,
];
formik.setFieldValue("options", newCombinedOptions);
}}
newValueLabel='add "or" option'
maxItems={1}
disableDragAndDrop
newValue={() =>
({
data: {
text: "",
description: "",
val: "",
exclusive: true,
},
}) as Option
}
Editor={BaseOptionsEditor}
editorExtraProps={{ showValueField: !!formik.values.fn }}
/>
</ErrorWrapper>
</Box>
) : (
<></>
)}
</ModalSectionContent>
);
};
Loading

0 comments on commit 2a7be85

Please sign in to comment.