diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts index a3b30b5a0d..7994270611 100644 --- a/editor.planx.uk/src/lib/featureFlags.ts +++ b/editor.planx.uk/src/lib/featureFlags.ts @@ -1,5 +1,9 @@ // add/edit/remove feature flags in array below -const AVAILABLE_FEATURE_FLAGS = ["FEE_BREAKDOWN", "EXCLUSIVE_OR"] as const; +const AVAILABLE_FEATURE_FLAGS = [ + "FEE_BREAKDOWN", + "EXCLUSIVE_OR", + "SORT_FLOWS", +] as const; type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number]; 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..16e47c4456 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, @@ -132,18 +132,27 @@ interface PublishFlowResponse { message: string; } +export type PublishedFlowSummary = { + publishedAt: string; + hasSendComponent: boolean; +}; + +export type FlowSummaryOperations = { + createdAt: string; + actor: { + firstName: string; + lastName: string; + }; +}; + export interface FlowSummary { id: string; name: string; slug: string; + status: "online" | "offline"; updatedAt: string; - operations: { - createdAt: string; - actor: { - firstName: string; - lastName: string; - }; - }[]; + operations: FlowSummaryOperations[]; + publishedFlows: PublishedFlowSummary[]; } export interface EditorStore extends Store.Store { @@ -189,7 +198,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 +391,7 @@ export const editorStore: StateCreator< id name slug + status updatedAt: updated_at operations(limit: 1, order_by: { created_at: desc }) { createdAt: created_at @@ -390,6 +400,13 @@ export const editorStore: StateCreator< lastName: last_name } } + publishedFlows: published_flows( + order_by: { created_at: desc } + limit: 1 + ) { + publishedAt: created_at + hasSendComponent: has_send_component + } } } `, @@ -614,14 +631,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..2a0f7c122c 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -11,12 +11,13 @@ 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 { hasFeatureFlag } from "lib/featureFlags"; 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 { SortFlowsSelect } from "ui/editor/SortFlowsSelect"; import { slugify } from "utils"; import { client } from "../lib/graphql"; @@ -351,6 +352,9 @@ const Team: React.FC = () => { {showAddFlowButton && } + {hasFeatureFlag("SORT_FLOWS") && flows && ( + + )} {teamHasFlows && ( {flows.map((flow) => ( diff --git a/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx b/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx new file mode 100644 index 0000000000..e8c7bda65b --- /dev/null +++ b/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx @@ -0,0 +1,152 @@ +import TrendingDownIcon from "@mui/icons-material/TrendingDown"; +import TrendingUpIcon from "@mui/icons-material/TrendingUp"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import MenuItem from "@mui/material/MenuItem"; +import { + FlowSummary, + FlowSummaryOperations, + PublishedFlowSummary, +} from "pages/FlowEditor/lib/store/editor"; +import React, { useEffect, useState } from "react"; +import { useNavigation } from "react-navi"; + +import SelectInput from "./SelectInput/SelectInput"; + +type SortDirection = "asc" | "desc"; +type SortKeys = keyof FlowSummary; +type SortNestedKeys = keyof PublishedFlowSummary | keyof FlowSummaryOperations; +type SortTypes = SortKeys | SortNestedKeys; + +interface BasicSort { + displayName: string; + sortKey: Exclude; +} + +interface PublishedFlowSort { + displayName: string; + sortKey: "publishedFlows"; + nestedKey: keyof PublishedFlowSummary; +} + +type SortObject = PublishedFlowSort | BasicSort; + +const sortArray: SortObject[] = [ + { displayName: "Name", sortKey: "name" }, + { displayName: "Last updated", sortKey: "updatedAt" }, + { displayName: "Status", sortKey: "status" }, + { + displayName: "Last published", + sortKey: "publishedFlows", + nestedKey: "publishedAt", + }, +]; + +const sortFlowList = ( + a: string | boolean, + b: string | boolean, + sortDirection: SortDirection, +) => { + if (a < b) { + return sortDirection === "asc" ? 1 : -1; + } + if (a > b) { + return sortDirection === "asc" ? -1 : 1; + } + return 0; +}; + +export const SortFlowsSelect = ({ + flows, + setFlows, +}: { + flows: FlowSummary[]; + setFlows: React.Dispatch>; +}) => { + const [sortBy, setSortBy] = useState(sortArray[0]); + const [sortDirection, setSortDirection] = useState("asc"); + + const navigation = useNavigation(); + + const addToSearchParams = (sortKey: SortTypes) => { + navigation.navigate( + { + pathname: window.location.pathname, + search: `?sort=${sortKey}`, + }, + { + replace: true, + }, + ); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const urlSort = params.get("sort") as SortTypes; + const newSortObj = sortArray.find( + (sort) => + sort.sortKey === urlSort || + (sort.sortKey === "publishedFlows" && sort.nestedKey === urlSort), + ); + newSortObj && setSortBy(newSortObj); + }, []); + + useEffect(() => { + const { sortKey } = sortBy; + + if (sortKey === "publishedFlows") { + const sortedFlows = flows?.sort((a: FlowSummary, b: FlowSummary) => { + const { nestedKey } = sortBy; + + // auto sort unpublished flows to bottom + if (!a[sortKey][0]) return 1; + if (!b[sortKey][0]) return -1; + + const aValue = a[sortKey][0][nestedKey]; + const bValue = b[sortKey][0][nestedKey]; + + return sortFlowList(aValue, bValue, sortDirection); + }); + sortedFlows && setFlows([...sortedFlows]); + addToSearchParams(sortBy.nestedKey); + } else { + const sortedFlows = flows?.sort((a: FlowSummary, b: FlowSummary) => + sortFlowList(a[sortKey], b[sortKey], sortDirection), + ); + sortedFlows && setFlows([...sortedFlows]); + addToSearchParams(sortBy.sortKey); + } + }, [sortBy, sortDirection]); + + return ( + + { + const targetKey = e.target.value as SortTypes; + const newSortObject = sortArray.find( + (sortObject) => sortObject.sortKey === targetKey, + ); + newSortObject && setSortBy(newSortObject); + }} + > + {sortArray.map(({ displayName, sortKey }) => ( + + {displayName} + + ))} + + + sortDirection === "asc" + ? setSortDirection("desc") + : setSortDirection("asc") + } + > + {sortDirection === "asc" ? : } + + + ); +};