Initial Commit
This commit is contained in:
255
src/tui/screens/TemplateList.tsx
Normal file
255
src/tui/screens/TemplateList.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Template List Screen - Lists available templates and starting actions.
|
||||
*
|
||||
* Allows users to:
|
||||
* - View imported templates
|
||||
* - Select a template and action to start a new transaction
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Screen } from '../components/Screen.js';
|
||||
import { useNavigation } from '../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
|
||||
import { colors, logoSmall } from '../theme.js';
|
||||
import type { XOTemplate, XOTemplateStartingActions } from '@xo-cash/types';
|
||||
|
||||
/**
|
||||
* Template item with metadata.
|
||||
*/
|
||||
interface TemplateItem {
|
||||
template: XOTemplate;
|
||||
templateIdentifier: string;
|
||||
startingActions: XOTemplateStartingActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template List Screen Component.
|
||||
* Displays templates and their starting actions.
|
||||
*/
|
||||
export function TemplateListScreen(): React.ReactElement {
|
||||
const { navigate } = useNavigation();
|
||||
const { walletController, showError } = useAppContext();
|
||||
const { setStatus } = useStatus();
|
||||
|
||||
// State
|
||||
const [templates, setTemplates] = useState<TemplateItem[]>([]);
|
||||
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState(0);
|
||||
const [selectedActionIndex, setSelectedActionIndex] = useState(0);
|
||||
const [focusedPanel, setFocusedPanel] = useState<'templates' | 'actions'>('templates');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
/**
|
||||
* Loads templates from the wallet controller.
|
||||
*/
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setStatus('Loading templates...');
|
||||
|
||||
const templateList = await walletController.getTemplates();
|
||||
const { generateTemplateIdentifier } = await import('@xo-cash/engine');
|
||||
|
||||
const loadedTemplates = await Promise.all(
|
||||
templateList.map(async (template) => {
|
||||
const templateIdentifier = generateTemplateIdentifier(template);
|
||||
const startingActions = await walletController.getStartingActions(templateIdentifier);
|
||||
return { template, templateIdentifier, startingActions };
|
||||
})
|
||||
);
|
||||
|
||||
setTemplates(loadedTemplates);
|
||||
setSelectedTemplateIndex(0);
|
||||
setSelectedActionIndex(0);
|
||||
setStatus('Ready');
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
showError(`Failed to load templates: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [walletController, setStatus, showError]);
|
||||
|
||||
// Load templates on mount
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
// Get current template and its actions
|
||||
const currentTemplate = templates[selectedTemplateIndex];
|
||||
const currentActions = currentTemplate?.startingActions ?? [];
|
||||
|
||||
/**
|
||||
* Handles action selection.
|
||||
*/
|
||||
const handleActionSelect = useCallback(() => {
|
||||
if (!currentTemplate || currentActions.length === 0) return;
|
||||
|
||||
const action = currentActions[selectedActionIndex];
|
||||
if (!action) return;
|
||||
|
||||
// Navigate to action wizard with selected template and action
|
||||
navigate('wizard', {
|
||||
templateIdentifier: currentTemplate.templateIdentifier,
|
||||
actionIdentifier: action.action,
|
||||
roleIdentifier: action.role,
|
||||
template: currentTemplate.template,
|
||||
});
|
||||
}, [currentTemplate, currentActions, selectedActionIndex, navigate]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
useInput((input, key) => {
|
||||
// Tab to switch panels
|
||||
if (key.tab) {
|
||||
setFocusedPanel(prev => prev === 'templates' ? 'actions' : 'templates');
|
||||
return;
|
||||
}
|
||||
|
||||
// Up/Down navigation
|
||||
if (key.upArrow || input === 'k') {
|
||||
if (focusedPanel === 'templates') {
|
||||
setSelectedTemplateIndex(prev => Math.max(0, prev - 1));
|
||||
setSelectedActionIndex(0); // Reset action selection when template changes
|
||||
} else {
|
||||
setSelectedActionIndex(prev => Math.max(0, prev - 1));
|
||||
}
|
||||
} else if (key.downArrow || input === 'j') {
|
||||
if (focusedPanel === 'templates') {
|
||||
setSelectedTemplateIndex(prev => Math.min(templates.length - 1, prev + 1));
|
||||
setSelectedActionIndex(0); // Reset action selection when template changes
|
||||
} else {
|
||||
setSelectedActionIndex(prev => Math.min(currentActions.length - 1, prev + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Enter to select action
|
||||
if (key.return && focusedPanel === 'actions') {
|
||||
handleActionSelect();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Header */}
|
||||
<Box borderStyle="single" borderColor={colors.secondary} paddingX={1}>
|
||||
<Text color={colors.primary} bold>{logoSmall} - Select Template & Action</Text>
|
||||
</Box>
|
||||
|
||||
{/* Main content - two columns */}
|
||||
<Box flexDirection="row" marginTop={1} flexGrow={1}>
|
||||
{/* Left column: Template list */}
|
||||
<Box flexDirection="column" width="40%" paddingRight={1}>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={focusedPanel === 'templates' ? colors.focus : colors.primary}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
flexGrow={1}
|
||||
>
|
||||
<Text color={colors.primary} bold> Templates </Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{isLoading ? (
|
||||
<Text color={colors.textMuted}>Loading...</Text>
|
||||
) : templates.length === 0 ? (
|
||||
<Text color={colors.textMuted}>No templates imported</Text>
|
||||
) : (
|
||||
templates.map((item, index) => (
|
||||
<Text
|
||||
key={item.templateIdentifier}
|
||||
color={index === selectedTemplateIndex ? colors.focus : colors.text}
|
||||
bold={index === selectedTemplateIndex}
|
||||
>
|
||||
{index === selectedTemplateIndex && focusedPanel === 'templates' ? '▸ ' : ' '}
|
||||
{index + 1}. {item.template.name || 'Unnamed Template'}
|
||||
</Text>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right column: Actions list */}
|
||||
<Box flexDirection="column" width="60%" paddingLeft={1}>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={focusedPanel === 'actions' ? colors.focus : colors.border}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
flexGrow={1}
|
||||
>
|
||||
<Text color={colors.primary} bold> Starting Actions </Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{!currentTemplate ? (
|
||||
<Text color={colors.textMuted}>Select a template...</Text>
|
||||
) : currentActions.length === 0 ? (
|
||||
<Text color={colors.textMuted}>No starting actions available</Text>
|
||||
) : (
|
||||
currentActions.map((action, index) => {
|
||||
const actionDef = currentTemplate.template.actions?.[action.action];
|
||||
const name = actionDef?.name || action.action;
|
||||
return (
|
||||
<Text
|
||||
key={`${action.action}-${action.role}`}
|
||||
color={index === selectedActionIndex ? colors.focus : colors.text}
|
||||
bold={index === selectedActionIndex}
|
||||
>
|
||||
{index === selectedActionIndex && focusedPanel === 'actions' ? '▸ ' : ' '}
|
||||
{index + 1}. {name} (as {action.role})
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Description box */}
|
||||
<Box marginTop={1}>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={colors.border}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
width="100%"
|
||||
>
|
||||
<Text color={colors.primary} bold> Description </Text>
|
||||
{currentTemplate ? (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={colors.text} bold>
|
||||
{currentTemplate.template.name || 'Unnamed Template'}
|
||||
</Text>
|
||||
<Text color={colors.textMuted}>
|
||||
{currentTemplate.template.description || 'No description available'}
|
||||
</Text>
|
||||
{currentTemplate.template.version !== undefined && (
|
||||
<Text color={colors.text}>
|
||||
Version: {currentTemplate.template.version}
|
||||
</Text>
|
||||
)}
|
||||
{currentTemplate.template.roles && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={colors.text}>Roles:</Text>
|
||||
{Object.entries(currentTemplate.template.roles).map(([roleId, role]) => (
|
||||
<Text key={roleId} color={colors.textMuted}>
|
||||
{' '}- {role.name || roleId}: {role.description || 'No description'}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={colors.textMuted}>Select a template to see details</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Help text */}
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.textMuted} dimColor>
|
||||
Tab: Switch list • Enter: Select action • ↑↓: Navigate • Esc: Back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user