Skip to content

Commit

Permalink
feat: "No services found" card (#3891)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Nov 5, 2024
1 parent 042df8d commit 9008373
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
30 changes: 24 additions & 6 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
copyNode: (id: NodeId) => void;
createFlow: (teamId: any, newSlug: any, newName: string) => Promise<string>;
createFlow: (
teamId: number,
newSlug: string,
newName: string
) => Promise<string>;
deleteFlow: (teamId: number, flowSlug: string) => Promise<object>;
validateAndDiffFlow: (flowId: string) => Promise<any>;
getFlows: (teamId: number) => Promise<any>;
getFlows: (teamId: number) => Promise<FlowSummary[]>;
isClone: (id: NodeId) => boolean;
lastPublished: (flowId: string) => Promise<string>;
lastPublisher: (flowId: string) => Promise<string>;
Expand All @@ -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<PublishFlowResponse>;
removeNode: (id: NodeId, parent: NodeId) => void;
updateNode: (node: any, relationships?: any) => void;
Expand Down Expand Up @@ -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 } } }) {
Expand All @@ -349,7 +367,7 @@ export const editorStore: StateCreator<
},
});

return data;
return flows;
},

isClone: (id) => {
Expand Down
149 changes: 76 additions & 73 deletions editor.planx.uk/src/pages/Team.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 }) => ({
Expand Down Expand Up @@ -103,32 +104,9 @@ const Confirm = ({
</Dialog>
);

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 (
<AddButtonRoot onClick={onClick}>
<AddCircleOutlineIcon sx={{ mr: 1 }} /> {children}
</AddButtonRoot>
);
}

interface FlowItemProps {
flow: any;
flows: any;
flow: FlowSummary;
flows: FlowSummary[];
teamId: number;
teamSlug: string;
refreshFlows: () => void;
Expand Down Expand Up @@ -279,30 +257,77 @@ const FlowItem: React.FC<FlowItemProps> = ({
);
};

const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => (
<Box sx={(theme) => ({
mt: 4,
backgroundColor: theme.palette.background.paper,
borderRadius: "8px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
padding: 2
})}>
<Typography variant="h3">No services found</Typography>
<Typography>Get started by creating your first service</Typography>
<AddFlowButton flows={flows}/>
</Box>
)

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(
<AddButton onClick={addFlow}>
Add a new service
</AddButton>
)
}

const Team: React.FC = () => {
const { id: teamId, slug } = useStore((state) => state.getTeam());
const [flows, setFlows] = useState<any[] | null>(null);
const navigation = useNavigation();
const [{ id: teamId, slug }, canUserEditTeam, getFlows] = useStore((state) => [state.getTeam(), state.canUserEditTeam, state.getFlows ]);
const [flows, setFlows] = useState<FlowSummary[] | null>(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 (
<Container maxWidth="formWrap">
<Box
Expand All @@ -324,42 +349,19 @@ const Team: React.FC = () => {
<Typography variant="h2" component="h1" pr={1}>
Services
</Typography>
{useStore.getState().canUserEditTeam(slug) ? (
{canUserEditTeam(slug) ? (
<Edit />
) : (
<Visibility />
)}
</Box>
{useStore.getState().canUserEditTeam(slug) && (
<AddButton
onClick={() => {
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
</AddButton>
{showAddFlowButton && (
<AddFlowButton flows={flows}/>
)}
</Box>
{flows && (
{teamHasFlows && (
<DashboardList>
{flows.map((flow: any) => (
{flows.map((flow) => (
<FlowItem
flow={flow}
flows={flows}
Expand All @@ -371,8 +373,9 @@ const Team: React.FC = () => {
}}
/>
))}
</DashboardList>
)}
</DashboardList>)
}
{ flows && !flows.length && <GetStarted flows={flows}/> }
</Container>
);
};
Expand Down
3 changes: 2 additions & 1 deletion editor.planx.uk/src/pages/Teams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions editor.planx.uk/src/ui/editor/AddButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AddButtonRoot onClick={onClick}>
<AddCircleOutlineIcon sx={{ mr: 1 }} /> {children}
</AddButtonRoot>
);
}

0 comments on commit 9008373

Please sign in to comment.