mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-20 18:40:17 -04:00
Merge ee4bea60ac
into bdc6600a3f
This commit is contained in:
commit
085957b76a
189
compass/components/Table/ColumnHeader.tsx
Normal file
189
compass/components/Table/ColumnHeader.tsx
Normal file
|
@ -0,0 +1,189 @@
|
|||
import { flexRender, Header } from "@tanstack/react-table";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
function DropdownCheckIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<CheckIcon className={`w-4 h-4 ml-auto ${className}`} strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for rendering the header of a table column,
|
||||
* as well as the dropdown menu for sorting and filtering.
|
||||
*/
|
||||
export function ColumnHeader<T>({ header }: { header: Header<T, unknown> }) {
|
||||
const { column } = header;
|
||||
|
||||
const [dropdownType, setDropdownType] = useState<"menu" | "filter" | null>(
|
||||
null
|
||||
);
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const isFiltered =
|
||||
column.getFilterValue() !== undefined &&
|
||||
column.getFilterValue() !== null &&
|
||||
column.getFilterValue() !== "";
|
||||
|
||||
const headerRef = useRef<HTMLTableCellElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const filterRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close the dropdown menu/filter input when clicking outside of it
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
const clickOutsideMenu =
|
||||
menuRef.current && !menuRef.current.contains(target);
|
||||
const clickOutsideFilter =
|
||||
filterRef.current && !filterRef.current.contains(target);
|
||||
|
||||
if (clickOutsideMenu || clickOutsideFilter) {
|
||||
setDropdownType(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleOutsideClick);
|
||||
};
|
||||
}, [dropdownType]);
|
||||
|
||||
// Set the sort direction based on the current state
|
||||
useEffect(() => {
|
||||
switch (sortDirection) {
|
||||
case "asc":
|
||||
column.toggleSorting(false);
|
||||
break;
|
||||
case "desc":
|
||||
column.toggleSorting(true);
|
||||
break;
|
||||
default:
|
||||
column.clearSorting();
|
||||
}
|
||||
}, [sortDirection, column]);
|
||||
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
className="border-gray-200 border-y font-medium"
|
||||
ref={headerRef}
|
||||
>
|
||||
<div>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div
|
||||
className="flex p-2 h-auto items-center justify-between px-2 relative cursor-pointer hover:bg-gray-200/50"
|
||||
onClick={() =>
|
||||
setDropdownType((prev) =>
|
||||
prev === null ? "menu" : null
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{flexRender(
|
||||
column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* Choose the icon based on sort direction */}
|
||||
{{
|
||||
asc: <ArrowUpIcon className="w-3 h-3 ml-1" />,
|
||||
desc: (
|
||||
<ArrowDownIcon className="w-3 h-3 ml-1" />
|
||||
),
|
||||
}[column.getIsSorted() as string] ?? null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{/* Dropdown menu to add sorting or filter */}
|
||||
{column.getCanFilter() && dropdownType === "menu" && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute flex flex-col justify-center items-center -top-2 left-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10"
|
||||
>
|
||||
<button
|
||||
className="flex items-center w-full text-left px-4 py-2 text-xs hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setSortDirection((prev) =>
|
||||
prev === "asc" ? null : "asc"
|
||||
);
|
||||
setDropdownType(null);
|
||||
}}
|
||||
>
|
||||
<ArrowUpIcon className="w-4 h-4 mr-2" />
|
||||
<span>Sort Ascending</span>
|
||||
{sortDirection === "asc" && <DropdownCheckIcon />}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center w-full text-left px-4 py-2 text-xs hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setSortDirection((prev) =>
|
||||
prev === "desc" ? null : "desc"
|
||||
);
|
||||
setDropdownType(null);
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon className="w-4 h-4 mr-2" />
|
||||
<span>Sort Descending</span>
|
||||
{sortDirection === "desc" && <DropdownCheckIcon />}
|
||||
</button>
|
||||
<hr className="w-40" />
|
||||
<button
|
||||
className="flex items-center w-full text-left px-4 py-2 text-xs hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
column.getCanFilter() &&
|
||||
column.setFilterValue("");
|
||||
setDropdownType(isFiltered ? null : "filter");
|
||||
}}
|
||||
>
|
||||
{isFiltered ? (
|
||||
<>
|
||||
<FunnelIcon className="`w-4 h-4 mr-2 text-red-400" />
|
||||
<span className="text-red-400">
|
||||
Clear Filter
|
||||
</span>
|
||||
<XMarkIcon className="w-4 h-4 ml-auto text-red-400" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FunnelIcon className="w-4 h-4 mr-2" />
|
||||
<span>Add Filter</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Dropdown menu to add a filter value */}
|
||||
{column.getCanFilter() && dropdownType === "filter" && (
|
||||
<div
|
||||
ref={filterRef}
|
||||
className="absolute -top-2 left-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10"
|
||||
>
|
||||
<div className="flex flex-col px-4 py-2">
|
||||
<span>Contains</span>
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
(column.getFilterValue() ?? "") as string
|
||||
}
|
||||
onChange={(e) => {
|
||||
column.setFilterValue(e.target.value);
|
||||
}}
|
||||
placeholder="Filter..."
|
||||
className="border border-gray-300 rounded p-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
|
@ -5,22 +5,19 @@ import {
|
|||
getCoreRowModel,
|
||||
flexRender,
|
||||
createColumnHelper,
|
||||
getFilteredRowModel,
|
||||
ColumnFiltersState,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
Key,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from "react";
|
||||
import { TableAction } from "./TableAction";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { ChangeEvent, useState, Dispatch, SetStateAction } from "react";
|
||||
import { TableSearch } from "@/components/Table/TableSearch";
|
||||
import { RowOptionMenu } from "@/components/Table/RowOptionMenu";
|
||||
import { ColumnHeader } from "@/components/Table/ColumnHeader";
|
||||
import CreateDrawer from "@/components/Drawer/CreateDrawer";
|
||||
import { Details } from "@/components/Drawer/Drawer";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
import CreateDrawer from "../Drawer/CreateDrawer";
|
||||
import { Details } from "../Drawer/Drawer";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
|
||||
type TableProps<T extends DataPoint> = {
|
||||
data: T[];
|
||||
|
@ -81,7 +78,8 @@ export default function Table<T extends DataPoint>({
|
|||
createEndpoint,
|
||||
isAdmin = false,
|
||||
}: TableProps<T>) {
|
||||
console.log(data);
|
||||
const [filters, setFilters] = useState<ColumnFiltersState>([]);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const columnHelper = createColumnHelper<T>();
|
||||
|
||||
|
@ -153,10 +151,6 @@ export default function Table<T extends DataPoint>({
|
|||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// Define Tanstack table
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
|
@ -166,39 +160,29 @@ export default function Table<T extends DataPoint>({
|
|||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
columnFilters: filters,
|
||||
sorting,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setFilters,
|
||||
onSortingChange: setSorting,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
<TableSearch query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
<ColumnHeader header={header} key={header.id} />
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
// TableAction.tsx
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
||||
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
|
||||
import { FilterBox } from "../FilterBox";
|
||||
|
||||
type TableActionProps = {
|
||||
type TableSearchProps = {
|
||||
query: string;
|
||||
handleChange: ChangeEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
export const TableAction: FunctionComponent<TableActionProps> = ({
|
||||
export const TableSearch: FunctionComponent<TableSearchProps> = ({
|
||||
query,
|
||||
handleChange,
|
||||
}) => {
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const [showFilterBox, setShowFilterBox] = useState(false);
|
||||
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
|
@ -25,29 +22,15 @@ export const TableAction: FunctionComponent<TableActionProps> = ({
|
|||
searchInput.current.addEventListener("focusout", () => {
|
||||
if (searchInput.current?.value.trim() === "") {
|
||||
searchInput.current.value = "";
|
||||
deactivateSearch();
|
||||
setSearchActive(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deactivateSearch = () => setSearchActive(false);
|
||||
|
||||
const toggleFilterBox = () => setShowFilterBox((prev) => !prev);
|
||||
|
||||
return (
|
||||
<div className="w-auto flex flex-row gap-x-0.5 items-center justify-between text-xs font-medium text-gray-500 p-2">
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50"
|
||||
onClick={toggleFilterBox}
|
||||
>
|
||||
Filter
|
||||
</span>
|
||||
{showFilterBox && <FilterBox />}
|
||||
<span className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100">
|
||||
Sort
|
||||
</span>
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100"
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 cursor-pointer"
|
||||
onClick={activateSearch}
|
||||
>
|
||||
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
Loading…
Reference in New Issue
Block a user