-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: Team Settings Editor Form UI Changes #3305
Changes from 11 commits
575b581
4b6c683
15e5e08
26a7640
1df271e
3ccff1f
f29a19c
ae06ade
63ca171
3f980d6
32c93e0
ca7ec39
3cc93cc
329fec4
52a56c0
9c4b56d
2f58c27
97f6064
a0eaeaa
0946197
8b3c4b7
6499098
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { useFormik } from "formik"; | ||
import React, { ChangeEvent, useState } from "react"; | ||
import InputDescription from "ui/editor/InputDescription"; | ||
import Input from "ui/shared/Input"; | ||
import InputRow from "ui/shared/InputRow"; | ||
import InputRowLabel from "ui/shared/InputRowLabel"; | ||
|
||
import { SettingsForm } from "../shared/SettingsForm"; | ||
import { FormProps } from "."; | ||
|
||
export default function BoundaryForm({ formikConfig, onSuccess }: FormProps) { | ||
const formik = useFormik({ | ||
...formikConfig, | ||
onSubmit(values, { resetForm }) { | ||
onSuccess(); | ||
resetForm({ values }); | ||
}, | ||
}); | ||
|
||
return ( | ||
<SettingsForm | ||
formik={formik} | ||
legend="Boundary" | ||
description={ | ||
<InputDescription> | ||
The boundary URL is used to retrieve the outer boundary of your | ||
council area. This can then help users define whether they are within | ||
your council area. | ||
<br /> | ||
<br /> | ||
The boundary should be given as a link from:{" "} | ||
<a | ||
href="https://www.planning.data.gov.uk/" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
https://www.planning.data.gov.uk/ | ||
</a> | ||
</InputDescription> | ||
} | ||
input={ | ||
<> | ||
<InputRow> | ||
<InputRowLabel> | ||
Boundary URL | ||
<Input | ||
name="boundary" | ||
value={formik.values.boundaryUrl} | ||
onChange={(ev: ChangeEvent<HTMLInputElement>) => { | ||
formik.setFieldValue("boundaryUrl", ev.target.value); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
</> | ||
} | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { useFormik } from "formik"; | ||
import React, { ChangeEvent } from "react"; | ||
import InputDescription from "ui/editor/InputDescription"; | ||
import Input from "ui/shared/Input"; | ||
import InputRow from "ui/shared/InputRow"; | ||
import InputRowLabel from "ui/shared/InputRowLabel"; | ||
import * as Yup from "yup"; | ||
|
||
import { SettingsForm } from "../shared/SettingsForm"; | ||
import { FormProps } from "."; | ||
|
||
export default function ContactForm({ formikConfig, onSuccess }: FormProps) { | ||
const formSchema = Yup.object().shape({ | ||
helpEmail: Yup.string() | ||
.email("Please enter valid email") | ||
.required("Help Email is required"), | ||
helpPhone: Yup.string().required("Help Phone is required"), | ||
helpOpeningHours: Yup.string(), | ||
}); | ||
|
||
const formik = useFormik({ | ||
...formikConfig, | ||
validationSchema: formSchema, | ||
onSubmit(values, { resetForm }) { | ||
onSuccess(); | ||
resetForm({ values }); | ||
}, | ||
}); | ||
|
||
const onChangeFn = (type: string, event: ChangeEvent<HTMLInputElement>) => | ||
formik.setFieldValue(type, event.target.value); | ||
|
||
return ( | ||
<SettingsForm | ||
legend="Contact Information" | ||
formik={formik} | ||
description={ | ||
<InputDescription> | ||
Details to help direct different messages, feedback, and enquiries | ||
from users. | ||
</InputDescription> | ||
} | ||
input={ | ||
<> | ||
<InputRow> | ||
<InputRowLabel> | ||
Help Email | ||
<Input | ||
name="helpEmail" | ||
onChange={(event) => { | ||
onChangeFn("helpEmail", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
<InputRow> | ||
<InputRowLabel> | ||
Help Phone | ||
<Input | ||
name="helpPhone" | ||
onChange={(event) => { | ||
onChangeFn("helpPhone", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
<InputRow> | ||
<InputRowLabel> | ||
Help Opening Hours | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit(ui/ux): I think we can remove the "Help..." prefixes from these labels - "Phone number", "Opening hours" and "Contact email address" feel like clearer / simpler labels. There's always room for the data model and it's representation to the user to be different 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! What do you think about keeping the data model fields with the "help" prefix to group and indicate use? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, I'd keep that internally, and just change the labels displayed to users 👍 |
||
<Input | ||
multiline | ||
name="helpOpeningHours" | ||
onChange={(event) => { | ||
onChangeFn("helpOpeningHours", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
</> | ||
} | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import RadioGroup from "@mui/material/RadioGroup"; | ||
import BasicRadio from "@planx/components/shared/Radio/BasicRadio"; | ||
import { useFormik } from "formik"; | ||
import React, { ChangeEvent, useState } from "react"; | ||
import InputDescription from "ui/editor/InputDescription"; | ||
import Input from "ui/shared/Input"; | ||
import InputRow from "ui/shared/InputRow"; | ||
import InputRowLabel from "ui/shared/InputRowLabel"; | ||
|
||
import { SettingsForm } from "../shared/SettingsForm"; | ||
import { FormProps } from "."; | ||
export default function HomepagePlanningForm({ | ||
formikConfig, | ||
onSuccess, | ||
}: FormProps) { | ||
const [showPlanningInputs, setShowPlanningInputs] = useState(false); | ||
|
||
const formik = useFormik({ | ||
...formikConfig, | ||
onSubmit(values, { resetForm }) { | ||
onSuccess(); | ||
resetForm({ values }); | ||
}, | ||
}); | ||
|
||
const onChangeFn = (type: string, event: ChangeEvent<HTMLInputElement>) => | ||
formik.setFieldValue(type, event.target.value); | ||
|
||
const boolTransform = (event: ChangeEvent<HTMLInputElement>) => { | ||
if (event.target.value === "yes") { | ||
setShowPlanningInputs(true); | ||
return true; | ||
} else if (event.target.value === "no") { | ||
setShowPlanningInputs(false); | ||
return false; | ||
} | ||
}; | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<SettingsForm | ||
legend="Homepage and Planning Portal" | ||
formik={formik} | ||
description={ | ||
<InputDescription> | ||
A link to your homepage displayed publicly to your users to help | ||
navigate your council services and a link to your Planning Portal to | ||
connect your planning data with our outputs. | ||
</InputDescription> | ||
} | ||
input={ | ||
<> | ||
<InputRow> | ||
<InputRowLabel> | ||
Homepage URL | ||
<Input | ||
name="homepage" | ||
onChange={(event) => { | ||
onChangeFn("homepage", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
<InputRow> | ||
<InputRowLabel> | ||
Do you collect Planning data? | ||
<RadioGroup | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name="isPlanningDataCollected" | ||
defaultValue={ | ||
formik.values.isPlanningDataCollected === true ? "yes" : "no" | ||
} | ||
onChange={(event) => { | ||
formik.setFieldValue( | ||
"isPlanningDataCollected", | ||
boolTransform(event), | ||
); | ||
}} | ||
> | ||
<BasicRadio | ||
title="Yes" | ||
variant="compact" | ||
id="yes" | ||
value="yes" | ||
onChange={() => {}} | ||
/> | ||
<BasicRadio | ||
title="No" | ||
variant="compact" | ||
id="no" | ||
value="no" | ||
onChange={() => {}} | ||
/> | ||
</RadioGroup> | ||
</InputRowLabel> | ||
</InputRow> | ||
<InputRow> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So actually I think we can remove this whole block! When we added this feature, we thought councils may want to have customisable URLs here, but so far this hasn't proved to be the case. Here's my recommendation -
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DafyddLlyr so remove this who file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry - should have been clearer! Just the inputs for Planning Portal name and Planning Portal URL. We've found that nobody uses custom values here currently 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah okay! but keep the Boolean field and the homepage? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asking because, as I am thinking, if it's just the Homepage + the Bool field, then what use is the bool field? If it is just the Homepage, could I just incorporate that into the Contact Form? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Homepage should stay, and could move into contact 👍 Just taken a closer look at What this means is -
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies for not spotting this issue with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sick! All makes sense |
||
<InputRowLabel> | ||
Planning Portal Name | ||
<Input | ||
name="portalName" | ||
disabled={showPlanningInputs ? false : true} | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onChange={(event) => { | ||
onChangeFn("portalName", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
<InputRow> | ||
<InputRowLabel> | ||
Planning Portal URL | ||
<Input | ||
name="portalUrl" | ||
disabled={showPlanningInputs ? false : true} | ||
onChange={(event) => { | ||
onChangeFn("portalUrl", event); | ||
}} | ||
/> | ||
</InputRowLabel> | ||
</InputRow> | ||
</> | ||
} | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import Alert from "@mui/material/Alert"; | ||
import Box from "@mui/material/Box"; | ||
import Snackbar from "@mui/material/Snackbar"; | ||
import { styled } from "@mui/material/styles"; | ||
import Typography from "@mui/material/Typography"; | ||
import { TeamTheme } from "@opensystemslab/planx-core/types"; | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { FormikConfig } from "formik"; | ||
import { useStore } from "pages/FlowEditor/lib/store"; | ||
import React, { useEffect, useState } from "react"; | ||
import EditorRow from "ui/editor/EditorRow"; | ||
|
||
import BoundaryForm from "./BoundaryForm"; | ||
import ContactForm from "./ContactForm"; | ||
import HomepagePlanningForm from "./HomepagePlanningForm"; | ||
|
||
export const DesignPreview = styled(Box)(({ theme }) => ({ | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
border: `2px solid ${theme.palette.border.input}`, | ||
padding: theme.spacing(2), | ||
boxShadow: "4px 4px 0px rgba(150, 150, 150, 0.5)", | ||
})); | ||
|
||
export const EXAMPLE_COLOUR = "#007078"; | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export interface GeneralSettings { | ||
boundaryUrl: string; | ||
helpEmail: string; | ||
helpPhone: string; | ||
helpOpeningHours: string; | ||
homepage: string; | ||
isPlanningDataCollected: boolean; | ||
portalName: string; | ||
portalUrl: string; | ||
} | ||
|
||
export interface FormProps { | ||
formikConfig: FormikConfig<GeneralSettings>; | ||
onSuccess: () => void; | ||
} | ||
|
||
const GeneralSettings: React.FC = () => { | ||
const [formikConfig, setFormikConfig] = useState< | ||
FormikConfig<GeneralSettings> | undefined | ||
>(undefined); | ||
|
||
const initialValues = { | ||
boundaryUrl: "", | ||
helpEmail: "", | ||
helpPhone: "", | ||
helpOpeningHours: "", | ||
homepage: "", | ||
isPlanningDataCollected: true, | ||
portalName: "", | ||
portalUrl: "", | ||
}; | ||
|
||
useEffect(() => { | ||
const fetchTeam = async () => { | ||
try { | ||
setFormikConfig({ | ||
initialValues: initialValues, | ||
onSubmit: () => {}, | ||
validateOnBlur: false, | ||
validateOnChange: false, | ||
enableReinitialize: true, | ||
}); | ||
} catch (error) { | ||
console.error("Error fetching team:", error); | ||
} | ||
}; | ||
|
||
fetchTeam(); | ||
}, []); | ||
|
||
const [open, setOpen] = useState(true); | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const [updateMessage, setUpdateMessage] = useState("Setting Updated"); | ||
|
||
const handleClose = ( | ||
_event?: React.SyntheticEvent | Event, | ||
reason?: string, | ||
) => { | ||
if (reason === "clickaway") { | ||
return; | ||
} | ||
|
||
setOpen(false); | ||
}; | ||
|
||
const onSuccess = () => setOpen(true); | ||
|
||
return ( | ||
<Box maxWidth="formWrap" mx="auto"> | ||
<EditorRow> | ||
<Typography variant="h2" component="h3" gutterBottom> | ||
General | ||
</Typography> | ||
<Typography variant="body1"> | ||
Important links and settings for how your users connect with you | ||
</Typography> | ||
</EditorRow> | ||
{formikConfig && ( | ||
<> | ||
<ContactForm formikConfig={formikConfig} onSuccess={onSuccess} /> | ||
<HomepagePlanningForm | ||
formikConfig={formikConfig} | ||
onSuccess={onSuccess} | ||
/> | ||
<BoundaryForm formikConfig={formikConfig} onSuccess={onSuccess} /> | ||
</> | ||
)} | ||
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}> | ||
<Alert onClose={handleClose} severity="success" sx={{ width: "100%" }}> | ||
{updateMessage} | ||
</Alert> | ||
</Snackbar> | ||
</Box> | ||
); | ||
}; | ||
|
||
export default GeneralSettings; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DafyddLlyr maybe this is something we pick up with @ianjon3s as well, but in terms of UX copy and tone of voice in message handling, is there a particular way to write these error messages?
To ensure consistency in tone, I know people do like to manage how these a written
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have standard copy for Editor-facing errors, but generally try to get input from the content team (+ Nomensa URs) on public-facing error messages.
I'm pretty sure most Editor-facing inputs just currently use
.required()
and would output whatever Yup outputs which would be something like${fieldName} is required
→"helpEmail is required"
I think what you have here is great (
${labelName} is required
→"Help email is required"
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DafyddLlyr @ianjon3s worth noting the advice here: https://opensystemslab.notion.site/How-to-write-good-planning-service-content-30313a55fc5b4499846e32e88b1cf76a#b02887d8bd1d4cbe8e4d6d842a3a398b