mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-20 10:30:16 -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,
|
getCoreRowModel,
|
||||||
flexRender,
|
flexRender,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
|
getFilteredRowModel,
|
||||||
|
ColumnFiltersState,
|
||||||
|
getSortedRowModel,
|
||||||
|
SortingState,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import { ChangeEvent, useState, Dispatch, SetStateAction } from "react";
|
||||||
ChangeEvent,
|
import { TableSearch } from "@/components/Table/TableSearch";
|
||||||
useState,
|
import { RowOptionMenu } from "@/components/Table/RowOptionMenu";
|
||||||
useEffect,
|
import { ColumnHeader } from "@/components/Table/ColumnHeader";
|
||||||
Key,
|
import CreateDrawer from "@/components/Drawer/CreateDrawer";
|
||||||
Dispatch,
|
import { Details } from "@/components/Drawer/Drawer";
|
||||||
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 DataPoint from "@/utils/models/DataPoint";
|
import DataPoint from "@/utils/models/DataPoint";
|
||||||
import CreateDrawer from "../Drawer/CreateDrawer";
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
import { Details } from "../Drawer/Drawer";
|
|
||||||
|
|
||||||
type TableProps<T extends DataPoint> = {
|
type TableProps<T extends DataPoint> = {
|
||||||
data: T[];
|
data: T[];
|
||||||
|
@ -81,7 +78,8 @@ export default function Table<T extends DataPoint>({
|
||||||
createEndpoint,
|
createEndpoint,
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
}: TableProps<T>) {
|
}: TableProps<T>) {
|
||||||
console.log(data);
|
const [filters, setFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<T>();
|
const columnHelper = createColumnHelper<T>();
|
||||||
|
|
||||||
|
@ -153,10 +151,6 @@ export default function Table<T extends DataPoint>({
|
||||||
setQuery(String(target.value));
|
setQuery(String(target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Filtering
|
|
||||||
|
|
||||||
// TODO: Sorting
|
|
||||||
|
|
||||||
// Define Tanstack table
|
// Define Tanstack table
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
columns,
|
columns,
|
||||||
|
@ -166,39 +160,29 @@ export default function Table<T extends DataPoint>({
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
globalFilter: query,
|
globalFilter: query,
|
||||||
|
columnFilters: filters,
|
||||||
|
sorting,
|
||||||
},
|
},
|
||||||
onGlobalFilterChange: setQuery,
|
onGlobalFilterChange: setQuery,
|
||||||
globalFilterFn: fuzzyFilter,
|
globalFilterFn: fuzzyFilter,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onColumnFiltersChange: setFilters,
|
||||||
|
onSortingChange: setSorting,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
<TableAction query={query} handleChange={handleSearchChange} />
|
<TableSearch query={query} handleChange={handleSearchChange} />
|
||||||
</div>
|
</div>
|
||||||
<table className="w-full text-xs text-left rtl:text-right">
|
<table className="w-full text-xs text-left rtl:text-right">
|
||||||
<thead className="text-xs text-gray-500 capitalize">
|
<thead className="text-xs text-gray-500 capitalize">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header, i) => (
|
{headerGroup.headers.map((header, i) => (
|
||||||
<th
|
<ColumnHeader header={header} key={header.id} />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
// TableAction.tsx
|
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
||||||
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
|
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
|
||||||
import { FilterBox } from "../FilterBox";
|
|
||||||
|
|
||||||
type TableActionProps = {
|
type TableSearchProps = {
|
||||||
query: string;
|
query: string;
|
||||||
handleChange: ChangeEventHandler<HTMLInputElement>;
|
handleChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableAction: FunctionComponent<TableActionProps> = ({
|
export const TableSearch: FunctionComponent<TableSearchProps> = ({
|
||||||
query,
|
query,
|
||||||
handleChange,
|
handleChange,
|
||||||
}) => {
|
}) => {
|
||||||
const searchInput = useRef<HTMLInputElement>(null);
|
const searchInput = useRef<HTMLInputElement>(null);
|
||||||
const [searchActive, setSearchActive] = useState(false);
|
const [searchActive, setSearchActive] = useState(false);
|
||||||
const [showFilterBox, setShowFilterBox] = useState(false);
|
|
||||||
|
|
||||||
const activateSearch = () => {
|
const activateSearch = () => {
|
||||||
setSearchActive(true);
|
setSearchActive(true);
|
||||||
|
@ -25,29 +22,15 @@ export const TableAction: FunctionComponent<TableActionProps> = ({
|
||||||
searchInput.current.addEventListener("focusout", () => {
|
searchInput.current.addEventListener("focusout", () => {
|
||||||
if (searchInput.current?.value.trim() === "") {
|
if (searchInput.current?.value.trim() === "") {
|
||||||
searchInput.current.value = "";
|
searchInput.current.value = "";
|
||||||
deactivateSearch();
|
setSearchActive(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deactivateSearch = () => setSearchActive(false);
|
|
||||||
|
|
||||||
const toggleFilterBox = () => setShowFilterBox((prev) => !prev);
|
|
||||||
|
|
||||||
return (
|
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">
|
<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
|
<span
|
||||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50"
|
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 cursor-pointer"
|
||||||
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"
|
|
||||||
onClick={activateSearch}
|
onClick={activateSearch}
|
||||||
>
|
>
|
||||||
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
Loading…
Reference in New Issue
Block a user