mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 06:28:34 +01:00
* feat: implement example of data table * feat: load item data into table * chore: begin switching dialogs * feat: implement old dialog for controlling headers and page size * feat: get table into relatively usable state * feat: enhance dropdown actions for multi-selection and CSV download * feat: enhance table cell and dropdown button styles for better usability * feat: json download for table * feat: add expanded row component for item details in data table * chore: add translation support * feat: restore table on home page * fix: oops need ids * feat: move card view to use tanstack to allow for pagination * feat: switch the items search to use ItemViewSelectable * fix: update pagination handling and improve button click logic * feat: improve selectable table * feat: add indeterminate to checkbox * feat: overhaul maintenance dialog to use new system and add maintenance options to table * feat: add label ids and location id to item patch api * feat: change location and labels in table view * feat: add quick actions preference and enable toggle in table settings * fix: lint * fix: remove sized 1 pages * fix: attempt to fix type error * fix: various issues * fix: remove * fix: refactor item fetching logic to use useAsyncData for improved reactivity and improve use confirm * fix: sort backend issues * fix: enhance CSV export functionality by escaping fields to prevent formula injection * fix: put aria sort on th not button * chore: update api types
271 lines
8.3 KiB
TypeScript
271 lines
8.3 KiB
TypeScript
import type { Column, ColumnDef } from "@tanstack/vue-table";
|
|
import { h } from "vue";
|
|
import DropdownAction from "./data-table-dropdown.vue";
|
|
import { ArrowDown, ArrowUpDown, Check, X } from "lucide-vue-next";
|
|
import Button from "~/components/ui/button/Button.vue";
|
|
import Checkbox from "~/components/Form/Checkbox.vue";
|
|
import type { ItemSummary } from "~/lib/api/types/data-contracts";
|
|
import Currency from "~/components/global/Currency.vue";
|
|
import DateTime from "~/components/global/DateTime.vue";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
/**
|
|
* Create columns with i18n support.
|
|
* Pass `t` from useI18n() when creating the columns in your component.
|
|
*/
|
|
export function makeColumns(t: (key: string) => string, refresh?: () => void): ColumnDef<ItemSummary>[] {
|
|
const sortable = (column: Column<ItemSummary, unknown>, key: string) => {
|
|
const sortState = column.getIsSorted(); // 'asc' | 'desc' | false
|
|
if (!sortState) {
|
|
// show the neutral up/down icon when not sorted
|
|
return [t(key), h(ArrowUpDown, { class: "ml-2 h-4 w-4 opacity-40" })];
|
|
}
|
|
// show a single arrow that points up for asc (rotate-180) and down for desc
|
|
return [
|
|
t(key),
|
|
h(ArrowDown, {
|
|
class: cn(["ml-2 h-4 w-4 transition-transform opacity-100", sortState === "asc" ? "rotate-180" : ""]),
|
|
}),
|
|
];
|
|
};
|
|
|
|
return [
|
|
{
|
|
id: "select",
|
|
header: ({ table }) =>
|
|
h(Checkbox, {
|
|
modelValue: table.getIsAllPageRowsSelected()
|
|
? true
|
|
: table.getSelectedRowModel().rows.length > 0
|
|
? ("indeterminate" as unknown as boolean) // :)
|
|
: false,
|
|
"onUpdate:modelValue": (value: boolean) => table.toggleAllPageRowsSelected(!!value),
|
|
ariaLabel: t("components.item.view.selectable.select_all"),
|
|
}),
|
|
cell: ({ row }) =>
|
|
h(Checkbox, {
|
|
modelValue: row.getIsSelected(),
|
|
"onUpdate:modelValue": (value: boolean) => row.toggleSelected(!!value),
|
|
ariaLabel: t("components.item.view.selectable.select_row"),
|
|
}),
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
id: "assetId",
|
|
accessorKey: "assetId",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.asset_id")
|
|
),
|
|
cell: ({ row }) => h("div", { class: "text-sm" }, String(row.getValue("assetId") ?? "")),
|
|
},
|
|
{
|
|
id: "name",
|
|
accessorKey: "name",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.name")
|
|
),
|
|
cell: ({ row }) => h("span", { class: "text-sm font-medium" }, row.getValue("name")),
|
|
},
|
|
{
|
|
id: "quantity",
|
|
accessorKey: "quantity",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.quantity")
|
|
),
|
|
cell: ({ row }) => h("div", { class: "text-center" }, String(row.getValue("quantity") ?? "")),
|
|
},
|
|
{
|
|
id: "insured",
|
|
accessorKey: "insured",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.insured")
|
|
),
|
|
cell: ({ row }) => {
|
|
const val = row.getValue("insured");
|
|
return h(
|
|
"div",
|
|
{ class: "block mx-auto w-min" },
|
|
val ? h(Check, { class: "h-4 w-4 text-green-500" }) : h(X, { class: "h-4 w-4 text-destructive" })
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "purchasePrice",
|
|
accessorKey: "purchasePrice",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.purchase_price")
|
|
),
|
|
cell: ({ row }) =>
|
|
h("div", { class: "text-center" }, h(Currency, { amount: Number(row.getValue("purchasePrice")) })),
|
|
},
|
|
{
|
|
id: "location",
|
|
accessorKey: "location",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.location")
|
|
),
|
|
cell: ({ row }) => {
|
|
const loc = (row.original as ItemSummary).location as { id: string; name: string } | null;
|
|
if (loc) {
|
|
return h("NuxtLink", { to: `/location/${loc.id}`, class: "hover:underline text-sm" }, () => loc.name);
|
|
}
|
|
return h("div", { class: "text-sm text-muted-foreground" }, "");
|
|
},
|
|
},
|
|
{
|
|
id: "archived",
|
|
accessorKey: "archived",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.archived")
|
|
),
|
|
cell: ({ row }) => {
|
|
const val = row.getValue("archived");
|
|
return h(
|
|
"div",
|
|
{ class: "block mx-auto w-min" },
|
|
val ? h(Check, { class: "h-4 w-4 text-green-500" }) : h(X, { class: "h-4 w-4 text-destructive" })
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "createdAt",
|
|
accessorKey: "createdAt",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.created_at")
|
|
),
|
|
cell: ({ row }) =>
|
|
h(
|
|
"div",
|
|
{ class: "text-center text-sm" },
|
|
h(DateTime, { date: row.getValue("createdAt") as Date, datetimeType: "date" })
|
|
),
|
|
},
|
|
{
|
|
id: "updatedAt",
|
|
accessorKey: "updatedAt",
|
|
header: ({ column }) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: "ghost",
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === "asc"),
|
|
},
|
|
() => sortable(column, "items.updated_at")
|
|
),
|
|
cell: ({ row }) =>
|
|
h(
|
|
"div",
|
|
{ class: "text-center text-sm" },
|
|
h(DateTime, { date: row.getValue("updatedAt") as Date, datetimeType: "date" })
|
|
),
|
|
},
|
|
{
|
|
id: "actions",
|
|
enableHiding: false,
|
|
header: ({ table }) => {
|
|
const selectedCount = table.getSelectedRowModel().rows.length;
|
|
return h(
|
|
"div",
|
|
{
|
|
class: [
|
|
"relative inline-flex items-center",
|
|
selectedCount === 0 ? "opacity-50 pointer-events-none" : "",
|
|
].join(" "),
|
|
},
|
|
[
|
|
h(DropdownAction, {
|
|
multi: {
|
|
items: table.getSelectedRowModel().rows,
|
|
columns: table.getAllColumns(),
|
|
},
|
|
onExpand: () => {
|
|
table.getSelectedRowModel().rows.forEach(row => row.toggleExpanded());
|
|
},
|
|
view: "table",
|
|
onRefresh: () => refresh?.(),
|
|
table,
|
|
}),
|
|
selectedCount > 0 &&
|
|
h(
|
|
"span",
|
|
{
|
|
class: "-right-1 -top-1 absolute flex size-4",
|
|
},
|
|
h(
|
|
"span",
|
|
{
|
|
class:
|
|
"relative flex size-4 items-center justify-center rounded-full bg-primary p-1 text-primary-foreground text-xs pointer-events-none whitespace-nowrap",
|
|
},
|
|
String(selectedCount)
|
|
)
|
|
),
|
|
]
|
|
);
|
|
},
|
|
cell: ({ row, table }) => {
|
|
const item = row.original;
|
|
return h(
|
|
"div",
|
|
{ class: "relative" },
|
|
h(DropdownAction, {
|
|
item,
|
|
onExpand: row.toggleExpanded,
|
|
view: "table",
|
|
onRefresh: () => refresh?.(),
|
|
table,
|
|
})
|
|
);
|
|
},
|
|
},
|
|
];
|
|
}
|