Skip to content
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 search to Team list #4174

Merged
merged 18 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ALL_FACETS, SearchFacets } from "./facets";
import { SearchHeader } from "./SearchHeader";
import { SearchResultCard } from "./SearchResultCard";

const DEBOUNCE_MS = 500;
export const DEBOUNCE_MS = 500;
RODO94 marked this conversation as resolved.
Show resolved Hide resolved

interface SearchNodes {
pattern: string;
Expand Down
35 changes: 17 additions & 18 deletions editor.planx.uk/src/pages/Team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Typography from "@mui/material/Typography";
import { hasFeatureFlag } from "lib/featureFlags";
import { isEmpty } from "lodash";
import React, { useCallback, useEffect, useState } from "react";
import { Link, useCurrentRoute, useNavigation } from "react-navi";
import { Link, useNavigation } from "react-navi";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import { borderedFocusStyle } from "theme";
import { AddButton } from "ui/editor/AddButton";
Expand Down Expand Up @@ -258,7 +258,7 @@ const FlowItem: React.FC<FlowItemProps> = ({
);
};

const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => (
const GetStarted: React.FC<{ flows: FlowSummary[] | null }> = ({ flows }) => (
<Box
sx={(theme) => ({
mt: 4,
Expand All @@ -277,7 +277,9 @@ const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => (
</Box>
);

const AddFlowButton: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => {
const AddFlowButton: React.FC<{ flows: FlowSummary[] | null }> = ({
flows,
}) => {
const { navigate } = useNavigation();
const { teamId, createFlow, teamSlug } = useStore();

Expand Down Expand Up @@ -311,10 +313,6 @@ const Team: React.FC = () => {
null,
);

const route = useCurrentRoute();

const haveFlowsBeenFiltered = filteredFlows?.length !== flows?.length;

const sortOptions: SortableFields<FlowSummary>[] = [
{
displayName: "Name",
Expand All @@ -333,6 +331,7 @@ const Team: React.FC = () => {
},
];
const fetchFlows = useCallback(() => {
console.log("fetch flows");
getFlows(teamId).then((flows) => {
// Copy the array and sort by most recently edited desc using last associated operation.createdAt, not flow.updatedAt
const sortedFlows = flows.toSorted((a, b) =>
Expand All @@ -341,16 +340,18 @@ const Team: React.FC = () => {
),
);
setFlows(sortedFlows);
setFilteredFlows(sortedFlows);
});
}, [teamId, setFlows, getFlows]);

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

const teamHasFlows =
!isEmpty(filteredFlows) && Boolean(filteredFlows?.length);
const showAddFlowButton = !isEmpty(flows) && canUserEditTeam(slug);
const teamHasFlows = !isEmpty(filteredFlows) && !isEmpty(flows);
const showAddFlowButton = teamHasFlows && canUserEditTeam(slug);

console.log(flows);

return (
<Container maxWidth="lg">
Expand All @@ -376,13 +377,11 @@ const Team: React.FC = () => {
<Typography variant="h2" component="h1" pr={1}>
Services
</Typography>
{/* {canUserEditTeam(slug) ? <Edit /> : <Visibility />} */}
{showAddFlowButton && <AddFlowButton flows={flows || []} />}
{showAddFlowButton && flows && <AddFlowButton flows={flows} />}
</Box>
{hasFeatureFlag("SORT_FLOWS") && !isEmpty(flows) && (
{hasFeatureFlag("SORT_FLOWS") && flows && (
<SearchBox<FlowSummary>
records={filteredFlows || []}
staticRecords={flows || []}
records={filteredFlows}
setRecords={setFilteredFlows}
searchKey={["name", "slug"]}
/>
Expand All @@ -396,12 +395,12 @@ const Team: React.FC = () => {
sortOptions={sortOptions}
/>
)}
{teamHasFlows && (
{teamHasFlows && flows && (
<DashboardList>
{filteredFlows?.map((flow) => (
<FlowItem
flow={flow}
flows={filteredFlows}
flows={flows}
key={flow.slug}
teamId={teamId}
teamSlug={slug}
Expand All @@ -412,7 +411,7 @@ const Team: React.FC = () => {
))}
</DashboardList>
)}
{flows && !flows.length && <GetStarted flows={flows} />}
{!flows && <GetStarted flows={flows} />}
</Container>
);
};
Expand Down
65 changes: 45 additions & 20 deletions editor.planx.uk/src/ui/shared/SearchBox/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import ClearIcon from "@mui/icons-material/Clear";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import { useFormik } from "formik";
import { FuseOptionKey } from "fuse.js";
import { useSearch } from "hooks/useSearch";
import { debounce } from "lodash";
import { DEBOUNCE_MS } from "pages/FlowEditor/components/Sidebar/Search";
import React, { useEffect, useMemo, useState } from "react";

import Input from "../Input/Input";
Expand All @@ -13,46 +15,51 @@ import InputRowItem from "../InputRowItem";
import InputRowLabel from "../InputRowLabel";

interface SearchBoxProps<T> {
records: T[];
staticRecords: T[];
records: T[] | null;
setRecords: React.Dispatch<React.SetStateAction<T[] | null>>;
searchKey: FuseOptionKey<T>[];
}

export const SearchBox = <T extends object>({
records,
staticRecords,
setRecords,
searchKey,
}: SearchBoxProps<T>) => {
const [isSearching, setIsSearching] = useState(false);
const [searchedTerm, setSearchedTerm] = useState<string>();
const [originalRecords] = useState(records);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This originalRecords variable is kept in place to handle behaviour for when we run setRecords(newRecords) for a searched term - this updates records in the parent component and triggers a re-render, but originalRecords is only set on mount and not reset when the components re-render, so it acts as a proper original version of the list.

Records as an immutable prop works differently since it will be updated when the Parent component is re-rendered, like when setRecords(newRecords) is triggered


const formik = useFormik({
initialValues: { pattern: "", keys: searchKey },
onSubmit: () => {},
onSubmit: ({ pattern }) => {
setIsSearching(true);
debouncedSearch(pattern);
},
});

const { results, search } = useSearch({
list: records,
list: originalRecords || [],
keys: formik.values.keys,
});

const debouncedSearch = useMemo(
() =>
debounce((recordsToUpdate: T[]) => {
setRecords(recordsToUpdate);
debounce((pattern: string) => {
search(pattern);
setSearchedTerm(pattern);
setIsSearching(false);
}, 500),
[],
}, DEBOUNCE_MS),
[search],
);

useEffect(() => {
const mappedResults = results.map((result) => result.item);
formik.values.pattern && debouncedSearch(mappedResults);
if (!formik.values.pattern) {
debouncedSearch(staticRecords);
if (results && searchedTerm) {
const mappedResults = results.map((result) => result.item);
setRecords(mappedResults);
}
if (results && !searchedTerm) {
originalRecords && setRecords(originalRecords);
}
}, [formik.values.pattern]);
}, [results, setRecords, searchedTerm, originalRecords]);

return (
<Box maxWidth={360}>
Expand All @@ -72,17 +79,15 @@ export const SearchBox = <T extends object>({
value={formik.values.pattern}
onChange={(e) => {
formik.setFieldValue("pattern", e.target.value);
setIsSearching(true);
search(e.target.value);
formik.submitForm();
}}
/>
{formik.values.pattern && !isSearching && (
{searchedTerm && !isSearching && (
<IconButton
aria-label="clear search"
onClick={() => {
formik.setFieldValue("pattern", "");
search("");
setRecords(staticRecords);
formik.submitForm();
}}
size="small"
sx={{
Expand All @@ -97,6 +102,26 @@ export const SearchBox = <T extends object>({
<ClearIcon fontSize="small" />
</IconButton>
)}
{isSearching && (
<IconButton
aria-label="clear search"
onClick={() => {
formik.setFieldValue("pattern", "");
formik.submitForm();
}}
size="small"
sx={{
position: "absolute",
right: (theme) => theme.spacing(1),
top: "50%",
transform: "translateY(-50%)",
padding: 0.5,
zIndex: 1,
}}
>
<CircularProgress size={"1.5rem"} />
</IconButton>
)}
</Box>
</InputRowItem>
</InputRow>
Expand Down