From 1fec38732340df4daba42f4d8b505a3a115a09fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Mon, 30 Sep 2024 08:20:46 +0100 Subject: [PATCH] refactor: Split up `ServiceSettings` component (#3732) --- .../components/Settings/ServiceSettings.tsx | 481 ------------------ .../FlowStatus/PublicLink.test.tsx} | 13 +- .../ServiceSettings/FlowStatus/PublicLink.tsx | 130 +++++ .../ServiceSettings/FlowStatus/index.tsx | 169 ++++++ .../FlowStatus/testUtils.tsx} | 2 +- .../FooterLinksAndLegalDisclaimer.tsx | 202 ++++++++ .../Settings/ServiceSettings/index.tsx | 14 + 7 files changed, 528 insertions(+), 483 deletions(-) delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings.tsx rename editor.planx.uk/src/pages/FlowEditor/components/Settings/{tests/ServiceSettings/PublicLinks.test.tsx => ServiceSettings/FlowStatus/PublicLink.test.tsx} (99%) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/index.tsx rename editor.planx.uk/src/pages/FlowEditor/components/Settings/{tests/helpers/setupServiceSettingsScreen.tsx => ServiceSettings/FlowStatus/testUtils.tsx} (94%) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FooterLinksAndLegalDisclaimer.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/index.tsx diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings.tsx deleted file mode 100644 index 5e223c925d..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Container from "@mui/material/Container"; -import FormControlLabel, { - formControlLabelClasses, -} from "@mui/material/FormControlLabel"; -import Link from "@mui/material/Link"; -import Switch, { SwitchProps } from "@mui/material/Switch"; -import Tooltip from "@mui/material/Tooltip"; -import Typography from "@mui/material/Typography"; -import { FlowStatus } from "@opensystemslab/planx-core/types"; -import axios from "axios"; -import { useFormik } from "formik"; -import { useToast } from "hooks/useToast"; -import React, { useState } from "react"; -import { rootFlowPath } from "routes/utils"; -import { FONT_WEIGHT_BOLD } from "theme"; -import InputGroup from "ui/editor/InputGroup"; -import InputLegend from "ui/editor/InputLegend"; -import RichTextInput from "ui/editor/RichTextInput"; -import SettingsDescription from "ui/editor/SettingsDescription"; -import SettingsSection from "ui/editor/SettingsSection"; -import Input, { Props as InputProps } from "ui/shared/Input"; -import InputRow from "ui/shared/InputRow"; -import InputRowItem from "ui/shared/InputRowItem"; - -import type { FlowSettings } from "../../../../types"; -import { useStore } from "../../lib/store"; - -const CopyButton = (props: { link: string; isActive: boolean }) => { - const [copyMessage, setCopyMessage] = useState<"copy" | "copied">("copy"); - return ( - - - - ); -}; - -const TitledLink: React.FC<{ - link: string; - isActive: boolean; - helpText: string | undefined; -}> = ({ link, isActive, helpText }) => { - return ( - - - Your public link - - - - {helpText} - - {isActive ? ( - - {link} - - ) : ( - - {link} - - )} - - ); -}; - -const PublicLink: React.FC<{ - isFlowPublished: boolean; - status: FlowStatus; - subdomain: string; - publishedLink: string; -}> = ({ isFlowPublished, status, subdomain, publishedLink }) => { - const isFlowPublic = isFlowPublished && status === "online"; - const hasSubdomain = Boolean(subdomain); - - const publicLinkHelpText = () => { - const isFlowOnline = status === "online"; - switch (true) { - case isFlowPublished && isFlowOnline: - return undefined; - case !isFlowPublished && isFlowOnline: - return "Publish your flow to activate the public link."; - case isFlowPublished && !isFlowOnline: - return "Switch your flow to 'online' to activate the public link."; - case !isFlowPublished && !isFlowOnline: - return "Publish your flow and switch it to 'online' to activate the public link."; - } - }; - - switch (true) { - case isFlowPublic && hasSubdomain: - return ( - - ); - case isFlowPublic && !hasSubdomain: - return ( - - ); - case !isFlowPublic && hasSubdomain: - return ( - - ); - case !isFlowPublic && !hasSubdomain: - return ( - - ); - } -}; - -const TextInput: React.FC<{ - title: string; - richText?: boolean; - description?: string; - switchProps?: SwitchProps; - headingInputProps?: InputProps; - contentInputProps?: InputProps; -}> = ({ - title, - richText = false, - description, - switchProps, - headingInputProps, - contentInputProps, -}) => { - return ( - - - - - {title} - - - - {description && ( - {description} - )} - - - - - - - - - {richText ? ( - - ) : ( - - )} - - - - ); -}; - -const ServiceSettings: React.FC = () => { - const [ - flowSettings, - updateFlowSettings, - flowStatus, - updateFlowStatus, - token, - teamSlug, - flowSlug, - teamDomain, - isFlowPublished, - ] = useStore((state) => [ - state.flowSettings, - state.updateFlowSettings, - state.flowStatus, - state.updateFlowStatus, - state.jwt, - state.teamSlug, - state.flowSlug, - state.teamDomain, - state.isFlowPublished, - ]); - const toast = useToast(); - - const sendFlowStatusSlackNotification = async (status: FlowStatus) => { - const skipTeamSlugs = [ - "open-digital-planning", - "opensystemslab", - "planx", - "templates", - "testing", - "wikihouse", - ]; - if (skipTeamSlugs.includes(teamSlug)) return; - - const emoji = { - online: ":large_green_circle:", - offline: ":no_entry:", - }; - const message = `${emoji[status]} *${teamSlug}/${flowSlug}* is now ${status} (@Silvia)`; - - return axios.post( - `${import.meta.env.VITE_APP_API_URL}/send-slack-notification`, - { - message: message, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - }; - - const elementsForm = useFormik({ - initialValues: { - elements: { - legalDisclaimer: { - heading: flowSettings?.elements?.legalDisclaimer?.heading ?? "", - content: flowSettings?.elements?.legalDisclaimer?.content ?? "", - show: flowSettings?.elements?.legalDisclaimer?.show ?? false, - }, - help: { - heading: flowSettings?.elements?.help?.heading ?? "", - content: flowSettings?.elements?.help?.content ?? "", - show: flowSettings?.elements?.help?.show ?? false, - }, - privacy: { - heading: flowSettings?.elements?.privacy?.heading ?? "", - content: flowSettings?.elements?.privacy?.content ?? "", - show: flowSettings?.elements?.privacy?.show ?? false, - }, - }, - }, - onSubmit: async (values) => { - await updateFlowSettings(values); - toast.success("Service settings updated successfully"); - }, - validate: () => {}, - }); - - const statusForm = useFormik<{ status: FlowStatus }>({ - initialValues: { - status: flowStatus || "online", - }, - onSubmit: async (values, { resetForm }) => { - const isSuccess = await updateFlowStatus(values.status); - if (isSuccess) { - toast.success("Service settings updated successfully"); - // Send a Slack notification to #planx-notifications - sendFlowStatusSlackNotification(values.status); - // Reset "dirty" status to disable Save & Reset buttons - resetForm({ values }); - } - }, - }); - - const publishedLink = `${window.location.origin}${rootFlowPath( - false, - )}/published`; - - const subdomainLink = teamDomain && `https://${teamDomain}/${flowSlug}`; - - return ( - - - - - Elements - - - Manage the features that users will be able to see. - - - - - - - - Footer Links - - - - - - - - - - - - - - - - Status - - - Manage the status of your service. - - - - - statusForm.setFieldValue( - "status", - statusForm.values.status === "online" - ? "offline" - : "online", - ) - } - /> - } - /> - -

Toggle your service between "offline" and "online".

-

- A service must be online to be accessed by the public, and to - enable analytics gathering. -

-

Offline services can still be edited and published as normal.

-
- - - - - - - -
-
-
- ); -}; - -export default ServiceSettings; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/ServiceSettings/PublicLinks.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.test.tsx similarity index 99% rename from editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/ServiceSettings/PublicLinks.test.tsx rename to editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.test.tsx index a2569d1a4e..54b1a11359 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/ServiceSettings/PublicLinks.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.test.tsx @@ -4,7 +4,7 @@ import { vi } from "vitest"; import setupServiceSettingsScreen, { mockWindowLocationObject, -} from "../helpers/setupServiceSettingsScreen"; +} from "./testUtils"; const { getState, setState } = useStore; @@ -62,6 +62,7 @@ describe("A team with a subdomain has an offline, published service.", () => { await inactiveLinkCheck(`https://${teamDomain}/${flowSlug}`); }); + it("has a disabled copy button", disabledCopyCheck); }); @@ -83,6 +84,7 @@ describe("A team with a subdomain has an online, unpublished service.", () => { await inactiveLinkCheck(`https://${teamDomain}/${flowSlug}`); }); + it("has a disabled copy button", disabledCopyCheck); }); @@ -107,11 +109,13 @@ describe("A team with a subdomain has an online, published service.", () => { await setupServiceSettingsScreen(); await activeLinkCheck(`https://${teamDomain}/${flowSlug}`); }); + it("has an enabled copy button", async () => { // render the comp await setupServiceSettingsScreen(); enabledCopyCheck(); }); + it("can be copied to the clipboard", async () => { const { flowSlug, teamDomain } = getState(); // render the comp @@ -127,6 +131,7 @@ describe("A team with a subdomain has an online, published service.", () => { ); }); }); + describe("A team with a subdomain has an offline, unpublished service.", () => { beforeEach(async () => { // setup state values that depends on @@ -145,8 +150,10 @@ describe("A team with a subdomain has an offline, unpublished service.", () => { await inactiveLinkCheck(`https://${teamDomain}/${flowSlug}`); }); + it("has a disabled copy button", disabledCopyCheck); }); + describe("A team without a subdomain has an offline, published service.", () => { beforeEach(async () => { // setup state values that depends on @@ -168,6 +175,7 @@ describe("A team without a subdomain has an offline, published service.", () => it("has a public link with the url in a

tag", async () => { await inactiveLinkCheck(publishedUrl); }); + it("has a disabled copy button", disabledCopyCheck); }); @@ -192,6 +200,7 @@ describe("A team without a subdomain has an online, unpublished service.", () => it("has a public link with the url in a

tag", async () => { await inactiveLinkCheck(publishedUrl); }); + it("has a disabled copy button", disabledCopyCheck); }); @@ -219,6 +228,7 @@ describe("A team without a subdomain has an online, published service.", () => { setupServiceSettingsScreen(); await activeLinkCheck(publishedUrl); }); + it("has an enabled copy button", () => { // render the comp setupServiceSettingsScreen(); @@ -258,5 +268,6 @@ describe("A team without a subdomain has an offline, unpublished service.", () = it("has a public link with the url in a

tag", async () => { await inactiveLinkCheck(publishedUrl); }); + it("has a disabled copy button", disabledCopyCheck); }); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.tsx new file mode 100644 index 0000000000..6bc4a1fab3 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/PublicLink.tsx @@ -0,0 +1,130 @@ +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import { FlowStatus } from "@opensystemslab/planx-core/types"; +import React, { useState } from "react"; +import SettingsDescription from "ui/editor/SettingsDescription"; + +const CopyButton = (props: { link: string; isActive: boolean }) => { + const [copyMessage, setCopyMessage] = useState<"copy" | "copied">("copy"); + return ( + + + + ); +}; + +const TitledLink: React.FC<{ + link: string; + isActive: boolean; + helpText: string | undefined; +}> = ({ link, isActive, helpText }) => { + return ( + + + Your public link + + + + {helpText} + + {isActive ? ( + + {link} + + ) : ( + + {link} + + )} + + ); +}; + +export const PublicLink: React.FC<{ + isFlowPublished: boolean; + status: FlowStatus; + subdomain: string; + publishedLink: string; +}> = ({ isFlowPublished, status, subdomain, publishedLink }) => { + const isFlowPublic = isFlowPublished && status === "online"; + const hasSubdomain = Boolean(subdomain); + + const publicLinkHelpText = () => { + const isFlowOnline = status === "online"; + switch (true) { + case isFlowPublished && isFlowOnline: + return undefined; + case !isFlowPublished && isFlowOnline: + return "Publish your flow to activate the public link."; + case isFlowPublished && !isFlowOnline: + return "Switch your flow to 'online' to activate the public link."; + case !isFlowPublished && !isFlowOnline: + return "Publish your flow and switch it to 'online' to activate the public link."; + } + }; + + switch (true) { + case isFlowPublic && hasSubdomain: + return ( + + ); + case isFlowPublic && !hasSubdomain: + return ( + + ); + case !isFlowPublic && hasSubdomain: + return ( + + ); + case !isFlowPublic && !hasSubdomain: + return ( + + ); + } +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/index.tsx new file mode 100644 index 0000000000..40a2afaef7 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/index.tsx @@ -0,0 +1,169 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import FormControlLabel, { + formControlLabelClasses, +} from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import Typography from "@mui/material/Typography"; +import type { FlowStatus } from "@opensystemslab/planx-core/types"; +import axios from "axios"; +import { useFormik } from "formik"; +import { useToast } from "hooks/useToast"; +import React from "react"; +import { rootFlowPath } from "routes/utils"; +import { FONT_WEIGHT_BOLD } from "theme"; +import SettingsDescription from "ui/editor/SettingsDescription"; +import SettingsSection from "ui/editor/SettingsSection"; + +import { useStore } from "../../../../lib/store"; +import { PublicLink } from "./PublicLink"; + +const FlowStatus = () => { + const [ + flowStatus, + updateFlowStatus, + token, + teamSlug, + flowSlug, + teamDomain, + isFlowPublished, + ] = useStore((state) => [ + state.flowStatus, + state.updateFlowStatus, + state.jwt, + state.teamSlug, + state.flowSlug, + state.teamDomain, + state.isFlowPublished, + ]); + const toast = useToast(); + + const statusForm = useFormik<{ status: FlowStatus }>({ + initialValues: { + status: flowStatus || "online", + }, + onSubmit: async (values, { resetForm }) => { + const isSuccess = await updateFlowStatus(values.status); + if (isSuccess) { + toast.success("Service settings updated successfully"); + // Send a Slack notification to #planx-notifications + sendFlowStatusSlackNotification(values.status); + // Reset "dirty" status to disable Save & Reset buttons + resetForm({ values }); + } + }, + }); + + const publishedLink = `${window.location.origin}${rootFlowPath( + false, + )}/published`; + + const subdomainLink = teamDomain && `https://${teamDomain}/${flowSlug}`; + + const sendFlowStatusSlackNotification = async (status: FlowStatus) => { + const skipTeamSlugs = [ + "open-digital-planning", + "opensystemslab", + "planx", + "templates", + "testing", + "wikihouse", + ]; + if (skipTeamSlugs.includes(teamSlug)) return; + + const emoji = { + online: ":large_green_circle:", + offline: ":no_entry:", + }; + const message = `${emoji[status]} *${teamSlug}/${flowSlug}* is now ${status} (@Silvia)`; + + return axios.post( + `${import.meta.env.VITE_APP_API_URL}/send-slack-notification`, + { + message: message, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + }; + + return ( + + + + Status + + + Manage the status of your service. + + + + + statusForm.setFieldValue( + "status", + statusForm.values.status === "online" ? "offline" : "online", + ) + } + /> + } + /> + +

Toggle your service between "offline" and "online".

+

+ A service must be online to be accessed by the public, and to enable + analytics gathering. +

+

Offline services can still be edited and published as normal.

+ + + + + + + + + + + ); +}; + +export default FlowStatus; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/helpers/setupServiceSettingsScreen.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/testUtils.tsx similarity index 94% rename from editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/helpers/setupServiceSettingsScreen.tsx rename to editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/testUtils.tsx index 6e17fc23d2..d9b5f943b8 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/tests/helpers/setupServiceSettingsScreen.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FlowStatus/testUtils.tsx @@ -5,7 +5,7 @@ import { HTML5Backend } from "react-dnd-html5-backend"; import { setup } from "testUtils"; import { vi } from "vitest"; -import ServiceSettings from "../../ServiceSettings"; +import ServiceSettings from ".."; export default async function setupServiceSettingsScreen() { const { user } = setup( diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FooterLinksAndLegalDisclaimer.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FooterLinksAndLegalDisclaimer.tsx new file mode 100644 index 0000000000..64b169f251 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/FooterLinksAndLegalDisclaimer.tsx @@ -0,0 +1,202 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Switch, { SwitchProps } from "@mui/material/Switch"; +import Typography from "@mui/material/Typography"; +import { useFormik } from "formik"; +import { useToast } from "hooks/useToast"; +import React from "react"; +import InputGroup from "ui/editor/InputGroup"; +import InputLegend from "ui/editor/InputLegend"; +import RichTextInput from "ui/editor/RichTextInput"; +import SettingsDescription from "ui/editor/SettingsDescription"; +import SettingsSection from "ui/editor/SettingsSection"; +import Input, { Props as InputProps } from "ui/shared/Input"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; + +import type { FlowSettings } from "../../../../../types"; +import { useStore } from "../../../lib/store"; + +const TextInput: React.FC<{ + title: string; + richText?: boolean; + description?: string; + switchProps?: SwitchProps; + headingInputProps?: InputProps; + contentInputProps?: InputProps; +}> = ({ + title, + richText = false, + description, + switchProps, + headingInputProps, + contentInputProps, +}) => { + return ( + + + + + {title} + + + + {description && ( + {description} + )} + + + + + + + + + {richText ? ( + + ) : ( + + )} + + + + ); +}; + +export const FooterLinksAndLegalDisclaimer = () => { + const [flowSettings, updateFlowSettings] = useStore((state) => [ + state.flowSettings, + state.updateFlowSettings, + ]); + const toast = useToast(); + + const elementsForm = useFormik({ + initialValues: { + elements: { + legalDisclaimer: { + heading: flowSettings?.elements?.legalDisclaimer?.heading ?? "", + content: flowSettings?.elements?.legalDisclaimer?.content ?? "", + show: flowSettings?.elements?.legalDisclaimer?.show ?? false, + }, + help: { + heading: flowSettings?.elements?.help?.heading ?? "", + content: flowSettings?.elements?.help?.content ?? "", + show: flowSettings?.elements?.help?.show ?? false, + }, + privacy: { + heading: flowSettings?.elements?.privacy?.heading ?? "", + content: flowSettings?.elements?.privacy?.content ?? "", + show: flowSettings?.elements?.privacy?.show ?? false, + }, + }, + }, + onSubmit: async (values) => { + await updateFlowSettings(values); + toast.success("Service settings updated successfully"); + }, + validate: () => {}, + }); + + return ( + + + + Elements + + + Manage the features that users will be able to see. + + + + + + + + Footer Links + + + + + + + + + + + + + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/index.tsx new file mode 100644 index 0000000000..dde243b1b3 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceSettings/index.tsx @@ -0,0 +1,14 @@ +import Container from "@mui/material/Container"; +import React from "react"; + +import FlowStatus from "./FlowStatus"; +import { FooterLinksAndLegalDisclaimer } from "./FooterLinksAndLegalDisclaimer"; + +const ServiceSettings: React.FC = () => ( + + + + +); + +export default ServiceSettings;