/** * Loop runtime state management. * Pure functions for managing loop item state (draft, committed items, editing). */ export interface LoopRuntimeState { items: unknown[]; // Committed items draft: Record; // Current draft being edited editIndex: number | null; // Index of item being edited, or null for new item } /** * Create initial loop state. */ export function createLoopState(): LoopRuntimeState { return { items: [], draft: {}, editIndex: null, }; } /** * Set a field value in the draft. */ export function setDraftField( state: LoopRuntimeState, fieldId: string, value: unknown ): LoopRuntimeState { return { ...state, draft: { ...state.draft, [fieldId]: value, }, }; } /** * Check if draft has any non-empty data (is dirty). * Returns true if draft contains any non-empty string, true, non-zero number, non-empty array, or non-empty object. */ export function isDraftDirty(draft: Record): boolean { for (const [key, value] of Object.entries(draft)) { if (value === null || value === undefined) { continue; } if (typeof value === "string" && value.trim() !== "") { return true; } if (typeof value === "boolean" && value === true) { return true; } if (typeof value === "number" && value !== 0) { return true; } if (Array.isArray(value) && value.length > 0) { return true; } if (typeof value === "object" && Object.keys(value).length > 0) { return true; } } return false; } /** * Commit draft to items (add new or update existing). */ export function commitDraft(state: LoopRuntimeState): LoopRuntimeState { // If draft is not dirty, return state unchanged if (!isDraftDirty(state.draft)) { return state; } const newItems = [...state.items]; if (state.editIndex !== null && state.editIndex >= 0 && state.editIndex < newItems.length) { // Update existing item newItems[state.editIndex] = { ...state.draft }; } else { // Add new item newItems.push({ ...state.draft }); } return { items: newItems, draft: {}, editIndex: null, }; } /** * Start editing an existing item (load into draft). */ export function startEdit( state: LoopRuntimeState, index: number ): LoopRuntimeState { if (index < 0 || index >= state.items.length) { return state; // Invalid index, return unchanged } const item = state.items[index]; if (!item || typeof item !== "object") { return state; } return { ...state, draft: { ...(item as Record) }, editIndex: index, }; } /** * Delete an item at the given index. */ export function deleteItem( state: LoopRuntimeState, index: number ): LoopRuntimeState { if (index < 0 || index >= state.items.length) { return state; // Invalid index, return unchanged } const newItems = [...state.items]; newItems.splice(index, 1); // If we were editing the deleted item or an item after it, adjust editIndex let newEditIndex = state.editIndex; if (state.editIndex !== null) { if (state.editIndex === index) { // Deleted the item we were editing newEditIndex = null; } else if (state.editIndex > index) { // Deleted an item before the one we're editing, adjust index newEditIndex = state.editIndex - 1; } } return { ...state, items: newItems, editIndex: newEditIndex, // Clear draft if we were editing the deleted item draft: newEditIndex === null ? {} : state.draft, }; } /** * Move an item up (swap with previous item). */ export function moveItemUp( state: LoopRuntimeState, index: number ): LoopRuntimeState { if (index <= 0 || index >= state.items.length) { return state; // Cannot move first item up or invalid index } const newItems = [...state.items]; const temp = newItems[index]; newItems[index] = newItems[index - 1]; newItems[index - 1] = temp; // Adjust editIndex if we're editing one of the moved items let newEditIndex = state.editIndex; if (state.editIndex !== null) { if (state.editIndex === index) { newEditIndex = index - 1; } else if (state.editIndex === index - 1) { newEditIndex = index; } } return { ...state, items: newItems, editIndex: newEditIndex, }; } /** * Move an item down (swap with next item). */ export function moveItemDown( state: LoopRuntimeState, index: number ): LoopRuntimeState { if (index < 0 || index >= state.items.length - 1) { return state; // Cannot move last item down or invalid index } const newItems = [...state.items]; const temp = newItems[index]; newItems[index] = newItems[index + 1]; newItems[index + 1] = temp; // Adjust editIndex if we're editing one of the moved items let newEditIndex = state.editIndex; if (state.editIndex !== null) { if (state.editIndex === index) { newEditIndex = index + 1; } else if (state.editIndex === index + 1) { newEditIndex = index; } } return { ...state, items: newItems, editIndex: newEditIndex, }; } /** * Clear draft and exit edit mode. */ export function clearDraft(state: LoopRuntimeState): LoopRuntimeState { return { ...state, draft: {}, editIndex: null, }; }