INIT frontend

This commit is contained in:
2025-11-28 11:55:01 -06:00
parent f99d21805f
commit 5029278228
31 changed files with 3766 additions and 0 deletions
+834
View File
@@ -0,0 +1,834 @@
import React, { useEffect, useState, useRef } from "react";
import {
Text,
Input,
Spinner,
Button,
Dialog,
DialogSurface,
DialogBody,
DialogTitle,
DialogActions,
Menu,
MenuTrigger,
MenuPopover,
MenuList,
MenuItem,
} from "@fluentui/react-components";
import { DeleteRegular, SearchRegular, ChevronDownRegular } from "@fluentui/react-icons";
import type {
GeneratedComponentProps,
ccllc_job,
ReadableTableRow,
QueryTableOptions,
} from "./RuntimeTypes";
/* ──────────────────────────────── DEFAULTS ──────────────────────────────── */
const DEFAULT_COLUMN_ORDER = [
"ccllc_jobnumber", "ccllc_jobname", "ccllc_jobaddress", "ccllc_jobtype",
"ccllc_flagstatus", "ccllc_jobstatus", "ccllc_jobdivision", "ccllc_estimatorname",
"ccllc_officerepresentative", "ccllc_clientcompany", "ccllc_contactperson",
"ccllc_contactphonenumber", "ccllc_contactemail", "ccllc_duedate", "ccllc_duedatecounter",
"ccllc_duetime", "ccllc_documentsuploaded", "ccllc_quickbookscreated", "ccllc_addedtocalendar",
"ccllc_pswiftuploaded", "ccllc_voxercreated", "ccllc_estimatedraft", "ccllc_estimatereviewed",
"ccllc_estimateapproved", "ccllc_notes", "ccllc_activestatus", "ccllc_jobquickbookslink",
"ccllc_voxerlink", "ccllc_jobcodes", "ccllc_submissiondate", "ccllc_powerappsid",
"ccllc_jobfolderlink", "ccllc_taxexemptstatus", "statecode", "statuscode", "ccllc_active",
] as const;
const DEFAULT_FRIENDLY_NAMES: Record<string, string> = {
ccllc_jobnumber: "Job Number",
ccllc_jobname: "Job Name",
statecode: "State",
statuscode: "Status",
ccllc_active: "Key",
ccllc_activestatus: "Active\nStatus",
ccllc_addedtocalendar: "Added to\nCalendar",
ccllc_clientcompany: "Client\nCompany",
ccllc_contactemail: "Contact\nEmail",
ccllc_contactperson: "Contact\nPerson",
ccllc_contactphonenumber: "Contact\nPhone Number",
ccllc_documentsuploaded: "Documents\nUploaded",
ccllc_duedate: "Due Date",
ccllc_duedatecounter: "Due Date\nCounter",
ccllc_duetime: "Due Time",
ccllc_estimateapproved: "Estimate\nApproved",
ccllc_estimatedraft: "Estimate\nDraft",
ccllc_estimatereviewed: "Estimate\nReviewed",
ccllc_estimatorname: "Estimator\nName",
ccllc_flagstatus: "Flag",
ccllc_jobaddress: "Job\nAddress",
ccllc_jobcodes: "Job\nCodes",
ccllc_jobdivision: "Job\nDivision",
ccllc_jobfolderlink: "Job Folder Link",
ccllc_jobquickbookslink: "Job QuickBooks\nLink",
ccllc_jobstatus: "Job\nStatus",
ccllc_jobtype: "Job\nType",
ccllc_notes: "Notes",
ccllc_officerepresentative: "Office Rep",
ccllc_powerappsid: "PowerApps\nID",
ccllc_pswiftuploaded: "PSwift\nUploaded",
ccllc_quickbookscreated: "QB Created",
ccllc_submissiondate: "Submission\nDate",
ccllc_taxexemptstatus: "Tax Exempt",
ccllc_voxercreated: "Voxer\nCreated",
ccllc_voxerlink: "Voxer Link",
};
const DEFAULT_COLUMN_ALIGNMENTS: Record<string, "left" | "center" | "right"> = {
ccllc_jobnumber: "center",
ccllc_jobname: "left",
statecode: "center",
statuscode: "center",
ccllc_active: "center",
ccllc_activestatus: "center",
ccllc_addedtocalendar: "center",
ccllc_clientcompany: "left",
ccllc_contactemail: "left",
ccllc_contactperson: "left",
ccllc_contactphonenumber: "left",
ccllc_documentsuploaded: "center",
ccllc_duedate: "center",
ccllc_duedatecounter: "center",
ccllc_duetime: "center",
ccllc_estimateapproved: "center",
ccllc_estimatedraft: "center",
ccllc_estimatereviewed: "center",
ccllc_estimatorname: "left",
ccllc_flagstatus: "center",
ccllc_jobaddress: "left",
ccllc_jobcodes: "left",
ccllc_jobdivision: "left",
ccllc_jobfolderlink: "left",
ccllc_jobquickbookslink: "left",
ccllc_jobstatus: "center",
ccllc_jobtype: "center",
ccllc_notes: "left",
ccllc_officerepresentative: "left",
ccllc_powerappsid: "center",
ccllc_pswiftuploaded: "center",
ccllc_quickbookscreated: "center",
ccllc_submissiondate: "center",
ccllc_taxexemptstatus: "center",
ccllc_voxercreated: "center",
ccllc_voxerlink: "left",
};
const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
ccllc_jobnumber: 100,
ccllc_jobname: 280,
statecode: 80,
statuscode: 80,
ccllc_active: 80,
ccllc_activestatus: 100,
ccllc_addedtocalendar: 120,
ccllc_clientcompany: 160,
ccllc_contactemail: 180,
ccllc_contactperson: 140,
ccllc_contactphonenumber: 140,
ccllc_documentsuploaded: 120,
ccllc_duedate: 110,
ccllc_duedatecounter: 110,
ccllc_duetime: 100,
ccllc_estimateapproved: 120,
ccllc_estimatedraft: 120,
ccllc_estimatereviewed: 120,
ccllc_estimatorname: 140,
ccllc_flagstatus: 80,
ccllc_jobaddress: 180,
ccllc_jobcodes: 120,
ccllc_jobdivision: 120,
ccllc_jobfolderlink: 160,
ccllc_jobquickbookslink: 160,
ccllc_jobstatus: 120,
ccllc_jobtype: 120,
ccllc_notes: 180,
ccllc_officerepresentative: 140,
ccllc_powerappsid: 120,
ccllc_pswiftuploaded: 120,
ccllc_quickbookscreated: 120,
ccllc_submissiondate: 120,
ccllc_taxexemptstatus: 120,
ccllc_voxercreated: 120,
ccllc_voxerlink: 160,
};
const DEFAULT_COLUMN_VISIBILITY = Object.fromEntries(
DEFAULT_COLUMN_ORDER.map(c => [c, true])
);
/* ──────────────────────────────── UTILS ──────────────────────────────── */
const getJobColumns = (row?: ReadableTableRow<ccllc_job>): string[] => {
const sticky = ["ccllc_jobnumber", "ccllc_jobname"];
const ignore = new Set(["ccllc_jobid", "@Odata.Etag", "@odata.etag"]);
if (!row) return sticky;
return [
...sticky,
...Object.keys(row).filter(
k =>
!sticky.includes(k) &&
!ignore.has(k) &&
!k.endsWith("@OData.Community.Display.V1.FormattedValue") &&
typeof row[k as keyof typeof row] !== "function"
),
];
};
const humanize = (col: string) =>
col.replace(/^ccllc_/, "").replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase());
/* ───────────────────────────── EditableCell ───────────────────────────── */
function EditableCell({
value,
onSave,
rowId,
colKey,
isSticky,
width,
disabled,
isFirstNonSticky,
align,
}: {
value: string;
onSave: (v: string) => Promise<void>;
rowId: string;
colKey: string;
isSticky?: boolean;
width?: number;
disabled?: boolean;
isFirstNonSticky?: boolean;
align?: "left" | "center" | "right";
}) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(value ?? "");
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
useEffect(() => setEditValue(value ?? ""), [value]);
const startEdit = () => !disabled && setEditing(true);
const finishEdit = async () => {
const final = colKey === "ccllc_jobnumber" ? editValue.replace(/,/g, "") : editValue;
if (final !== value) await onSave(final);
setEditing(false);
};
const cellStyle: React.CSSProperties = {
minWidth: width ? `${width}px` : undefined,
maxWidth: width ? `${width}px` : undefined,
width: width ? `${width}px` : undefined,
borderTop: "1px solid #000",
borderLeft: isFirstNonSticky ? "none" : "1px solid #000",
borderRight: colKey === "ccllc_jobnumber" ? "none" : "1px solid #000",
borderBottom: "1px solid #000",
boxSizing: "border-box",
position: isSticky ? "sticky" : undefined,
left: isSticky && colKey === "ccllc_jobnumber" ? 0 : isSticky && colKey === "ccllc_jobname" ? 100 : undefined,
zIndex: isSticky ? 1500 : undefined,
background: isSticky ? "#fff" : undefined,
overflow: "hidden",
whiteSpace: "nowrap",
padding: "6px 10px",
cursor: disabled ? "not-allowed" : "pointer",
userSelect: "none",
textAlign: align || "left",
};
return (
<td style={cellStyle} onClick={startEdit} tabIndex={disabled ? -1 : 0}>
{editing && !disabled ? (
<input
ref={inputRef}
value={editValue}
onChange={e => setEditValue(e.target.value)}
onBlur={finishEdit}
onKeyDown={e => {
if (e.key === "Enter") finishEdit();
if (e.key === "Escape") {
setEditValue(value ?? "");
setEditing(false);
}
}}
style={{
width: "100%",
background: "transparent",
border: "none",
outline: "none",
font: "inherit",
padding: 0,
margin: 0,
textAlign: align || "left",
}}
/>
) : (
<span style={{ display: "block", width: "100%", overflow: "hidden", textOverflow: "ellipsis" }}>
{colKey === "ccllc_jobnumber" ? (value ?? "").replace(/,/g, "") : value}
</span>
)}
</td>
);
}
/* ─────────────────────────── EditableHeaderCell ─────────────────────────── */
function EditableHeaderCell({
value,
onSave,
colKey,
isSticky,
width,
isFirstNonSticky,
align,
}: {
value: string;
onSave: (newValue: string) => void;
colKey: string;
isSticky?: boolean;
width?: number;
isFirstNonSticky?: boolean;
align?: "left" | "center" | "right";
}) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
useEffect(() => setEditValue(value), [value]);
const handleCellClick = (e: React.MouseEvent) => {
if (!(e.target as HTMLElement).closest(".header-dropdown-btn")) {
setEditing(true);
}
};
const handleSave = () => {
if (editValue !== value) onSave(editValue);
setEditing(false);
};
const handleInputBlur = () => handleSave();
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") handleSave();
else if (e.key === "Escape") {
setEditValue(value);
setEditing(false);
}
};
// Sticky header cells for "Job Number" and "Job Name" should have top: 0 to stay fixed on vertical scroll
const isStickyHeader = isSticky && (colKey === "ccllc_jobnumber" || colKey === "ccllc_jobname");
// Always sticky at top for all header cells
const headerStyle: React.CSSProperties = {
minWidth: width ? `${width}px` : "60px",
maxWidth: width ? `${width + 20}px` : undefined,
width: width ? `${width}px` : undefined,
borderTop: "1px solid #000",
borderLeft: isFirstNonSticky ? "none" : "1px solid #000",
borderRight: value === "Job Number" ? "none" : "1px solid #000",
borderBottom: "1px solid #000",
boxSizing: "border-box",
position: "sticky",
top: 0,
left: isSticky && colKey === "ccllc_jobnumber" ? 0 : isSticky && colKey === "ccllc_jobname" ? 100 : undefined,
zIndex: isSticky
? 2000 // sticky columns in header
: 1500, // normal header
background: "#f3f3f3",
whiteSpace: "normal",
padding: 0,
fontWeight: 600,
textAlign: align || "left",
cursor: "pointer",
userSelect: "none",
minHeight: "40px",
height: "40px",
overflow: "visible",
};
const flexRowStyle: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: "100%",
height: "100%",
minHeight: "40px",
overflow: "visible",
};
const labelAreaStyle: React.CSSProperties = {
flex: "1 1 0",
minWidth: 0,
display: "flex",
alignItems: "center",
padding: "8px 8px 8px 10px",
boxSizing: "border-box",
overflow: "hidden",
};
const dropdownCellStyle: React.CSSProperties = {
width: "20px",
minWidth: "20px",
maxWidth: "20px",
height: "40px",
boxSizing: "border-box",
overflow: "visible",
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
paddingBottom: "4px",
zIndex: 1600,
};
const dropdownButtonStyle: React.CSSProperties = {
width: "16px",
height: "16px",
minWidth: "16px",
minHeight: "16px",
padding: 0,
margin: 0,
borderRadius: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#C0C0C0",
zIndex: 1600,
};
const renderHeaderText = (text: string) => {
const lines = text.split("\n");
return (
<span style={{ display: "block", width: "100%", wordBreak: "break-word", whiteSpace: "pre-line" }}>
{lines.map((line, idx) => (
<React.Fragment key={idx}>
{line}
{idx < lines.length - 1 ? <br /> : null}
</React.Fragment>
))}
</span>
);
};
return (
<th style={headerStyle} onClick={handleCellClick} tabIndex={0} aria-label={`Edit column: ${value}`}>
<div style={flexRowStyle}>
<div style={labelAreaStyle}>
{editing ? (
<input
ref={inputRef}
value={editValue}
onChange={e => setEditValue(e.target.value)}
onBlur={handleInputBlur}
onKeyDown={handleInputKeyDown}
style={{
width: "100%",
background: "transparent",
border: "none",
outline: "none",
font: "inherit",
padding: 0,
margin: 0,
color: "#222",
whiteSpace: "pre-line",
textAlign: align || "left",
}}
/>
) : (
renderHeaderText(editValue)
)}
</div>
<div style={dropdownCellStyle}>
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button
appearance="subtle"
size="small"
icon={<ChevronDownRegular fontSize="15px" />}
aria-label="Column options"
className="header-dropdown-btn"
style={dropdownButtonStyle}
tabIndex={0}
/>
</MenuTrigger>
<MenuPopover style={{ zIndex: 1700 }}>
<MenuList>
<MenuItem>Column options</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</div>
</div>
</th>
);
}
/* ─────────────────────────────── MAIN COMPONENT ─────────────────────────────── */
const BATCH_SIZE = 50; // Show 50 records at a time
const DEFAULT_SEARCH_BATCH_SIZE = 10;
const SEARCH_ALL_PAGE_SIZE = 100;
const GeneratedComponent: React.FC<GeneratedComponentProps> = ({ dataApi }) => {
const [mainRows, setMainRows] = useState<ReadableTableRow<ccllc_job>[]>([]);
const [searchRows, setSearchRows] = useState<ReadableTableRow<ccllc_job>[]>([]);
const [searchText, setSearchText] = useState("");
const [searchAll, setSearchAll] = useState(false);
const [loading, setLoading] = useState(true);
const [searchLoading, setSearchLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [headerNames, setHeaderNames] = useState({ ...DEFAULT_FRIENDLY_NAMES });
const [deleting, setDeleting] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [rowToDelete, setRowToDelete] = useState<ReadableTableRow<ccllc_job> | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let mounted = true;
const load = async () => {
try {
setLoading(true);
const { rows } = await dataApi.queryTable("ccllc_job", {
select: DEFAULT_COLUMN_ORDER,
pageSize: BATCH_SIZE,
orderBy: "ccllc_jobnumber desc",
});
const clean = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
if (mounted) setMainRows(clean);
} catch {
if (mounted) setError("Failed to load jobs.");
} finally {
if (mounted) setLoading(false);
}
};
load();
return () => { mounted = false; };
}, [dataApi]);
useEffect(() => {
if (!searchText.trim()) {
setSearchRows([]);
setSearchLoading(false);
return;
}
let cancelled = false;
const run = async () => {
setSearchLoading(true);
const cols = [
"ccllc_jobname", "ccllc_jobnumber", "ccllc_jobaddress", "ccllc_clientcompany",
"ccllc_contactperson", "ccllc_contactemail", "ccllc_notes", "ccllc_estimatorname",
"ccllc_officerepresentative",
];
const filter = cols
.map(c => `contains(${c},'${searchText.replace(/'/g, "''")}')`)
.join(" or ");
const allResults: ReadableTableRow<ccllc_job>[] = [];
const pageSize = searchAll ? 100 : DEFAULT_SEARCH_BATCH_SIZE; // 100 or 10
let skip = 0;
let hasMore = searchAll; // Only paginate if "Show All"
try {
// Initial request
let opts: QueryTableOptions<ccllc_job> = {
select: DEFAULT_COLUMN_ORDER,
pageSize,
orderBy: "ccllc_jobnumber desc",
filter,
skip,
};
let { rows, nextLink } = await dataApi.queryTable("ccllc_job", opts);
// Clean and store first page
const clean = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
allResults.push(...clean);
// Only continue if "Show All" and there's more
while (!cancelled && searchAll && nextLink && clean.length === pageSize) {
skip += pageSize;
opts = { ...opts, skip };
const result = await dataApi.queryTable("ccllc_job", opts);
rows = result.rows;
nextLink = result.nextLink;
const cleanPage = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
allResults.push(...cleanPage);
}
if (!cancelled) setSearchRows(allResults);
} catch (err) {
if (!cancelled) setError("Search failed.");
} finally {
if (!cancelled) setSearchLoading(false);
}
};
run();
return () => { cancelled = true; };
}, [searchText, searchAll, dataApi]);
const handleCellSave = async (rowId: string, colKey: string, newValue: string) => {
try {
const all = [...searchRows, ...mainRows];
const row = all.find(r => r.ccllc_jobid === rowId);
if (!row) return;
const final = colKey === "ccllc_jobnumber" ? newValue.replace(/,/g, "") : newValue;
if ((row as any)[colKey] === final) return;
await dataApi.updateRow("ccllc_job", rowId, { [colKey]: final });
const update = (list: typeof all) =>
list.map(r => (r.ccllc_jobid === rowId ? { ...r, [colKey]: final } : r));
setSearchRows(update);
setMainRows(update);
} catch {
setError("Update failed.");
}
};
const handleHeaderSave = (col: string, name: string) => {
setHeaderNames(p => ({ ...p, [col]: name }));
};
const openDelete = (row: ReadableTableRow<ccllc_job>) => {
setRowToDelete(row);
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!rowToDelete) return;
setDeleting(true);
try {
await dataApi.deleteRow("ccllc_job", rowToDelete.ccllc_jobid);
const id = rowToDelete.ccllc_jobid;
setSearchRows(p => p.filter(r => r.ccllc_jobid !== id));
setMainRows(p => p.filter(r => r.ccllc_jobid !== id));
} catch {
setError("Delete failed.");
} finally {
setDeleting(false);
setDeleteDialogOpen(false);
setRowToDelete(null);
}
};
const visibleCols = (mainRows.length
? getJobColumns(mainRows[0])
: DEFAULT_COLUMN_ORDER
).filter(c => c !== "@Odata.Etag" && DEFAULT_COLUMN_VISIBILITY[c] !== false);
const firstNonStickyIdx = visibleCols.findIndex(
c => c !== "ccllc_jobnumber" && c !== "ccllc_jobname"
);
const rows = searchText.trim() ? searchRows : mainRows;
const isShowAllSearch = searchText.trim() && searchAll;
const displayedRows = isShowAllSearch ? rows : rows.slice(0, 50);
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
boxSizing: "border-box",
padding: 20,
}}
>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12, gap: 12, flexShrink: 0, minHeight: 36 }}>
<div style={{ border: "2px solid #000", background: "#f3f3f3", borderRadius: 8, padding: "0px 16px",display: "flex", height: 36, }}>
<Text as="h1" size={700} weight="semibold">Job List</Text>
</div>
<div style={{ display: "flex", gap: 8 }}>
<Input
value={searchText}
onChange={(_, d) => setSearchText(d.value)}
placeholder="Search jobs..."
contentBefore={<SearchRegular />}
style={{ height: 36 }}
/>
<Button
appearance="subtle"
onClick={() => setSearchAll(a => !a)}
style={{ background: searchAll ? "#e5ffe5" : "#f3f3f3", border: "1px solid #000", }}
>
{searchAll ? "Top 10" : "Show All"}
</Button>
</div>
</div>
{error && <Text style={{ color: "#b00", marginBottom: 8, flexShrink: 0 }}>{error}</Text>}
{/* TABLE CONTAINER */}
<div
style={{
flex: 1,
minHeight: 0,
border: "1px solid #000",
borderRadius: 6,
overflow: "hidden",
position: "relative",
display: "flex",
flexDirection: "column",
}}
>
{(loading || searchLoading) && displayedRows.length === 0 ? (
<div style={{ flex: 1, display: "flex", justifyContent: "center", alignItems: "center" }}>
<Spinner size="medium" label="Loading…" />
</div>
) : (
<div
ref={scrollRef}
style={{
flex: 1,
minHeight: 0,
maxHeight: 600, // Force vertical scroll for more than 50 rows
overflowY: "auto",
overflowX: "auto",
WebkitOverflowScrolling: "touch",
boxSizing: "border-box",
}}
>
<table
style={{
borderCollapse: "separate",
borderSpacing: 0,
width: "max-content",
minWidth: "100%",
tableLayout: "fixed",
}}
>
<thead>
<tr>
{visibleCols.map((col, idx) => (
<EditableHeaderCell
key={col}
colKey={col}
value={headerNames[col] || humanize(col)}
onSave={v => handleHeaderSave(col, v)}
isSticky={col === "ccllc_jobnumber" || col === "ccllc_jobname"}
width={DEFAULT_COLUMN_WIDTHS[col]}
isFirstNonSticky={idx === firstNonStickyIdx}
align={DEFAULT_COLUMN_ALIGNMENTS[col]}
/>
))}
<th
style={{
minWidth: 60,
width: 60,
border: "1px solid #000",
background: "#f3f3f3",
position: "sticky",
top: 0,
right: undefined,
zIndex: 1500,
transform: "none",
}}
>
Delete
</th>
</tr>
</thead>
<tbody>
{displayedRows.map(row => (
<tr key={row.ccllc_jobid}>
{visibleCols.map((col, idx) => (
<EditableCell
key={col}
value={row[col as keyof typeof row]?.toString() ?? ""}
onSave={v => handleCellSave(row.ccllc_jobid, col, v)}
rowId={row.ccllc_jobid}
colKey={col}
isSticky={col === "ccllc_jobnumber" || col === "ccllc_jobname"}
width={DEFAULT_COLUMN_WIDTHS[col]}
disabled={col === "ccllc_jobid"}
isFirstNonSticky={idx === firstNonStickyIdx}
align={DEFAULT_COLUMN_ALIGNMENTS[col]}
/>
))}
<td
style={{
minWidth: 60,
width: 60,
borderTop: "1px solid #000",
borderBottom: "1px solid #000",
borderLeft: "1px solid #000",
background: "#fff",
position: "static",
right: undefined,
zIndex: undefined,
transform: "none",
textAlign: "center",
}}
>
<Button
appearance="subtle"
icon={<DeleteRegular />}
onClick={() => openDelete(row)}
disabled={deleting}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Delete Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={(_, { open }) => !open && setDeleteDialogOpen(false)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Confirm Delete</DialogTitle>
<Text block size={400} style={{ marginBottom: 16 }}>
Are you sure you want to delete this job record?
</Text>
<DialogActions>
<Button appearance="secondary" onClick={() => setDeleteDialogOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button
appearance="primary"
style={{ backgroundColor: "#b00", color: "#fff" }}
onClick={confirmDelete}
disabled={deleting}
>
{deleting ? "Deleting…" : "Delete"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</div>
);
};
export const TablePage = GeneratedComponent;
export default TablePage;
+23
View File
@@ -0,0 +1,23 @@
// table.tsx
import { Table, TableBody, TableCell, TableRow } from '@fluentui/react-components';
const SimpleTable = () => (
<Table>
<TableBody>
<TableRow>
<TableCell><strong>Name</strong></TableCell>
<TableCell><strong>Age</strong></TableCell>
</TableRow>
<TableRow>
<TableCell>Alice</TableCell>
<TableCell>30</TableCell>
</TableRow>
<TableRow>
<TableCell>Bob</TableCell>
<TableCell>25</TableCell>
</TableRow>
</TableBody>
</Table>
);
export default SimpleTable;