Pane 2 mit Edit, Delete, sort, etc.
This commit is contained in:
parent
8ba098c780
commit
684217bdf4
233
src/interview/loopState.ts
Normal file
233
src/interview/loopState.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -183,6 +183,14 @@ function parseStep(raw: Record<string, unknown>): InterviewStep | null {
|
|||
step.output = { join: output.join };
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ui.commit
|
||||
if (raw.ui && typeof raw.ui === "object") {
|
||||
const ui = raw.ui as Record<string, unknown>;
|
||||
if (ui.commit === "explicit_add" || ui.commit === "on_next") {
|
||||
step.ui = { commit: ui.commit };
|
||||
}
|
||||
}
|
||||
|
||||
for (const itemRaw of nestedSteps) {
|
||||
if (!itemRaw || typeof itemRaw !== "object") {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ export interface LoopStep {
|
|||
output?: {
|
||||
join?: string; // String to join items (default: "\n\n")
|
||||
};
|
||||
ui?: {
|
||||
commit?: "explicit_add" | "on_next"; // Commit mode (default: "explicit_add")
|
||||
};
|
||||
}
|
||||
|
||||
export interface LLMDialogStep {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import type { InterviewProfile, InterviewStep } from "./types";
|
||||
import type { LoopRuntimeState } from "./loopState";
|
||||
|
||||
export interface WizardState {
|
||||
profile: InterviewProfile;
|
||||
currentStepIndex: number;
|
||||
stepHistory: number[]; // Stack for back navigation
|
||||
collectedData: Map<string, unknown>; // key -> value
|
||||
loopContexts: Map<string, unknown[]>; // loop key -> array of collected items
|
||||
loopContexts: Map<string, unknown[]>; // loop key -> array of collected items (deprecated, use loopRuntimeStates)
|
||||
loopRuntimeStates: Map<string, LoopRuntimeState>; // loop step key -> runtime state
|
||||
patches: Patch[]; // Collected patches to apply
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +33,8 @@ export function createWizardState(profile: InterviewProfile): WizardState {
|
|||
currentStepIndex: 0,
|
||||
stepHistory: [],
|
||||
collectedData: new Map(),
|
||||
loopContexts: new Map(),
|
||||
loopContexts: new Map(), // Keep for backwards compatibility
|
||||
loopRuntimeStates: new Map(),
|
||||
patches: [],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
383
src/tests/interview/loopState.test.ts
Normal file
383
src/tests/interview/loopState.test.ts
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createLoopState,
|
||||
setDraftField,
|
||||
isDraftDirty,
|
||||
commitDraft,
|
||||
startEdit,
|
||||
deleteItem,
|
||||
moveItemUp,
|
||||
moveItemDown,
|
||||
clearDraft,
|
||||
type LoopRuntimeState,
|
||||
} from "../../interview/loopState";
|
||||
|
||||
describe("loopState", () => {
|
||||
describe("createLoopState", () => {
|
||||
it("should create initial empty state", () => {
|
||||
const state = createLoopState();
|
||||
expect(state.items).toEqual([]);
|
||||
expect(state.draft).toEqual({});
|
||||
expect(state.editIndex).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDraftField", () => {
|
||||
it("should set a field in draft", () => {
|
||||
const state = createLoopState();
|
||||
const newState = setDraftField(state, "field1", "value1");
|
||||
expect(newState.draft.field1).toBe("value1");
|
||||
expect(newState.items).toEqual([]);
|
||||
expect(newState.editIndex).toBeNull();
|
||||
});
|
||||
|
||||
it("should update existing field", () => {
|
||||
const state = createLoopState();
|
||||
let newState = setDraftField(state, "field1", "value1");
|
||||
newState = setDraftField(newState, "field1", "value2");
|
||||
expect(newState.draft.field1).toBe("value2");
|
||||
});
|
||||
|
||||
it("should not mutate original state", () => {
|
||||
const state = createLoopState();
|
||||
setDraftField(state, "field1", "value1");
|
||||
expect(state.draft).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDraftDirty", () => {
|
||||
it("should return false for empty draft", () => {
|
||||
expect(isDraftDirty({})).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for non-empty string", () => {
|
||||
expect(isDraftDirty({ text: "content" })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for empty string", () => {
|
||||
expect(isDraftDirty({ text: "" })).toBe(false);
|
||||
expect(isDraftDirty({ text: " " })).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for boolean true", () => {
|
||||
expect(isDraftDirty({ flag: true })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for boolean false", () => {
|
||||
expect(isDraftDirty({ flag: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for non-zero number", () => {
|
||||
expect(isDraftDirty({ count: 5 })).toBe(true);
|
||||
expect(isDraftDirty({ count: -1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for zero", () => {
|
||||
expect(isDraftDirty({ count: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for non-empty array", () => {
|
||||
expect(isDraftDirty({ items: [1, 2] })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for empty array", () => {
|
||||
expect(isDraftDirty({ items: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for non-empty object", () => {
|
||||
expect(isDraftDirty({ obj: { key: "value" } })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for empty object", () => {
|
||||
expect(isDraftDirty({ obj: {} })).toBe(false);
|
||||
});
|
||||
|
||||
it("should ignore null and undefined", () => {
|
||||
expect(isDraftDirty({ nullVal: null, undefinedVal: undefined })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("commitDraft", () => {
|
||||
it("should add new item when editIndex is null", () => {
|
||||
const state = createLoopState();
|
||||
let newState = setDraftField(state, "field1", "value1");
|
||||
newState = commitDraft(newState);
|
||||
|
||||
expect(newState.items.length).toBe(1);
|
||||
expect(newState.items[0]).toEqual({ field1: "value1" });
|
||||
expect(newState.draft).toEqual({});
|
||||
expect(newState.editIndex).toBeNull();
|
||||
});
|
||||
|
||||
it("should update existing item when editIndex is set", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ field1: "old" }],
|
||||
draft: { field1: "new" },
|
||||
editIndex: 0,
|
||||
};
|
||||
|
||||
const newState = commitDraft(state);
|
||||
|
||||
expect(newState.items.length).toBe(1);
|
||||
expect(newState.items[0]).toEqual({ field1: "new" });
|
||||
expect(newState.draft).toEqual({});
|
||||
expect(newState.editIndex).toBeNull();
|
||||
});
|
||||
|
||||
it("should not commit if draft is not dirty", () => {
|
||||
const state = createLoopState();
|
||||
const newState = commitDraft(state);
|
||||
|
||||
expect(newState.items.length).toBe(0);
|
||||
expect(newState.draft).toEqual({});
|
||||
});
|
||||
|
||||
it("should not mutate original state", () => {
|
||||
const state = createLoopState();
|
||||
let newState = setDraftField(state, "field1", "value1");
|
||||
commitDraft(newState);
|
||||
expect(newState.draft).toEqual({ field1: "value1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("startEdit", () => {
|
||||
it("should load item into draft and set editIndex", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ field1: "value1", field2: "value2" }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = startEdit(state, 0);
|
||||
|
||||
expect(newState.draft).toEqual({ field1: "value1", field2: "value2" });
|
||||
expect(newState.editIndex).toBe(0);
|
||||
expect(newState.items.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle invalid index", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ field1: "value1" }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = startEdit(state, 5);
|
||||
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
|
||||
it("should not mutate original state", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ field1: "value1" }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
startEdit(state, 0);
|
||||
expect(state.draft).toEqual({});
|
||||
expect(state.editIndex).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteItem", () => {
|
||||
it("should delete item at index", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = deleteItem(state, 1);
|
||||
|
||||
expect(newState.items.length).toBe(2);
|
||||
expect(newState.items[0]).toEqual({ id: 1 });
|
||||
expect(newState.items[1]).toEqual({ id: 3 });
|
||||
});
|
||||
|
||||
it("should clear draft if editing deleted item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
draft: { id: 2 },
|
||||
editIndex: 1,
|
||||
};
|
||||
|
||||
const newState = deleteItem(state, 1);
|
||||
|
||||
expect(newState.items.length).toBe(1);
|
||||
expect(newState.editIndex).toBeNull();
|
||||
expect(newState.draft).toEqual({});
|
||||
});
|
||||
|
||||
it("should adjust editIndex when deleting before edited item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
draft: { id: 3 },
|
||||
editIndex: 2,
|
||||
};
|
||||
|
||||
const newState = deleteItem(state, 1);
|
||||
|
||||
expect(newState.items.length).toBe(2);
|
||||
expect(newState.editIndex).toBe(1); // Adjusted from 2 to 1
|
||||
});
|
||||
|
||||
it("should handle invalid index", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = deleteItem(state, 5);
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveItemUp", () => {
|
||||
it("should move item up", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemUp(state, 1);
|
||||
|
||||
expect(newState.items[0]).toEqual({ id: 2 });
|
||||
expect(newState.items[1]).toEqual({ id: 1 });
|
||||
expect(newState.items[2]).toEqual({ id: 3 });
|
||||
});
|
||||
|
||||
it("should adjust editIndex when moving edited item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
draft: { id: 2 },
|
||||
editIndex: 1,
|
||||
};
|
||||
|
||||
const newState = moveItemUp(state, 1);
|
||||
|
||||
expect(newState.items[0]).toEqual({ id: 2 });
|
||||
expect(newState.editIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("should adjust editIndex when moving item before edited item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
draft: { id: 3 },
|
||||
editIndex: 2,
|
||||
};
|
||||
|
||||
const newState = moveItemUp(state, 1);
|
||||
|
||||
// After moving item 1 up: [id:2, id:1, id:3]
|
||||
expect(newState.items[0]).toEqual({ id: 2 });
|
||||
expect(newState.items[1]).toEqual({ id: 1 });
|
||||
expect(newState.items[2]).toEqual({ id: 3 }); // Item 3 stays at index 2
|
||||
expect(newState.editIndex).toBe(2); // Still editing item 3, still at index 2
|
||||
});
|
||||
|
||||
it("should not move first item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemUp(state, 0);
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
|
||||
it("should handle invalid index", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemUp(state, 5);
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveItemDown", () => {
|
||||
it("should move item down", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemDown(state, 1);
|
||||
|
||||
expect(newState.items[0]).toEqual({ id: 1 });
|
||||
expect(newState.items[1]).toEqual({ id: 3 });
|
||||
expect(newState.items[2]).toEqual({ id: 2 });
|
||||
});
|
||||
|
||||
it("should adjust editIndex when moving edited item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
draft: { id: 1 },
|
||||
editIndex: 0,
|
||||
};
|
||||
|
||||
const newState = moveItemDown(state, 0);
|
||||
|
||||
expect(newState.items[0]).toEqual({ id: 2 });
|
||||
expect(newState.items[1]).toEqual({ id: 1 });
|
||||
expect(newState.editIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("should not move last item", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemDown(state, 1);
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
|
||||
it("should handle invalid index", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
|
||||
const newState = moveItemDown(state, 5);
|
||||
expect(newState).toBe(state); // Should return unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearDraft", () => {
|
||||
it("should clear draft and reset editIndex", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [{ id: 1 }],
|
||||
draft: { field1: "value1" },
|
||||
editIndex: 0,
|
||||
};
|
||||
|
||||
const newState = clearDraft(state);
|
||||
|
||||
expect(newState.draft).toEqual({});
|
||||
expect(newState.editIndex).toBeNull();
|
||||
expect(newState.items.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should not mutate original state", () => {
|
||||
const state: LoopRuntimeState = {
|
||||
items: [],
|
||||
draft: { field1: "value1" },
|
||||
editIndex: 0,
|
||||
};
|
||||
|
||||
clearDraft(state);
|
||||
expect(state.draft).toEqual({ field1: "value1" });
|
||||
expect(state.editIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -26,6 +26,18 @@ import {
|
|||
createMarkdownToolbar,
|
||||
} from "./markdownToolbar";
|
||||
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
||||
import {
|
||||
type LoopRuntimeState,
|
||||
createLoopState,
|
||||
setDraftField,
|
||||
isDraftDirty,
|
||||
commitDraft,
|
||||
startEdit,
|
||||
deleteItem,
|
||||
moveItemUp,
|
||||
moveItemDown,
|
||||
clearDraft,
|
||||
} from "../interview/loopState";
|
||||
|
||||
export interface WizardResult {
|
||||
applied: boolean;
|
||||
|
|
@ -580,308 +592,428 @@ export class InterviewWizardModal extends Modal {
|
|||
renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void {
|
||||
if (step.type !== "loop") return;
|
||||
|
||||
const items = this.state.loopContexts.get(step.key) || [];
|
||||
const currentItemIndex = items.length; // Next item index
|
||||
// Initialize or get loop runtime state
|
||||
let loopState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (!loopState) {
|
||||
// Initialize from legacy loopContexts if available
|
||||
const legacyItems = this.state.loopContexts.get(step.key) || [];
|
||||
loopState = {
|
||||
items: legacyItems,
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
};
|
||||
this.state.loopRuntimeStates.set(step.key, loopState);
|
||||
}
|
||||
|
||||
const commitMode = step.ui?.commit || "explicit_add";
|
||||
|
||||
console.log("Render loop step", {
|
||||
stepKey: step.key,
|
||||
stepLabel: step.label,
|
||||
itemsCount: items.length,
|
||||
itemsCount: loopState.items.length,
|
||||
nestedStepsCount: step.items.length,
|
||||
nestedStepTypes: step.items.map(s => s.type),
|
||||
currentItemIndex: currentItemIndex,
|
||||
editIndex: loopState.editIndex,
|
||||
commitMode: commitMode,
|
||||
});
|
||||
|
||||
// Title
|
||||
containerEl.createEl("h2", {
|
||||
text: step.label || "Loop",
|
||||
});
|
||||
|
||||
// Show existing items
|
||||
if (items.length > 0) {
|
||||
const itemsList = containerEl.createEl("div", { cls: "loop-items-list" });
|
||||
itemsList.style.width = "100%";
|
||||
itemsList.createEl("h3", { text: `Gesammelte Items (${items.length}):` });
|
||||
const ul = itemsList.createEl("ul");
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const li = ul.createEl("li");
|
||||
li.createSpan({ text: `Item ${i + 1}: ` });
|
||||
// Show editing indicator
|
||||
if (loopState.editIndex !== null) {
|
||||
const indicator = containerEl.createEl("div", {
|
||||
cls: "loop-editing-indicator",
|
||||
text: `✏️ Editing item ${loopState.editIndex + 1}`,
|
||||
});
|
||||
indicator.style.padding = "0.5em";
|
||||
indicator.style.background = "var(--background-modifier-border-hover)";
|
||||
indicator.style.borderRadius = "4px";
|
||||
indicator.style.marginBottom = "1em";
|
||||
}
|
||||
|
||||
// 2-pane container
|
||||
const paneContainer = containerEl.createEl("div", {
|
||||
cls: "loop-pane-container",
|
||||
});
|
||||
paneContainer.style.display = "flex";
|
||||
paneContainer.style.gap = "1em";
|
||||
paneContainer.style.width = "100%";
|
||||
paneContainer.style.minHeight = "400px";
|
||||
|
||||
// Left pane: Items list
|
||||
const leftPane = paneContainer.createEl("div", {
|
||||
cls: "loop-items-pane",
|
||||
});
|
||||
leftPane.style.width = "40%";
|
||||
leftPane.style.minWidth = "250px";
|
||||
leftPane.style.borderRight = "1px solid var(--background-modifier-border)";
|
||||
leftPane.style.paddingRight = "1em";
|
||||
leftPane.style.overflowY = "auto";
|
||||
leftPane.style.maxHeight = "600px";
|
||||
|
||||
leftPane.createEl("h3", {
|
||||
text: `Items (${loopState.items.length})`,
|
||||
});
|
||||
|
||||
// Items list
|
||||
const itemsList = leftPane.createEl("div", {
|
||||
cls: "loop-items-list",
|
||||
});
|
||||
|
||||
if (loopState.items.length === 0) {
|
||||
itemsList.createEl("p", {
|
||||
text: "No items yet. Use the editor on the right to add items.",
|
||||
cls: "interview-note",
|
||||
});
|
||||
} else {
|
||||
for (let i = 0; i < loopState.items.length; i++) {
|
||||
const item = loopState.items[i];
|
||||
const itemEl = itemsList.createEl("div", {
|
||||
cls: `loop-item-entry ${loopState.editIndex === i ? "is-editing" : ""}`,
|
||||
});
|
||||
itemEl.style.padding = "0.5em";
|
||||
itemEl.style.marginBottom = "0.5em";
|
||||
itemEl.style.border = "1px solid var(--background-modifier-border)";
|
||||
itemEl.style.borderRadius = "4px";
|
||||
if (loopState.editIndex === i) {
|
||||
itemEl.style.background = "var(--background-modifier-border-hover)";
|
||||
}
|
||||
|
||||
// Item preview
|
||||
const preview = itemEl.createEl("div", {
|
||||
cls: "loop-item-preview",
|
||||
});
|
||||
preview.style.marginBottom = "0.5em";
|
||||
|
||||
if (typeof item === "object" && item !== null) {
|
||||
// Show readable format instead of JSON
|
||||
const itemEntries = Object.entries(item);
|
||||
const itemEntries = Object.entries(item as Record<string, unknown>);
|
||||
if (itemEntries.length > 0) {
|
||||
const itemText = itemEntries
|
||||
const previewText = itemEntries
|
||||
.map(([key, value]) => {
|
||||
const nestedStep = step.items.find(s => s.key === key);
|
||||
const label = nestedStep?.label || key;
|
||||
return `${label}: ${String(value).substring(0, 50)}${String(value).length > 50 ? "..." : ""}`;
|
||||
const strValue = String(value);
|
||||
return `${label}: ${strValue.substring(0, 40)}${strValue.length > 40 ? "..." : ""}`;
|
||||
})
|
||||
.join(", ");
|
||||
li.createSpan({ text: itemText });
|
||||
preview.createSpan({ text: previewText });
|
||||
} else {
|
||||
li.createSpan({ text: "Empty" });
|
||||
preview.createSpan({ text: "Empty item" });
|
||||
}
|
||||
} else {
|
||||
li.createSpan({ text: String(item) });
|
||||
preview.createSpan({ text: String(item) });
|
||||
}
|
||||
|
||||
// Actions
|
||||
const actions = itemEl.createEl("div", {
|
||||
cls: "loop-item-actions",
|
||||
});
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "0.25em";
|
||||
actions.style.flexWrap = "wrap";
|
||||
|
||||
// Edit button
|
||||
const editBtn = actions.createEl("button", {
|
||||
text: "Edit",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
editBtn.style.fontSize = "0.85em";
|
||||
editBtn.style.padding = "0.25em 0.5em";
|
||||
editBtn.onclick = () => {
|
||||
const newState = startEdit(loopState!, i);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
this.renderStep();
|
||||
};
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = actions.createEl("button", {
|
||||
text: "Delete",
|
||||
});
|
||||
deleteBtn.style.fontSize = "0.85em";
|
||||
deleteBtn.style.padding = "0.25em 0.5em";
|
||||
deleteBtn.onclick = () => {
|
||||
if (confirm(`Delete item ${i + 1}?`)) {
|
||||
const newState = deleteItem(loopState!, i);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
// Update answers
|
||||
this.state.loopContexts.set(step.key, newState.items);
|
||||
this.renderStep();
|
||||
}
|
||||
};
|
||||
|
||||
// Move Up button
|
||||
const moveUpBtn = actions.createEl("button", {
|
||||
text: "↑",
|
||||
});
|
||||
moveUpBtn.style.fontSize = "0.85em";
|
||||
moveUpBtn.style.padding = "0.25em 0.5em";
|
||||
moveUpBtn.disabled = i === 0;
|
||||
moveUpBtn.onclick = () => {
|
||||
const newState = moveItemUp(loopState!, i);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
this.state.loopContexts.set(step.key, newState.items);
|
||||
this.renderStep();
|
||||
};
|
||||
|
||||
// Move Down button
|
||||
const moveDownBtn = actions.createEl("button", {
|
||||
text: "↓",
|
||||
});
|
||||
moveDownBtn.style.fontSize = "0.85em";
|
||||
moveDownBtn.style.padding = "0.25em 0.5em";
|
||||
moveDownBtn.disabled = i === loopState.items.length - 1;
|
||||
moveDownBtn.onclick = () => {
|
||||
const newState = moveItemDown(loopState!, i);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
this.state.loopContexts.set(step.key, newState.items);
|
||||
this.renderStep();
|
||||
};
|
||||
}
|
||||
itemsList.createEl("p", {
|
||||
text: "Sie können weitere Items hinzufügen oder mit 'Next' fortfahren.",
|
||||
cls: "interview-note",
|
||||
});
|
||||
}
|
||||
|
||||
// Render nested steps for current item
|
||||
// Right pane: Editor
|
||||
const rightPane = paneContainer.createEl("div", {
|
||||
cls: "loop-editor-pane",
|
||||
});
|
||||
rightPane.style.width = "60%";
|
||||
rightPane.style.flex = "1";
|
||||
|
||||
const editorTitle = rightPane.createEl("h3", {
|
||||
text: loopState.editIndex !== null
|
||||
? `Edit Item ${loopState.editIndex + 1}`
|
||||
: "New Item",
|
||||
});
|
||||
|
||||
// Render nested steps bound to draft
|
||||
if (step.items.length > 0) {
|
||||
const itemFormContainer = containerEl.createEl("div", {
|
||||
cls: "loop-item-form",
|
||||
});
|
||||
itemFormContainer.style.width = "100%";
|
||||
|
||||
itemFormContainer.createEl("h3", {
|
||||
text: items.length === 0 ? "First Item" : `Item ${items.length + 1}`,
|
||||
const editorContainer = rightPane.createEl("div", {
|
||||
cls: "loop-item-editor",
|
||||
});
|
||||
|
||||
// Create a temporary state for the current loop item
|
||||
const itemDataKey = `${step.key}_item_${currentItemIndex}`;
|
||||
const itemData = new Map<string, unknown>();
|
||||
|
||||
// Render each nested step
|
||||
for (const nestedStep of step.items) {
|
||||
if (nestedStep.type === "capture_text") {
|
||||
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
|
||||
// Field container with vertical layout
|
||||
const fieldContainer = itemFormContainer.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
if (nestedStep.label) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: nestedStep.label,
|
||||
});
|
||||
const draftValue = loopState.draft[nestedStep.key];
|
||||
this.renderLoopNestedStep(nestedStep, editorContainer, step.key, draftValue, (fieldId, value) => {
|
||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (currentState) {
|
||||
const newState = setDraftField(currentState, fieldId, value);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Editor container
|
||||
const editorContainer = fieldContainer.createEl("div", {
|
||||
cls: "markdown-editor-container",
|
||||
});
|
||||
editorContainer.style.width = "100%";
|
||||
editorContainer.style.position = "relative";
|
||||
|
||||
const textEditorContainer = editorContainer.createEl("div", {
|
||||
cls: "markdown-editor-wrapper",
|
||||
});
|
||||
textEditorContainer.style.width = "100%";
|
||||
|
||||
const textSetting = new Setting(textEditorContainer);
|
||||
textSetting.settingEl.style.width = "100%";
|
||||
textSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label from Setting component
|
||||
const settingNameEl = textSetting.settingEl.querySelector(".setting-item-name") as HTMLElement;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
textSetting.addTextArea((text) => {
|
||||
text.setValue(existingValue);
|
||||
this.currentInputValues.set(inputKey, existingValue);
|
||||
|
||||
text.onChange((value) => {
|
||||
this.currentInputValues.set(inputKey, value);
|
||||
itemData.set(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.rows = 10;
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.minHeight = "200px";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
|
||||
// Add toolbar for loop item textarea
|
||||
setTimeout(() => {
|
||||
const textarea = textEditorContainer.querySelector("textarea");
|
||||
if (textarea) {
|
||||
const itemToolbar = createMarkdownToolbar(textarea);
|
||||
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
||||
}
|
||||
}, 10);
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
|
||||
// Field container with vertical layout
|
||||
const fieldContainer = itemFormContainer.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
if (nestedStep.label) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: nestedStep.label,
|
||||
});
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Input container
|
||||
const inputContainer = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__input",
|
||||
});
|
||||
inputContainer.style.width = "100%";
|
||||
|
||||
const fieldSetting = new Setting(inputContainer);
|
||||
fieldSetting.settingEl.style.width = "100%";
|
||||
fieldSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label from Setting component
|
||||
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
fieldSetting.addText((text) => {
|
||||
text.setValue(existingValue);
|
||||
this.currentInputValues.set(inputKey, existingValue);
|
||||
|
||||
text.onChange((value) => {
|
||||
this.currentInputValues.set(inputKey, value);
|
||||
itemData.set(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
} else if (nestedStep.type === "capture_frontmatter") {
|
||||
const existingValue = (itemData.get(nestedStep.key) as string) || "";
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
|
||||
// Field container with vertical layout
|
||||
const fieldContainer = itemFormContainer.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
const labelText = nestedStep.label || nestedStep.field || nestedStep.key;
|
||||
if (labelText) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: labelText,
|
||||
});
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Input container
|
||||
const inputContainer = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__input",
|
||||
});
|
||||
inputContainer.style.width = "100%";
|
||||
|
||||
const fieldSetting = new Setting(inputContainer);
|
||||
fieldSetting.settingEl.style.width = "100%";
|
||||
fieldSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label from Setting component
|
||||
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
fieldSetting.addText((text) => {
|
||||
text.setValue(existingValue);
|
||||
this.currentInputValues.set(inputKey, existingValue);
|
||||
|
||||
text.onChange((value) => {
|
||||
this.currentInputValues.set(inputKey, value);
|
||||
itemData.set(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add item button
|
||||
const addButtonSetting = new Setting(itemFormContainer);
|
||||
addButtonSetting.settingEl.style.width = "100%";
|
||||
addButtonSetting.addButton((button) => {
|
||||
button
|
||||
.setButtonText("Add Item")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
// Collect all item data
|
||||
const itemDataObj: Record<string, unknown> = {};
|
||||
let hasData = false;
|
||||
|
||||
for (const nestedStep of step.items) {
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
const value = this.currentInputValues.get(inputKey);
|
||||
if (value !== undefined && String(value).trim()) {
|
||||
itemDataObj[nestedStep.key] = value;
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
new Notice("Please enter at least one field before adding an item");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to loop context
|
||||
items.push(itemDataObj);
|
||||
this.state.loopContexts.set(step.key, items);
|
||||
|
||||
// Clear input values for this item
|
||||
for (const nestedStep of step.items) {
|
||||
const inputKey = `${itemDataKey}_${nestedStep.key}`;
|
||||
this.currentInputValues.delete(inputKey);
|
||||
}
|
||||
|
||||
console.log("Loop item added", {
|
||||
stepKey: step.key,
|
||||
itemIndex: items.length - 1,
|
||||
itemData: itemDataObj,
|
||||
totalItems: items.length,
|
||||
});
|
||||
|
||||
new Notice(`Item ${items.length} added. You can add more or click Next to continue.`);
|
||||
|
||||
// Re-render to show new item and reset form
|
||||
this.renderStep();
|
||||
});
|
||||
});
|
||||
|
||||
// Show hint if no items yet
|
||||
if (items.length === 0) {
|
||||
itemFormContainer.createEl("p", {
|
||||
text: "⚠️ Please add at least one item before continuing",
|
||||
cls: "interview-warning",
|
||||
});
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
const buttonContainer = rightPane.createEl("div", {
|
||||
cls: "loop-editor-actions",
|
||||
});
|
||||
buttonContainer.style.display = "flex";
|
||||
buttonContainer.style.gap = "0.5em";
|
||||
buttonContainer.style.marginTop = "1em";
|
||||
|
||||
// Add/Save Item button
|
||||
const commitBtn = buttonContainer.createEl("button", {
|
||||
text: loopState.editIndex !== null ? "Save Item" : "Add Item",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
commitBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (currentState && isDraftDirty(currentState.draft)) {
|
||||
const newState = commitDraft(currentState);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
// Update answers
|
||||
this.state.loopContexts.set(step.key, newState.items);
|
||||
this.renderStep();
|
||||
} else {
|
||||
new Notice("Please enter at least one field");
|
||||
}
|
||||
};
|
||||
|
||||
// Clear button
|
||||
if (loopState.editIndex !== null || isDraftDirty(loopState.draft)) {
|
||||
const clearBtn = buttonContainer.createEl("button", {
|
||||
text: "Clear",
|
||||
});
|
||||
clearBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (currentState) {
|
||||
const newState = clearDraft(currentState);
|
||||
this.state.loopRuntimeStates.set(step.key, newState);
|
||||
this.renderStep();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a nested step within a loop editor.
|
||||
*/
|
||||
private renderLoopNestedStep(
|
||||
nestedStep: InterviewStep,
|
||||
containerEl: HTMLElement,
|
||||
loopKey: string,
|
||||
draftValue: unknown,
|
||||
onFieldChange: (fieldId: string, value: unknown) => void
|
||||
): void {
|
||||
const existingValue = draftValue !== undefined ? String(draftValue) : "";
|
||||
|
||||
if (nestedStep.type === "capture_text") {
|
||||
// Field container
|
||||
const fieldContainer = containerEl.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
if (nestedStep.label) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: nestedStep.label,
|
||||
});
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Editor container
|
||||
const editorContainer = fieldContainer.createEl("div", {
|
||||
cls: "markdown-editor-container",
|
||||
});
|
||||
editorContainer.style.width = "100%";
|
||||
editorContainer.style.position = "relative";
|
||||
|
||||
const textEditorContainer = editorContainer.createEl("div", {
|
||||
cls: "markdown-editor-wrapper",
|
||||
});
|
||||
textEditorContainer.style.width = "100%";
|
||||
|
||||
const textSetting = new Setting(textEditorContainer);
|
||||
textSetting.settingEl.style.width = "100%";
|
||||
textSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label
|
||||
const settingNameEl = textSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
textSetting.addTextArea((text) => {
|
||||
text.setValue(existingValue);
|
||||
text.onChange((value) => {
|
||||
onFieldChange(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.rows = 8;
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.minHeight = "150px";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
|
||||
// Add toolbar
|
||||
setTimeout(() => {
|
||||
const textarea = textEditorContainer.querySelector("textarea");
|
||||
if (textarea) {
|
||||
const itemToolbar = createMarkdownToolbar(textarea);
|
||||
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
||||
}
|
||||
}, 10);
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
// Field container
|
||||
const fieldContainer = containerEl.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
if (nestedStep.label) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: nestedStep.label,
|
||||
});
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Input container
|
||||
const inputContainer = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__input",
|
||||
});
|
||||
inputContainer.style.width = "100%";
|
||||
|
||||
const fieldSetting = new Setting(inputContainer);
|
||||
fieldSetting.settingEl.style.width = "100%";
|
||||
fieldSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label
|
||||
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
fieldSetting.addText((text) => {
|
||||
text.setValue(existingValue);
|
||||
text.onChange((value) => {
|
||||
onFieldChange(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
} else if (nestedStep.type === "capture_frontmatter") {
|
||||
// Field container
|
||||
const fieldContainer = containerEl.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
|
||||
// Label
|
||||
const labelText = nestedStep.label || nestedStep.field || nestedStep.key;
|
||||
if (labelText) {
|
||||
const labelEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__label",
|
||||
text: labelText,
|
||||
});
|
||||
}
|
||||
|
||||
// Description/Prompt
|
||||
if (nestedStep.prompt) {
|
||||
const descEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: nestedStep.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Input container
|
||||
const inputContainer = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__input",
|
||||
});
|
||||
inputContainer.style.width = "100%";
|
||||
|
||||
const fieldSetting = new Setting(inputContainer);
|
||||
fieldSetting.settingEl.style.width = "100%";
|
||||
fieldSetting.controlEl.style.width = "100%";
|
||||
|
||||
// Hide the default label
|
||||
const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null;
|
||||
if (settingNameEl) {
|
||||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
fieldSetting.addText((text) => {
|
||||
text.setValue(existingValue);
|
||||
text.onChange((value) => {
|
||||
onFieldChange(nestedStep.key, value);
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -979,6 +1111,13 @@ export class InterviewWizardModal extends Modal {
|
|||
li.createSpan({ text: String(value) });
|
||||
}
|
||||
|
||||
// Show loop items (from runtime states)
|
||||
for (const [loopKey, loopState] of this.state.loopRuntimeStates.entries()) {
|
||||
const loopLi = dataList.createEl("li");
|
||||
loopLi.createEl("strong", { text: `${loopKey} (${loopState.items.length} items): ` });
|
||||
loopLi.createSpan({ text: `${loopState.items.length} committed items` });
|
||||
}
|
||||
|
||||
// Show patches
|
||||
const patchesList = containerEl.createEl("ul");
|
||||
for (const patch of this.state.patches) {
|
||||
|
|
@ -1016,8 +1155,12 @@ export class InterviewWizardModal extends Modal {
|
|||
const isReview = step?.type === "review";
|
||||
const isLoop = step?.type === "loop";
|
||||
|
||||
// For loop steps, disable Next until at least one item is added
|
||||
const loopItems = isLoop ? (this.state.loopContexts.get(step.key) || []) : [];
|
||||
// For loop steps, check if we have items (from runtime state or legacy context)
|
||||
let loopItems: unknown[] = [];
|
||||
if (isLoop) {
|
||||
const loopState = this.state.loopRuntimeStates.get(step.key);
|
||||
loopItems = loopState ? loopState.items : (this.state.loopContexts.get(step.key) || []);
|
||||
}
|
||||
const canProceedLoop = !isLoop || loopItems.length > 0;
|
||||
|
||||
button
|
||||
|
|
@ -1061,6 +1204,24 @@ export class InterviewWizardModal extends Modal {
|
|||
goNext(): void {
|
||||
const currentStep = getCurrentStep(this.state);
|
||||
|
||||
// Handle loop commit mode
|
||||
if (currentStep && currentStep.type === "loop") {
|
||||
const loopState = this.state.loopRuntimeStates.get(currentStep.key);
|
||||
const commitMode = currentStep.ui?.commit || "explicit_add";
|
||||
|
||||
// In on_next mode, auto-commit dirty draft
|
||||
if (commitMode === "on_next" && loopState && isDraftDirty(loopState.draft)) {
|
||||
const newState = commitDraft(loopState);
|
||||
this.state.loopRuntimeStates.set(currentStep.key, newState);
|
||||
// Update answers
|
||||
this.state.loopContexts.set(currentStep.key, newState.items);
|
||||
console.log("Auto-committed draft on Next", {
|
||||
stepKey: currentStep.key,
|
||||
itemsCount: newState.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save current step data before navigating
|
||||
if (currentStep) {
|
||||
this.saveCurrentStepData(currentStep);
|
||||
|
|
@ -1164,6 +1325,11 @@ export class InterviewWizardModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
// Sync loopRuntimeStates to loopContexts for renderer
|
||||
for (const [loopKey, loopState] of this.state.loopRuntimeStates.entries()) {
|
||||
this.state.loopContexts.set(loopKey, loopState.items);
|
||||
}
|
||||
|
||||
// Use renderer to generate markdown from collected data
|
||||
const answers: RenderAnswers = {
|
||||
collectedData: this.state.collectedData,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user