114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
/**
|
|
* VariablesStep — collects all required variable values for invitation import.
|
|
*
|
|
* This runs after role selection and before input selection so cashasm
|
|
* expressions can resolve required variables during `getSatsOut()`.
|
|
*/
|
|
|
|
import React, { useMemo, useState, useCallback } from "react";
|
|
import { Box, Text } from "ink";
|
|
import { colors } from "../../../../theme.js";
|
|
import { useLayeredInput } from "../../../../hooks/useInputLayer.js";
|
|
import { VariableInputField } from "../../../../components/VariableInputField.js";
|
|
import type { VariablesStepProps } from "../types.js";
|
|
|
|
/**
|
|
* Build a user-facing validation error for empty required fields.
|
|
*/
|
|
function validateVariables(
|
|
variables: VariablesStepProps["variables"],
|
|
): string | null {
|
|
const empty = variables.filter((v) => v.value.trim() === "");
|
|
if (empty.length === 0) return null;
|
|
return `Please enter values for: ${empty.map((v) => v.name).join(", ")}`;
|
|
}
|
|
|
|
export function VariablesStep({
|
|
variables,
|
|
onUpdateVariable,
|
|
onComplete,
|
|
onCancel,
|
|
isActive,
|
|
}: VariablesStepProps): React.ReactElement {
|
|
const [focusedInput, setFocusedInput] = useState(0);
|
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
|
|
const helpText = useMemo(() => {
|
|
if (variables.length === 0) {
|
|
return "No variables required for this role.";
|
|
}
|
|
return "Enter a value for each variable, then press Enter on the last field to continue.";
|
|
}, [variables.length]);
|
|
|
|
/**
|
|
* Move focus to next input, or finish the step if this is the last one.
|
|
*/
|
|
const handleInputSubmit = useCallback(() => {
|
|
if (variables.length === 0) {
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
if (focusedInput < variables.length - 1) {
|
|
setFocusedInput((prev) => prev + 1);
|
|
return;
|
|
}
|
|
|
|
const validation = validateVariables(variables);
|
|
setValidationError(validation);
|
|
if (!validation) {
|
|
onComplete();
|
|
}
|
|
}, [variables, focusedInput, onComplete]);
|
|
|
|
// Keyboard navigation for non-text actions.
|
|
useLayeredInput(
|
|
"import-flow",
|
|
(input, key) => {
|
|
if (key.upArrow || input === "k") {
|
|
setFocusedInput((prev) => Math.max(0, prev - 1));
|
|
} else if (key.downArrow || input === "j") {
|
|
setFocusedInput((prev) => Math.min(variables.length - 1, prev + 1));
|
|
} else if (key.escape) {
|
|
onCancel();
|
|
}
|
|
},
|
|
{ isActive },
|
|
);
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Text color={colors.primary} bold>
|
|
Required Variables
|
|
</Text>
|
|
|
|
<Box marginTop={1} flexDirection="column">
|
|
{variables.map((variable, index) => (
|
|
<VariableInputField
|
|
key={variable.id}
|
|
variable={variable}
|
|
index={index}
|
|
isFocused={focusedInput === index}
|
|
onChange={onUpdateVariable}
|
|
onSubmit={handleInputSubmit}
|
|
borderColor={colors.border as string}
|
|
focusColor={colors.primary as string}
|
|
/>
|
|
))}
|
|
</Box>
|
|
|
|
{validationError && (
|
|
<Box marginTop={1}>
|
|
<Text color={colors.error}>{validationError}</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<Box marginTop={1}>
|
|
<Text color={colors.textMuted}>
|
|
{helpText} ↑↓: Change field • Esc: Cancel
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|