/** * 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, useMemo } from 'react'; import { Box, Text, useInput } from 'ink'; import { ScrollableList, type ListItemData } from '../components/List.js'; import { useNavigation } from '../hooks/useNavigation.js'; import { useAppContext, useStatus } from '../hooks/useAppContext.js'; import { colors, logoSmall } from '../theme.js'; // XO Imports import { generateTemplateIdentifier } from '@xo-cash/engine'; import type { XOTemplate } from '@xo-cash/types'; // Import utility functions import { formatTemplateListItem, formatActionListItem, deduplicateStartingActions, getTemplateRoles, getRolesForAction, type UniqueStartingAction, } from '../../utils/template-utils.js'; /** * Template item with metadata. */ interface TemplateItem { template: XOTemplate; templateIdentifier: string; startingActions: UniqueStartingAction[]; } /** * Template list item with TemplateItem value. */ type TemplateListItem = ListItemData; /** * Action list item with UniqueStartingAction value. */ type ActionListItem = ListItemData; /** * Template List Screen Component. * Displays templates and their starting actions. */ export function TemplateListScreen(): React.ReactElement { const { navigate } = useNavigation(); const { appService, showError } = useAppContext(); const { setStatus } = useStatus(); // State const [templates, setTemplates] = useState([]); 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 engine. */ const loadTemplates = useCallback(async () => { if (!appService) { showError('AppService not initialized'); return; } try { setIsLoading(true); setStatus('Loading templates...'); const templateList = await appService.engine.listImportedTemplates(); const loadedTemplates = await Promise.all( templateList.map(async (template) => { const templateIdentifier = generateTemplateIdentifier(template); const rawStartingActions = await appService.engine.listStartingActions(templateIdentifier); // Use utility function to deduplicate actions const startingActions = deduplicateStartingActions(template, rawStartingActions); 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); } }, [appService, setStatus, showError]); // Load templates on mount useEffect(() => { loadTemplates(); }, [loadTemplates]); // Get current template and its actions const currentTemplate = templates[selectedTemplateIndex]; const currentActions = currentTemplate?.startingActions ?? []; /** * Build template list items for ScrollableList. */ const templateListItems = useMemo((): TemplateListItem[] => { return templates.map((item, index) => { const formatted = formatTemplateListItem(item.template, index); return { key: item.templateIdentifier, label: formatted.label, description: formatted.description, value: item, hidden: !formatted.isValid, }; }); }, [templates]); /** * Build action list items for ScrollableList. */ const actionListItems = useMemo((): ActionListItem[] => { return currentActions.map((action, index) => { const formatted = formatActionListItem( action.actionIdentifier, currentTemplate?.template?.actions?.[action.actionIdentifier], action.roleCount, index ); return { key: action.actionIdentifier, label: formatted.label, description: formatted.description, value: action, hidden: !formatted.isValid, }; }); }, [currentActions, currentTemplate]); /** * Handle template selection change. */ const handleTemplateSelect = useCallback((index: number) => { setSelectedTemplateIndex(index); setSelectedActionIndex(0); // Reset action selection when template changes }, []); /** * Handles action selection. * Navigates to the Action Wizard where the user will choose their role. */ const handleActionActivate = useCallback((item: ActionListItem, index: number) => { if (!currentTemplate || !item.value) return; const action = item.value; // Navigate to the Action Wizard — role selection happens there navigate('wizard', { templateIdentifier: currentTemplate.templateIdentifier, actionIdentifier: action.actionIdentifier, template: currentTemplate.template, }); }, [currentTemplate, navigate]); // Handle keyboard navigation useInput((input, key) => { // Tab to switch panels if (key.tab) { setFocusedPanel(prev => prev === 'templates' ? 'actions' : 'templates'); return; } }); /** * Render custom template list item. */ const renderTemplateItem = useCallback(( item: TemplateListItem, isSelected: boolean, isFocused: boolean ): React.ReactNode => { return ( {isFocused ? '▸ ' : ' '} {item.label} ); }, []); /** * Render custom action list item. */ const renderActionItem = useCallback(( item: ActionListItem, isSelected: boolean, isFocused: boolean ): React.ReactNode => { return ( {isFocused ? '▸ ' : ' '} {item.label} ); }, []); return ( {/* Header */} {logoSmall} - Select Template & Action {/* Main content - two columns */} {/* Left column: Template list */} Templates {isLoading ? ( Loading... ) : ( )} {/* Right column: Actions list */} Starting Actions {isLoading ? ( Loading... ) : !currentTemplate ? ( Select a template... ) : ( )} {/* Description box */} Description {/* Show template description when templates panel is focused */} {focusedPanel === 'templates' && currentTemplate ? ( {currentTemplate.template.name || 'Unnamed Template'} {currentTemplate.template.description || 'No description available'} {currentTemplate.template.version !== undefined && ( Version: {currentTemplate.template.version} )} {currentTemplate.template.roles && ( Roles: {getTemplateRoles(currentTemplate.template).map((role) => ( {' '}- {role.name}: {role.description || 'No description'} ))} )} ) : focusedPanel === 'templates' && !currentTemplate ? ( Select a template to see details ) : null} {/* Show action description when actions panel is focused */} {focusedPanel === 'actions' && currentTemplate && currentActions.length > 0 ? ( {(() => { const action = currentActions[selectedActionIndex]; if (!action) return null; // Get roles that can start this action using utility function const availableRoles = getRolesForAction(currentTemplate.template, action.actionIdentifier); return ( <> {action.name} {action.description || 'No description available'} {/* List available roles for this action */} {availableRoles.length > 0 && ( Available Roles: {availableRoles.map((role) => ( {' '}- {role.name} {role.description ? `: ${role.description}` : ''} ))} )} ); })()} ) : focusedPanel === 'actions' && !currentTemplate ? ( Select a template first ) : focusedPanel === 'actions' && currentActions.length === 0 ? ( No starting actions available ) : null} {/* Help text */} Tab: Switch list • Enter: Select action • ↑↓: Navigate • Esc: Back ); }