diff --git a/editor.planx.uk/src/pages/FlowCard.tsx b/editor.planx.uk/src/pages/FlowCard.tsx new file mode 100644 index 0000000000..5fcb8767ca --- /dev/null +++ b/editor.planx.uk/src/pages/FlowCard.tsx @@ -0,0 +1,288 @@ +import { gql } from "@apollo/client"; +import MoreHoriz from "@mui/icons-material/MoreHoriz"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +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 React from "react"; +import { Link } from "react-navi"; +import { inputFocusStyle } from "theme"; +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"; + +export const Card = styled("li")(({ theme }) => ({ + listStyle: "none", + position: "relative", + display: "flex", + flexDirection: "column", + justifyContent: "stretch", + borderRadius: "3px", + backgroundColor: theme.palette.background.default, + border: `1px solid ${theme.palette.border.main}`, + boxShadow: "0 2px 4px 0 rgba(0, 0, 0, 0.1)", +})); + +export const CardContent = styled(Box)(({ theme }) => ({ + position: "relative", + height: "100%", + textDecoration: "none", + color: "currentColor", + padding: theme.spacing(2), + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: theme.spacing(1.5), + margin: 0, + width: "100%", +})); + +const DashboardLink = styled(Link)(() => ({ + position: "absolute", + left: 0, + top: 0, + width: "100%", + height: "100%", + zIndex: 1, + "&:focus": { + ...inputFocusStyle, + }, +})); + +const LinkSubText = styled(Box)(({ theme }) => ({ + color: theme.palette.text.secondary, + fontWeight: "normal", + paddingTop: theme.spacing(0.75), +})); + +const StyledSimpleMenu = styled(SimpleMenu)(({ theme }) => ({ + display: "flex", + marginTop: "auto", + borderTop: `1px solid ${theme.palette.border.main}`, + backgroundColor: theme.palette.background.paper, + overflow: "hidden", + borderRadius: "0px 0px 4px 4px", + "& > button": { + padding: theme.spacing(0.25, 1), + width: "100%", + justifyContent: "flex-start", + "& > svg": { + display: "none", + }, + }, +})); + +const Confirm = ({ + title, + content, + submitLabel, + open, + onClose, + onConfirm, +}: { + title: string; + content: string; + submitLabel: string; + open: boolean; + onClose: () => void; + onConfirm: () => void; +}) => ( + { + onClose(); + }} + > + {title} + + {content} + + + + Cancel + + + {submitLabel} + + + +); + +interface FlowCardProps { + flow: FlowSummary; + flows: FlowSummary[]; + teamId: number; + teamSlug: string; + refreshFlows: () => void; +} + +const FlowCard: React.FC = ({ + flow, + flows, + teamId, + teamSlug, + refreshFlows, +}) => { + const [deleting, setDeleting] = React.useState(false); + + const handleDelete = () => { + useStore + .getState() + .deleteFlow(teamId, flow.slug) + .then(() => { + setDeleting(false); + refreshFlows(); + }); + }; + const handleCopy = () => { + useStore + .getState() + .copyFlow(flow.id) + .then(() => { + refreshFlows(); + }); + }; + const handleMove = (newTeam: string) => { + useStore + .getState() + .moveFlow(flow.id, newTeam) + .then(() => { + refreshFlows(); + }); + }; + + return ( + <> + {deleting && ( + { + setDeleting(false); + }} + onConfirm={handleDelete} + submitLabel="Delete Service" + /> + )} + + + + + {flow.name} + + + {formatLastEditMessage( + flow.operations[0]?.createdAt, + flow.operations[0]?.actor, + )} + + + + + {useStore.getState().canUserEditTeam(teamSlug) && ( + { + const newName = prompt("New name", flow.name); + if (newName && newName !== flow.name) { + const newSlug = slugify(newName); + const duplicateFlowName = flows?.find( + (flow: any) => flow.slug === newSlug, + ); + if (!duplicateFlowName) { + await client.mutate({ + mutation: gql` + mutation UpdateFlowSlug( + $teamId: Int + $slug: String + $newSlug: String + $newName: String + ) { + update_flows( + where: { + team: { id: { _eq: $teamId } } + slug: { _eq: $slug } + } + _set: { slug: $newSlug, name: $newName } + ) { + affected_rows + } + } + `, + variables: { + teamId: teamId, + slug: flow.slug, + newSlug: newSlug, + newName: newName, + }, + }); + + refreshFlows(); + } else if (duplicateFlowName) { + alert( + `The flow "${newName}" already exists. Enter a unique flow name to continue`, + ); + } + } + }, + label: "Rename", + }, + { + label: "Copy", + onClick: () => { + handleCopy(); + }, + }, + { + label: "Move", + onClick: () => { + const newTeam = prompt("New team"); + if (newTeam) { + if (slugify(newTeam) === teamSlug) { + alert( + `This flow already belongs to ${teamSlug}, skipping move`, + ); + } else { + handleMove(slugify(newTeam)); + } + } + }, + }, + { + label: "Delete", + onClick: () => { + setDeleting(true); + }, + error: true, + }, + ]} + > + + + + Menu + + + + )} + + > + ); +}; + +export default FlowCard; diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index e462dfbbd3..92efd3380d 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -1,8 +1,4 @@ -import { gql } from "@apollo/client"; -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 Container from "@mui/material/Container"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; @@ -22,264 +18,40 @@ import { SortableFields, SortControl } from "ui/editor/SortControl"; import InputLabel from "ui/public/InputLabel"; import { slugify } from "utils"; -import { client } from "../lib/graphql"; -import SimpleMenu from "../ui/editor/SimpleMenu"; +import FlowCard, { Card, CardContent } from "./FlowCard"; import { useStore } from "./FlowEditor/lib/store"; import { FlowSummary } from "./FlowEditor/lib/store/editor"; -import { formatLastEditMessage } from "./FlowEditor/utils"; const DashboardList = styled("ul")(({ theme }) => ({ - padding: theme.spacing(0, 0, 3), - borderBottom: "1px solid #fff", + padding: theme.spacing(3, 0), margin: 0, -})); - -const DashboardListItem = styled("li")(({ theme }) => ({ - listStyle: "none", - position: "relative", - color: theme.palette.common.white, - margin: theme.spacing(1, 0), - background: theme.palette.text.primary, - display: "flex", - justifyContent: "space-between", - alignItems: "stretch", - borderRadius: "2px", -})); - -const DashboardLink = styled(Link)(({ theme }) => ({ - display: "block", - fontSize: theme.typography.h4.fontSize, - textDecoration: "none", - color: "currentColor", - fontWeight: FONT_WEIGHT_SEMI_BOLD, - padding: theme.spacing(2), - margin: 0, - width: "100%", - "&:focus-within": { - ...borderedFocusStyle, + display: "grid", + gridAutoRows: "1fr", + gridTemplateColumns: "repeat(1, 1fr)", + gridGap: theme.spacing(2), + [theme.breakpoints.up("md")]: { + gridTemplateColumns: "repeat(2, 1fr)", + }, + [theme.breakpoints.up("lg")]: { + gridTemplateColumns: "repeat(3, 1fr)", }, })); -const StyledSimpleMenu = styled(SimpleMenu)(({ theme }) => ({ - display: "flex", - borderLeft: `1px solid ${theme.palette.border.main}`, -})); - -const LinkSubText = styled(Box)(({ theme }) => ({ - color: theme.palette.grey[400], - fontWeight: "normal", - paddingTop: "0.5em", -})); - -const Confirm = ({ - title, - content, - submitLabel, - open, - onClose, - onConfirm, -}: { - title: string; - content: string; - submitLabel: string; - open: boolean; - onClose: () => void; - onConfirm: () => void; -}) => ( - { - onClose(); - }} - > - {title} - - {content} - - - - Cancel - - - {submitLabel} - - - +const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => ( + + + + No services found + Get started by creating your first service + + + + ); -interface FlowItemProps { - flow: FlowSummary; - flows: FlowSummary[]; - teamId: number; - teamSlug: string; - refreshFlows: () => void; -} - -const FlowItem: React.FC = ({ - flow, +const AddFlowButton: React.FC<{ flows: FlowSummary[] | null }> = ({ flows, - teamId, - teamSlug, - refreshFlows, }) => { - const [deleting, setDeleting] = useState(false); - const handleDelete = () => { - useStore - .getState() - .deleteFlow(teamId, flow.slug) - .then(() => { - setDeleting(false); - refreshFlows(); - }); - }; - const handleCopy = () => { - useStore - .getState() - .copyFlow(flow.id) - .then(() => { - refreshFlows(); - }); - }; - const handleMove = (newTeam: string) => { - useStore - .getState() - .moveFlow(flow.id, newTeam) - .then(() => { - refreshFlows(); - }); - }; - - return ( - <> - {deleting && ( - { - setDeleting(false); - }} - onConfirm={handleDelete} - submitLabel="Delete Service" - /> - )} - - - - {flow.name} - - - {formatLastEditMessage( - flow.operations[0].createdAt, - flow.operations[0]?.actor, - )} - - - {useStore.getState().canUserEditTeam(teamSlug) && ( - { - const newName = prompt("New name", flow.name); - if (newName && newName !== flow.name) { - const newSlug = slugify(newName); - const duplicateFlowName = flows?.find( - (flow: any) => flow.slug === newSlug, - ); - if (!duplicateFlowName) { - await client.mutate({ - mutation: gql` - mutation UpdateFlowSlug( - $teamId: Int - $slug: String - $newSlug: String - $newName: String - ) { - update_flows( - where: { - team: { id: { _eq: $teamId } } - slug: { _eq: $slug } - } - _set: { slug: $newSlug, name: $newName } - ) { - affected_rows - } - } - `, - variables: { - teamId: teamId, - slug: flow.slug, - newSlug: newSlug, - newName: newName, - }, - }); - - refreshFlows(); - } else if (duplicateFlowName) { - alert( - `The flow "${newName}" already exists. Enter a unique flow name to continue`, - ); - } - } - }, - label: "Rename", - }, - { - label: "Copy", - onClick: () => { - handleCopy(); - }, - }, - { - label: "Move", - onClick: () => { - const newTeam = prompt("New team"); - if (newTeam) { - if (slugify(newTeam) === teamSlug) { - alert( - `This flow already belongs to ${teamSlug}, skipping move`, - ); - } else { - handleMove(slugify(newTeam)); - } - } - }, - }, - { - label: "Delete", - onClick: () => { - setDeleting(true); - }, - error: true, - }, - ]} - /> - )} - - > - ); -}; - -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(); @@ -419,27 +191,50 @@ const Team: React.FC = () => { const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); return ( - - + + - - Services - - {canUserEditTeam(slug) ? : } + + + Services + + {showAddFlowButton && } + + + + + Search + + + + theme.palette.border.input, + pr: 5, + }} + name="search" + id="search" + /> + + + + ({ flexDirection: "row", width: "100%", overflow: "hidden", - [`& > .${containerClasses.root}`]: { + [`& > .${containerClasses.root}, & > div > .${containerClasses.root}`]: { paddingTop: theme.spacing(3), paddingBottom: theme.spacing(3), [theme.breakpoints.up("lg")]: { diff --git a/editor.planx.uk/src/ui/editor/SimpleMenu.tsx b/editor.planx.uk/src/ui/editor/SimpleMenu.tsx index b0fd6e0c66..6459cc18af 100644 --- a/editor.planx.uk/src/ui/editor/SimpleMenu.tsx +++ b/editor.planx.uk/src/ui/editor/SimpleMenu.tsx @@ -2,10 +2,11 @@ import MoreVert from "@mui/icons-material/MoreVert"; import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import React, { useState } from "react"; +import React, { PropsWithChildren, useState } from "react"; interface Props { className?: string; + children?: React.ReactNode; items: Array<{ label: string; disabled?: boolean; @@ -14,7 +15,11 @@ interface Props { }>; } -export default function SimpleMenu({ items, ...restProps }: Props): FCReturn { +export default function SimpleMenu({ + items, + children, + ...restProps +}: PropsWithChildren): FCReturn { const [anchorEl, setAnchorEl] = useState(null); return ( @@ -25,8 +30,10 @@ export default function SimpleMenu({ items, ...restProps }: Props): FCReturn { }} aria-label="Options" size="large" + disableRipple > + {children}