-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add select for sorting flow list #4137
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,6 @@ | ||
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 { get, orderBy } from "lodash"; | ||
import React, { useEffect, useMemo, useState } from "react"; | ||
import { useCurrentRoute, useNavigation } from "react-navi"; | ||
import { Paths } from "type-fest"; | ||
|
@@ -14,24 +11,28 @@ 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 }; | ||
} | ||
|
||
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; | ||
}; | ||
|
||
/** | ||
* @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, | ||
|
@@ -51,12 +52,8 @@ export const SortControl = <T extends object>({ | |
const selectedDisplaySlug = slugify(selectedSort.displayName); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably the part I feel most unsure about, but I'm comfortable that it delivers a unifying parameter I can use to search for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it's a good question - There's no guarantee that This might just be a data structure question. If we were to make sortOptions: Record<string, SortableFields<T>>
...
const sortArray: Record<string, SortableFields<FlowSummary>> = {
name: { displayName: "Name", fieldName: "name" },
lastUpdated: { displayName: "Last updated", fieldName: "updatedAt" },
lastPublished: {
displayName: "Last published",
fieldName: `publishedFlows.0.publishedAt`,
},
}
This would -
What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this was an iteration I considered, I was thinking this is dealing with a complexity which is within the component (so not obvious when you add it to your table comp) so it is better to handle it within the component? Imagining someone building this into their component and being confused why you have to add so many names, ultimately the same thing three times. I can see the risk of duplicates but thought it was quite edge casey and resolvable. But, I'm happy to integrate the change, I can see the benefits of the safety, so you don't need to try find your error in the naming. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this is a fair point - lets aim to keep the complexity within the component I think 👍 Your version is clearer and simpler for consumers. The risk of duplicates is very very low! |
||
const sortOptionsMap = useMemo(() => { | ||
return sortOptions.reduce( | ||
RODO94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(acc, option) => ({ | ||
...acc, | ||
[slugify(option.displayName)]: option, | ||
}), | ||
{} as Record<string, SortableFields<T>>, | ||
return Object.groupBy(sortOptions, ({ displayName }) => | ||
slugify(displayName), | ||
); | ||
}, [sortOptions]); | ||
|
||
|
@@ -75,22 +72,25 @@ export const SortControl = <T extends object>({ | |
); | ||
}; | ||
|
||
useEffect(() => { | ||
const parseStateFromURL = () => { | ||
const { sort: sortParam, sortDirection: sortDirectionParam } = | ||
route.url.query; | ||
const matchingSortOption = sortOptionsMap[sortParam]; | ||
matchingSortOption && setSelectedSort(matchingSortOption); | ||
if (!matchingSortOption) return; | ||
setSelectedSort(matchingSortOption[0]); | ||
if (sortDirectionParam === "asc" || sortDirectionParam === "desc") { | ||
setSortDirection(sortDirection); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
parseStateFromURL(); | ||
}, []); | ||
|
||
useEffect(() => { | ||
const { fieldName } = selectedSort; | ||
const sortedFlows = records?.sort((a: T, b: T) => | ||
compareValues(get(a, fieldName), get(b, fieldName), sortDirection), | ||
); | ||
sortedFlows && setRecords([...sortedFlows]); | ||
const sortNewFlows = orderBy(records, fieldName, sortDirection); | ||
setRecords(sortNewFlows); | ||
updateSortParam(selectedDisplaySlug); | ||
}, [selectedSort, sortDirection]); | ||
|
||
|
@@ -101,7 +101,8 @@ export const SortControl = <T extends object>({ | |
onChange={(e) => { | ||
const targetKey = e.target.value as string; | ||
const matchingSortOption = sortOptionsMap[targetKey]; | ||
matchingSortOption && setSelectedSort(matchingSortOption); | ||
if (!matchingSortOption) return; | ||
setSelectedSort(matchingSortOption?.[0]); | ||
}} | ||
> | ||
{sortOptions.map(({ displayName }) => ( | ||
|
@@ -110,17 +111,23 @@ export const SortControl = <T extends object>({ | |
</MenuItem> | ||
))} | ||
</SelectInput> | ||
<IconButton | ||
title="ordering" | ||
aria-label="ordering" | ||
onClick={() => | ||
sortDirection === "asc" | ||
? setSortDirection("desc") | ||
: setSortDirection("asc") | ||
} | ||
<SelectInput | ||
value={sortDirection} | ||
onChange={(e) => { | ||
const newDirection = e.target.value as SortDirection; | ||
setSortDirection(newDirection); | ||
}} | ||
> | ||
{sortDirection === "asc" ? <TrendingUpIcon /> : <TrendingDownIcon />} | ||
</IconButton> | ||
<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> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! When we have more use cases for this we may find we want these display values centralised within the component, and just allow consumers to define by type (e.g.
direction: "alphabetical" | "date"
). This is perfect for now 👍