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

View File

@@ -0,0 +1,264 @@
/**
* Invitation utility functions.
*
* Pure functions for parsing and formatting invitation data.
* These functions have no React dependencies and can be used
* in both TUI and CLI contexts.
*/
import type { Invitation } from '../services/invitation.js';
import type { XOTemplate } from '@xo-cash/types';
/**
* Color names for invitation states.
* These are semantic color names that can be mapped to actual colors
* by the consuming application (TUI or CLI).
*/
export type StateColorName = 'info' | 'warning' | 'success' | 'error' | 'muted';
/**
* Input data extracted from invitation commits.
*/
export interface InvitationInput {
inputIdentifier?: string;
roleIdentifier?: string;
entityIdentifier: string;
}
/**
* Output data extracted from invitation commits.
*/
export interface InvitationOutput {
outputIdentifier?: string;
roleIdentifier?: string;
valueSatoshis?: bigint;
entityIdentifier: string;
}
/**
* Variable data extracted from invitation commits.
*/
export interface InvitationVariable {
variableIdentifier: string;
value: unknown;
roleIdentifier?: string;
entityIdentifier: string;
}
/**
* Formatted invitation list item data.
*/
export interface FormattedInvitationItem {
/** The display label for the invitation */
label: string;
/** The current status of the invitation */
status: string;
/** The semantic color name for the status */
statusColor: StateColorName;
/** Whether the invitation data is valid */
isValid: boolean;
}
/**
* Get the current state/status of an invitation.
*
* @param invitation - The invitation to get state for
* @returns The status string
*/
export function getInvitationState(invitation: Invitation): string {
return invitation.status;
}
/**
* Get the semantic color name for an invitation state.
*
* @param state - The invitation state string
* @returns A semantic color name
*/
export function getStateColorName(state: string): StateColorName {
switch (state) {
case 'created':
case 'published':
return 'info';
case 'pending':
return 'warning';
case 'ready':
case 'signed':
case 'broadcast':
case 'completed':
return 'success';
case 'expired':
case 'error':
return 'error';
default:
return 'muted';
}
}
/**
* Extract all inputs from invitation commits.
*
* @param invitation - The invitation to extract inputs from
* @returns Array of input data
*/
export function getInvitationInputs(invitation: Invitation): InvitationInput[] {
const inputs: InvitationInput[] = [];
for (const commit of invitation.data.commits || []) {
for (const input of commit.data?.inputs || []) {
inputs.push({
inputIdentifier: input.inputIdentifier,
roleIdentifier: input.roleIdentifier,
entityIdentifier: commit.entityIdentifier,
});
}
}
return inputs;
}
/**
* Extract all outputs from invitation commits.
*
* @param invitation - The invitation to extract outputs from
* @returns Array of output data
*/
export function getInvitationOutputs(invitation: Invitation): InvitationOutput[] {
const outputs: InvitationOutput[] = [];
for (const commit of invitation.data.commits || []) {
for (const output of commit.data?.outputs || []) {
outputs.push({
outputIdentifier: output.outputIdentifier,
roleIdentifier: output.roleIdentifier,
valueSatoshis: output.valueSatoshis,
entityIdentifier: commit.entityIdentifier,
});
}
}
return outputs;
}
/**
* Extract all variables from invitation commits.
*
* @param invitation - The invitation to extract variables from
* @returns Array of variable data
*/
export function getInvitationVariables(invitation: Invitation): InvitationVariable[] {
const variables: InvitationVariable[] = [];
for (const commit of invitation.data.commits || []) {
for (const variable of commit.data?.variables || []) {
variables.push({
variableIdentifier: variable.variableIdentifier,
value: variable.value,
roleIdentifier: variable.roleIdentifier,
entityIdentifier: commit.entityIdentifier,
});
}
}
return variables;
}
/**
* Get the user's role from commits (the role they have accepted).
*
* @param invitation - The invitation to check
* @param userEntityId - The user's entity identifier
* @returns The role identifier if found, null otherwise
*/
export function getUserRole(invitation: Invitation, userEntityId: string | null): string | null {
if (!userEntityId) return null;
for (const commit of invitation.data.commits || []) {
if (commit.entityIdentifier === userEntityId) {
// Check inputs for role
for (const input of commit.data?.inputs || []) {
if (input.roleIdentifier) return input.roleIdentifier;
}
// Check outputs for role
for (const output of commit.data?.outputs || []) {
if (output.roleIdentifier) return output.roleIdentifier;
}
// Check variables for role
for (const variable of commit.data?.variables || []) {
if (variable.roleIdentifier) return variable.roleIdentifier;
}
}
}
return null;
}
/**
* Format an invitation for display in a list.
*
* @param invitation - The invitation to format
* @param template - Optional template for additional info (name)
* @returns Formatted item data for display
*/
export function formatInvitationListItem(
invitation: Invitation,
template?: XOTemplate | null
): FormattedInvitationItem {
// Validate that we have the minimum required data
const invitationId = invitation?.data?.invitationIdentifier;
const actionId = invitation?.data?.actionIdentifier;
if (!invitationId || !actionId) {
return {
label: '',
status: 'error',
statusColor: 'error',
isValid: false,
};
}
const state = getInvitationState(invitation);
const templateName = template?.name ?? 'Unknown';
const shortId = formatInvitationId(invitationId, 8);
return {
label: `[${state}] ${templateName}-${actionId} (${shortId})`,
status: state,
statusColor: getStateColorName(state),
isValid: true,
};
}
/**
* Format an invitation ID for display (truncated).
*
* @param id - The full invitation ID
* @param maxLength - Maximum length for display
* @returns Truncated ID string
*/
export function formatInvitationId(id: string, maxLength: number = 16): string {
if (id.length <= maxLength) return id;
const half = Math.floor((maxLength - 3) / 2);
return `${id.slice(0, half)}...${id.slice(-half)}`;
}
/**
* Get all unique entity identifiers from an invitation's commits.
*
* @param invitation - The invitation to check
* @returns Array of unique entity identifiers
*/
export function getInvitationParticipants(invitation: Invitation): string[] {
const participants = new Set<string>();
for (const commit of invitation.data.commits || []) {
if (commit.entityIdentifier) {
participants.add(commit.entityIdentifier);
}
}
return Array.from(participants);
}
/**
* Check if a user is a participant in an invitation.
*
* @param invitation - The invitation to check
* @param userEntityId - The user's entity identifier
* @returns True if the user has made at least one commit
*/
export function isUserParticipant(invitation: Invitation, userEntityId: string | null): boolean {
if (!userEntityId) return false;
return getInvitationParticipants(invitation).includes(userEntityId);
}