feat: add custom-fields

This commit is contained in:
Abhimanyu Saharan
2026-02-13 21:24:36 +05:30
parent b032e94ca1
commit 277bfcb33a
127 changed files with 11305 additions and 6643 deletions

View File

@@ -0,0 +1,205 @@
import { useMemo, useState } from "react";
import {
type ColumnDef,
type OnChangeFn,
type SortingState,
type Updater,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
DataTable,
type DataTableEmptyState,
} from "@/components/tables/DataTable";
import { dateCell } from "@/components/tables/cell-formatters";
import type { TaskCustomFieldDefinitionRead } from "@/api/generated/model";
type CustomFieldsTableProps = {
fields: TaskCustomFieldDefinitionRead[];
isLoading?: boolean;
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
stickyHeader?: boolean;
editHref?: (field: TaskCustomFieldDefinitionRead) => string;
onDelete?: (field: TaskCustomFieldDefinitionRead) => void;
emptyState?: Omit<DataTableEmptyState, "icon"> & {
icon?: DataTableEmptyState["icon"];
};
};
const DEFAULT_EMPTY_ICON = (
<svg
className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M8 6h13" />
<path d="M8 12h13" />
<path d="M8 18h13" />
<path d="M3 6h.01" />
<path d="M3 12h.01" />
<path d="M3 18h.01" />
</svg>
);
const formatDefaultValue = (value: unknown): string => {
if (value === null || value === undefined) return "";
if (typeof value === "string") return value;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
};
export function CustomFieldsTable({
fields,
isLoading = false,
sorting,
onSortingChange,
stickyHeader = false,
editHref,
onDelete,
emptyState,
}: CustomFieldsTableProps) {
const [internalSorting, setInternalSorting] = useState<SortingState>([
{ id: "field_key", desc: false },
]);
const resolvedSorting = sorting ?? internalSorting;
const handleSortingChange: OnChangeFn<SortingState> =
onSortingChange ??
((updater: Updater<SortingState>) => {
setInternalSorting(updater);
});
const columns = useMemo<ColumnDef<TaskCustomFieldDefinitionRead>[]>(
() => [
{
accessorKey: "field_key",
header: "Field",
cell: ({ row }) => (
<div>
<p className="text-sm font-semibold text-slate-900">
{row.original.label || row.original.field_key}
</p>
<p className="mt-1 font-mono text-xs text-slate-500">
key: {row.original.field_key}
</p>
<p className="mt-1 text-xs text-slate-500">
{row.original.description || "No description"}
</p>
</div>
),
},
{
accessorKey: "required",
header: "Required",
cell: ({ row }) => (
<span className="text-sm text-slate-700">
{row.original.required === true ? "Required" : "Optional"}
</span>
),
},
{
accessorKey: "field_type",
header: "Type",
cell: ({ row }) => (
<span className="text-sm text-slate-700">
{row.original.field_type}
</span>
),
},
{
accessorKey: "ui_visibility",
header: "UI visible",
cell: ({ row }) => (
<span className="text-sm text-slate-700">
{row.original.ui_visibility}
</span>
),
},
{
accessorKey: "default_value",
header: "Default value",
enableSorting: false,
cell: ({ row }) => (
<p className="font-mono text-xs break-all text-slate-700">
{formatDefaultValue(row.original.default_value) || "—"}
</p>
),
},
{
accessorKey: "updated_at",
header: "Updated",
cell: ({ row }) => dateCell(row.original.updated_at),
},
],
[],
);
// eslint-disable-next-line react-hooks/incompatible-library
const table = useReactTable({
data: fields,
columns,
state: {
sorting: resolvedSorting,
},
onSortingChange: handleSortingChange,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<DataTable
table={table}
isLoading={isLoading}
stickyHeader={stickyHeader}
rowClassName="transition hover:bg-slate-50"
cellClassName="px-6 py-4 align-top"
rowActions={
editHref || onDelete
? {
actions: [
...(editHref
? [
{
key: "edit",
label: "Edit",
href: editHref,
},
]
: []),
...(onDelete
? [
{
key: "delete",
label: "Delete",
onClick: onDelete,
},
]
: []),
],
}
: undefined
}
emptyState={
emptyState
? {
icon: emptyState.icon ?? DEFAULT_EMPTY_ICON,
title: emptyState.title,
description: emptyState.description,
actionHref: emptyState.actionHref,
actionLabel: emptyState.actionLabel,
}
: undefined
}
/>
);
}

View File

@@ -11,6 +11,7 @@ import {
Building2,
LayoutGrid,
Network,
Settings,
Tags,
} from "lucide-react";
@@ -146,6 +147,20 @@ export function DashboardSidebar() {
<CheckCircle2 className="h-4 w-4" />
Approvals
</Link>
{isAdmin ? (
<Link
href="/custom-fields"
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/custom-fields")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
>
<Settings className="h-4 w-4" />
Custom fields
</Link>
) : null}
</div>
</div>