Skip to content

Commit

Permalink
feat: exclusive 'or' checklist with grouped options (#4116)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Jan 14, 2025
1 parent a3198bb commit 6aa1556
Show file tree
Hide file tree
Showing 18 changed files with 800 additions and 377 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { DataFieldAutocomplete } from "@planx/components/shared/DataFieldAutocomplete";
import { FormikErrors, FormikValues, useFormik } from "formik";
import React, { useEffect, useRef } from "react";
import ImgInput from "ui/editor/ImgInput/ImgInput";
Expand All @@ -12,7 +13,6 @@ import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";

import { DataFieldAutocomplete } from "@planx/components/shared/DataFieldAutocomplete";
import { Option, parseBaseNodeData } from "../../shared";
import { ICONS } from "../../shared/icons";
import type { Checklist } from "../model";
Expand Down Expand Up @@ -41,7 +41,7 @@ export const ChecklistEditor: 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 @@ -68,7 +68,7 @@ export const ChecklistEditor: React.FC<ChecklistProps> = (props) => {
}),
},
},
processedOptions
processedOptions,
);
} else {
alert(JSON.stringify({ type, ...values, options }, null, 2));
Expand All @@ -77,16 +77,16 @@ export const ChecklistEditor: React.FC<ChecklistProps> = (props) => {
validate: ({ options, groupedOptions, allRequired, ...values }) => {
const errors: FormikErrors<FormikValues> = {};

// Account for flat or expandable Checklist options
options = options || groupedOptions?.flatMap((group) => group.children);

const exclusiveOptions: Option[] | undefined = options?.filter(
(option) => option.data.exclusive
(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";
Expand Down Expand Up @@ -164,7 +164,7 @@ export const ChecklistEditor: React.FC<ChecklistProps> = (props) => {
onChange={() =>
formik.setFieldValue(
"allRequired",
!formik.values.allRequired
!formik.values.allRequired,
)
}
label="All required"
Expand All @@ -177,7 +177,7 @@ export const ChecklistEditor: React.FC<ChecklistProps> = (props) => {
onChange={() =>
formik.setFieldValue(
"neverAutoAnswer",
!formik.values.neverAutoAnswer
!formik.values.neverAutoAnswer,
)
}
label="Always put to user (forgo automation)"
Expand Down
237 changes: 49 additions & 188 deletions editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,213 +1,74 @@
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 { getOptionsSchemaByFn } from "@planx/components/shared/utils";
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";

import { getOptionsSchemaByFn } from "@planx/components/shared/utils";
import { useStore } from "pages/FlowEditor/lib/store";
import { Option } from "../../shared";
import type { Group } from "../model";
import ChecklistOptionsEditor from "./OptionsEditor";
import { useInitialOptions } from "../Public/hooks/useInitialOptions";
import { ExclusiveOrOptionManager } from "./components/ExclusiveOrOptionManager";
import { GroupedOptions } from "./components/GroupedOptions";
import ChecklistOptionsEditor from "./components/OptionsEditor";

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

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

const schema = useStore().getFlowSchema()?.options;
const initialOptions: Option[] | undefined = formik.initialValues.options || formik.initialValues.groupedOptions?.map((group: Group<Option>) => group.children)?.flat();
const initialOptionVals = initialOptions?.map((option) => option.data?.val);
hasFeatureFlag("EXCLUSIVE_OR") && nonExclusiveOptions.length > 0;

const { schema, initialOptionVals } = useInitialOptions(formik);

return (
<ModalSectionContent subtitle="Options">
{formik.values.groupedOptions ? (
<Box>
{formik.values.groupedOptions.map(
(groupedOption: Group<Option>, groupIndex: number) => (
<Box key={groupIndex} mt={groupIndex === 0 ? 0 : 4}>
<Box display="flex" pb={1}>
<InputRow>
<Input
required
format="bold"
name={`groupedOptions[${groupIndex}].title`}
value={groupedOption.title}
placeholder="Section Title"
onChange={formik.handleChange}
/>
</InputRow>
<Box flex={0}>
<IconButton
title="Delete group"
aria-label="Delete group"
onClick={() => {
formik.setFieldValue(
`groupedOptions`,
remove(groupIndex, 1, formik.values.groupedOptions)
);
}}
size="large"
>
<Delete />
</IconButton>
</Box>
</Box>
<Box pl={{ md: 2 }}>
<ListManager
values={groupedOption.children}
onChange={(newOptions) => {
formik.setFieldValue(
`groupedOptions[${groupIndex}].children`,
newOptions
);
}}
newValue={() =>
({
data: {
text: "",
description: "",
val: "",
},
}) as Option
}
newValueLabel="add new option"
Editor={ChecklistOptionsEditor}
editorExtraProps={{
groupIndex,
showValueField: !!formik.values.fn,
onMoveToGroup: (
movedItemIndex: number,
moveToGroupIndex: number
) => {
const item = groupedOption.children[movedItemIndex];
formik.setFieldValue(
"groupedOptions",
compose(
adjust(
moveToGroupIndex,
(option: Group<Option>) => ({
...option,
children: [...option.children, item],
})
),
adjust(groupIndex, (option: Group<Option>) => ({
...option,
children: remove(
movedItemIndex,
1,
option.children
),
}))
)(formik.values.groupedOptions)
);
},
groups: formik.values.groupedOptions.map(
(opt: Group<Option>) => opt.title
),
schema: getOptionsSchemaByFn(formik.values.fn, schema, initialOptionVals),
}}
/>
</Box>
</Box>
)
)}
<Box mt={1}>
<Button
size="large"
onClick={() => {
formik.setFieldValue(`groupedOptions`, [
...formik.values.groupedOptions,
{
title: "",
children: [],
},
]);
}}
>
add new group
</Button>
</Box>
</Box>
<GroupedOptions formik={formik} />
) : (
<ListManager
values={nonExclusiveOptions || []}
onChange={(newOptions) => {
const newCombinedOptions =
newOptions.length === 0
? []
: [...exclusiveOptions, ...newOptions];
<>
<ListManager
values={nonExclusiveOptions || []}
onChange={(newOptions) => {
const newCombinedOptions =
newOptions.length === 0
? []
: [...exclusiveOptions, ...newOptions];

formik.setFieldValue("options", newCombinedOptions);
}}
newValueLabel="add new option"
newValue={() =>
({
data: {
text: "",
description: "",
val: "",
},
}) as Option
}
Editor={ChecklistOptionsEditor}
editorExtraProps={{
showValueField: !!formik.values.fn,
schema: getOptionsSchemaByFn(formik.values.fn, schema, initialOptionVals),
}}
/>
)}
{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,
schema: getOptionsSchemaByFn(formik.values.fn, schema, initialOptionVals),
}}
formik.setFieldValue("options", newCombinedOptions);
}}
newValueLabel="add new option"
newValue={() =>
({
data: {
text: "",
description: "",
val: "",
},
}) as Option
}
Editor={ChecklistOptionsEditor}
editorExtraProps={{
showValueField: !!formik.values.fn,
schema: getOptionsSchemaByFn(
formik.values.fn,
schema,
initialOptionVals,
),
}}
/>
{exclusiveOrOptionManagerShouldRender ? (
<ExclusiveOrOptionManager
formik={formik}
exclusiveOptions={exclusiveOptions}
nonExclusiveOptions={nonExclusiveOptions}
/>
</ErrorWrapper>
</Box>
) : (
<></>
) : (
<></>
)}
</>
)}
</ModalSectionContent>
);
Expand Down
Loading

0 comments on commit 6aa1556

Please sign in to comment.