Invitations screen changes. Scrollable list. Details. And role selection on import

This commit is contained in:
2026-02-09 08:14:52 +00:00
parent df57f1b9ad
commit ef169e76db
10 changed files with 2237 additions and 557 deletions

246
src/utils/template-utils.ts Normal file
View File

@@ -0,0 +1,246 @@
/**
* Template utility functions.
*
* Pure functions for parsing and formatting template data.
* These functions have no React dependencies and can be used
* in both TUI and CLI contexts.
*/
import type { XOTemplate, XOTemplateAction } from '@xo-cash/types';
/**
* Formatted template list item data.
*/
export interface FormattedTemplateItem {
/** The display label for the template */
label: string;
/** The template description */
description?: string;
/** Whether the template data is valid */
isValid: boolean;
}
/**
* Formatted action list item data.
*/
export interface FormattedActionItem {
/** The display label for the action */
label: string;
/** The action description */
description?: string;
/** Number of roles that can start this action */
roleCount: number;
/** Whether the action data is valid */
isValid: boolean;
}
/**
* A unique starting action (deduplicated by action identifier).
* Multiple roles that can start the same action are counted
* but not shown as separate entries.
*/
export interface UniqueStartingAction {
actionIdentifier: string;
name: string;
description?: string;
roleCount: number;
}
/**
* Role information from a template.
*/
export interface TemplateRole {
roleId: string;
name: string;
description?: string;
}
/**
* Format a template for display in a list.
*
* @param template - The template to format
* @param index - Optional index for numbered display
* @returns Formatted item data for display
*/
export function formatTemplateListItem(
template: XOTemplate | null | undefined,
index?: number
): FormattedTemplateItem {
if (!template) {
return {
label: '',
description: undefined,
isValid: false,
};
}
const name = template.name || 'Unnamed Template';
const prefix = index !== undefined ? `${index + 1}. ` : '';
return {
label: `${prefix}${name}`,
description: template.description,
isValid: true,
};
}
/**
* Format an action for display in a list.
*
* @param actionId - The action identifier
* @param action - The action definition from the template
* @param roleCount - Number of roles that can start this action
* @param index - Optional index for numbered display
* @returns Formatted item data for display
*/
export function formatActionListItem(
actionId: string,
action: XOTemplateAction | null | undefined,
roleCount: number = 1,
index?: number
): FormattedActionItem {
if (!actionId) {
return {
label: '',
description: undefined,
roleCount: 0,
isValid: false,
};
}
const name = action?.name || actionId;
const prefix = index !== undefined ? `${index + 1}. ` : '';
const roleSuffix = roleCount > 1 ? ` (${roleCount} roles)` : '';
return {
label: `${prefix}${name}${roleSuffix}`,
description: action?.description,
roleCount,
isValid: true,
};
}
/**
* Deduplicate starting actions from a template.
* Multiple roles that can start the same action are counted
* but returned as a single entry.
*
* @param template - The template to process
* @param startingActions - Array of { action, role } pairs
* @returns Array of unique starting actions with role counts
*/
export function deduplicateStartingActions(
template: XOTemplate,
startingActions: Array<{ action: string; role: string }>
): UniqueStartingAction[] {
const actionMap = new Map<string, UniqueStartingAction>();
for (const sa of startingActions) {
if (actionMap.has(sa.action)) {
actionMap.get(sa.action)!.roleCount++;
} else {
const actionDef = template.actions?.[sa.action];
actionMap.set(sa.action, {
actionIdentifier: sa.action,
name: actionDef?.name || sa.action,
description: actionDef?.description,
roleCount: 1,
});
}
}
return Array.from(actionMap.values());
}
/**
* Get all roles from a template.
*
* @param template - The template to process
* @returns Array of role information
*/
export function getTemplateRoles(template: XOTemplate): TemplateRole[] {
if (!template.roles) return [];
return Object.entries(template.roles).map(([roleId, role]) => {
// Handle case where role might be a string instead of object
const roleObj = typeof role === 'object' ? role : null;
return {
roleId,
name: roleObj?.name || roleId,
description: roleObj?.description,
};
});
}
/**
* Get roles that can start a specific action.
*
* @param template - The template to check
* @param actionIdentifier - The action to check
* @returns Array of role information for roles that can start this action
*/
export function getRolesForAction(
template: XOTemplate,
actionIdentifier: string
): TemplateRole[] {
const startEntries = (template.start ?? [])
.filter((s) => s.action === actionIdentifier);
return startEntries.map((entry) => {
const roleDef = template.roles?.[entry.role];
const roleObj = typeof roleDef === 'object' ? roleDef : null;
return {
roleId: entry.role,
name: roleObj?.name || entry.role,
description: roleObj?.description,
};
});
}
/**
* Get template name safely.
*
* @param template - The template
* @returns The template name or a default
*/
export function getTemplateName(template: XOTemplate | null | undefined): string {
return template?.name || 'Unknown Template';
}
/**
* Get template description safely.
*
* @param template - The template
* @returns The template description or undefined
*/
export function getTemplateDescription(template: XOTemplate | null | undefined): string | undefined {
return template?.description;
}
/**
* Get action name safely.
*
* @param template - The template containing the action
* @param actionId - The action identifier
* @returns The action name or the action ID as fallback
*/
export function getActionName(
template: XOTemplate | null | undefined,
actionId: string
): string {
return template?.actions?.[actionId]?.name || actionId;
}
/**
* Get action description safely.
*
* @param template - The template containing the action
* @param actionId - The action identifier
* @returns The action description or undefined
*/
export function getActionDescription(
template: XOTemplate | null | undefined,
actionId: string
): string | undefined {
return template?.actions?.[actionId]?.description;
}