diff --git a/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx b/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx index ca04a50ef5..b20401f812 100644 --- a/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx +++ b/editor.planx.uk/src/components/Feedback/MoreInfoFeedback.tsx @@ -23,11 +23,11 @@ const MoreInfoFeedback = styled(Box)(({ theme }) => ({ }, })); +export type Sentiment = "helpful" | "unhelpful"; + const MoreInfoFeedbackComponent: React.FC = () => { type View = "yes/no" | "input" | "thanks"; - type Sentiment = "helpful" | "unhelpful"; - const [currentFeedbackView, setCurrentFeedbackView] = useState("yes/no"); const [feedbackOption, setFeedbackOption] = useState(null); diff --git a/editor.planx.uk/src/components/Feedback/index.tsx b/editor.planx.uk/src/components/Feedback/index.tsx index ac2ff82ea0..f8dd889277 100644 --- a/editor.planx.uk/src/components/Feedback/index.tsx +++ b/editor.planx.uk/src/components/Feedback/index.tsx @@ -80,9 +80,9 @@ export type FeedbackFormInput = { id: string; }; -const Feedback: React.FC = () => { - type FeedbackCategory = "issue" | "idea" | "comment" | "inaccuracy"; +export type FeedbackCategory = "issue" | "idea" | "comment" | "inaccuracy"; +const Feedback: React.FC = () => { type View = "banner" | "triage" | FeedbackCategory | "thanks"; type ClickEvents = "close" | "back" | "triage" | FeedbackCategory; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/EditorMenu.tsx b/editor.planx.uk/src/pages/FlowEditor/components/EditorMenu.tsx index 09c3dbb45f..e99fb24bd9 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/EditorMenu.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/EditorMenu.tsx @@ -1,4 +1,5 @@ import FactCheckIcon from "@mui/icons-material/FactCheck"; +import RateReviewIcon from "@mui/icons-material/RateReview"; import TuneIcon from "@mui/icons-material/Tune"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; @@ -68,10 +69,10 @@ const MenuButton = styled(IconButton, { function EditorMenu() { const { navigate } = useNavigation(); - const { lastChunk } = useCurrentRoute(); + const { url } = useCurrentRoute(); const rootPath = rootFlowPath(); - const isActive = (route: string) => lastChunk.url.pathname.endsWith(route); + const isActive = (route: string) => url.pathname.endsWith(route); const handleClick = (route: string) => !isActive(route) && navigate(rootPath + route); @@ -91,6 +92,11 @@ function EditorMenu() { Icon: FactCheckIcon, route: "/submissions-log", }, + { + title: "Feedback", + Icon: RateReviewIcon, + route: "/feedback", + }, ]; return ( diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Flow/FeedbackPage.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Flow/FeedbackPage.tsx new file mode 100644 index 0000000000..59a2788649 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Flow/FeedbackPage.tsx @@ -0,0 +1,65 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import gql from "graphql-tag"; +import { client } from "lib/graphql"; +import React from "react"; +import { Feedback } from "routes/feedback"; + +interface Props { + feedback: Feedback[]; +} + +const GET_FEEDBACK_BY_ID_QUERY = gql` + query GetFeedbackById($feedbackId: Int!) { + feedback: feedback_summary(where: { feedback_id: { _eq: $feedbackId } }) { + address + createdAt: created_at + device + feedbackId: feedback_id + feedbackType: feedback_type + helpDefinition: help_definition + helpSources: help_sources + helpText: help_text + intersectingConstraints: intersecting_constraints + nodeData: node_data + nodeId: node_id + nodeText: node_text + nodeTitle: node_title + nodeType: node_type + projectType: project_type + serviceSlug: service_slug + teamSlug: team_slug + status + uprn + userComment: user_comment + userContext: user_context + } + } +`; + +const getDetailedFeedback = async (feedbackId: number) => { + const { + data: { + feedback: [detailedFeedback], + }, + } = await client.query({ + query: GET_FEEDBACK_BY_ID_QUERY, + variables: { feedbackId }, + }); + console.log(detailedFeedback); +}; + +export const FeedbackPage: React.FC = ({ feedback }) => { + return ( + + {feedback.map((item) => ( + + {JSON.stringify(item, null, 4)} + + + ))} + + ); +}; diff --git a/editor.planx.uk/src/routes/feedback.tsx b/editor.planx.uk/src/routes/feedback.tsx new file mode 100644 index 0000000000..078223a994 --- /dev/null +++ b/editor.planx.uk/src/routes/feedback.tsx @@ -0,0 +1,73 @@ +import { ComponentType } from "@opensystemslab/planx-core/types"; +import { FeedbackCategory } from "components/Feedback"; +import { Sentiment } from "components/Feedback/MoreInfoFeedback"; +import gql from "graphql-tag"; +import { compose, mount, NotFoundError, route, withData } from "navi"; +import { FeedbackPage } from "pages/FlowEditor/components/Flow/FeedbackPage"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; + +import { client } from "../lib/graphql"; +import { makeTitle } from "./utils"; + +type FeedbackType = Sentiment & FeedbackCategory; + +export interface Feedback { + id: number; + type: FeedbackType; + nodeTitle: string | null; + nodeType: keyof typeof ComponentType | null; + userComment: string | null; + userContext: string | null; + createdAt: string; +} + +const feedbackRoutes = compose( + withData((req) => ({ + mountpath: req.mountpath, + })), + + mount({ + "/": route(async (req) => { + const { team: teamSlug, flow: flowSlug } = req.params; + + const isAuthorised = useStore.getState().canUserEditTeam(teamSlug); + if (!isAuthorised) + throw new NotFoundError( + `User does not have access to ${req.originalUrl}`, + ); + + const { + data: { feedback }, + } = await client.query<{ feedback: Feedback[] }>({ + query: gql` + query GetFeebackForFlow($teamSlug: String!, $flowSlug: String!) { + feedback: feedback_summary( + order_by: { created_at: asc } + where: { + team_slug: { _eq: $teamSlug } + service_slug: { _eq: $flowSlug } + } + ) { + id: feedback_id + type: feedback_type + nodeTitle: node_title + nodeType: node_type + userComment: user_comment + userContext: user_context + createdAt: created_at + } + } + `, + variables: { teamSlug, flowSlug }, + }); + + return { + title: makeTitle("Flow Feedback"), + view: , + }; + }), + }), +); + +export default feedbackRoutes; diff --git a/editor.planx.uk/src/routes/flow.tsx b/editor.planx.uk/src/routes/flow.tsx index 22a797e35c..a21a13446b 100644 --- a/editor.planx.uk/src/routes/flow.tsx +++ b/editor.planx.uk/src/routes/flow.tsx @@ -202,6 +202,8 @@ const routes = compose( }; }), + "/feedback": lazy(() => import("./feedback")), + "/nodes": compose( withView((req) => { const [flow, ...breadcrumbs] = req.params.flow.split(","); diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index d4ac73921d..d66aacabf7 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -268,6 +268,76 @@ - table: name: feedback_summary schema: public + object_relationships: + - name: team + using: + manual_configuration: + column_mapping: + team_slug: slug + insertion_order: null + remote_table: + name: teams + schema: public + select_permissions: + - role: platformAdmin + permission: + columns: + - feedback_id + - device + - node_data + - address + - feedback_type + - help_definition + - help_sources + - help_text + - intersecting_constraints + - node_id + - node_text + - node_title + - node_type + - project_type + - service_slug + - status + - team_slug + - uprn + - user_comment + - user_context + - created_at + filter: {} + comment: "" + - role: teamEditor + permission: + columns: + - feedback_id + - device + - node_data + - address + - feedback_type + - help_definition + - help_sources + - help_text + - intersecting_constraints + - node_id + - node_text + - node_title + - node_type + - project_type + - service_slug + - status + - team_slug + - uprn + - user_comment + - user_context + - created_at + filter: + team: + members: + _and: + - user_id: + _eq: x-hasura-user-id + - role: + _eq: teamEditor + comment: "" - table: name: feedback_type_enum schema: public diff --git a/hasura.planx.uk/migrations/1715929881942_run_sql_migration/down.sql b/hasura.planx.uk/migrations/1715929881942_run_sql_migration/down.sql new file mode 100644 index 0000000000..acfa8a9478 --- /dev/null +++ b/hasura.planx.uk/migrations/1715929881942_run_sql_migration/down.sql @@ -0,0 +1,40 @@ +DROP VIEW "public"."feedback_summary"; + +CREATE OR REPLACE VIEW "public"."feedback_summary" AS +SELECT + fb.id AS feedback_id, + t.slug AS team, + f.slug AS service_slug, + fb.created_at, + fb.node_id, + fb.device, + fb.user_context, + fb.user_comment, + fb.feedback_type, + fb.status, + fb.node_type, + fb.node_data, + COALESCE( + fb.node_data ->> 'title', + fb.node_data ->> 'text', + fb.node_data ->> 'flagSet' + ) AS node_title, + fb.node_data ->> 'description' AS node_text, + fb.node_data ->> 'info' AS help_text, + fb.node_data ->> 'policyRef' AS help_sources, + fb.node_data ->> 'howMeasured' AS help_definition, + COALESCE( + fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'single_line_address', + fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'title' + ) AS address, + (fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'uprn') AS uprn, + (fb.user_data -> 'passport' -> 'data' ->> 'proposal.projectType') AS project_type, + (fb.user_data -> 'passport' -> 'data' ->> 'property.constraints.planning') AS intersecting_constraints +FROM + feedback fb +LEFT JOIN + flows f ON f.id = fb.flow_id +LEFT JOIN + teams t ON t.id = fb.team_id; + +GRANT SELECT ON public.feedback_summary TO metabase_read_only; diff --git a/hasura.planx.uk/migrations/1715929881942_run_sql_migration/up.sql b/hasura.planx.uk/migrations/1715929881942_run_sql_migration/up.sql new file mode 100644 index 0000000000..c03d19bf31 --- /dev/null +++ b/hasura.planx.uk/migrations/1715929881942_run_sql_migration/up.sql @@ -0,0 +1,40 @@ +DROP VIEW "public"."feedback_summary"; + +CREATE OR REPLACE VIEW "public"."feedback_summary" AS +SELECT + fb.id AS feedback_id, + t.slug AS team_slug, + f.slug AS service_slug, + fb.created_at, + fb.node_id, + fb.device, + fb.user_context, + fb.user_comment, + fb.feedback_type, + fb.status, + fb.node_type, + fb.node_data, + COALESCE( + fb.node_data ->> 'title', + fb.node_data ->> 'text', + fb.node_data ->> 'flagSet' + ) AS node_title, + fb.node_data ->> 'description' AS node_text, + fb.node_data ->> 'info' AS help_text, + fb.node_data ->> 'policyRef' AS help_sources, + fb.node_data ->> 'howMeasured' AS help_definition, + COALESCE( + fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'single_line_address', + fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'title' + ) AS address, + (fb.user_data -> 'passport' -> 'data' -> '_address' ->> 'uprn') AS uprn, + (fb.user_data -> 'passport' -> 'data' ->> 'proposal.projectType') AS project_type, + (fb.user_data -> 'passport' -> 'data' ->> 'property.constraints.planning') AS intersecting_constraints +FROM + feedback fb +LEFT JOIN + flows f ON f.id = fb.flow_id +LEFT JOIN + teams t ON t.id = fb.team_id; + +GRANT SELECT ON public.feedback_summary TO metabase_read_only;