import { type TransactionStatusCode, transactionStatusLabels } from "@olenbetong/appframe-updater";
import { Box, Text } from "ink";
import { type ReactNode, useMemo } from "react";
import type { Server } from "../lib/Server.js";
import { FullScreenBox } from "./FullScreenBox.js";
import { STATUS_DIRTY_INDICATOR } from "./tableFormatting.js";
import { useCommand } from "./useCommand.js";
import { useScreenSize } from "./useScreenSize.js";
import { useScrollableText } from "./useScrollableText.js";
import { useTransactionEdits } from "./useTransactionEdits.js";
import { useTransactions } from "./useTransactions.js";
import { useTransactionText } from "./useTransactionText.js";
import { useVirtualText } from "./useVirtualText.js";
const PREVIEW_SAVE_SHORTCUTS = ["ctrl+s", "meta+s"];
const NO_TRANSACTION_FILTER = "1 = 0";
function escapePrimKey(primKey: string) {
return primKey.replace(/'/g, "''");
}
function createPrimKeyFilter(primKey: string | null) {
if (!primKey) {
return NO_TRANSACTION_FILTER;
}
return `PrimKey = '${escapePrimKey(primKey)}'`;
}
export type TransactionsPreviewDialogProps = {
server: Server;
namespace: string | number;
frameWidth: number;
focusedPrimKey: string | null;
focusNext: () => void;
focusPrevious: () => void;
};
function Shortcut({ keyboard, command }: { keyboard: string; command: string }) {
return (
[{keyboard}] {command}
);
}
export function TransactionsPreviewDialog({
server,
namespace,
frameWidth,
focusedPrimKey,
focusNext,
focusPrevious,
}: TransactionsPreviewDialogProps) {
let transactionFilter = useMemo(() => createPrimKeyFilter(focusedPrimKey), [focusedPrimKey]);
let { loading, error, refresh, normalizedTransactions, transactionMap } = useTransactions({
server,
namespace,
filter: transactionFilter,
});
let selectedPrimKeys = useMemo(
() => (focusedPrimKey ? new Set([focusedPrimKey]) : new Set()),
[focusedPrimKey],
);
let { pendingStatuses, cycleStatus, save } = useTransactionEdits({
normalizedTransactions,
transactionRecordMap: transactionMap,
selectedPrimKeys,
server,
refresh,
});
let record = focusedPrimKey ? transactionMap.get(focusedPrimKey) : undefined;
let pendingStatus = focusedPrimKey ? pendingStatuses[focusedPrimKey] : undefined;
let statusPreviewValue = useMemo(() => {
if (!record) {
return "";
}
let originalStatus = record.Status;
let numericOriginal =
typeof originalStatus === "number"
? originalStatus
: typeof originalStatus === "string"
? Number(originalStatus)
: null;
let baseText: string;
if (pendingStatus !== undefined) {
baseText = String(pendingStatus);
} else if (originalStatus === null) {
baseText = "null";
} else if (originalStatus === undefined) {
baseText = "";
} else {
baseText = String(originalStatus);
}
let labelSource: number | null = null;
if (pendingStatus !== undefined) {
labelSource = pendingStatus;
} else if (numericOriginal !== null && Number.isFinite(numericOriginal)) {
labelSource = numericOriginal;
}
if (labelSource !== null) {
let label = transactionStatusLabels[labelSource as TransactionStatusCode];
if (label) {
baseText = baseText.length > 0 ? `${baseText} (${label})` : `(${label})`;
}
}
let isDirty = false;
if (pendingStatus !== undefined) {
isDirty = numericOriginal === null || !Number.isFinite(numericOriginal) || pendingStatus !== numericOriginal;
}
if (isDirty) {
baseText = `${baseText}${STATUS_DIRTY_INDICATOR}`;
}
return baseText;
}, [pendingStatus, record]);
let { text: transactionText, dialogWidth } = useTransactionText({
transaction: record,
statusPreviewValue,
frameWidth,
});
let { height: screenHeight } = useScreenSize();
let headerLineCount = 3 + (record?.Name ? 1 : 0);
let marginLines = 1; // Box marginTop between header and content
let baseNonContentLines = headerLineCount + marginLines;
let contentLineCount = transactionText.length > 0 ? transactionText.length : 1;
let computeLayout = (extraNonContentLines: number) => {
let nonContentLines = baseNonContentLines + extraNonContentLines;
let naturalDialogHeight = nonContentLines + contentLineCount;
let maxDialogHeight = Math.max(screenHeight - 4, nonContentLines + 1);
maxDialogHeight = Math.min(maxDialogHeight, Math.max(screenHeight - 2, 1));
let dialogHeightValue = Math.min(naturalDialogHeight, maxDialogHeight);
let contentHeightValue = Math.max(dialogHeightValue - nonContentLines, 1);
return { dialogHeight: dialogHeightValue, contentHeight: contentHeightValue, nonContentLines };
};
let { dialogHeight, contentHeight } = computeLayout(0);
let showScrollSummary = record && transactionText.length > contentHeight;
if (showScrollSummary) {
({ dialogHeight, contentHeight } = computeLayout(2));
}
let previewSignature = useMemo(
() => `${focusedPrimKey ?? ""}|${transactionText.join("|")}`,
[focusedPrimKey, transactionText],
);
let { scrollOffset } = useScrollableText({
textLength: transactionText.length,
viewportHeight: contentHeight,
signature: previewSignature,
});
let { visibleText, totalLines, offset } = useVirtualText({
text: transactionText,
viewportHeight: contentHeight,
scrollOffset,
});
useCommand("left", focusPrevious, true);
useCommand("right", focusNext, true);
useCommand(
PREVIEW_SAVE_SHORTCUTS,
() => {
if (focusedPrimKey) {
save({ primKey: focusedPrimKey });
}
},
focusedPrimKey !== null,
);
useCommand(
"s",
() => {
if (focusedPrimKey) {
cycleStatus(focusedPrimKey, { exclusive: true });
}
},
focusedPrimKey !== null,
);
let summaryText = useMemo(() => {
if (totalLines === 0) {
return "";
}
let start = offset + 1;
let end = Math.min(offset + contentHeight, totalLines);
return `Showing ${start}–${end} of ${totalLines}`;
}, [contentHeight, offset, totalLines]);
let bodyContent: ReactNode;
if (!focusedPrimKey) {
bodyContent = No transaction selected.;
} else if (error) {
bodyContent = {error.message};
} else if (!record) {
bodyContent = loading ? (
Loading transaction…
) : (
Transaction not found.
);
} else if (transactionText.length === 0) {
bodyContent = No data available.;
} else {
bodyContent = visibleText.map((line, index) => (
{line.length > 0 ? line : " "}
));
}
return (
Transaction preview
{record?.Name ? {record.Name} : null}
· ·{" "}
· ·{" "}
·
{bodyContent}
{showScrollSummary ? (
{summaryText}
) : null}
);
}