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 }) => (
+
+ ))}
+
+
+ sortDirection === "asc"
+ ? setSortDirection("desc")
+ : setSortDirection("asc")
+ }
+ >
+ {sortDirection === "asc" ? : }
+
+
+ );
+};