From 9008373a25aeee74e84a34ae56b25509d445d222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 5 Nov 2024 14:55:55 +0000 Subject: [PATCH] feat: "No services found" card (#3891) --- .../Team/components/MembersTable.tsx | 2 +- .../src/pages/FlowEditor/lib/store/editor.ts | 30 +++- editor.planx.uk/src/pages/Team.tsx | 149 +++++++++--------- editor.planx.uk/src/pages/Teams.tsx | 3 +- editor.planx.uk/src/ui/editor/AddButton.tsx | 28 ++++ 5 files changed, 131 insertions(+), 81 deletions(-) create mode 100644 editor.planx.uk/src/ui/editor/AddButton.tsx diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx index ef3aea6b00..d07d63184e 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx @@ -9,8 +9,8 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import { Role } from "@opensystemslab/planx-core/types"; -import { AddButton } from "pages/Team"; import React, { useState } from "react"; +import { AddButton } from "ui/editor/AddButton"; import Permission from "ui/editor/Permission"; import { StyledAvatar, StyledTableRow } from "./../styles"; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts index b12d7f920e..64271e0ae7 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -104,16 +104,34 @@ interface PublishFlowResponse { message: string; } +export interface FlowSummary { + id: string; + name: string; + slug: string; + updatedAt: string; + operations: { + createdAt: string; + actor: { + firstName: string; + lastName: string; + } + }[] +} + export interface EditorStore extends Store.Store { addNode: (node: any, relationships?: any) => void; connect: (src: NodeId, tgt: NodeId, object?: any) => void; connectTo: (id: NodeId) => void; copyFlow: (flowId: string) => Promise; copyNode: (id: NodeId) => void; - createFlow: (teamId: any, newSlug: any, newName: string) => Promise; + createFlow: ( + teamId: number, + newSlug: string, + newName: string + ) => Promise; deleteFlow: (teamId: number, flowSlug: string) => Promise; validateAndDiffFlow: (flowId: string) => Promise; - getFlows: (teamId: number) => Promise; + getFlows: (teamId: number) => Promise; isClone: (id: NodeId) => boolean; lastPublished: (flowId: string) => Promise; lastPublisher: (flowId: string) => Promise; @@ -124,12 +142,12 @@ export interface EditorStore extends Store.Store { id: NodeId, parent?: NodeId, toBefore?: NodeId, - toParent?: NodeId, + toParent?: NodeId ) => void; pasteNode: (toParent: NodeId, toBefore: NodeId) => void; publishFlow: ( flowId: string, - summary?: string, + summary?: string ) => Promise; removeNode: (id: NodeId, parent: NodeId) => void; updateNode: (node: any, relationships?: any) => void; @@ -326,7 +344,7 @@ export const editorStore: StateCreator< getFlows: async (teamId) => { client.cache.reset(); - const { data } = await client.query({ + const { data: { flows } } = await client.query<{ flows: FlowSummary[] }>({ query: gql` query GetFlows($teamId: Int!) { flows(where: { team: { id: { _eq: $teamId } } }) { @@ -349,7 +367,7 @@ export const editorStore: StateCreator< }, }); - return data; + return flows; }, isClone: (id) => { diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index 7b9b2964cb..ad0c8b9d0b 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -1,10 +1,8 @@ import { gql } from "@apollo/client"; -import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; import Edit from "@mui/icons-material/Edit"; import Visibility from "@mui/icons-material/Visibility"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; -import ButtonBase from "@mui/material/ButtonBase"; import Container from "@mui/material/Container"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; @@ -13,15 +11,18 @@ import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; +import { flow } from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { Link, useNavigation } from "react-navi"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; import { borderedFocusStyle } from "theme"; +import { AddButton } from "ui/editor/AddButton"; import { slugify } from "utils"; import { client } from "../lib/graphql"; import SimpleMenu from "../ui/editor/SimpleMenu"; import { useStore } from "./FlowEditor/lib/store"; +import { FlowSummary } from "./FlowEditor/lib/store/editor"; import { formatLastEditMessage } from "./FlowEditor/utils"; const DashboardList = styled("ul")(({ theme }) => ({ @@ -103,32 +104,9 @@ const Confirm = ({ ); -const AddButtonRoot = styled(ButtonBase)(({ theme }) => ({ - fontSize: 20, - display: "flex", - alignItems: "center", - textAlign: "left", - color: theme.palette.primary.main, - fontWeight: FONT_WEIGHT_SEMI_BOLD, -})); - -export function AddButton({ - children, - onClick, -}: { - children: string; - onClick: () => void; -}): FCReturn { - return ( - - {children} - - ); -} - interface FlowItemProps { - flow: any; - flows: any; + flow: FlowSummary; + flows: FlowSummary[]; teamId: number; teamSlug: string; refreshFlows: () => void; @@ -279,30 +257,77 @@ const FlowItem: React.FC = ({ ); }; +const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => ( + ({ + mt: 4, + backgroundColor: theme.palette.background.paper, + borderRadius: "8px", + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 2, + padding: 2 + })}> + No services found + Get started by creating your first service + + +) + +const AddFlowButton: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => { + const { navigate } = useNavigation(); + const { teamId, createFlow, teamSlug } = useStore() + + const addFlow = async () => { + const newFlowName = prompt("Service name"); + if (!newFlowName) return; + + const newFlowSlug = slugify(newFlowName); + const duplicateFlowName = flows?.find( + (flow) => flow.slug === newFlowSlug, + ); + + if (duplicateFlowName) { + alert( + `The flow "${newFlowName}" already exists. Enter a unique flow name to continue`, + ); + } + + const newId = await createFlow(teamId, newFlowSlug, newFlowName); + navigate(`/${teamSlug}/${newId}`); + } + + return( + + Add a new service + + ) +} + const Team: React.FC = () => { - const { id: teamId, slug } = useStore((state) => state.getTeam()); - const [flows, setFlows] = useState(null); - const navigation = useNavigation(); + const [{ id: teamId, slug }, canUserEditTeam, getFlows] = useStore((state) => [state.getTeam(), state.canUserEditTeam, state.getFlows ]); + const [flows, setFlows] = useState(null); const fetchFlows = useCallback(() => { - useStore - .getState() - .getFlows(teamId) - .then((res: { flows: any[] }) => { - // Copy the array and sort by most recently edited desc using last associated operation.createdAt, not flow.updatedAt - const sortedFlows = res.flows.toSorted((a, b) => - b.operations[0]["createdAt"].localeCompare( - a.operations[0]["createdAt"], - ), - ); - setFlows(sortedFlows); - }); - }, [teamId, setFlows]); + getFlows(teamId) + .then((flows) => { + // Copy the array and sort by most recently edited desc using last associated operation.createdAt, not flow.updatedAt + const sortedFlows = flows.toSorted((a, b) => + b.operations[0]["createdAt"].localeCompare( + a.operations[0]["createdAt"], + ), + ); + setFlows(sortedFlows); + }); + }, [teamId, setFlows, getFlows]); useEffect(() => { fetchFlows(); }, [fetchFlows]); + const teamHasFlows = flows && Boolean(flows.length) + const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); + return ( { Services - {useStore.getState().canUserEditTeam(slug) ? ( + {canUserEditTeam(slug) ? ( ) : ( )} - {useStore.getState().canUserEditTeam(slug) && ( - { - const newFlowName = prompt("Service name"); - if (newFlowName) { - const newFlowSlug = slugify(newFlowName); - const duplicateFlowName = flows?.find( - (flow) => flow.slug === newFlowSlug, - ); - - !duplicateFlowName - ? useStore - .getState() - .createFlow(teamId, newFlowSlug, newFlowName) - .then((newId: string) => { - navigation.navigate(`/${slug}/${newId}`); - }) - : alert( - `The flow "${newFlowName}" already exists. Enter a unique flow name to continue`, - ); - } - }} - > - Add a new service - + {showAddFlowButton && ( + )} - {flows && ( + {teamHasFlows && ( - {flows.map((flow: any) => ( + {flows.map((flow) => ( { }} /> ))} - - )} + ) + } + { flows && !flows.length && } ); }; diff --git a/editor.planx.uk/src/pages/Teams.tsx b/editor.planx.uk/src/pages/Teams.tsx index 58c78f88b6..2c42c259b0 100644 --- a/editor.planx.uk/src/pages/Teams.tsx +++ b/editor.planx.uk/src/pages/Teams.tsx @@ -8,11 +8,12 @@ import navigation from "lib/navigation"; import React from "react"; import { Link } from "react-navi"; import { borderedFocusStyle } from "theme"; +import { AddButton } from "ui/editor/AddButton"; import Permission from "ui/editor/Permission"; import { slugify } from "utils"; import { useStore } from "./FlowEditor/lib/store"; -import { AddButton } from "./Team"; + interface TeamTheme { slug: string; diff --git a/editor.planx.uk/src/ui/editor/AddButton.tsx b/editor.planx.uk/src/ui/editor/AddButton.tsx new file mode 100644 index 0000000000..a0b1b446fa --- /dev/null +++ b/editor.planx.uk/src/ui/editor/AddButton.tsx @@ -0,0 +1,28 @@ +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import ButtonBase from "@mui/material/ButtonBase"; +import { styled } from "@mui/material/styles"; +import React, { } from "react"; +import { FONT_WEIGHT_SEMI_BOLD } from "theme"; + +const AddButtonRoot = styled(ButtonBase)(({ theme }) => ({ + fontSize: 20, + display: "flex", + alignItems: "center", + textAlign: "left", + color: theme.palette.primary.main, + fontWeight: FONT_WEIGHT_SEMI_BOLD, +})); + +export function AddButton({ + children, + onClick, +}: { + children: string; + onClick: () => void; +}): FCReturn { + return ( + + {children} + + ); +} \ No newline at end of file