diff --git a/api.planx.uk/modules/ordnanceSurvey/controller.ts b/api.planx.uk/modules/ordnanceSurvey/controller.ts index d950eb36ab..41508db02b 100644 --- a/api.planx.uk/modules/ordnanceSurvey/controller.ts +++ b/api.planx.uk/modules/ordnanceSurvey/controller.ts @@ -4,37 +4,16 @@ import { IncomingMessage } from "http"; export const OS_DOMAIN = "https://api.os.uk"; -const MAP_ALLOWLIST: RegExp[] = [ - // Local development - /^http:\/\/(127\.0\.0\.1|localhost):(3000|5173|6006|7007)\/$/i, - // Documentation - /^https:\/\/.*\.netlify\.app\/$/i, - // PlanX - /^https:\/\/.*planx\.(pizza|dev|uk)\/$/i, - // Custom domains - /^https:\/\/.*(\.gov\.uk\/)$/i, -]; - export const useOrdnanceSurveyProxy = async ( req: Request, res: Response, next: NextFunction, -) => { - if (!isValid(req)) - return next({ - status: 401, - message: "Unauthorised", - }); - - return useProxy({ +) => + useProxy({ target: OS_DOMAIN, onProxyRes: (proxyRes) => setCORPHeaders(proxyRes), pathRewrite: (fullPath, req) => appendAPIKey(fullPath, req), })(req, res, next); -}; - -const isValid = (req: Request): boolean => - MAP_ALLOWLIST.some((re) => re.test(req.headers?.referer as string)); const setCORPHeaders = (proxyRes: IncomingMessage): void => { proxyRes.headers["Cross-Origin-Resource-Policy"] = "cross-origin"; diff --git a/api.planx.uk/modules/ordnanceSurvey/ordnanceSurvey.test.ts b/api.planx.uk/modules/ordnanceSurvey/ordnanceSurvey.test.ts index eebd817e33..56e38d548c 100644 --- a/api.planx.uk/modules/ordnanceSurvey/ordnanceSurvey.test.ts +++ b/api.planx.uk/modules/ordnanceSurvey/ordnanceSurvey.test.ts @@ -17,7 +17,6 @@ describe("Ordnance Survey proxy endpoint", () => { .reply(200, { test: "returned tile" }); await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://123.planx.pizza/" }) .expect(200) .then((response) => { expect(response.body).toEqual({ @@ -33,7 +32,6 @@ describe("Ordnance Survey proxy endpoint", () => { .reply(200, { test: "returned tile" }); await get(ENDPOINT + TILE_PATH + "?srs=3857") - .set({ referer: "https://www.planx.dev/" }) .expect(200) .then((response) => { expect(response.body).toEqual({ @@ -49,7 +47,6 @@ describe("Ordnance Survey proxy endpoint", () => { .reply(401, { test: "failed request" }); await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://www.planx.uk/" }) .expect(401) .then((response) => { expect(response.body).toEqual({ @@ -57,76 +54,6 @@ describe("Ordnance Survey proxy endpoint", () => { }); }); }); - - describe("CORS functionality", () => { - it("blocks requests which are not from a valid referrer", async () => { - await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://www.invalid-site.com/" }) - .expect(401) - .then((response) => { - expect(response.body).toEqual({ - error: "Unauthorised", - }); - }); - }); - - it("allows requests from allow-listed URLs", async () => { - nock(OS_DOMAIN) - .get(TILE_PATH) - .query({ key: process.env.ORDNANCE_SURVEY_API_KEY }) - .reply(200, { test: "returned tile" }); - - await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://oslmap.netlify.app/" }) - .expect(200) - .then((response) => { - expect(response.body).toEqual({ - test: "returned tile", - }); - expect(response.headers["cross-origin-resource-policy"]).toEqual( - "cross-origin", - ); - }); - }); - - it("allows requests from PlanX", async () => { - nock(OS_DOMAIN) - .get(TILE_PATH) - .query({ key: process.env.ORDNANCE_SURVEY_API_KEY }) - .reply(200, { test: "returned tile" }); - - await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://www.planx.dev/" }) - .expect(200) - .then((response) => { - expect(response.body).toEqual({ - test: "returned tile", - }); - expect(response.headers["cross-origin-resource-policy"]).toEqual( - "cross-origin", - ); - }); - }); - - it("allows requests from custom domains", async () => { - nock(OS_DOMAIN) - .get(TILE_PATH) - .query({ key: process.env.ORDNANCE_SURVEY_API_KEY }) - .reply(200, { test: "returned tile" }); - - await get(ENDPOINT + TILE_PATH) - .set({ referer: "https://planningservices.buckinghamshire.gov.uk/" }) - .expect(200) - .then((response) => { - expect(response.body).toEqual({ - test: "returned tile", - }); - expect(response.headers["cross-origin-resource-policy"]).toEqual( - "cross-origin", - ); - }); - }); - }); }); describe("appendAPIKey helper function", () => { diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index 8c95c29f04..ee394f463b 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -39,14 +39,20 @@ useSwaggerDocs(app); app.set("trust proxy", 1); const checkAllowedOrigins: CorsOptions["origin"] = (origin, callback) => { + if (!origin) return callback(null, true); + const isTest = process.env.NODE_ENV === "test"; - const isDevelopment = process.env.APP_ENVIRONMENT === "development"; + const localDevEnvs = + /^http:\/\/(127\.0\.0\.1|localhost):(3000|5173|6006|7007)$/; + const isDevelopment = + process.env.APP_ENVIRONMENT === "development" || localDevEnvs.test(origin); const allowList = process.env.CORS_ALLOWLIST?.split(", ") || []; - const isAllowed = Boolean(origin && allowList.includes(origin)); + const isAllowed = Boolean(allowList.includes(origin)); + const isMapDocs = Boolean(origin.endsWith("oslmap.netlify.app")); - !origin || isTest || isDevelopment || isAllowed + isTest || isDevelopment || isAllowed || isMapDocs ? callback(null, true) - : callback(new Error("Not allowed by CORS")); + : callback(new Error(`Not allowed by CORS. Origin: ${origin}`)); }; app.use( diff --git a/e2e/tests/ui-driven/src/globalHelpers.ts b/e2e/tests/ui-driven/src/globalHelpers.ts index a6f8552b90..52be7509c0 100644 --- a/e2e/tests/ui-driven/src/globalHelpers.ts +++ b/e2e/tests/ui-driven/src/globalHelpers.ts @@ -139,7 +139,7 @@ export async function clickContinue({ export async function clickBack({ page }: { page: Page }) { const waitPromise = waitForDebugLog(page); // assume debug message is triggered on state transition - await page.getByRole("button", { name: "Back", exact: true }).click(); + await page.getByTestId("backButton").click(); await waitPromise; } @@ -193,7 +193,7 @@ export async function answerChecklist({ }); await expect(checklist).toBeVisible(); for (const answer of answers) { - await page.locator("label", { hasText: answer }).click(); + await page.getByLabel(answer, { exact: true }).click(); } } diff --git a/editor.planx.uk/public/index.html b/editor.planx.uk/public/index.html index 9e84e83265..448f2a6f58 100644 --- a/editor.planx.uk/public/index.html +++ b/editor.planx.uk/public/index.html @@ -2,7 +2,7 @@ - + + + + + + + + + + + + + + Back + + + + + + + + + + + Share an idea + + + + + + + + + + + + + + + + + + + + Back + + + + + + + + + + + Share a comment + + + + + + + + + + + + + + + + + + + Thank you for sharing feedback + + + + + + + + + + We appreciate it lorem ipsum dolor sit amet, consectetuer + adipiscing elit. Aenean commodo ligula eget dolor. + + + + + + + ); +}; + +export default FeedbackComponent; diff --git a/editor.planx.uk/src/components/FeedbackPhaseBanner.tsx b/editor.planx.uk/src/components/FeedbackPhaseBanner.tsx new file mode 100644 index 0000000000..37809ce5e0 --- /dev/null +++ b/editor.planx.uk/src/components/FeedbackPhaseBanner.tsx @@ -0,0 +1,83 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Container from "@mui/material/Container"; +import Link from "@mui/material/Link"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import React from "react"; + +const Root = styled(Box)(({ theme }) => ({ + width: "100%", + backgroundColor: theme.palette.background.paper, +})); + +const ReportButton = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.common.white, + color: theme.palette.primary.main, + padding: "0.7em 1em", +})); + +const Inner = styled(Box)(({ theme }) => ({ + width: "100%", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + flexWrap: "wrap", + padding: theme.spacing(0.75, 0), +})); + +const PhaseWrap = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: "start", + alignItems: "start", + padding: theme.spacing(0.5, 1, 0.5, 0), +})); + +const BetaFlag = styled(Box)(({ theme }) => ({ + betaIcon: { + width: "100%", + [theme.breakpoints.up("sm")]: { + width: "auto", + marginRight: theme.spacing(2), + }, + }, +})); + +export default function FeedbackPhaseBanner(): FCReturn { + return ( + + + + + + PUBLIC BETA + + + This is a new service. Your feedback will help us + improve it. + + + Report an issue with this page + + + + ); +} diff --git a/editor.planx.uk/src/components/MoreInfoFeedback.tsx b/editor.planx.uk/src/components/MoreInfoFeedback.tsx new file mode 100644 index 0000000000..530f56a721 --- /dev/null +++ b/editor.planx.uk/src/components/MoreInfoFeedback.tsx @@ -0,0 +1,82 @@ +import CancelIcon from "@mui/icons-material/Cancel"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Container from "@mui/material/Container"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { contentFlowSpacing } from "@planx/components/shared/Preview/Card"; +import React from "react"; +import FeedbackDisclaimer from "ui/public/FeedbackDisclaimer"; +import FeedbackOption from "ui/public/FeedbackOption"; +import Input from "ui/shared/Input"; + +const MoreInfoFeedback = styled(Box)(({ theme }) => ({ + borderTop: `2px solid ${theme.palette.border.main}`, + padding: theme.spacing(2.5, 4, 8, 0), + [theme.breakpoints.up("sm")]: { + padding: theme.spacing(3, 4, 8, 1), + }, +})); + +const FeedbackBody = styled(Box)(({ theme }) => ({ + padding: theme.spacing(1, 0), + "& form > * + *": { + ...contentFlowSpacing(theme), + }, +})); + +const MoreInfoFeedbackComponent: React.FC = () => { + return ( + <> + + + + Did this help to answer your question? + + + + + + + + + + + + Please help us to improve this service by sharing feedback + + +
+ + + + +
+
+
+ + + + + Thank you for sharing feedback + + + + We appreciate it lorem ipsum dolor sit amet, consectetuer + adipiscing elit. Aenean commodo ligula eget dolor. + + + + + + ); +}; + +export default MoreInfoFeedbackComponent; diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts index 7bf56fbe6f..d676604615 100644 --- a/editor.planx.uk/src/lib/featureFlags.ts +++ b/editor.planx.uk/src/lib/featureFlags.ts @@ -2,6 +2,7 @@ const AVAILABLE_FEATURE_FLAGS = [ "DISABLE_SAVE_AND_RETURN", "SHOW_TEAM_SETTINGS", + "SHOW_INTERNAL_FEEDBACK", ] as const; type featureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx deleted file mode 100644 index 3d759f8b87..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Link from "@mui/material/Link"; -import { styled } from "@mui/material/styles"; -import Typography from "@mui/material/Typography"; -import { useFormik } from "formik"; -import { hasFeatureFlag } from "lib/featureFlags"; -import React from "react"; -import ColorPicker from "ui/editor/ColorPicker"; -import EditorRow from "ui/editor/EditorRow"; -import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; -import InputDescription from "ui/editor/InputDescription"; -import InputGroup from "ui/editor/InputGroup"; -import InputLegend from "ui/editor/InputLegend"; -import InputRow from "ui/shared/InputRow"; -import InputRowItem from "ui/shared/InputRowItem"; -import InputRowLabel from "ui/shared/InputRowLabel"; -import PublicFileUploadButton from "ui/shared/PublicFileUploadButton"; - -const DesignPreview = styled(Box)(({ theme }) => ({ - border: `2px solid ${theme.palette.border.input}`, - padding: theme.spacing(2), - boxShadow: "4px 4px 0px rgba(150, 150, 150, 0.5)", -})); - -const exampleColor = "#007078"; - -const DesignSettings: React.FC = () => { - const formik = useFormik<{ - themeColor: string; - buttonColor: string; - linkColor: string; - }>({ - initialValues: { - themeColor: exampleColor, - buttonColor: exampleColor, - linkColor: exampleColor, - }, - onSubmit: () => {}, - validate: () => {}, - }); - - const isUsingFeatureFlag = () => hasFeatureFlag("SHOW_TEAM_SETTINGS"); - - return ( - <> - - - Design - - - How your service appears to public users - - - {!isUsingFeatureFlag() ? ( - - {" "} - - ) : ( - <> - -
- - Theme colour & logo - - The theme colour and logo, are used in the header of the - service. The theme colour should be a dark colour that - contrasts with white ("#ffffff"). The logo should contrast - with a dark background colour (your theme colour) and have a - transparent background. - - - - See our guide for setting theme colours and logos - - - - - - formik.setFieldValue("themeColor", color) - } - label="Theme colour" - /> - - - - Logo: - - - - - .png or .svg - - - - - - Preview: - - - council logo - - - - - - -
-
- -
- - Button colour - - The button background colour should be either a dark or light - colour. The text will be programatically selected to contrast - with the selected colour (being either black or white). - - - - See our guide for setting button colours - - - - - - formik.setFieldValue("buttonColor", color) - } - label="Button colour" - /> - - - - - - Preview: - - - - - - - - - -
-
- -
- - Text link colour - - The text link colour should be a dark colour that contrasts - with white ("#ffffff"). - - - - See our guide for setting text link colours - - - - - - formik.setFieldValue("linkColor", color) - } - label="Text link colour" - /> - - - - - - Preview: - - - Example text link - - - - - - -
-
- -
- - Favicon - - Set the favicon to be used in the browser tab. The favicon - should be 32x32px and in .ico or .png format. - - - - See our guide for favicons - - - - Favicon: - - - - - .ico or .png - - - - - - - -
-
- - )} - - ); -}; - -export default DesignSettings; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx new file mode 100644 index 0000000000..25a5ea811b --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx @@ -0,0 +1,86 @@ +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import { darken, useTheme } from "@mui/material/styles"; +import { TeamTheme } from "@opensystemslab/planx-core/types"; +import { useFormik } from "formik"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { getContrastTextColor } from "styleUtils"; +import ColorPicker from "ui/editor/ColorPicker"; +import InputDescription from "ui/editor/InputDescription"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; + +import { DesignPreview, FormProps, SettingsForm } from "."; + +export const ButtonForm: React.FC = ({ + formikConfig, + onSuccess, +}) => { + const theme = useTheme(); + const formik = useFormik({ + ...formikConfig, + onSubmit: async (values, { resetForm }) => { + const isSuccess = await useStore.getState().updateTeamTheme({ + actionColour: values.actionColour, + }); + if (isSuccess) { + onSuccess(); + // Reset "dirty" status to disable Save & Reset buttons + resetForm({ values }); + } + }, + }); + + return ( + + + The button background colour should be either a dark or light + colour. The text will be programmatically selected to contrast with + the selected colour (being either black or white). + + + + See our guide for setting button colours (TODO) + + + + } + input={ + + + formik.setFieldValue("actionColour", color)} + label="Button colour" + /> + + + } + preview={ + + + + } + /> + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx new file mode 100644 index 0000000000..76860aa81a --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx @@ -0,0 +1,79 @@ +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; +import { TeamTheme } from "@opensystemslab/planx-core/types"; +import { useFormik } from "formik"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import ImgInput from "ui/editor/ImgInput"; +import InputDescription from "ui/editor/InputDescription"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; +import InputRowLabel from "ui/shared/InputRowLabel"; + +import { FormProps, SettingsForm } from "."; + +export const FaviconForm: React.FC = ({ + formikConfig, + onSuccess, +}) => { + const formik = useFormik({ + ...formikConfig, + onSubmit: async (values, { resetForm }) => { + const isSuccess = await useStore.getState().updateTeamTheme({ + favicon: values.favicon, + }); + if (isSuccess) { + onSuccess(); + // Reset "dirty" status to disable Save & Reset buttons + resetForm({ values }); + } + }, + }); + + const updateFavicon = (newFile: string | undefined) => + newFile + ? formik.setFieldValue("favicon", newFile) + : formik.setFieldValue("favicon", null); + + return ( + + + Set the favicon to be used in the browser tab. The favicon should be + 32x32px and in .ico or .png format. + + + See our guide for favicons (TODO) + + + } + input={ + + Favicon: + + + + + .ico or .png + + + } + /> + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx new file mode 100644 index 0000000000..bd177af46f --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx @@ -0,0 +1,82 @@ +import Link from "@mui/material/Link"; +import { getContrastRatio, useTheme } from "@mui/material/styles"; +import { TeamTheme } from "@opensystemslab/planx-core/types"; +import { useFormik } from "formik"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import ColorPicker from "ui/editor/ColorPicker"; +import InputDescription from "ui/editor/InputDescription"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; + +import { DesignPreview, FormProps, SettingsForm } from "."; + +export const TextLinkForm: React.FC = ({ + formikConfig, + onSuccess, +}) => { + const theme = useTheme(); + + const formik = useFormik({ + ...formikConfig, + validate: ({ linkColour }) => { + const isContrastThresholdMet = + getContrastRatio("#FFF", linkColour) > theme.palette.contrastThreshold; + + if (!isContrastThresholdMet) { + return { + linkColour: + "Colour does not meet accessibility contrast requirements (3:1)", + }; + } + }, + onSubmit: async (values, { resetForm }) => { + const isSuccess = await useStore.getState().updateTeamTheme({ + linkColour: values.linkColour, + }); + if (isSuccess) { + onSuccess(); + // Reset "dirty" status to disable Save & Reset buttons + resetForm({ values }); + } + }, + }); + + return ( + + + The text link colour should be a dark colour that contrasts with + white ("#ffffff"). + + + + See our guide for setting text link colours (TODO) + + + + } + input={ + + + formik.setFieldValue("linkColour", color)} + label="Text link colour" + /> + + + } + preview={ + + + Example text link + + + } + /> + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx new file mode 100644 index 0000000000..013865dd87 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx @@ -0,0 +1,125 @@ +import Link from "@mui/material/Link"; +import { getContrastRatio, useTheme } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { TeamTheme } from "@opensystemslab/planx-core/types"; +import { useFormik } from "formik"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import ColorPicker from "ui/editor/ColorPicker"; +import ImgInput from "ui/editor/ImgInput"; +import InputDescription from "ui/editor/InputDescription"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; +import InputRowLabel from "ui/shared/InputRowLabel"; + +import { DesignPreview, FormProps, SettingsForm } from "."; + +export const ThemeAndLogoForm: React.FC = ({ + formikConfig, + onSuccess, +}) => { + const theme = useTheme(); + const teamSlug = useStore((state) => state.teamSlug); + + const formik = useFormik({ + ...formikConfig, + validate: ({ primaryColour }) => { + const isContrastThresholdMet = + getContrastRatio("#FFF", primaryColour) > + theme.palette.contrastThreshold; + + if (!isContrastThresholdMet) { + return { + primaryColour: + "Theme colour does not meet accessibility contrast requirements (3:1)", + }; + } + }, + onSubmit: async (values, { resetForm }) => { + const isSuccess = await useStore.getState().updateTeamTheme({ + primaryColour: values.primaryColour, + logo: values.logo, + }); + if (isSuccess) { + onSuccess(); + // Reset "dirty" status to disable Save & Reset buttons + resetForm({ values }); + } + }, + }); + + const updateLogo = (newFile: string | undefined) => + newFile + ? formik.setFieldValue("logo", newFile) + : formik.setFieldValue("logo", null); + + return ( + + + The theme colour and logo, are used in the header of the service. + The theme colour should be a dark colour that contrasts with white + ("#ffffff"). The logo should contrast with a dark background colour + (your theme colour) and have a transparent background. + + + + See our guide for setting theme colours and logos + + + + } + input={ + <> + + + + formik.setFieldValue("primaryColour", color) + } + label="Theme colour" + /> + + + + Logo: + + + + + .png or .svg + + + + } + preview={ + + {formik.values.logo ? ( + council logo + ) : ( + + Planâś• / {teamSlug} + + )} + + } + /> + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx new file mode 100644 index 0000000000..56a953060a --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx @@ -0,0 +1,180 @@ +import Alert from "@mui/material/Alert"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +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"; +import { FormikConfig, FormikProps } from "formik"; +import { hasFeatureFlag } from "lib/featureFlags"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React, { useEffect, useState } from "react"; +import EditorRow from "ui/editor/EditorRow"; +import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; +import InputGroup from "ui/editor/InputGroup"; +import InputLegend from "ui/editor/InputLegend"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; + +import { ButtonForm } from "./ButtonForm"; +import { FaviconForm } from "./FaviconForm"; +import { TextLinkForm } from "./TextLinkForm"; +import { ThemeAndLogoForm } from "./ThemeAndLogoForm"; + +export const DesignPreview = styled(Box)(({ theme }) => ({ + 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"; + +type SettingsFormProps = { + legend: string; + description: React.ReactElement; + input: React.ReactElement; + formik: FormikProps; + preview?: React.ReactElement; +}; + +export interface FormProps { + formikConfig: FormikConfig; + onSuccess: () => void; +} + +export const SettingsForm: React.FC = ({ + formik, + legend, + description, + input, + preview, +}) => { + return ( + +
+ + {legend} + {description} + {input} + + {preview && ( + + + Preview: + + {preview} + + )} + + + + + + +
+
+ ); +}; + +const DesignSettings: React.FC = () => { + const isUsingFeatureFlag = hasFeatureFlag("SHOW_TEAM_SETTINGS"); + const [formikConfig, setFormikConfig] = useState< + FormikConfig | undefined + >(undefined); + + /** + * Fetch current team and setup shared form config + */ + useEffect(() => { + const fetchTeam = async () => { + try { + const fetchedTeam = await useStore.getState().fetchCurrentTeam(); + if (!fetchedTeam) throw Error("Unable to find team"); + + setFormikConfig({ + initialValues: fetchedTeam.theme, + // This value will be set per form section + onSubmit: () => {}, + validateOnBlur: false, + validateOnChange: false, + enableReinitialize: true, + }); + } catch (error) { + console.error("Error fetching team:", error); + } + }; + + fetchTeam(); + }, []); + + const [open, setOpen] = useState(false); + + const handleClose = ( + _event?: React.SyntheticEvent | Event, + reason?: string, + ) => { + if (reason === "clickaway") { + return; + } + + setOpen(false); + }; + + const onSuccess = () => setOpen(true); + + return ( + <> + + + Design + + + How your service appears to public users + + + {!isUsingFeatureFlag ? ( + + {" "} + + ) : ( + <> + {formikConfig && ( + <> + + + + + + )} + + + Theme updated successfully + + + + )} + + ); +}; + +export default DesignSettings; diff --git a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss index c04cd21333..d893e43c00 100644 --- a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss +++ b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss @@ -171,7 +171,7 @@ $pixel: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAA opacity: 0.3; } - > span:not(:first-child) { + > span:not(:nth-child(1)) { margin-left: 6px; } } @@ -352,7 +352,7 @@ $pixel: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAA $lineWidth; background-repeat: no-repeat, repeat-x, repeat-x; - &:first-child { + &:nth-child(1) { background-position: top center, top right, @@ -392,7 +392,7 @@ $pixel: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAA $lineWidth 100%, 1px 100%; - &:first-child { + &:nth-child(1) { background-position: left center, left bottom, diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts index 7506f3c70b..437b4142b4 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts @@ -24,6 +24,7 @@ export interface TeamStore { initTeamStore: (slug: string) => Promise; clearTeamStore: () => void; fetchCurrentTeam: () => Promise; + updateTeamTheme: (theme: Partial) => Promise; } export const teamStore: StateCreator< @@ -40,7 +41,7 @@ export const teamStore: StateCreator< notifyPersonalisation: undefined, boundaryBBox: undefined, - setTeam: (team) => + setTeam: (team) => { set({ teamId: team.id, teamTheme: team.theme, @@ -49,7 +50,13 @@ export const teamStore: StateCreator< teamSlug: team.slug, notifyPersonalisation: team.notifyPersonalisation, boundaryBBox: team.boundaryBBox, - }), + }); + + if (team.theme?.favicon) { + const favicon = document.getElementById("favicon") as HTMLLinkElement; + favicon.href = team.theme.favicon; + } + }, getTeam: () => ({ id: get().teamId, @@ -115,4 +122,10 @@ export const teamStore: StateCreator< const team = await $client.team.getBySlug(teamSlug); return team; }, + + updateTeamTheme: async (theme: Partial) => { + const { teamId, $client } = get(); + const isSuccess = await $client.team.updateTheme(teamId, theme); + return isSuccess; + }, }); diff --git a/editor.planx.uk/src/pages/Preview/Questions.tsx b/editor.planx.uk/src/pages/Preview/Questions.tsx index 58b1dafaf7..5f83ef110a 100644 --- a/editor.planx.uk/src/pages/Preview/Questions.tsx +++ b/editor.planx.uk/src/pages/Preview/Questions.tsx @@ -23,7 +23,7 @@ const BackBar = styled(Box)(() => ({ zIndex: "1000", })); -const BackButton = styled(ButtonBase)(({ theme, hidden }) => ({ +export const BackButton = styled(ButtonBase)(({ theme, hidden }) => ({ visibility: "visible", pointerEvents: "auto", display: "flex", @@ -170,7 +170,11 @@ const Questions = ({ previewEnvironment }: QuestionsProps) => {