Skip to content

Commit

Permalink
feat: add select for sorting flow list (#4137)
Browse files Browse the repository at this point in the history
RODO94 authored Jan 17, 2025
1 parent 58e9220 commit 9cde7ae
Showing 6 changed files with 197 additions and 18 deletions.
1 change: 1 addition & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
@@ -89,6 +89,7 @@
"subscriptions-transport-ws": "^0.11.0",
"swr": "^2.2.4",
"tippy.js": "^6.3.7",
"type-fest": "^4.32.0",
"uuid": "^9.0.1",
"vite": "^5.4.6",
"vite-jest": "^0.1.4",
10 changes: 4 additions & 6 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion editor.planx.uk/src/lib/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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];

38 changes: 28 additions & 10 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import {
ComponentType as TYPES,
flatFlags,
FlowGraph,
FlowStatus,
NodeId,
OrderedFlow,
} from "@opensystemslab/planx-core/types";
@@ -132,18 +133,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: FlowStatus;
updatedAt: string;
operations: {
createdAt: string;
actor: {
firstName: string;
lastName: string;
};
}[];
operations: FlowSummaryOperations[];
publishedFlows: PublishedFlowSummary[];
}

export interface EditorStore extends Store.Store {
@@ -382,6 +392,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 +401,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,9 +632,9 @@ 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);
27 changes: 26 additions & 1 deletion editor.planx.uk/src/pages/Team.tsx
Original file line number Diff line number Diff line change
@@ -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 { SortableFields, SortControl } from "ui/editor/SortControl";
import { slugify } from "utils";

import { client } from "../lib/graphql";
@@ -307,6 +308,23 @@ const Team: React.FC = () => {
);
const [flows, setFlows] = useState<FlowSummary[] | null>(null);

const sortOptions: SortableFields<FlowSummary>[] = [
{
displayName: "Name",
fieldName: "name",
directionNames: { asc: "A - Z", desc: "Z - A" },
},
{
displayName: "Last updated",
fieldName: "updatedAt",
directionNames: { asc: "Oldest first", desc: "Newest first" },
},
{
displayName: "Last published",
fieldName: `publishedFlows.0.publishedAt`,
directionNames: { asc: "Oldest first", desc: "Newest first" },
},
];
const fetchFlows = useCallback(() => {
getFlows(teamId).then((flows) => {
// Copy the array and sort by most recently edited desc using last associated operation.createdAt, not flow.updatedAt
@@ -351,6 +369,13 @@ const Team: React.FC = () => {
</Box>
{showAddFlowButton && <AddFlowButton flows={flows} />}
</Box>
{hasFeatureFlag("SORT_FLOWS") && flows && (
<SortControl<FlowSummary>
records={flows}
setRecords={setFlows}
sortOptions={sortOptions}
/>
)}
{teamHasFlows && (
<DashboardList>
{flows.map((flow) => (
133 changes: 133 additions & 0 deletions editor.planx.uk/src/ui/editor/SortControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import { get, orderBy } from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useCurrentRoute, useNavigation } from "react-navi";
import { Paths } from "type-fest";
import { slugify } from "utils";

import SelectInput from "./SelectInput/SelectInput";

type SortDirection = "asc" | "desc";

export interface SortableFields<T> {
/** displayName is a string to use in the Select */
displayName: string;
/** fieldName is the key of the object used to sort */
fieldName: Paths<T>;
/** directionNames should be an object specifying display values for ascending and descending sort order */
directionNames: { asc: string; desc: string };
}
/**
* @component
* @description Sorts a list of objects
* @param {Type} T - a type to define the shape of the data in the records array
* @param {Array} props.records - an array of objects to sort
* @param {Function} props.setRecords - A way to set the new sorted order of the array
* @param {Array} props.sortOptions - An array of objects to define displayName, fieldName, and directionNames
* @returns {JSX.Element} Two select components to switch between fieldName and directionNames
* @example
* <SortControl<FlowSummary>
* records={flows}
* setRecords={setFlows}
* sortOptions={sortOptions}
* />
*/
export const SortControl = <T extends object>({
records,
setRecords,
sortOptions,
}: {
records: T[];
setRecords: React.Dispatch<React.SetStateAction<T[] | null>>;
sortOptions: SortableFields<T>[];
}) => {
const [selectedSort, setSelectedSort] = useState<SortableFields<T>>(
sortOptions[0],
);
const [sortDirection, setSortDirection] = useState<SortDirection>("asc");

const navigation = useNavigation();
const route = useCurrentRoute();
const selectedDisplaySlug = slugify(selectedSort.displayName);

const sortOptionsMap = useMemo(() => {
return Object.groupBy(sortOptions, ({ displayName }) =>
slugify(displayName),
);
}, [sortOptions]);

const updateSortParam = (sortOption: string) => {
const searchParams = new URLSearchParams();
searchParams.set("sort", sortOption);
searchParams.set("sortDirection", sortDirection);
navigation.navigate(
{
pathname: window.location.pathname,
search: `?${searchParams.toString()}`,
},
{
replace: true,
},
);
};

const parseStateFromURL = () => {
const { sort: sortParam, sortDirection: sortDirectionParam } =
route.url.query;
const matchingSortOption = sortOptionsMap[sortParam];
if (!matchingSortOption) return;
setSelectedSort(matchingSortOption[0]);
if (sortDirectionParam === "asc" || sortDirectionParam === "desc") {
setSortDirection(sortDirection);
}
};

useEffect(() => {
parseStateFromURL();
}, []);

useEffect(() => {
const { fieldName } = selectedSort;
const sortNewFlows = orderBy(records, fieldName, sortDirection);
setRecords(sortNewFlows);
updateSortParam(selectedDisplaySlug);
}, [selectedSort, sortDirection]);

return (
<Box display={"flex"}>
<SelectInput
value={selectedDisplaySlug}
onChange={(e) => {
const targetKey = e.target.value as string;
const matchingSortOption = sortOptionsMap[targetKey];
if (!matchingSortOption) return;
setSelectedSort(matchingSortOption?.[0]);
}}
>
{sortOptions.map(({ displayName }) => (
<MenuItem key={slugify(displayName)} value={slugify(displayName)}>
{displayName}
</MenuItem>
))}
</SelectInput>
<SelectInput
value={sortDirection}
onChange={(e) => {
const newDirection = e.target.value as SortDirection;
setSortDirection(newDirection);
}}
>
<MenuItem key={slugify(selectedSort.directionNames.asc)} value={"asc"}>
{selectedSort.directionNames.asc}
</MenuItem>
<MenuItem
key={slugify(selectedSort.directionNames.desc)}
value={"desc"}
>
{selectedSort.directionNames.desc}
</MenuItem>
</SelectInput>
</Box>
);
};

0 comments on commit 9cde7ae

Please sign in to comment.