From 06a96917b5f253f08bcbbbbc2d3ace1a74a39744 Mon Sep 17 00:00:00 2001 From: Mike <36415632+Mike-Heneghan@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:08:04 +0000 Subject: [PATCH] feat: wire up feedback forms (#2701) --- .../components/shared/Preview/MoreInfo.tsx | 2 +- .../src/components/Feedback/FeedbackForm.tsx | 76 ++++++++ .../{ => Feedback}/FeedbackPhaseBanner.tsx | 0 .../{ => Feedback}/MoreInfoFeedback.tsx | 61 +++--- .../{Feedback.tsx => Feedback/index.tsx} | 173 ++++++++---------- editor.planx.uk/src/components/Footer.tsx | 5 +- editor.planx.uk/src/lib/feedback.ts | 94 +++++++++- hasura.planx.uk/metadata/tables.yaml | 8 +- .../down.sql | 14 ++ .../up.sql | 16 ++ 10 files changed, 312 insertions(+), 137 deletions(-) create mode 100644 editor.planx.uk/src/components/Feedback/FeedbackForm.tsx rename editor.planx.uk/src/components/{ => Feedback}/FeedbackPhaseBanner.tsx (100%) rename editor.planx.uk/src/components/{ => Feedback}/MoreInfoFeedback.tsx (76%) rename editor.planx.uk/src/components/{Feedback.tsx => Feedback/index.tsx} (71%) create mode 100644 hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/down.sql create mode 100644 hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/up.sql diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/MoreInfo.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/MoreInfo.tsx index 2d6bd4edec..89e551aab0 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/MoreInfo.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/MoreInfo.tsx @@ -4,7 +4,7 @@ import Container from "@mui/material/Container"; import Drawer, { DrawerProps } from "@mui/material/Drawer"; import IconButton from "@mui/material/IconButton"; import { styled } from "@mui/material/styles"; -import MoreInfoFeedbackComponent from "components/MoreInfoFeedback"; +import MoreInfoFeedbackComponent from "components/Feedback/MoreInfoFeedback"; import { hasFeatureFlag } from "lib/featureFlags"; import React from "react"; diff --git a/editor.planx.uk/src/components/Feedback/FeedbackForm.tsx b/editor.planx.uk/src/components/Feedback/FeedbackForm.tsx new file mode 100644 index 0000000000..b500d143ed --- /dev/null +++ b/editor.planx.uk/src/components/Feedback/FeedbackForm.tsx @@ -0,0 +1,76 @@ +import Button from "@mui/material/Button"; +import { styled } from "@mui/material/styles"; +import { contentFlowSpacing } from "@planx/components/shared/Preview/Card"; +import { Form, Formik, useFormikContext } from "formik"; +import React from "react"; +import FeedbackDisclaimer from "ui/public/FeedbackDisclaimer"; +import InputLabel from "ui/public/InputLabel"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; +import Input from "ui/shared/Input"; + +import { FeedbackFormInput, FormProps, UserFeedback } from "."; + +const StyledForm = styled(Form)(({ theme }) => ({ + "& > *": contentFlowSpacing(theme), +})); + +function FormInputs({ inputs }: { inputs: FeedbackFormInput[] }): FCReturn { + const { values, errors, handleChange } = useFormikContext(); + + return ( + <> + {inputs.map((input: FeedbackFormInput) => ( + + {input.label ? ( + + + + ) : ( + + )} + + ))} + + ); +} + +const FeedbackForm: React.FC = ({ inputs, handleSubmit }) => { + const initialValues: UserFeedback = { + userContext: undefined, + userComment: "", + }; + + return ( + + + + + + + + ); +}; + +export default FeedbackForm; diff --git a/editor.planx.uk/src/components/FeedbackPhaseBanner.tsx b/editor.planx.uk/src/components/Feedback/FeedbackPhaseBanner.tsx similarity index 100% rename from editor.planx.uk/src/components/FeedbackPhaseBanner.tsx rename to editor.planx.uk/src/components/Feedback/FeedbackPhaseBanner.tsx diff --git a/editor.planx.uk/src/components/MoreInfoFeedback.tsx b/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx similarity index 76% rename from editor.planx.uk/src/components/MoreInfoFeedback.tsx rename to editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx index bb6a7b02a8..cbe22755bd 100644 --- a/editor.planx.uk/src/components/MoreInfoFeedback.tsx +++ b/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx @@ -1,15 +1,19 @@ 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 { + getInternalFeedbackMetadata, + insertFeedbackMutation, +} from "lib/feedback"; import React, { useState } from "react"; -import FeedbackDisclaimer from "ui/public/FeedbackDisclaimer"; import FeedbackOption from "ui/public/FeedbackOption"; -import Input from "ui/shared/Input"; + +import { FeedbackFormInput, UserFeedback } from "."; +import FeedbackForm from "./FeedbackForm"; const MoreInfoFeedback = styled(Box)(({ theme }) => ({ borderTop: `2px solid ${theme.palette.border.main}`, @@ -21,9 +25,7 @@ const MoreInfoFeedback = styled(Box)(({ theme }) => ({ const FeedbackBody = styled(Box)(({ theme }) => ({ padding: theme.spacing(1, 0), - "& form > * + *": { - ...contentFlowSpacing(theme), - }, + "& form > * + *": contentFlowSpacing(theme), })); const MoreInfoFeedbackComponent: React.FC = () => { @@ -49,23 +51,14 @@ const MoreInfoFeedbackComponent: React.FC = () => { } }; - const handleFeedbackFormSubmit = (e: any) => { - e.preventDefault(); - - const formData = new FormData(e.target); - const formDataPayload: any = {}; - - for (const [key, value] of formData.entries()) { - formDataPayload[key] = value; - } - - console.log("The users selection", feedbackOption); - - console.log("The user inputs", formDataPayload); - // Prep the form data payload? - + async function handleFeedbackFormSubmit(values: UserFeedback) { + if (!feedbackOption) return; + const metadata = await getInternalFeedbackMetadata(); + const feedbackType = { feedbackType: feedbackOption }; + const data = { ...metadata, ...feedbackType, ...values }; + await insertFeedbackMutation(data); setCurrentFeedbackView("thanks"); - }; + } function FeedbackYesNo(): FCReturn { return ( @@ -94,6 +87,13 @@ const MoreInfoFeedbackComponent: React.FC = () => { } function FeedbackInput(): FCReturn { + const commentFormInputs: FeedbackFormInput[] = [ + { + name: "userComment", + ariaDescribedBy: "comment-title", + }, + ]; + return ( @@ -101,19 +101,10 @@ const MoreInfoFeedbackComponent: React.FC = () => { Please help us to improve this service by sharing feedback -
handleFeedbackFormSubmit(e)}> - - - - +
diff --git a/editor.planx.uk/src/components/Feedback.tsx b/editor.planx.uk/src/components/Feedback/index.tsx similarity index 71% rename from editor.planx.uk/src/components/Feedback.tsx rename to editor.planx.uk/src/components/Feedback/index.tsx index 7527cceabd..0ecffc2e0e 100644 --- a/editor.planx.uk/src/components/Feedback.tsx +++ b/editor.planx.uk/src/components/Feedback/index.tsx @@ -4,21 +4,23 @@ import LightbulbIcon from "@mui/icons-material/Lightbulb"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import WarningIcon from "@mui/icons-material/Warning"; import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; import Container from "@mui/material/Container"; import IconButton from "@mui/material/IconButton"; import { styled } from "@mui/material/styles"; import SvgIcon from "@mui/material/SvgIcon"; import Typography from "@mui/material/Typography"; -import { contentFlowSpacing } from "@planx/components/shared/Preview/Card"; -import FeedbackPhaseBanner from "components/FeedbackPhaseBanner"; +import { + getInternalFeedbackMetadata, + insertFeedbackMutation, +} from "lib/feedback"; +import { useStore } from "pages/FlowEditor/lib/store"; import { BackButton } from "pages/Preview/Questions"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { usePrevious } from "react-use"; -import FeedbackDisclaimer from "ui/public/FeedbackDisclaimer"; import FeedbackOption from "ui/public/FeedbackOption"; -import InputLabel from "ui/public/InputLabel"; -import Input from "ui/shared/Input"; + +import FeedbackForm from "./FeedbackForm"; +import FeedbackPhaseBanner from "./FeedbackPhaseBanner"; const FeedbackWrapper = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.background.paper, @@ -61,13 +63,24 @@ const FeedbackBody = styled(Box)(({ theme }) => ({ maxWidth: theme.breakpoints.values.formWrap, })); -const FeedbackForm = styled("form")(({ theme }) => ({ - "& > *": { - ...contentFlowSpacing(theme), - }, -})); +export type UserFeedback = { + userContext?: string; + userComment: string; +}; + +export interface FormProps { + inputs: FeedbackFormInput[]; + handleSubmit: (values: UserFeedback) => void; +} -const FeedbackComponent: React.FC = () => { +export type FeedbackFormInput = { + name: keyof UserFeedback; + label?: string; + id?: string; + ariaDescribedBy?: string; +}; + +const Feedback: React.FC = () => { type FeedbackCategory = "issue" | "idea" | "comment"; type View = "banner" | "triage" | FeedbackCategory | "thanks"; @@ -77,6 +90,13 @@ const FeedbackComponent: React.FC = () => { const [currentFeedbackView, setCurrentFeedbackView] = useState("banner"); const previousFeedbackView = usePrevious(currentFeedbackView); + const breadcrumbs = useStore((state) => state.breadcrumbs); + + useEffect(() => { + if (currentFeedbackView === "thanks") { + setCurrentFeedbackView("banner"); + } + }, [breadcrumbs]); function handleFeedbackViewClick(event: ClickEvents) { switch (event) { @@ -92,20 +112,13 @@ const FeedbackComponent: React.FC = () => { } } - const handleFeedbackFormSubmit = (e: any) => { - e.preventDefault(); - const formData = new FormData(e.target); - const formDataPayload: any = {}; - - for (const [key, value] of formData.entries()) { - formDataPayload[key] = value; - } - - console.log("The user inputs", formDataPayload); - // Prep the form data payload? - + async function handleFeedbackFormSubmit(values: UserFeedback) { + const metadata = await getInternalFeedbackMetadata(); + const feedbackType = { feedbackType: currentFeedbackView }; + const data = { ...metadata, ...feedbackType, ...values }; + await insertFeedbackMutation(data); setCurrentFeedbackView("thanks"); - }; + } function BackAndCloseFeedbackHeader(): FCReturn { return ( @@ -224,43 +237,29 @@ const FeedbackComponent: React.FC = () => { } function ReportAnIssue(): FCReturn { + const issueFormInputs: FeedbackFormInput[] = [ + { + name: "userContext", + label: "What were you doing?", + id: "issue-input-1", + }, + { + name: "userComment", + label: "What went wrong?", + id: "issue-input-2", + }, + ]; + return ( - handleFeedbackFormSubmit(e)}> - - - - - - - - - + @@ -269,6 +268,13 @@ const FeedbackComponent: React.FC = () => { } function ShareAnIdea(): FCReturn { + const shareFormInputs: FeedbackFormInput[] = [ + { + name: "userComment", + ariaDescribedBy: "idea-title", + }, + ]; + return ( @@ -280,24 +286,11 @@ const FeedbackComponent: React.FC = () => { Share an idea - handleFeedbackFormSubmit(e)}> - - - - - + + @@ -306,6 +299,13 @@ const FeedbackComponent: React.FC = () => { } function ShareAComment(): FCReturn { + const commentFormInputs: FeedbackFormInput[] = [ + { + name: "userComment", + ariaDescribedBy: "comment-title", + }, + ]; + return ( @@ -318,23 +318,10 @@ const FeedbackComponent: React.FC = () => { - handleFeedbackFormSubmit(e)}> - - - - + @@ -380,4 +367,4 @@ const FeedbackComponent: React.FC = () => { return ; }; -export default FeedbackComponent; +export default Feedback; diff --git a/editor.planx.uk/src/components/Footer.tsx b/editor.planx.uk/src/components/Footer.tsx index 67cef947e8..01f27e698b 100644 --- a/editor.planx.uk/src/components/Footer.tsx +++ b/editor.planx.uk/src/components/Footer.tsx @@ -8,6 +8,7 @@ import DialogContent from "@mui/material/DialogContent"; import Link from "@mui/material/Link"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; +import { hasFeatureFlag } from "lib/featureFlags"; import { getFeedbackMetadata } from "lib/feedback"; import React, { useEffect, useState } from "react"; import { Link as ReactNaviLink } from "react-navi"; @@ -80,6 +81,8 @@ export default function Footer(props: Props) { const feedbackFishId = process.env.REACT_APP_FEEDBACK_FISH_ID; + const isUsingFeatureFlag = hasFeatureFlag("SHOW_INTERNAL_FEEDBACK"); + useEffect(() => { let feedbackFishPostMessageWorkingCorrectly: boolean; const handleMessage = (event: MessageEvent) => { @@ -134,7 +137,7 @@ export default function Footer(props: Props) { {items ?.filter((item) => item.title) .map((item) => )} - {feedbackFishId && ( + {feedbackFishId && !isUsingFeatureFlag && ( <> {feedbackPrivacyNoteVisible && ( diff --git a/editor.planx.uk/src/lib/feedback.ts b/editor.planx.uk/src/lib/feedback.ts index 3eed6c885b..b90bd96e5e 100644 --- a/editor.planx.uk/src/lib/feedback.ts +++ b/editor.planx.uk/src/lib/feedback.ts @@ -1,4 +1,9 @@ -import { useStore } from "pages/FlowEditor/lib/store"; +import { gql } from "@apollo/client"; +import { TYPES } from "@planx/components/types"; +import Bowser from "bowser"; +import { Store, useStore } from "pages/FlowEditor/lib/store"; + +import { publicClient } from "./graphql"; export const submitFeedback = ( text: string, @@ -49,3 +54,90 @@ export const getFeedbackMetadata = (): Record => { }; return feedbackMetadata; }; + +type UserData = { + breadcrumbs: Store.breadcrumbs; + passport: Store.passport; +}; + +export type FeedbackMetadata = { + teamId?: number; + flowId?: string; + nodeId?: string | null; + nodeType?: string | null; + device: Bowser.Parser.ParsedResult; + userData: UserData; +}; + +export async function getInternalFeedbackMetadata(): Promise { + const { + breadcrumbs, + currentCard, + computePassport, + fetchCurrentTeam, + id: flowId, + } = useStore.getState(); + const { id: teamId } = await fetchCurrentTeam(); + const node = currentCard(); + const userData = { + breadcrumbs: breadcrumbs, + passport: computePassport(), + }; + const metadata = { + teamId, + flowId, + nodeId: node?.id, + nodeType: node?.type ? TYPES[node.type] : null, + device: Bowser.parse(window.navigator.userAgent), + userData: userData, + }; + + return metadata; +} + +export async function insertFeedbackMutation(data: { + teamId?: number; + flowId?: string; + nodeId?: string | null; + nodeType?: string | null; + device?: Bowser.Parser.ParsedResult; + userData?: UserData; + userContext?: string; + userComment: string; + feedbackType: string; +}) { + const result = await publicClient.mutate({ + mutation: gql` + mutation InsertFeedback( + $teamId: Int + $flowId: uuid + $nodeId: String + $nodeType: String + $device: jsonb + $userData: jsonb + $userContext: String + $userComment: String! + $feedbackType: feedback_type_enum_enum! + ) { + insert_feedback( + objects: { + team_id: $teamId + flow_id: $flowId + node_id: $nodeId + node_type: $nodeType + device: $device + user_data: $userData + user_context: $userContext + user_comment: $userComment + feedback_type: $feedbackType + } + ) { + affected_rows + } + } + `, + variables: data, + }); + + return result.data.insert_feedback.affected_rows; +} diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index eedbef35b4..8c86335fb9 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -235,22 +235,18 @@ permission: check: {} columns: - - address - - breadcrumbs - - component_metadata - created_at - device - feedback_type - flow_id - - help_text - id - node_id - - node_text - - project_type + - node_type - status - team_id - user_comment - user_context + - user_data comment: Allow users who want to leave feedback on their experience to write to the table - table: name: feedback_status_enum diff --git a/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/down.sql b/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/down.sql new file mode 100644 index 0000000000..25ece46a68 --- /dev/null +++ b/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/down.sql @@ -0,0 +1,14 @@ + +alter table "public"."feedback" drop column "node_type"; + +alter table "public"."feedback" add column "address" text; + +alter table "public"."feedback" add column "project_type" text; + +alter table "public"."feedback" add column "component_metadata" jsonb; + +alter table "public"."feedback" rename column "user_data" to "breadcrumbs"; + +alter table "public"."feedback" add column "help_text" text; + +alter table "public"."feedback" add column "node_text" text; \ No newline at end of file diff --git a/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/up.sql b/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/up.sql new file mode 100644 index 0000000000..be152646f8 --- /dev/null +++ b/hasura.planx.uk/migrations/1706722642884_alter_table_public_feedback_drop_columns_rename_column_and_add_column/up.sql @@ -0,0 +1,16 @@ +alter table "public"."feedback" drop column "node_text" cascade; + +alter table "public"."feedback" drop column "help_text" cascade; + +alter table "public"."feedback" rename column "breadcrumbs" to "user_data"; + +alter table "public"."feedback" drop column "address" cascade; + +alter table "public"."feedback" drop column "component_metadata" cascade; + +alter table "public"."feedback" drop column "project_type" cascade; + +alter table "public"."feedback" add column "node_type" text + null; + +comment on column "public"."feedback"."node_type" is E'The human readable type of the node';