Skip to content

Commit

Permalink
fix: table2 refactor + breadcrumb + linting
Browse files Browse the repository at this point in the history
  • Loading branch information
thomhickey committed Nov 17, 2024
1 parent 9e70782 commit ae967c1
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 148 deletions.
7 changes: 6 additions & 1 deletion src/components/design_system/breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import $breadcrumbs from "./Breadcrumbs.module.css";

type Student = SelectableForTable<"student">;
type Para = SelectableForTable<"user">;
type User = SelectableForTable<"user">;

const BreadcrumbsNav = () => {
const router = useRouter();
Expand All @@ -22,8 +23,12 @@ const BreadcrumbsNav = () => {
{ user_id: paths[2] },
{ enabled: Boolean(paths[2] && paths[1] === "staff") }
);
const { data: user } = trpc.user.getUserById.useQuery(
{ user_id: paths[2] },
{ enabled: Boolean(paths[2] && paths[1] === "admin") }
);

const personData: Student | Para | undefined = student || para;
const personData: Student | Para | User | undefined = student || para || user;

// An array of breadcrumbs fixed to students/staff as the first index. This will be modified depending on how the address bar will be displayed.
const breadcrumbs = paths.map((path, index) => {
Expand Down
63 changes: 63 additions & 0 deletions src/components/table/renderers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { TextField } from "@mui/material";
import { Dropdown } from "@/components/design_system/dropdown/Dropdown";
import { ColumnDefinition, BaseEntity, SelectOption } from "./types";

export function renderTableInput<T extends BaseEntity>(
column: ColumnDefinition<T>,
value: T[keyof T] | undefined,
onChange: (value: T[keyof T]) => void
): React.ReactNode {
switch (column.type) {
case "text":
return (
<TextField
size="small"
label={column.label}
value={(value as string) || ""}
onChange={(e) => onChange(e.target.value as T[keyof T])}
/>
);
case "number":
return (
<TextField
size="small"
type="number"
label={column.label}
value={(value as number) || ""}
onChange={(e) => onChange(Number(e.target.value) as T[keyof T])}
/>
);
case "select":
return (
<Dropdown
itemList={column.options as SelectOption[]}
selectedOption={(value as string) || ""}
setSelectedOption={(newValue) => onChange(newValue as T[keyof T])}
label={column.label}
/>
);
default:
return String(value || "");
}
}

export function renderTableCell<T extends BaseEntity>(
column: ColumnDefinition<T>,
value: T[keyof T]
): React.ReactNode {
if (column.customRender) {
return column.customRender(value);
}

switch (column.type) {
case "select":
return (
column.options?.find((opt) => opt.value === value)?.label ||
String(value)
);
case "date":
return value instanceof Date ? value.toLocaleDateString() : String(value);
default:
return String(value || "");
}
}
115 changes: 31 additions & 84 deletions src/components/table/table2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,17 @@ import {
TableHead,
TableRow,
Box,
TextField,
Button,
TableSortLabel,
TextField,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import styles from "./Table.module.css";
import { visuallyHidden } from "@mui/utils";
import SearchIcon from "@mui/icons-material/Search";

export interface BaseEntity {
id?: string | number;
first_name: string;
last_name: string;
email: string;
[key: string]: string | number | undefined;
}

export interface Column<T extends BaseEntity> {
id: keyof T;
label: string;
renderInput?: (
value: T[keyof T] | undefined,
onChange: (value: T[keyof T]) => void
) => React.ReactNode;
renderCell?: (value: T[keyof T]) => React.ReactNode;
}

interface TableProps<T extends BaseEntity> {
data: T[];
columns: Column<T>[];
type: string;
onRowClick?: (row: T) => void;
page?: number;
totalPages?: number;
onPageChange?: (page: number) => void;
sortBy: keyof T;
sortOrder: "asc" | "desc";
onSort: (sortBy: keyof T, sortOrder: "asc" | "desc") => void;
onSearch?: (search: string) => void;
searchTerm?: string;
onAdd?: (data: Omit<T, "id">) => Promise<void>;
showAddRow?: boolean;
}
import { TableProps, BaseEntity } from "./types";
import { renderTableInput, renderTableCell } from "./renderers";
import $table from "./Table.module.css";
import $button from "@/components/design_system/button/Button.module.css";

const StyledTableRow = styled(TableRow)(() => ({
"&:nth-of-type(odd)": {
Expand Down Expand Up @@ -96,63 +64,55 @@ export function Table2<T extends BaseEntity>({
const handleAddRow = async (e: React.FormEvent) => {
e.preventDefault();
if (onAdd) {
try {
await onAdd(newRowData as Omit<T, "id">);
setNewRowData({});
setIsAddingRow(false);
} catch (error) {
console.error(error);
}
await onAdd(newRowData as Omit<T, "id">);
setIsAddingRow(false);
setNewRowData({});
}
};

return (
<Box sx={{ width: "100%" }}>
<Box sx={{ mb: 2, display: "flex", justifyContent: "space-between" }}>
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
<h3>{type}</h3>
{onAdd && !isAddingRow && showAddRow && (
<Button variant="contained" onClick={() => setIsAddingRow(true)}>
Add {type.slice(0, -1)}
</Button>
)}
</Box>
<Box>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
<TextField
size="small"
placeholder="Search..."
placeholder={`Search ${type}`}
value={localSearchTerm}
onChange={(e) => setLocalSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
InputProps={{
startAdornment: <SearchIcon />,
}}
/>
{showAddRow && !isAddingRow && (
<button
onClick={() => setIsAddingRow(true)}
className={$button.default}
>
Add {type}
</button>
)}
</Box>

<TableContainer>
<Table>
<TableHead className={styles.header}>
<TableHead className={$table.header}>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.id.toString()}
align="left"
sortDirection={sortBy === column.id ? sortOrder : false}
>
<TableCell key={column.id.toString()}>
<TableSortLabel
active={sortBy === column.id}
direction={sortBy === column.id ? sortOrder : "asc"}
onClick={() => handleRequestSort(column.id)}
className={styles.headerLabel}
className={$table.headerLabel}
>
{column.label}
{sortBy === column.id ? (
{sortBy === column.id && (
<Box component="span" sx={visuallyHidden}>
{sortOrder === "desc"
? "sorted descending"
: "sorted ascending"}
</Box>
) : null}
)}
</TableSortLabel>
</TableCell>
))}
Expand All @@ -161,32 +121,21 @@ export function Table2<T extends BaseEntity>({
<TableBody>
{isAddingRow && (
<TableRow>
<TableCell colSpan={columns.length + 1}>
<TableCell colSpan={columns.length}>
<form onSubmit={handleAddRow}>
<Box
sx={{ display: "flex", gap: 2, alignItems: "flex-end" }}
>
{columns.map((column) => (
<Box key={column.id.toString()}>
{column.renderInput?.(
{renderTableInput<T>(
column,
newRowData[column.id],
(value: T[keyof T]) =>
(value) =>
setNewRowData((prev) => ({
...prev,
[column.id]: value,
}))
) ?? (
<TextField
size="small"
label={column.label}
value={newRowData[column.id] || ""}
onChange={(e) =>
setNewRowData((prev) => ({
...prev,
[column.id]: e.target.value,
}))
}
/>
)}
</Box>
))}
Expand All @@ -209,16 +158,14 @@ export function Table2<T extends BaseEntity>({
</TableCell>
</TableRow>
)}
{data.map((row, index) => (
{data.map((row) => (
<StyledTableRow
key={row.id || index}
key={row.id || row.email}
onClick={() => onRowClick?.(row)}
>
{columns.map((column) => (
<TableCell key={column.id.toString()}>
{column.renderCell
? column.renderCell(row[column.id])
: row[column.id]}
{renderTableCell(column, row[column.id])}
</TableCell>
))}
</StyledTableRow>
Expand Down
40 changes: 40 additions & 0 deletions src/components/table/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export type ColumnType = "text" | "number" | "select" | "date";

export interface BaseEntity {
id?: string | number;
first_name: string;
last_name: string;
email: string;
[key: string]: string | number | Date | undefined;
}

export interface SelectOption {
value: string;
label: string;
}

export interface ColumnDefinition<T extends BaseEntity> {
id: keyof T;
label: string;
type: ColumnType;
options?: SelectOption[];
validation?: (value: T[keyof T]) => boolean;
customRender?: (value: T[keyof T]) => React.ReactNode;
}

export interface TableProps<T extends BaseEntity> {
data: T[];
columns: ColumnDefinition<T>[];
type: string;
onRowClick?: (row: T) => void;
page?: number;
totalPages?: number;
onPageChange?: (page: number) => void;
sortBy: keyof T;
sortOrder: "asc" | "desc";
onSort: (sortBy: keyof T, sortOrder: "asc" | "desc") => void;
onSearch?: (search: string) => void;
searchTerm?: string;
onAdd?: (data: Omit<T, "id">) => Promise<void>;
showAddRow?: boolean;
}
File renamed without changes.
Loading

0 comments on commit ae967c1

Please sign in to comment.