Move sorting to server-side for full dataset ordering

- usePaginatedList now manages sortBy/sortDir state
- Sort params passed to fetch function and sent to API
- SharedTable receives sort state as props instead of managing internally
- Clicking column headers triggers API refetch with new sort
- Removes client-side sorting that only worked on current page
This commit is contained in:
Adam Moussa 2026-04-17 12:50:23 -04:00
parent 79f7b4782a
commit e2fc183636
5 changed files with 32 additions and 38 deletions

View file

@ -87,14 +87,16 @@ const SharedTable = ({
setPageSize,
totalCount,
loading,
error
error,
sortBy,
setSortBy,
sortDir,
setSortDir
}) => {
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
const [colWidths, setColWidths] = useState(null);
const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.key));
const [hiddenColumns, setHiddenColumns] = useState([]);
const [sortKey, setSortKey] = useState("lastUpdated");
const [sortDir, setSortDir] = useState("desc");
const gridRef = useRef(null);
useEffect(() => {
@ -166,25 +168,16 @@ const SharedTable = ({
}, [snapshotWidths, visibleColumns]);
const handleSort = (key) => {
if (sortKey === key) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
if (!setSortBy || !setSortDir) return;
if (sortBy === key) {
setSortDir(sortDir === "asc" ? "desc" : "asc");
} else {
setSortKey(key);
setSortBy(key);
setSortDir("asc");
}
if (setPage) setPage(1);
};
const sortedRows = [...rows].sort((a, b) => {
if (!sortKey) return 0;
let aVal = a[sortKey] ?? "";
let bVal = b[sortKey] ?? "";
if (typeof aVal === "string") aVal = aVal.toLowerCase();
if (typeof bVal === "string") bVal = bVal.toLowerCase();
if (aVal < bVal) return sortDir === "asc" ? -1 : 1;
if (aVal > bVal) return sortDir === "asc" ? 1 : -1;
return 0;
});
if (loading) return <div className="loading">Loading...</div>;
if (error) return <div className="error">{error}</div>;
@ -243,11 +236,11 @@ const SharedTable = ({
<div
key={col.key}
data-col={col.key}
className={`gridCell gridHeader gridSortable ${sortKey === col.key ? "sorted" : ""}`}
className={`gridCell gridHeader gridSortable ${sortBy === col.key ? "sorted" : ""}`}
onClick={() => handleSort(col.key)}
>
{col.label}
{sortKey === col.key && <span className="sortArrow">{sortDir === "asc" ? " ▲" : " ▼"}</span>}
{sortBy === col.key && <span className="sortArrow">{sortDir === "asc" ? " ▲" : " ▼"}</span>}
<span
className="colResizeHandle"
onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, col.key); }}
@ -256,7 +249,7 @@ const SharedTable = ({
))}
<div className="gridCell gridHeader">Actions</div>
{sortedRows.map((row, idx) => (
{rows.map((row, idx) => (
<React.Fragment key={row.id || idx}>
<div className="gridCell gridBody">
<input type="checkbox" />

View file

@ -9,6 +9,8 @@ export default function usePaginatedList(fetchFn, extraParams = {}) {
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState("");
const [sortBy, setSortBy] = useState("lastUpdated");
const [sortDir, setSortDir] = useState("desc");
const debounceRef = useRef(null);
const extraKey = JSON.stringify(extraParams);
@ -25,19 +27,19 @@ export default function usePaginatedList(fetchFn, extraParams = {}) {
const reload = useCallback(() => {
setLoading(true);
setErr("");
fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams })
fetchFn({ search: debouncedSearch, page, pageSize, sortBy, sortDir, ...extraParams })
.then(({ rows, totalCount }) => {
setRows(rows);
setTotalCount(totalCount);
})
.catch((e) => setErr(e.message || "Failed to load"))
.finally(() => setLoading(false));
}, [fetchFn, debouncedSearch, page, pageSize, extraKey]);
}, [fetchFn, debouncedSearch, page, pageSize, sortBy, sortDir, extraKey]);
useEffect(() => { reload(); }, [reload]);
return useMemo(() => ({
rows, search, setSearch, page, setPage, pageSize, setPageSize,
totalCount, loading, err, reload
}), [rows, search, page, pageSize, totalCount, loading, err, reload]);
totalCount, loading, err, reload, sortBy, setSortBy, sortDir, setSortDir
}), [rows, search, page, pageSize, totalCount, loading, err, reload, sortBy, sortDir]);
}

View file

@ -11,16 +11,8 @@ import "./styles.css";
export default function WorkOrdersList() {
const navigate = useNavigate();
const {
rows,
search,
setSearch,
page,
setPage,
pageSize,
setPageSize,
totalCount,
loading,
err
rows, search, setSearch, page, setPage, pageSize, setPageSize,
totalCount, loading, err, sortBy, setSortBy, sortDir, setSortDir
} = usePaginatedList(fetchWorkOrdersPaged);
const [deleteModal, setDeleteModal] = useState({
@ -124,6 +116,10 @@ export default function WorkOrdersList() {
setPageSize={setPageSize}
totalCount={totalCount}
totalPages={totalPages}
sortBy={sortBy}
setSortBy={setSortBy}
sortDir={sortDir}
setSortDir={setSortDir}
/>
<DeleteModal

View file

@ -143,7 +143,7 @@ function AssignDropdown({ value, rowId, onUpdate }) {
);
}
export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages, onRowUpdate }) {
export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages, onRowUpdate, sortBy, setSortBy, sortDir, setSortDir }) {
const [localRows, setLocalRows] = useState(rows);
useEffect(() => {
@ -194,6 +194,6 @@ export default function Table({ loading, error, rows, onEdit, onDelete, page, se
];
return (
<SharedTable rows={localRows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} />
<SharedTable rows={localRows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} sortBy={sortBy} setSortBy={setSortBy} sortDir={sortDir} setSortDir={setSortDir} />
);
}

View file

@ -1,8 +1,11 @@
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchWorkOrdersPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
const res = await get(`${API_URL}/WorkOrder/GetWorkOrderList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
export const fetchWorkOrdersPaged = async ({ search = "", page = 1, pageSize = 12, sortBy = "", sortDir = "" }) => {
const params = new URLSearchParams({ search, page, pageSize });
if (sortBy) params.append("sortBy", sortBy);
if (sortDir) params.append("sortDir", sortDir);
const res = await get(`${API_URL}/WorkOrder/GetWorkOrderList?${params.toString()}`);
const rows = (res?.data || res?.Data || []).map(x => ({
id: x.id ?? x.Id,
number: x.internalWONumber ?? x.number ?? x.workOrderNumber ?? "",