Refactor out Search components and hooks

This commit is contained in:
Kazi 2024-12-06 08:00:01 +06:00
parent 835b9a20fa
commit dba614fcc9
3 changed files with 137 additions and 57 deletions

View File

@ -0,0 +1,68 @@
import {
Field,
SearchBox as FUISearchBox,
tokens,
} from "@fluentui/react-components";
import { useState } from "react";
const styles: Record<string, React.CSSProperties> = {
search: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
},
searchBox: {
width: "28ch",
},
};
export function useFilteredBySearch<T>(
data: T[],
filterFn: (d: T, search: string) => boolean,
) {
const [search, setSearch] = useState("");
const filteredItems = data.filter((d) => filterFn(d, search));
return {
search,
setSearch,
filteredItems,
};
}
export function SearchBox({
onSearch,
}: {
onSearch: (search: string) => void;
}) {
return (
<Field label="Search" style={styles.search}>
<FUISearchBox
autoComplete="off"
onChange={(_, data) => {
const search = data.value.toLowerCase();
onSearch(search);
}}
style={styles.searchBox}
/>
</Field>
);
}
export function filterBySearch(
search: string,
searchableFields: Record<string, string>,
): boolean {
if (search.length) {
// Find matching search term in data values
const match = Object.values(searchableFields).some((v) =>
String(v).toLowerCase().includes(search),
);
return match;
}
return true;
}

View File

@ -1,5 +1,22 @@
@import url("reset.css") layer(reset);
:root {
/* FluentUI Spacing - https://react.fluentui.dev/?path=/docs/theme-spacing--docs */
--spacingXXS: 2px;
--spacingXS: 4px;
--spacingSNudge: 6px;
--spacingS: 8px;
--spacingMNudge: 10px;
--spacingM: 12px;
--spacingL: 16px;
--spacingXL: 20px;
--spacingXXL: 24px;
--spacingXXXL: 32px;
/* App variables */
--gutter: var(--spacingM);
}
@layer composition {
/*
FLOW COMPOSITION
@ -9,6 +26,30 @@
.flow > * + * {
margin-top: var(--flow-space, 1em);
}
/*
CLUSTER
More info: https://every-layout.dev/layouts/cluster/
A layout that lets you distribute items with consitent
spacing, regardless of their size
CUSTOM PROPERTIES AND CONFIGURATION
--gutter: This defines the space
between each item.
--cluster-horizontal-alignment (flex-start) How items should align
horizontally. Can be any acceptable flexbox aligmnent value.
--cluster-vertical-alignment How items should align vertically.
Can be any acceptable flexbox alignment value.
*/
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--gutter);
justify-content: var(--cluster-horizontal-alignment, flex-start);
align-items: var(--cluster-vertical-alignment, center);
}
}
.table-wrapper {

View File

@ -7,11 +7,9 @@ import {
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
Field,
makeStyles,
MenuButton,
MenuItem,
SearchBox,
Spinner,
TableCellLayout,
TableColumnDefinition,
@ -37,6 +35,11 @@ import {
PaginationItemsPerPageSelector,
usePagination,
} from "../components/Pagination";
import {
filterBySearch,
SearchBox,
useFilteredBySearch,
} from "../components/SearchBox";
const compareNum = (a: number, b: number) => {
return a == b ? 0 : a < b ? 1 : -1;
@ -113,18 +116,7 @@ const columns: TableColumnDefinition<StatusClientItem>[] = [
];
const useStyles = makeStyles({
topFilters: {
display: "flex",
gap: tokens.spacingHorizontalM,
},
search: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
},
searchBox: {
width: "28ch",
},
// TODO: remove following unused style
pagination: {
marginInlineStart: "auto",
},
@ -151,11 +143,12 @@ const Status = () => {
const classes = useStyles();
const [search, setSearch] = useState("");
const data = statusResult.data!.status;
const dataItems = statusResult.data!.status;
const filteredItems = filterClientData(dataItems, search);
const { setSearch, filteredItems } = useFilteredBySearch<StatusClientItem>(
data,
filterClientData,
);
const { itemsPerPage, setItemsPerPage, pageData, page, setPage } =
usePagination(filteredItems);
@ -165,18 +158,8 @@ const Status = () => {
<Suspense fallback={<Spinner />}>
<TableWrapper>
<h3>Status page</h3>
<div className={classes.topFilters}>
<Field label="Search" className={classes.search}>
<SearchBox
autoComplete="off"
className={classes.searchBox}
onChange={(_, data) => {
const search = data.value.toLowerCase();
setSearch(search);
}}
/>
</Field>
<div className="cluster">
<SearchBox onSearch={setSearch} />
<PaginationItemsPerPageSelector
itemsPerPage={itemsPerPage}
setItemsPerPage={setItemsPerPage}
@ -289,34 +272,22 @@ function transformSelectedRows(selectedRows: Set<TableRowId>) {
return clientIds;
}
function filterClientData(dataItems: StatusClientItem[], search: string) {
return dataItems.filter((d) => {
// Hide items scheduled for delete
if (d.delete_pending === "1") {
return false;
}
function filterClientData(item: StatusClientItem, search: string) {
// Hide items scheduled for delete
if (item.delete_pending === "1") {
return false;
}
// If there's a search term, filter by search term within object values
if (search.length) {
const { id, name, lastbackup, lastbackup_image } = d;
const { id, name, lastbackup, lastbackup_image } = item;
// Search in fields as displayed in the table
const searchableFields = {
id,
name,
lastbackup: lastbackup === 0 ? "Never" : formatDatetime(lastbackup),
lastbackup_image:
lastbackup === 0 ? "Never" : formatDatetime(lastbackup_image),
};
// Search in fields as displayed in the table
const searchableFields = {
id: String(id),
name,
lastbackup: lastbackup === 0 ? "Never" : formatDatetime(lastbackup),
lastbackup_image:
lastbackup === 0 ? "Never" : formatDatetime(lastbackup_image),
};
// Find matching search term in data values
const match = Object.values(searchableFields).some((v) =>
String(v).toLowerCase().includes(search),
);
return match;
}
return true;
});
return filterBySearch(search, searchableFields);
}