diff --git a/package.json b/package.json
index da0ed1d..7ef3bc4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@schemavaults/ui",
- "version": "0.13.15",
+ "version": "0.14.0",
"private": false,
"license": "UNLICENSED",
"description": "React.js UI components for SchemaVaults frontend applications",
diff --git a/src/components/ui/datatable/Datatable.stories.tsx b/src/components/ui/datatable/Datatable.stories.tsx
index 5743c7d..b9ea062 100644
--- a/src/components/ui/datatable/Datatable.stories.tsx
+++ b/src/components/ui/datatable/Datatable.stories.tsx
@@ -198,3 +198,40 @@ export const CustomPageSize: Story = {
export const LargeDefaultPageSize: Story = {
render: (): ReactElement => ,
};
+
+function SortableColumnsDemo(): ReactElement {
+ return (
+
+ );
+}
+
+function DefaultSortDemo(): ReactElement {
+ return (
+
+ );
+}
+
+export const SortableColumns: Story = {
+ render: (): ReactElement => ,
+};
+
+export const DefaultSort: Story = {
+ render: (): ReactElement => ,
+};
diff --git a/src/components/ui/datatable/datatable.tsx b/src/components/ui/datatable/datatable.tsx
index a6cfbf8..114c611 100644
--- a/src/components/ui/datatable/datatable.tsx
+++ b/src/components/ui/datatable/datatable.tsx
@@ -15,6 +15,9 @@ import {
type RowSelectionState,
} from "@tanstack/react-table";
import {
+ ArrowDown,
+ ArrowUp,
+ ArrowUpDown,
ChevronDown,
ChevronLeft,
ChevronRight,
@@ -45,7 +48,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
-import { type ReactElement, useState, type FC } from "react";
+import { type ReactElement, useMemo, useState, type FC } from "react";
export type { ColumnDef };
@@ -72,6 +75,10 @@ export interface DatatableProps {
defaultPageSize?: number;
/** Options shown in the rows-per-page selector. Defaults to [10, 20, 50, 100]. */
pageSizeOptions?: number[];
+ /** Column IDs that should be sortable via clickable headers. If omitted, no columns are sortable. */
+ sortableColumns?: string[];
+ /** Initial sort state applied on mount. The column must also be listed in sortableColumns. */
+ defaultSort?: { id: string; desc: boolean };
}
export function Datatable({
@@ -84,8 +91,12 @@ export function Datatable({
HeaderButtons,
defaultPageSize = 10,
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS as unknown as number[],
+ sortableColumns,
+ defaultSort,
}: DatatableProps): ReactElement {
- const [sorting, setSorting] = useState([]);
+ const [sorting, setSorting] = useState(
+ defaultSort ? [defaultSort] : [],
+ );
const [columnFilters, setColumnFilters] = useState([]);
const [globalFilter, setGlobalFilter] = useState("");
const [columnVisibility, setColumnVisibility] = useState(
@@ -118,9 +129,30 @@ export function Datatable({
});
};
+ // Set of column IDs that are sortable, for O(1) lookups.
+ const sortableSet = useMemo>(
+ () => new Set(sortableColumns ?? []),
+ [sortableColumns],
+ );
+
+ // Override enableSorting per column so only sortableColumns are interactive.
+ const columnsWithSorting = useMemo[]>(
+ () =>
+ columns.map((col) => {
+ const colId =
+ (col as { accessorKey?: string }).accessorKey ??
+ (col as { id?: string }).id;
+ return {
+ ...col,
+ enableSorting: colId ? sortableSet.has(colId) : false,
+ };
+ }),
+ [columns, sortableSet],
+ );
+
const table = useReactTable({
data,
- columns: columns satisfies ColumnDef[],
+ columns: columnsWithSorting satisfies ColumnDef[],
initialState: {
pagination: {
pageSize: defaultPageSize,
@@ -221,12 +253,34 @@ export function Datatable({
{headerGroup.headers.map((header) => {
return (
- {header.isPlaceholder
- ? null
- : flexRender(
+ {header.isPlaceholder ? null : header.column.getCanSort() ? (
+
+ ) : (
+ flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )
+ )}
);
})}