mirror of
https://github.com/cssgunc/compass.git
synced 2025-05-20 07:09:49 -04:00
Merge 81d3a4b7aa
into 469236cb04
This commit is contained in:
commit
a057524b20
206
compass/components/Table/ColumnHeader.tsx
Normal file
206
compass/components/Table/ColumnHeader.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { flexRender, Header } from "@tanstack/react-table";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Details } from "../Drawer/Drawer";
|
||||
import FilterDropdown, { FilterFn } from "./FilterDropdown";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
|
||||
interface ColumnHeaderProps<T extends DataPoint> {
|
||||
header: Header<T, any>;
|
||||
details: Details | undefined;
|
||||
setFilterFn?: (field: string, filterFn: FilterFn) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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.
|
||||
* @param props.header The header object from TanStack Table.
|
||||
* @param props.details The details object containing metadata about the column.
|
||||
* @param props.setFilterFn Include this state setter if the column has multiple filter options.
|
||||
* @param props.className Optional additional class names for styling.
|
||||
*/
|
||||
export function ColumnHeader<T extends DataPoint>({
|
||||
header,
|
||||
details,
|
||||
setFilterFn,
|
||||
className
|
||||
}: ColumnHeaderProps<T>) {
|
||||
const { column } = header;
|
||||
|
||||
const [dropdownType, setDropdownType] = useState<"menu" | "filter" | null>(
|
||||
null
|
||||
);
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const isFiltered =
|
||||
column.getFilterValue() != null && column.getFilterValue() !== "";
|
||||
|
||||
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]);
|
||||
|
||||
if (!details) {
|
||||
return <div className="border-gray-200 border-y" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
className={`border-gray-200 border-y font-medium ${
|
||||
isFiltered ? "bg-purple-50" : ""
|
||||
} ${className ?? ""}`}
|
||||
>
|
||||
<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 ${
|
||||
isFiltered ? "" : ""
|
||||
}`}
|
||||
>
|
||||
{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 "asc" | "desc"]
|
||||
}
|
||||
</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 ${
|
||||
isFiltered ? "text-red-400" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
column.getCanFilter() &&
|
||||
column.setFilterValue("");
|
||||
setDropdownType(isFiltered ? null : "filter");
|
||||
}}
|
||||
>
|
||||
<FunnelIcon className="w-4 h-4 mr-2" />
|
||||
{isFiltered ? (
|
||||
<>
|
||||
<span>Clear Filter</span>
|
||||
<XMarkIcon className="w-4 h-4 ml-auto" />
|
||||
</>
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
<FilterDropdown
|
||||
column={column}
|
||||
details={details}
|
||||
setFilterFn={setFilterFn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
91
compass/components/Table/FilterDropdown.tsx
Normal file
91
compass/components/Table/FilterDropdown.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import DataPoint from "@/utils/models/DataPoint";
|
||||
import { Column } from "@tanstack/react-table";
|
||||
import { Details } from "../Drawer/Drawer";
|
||||
import { useEffect, useState } from "react";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
export type FilterFn = "arrIncludesSome" | "arrIncludesAll";
|
||||
|
||||
interface FilterDropdownProps<T extends DataPoint> {
|
||||
details: Details;
|
||||
column: Column<T, any>;
|
||||
setFilterFn?: (field: string, filterFn: FilterFn) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for rendering a dropdown menu when adding a filter to a column.
|
||||
* @param props.details The details object containing metadata about the column.
|
||||
* @param props.column The column object from TanStack Table.
|
||||
* @param props.setFilterFn Include this state setter if the column has multiple filter options.
|
||||
*/
|
||||
export default function FilterDropdown<T extends DataPoint>({
|
||||
details,
|
||||
column,
|
||||
setFilterFn,
|
||||
}: FilterDropdownProps<T>) {
|
||||
const filterState = useState<FilterFn | null>(
|
||||
details.inputType === "select-multiple" ||
|
||||
details.inputType === "select-one"
|
||||
? "arrIncludesSome"
|
||||
: null
|
||||
);
|
||||
const [filter] = filterState;
|
||||
const { inputType, presetOptionsValues, presetOptionsSetter } = details;
|
||||
|
||||
// Update the column filter function when the state changes
|
||||
useEffect(() => {
|
||||
if (filter && setFilterFn) {
|
||||
setFilterFn(details.key, filter);
|
||||
column.setFilterValue((prev: any) => prev); // Trigger a re-render based on new filter value
|
||||
}
|
||||
}, [details.key, filter, setFilterFn, column]);
|
||||
|
||||
switch (inputType) {
|
||||
case "select-one":
|
||||
return (
|
||||
<div className="absolute -top-5 -left-1">
|
||||
<TagsInput
|
||||
presetOptions={presetOptionsValues ?? []}
|
||||
setPresetOptions={presetOptionsSetter ?? (() => {})}
|
||||
presetValue={[]}
|
||||
onTagsChange={(tags) => {
|
||||
const tagsArray = Array.from(tags);
|
||||
column.setFilterValue(tagsArray);
|
||||
}}
|
||||
cellSelectedPreset={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "select-multiple":
|
||||
return (
|
||||
<div className="absolute -top-5 -left-1">
|
||||
<TagsInput
|
||||
presetOptions={presetOptionsValues ?? []}
|
||||
setPresetOptions={presetOptionsSetter ?? (() => {})}
|
||||
presetValue={[]}
|
||||
onTagsChange={(tags) => {
|
||||
const tagsArray = Array.from(tags);
|
||||
column.setFilterValue(tagsArray);
|
||||
}}
|
||||
cellSelectedPreset={true}
|
||||
filterState={filterState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<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="Type a value…"
|
||||
className="border border-gray-300 rounded p-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
Bars2Icon,
|
||||
CheckCircleIcon,
|
||||
DocumentTextIcon,
|
||||
ListBulletIcon,
|
||||
|
@ -13,6 +12,7 @@ import Service from "@/utils/models/Service";
|
|||
import { Details } from "../Drawer/Drawer";
|
||||
import { Tag } from "../TagsInput/Tag";
|
||||
import User from "@/utils/models/User";
|
||||
import { FilterFn } from "./FilterDropdown";
|
||||
|
||||
type ServiceTableProps = {
|
||||
data: Service[];
|
||||
|
@ -31,6 +31,8 @@ export default function ServiceTable({
|
|||
user,
|
||||
}: ServiceTableProps) {
|
||||
const columnHelper = createColumnHelper<Service>();
|
||||
const [requirementsFilterFn, setRequirementsFilterFn] =
|
||||
useState<FilterFn>("arrIncludesSome");
|
||||
|
||||
const [programPresets, setProgramPresets] = useState([
|
||||
"DOMESTIC",
|
||||
|
@ -151,6 +153,14 @@ export default function ServiceTable({
|
|||
</Tag>
|
||||
</div>
|
||||
),
|
||||
// Filter by if the value is in the tags array
|
||||
filterFn: (row, columnId, filterValue) => {
|
||||
const rowValue = row.getValue(columnId);
|
||||
if (Array.isArray(filterValue)) {
|
||||
return filterValue.includes(rowValue);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("requirements", {
|
||||
header: () => (
|
||||
|
@ -170,6 +180,7 @@ export default function ServiceTable({
|
|||
)}
|
||||
</div>
|
||||
),
|
||||
filterFn: requirementsFilterFn,
|
||||
}),
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
|
@ -188,11 +199,18 @@ export default function ServiceTable({
|
|||
}),
|
||||
];
|
||||
|
||||
const setFilterFn = (field: string, filterFn: FilterFn) => {
|
||||
if (field === "requirements") {
|
||||
setRequirementsFilterFn(filterFn);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={data}
|
||||
setData={setData}
|
||||
columns={columns}
|
||||
setFilterFn={setFilterFn}
|
||||
details={serviceDetails}
|
||||
createEndpoint={`/api/service/create?uuid=${user?.uuid}`}
|
||||
deleteEndpoint={`/api/service/delete?uuid=${user?.uuid}`}
|
||||
|
|
|
@ -5,27 +5,26 @@ 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";
|
||||
import { FilterFn } from "./FilterDropdown";
|
||||
|
||||
type TableProps<T extends DataPoint> = {
|
||||
data: T[];
|
||||
setData: Dispatch<SetStateAction<T[]>>;
|
||||
columns: ColumnDef<T, any>[];
|
||||
setFilterFn?: (field: string, filterFn: FilterFn) => void;
|
||||
details: Details[];
|
||||
createEndpoint: string;
|
||||
deleteEndpoint: string;
|
||||
|
@ -73,16 +72,21 @@ const fuzzyFilter = (
|
|||
* @param props.data Stateful list of data to be held in the table
|
||||
* @param props.setData State setter for the list of data
|
||||
* @param props.columns Column definitions made with Tanstack columnHelper
|
||||
* @param props.setFilterFn This optional state setter should change the filter funciton of the provided column if possible.
|
||||
* It should be included if the column has multiple filter options.
|
||||
*/
|
||||
export default function Table<T extends DataPoint>({
|
||||
data,
|
||||
setData,
|
||||
columns,
|
||||
setFilterFn,
|
||||
details,
|
||||
createEndpoint,
|
||||
deleteEndpoint,
|
||||
isAdmin = false,
|
||||
}: TableProps<T>) {
|
||||
const [filters, setFilters] = useState<ColumnFiltersState>([]);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const offset = isAdmin ? 1 : 0;
|
||||
|
||||
const columnHelper = createColumnHelper<T>();
|
||||
|
@ -114,20 +118,6 @@ export default function Table<T extends DataPoint>({
|
|||
const visibilitySort = (a: T, b: T) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1;
|
||||
|
||||
// // Sort data on load
|
||||
// useEffect(() => {
|
||||
// setData((prevData) => prevData.sort(visibilitySort));
|
||||
// }, [setData]);
|
||||
|
||||
// // Data manipulation methods
|
||||
// // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
||||
// const deleteData = (dataId: number) => {
|
||||
// console.log(data);
|
||||
// setData((currentData) =>
|
||||
// currentData.filter((data) => data.id !== dataId)
|
||||
// );
|
||||
// };
|
||||
|
||||
const hideData = (dataId: number) => {
|
||||
console.log(`Toggling visibility for data with ID: ${dataId}`);
|
||||
setData((currentData) => {
|
||||
|
@ -189,10 +179,6 @@ export default function Table<T extends DataPoint>({
|
|||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// Define Tanstack table
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
|
@ -202,39 +188,41 @@ 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"
|
||||
<ColumnHeader
|
||||
header={header}
|
||||
details={details.find(
|
||||
(d) => d.key === header.column.id
|
||||
)}
|
||||
setFilterFn={setFilterFn}
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(0 + offset < i && i < columns.length - 1
|
||||
offset < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
: ""
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
@ -243,16 +231,21 @@ export default function Table<T extends DataPoint>({
|
|||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isDataVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${!isDataVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isDataVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
`[&:nth-child(n+${2 + offset})]:border-x relative first:text-left first:px-0 last:border-none`
|
||||
}
|
||||
className={`[&:nth-child(n+${
|
||||
2 + offset
|
||||
})]:border-x px-2 relative first:text-left first:px-0 last:border-none ${
|
||||
cell.column.getIsFiltered()
|
||||
? "bg-purple-50"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
|
|
|
@ -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" />
|
|
@ -10,6 +10,7 @@ import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
|||
import User from "@/utils/models/User";
|
||||
import { Details } from "../Drawer/Drawer";
|
||||
import { Tag } from "../TagsInput/Tag";
|
||||
import { FilterFn } from "./FilterDropdown";
|
||||
|
||||
type UserTableProps = {
|
||||
data: User[];
|
||||
|
@ -24,6 +25,8 @@ type UserTableProps = {
|
|||
*/
|
||||
export default function UserTable({ data, setData, user }: UserTableProps) {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const [programFilterFn, setProgramFilterFn] =
|
||||
useState<FilterFn>("arrIncludesSome");
|
||||
|
||||
const [rolePresets, setRolePresets] = useState([
|
||||
"ADMIN",
|
||||
|
@ -103,6 +106,7 @@ export default function UserTable({ data, setData, user }: UserTableProps) {
|
|||
</Tag>
|
||||
</div>
|
||||
),
|
||||
filterFn: "arrIncludesSome",
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: () => (
|
||||
|
@ -133,14 +137,22 @@ export default function UserTable({ data, setData, user }: UserTableProps) {
|
|||
)}
|
||||
</div>
|
||||
),
|
||||
filterFn: programFilterFn,
|
||||
}),
|
||||
];
|
||||
|
||||
const setFilterFn = (field: string, filterFn: FilterFn) => {
|
||||
if (field === "program") {
|
||||
setProgramFilterFn(filterFn);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table<User>
|
||||
<Table
|
||||
data={data}
|
||||
setData={setData}
|
||||
columns={columns}
|
||||
setFilterFn={setFilterFn}
|
||||
details={userDetails}
|
||||
createEndpoint={`/api/user/create?uuid=${user?.uuid}`}
|
||||
deleteEndpoint={`/api/user/delete?uuid=${user?.uuid}`}
|
||||
|
|
|
@ -3,6 +3,7 @@ import "tailwindcss/tailwind.css";
|
|||
import { TagsArray } from "./TagsArray";
|
||||
import { TagDropdown } from "./TagDropdown";
|
||||
import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||
import { FilterFn } from "../Table/FilterDropdown";
|
||||
|
||||
interface TagsInputProps {
|
||||
presetOptions: string[];
|
||||
|
@ -10,6 +11,8 @@ interface TagsInputProps {
|
|||
setPresetOptions: Dispatch<SetStateAction<string[]>>;
|
||||
onTagsChange?: (tags: Set<string>) => void;
|
||||
singleValue?: boolean;
|
||||
cellSelectedPreset?: boolean;
|
||||
filterState?: [FilterFn | null, Dispatch<SetStateAction<FilterFn | null>>];
|
||||
}
|
||||
|
||||
const TagsInput: React.FC<TagsInputProps> = ({
|
||||
|
@ -18,9 +21,11 @@ const TagsInput: React.FC<TagsInputProps> = ({
|
|||
setPresetOptions,
|
||||
onTagsChange,
|
||||
singleValue = false,
|
||||
cellSelectedPreset = false,
|
||||
filterState,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [cellSelected, setCellSelected] = useState(false);
|
||||
const [cellSelected, setCellSelected] = useState(cellSelectedPreset);
|
||||
|
||||
// TODO: Add tags to the database and remove the presetValue and lowercasing
|
||||
const [tags, setTags] = useState<Set<string>>(new Set(presetValue));
|
||||
|
@ -138,19 +143,42 @@ const TagsInput: React.FC<TagsInputProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const FilterSelect = () => {
|
||||
const [filter, setFilter] = filterState ?? [null, null];
|
||||
return (
|
||||
filter != null &&
|
||||
setFilter != null && (
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value as FilterFn);
|
||||
}}
|
||||
className="cursor-pointer bg-inherit rounded py-0 px-1"
|
||||
>
|
||||
<option value="arrIncludesSome">Includes</option>
|
||||
<option value="arrIncludesAll">Includes All</option>
|
||||
</select>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer" onClick={handleClick}>
|
||||
{!cellSelected ? (
|
||||
<TagsArray
|
||||
active={true}
|
||||
handleDelete={handleDeleteTag}
|
||||
tags={tags}
|
||||
/>
|
||||
<>
|
||||
<FilterSelect />
|
||||
<TagsArray
|
||||
active={true}
|
||||
handleDelete={handleDeleteTag}
|
||||
tags={tags}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div ref={dropdown}>
|
||||
<div className="absolute w-64 z-50 ml-1 mt-5">
|
||||
<div className="rounded-md border border-gray-200 shadow">
|
||||
<div className="flex flex-wrap rounded-t-md items-center gap-2 bg-gray-50 p-2">
|
||||
<div className="flex flex-col flex-wrap rounded-t-md items-start justify-center gap-2 bg-gray-50 p-2">
|
||||
<FilterSelect />
|
||||
<TagsArray
|
||||
handleDelete={handleDeleteTag}
|
||||
active
|
||||
|
|
16
compass/package-lock.json
generated
16
compass/package-lock.json
generated
|
@ -13,6 +13,7 @@
|
|||
"@supabase/supabase-js": "^2.42.3",
|
||||
"@tanstack/match-sorter-utils": "^8.15.1",
|
||||
"@tanstack/react-table": "^8.15.0",
|
||||
"@tanstack/table-core": "^8.21.3",
|
||||
"bufferutil": "^4.0.8",
|
||||
"next": "^13.5.8",
|
||||
"react": "^18",
|
||||
|
@ -655,7 +656,7 @@
|
|||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"node_modules/@tanstack/react-table/node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
|
||||
"integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
|
||||
|
@ -668,6 +669,19 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"@supabase/supabase-js": "^2.42.3",
|
||||
"@tanstack/match-sorter-utils": "^8.15.1",
|
||||
"@tanstack/react-table": "^8.15.0",
|
||||
"@tanstack/table-core": "^8.21.3",
|
||||
"bufferutil": "^4.0.8",
|
||||
"next": "^13.5.8",
|
||||
"react": "^18",
|
||||
|
|
Loading…
Reference in New Issue
Block a user