From 40093f642a4370051753446967ad975b6cb16161 Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Wed, 8 Jan 2025 19:02:28 +0000 Subject: [PATCH 1/7] wip: Team page filters and cards --- editor.planx.uk/src/pages/Filters.tsx | 109 ++++++++++++++++++ .../src/pages/FlowEditor/lib/store/editor.ts | 16 +-- editor.planx.uk/src/pages/Team.tsx | 56 ++++++--- 3 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 editor.planx.uk/src/pages/Filters.tsx diff --git a/editor.planx.uk/src/pages/Filters.tsx b/editor.planx.uk/src/pages/Filters.tsx new file mode 100644 index 0000000000..0b88b11306 --- /dev/null +++ b/editor.planx.uk/src/pages/Filters.tsx @@ -0,0 +1,109 @@ +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import React from "react"; +import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem"; + +const FiltersContainer = styled(Box)(({ theme }) => ({ + width: "100%", + display: "flex", + flexDirection: "column", + background: "#eee", + margin: theme.spacing(1, 0, 3), +})); + +const FiltersHeader = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + padding: theme.spacing(2), + gap: theme.spacing(1), +})); + +const FiltersBody = styled(Box)(({ theme }) => ({})); + +const FiltersContent = styled(Box)(({ theme }) => ({ + borderTop: `1px solid ${theme.palette.border.main}`, + padding: theme.spacing(1.5, 2.5), + display: "flex", + flexDirection: "row", + flexWrap: "wrap", +})); + +const FiltersColumn = styled(Box)(({ theme }) => ({ + flexBasis: "20%", +})); + +const FiltersFooter = styled(Box)(({ theme }) => ({ + borderTop: `1px solid ${theme.palette.border.main}`, + padding: theme.spacing(1.5, 2), +})); + +const Filters: React.FC = () => { + return ( + + + + Hide filters + + + + + Online status + {}} + label={"Online"} + checked={false} + variant="compact" + /> + {}} + label={"Offline"} + checked={false} + variant="compact" + /> + + + Application type + {}} + label={"Submission"} + checked={false} + variant="compact" + /> + {}} + label={"Guidance"} + checked={false} + variant="compact" + /> + + + Service type + {}} + label={"Statutory"} + checked={false} + variant="compact" + /> + {}} + label={"Discretionary"} + checked={false} + variant="compact" + /> + + + + + + + + ); +}; + +export default Filters; 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 c55a0eae06..145707a758 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -2,10 +2,10 @@ import { gql } from "@apollo/client"; import { getPathForNode, sortFlow } from "@opensystemslab/planx-core"; import { ComponentType as TYPES, + flatFlags, FlowGraph, NodeId, OrderedFlow, - flatFlags, } from "@opensystemslab/planx-core/types"; import { add, @@ -137,6 +137,7 @@ export interface FlowSummary { name: string; slug: string; updatedAt: string; + status: string; operations: { createdAt: string; actor: { @@ -189,7 +190,7 @@ export interface EditorStore extends Store.Store { href: string; }) => void; getURLForNode: (nodeId: string) => string; - getFlowSchema: () => { nodes?: string[], options?: string[] } | undefined; + getFlowSchema: () => { nodes?: string[]; options?: string[] } | undefined; } export const editorStore: StateCreator< @@ -382,6 +383,7 @@ export const editorStore: StateCreator< id name slug + status updatedAt: updated_at operations(limit: 1, order_by: { created_at: desc }) { createdAt: created_at @@ -614,14 +616,14 @@ export const editorStore: StateCreator< Object.entries(flow).map(([_id, node]) => { if (node.data?.fn) { // Exclude Filter fn value as not exposed to editors - if (node.data?.fn !== "flag") nodes.add(node.data.fn) - }; - + if (node.data?.fn !== "flag") nodes.add(node.data.fn); + } + if (node.data?.val) { // Exclude Filter Option flag values as not exposed to editors const flagVals = flatFlags.map((flag) => flag.value); - if (!flagVals.includes(node.data.val)) options.add(node.data.val) - }; + if (!flagVals.includes(node.data.val)) options.add(node.data.val); + } }); return { diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index 37c10d6d37..5342a1efd4 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -3,6 +3,7 @@ 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 Chip from "@mui/material/Chip"; import Container from "@mui/material/Container"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; @@ -11,16 +12,20 @@ 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 Input from "ui/shared/Input/Input"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; +import InputRowLabel from "ui/shared/InputRowLabel"; import { slugify } from "utils"; import { client } from "../lib/graphql"; import SimpleMenu from "../ui/editor/SimpleMenu"; +import Filters from "./Filters"; import { useStore } from "./FlowEditor/lib/store"; import { FlowSummary } from "./FlowEditor/lib/store/editor"; import { formatLastEditMessage } from "./FlowEditor/utils"; @@ -29,18 +34,21 @@ const DashboardList = styled("ul")(({ theme }) => ({ padding: theme.spacing(0, 0, 3), borderBottom: "1px solid #fff", margin: 0, + display: "grid", + gridAutoRows: "1fr", + gridTemplateColumns: "1fr 1fr 1fr", + gridGap: theme.spacing(2), })); 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", + flexDirection: "column", + borderRadius: "4px", + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.border.main}`, + boxShadow: "0 2px 4px 0 rgba(0, 0, 0, 0.1)", })); const DashboardLink = styled(Link)(({ theme }) => ({ @@ -59,11 +67,11 @@ const DashboardLink = styled(Link)(({ theme }) => ({ const StyledSimpleMenu = styled(SimpleMenu)(({ theme }) => ({ display: "flex", - borderLeft: `1px solid ${theme.palette.border.main}`, + marginTop: "auto", })); const LinkSubText = styled(Box)(({ theme }) => ({ - color: theme.palette.grey[400], + color: theme.palette.text.secondary, fontWeight: "normal", paddingTop: "0.5em", })); @@ -111,7 +119,6 @@ interface FlowItemProps { teamSlug: string; refreshFlows: () => void; } - const FlowItem: React.FC = ({ flow, flows, @@ -146,6 +153,10 @@ const FlowItem: React.FC = ({ }); }; + const getStatusColor = (status: string) => { + return status === "online" ? "success" : "secondary"; + }; + return ( <> {deleting && ( @@ -162,7 +173,7 @@ const FlowItem: React.FC = ({ )} - + {flow.name} @@ -171,6 +182,11 @@ const FlowItem: React.FC = ({ flow.operations[0]?.actor, )} + {useStore.getState().canUserEditTeam(teamSlug) && ( { const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); return ( - + { display: "flex", flexDirection: "row", alignItems: "center", + gap: 2, }} > Services - {canUserEditTeam(slug) ? : } + {/* {canUserEditTeam(slug) ? : } */} + {showAddFlowButton && } + + + + + Search + + + + + - {showAddFlowButton && } + {teamHasFlows && ( {flows.map((flow) => ( From 2a457432761316e4747bb02fdc638479df0b1fcc Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Thu, 9 Jan 2025 10:55:51 +0000 Subject: [PATCH 2/7] wip: Flow card styling --- editor.planx.uk/src/pages/Team.tsx | 212 +++++++++++------- .../src/pages/layout/AuthenticatedLayout.tsx | 2 +- editor.planx.uk/src/ui/editor/FlowTag.tsx | 69 ++++++ 3 files changed, 200 insertions(+), 83 deletions(-) create mode 100644 editor.planx.uk/src/ui/editor/FlowTag.tsx diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index 5342a1efd4..c20a8cc8a5 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -1,9 +1,6 @@ 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 Chip from "@mui/material/Chip"; import Container from "@mui/material/Container"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; @@ -14,9 +11,9 @@ import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; 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 FlowTag, { FlowTagType, StatusVariant } from "ui/editor/FlowTag"; import Input from "ui/shared/Input/Input"; import InputRow from "ui/shared/InputRow"; import InputRowItem from "ui/shared/InputRowItem"; @@ -31,7 +28,7 @@ import { FlowSummary } from "./FlowEditor/lib/store/editor"; import { formatLastEditMessage } from "./FlowEditor/utils"; const DashboardList = styled("ul")(({ theme }) => ({ - padding: theme.spacing(0, 0, 3), + padding: theme.spacing(3, 0), borderBottom: "1px solid #fff", margin: 0, display: "grid", @@ -40,40 +37,56 @@ const DashboardList = styled("ul")(({ theme }) => ({ gridGap: theme.spacing(2), })); -const DashboardListItem = styled("li")(({ theme }) => ({ +const FlowCard = styled("li")(({ theme }) => ({ listStyle: "none", position: "relative", display: "flex", flexDirection: "column", + justifyContent: "stretch", borderRadius: "4px", - backgroundColor: theme.palette.background.paper, + backgroundColor: theme.palette.background.default, border: `1px solid ${theme.palette.border.main}`, boxShadow: "0 2px 4px 0 rgba(0, 0, 0, 0.1)", + overflow: "hidden", })); -const DashboardLink = styled(Link)(({ theme }) => ({ - display: "block", - fontSize: theme.typography.h4.fontSize, +const FlowCardContent = styled(Box)(({ theme }) => ({ + position: "relative", + height: "100%", textDecoration: "none", color: "currentColor", - fontWeight: FONT_WEIGHT_SEMI_BOLD, padding: theme.spacing(2), + display: "flex", + flexDirection: "column", + gap: theme.spacing(1.5), margin: 0, width: "100%", +})); + +const DashboardLink = styled(Link)(({ theme }) => ({ + position: "absolute", + left: 0, + top: 0, + width: "100%", + height: "100%", + zIndex: 1, "&:focus-within": { ...borderedFocusStyle, }, })); -const StyledSimpleMenu = styled(SimpleMenu)(({ theme }) => ({ - display: "flex", - marginTop: "auto", -})); - const LinkSubText = styled(Box)(({ theme }) => ({ color: theme.palette.text.secondary, fontWeight: "normal", - paddingTop: "0.5em", + 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", })); const Confirm = ({ @@ -119,6 +132,7 @@ interface FlowItemProps { teamSlug: string; refreshFlows: () => void; } + const FlowItem: React.FC = ({ flow, flows, @@ -127,6 +141,7 @@ const FlowItem: React.FC = ({ refreshFlows, }) => { const [deleting, setDeleting] = useState(false); + const handleDelete = () => { useStore .getState() @@ -136,6 +151,7 @@ const FlowItem: React.FC = ({ refreshFlows(); }); }; + const handleCopy = () => { useStore .getState() @@ -144,6 +160,7 @@ const FlowItem: React.FC = ({ refreshFlows(); }); }; + const handleMove = (newTeam: string) => { useStore .getState() @@ -153,9 +170,8 @@ const FlowItem: React.FC = ({ }); }; - const getStatusColor = (status: string) => { - return status === "online" ? "success" : "secondary"; - }; + const statusVariant = + flow.status === "online" ? StatusVariant.Online : StatusVariant.Offline; return ( <> @@ -171,23 +187,33 @@ const FlowItem: React.FC = ({ submitLabel="Delete Service" /> )} - - - - {flow.name} + + + + + {flow.name} + + + {formatLastEditMessage( + flow.operations[0].createdAt, + flow.operations[0]?.actor, + )} + + + + + {flow.status} + + + + a": { position: "relative", zIndex: 2 } }} + > + Allows users to apply for prior approval that is required for some + types of... read more - - {formatLastEditMessage( - flow.operations[0].createdAt, - flow.operations[0]?.actor, - )} - - - + {useStore.getState().canUserEditTeam(teamSlug) && ( = ({ ]} /> )} - + ); }; @@ -343,60 +369,82 @@ const Team: React.FC = () => { const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); return ( - - + + - - Services - - {/* {canUserEditTeam(slug) ? : } */} - {showAddFlowButton && } - - - - - Search - - - - - + + + Services + + {/* {canUserEditTeam(slug) ? : } */} + {showAddFlowButton && } + + + + + Search + + + + + + - - - {teamHasFlows && ( - - {flows.map((flow) => ( - { - fetchFlows(); + + {teamHasFlows && ( + <> + - ))} - - )} - {flows && !flows.length && } - + > + + Showing all services + + Order toggle + + + + {flows.map((flow) => ( + { + fetchFlows(); + }} + /> + ))} + + + )} + {flows && !flows.length && } + + ); }; diff --git a/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx b/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx index 97b070d191..321bb225c1 100644 --- a/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx +++ b/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx @@ -24,7 +24,7 @@ const DashboardContainer = styled(Box)(({ theme }) => ({ 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/FlowTag.tsx b/editor.planx.uk/src/ui/editor/FlowTag.tsx new file mode 100644 index 0000000000..de1fa7ec68 --- /dev/null +++ b/editor.planx.uk/src/ui/editor/FlowTag.tsx @@ -0,0 +1,69 @@ +import Box from "@mui/material/Box"; +import { styled } from "@mui/material/styles"; +import React from "react"; +import { FONT_WEIGHT_SEMI_BOLD } from "theme"; + +export enum FlowTagType { + Status = "status", + ApplicationType = "applicationType", + ServiceType = "serviceType", +} + +export enum StatusVariant { + Online = "online", + Offline = "offline", +} + +const BG_ONLINE = "#D6FFD7"; +const BG_OFFLINE = "#EAEAEA"; +const BG_APPLICATION_TYPE = "#D6EFFF"; +const BG_SERVICE_TYPE = "#FFEABE"; + +const Root = styled(Box, { + shouldForwardProp: (prop) => prop !== "tagType" && prop !== "statusVariant", +})(({ theme, tagType, statusVariant }) => ({ + fontSize: theme.typography.body2.fontSize, + fontWeight: FONT_WEIGHT_SEMI_BOLD, + padding: "2px 6px", + display: "flex", + alignItems: "center", + gap: theme.spacing(0.5), + borderRadius: "4px", + textTransform: "capitalize", + border: "1px solid rgba(0, 0, 0, 0.2)", + ...(tagType === FlowTagType.Status && { + backgroundColor: + statusVariant === StatusVariant.Online ? BG_ONLINE : BG_OFFLINE, + "&::before": { + content: '""', + width: "8px", + height: "8px", + borderRadius: "50%", + background: + statusVariant === StatusVariant.Online + ? theme.palette.success.main + : "#a1a1a1", + }, + }), + ...(tagType === FlowTagType.ApplicationType && { + backgroundColor: BG_APPLICATION_TYPE, + }), + ...(tagType === FlowTagType.ServiceType && { + backgroundColor: BG_SERVICE_TYPE, + }), +})); + +export interface Props { + id?: string; + tagType: FlowTagType; + statusVariant?: StatusVariant; + children?: React.ReactNode; +} + +const FlowTag: React.FC = ({ id, tagType, statusVariant, children }) => ( + + {children} + +); + +export default FlowTag; From a559afa7daf0133973d94c2fd517fa8b56404106 Mon Sep 17 00:00:00 2001 From: Rory Doak Date: Mon, 13 Jan 2025 15:50:57 +0000 Subject: [PATCH 3/7] add in initial logic for filtering flows remove unused imports move from useEffect to onClick --- editor.planx.uk/src/pages/Filters.tsx | 132 +++++++++++++++++++++++--- editor.planx.uk/src/pages/Team.tsx | 16 +++- 2 files changed, 128 insertions(+), 20 deletions(-) diff --git a/editor.planx.uk/src/pages/Filters.tsx b/editor.planx.uk/src/pages/Filters.tsx index 0b88b11306..73787e5cfc 100644 --- a/editor.planx.uk/src/pages/Filters.tsx +++ b/editor.planx.uk/src/pages/Filters.tsx @@ -3,9 +3,12 @@ import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useNavigation } from "react-navi"; import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem"; +import { FlowSummary } from "./FlowEditor/lib/store/editor"; + const FiltersContainer = styled(Box)(({ theme }) => ({ width: "100%", display: "flex", @@ -41,7 +44,106 @@ const FiltersFooter = styled(Box)(({ theme }) => ({ padding: theme.spacing(1.5, 2), })); -const Filters: React.FC = () => { +interface FiltersProps { + flows: FlowSummary[]; + setFilteredFlows: React.Dispatch>; +} + +interface FilterState { + status?: "online" | "offline"; + applicationType?: "submission" | "guidance"; + serviceType?: "statutory" | "discretionary"; +} + +type FilterKeys = keyof FilterState; +type FilterValues = FilterState[keyof FilterState]; + +export const Filters: React.FC = ({ + flows, + setFilteredFlows, +}) => { + { + /* + - thinking here is to build in type safety into the filter categories + - create a fn to setFlows based on the filters selected + - could we build a component which has the Column, Typo and ChecklistItems + ~ This way we could build a type safe filterOptions array which we can iterate over to create + the filter options. Could make it scale nicely + - I've also added a new flows list called filteredFlows so we can maintain an original + */ + } + + const [filters, setFilters] = useState(); + + const navigation = useNavigation(); + + const addToSearchParams = (params: FilterState) => { + const newSearchParams = new URLSearchParams(); + filters && + Object.entries(params).forEach(([key, value]) => { + if (value) { + newSearchParams.set(key, value); + } else { + newSearchParams.delete(key); + } + }); + + navigation.navigate( + { + pathname: window.location.pathname, + search: newSearchParams.toString() + ? `?${newSearchParams.toString()}` + : "", + }, + { + replace: true, + }, + ); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + let filterObj = {}; + params.forEach((value, key) => { + switch (key) { + case "status": + filterObj = { ...filterObj, status: value }; + break; + case "applicationType": + filterObj = { ...filterObj, applicationType: value }; + break; + case "serviceType": + filterObj = { ...filterObj, serviceType: value }; + break; + } + setFilters(filterObj); + }); + }, []); + + const handleFiltering = () => { + const filterByStatus = flows.filter((flow: FlowSummary) => { + if (filters?.status) { + return flow.status === filters.status; + } else { + return true; + } + }); + filterByStatus && setFilteredFlows(filterByStatus); + filters && addToSearchParams(filters); + if ( + !filters?.status && + !filters?.applicationType && + !filters?.serviceType + ) { + setFilteredFlows(flows); + } + }; + + const handleChange = (filterKey: FilterKeys, filterValue: FilterValues) => + filters?.[filterKey] === filterValue + ? setFilters({ ...filters, [filterKey]: undefined }) + : setFilters({ ...filters, [filterKey]: filterValue }); + return ( @@ -53,51 +155,51 @@ const Filters: React.FC = () => { Online status {}} + onChange={() => handleChange("status", "online")} label={"Online"} - checked={false} + checked={filters?.status === "online"} variant="compact" /> {}} + onChange={() => handleChange("status", "offline")} label={"Offline"} - checked={false} + checked={filters?.status === "offline"} variant="compact" /> Application type {}} + onChange={() => handleChange("applicationType", "submission")} label={"Submission"} - checked={false} + checked={filters?.applicationType === "submission"} variant="compact" /> {}} + onChange={() => handleChange("applicationType", "guidance")} label={"Guidance"} - checked={false} + checked={filters?.applicationType === "guidance"} variant="compact" /> Service type {}} + onChange={() => handleChange("serviceType", "statutory")} label={"Statutory"} - checked={false} + checked={filters?.serviceType === "statutory"} variant="compact" /> {}} + onChange={() => handleChange("serviceType", "discretionary")} label={"Discretionary"} - checked={false} + checked={filters?.serviceType === "discretionary"} variant="compact" /> - diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index c20a8cc8a5..bba5bbbff9 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -318,7 +318,9 @@ const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => ( ); -const AddFlowButton: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => { +const AddFlowButton: React.FC<{ flows: FlowSummary[] | null }> = ({ + flows, +}) => { const { navigate } = useNavigation(); const { teamId, createFlow, teamSlug } = useStore(); @@ -348,6 +350,9 @@ const Team: React.FC = () => { (state) => [state.getTeam(), state.canUserEditTeam, state.getFlows], ); const [flows, setFlows] = useState(null); + const [filteredFlows, setFilteredFlows] = useState( + null, + ); const fetchFlows = useCallback(() => { getFlows(teamId).then((flows) => { @@ -358,6 +363,7 @@ const Team: React.FC = () => { ), ); setFlows(sortedFlows); + setFilteredFlows(sortedFlows); }); }, [teamId, setFlows, getFlows]); @@ -365,7 +371,7 @@ const Team: React.FC = () => { fetchFlows(); }, [fetchFlows]); - const teamHasFlows = flows && Boolean(flows.length); + const teamHasFlows = filteredFlows && Boolean(filteredFlows.length); const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); return ( @@ -409,7 +415,7 @@ const Team: React.FC = () => { - + {flows && } {teamHasFlows && ( <> { - {flows.map((flow) => ( + {filteredFlows.map((flow) => ( Date: Thu, 9 Jan 2025 17:07:18 +0000 Subject: [PATCH 4/7] wip: Load in description --- .../src/pages/FlowEditor/lib/store/editor.ts | 2 ++ editor.planx.uk/src/pages/Team.tsx | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) 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 145707a758..37c54953c8 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -138,6 +138,7 @@ export interface FlowSummary { slug: string; updatedAt: string; status: string; + description: string; operations: { createdAt: string; actor: { @@ -384,6 +385,7 @@ export const editorStore: StateCreator< name slug status + description updatedAt: updated_at operations(limit: 1, order_by: { created_at: desc }) { createdAt: created_at diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index bba5bbbff9..bad86e9c78 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -11,7 +11,7 @@ import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; import React, { useCallback, useEffect, useState } from "react"; import { Link, useNavigation } from "react-navi"; -import { borderedFocusStyle } from "theme"; +import { inputFocusStyle } from "theme"; import { AddButton } from "ui/editor/AddButton"; import FlowTag, { FlowTagType, StatusVariant } from "ui/editor/FlowTag"; import Input from "ui/shared/Input/Input"; @@ -47,7 +47,6 @@ const FlowCard = styled("li")(({ theme }) => ({ backgroundColor: theme.palette.background.default, border: `1px solid ${theme.palette.border.main}`, boxShadow: "0 2px 4px 0 rgba(0, 0, 0, 0.1)", - overflow: "hidden", })); const FlowCardContent = styled(Box)(({ theme }) => ({ @@ -70,8 +69,8 @@ const DashboardLink = styled(Link)(({ theme }) => ({ width: "100%", height: "100%", zIndex: 1, - "&:focus-within": { - ...borderedFocusStyle, + "&:focus": { + ...inputFocusStyle, }, })); @@ -87,6 +86,7 @@ const StyledSimpleMenu = styled(SimpleMenu)(({ theme }) => ({ borderTop: `1px solid ${theme.palette.border.main}`, backgroundColor: theme.palette.background.paper, overflow: "hidden", + borderRadius: "0px 0px 4px 4px", })); const Confirm = ({ @@ -206,13 +206,22 @@ const FlowItem: React.FC = ({ - a": { position: "relative", zIndex: 2 } }} - > - Allows users to apply for prior approval that is required for some - types of... read more - + {flow.description && ( + a": { position: "relative", zIndex: 2 } }} + > + {`${flow.description.split(" ").slice(0, 10).join(" ")}... `} + + + )} {useStore.getState().canUserEditTeam(teamSlug) && ( Date: Tue, 14 Jan 2025 13:01:30 +0000 Subject: [PATCH 5/7] add accordion functionality --- editor.planx.uk/src/pages/Filters.tsx | 78 +++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/editor.planx.uk/src/pages/Filters.tsx b/editor.planx.uk/src/pages/Filters.tsx index 73787e5cfc..3936c298a0 100644 --- a/editor.planx.uk/src/pages/Filters.tsx +++ b/editor.planx.uk/src/pages/Filters.tsx @@ -1,4 +1,10 @@ +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import Accordion, { accordionClasses } from "@mui/material/Accordion"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import AccordionSummary, { + accordionSummaryClasses, +} from "@mui/material/AccordionSummary"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import { styled } from "@mui/material/styles"; @@ -9,27 +15,61 @@ import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem"; import { FlowSummary } from "./FlowEditor/lib/store/editor"; -const FiltersContainer = styled(Box)(({ theme }) => ({ +const FiltersContainer = styled(Accordion)(({ theme }) => ({ width: "100%", display: "flex", flexDirection: "column", - background: "#eee", margin: theme.spacing(1, 0, 3), + [`&.${accordionClasses.root}.Mui-expanded`]: { + margin: theme.spacing(1, 0, 3), + }, + [`& .${accordionSummaryClasses.root} > div`]: { + margin: "0", + }, })); -const FiltersHeader = styled(Box)(({ theme }) => ({ +const FiltersHeader = styled(AccordionSummary)(({ theme }) => ({ display: "flex", alignItems: "center", - justifyContent: "flex-start", + justifyContent: "space-between", padding: theme.spacing(2), - gap: theme.spacing(1), + gap: theme.spacing(3), + background: "#eee", + "&:hover": { + background: "#eee", + }, + "& .MuiAccordionSummary-expandIconWrapper": { + display: "none", + }, })); -const FiltersBody = styled(Box)(({ theme }) => ({})); +const FiltersToggle = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: theme.spacing(0.5), + minWidth: "160px", +})); + +const FilterChip = styled(Button)(({ theme }) => ({ + background: theme.palette.background.default, + color: theme.palette.text.primary, + padding: theme.spacing(0.5, 1), + boxShadow: "none", + "&::before": { + content: '"✕"', + fontSize: "0.8em", + paddingRight: theme.spacing(0.75), + }, +})); + +const FiltersBody = styled(AccordionDetails)(({ theme }) => ({ + background: "#eee", + padding: 0, +})); const FiltersContent = styled(Box)(({ theme }) => ({ borderTop: `1px solid ${theme.palette.border.main}`, - padding: theme.spacing(1.5, 2.5), + padding: theme.spacing(2.5), display: "flex", flexDirection: "row", flexWrap: "wrap", @@ -74,6 +114,7 @@ export const Filters: React.FC = ({ } const [filters, setFilters] = useState(); + const [expanded, setExpanded] = useState(false); const navigation = useNavigation(); @@ -145,10 +186,25 @@ export const Filters: React.FC = ({ : setFilters({ ...filters, [filterKey]: filterValue }); return ( - - - - Hide filters + setExpanded(!expanded)} + > + : + } + > + + {expanded ? : } + + {expanded ? "Hide filters" : "Show filters"} + + + {/* Example chips to show active filters */} + + e.stopPropagation()}>Online + From e159b84bd303ff57df3eeaad7103c27b15856f3e Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Wed, 15 Jan 2025 15:36:45 +0000 Subject: [PATCH 6/7] query number of filtered --- editor.planx.uk/src/pages/Team.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index bad86e9c78..843a4bf4e0 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -383,6 +383,13 @@ const Team: React.FC = () => { const teamHasFlows = filteredFlows && Boolean(filteredFlows.length); const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); + const numberOfServices = filteredFlows?.length || 0; + const pluralisedService = numberOfServices === 1 ? "service" : "services"; + const listTitle = + filteredFlows === flows + ? "Showing all services" + : `Showing ${numberOfServices} ${pluralisedService}`; + return ( @@ -436,7 +443,7 @@ const Team: React.FC = () => { }} > - Showing all services + {listTitle} Order toggle From 2af6e413269d9cfd2525b45450dac506e84c9dae Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Wed, 15 Jan 2025 16:38:16 +0000 Subject: [PATCH 7/7] add chips based on filters --- editor.planx.uk/src/pages/Filters.tsx | 68 ++++++++++++++++++--------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/editor.planx.uk/src/pages/Filters.tsx b/editor.planx.uk/src/pages/Filters.tsx index 3936c298a0..ca42af9fa9 100644 --- a/editor.planx.uk/src/pages/Filters.tsx +++ b/editor.planx.uk/src/pages/Filters.tsx @@ -7,6 +7,7 @@ import AccordionSummary, { } from "@mui/material/AccordionSummary"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; import React, { useEffect, useState } from "react"; @@ -50,18 +51,6 @@ const FiltersToggle = styled(Box)(({ theme }) => ({ minWidth: "160px", })); -const FilterChip = styled(Button)(({ theme }) => ({ - background: theme.palette.background.default, - color: theme.palette.text.primary, - padding: theme.spacing(0.5, 1), - boxShadow: "none", - "&::before": { - content: '"✕"', - fontSize: "0.8em", - paddingRight: theme.spacing(0.75), - }, -})); - const FiltersBody = styled(AccordionDetails)(({ theme }) => ({ background: "#eee", padding: 0, @@ -114,6 +103,7 @@ export const Filters: React.FC = ({ } const [filters, setFilters] = useState(); + const [selectedFilters, setSelectedFilters] = useState(); const [expanded, setExpanded] = useState(false); const navigation = useNavigation(); @@ -158,23 +148,25 @@ export const Filters: React.FC = ({ break; } setFilters(filterObj); + setSelectedFilters(Object.values(filterObj)); }); }, []); - const handleFiltering = () => { + const handleFiltering = (filtersArg) => { const filterByStatus = flows.filter((flow: FlowSummary) => { - if (filters?.status) { - return flow.status === filters.status; + if (filtersArg?.status) { + return flow.status === filtersArg.status; } else { return true; } }); filterByStatus && setFilteredFlows(filterByStatus); - filters && addToSearchParams(filters); + filtersArg && addToSearchParams(filtersArg); + filtersArg && setSelectedFilters(Object.values(filtersArg)); if ( - !filters?.status && - !filters?.applicationType && - !filters?.serviceType + !filtersArg?.status && + !filtersArg?.applicationType && + !filtersArg?.serviceType ) { setFilteredFlows(flows); } @@ -185,6 +177,8 @@ export const Filters: React.FC = ({ ? setFilters({ ...filters, [filterKey]: undefined }) : setFilters({ ...filters, [filterKey]: filterValue }); + filters && console.log(Object.values(filters)); + return ( = ({ {expanded ? "Hide filters" : "Show filters"} - {/* Example chips to show active filters */} - e.stopPropagation()}>Online + {selectedFilters && + selectedFilters.map( + (filter) => + filter && ( + e.stopPropagation()} + label={filter} + onDelete={() => { + const deleteFilter = + filters && + Object.keys(filters).find((key) => { + return filters[key] === filter; + }); + console.log(deleteFilter); + deleteFilter && + setFilters({ ...filters, [deleteFilter]: undefined }); + deleteFilter && + handleFiltering({ + ...filters, + [deleteFilter]: undefined, + }); + }} + /> + ), + )} @@ -255,7 +273,13 @@ export const Filters: React.FC = ({ -