234 lines
5.3 KiB
TypeScript
234 lines
5.3 KiB
TypeScript
/**
|
|
* Loop runtime state management.
|
|
* Pure functions for managing loop item state (draft, committed items, editing).
|
|
*/
|
|
|
|
export interface LoopRuntimeState {
|
|
items: unknown[]; // Committed items
|
|
draft: Record<string, unknown>; // 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<string, unknown>): 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<string, unknown>) },
|
|
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,
|
|
};
|
|
}
|