diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index e3fc1a3d94..0db4ddde64 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -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", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index 3633ae2e6a..ebfbb429cc 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -269,6 +269,9 @@ dependencies: tippy.js: specifier: ^6.3.7 version: 6.3.7 + type-fest: + specifier: ^4.32.0 + version: 4.32.0 uuid: specifier: ^9.0.1 version: 9.0.1 @@ -6835,7 +6838,7 @@ packages: camelcase: 8.0.0 map-obj: 5.0.0 quick-lru: 6.1.2 - type-fest: 4.30.0 + type-fest: 4.32.0 dev: false /camelcase@5.3.1: @@ -13592,11 +13595,6 @@ packages: engines: {node: '>=12.20'} dev: true - /type-fest@4.30.0: - resolution: {integrity: sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==} - engines: {node: '>=16'} - dev: false - /type-fest@4.32.0: resolution: {integrity: sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==} engines: {node: '>=16'} diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index 2a0f7c122c..9ea919d721 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -17,7 +17,10 @@ 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 { + SortableFields, + SortControl, +} from "ui/editor/SortControl"; import { slugify } from "utils"; import { client } from "../lib/graphql"; @@ -308,6 +311,15 @@ const Team: React.FC = () => { ); const [flows, setFlows] = useState(null); + const sortArray: SortableFields[] = [ + { displayName: "Name", fieldName: "name" }, + { displayName: "Last updated", fieldName: "updatedAt" }, + { + displayName: "Last published", + fieldName: `publishedFlows.0.publishedAt`, + }, + ]; + 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 @@ -353,7 +365,11 @@ const Team: React.FC = () => { {showAddFlowButton && } {hasFeatureFlag("SORT_FLOWS") && flows && ( - + + records={flows} + setRecords={setFlows} + sortOptions={sortArray} + /> )} {teamHasFlows && ( diff --git a/editor.planx.uk/src/ui/editor/SortControl.tsx b/editor.planx.uk/src/ui/editor/SortControl.tsx new file mode 100644 index 0000000000..771ac62a7d --- /dev/null +++ b/editor.planx.uk/src/ui/editor/SortControl.tsx @@ -0,0 +1,126 @@ +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 { get } 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 { + displayName: string; + fieldName: Paths; +} + +const compareValues = ( + 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 SortControl = ({ + records, + setRecords, + sortOptions, +}: { + records: T[]; + setRecords: React.Dispatch>; + sortOptions: SortableFields[]; +}) => { + const [selectedSort, setSelectedSort] = useState>( + sortOptions[0], + ); + const [sortDirection, setSortDirection] = useState("asc"); + + const navigation = useNavigation(); + const route = useCurrentRoute(); + const selectedDisplaySlug = slugify(selectedSort.displayName); + + const sortOptionsMap = useMemo(() => { + return sortOptions.reduce( + (acc, option) => ({ + ...acc, + [slugify(option.displayName)]: option, + }), + {} as Record>, + ); + }, [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, + }, + ); + }; + + useEffect(() => { + const { sort: sortParam, sortDirection: sortDirectionParam } = + route.url.query; + const matchingSortOption = sortOptionsMap[sortParam]; + matchingSortOption && setSelectedSort(matchingSortOption); + if (sortDirectionParam === "asc" || sortDirectionParam === "desc") { + setSortDirection(sortDirection); + } + }, []); + + useEffect(() => { + const { fieldName } = selectedSort; + const sortedFlows = records?.sort((a: T, b: T) => + compareValues(get(a, fieldName), get(b, fieldName), sortDirection), + ); + sortedFlows && setRecords([...sortedFlows]); + updateSortParam(selectedDisplaySlug); + }, [selectedSort, sortDirection]); + + return ( + + { + const targetKey = e.target.value as string; + const matchingSortOption = sortOptionsMap[targetKey]; + matchingSortOption && setSelectedSort(matchingSortOption); + }} + > + {sortOptions.map(({ displayName }) => ( + + {displayName} + + ))} + + + sortDirection === "asc" + ? setSortDirection("desc") + : setSortDirection("asc") + } + > + {sortDirection === "asc" ? : } + + + ); +}; diff --git a/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx b/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx deleted file mode 100644 index e8c7bda65b..0000000000 --- a/editor.planx.uk/src/ui/editor/SortFlowsSelect.tsx +++ /dev/null @@ -1,152 +0,0 @@ -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" ? : } - - - ); -};