ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:07:20 +08:00
parent 56d984a26a
commit fbef7cb6e6
283 changed files with 25961 additions and 1072 deletions
+89 -4
View File
@@ -1,3 +1,28 @@
/***********************************************************************************************************************************************************************
* File Name : table.util.jsx
* Type : Utility
* Description : Shared helpers for the generic admin DataTable system.
* Provides:
* - pageSizes constant for the page-size selector
* - BADGE_STYLES + EnumBadge for enum-type column rendering
* - formatDate (date-only, "MMM d, yyyy") used as the default
* renderer for "date"-type attributes app-wide
* - renderCell, the default per-type cell renderer dispatched
* by buildColumns when no cellOverride is supplied
* - ColumnFilter, the per-column filter UI (enum/date/text)
* - SortIcon, the header sort indicator
* - longTextCell, a reusable cellOverride factory for long
* values (UUIDs, free text notes, etc.) — truncates the
* value and shows an "Eye · View" trigger that opens a
* shadcn Dialog with the full value
* - buildColumns, which turns server-provided attribute
* metadata into TanStack column definitions
*
* Author: rgrgogu
* Modified by: Kenneth Obsequio (@lash0000)
* Date Created: May 6, 2026
* Date Modified: Jun 17, 2026
***********************************************************************************************************************************************************************/
import { useState, useEffect } from "react";
import { createColumnHelper } from "@tanstack/react-table";
@@ -6,7 +31,10 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
import { Search } from "lucide-react";
import {
Dialog, DialogContent, DialogHeader, DialogTitle,
} from "@/components/ui/dialog";
import { Search, ArrowUp, ArrowDown, ArrowUpDown, Eye } from "lucide-react";
export const pageSizes = [10, 25, 50, 75, 100, 250, 500, 750, 1000]
@@ -106,7 +134,7 @@ export function ColumnFilter({ column, attr }) {
onValueChange={(v) => {
const value = v === "__all__" ? "" : v;
setInputValue(value);
column.setFilterValue(value); // ✅ auto apply
column.setFilterValue(value);
}}
>
<SelectTrigger className="h-7 text-xs w-full">
@@ -254,6 +282,64 @@ export function SortIcon({ column }) {
return <ArrowUpDown className="ml-1 h-3 w-3 text-muted-foreground/40 shrink-0" />;
}
// ── Long text cell — truncate + "Eye · View" dialog trigger ────────────────────
//
// Generic, reusable across any table/column. Any value longer than the
// threshold renders truncated with a small "View" indicator; clicking opens
// a shadcn Dialog showing the full value. Values at or under the threshold
// render plainly, same as the default text cell.
//
// Usage in any columns.config.jsx:
// import { longTextCell } from "@/utils/table.util";
// const cellOverrides = {
// completion_id: longTextCell("Completion ID"),
// note: longTextCell("Note"),
// };
const LONG_TEXT_THRESHOLD = 24; // characters — like UUIDs are 36, short notes may be under this
function LongTextCell({ value, label = "Full value" }) {
const [open, setOpen] = useState(false);
if (value === null || value === undefined || value === "") {
return <span className="text-muted-foreground/40">-</span>;
}
const str = String(value);
if (str.length <= LONG_TEXT_THRESHOLD) {
return <span className="text-sm">{str}</span>;
}
return (
<>
<Button
type="button"
onClick={() => setOpen(true)}
variant="ghost"
size="sm"
>
<div className="truncate max-w-[160px]">{str}</div>
<div className="inline-flex items-center gap-1 text-xs text-muted-foreground shrink-0">
<Eye className="size-3.5" /> View
</div>
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{label}</DialogTitle>
</DialogHeader>
<p className="text-sm break-all bg-muted rounded-md p-3">{str}</p>
</DialogContent>
</Dialog>
</>
);
}
export function longTextCell(label) {
return (info) => <LongTextCell value={info.getValue()} label={label} />;
}
// ── Build columns dynamically from attributes ──────────────────────────────────
const columnHelper = createColumnHelper();
@@ -276,5 +362,4 @@ export function buildColumns(attributes, { cellOverrides = {} } = {}) {
meta: { attr },
})
);
}
}
+19
View File
@@ -12,6 +12,25 @@ export function getTimestamp() {
return `${YYYY}${MM}${DD}_${HH}${mm}${SS}`;
}
export function timeAgo(ts) {
if (!ts) return "—";
const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
if (diff < 5) return "just now";
if (diff < 60) return `${diff} sec ago`;
const mins = Math.floor(diff / 60);
if (mins < 60) return `${mins} min${mins !== 1 ? "s" : ""} ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs} hr${hrs !== 1 ? "s" : ""} ago`;
const days = Math.floor(hrs / 24);
if (days < 7) return `${days} day${days !== 1 ? "s" : ""} ago`;
const weeks = Math.floor(days / 7);
if (weeks < 5) return `${weeks} week${weeks !== 1 ? "s" : ""} ago`;
const months = Math.floor(days / 30);
if (months < 12) return `${months} month${months !== 1 ? "s" : ""} ago`;
const years = Math.floor(days / 365);
return `${years} year${years !== 1 ? "s" : ""} ago`;
}
export function formatDuration (secs) {
if (!secs) return "0 min";
const h = Math.floor(secs / 3600);