Eingebette Loops
This commit is contained in:
parent
684217bdf4
commit
5a171987b2
46
experience_cluster_config.yaml
Normal file
46
experience_cluster_config.yaml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
- key: experience_cluster
|
||||||
|
group: experience
|
||||||
|
label: "Experience – Cluster"
|
||||||
|
note_type: experience
|
||||||
|
defaults:
|
||||||
|
status: active
|
||||||
|
steps:
|
||||||
|
- id: title
|
||||||
|
kind: capture_frontmatter
|
||||||
|
field: title
|
||||||
|
label: "Hub Titel"
|
||||||
|
required: true
|
||||||
|
# Einzeiliges Feld
|
||||||
|
- id: subtitle
|
||||||
|
kind: capture_text_line
|
||||||
|
label: "Untertitel"
|
||||||
|
prompt: "Kurzer Untertitel"
|
||||||
|
- id: intro
|
||||||
|
kind: capture_text
|
||||||
|
section: "## Einleitung"
|
||||||
|
label: "Einleitung"
|
||||||
|
prompt: "Beschreibe die Einleitung"
|
||||||
|
- id: items
|
||||||
|
kind: loop
|
||||||
|
label: "Erlebnisse"
|
||||||
|
item_label: "Erlebnis"
|
||||||
|
min_items: 1
|
||||||
|
steps:
|
||||||
|
- id: item_Headline
|
||||||
|
kind: capture_text_line
|
||||||
|
label: "Überschrift"
|
||||||
|
required: false
|
||||||
|
prompt: "Nennen kurz die Gruppenüberschrift"
|
||||||
|
- id: item_list
|
||||||
|
kind: loop
|
||||||
|
label: "Listeneinträge"
|
||||||
|
steps:
|
||||||
|
- id: list_item
|
||||||
|
kind: capture_text_line
|
||||||
|
label: "Eintrag"
|
||||||
|
required: true
|
||||||
|
prompt: "Einzelner Listeneintrag"
|
||||||
|
- id: review
|
||||||
|
kind: review
|
||||||
|
label: "Review & Apply"
|
||||||
|
checks: [lint_current_note]
|
||||||
|
|
@ -7,6 +7,7 @@ export interface LoopRuntimeState {
|
||||||
items: unknown[]; // Committed items
|
items: unknown[]; // Committed items
|
||||||
draft: Record<string, unknown>; // Current draft being edited
|
draft: Record<string, unknown>; // Current draft being edited
|
||||||
editIndex: number | null; // Index of item being edited, or null for new item
|
editIndex: number | null; // Index of item being edited, or null for new item
|
||||||
|
activeItemStepIndex: number; // Current step index in subwizard (0-based)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,6 +18,7 @@ export function createLoopState(): LoopRuntimeState {
|
||||||
items: [],
|
items: [],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ export function commitDraft(state: LoopRuntimeState): LoopRuntimeState {
|
||||||
items: newItems,
|
items: newItems,
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,6 +120,7 @@ export function startEdit(
|
||||||
...state,
|
...state,
|
||||||
draft: { ...(item as Record<string, unknown>) },
|
draft: { ...(item as Record<string, unknown>) },
|
||||||
editIndex: index,
|
editIndex: index,
|
||||||
|
activeItemStepIndex: 0, // Reset to first step when starting edit
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +156,7 @@ export function deleteItem(
|
||||||
editIndex: newEditIndex,
|
editIndex: newEditIndex,
|
||||||
// Clear draft if we were editing the deleted item
|
// Clear draft if we were editing the deleted item
|
||||||
draft: newEditIndex === null ? {} : state.draft,
|
draft: newEditIndex === null ? {} : state.draft,
|
||||||
|
activeItemStepIndex: newEditIndex === null ? 0 : state.activeItemStepIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,5 +234,64 @@ export function clearDraft(state: LoopRuntimeState): LoopRuntimeState {
|
||||||
...state,
|
...state,
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set active item step index.
|
||||||
|
*/
|
||||||
|
export function setActiveItemStepIndex(
|
||||||
|
state: LoopRuntimeState,
|
||||||
|
index: number
|
||||||
|
): LoopRuntimeState {
|
||||||
|
if (index < 0) {
|
||||||
|
return state; // Invalid index
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeItemStepIndex: index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to next step in item subwizard.
|
||||||
|
*/
|
||||||
|
export function itemNextStep(
|
||||||
|
state: LoopRuntimeState,
|
||||||
|
childStepCount: number
|
||||||
|
): LoopRuntimeState {
|
||||||
|
if (state.activeItemStepIndex >= childStepCount - 1) {
|
||||||
|
return state; // Already at last step
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeItemStepIndex: state.activeItemStepIndex + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to previous step in item subwizard.
|
||||||
|
*/
|
||||||
|
export function itemPrevStep(state: LoopRuntimeState): LoopRuntimeState {
|
||||||
|
if (state.activeItemStepIndex <= 0) {
|
||||||
|
return state; // Already at first step
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeItemStepIndex: state.activeItemStepIndex - 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset item wizard to first step.
|
||||||
|
*/
|
||||||
|
export function resetItemWizard(state: LoopRuntimeState): LoopRuntimeState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,23 @@ function renderCaptureFrontmatter(step: CaptureFrontmatterStep, answers: RenderA
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render loop step.
|
* Render loop step (recursive, supports arbitrary nesting depth).
|
||||||
*/
|
*/
|
||||||
function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
||||||
|
return renderLoopRecursive(step, answers, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive helper to render loop items with nested steps.
|
||||||
|
* Supports arbitrary nesting depth (no hard limit, but practical limits apply due to stack size).
|
||||||
|
*/
|
||||||
|
function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: number): string | null {
|
||||||
|
// Safety check: prevent infinite recursion (practical limit: 100 levels)
|
||||||
|
if (depth > 100) {
|
||||||
|
console.warn(`Loop nesting depth ${depth} exceeds practical limit, stopping recursion`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const items = answers.loopContexts.get(step.key);
|
const items = answers.loopContexts.get(step.key);
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -147,7 +161,7 @@ function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
||||||
|
|
||||||
const itemParts: string[] = [];
|
const itemParts: string[] = [];
|
||||||
|
|
||||||
// Render each nested step for this item
|
// Render each nested step for this item (recursively)
|
||||||
for (const nestedStep of step.items) {
|
for (const nestedStep of step.items) {
|
||||||
const fieldValue = (item as Record<string, unknown>)[nestedStep.key];
|
const fieldValue = (item as Record<string, unknown>)[nestedStep.key];
|
||||||
|
|
||||||
|
|
@ -198,6 +212,26 @@ function renderLoop(step: LoopStep, answers: RenderAnswers): string | null {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (nestedStep.type === "loop") {
|
||||||
|
// Recursively handle nested loop: fieldValue should be an array of items
|
||||||
|
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
|
||||||
|
const nestedLoopStep = nestedStep as LoopStep;
|
||||||
|
|
||||||
|
// Create a temporary answers object for the nested loop
|
||||||
|
// The nested loop items are stored in fieldValue, so we need to create
|
||||||
|
// a temporary loopContexts map for the nested loop
|
||||||
|
const nestedAnswers: RenderAnswers = {
|
||||||
|
collectedData: answers.collectedData,
|
||||||
|
loopContexts: new Map(answers.loopContexts),
|
||||||
|
};
|
||||||
|
nestedAnswers.loopContexts.set(nestedLoopStep.key, fieldValue);
|
||||||
|
|
||||||
|
// Recursively render the nested loop
|
||||||
|
const nestedOutput = renderLoopRecursive(nestedLoopStep, nestedAnswers, depth + 1);
|
||||||
|
if (nestedOutput) {
|
||||||
|
itemParts.push(nestedOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ import {
|
||||||
moveItemUp,
|
moveItemUp,
|
||||||
moveItemDown,
|
moveItemDown,
|
||||||
clearDraft,
|
clearDraft,
|
||||||
|
setActiveItemStepIndex,
|
||||||
|
itemNextStep,
|
||||||
|
itemPrevStep,
|
||||||
|
resetItemWizard,
|
||||||
type LoopRuntimeState,
|
type LoopRuntimeState,
|
||||||
} from "../../interview/loopState";
|
} from "../../interview/loopState";
|
||||||
|
|
||||||
|
|
@ -114,6 +118,7 @@ describe("loopState", () => {
|
||||||
items: [{ field1: "old" }],
|
items: [{ field1: "old" }],
|
||||||
draft: { field1: "new" },
|
draft: { field1: "new" },
|
||||||
editIndex: 0,
|
editIndex: 0,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = commitDraft(state);
|
const newState = commitDraft(state);
|
||||||
|
|
@ -146,6 +151,7 @@ describe("loopState", () => {
|
||||||
items: [{ field1: "value1", field2: "value2" }],
|
items: [{ field1: "value1", field2: "value2" }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = startEdit(state, 0);
|
const newState = startEdit(state, 0);
|
||||||
|
|
@ -160,6 +166,7 @@ describe("loopState", () => {
|
||||||
items: [{ field1: "value1" }],
|
items: [{ field1: "value1" }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = startEdit(state, 5);
|
const newState = startEdit(state, 5);
|
||||||
|
|
@ -172,6 +179,7 @@ describe("loopState", () => {
|
||||||
items: [{ field1: "value1" }],
|
items: [{ field1: "value1" }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
startEdit(state, 0);
|
startEdit(state, 0);
|
||||||
|
|
@ -186,6 +194,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = deleteItem(state, 1);
|
const newState = deleteItem(state, 1);
|
||||||
|
|
@ -200,6 +209,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }],
|
items: [{ id: 1 }, { id: 2 }],
|
||||||
draft: { id: 2 },
|
draft: { id: 2 },
|
||||||
editIndex: 1,
|
editIndex: 1,
|
||||||
|
activeItemStepIndex: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = deleteItem(state, 1);
|
const newState = deleteItem(state, 1);
|
||||||
|
|
@ -214,6 +224,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
draft: { id: 3 },
|
draft: { id: 3 },
|
||||||
editIndex: 2,
|
editIndex: 2,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = deleteItem(state, 1);
|
const newState = deleteItem(state, 1);
|
||||||
|
|
@ -227,6 +238,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }],
|
items: [{ id: 1 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = deleteItem(state, 5);
|
const newState = deleteItem(state, 5);
|
||||||
|
|
@ -240,6 +252,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemUp(state, 1);
|
const newState = moveItemUp(state, 1);
|
||||||
|
|
@ -254,6 +267,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }],
|
items: [{ id: 1 }, { id: 2 }],
|
||||||
draft: { id: 2 },
|
draft: { id: 2 },
|
||||||
editIndex: 1,
|
editIndex: 1,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemUp(state, 1);
|
const newState = moveItemUp(state, 1);
|
||||||
|
|
@ -267,6 +281,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
draft: { id: 3 },
|
draft: { id: 3 },
|
||||||
editIndex: 2,
|
editIndex: 2,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemUp(state, 1);
|
const newState = moveItemUp(state, 1);
|
||||||
|
|
@ -283,6 +298,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }],
|
items: [{ id: 1 }, { id: 2 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemUp(state, 0);
|
const newState = moveItemUp(state, 0);
|
||||||
|
|
@ -294,6 +310,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }],
|
items: [{ id: 1 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemUp(state, 5);
|
const newState = moveItemUp(state, 5);
|
||||||
|
|
@ -307,6 +324,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemDown(state, 1);
|
const newState = moveItemDown(state, 1);
|
||||||
|
|
@ -321,6 +339,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }],
|
items: [{ id: 1 }, { id: 2 }],
|
||||||
draft: { id: 1 },
|
draft: { id: 1 },
|
||||||
editIndex: 0,
|
editIndex: 0,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemDown(state, 0);
|
const newState = moveItemDown(state, 0);
|
||||||
|
|
@ -335,6 +354,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }, { id: 2 }],
|
items: [{ id: 1 }, { id: 2 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemDown(state, 1);
|
const newState = moveItemDown(state, 1);
|
||||||
|
|
@ -346,6 +366,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }],
|
items: [{ id: 1 }],
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = moveItemDown(state, 5);
|
const newState = moveItemDown(state, 5);
|
||||||
|
|
@ -359,6 +380,7 @@ describe("loopState", () => {
|
||||||
items: [{ id: 1 }],
|
items: [{ id: 1 }],
|
||||||
draft: { field1: "value1" },
|
draft: { field1: "value1" },
|
||||||
editIndex: 0,
|
editIndex: 0,
|
||||||
|
activeItemStepIndex: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = clearDraft(state);
|
const newState = clearDraft(state);
|
||||||
|
|
@ -373,11 +395,126 @@ describe("loopState", () => {
|
||||||
items: [],
|
items: [],
|
||||||
draft: { field1: "value1" },
|
draft: { field1: "value1" },
|
||||||
editIndex: 0,
|
editIndex: 0,
|
||||||
|
activeItemStepIndex: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
clearDraft(state);
|
clearDraft(state);
|
||||||
expect(state.draft).toEqual({ field1: "value1" });
|
expect(state.draft).toEqual({ field1: "value1" });
|
||||||
expect(state.editIndex).toBe(0);
|
expect(state.editIndex).toBe(0);
|
||||||
|
expect(state.activeItemStepIndex).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("subwizard navigation", () => {
|
||||||
|
it("should set active item step index", () => {
|
||||||
|
const state = createLoopState();
|
||||||
|
const newState = setActiveItemStepIndex(state, 2);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(2);
|
||||||
|
expect(newState.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set negative index", () => {
|
||||||
|
const state = createLoopState();
|
||||||
|
const newState = setActiveItemStepIndex(state, -1);
|
||||||
|
expect(newState).toBe(state); // Should return unchanged
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move to next step", () => {
|
||||||
|
const state = createLoopState();
|
||||||
|
let newState = itemNextStep(state, 3);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(1);
|
||||||
|
|
||||||
|
newState = itemNextStep(newState, 3);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not move beyond last step", () => {
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [],
|
||||||
|
draft: {},
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = itemNextStep(state, 3);
|
||||||
|
expect(newState).toBe(state); // Should return unchanged
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move to previous step", () => {
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [],
|
||||||
|
draft: {},
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let newState = itemPrevStep(state);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(1);
|
||||||
|
|
||||||
|
newState = itemPrevStep(newState);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not move before first step", () => {
|
||||||
|
const state = createLoopState();
|
||||||
|
const newState = itemPrevStep(state);
|
||||||
|
expect(newState).toBe(state); // Should return unchanged
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset item wizard to first step", () => {
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [],
|
||||||
|
draft: {},
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = resetItemWizard(state);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset to first step when starting edit", () => {
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [{ field1: "value1" }],
|
||||||
|
draft: {},
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = startEdit(state, 0);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(0);
|
||||||
|
expect(newState.editIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset to first step when committing draft", () => {
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [],
|
||||||
|
draft: { field1: "value1" },
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = commitDraft(state);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(0);
|
||||||
|
expect(newState.items.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow commit:on_next even when not on last step", () => {
|
||||||
|
// Test that commit logic works regardless of activeItemStepIndex
|
||||||
|
const state: LoopRuntimeState = {
|
||||||
|
items: [],
|
||||||
|
draft: { field1: "value1", field2: "value2" },
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 1, // Not on last step
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draft is dirty, should commit
|
||||||
|
expect(isDraftDirty(state.draft)).toBe(true);
|
||||||
|
|
||||||
|
const newState = commitDraft(state);
|
||||||
|
expect(newState.items.length).toBe(1);
|
||||||
|
expect(newState.activeItemStepIndex).toBe(0);
|
||||||
|
expect(newState.draft).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ import {
|
||||||
moveItemUp,
|
moveItemUp,
|
||||||
moveItemDown,
|
moveItemDown,
|
||||||
clearDraft,
|
clearDraft,
|
||||||
|
setActiveItemStepIndex,
|
||||||
|
itemNextStep,
|
||||||
|
itemPrevStep,
|
||||||
|
resetItemWizard,
|
||||||
} from "../interview/loopState";
|
} from "../interview/loopState";
|
||||||
|
|
||||||
export interface WizardResult {
|
export interface WizardResult {
|
||||||
|
|
@ -601,6 +605,7 @@ export class InterviewWizardModal extends Modal {
|
||||||
items: legacyItems,
|
items: legacyItems,
|
||||||
draft: {},
|
draft: {},
|
||||||
editIndex: null,
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
};
|
};
|
||||||
this.state.loopRuntimeStates.set(step.key, loopState);
|
this.state.loopRuntimeStates.set(step.key, loopState);
|
||||||
}
|
}
|
||||||
|
|
@ -722,9 +727,13 @@ export class InterviewWizardModal extends Modal {
|
||||||
editBtn.style.fontSize = "0.85em";
|
editBtn.style.fontSize = "0.85em";
|
||||||
editBtn.style.padding = "0.25em 0.5em";
|
editBtn.style.padding = "0.25em 0.5em";
|
||||||
editBtn.onclick = () => {
|
editBtn.onclick = () => {
|
||||||
const newState = startEdit(loopState!, i);
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||||
this.state.loopRuntimeStates.set(step.key, newState);
|
if (currentState) {
|
||||||
this.renderStep();
|
let newState = startEdit(currentState, i);
|
||||||
|
newState = resetItemWizard(newState); // Reset to first step
|
||||||
|
this.state.loopRuntimeStates.set(step.key, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete button
|
// Delete button
|
||||||
|
|
@ -780,68 +789,156 @@ export class InterviewWizardModal extends Modal {
|
||||||
rightPane.style.width = "60%";
|
rightPane.style.width = "60%";
|
||||||
rightPane.style.flex = "1";
|
rightPane.style.flex = "1";
|
||||||
|
|
||||||
const editorTitle = rightPane.createEl("h3", {
|
// Subwizard header
|
||||||
text: loopState.editIndex !== null
|
const itemTitle = rightPane.createEl("h3");
|
||||||
? `Edit Item ${loopState.editIndex + 1}`
|
const itemTitleText = loopState.editIndex !== null
|
||||||
: "New Item",
|
? `Item: ${loopState.editIndex + 1} (editing)`
|
||||||
});
|
: "Item: New";
|
||||||
|
const stepCounter = step.items.length > 0
|
||||||
|
? ` - Step ${loopState.activeItemStepIndex + 1}/${step.items.length}`
|
||||||
|
: "";
|
||||||
|
itemTitle.textContent = itemTitleText + stepCounter;
|
||||||
|
|
||||||
// Render nested steps bound to draft
|
// Render only the active nested step (subwizard)
|
||||||
if (step.items.length > 0) {
|
if (step.items.length > 0) {
|
||||||
const editorContainer = rightPane.createEl("div", {
|
const activeStepIndex = Math.min(loopState.activeItemStepIndex, step.items.length - 1);
|
||||||
cls: "loop-item-editor",
|
const activeNestedStep = step.items[activeStepIndex];
|
||||||
});
|
|
||||||
|
|
||||||
for (const nestedStep of step.items) {
|
if (activeNestedStep) {
|
||||||
const draftValue = loopState.draft[nestedStep.key];
|
const editorContainer = rightPane.createEl("div", {
|
||||||
this.renderLoopNestedStep(nestedStep, editorContainer, step.key, draftValue, (fieldId, value) => {
|
cls: "loop-item-editor",
|
||||||
|
});
|
||||||
|
|
||||||
|
const draftValue = loopState.draft[activeNestedStep.key];
|
||||||
|
this.renderLoopNestedStep(activeNestedStep, editorContainer, step.key, draftValue, (fieldId, value) => {
|
||||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||||
if (currentState) {
|
if (currentState) {
|
||||||
const newState = setDraftField(currentState, fieldId, value);
|
const newState = setDraftField(currentState, fieldId, value);
|
||||||
this.state.loopRuntimeStates.set(step.key, newState);
|
this.state.loopRuntimeStates.set(step.key, newState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Action buttons
|
// Subwizard navigation buttons
|
||||||
const buttonContainer = rightPane.createEl("div", {
|
const subwizardNav = rightPane.createEl("div", {
|
||||||
cls: "loop-editor-actions",
|
cls: "loop-subwizard-navigation",
|
||||||
});
|
|
||||||
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 = () => {
|
subwizardNav.style.display = "flex";
|
||||||
|
subwizardNav.style.gap = "0.5em";
|
||||||
|
subwizardNav.style.marginTop = "1em";
|
||||||
|
subwizardNav.style.justifyContent = "space-between";
|
||||||
|
|
||||||
|
// Left side: Item Back/Next
|
||||||
|
const itemNavLeft = subwizardNav.createEl("div", {
|
||||||
|
cls: "loop-item-nav-left",
|
||||||
|
});
|
||||||
|
itemNavLeft.style.display = "flex";
|
||||||
|
itemNavLeft.style.gap = "0.5em";
|
||||||
|
|
||||||
|
// Item Back button
|
||||||
|
const itemBackBtn = itemNavLeft.createEl("button", {
|
||||||
|
text: "← Item Back",
|
||||||
|
});
|
||||||
|
itemBackBtn.disabled = activeStepIndex === 0;
|
||||||
|
itemBackBtn.onclick = () => {
|
||||||
const currentState = this.state.loopRuntimeStates.get(step.key);
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||||
if (currentState) {
|
if (currentState) {
|
||||||
const newState = clearDraft(currentState);
|
const newState = itemPrevStep(currentState);
|
||||||
this.state.loopRuntimeStates.set(step.key, newState);
|
this.state.loopRuntimeStates.set(step.key, newState);
|
||||||
this.renderStep();
|
this.renderStep();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Item Next button
|
||||||
|
const itemNextBtn = itemNavLeft.createEl("button", {
|
||||||
|
text: "Item Next →",
|
||||||
|
});
|
||||||
|
itemNextBtn.disabled = activeStepIndex >= step.items.length - 1;
|
||||||
|
itemNextBtn.onclick = () => {
|
||||||
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||||
|
if (currentState) {
|
||||||
|
const newState = itemNextStep(currentState, step.items.length);
|
||||||
|
this.state.loopRuntimeStates.set(step.key, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right side: Done/Save Item
|
||||||
|
const itemNavRight = subwizardNav.createEl("div", {
|
||||||
|
cls: "loop-item-nav-right",
|
||||||
|
});
|
||||||
|
itemNavRight.style.display = "flex";
|
||||||
|
itemNavRight.style.gap = "0.5em";
|
||||||
|
|
||||||
|
// Check for required fields
|
||||||
|
const missingRequired: string[] = [];
|
||||||
|
for (const nestedStep of step.items) {
|
||||||
|
if (
|
||||||
|
(nestedStep.type === "capture_text" || nestedStep.type === "capture_text_line" || nestedStep.type === "capture_frontmatter") &&
|
||||||
|
nestedStep.required
|
||||||
|
) {
|
||||||
|
const value = loopState.draft[nestedStep.key];
|
||||||
|
if (!value || (typeof value === "string" && value.trim() === "")) {
|
||||||
|
missingRequired.push(nestedStep.label || nestedStep.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done/Save Item button
|
||||||
|
const doneBtn = itemNavRight.createEl("button", {
|
||||||
|
text: loopState.editIndex !== null ? "Save Item" : "Done",
|
||||||
|
cls: "mod-cta",
|
||||||
|
});
|
||||||
|
doneBtn.onclick = () => {
|
||||||
|
const currentState = this.state.loopRuntimeStates.get(step.key);
|
||||||
|
if (!currentState) return;
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if (missingRequired.length > 0) {
|
||||||
|
const msg = `Required fields missing: ${missingRequired.join(", ")}. Save anyway?`;
|
||||||
|
if (!confirm(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDraftDirty(currentState.draft)) {
|
||||||
|
const wasNewItem = currentState.editIndex === null;
|
||||||
|
const newState = commitDraft(currentState);
|
||||||
|
this.state.loopRuntimeStates.set(step.key, newState);
|
||||||
|
// Update answers
|
||||||
|
this.state.loopContexts.set(step.key, newState.items);
|
||||||
|
|
||||||
|
// If we just created a new item, reset all nested loop states
|
||||||
|
if (wasNewItem) {
|
||||||
|
for (const nestedStep of step.items) {
|
||||||
|
if (nestedStep.type === "loop") {
|
||||||
|
const nestedLoopKey = `${step.key}.${nestedStep.key}`;
|
||||||
|
const nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (nestedLoopState) {
|
||||||
|
const resetState = clearDraft(nestedLoopState);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, resetState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderStep();
|
||||||
|
} else {
|
||||||
|
new Notice("Please enter at least one field");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show warning if required fields missing
|
||||||
|
if (missingRequired.length > 0) {
|
||||||
|
const warning = rightPane.createEl("div", {
|
||||||
|
cls: "loop-required-warning",
|
||||||
|
text: `⚠️ Required fields missing: ${missingRequired.join(", ")}`,
|
||||||
|
});
|
||||||
|
warning.style.padding = "0.5em";
|
||||||
|
warning.style.background = "var(--background-modifier-error)";
|
||||||
|
warning.style.borderRadius = "4px";
|
||||||
|
warning.style.marginTop = "0.5em";
|
||||||
|
warning.style.color = "var(--text-error)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1014,6 +1111,300 @@ export class InterviewWizardModal extends Modal {
|
||||||
text.inputEl.style.width = "100%";
|
text.inputEl.style.width = "100%";
|
||||||
text.inputEl.style.boxSizing = "border-box";
|
text.inputEl.style.boxSizing = "border-box";
|
||||||
});
|
});
|
||||||
|
} else if (nestedStep.type === "loop") {
|
||||||
|
// Nested loop: render as a nested loop editor
|
||||||
|
// The draft value should be an array of items
|
||||||
|
const nestedLoopItems = Array.isArray(draftValue) ? draftValue : [];
|
||||||
|
|
||||||
|
// Get or create nested loop state
|
||||||
|
const nestedLoopKey = `${loopKey}.${nestedStep.key}`;
|
||||||
|
let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (!nestedLoopState) {
|
||||||
|
nestedLoopState = {
|
||||||
|
items: nestedLoopItems,
|
||||||
|
draft: {},
|
||||||
|
editIndex: null,
|
||||||
|
activeItemStepIndex: 0,
|
||||||
|
};
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render nested loop UI - use a simplified 2-pane layout
|
||||||
|
const nestedLoopContainer = containerEl.createEl("div", {
|
||||||
|
cls: "nested-loop-container",
|
||||||
|
});
|
||||||
|
nestedLoopContainer.style.border = "1px solid var(--background-modifier-border)";
|
||||||
|
nestedLoopContainer.style.borderRadius = "4px";
|
||||||
|
nestedLoopContainer.style.padding = "1em";
|
||||||
|
nestedLoopContainer.style.marginTop = "0.5em";
|
||||||
|
|
||||||
|
if (nestedStep.label) {
|
||||||
|
const labelEl = nestedLoopContainer.createEl("div", {
|
||||||
|
cls: "mindnet-field__label",
|
||||||
|
text: nestedStep.label,
|
||||||
|
});
|
||||||
|
labelEl.style.marginBottom = "0.5em";
|
||||||
|
labelEl.style.fontWeight = "bold";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 2-pane layout for nested loop
|
||||||
|
const nestedPaneContainer = nestedLoopContainer.createEl("div", {
|
||||||
|
cls: "nested-loop-panes",
|
||||||
|
});
|
||||||
|
nestedPaneContainer.style.display = "flex";
|
||||||
|
nestedPaneContainer.style.gap = "1em";
|
||||||
|
nestedPaneContainer.style.minHeight = "200px";
|
||||||
|
|
||||||
|
// Left pane: Items list (narrower for nested loops)
|
||||||
|
const nestedLeftPane = nestedPaneContainer.createEl("div", {
|
||||||
|
cls: "nested-loop-items-pane",
|
||||||
|
});
|
||||||
|
nestedLeftPane.style.width = "25%";
|
||||||
|
nestedLeftPane.style.borderRight = "1px solid var(--background-modifier-border)";
|
||||||
|
nestedLeftPane.style.paddingRight = "1em";
|
||||||
|
nestedLeftPane.style.maxHeight = "300px";
|
||||||
|
nestedLeftPane.style.overflowY = "auto";
|
||||||
|
|
||||||
|
const nestedItemsTitle = nestedLeftPane.createEl("div", {
|
||||||
|
text: "Einträge",
|
||||||
|
cls: "mindnet-field__label",
|
||||||
|
});
|
||||||
|
nestedItemsTitle.style.marginBottom = "0.5em";
|
||||||
|
|
||||||
|
// Render items list
|
||||||
|
nestedLoopState.items.forEach((item, i) => {
|
||||||
|
const itemEl = nestedLeftPane.createEl("div", {
|
||||||
|
cls: "nested-loop-item",
|
||||||
|
});
|
||||||
|
itemEl.style.padding = "0.5em";
|
||||||
|
itemEl.style.marginBottom = "0.25em";
|
||||||
|
itemEl.style.background = "var(--background-secondary)";
|
||||||
|
itemEl.style.borderRadius = "4px";
|
||||||
|
itemEl.style.cursor = "pointer";
|
||||||
|
|
||||||
|
// Extract first non-empty field value for display
|
||||||
|
let itemText = `Item ${i + 1}`;
|
||||||
|
if (typeof item === "object" && item !== null) {
|
||||||
|
const itemObj = item as Record<string, unknown>;
|
||||||
|
// Try to find first non-empty string value
|
||||||
|
for (const [key, value] of Object.entries(itemObj)) {
|
||||||
|
if (value && typeof value === "string" && value.trim() !== "") {
|
||||||
|
itemText = value.trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (item) {
|
||||||
|
itemText = String(item);
|
||||||
|
}
|
||||||
|
itemEl.textContent = itemText.length > 40 ? itemText.substring(0, 40) + "..." : itemText;
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
const editBtn = itemEl.createEl("button", {
|
||||||
|
text: "✏️",
|
||||||
|
cls: "nested-edit-btn",
|
||||||
|
});
|
||||||
|
editBtn.style.float = "right";
|
||||||
|
editBtn.style.marginLeft = "0.5em";
|
||||||
|
editBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
let newState = startEdit(currentNestedState, i);
|
||||||
|
newState = resetItemWizard(newState);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move Up button
|
||||||
|
const moveUpBtn = itemEl.createEl("button", {
|
||||||
|
text: "↑",
|
||||||
|
cls: "nested-move-up-btn",
|
||||||
|
});
|
||||||
|
moveUpBtn.style.float = "right";
|
||||||
|
moveUpBtn.style.marginLeft = "0.25em";
|
||||||
|
moveUpBtn.disabled = i === 0;
|
||||||
|
moveUpBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = moveItemUp(currentNestedState, i);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
// Update parent draft
|
||||||
|
onFieldChange(nestedStep.key, newState.items);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move Down button
|
||||||
|
const moveDownBtn = itemEl.createEl("button", {
|
||||||
|
text: "↓",
|
||||||
|
cls: "nested-move-down-btn",
|
||||||
|
});
|
||||||
|
moveDownBtn.style.float = "right";
|
||||||
|
moveDownBtn.style.marginLeft = "0.25em";
|
||||||
|
moveDownBtn.disabled = i >= nestedLoopState.items.length - 1;
|
||||||
|
moveDownBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = moveItemDown(currentNestedState, i);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
// Update parent draft
|
||||||
|
onFieldChange(nestedStep.key, newState.items);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
const deleteBtn = itemEl.createEl("button", {
|
||||||
|
text: "🗑️",
|
||||||
|
cls: "nested-delete-btn",
|
||||||
|
});
|
||||||
|
deleteBtn.style.float = "right";
|
||||||
|
deleteBtn.style.marginLeft = "0.25em";
|
||||||
|
deleteBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = deleteItem(currentNestedState, i);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
// Update parent draft
|
||||||
|
onFieldChange(nestedStep.key, newState.items);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Right pane: Editor for nested loop (wider for nested loops)
|
||||||
|
const nestedRightPane = nestedPaneContainer.createEl("div", {
|
||||||
|
cls: "nested-loop-editor-pane",
|
||||||
|
});
|
||||||
|
nestedRightPane.style.width = "75%";
|
||||||
|
nestedRightPane.style.flex = "1";
|
||||||
|
|
||||||
|
const nestedEditorTitle = nestedRightPane.createEl("div", {
|
||||||
|
cls: "mindnet-field__label",
|
||||||
|
});
|
||||||
|
const nestedTitleText = nestedLoopState.editIndex !== null
|
||||||
|
? `Eintrag ${nestedLoopState.editIndex + 1} bearbeiten`
|
||||||
|
: "Neuer Eintrag";
|
||||||
|
const nestedStepCounter = nestedStep.items.length > 0
|
||||||
|
? ` - Schritt ${nestedLoopState.activeItemStepIndex + 1}/${nestedStep.items.length}`
|
||||||
|
: "";
|
||||||
|
nestedEditorTitle.textContent = nestedTitleText + nestedStepCounter;
|
||||||
|
nestedEditorTitle.style.marginBottom = "0.5em";
|
||||||
|
|
||||||
|
// Render active nested step
|
||||||
|
if (nestedStep.items.length > 0) {
|
||||||
|
const activeNestedStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1);
|
||||||
|
const activeNestedNestedStep = nestedStep.items[activeNestedStepIndex];
|
||||||
|
|
||||||
|
// Recursively render nested step (supports arbitrary nesting depth)
|
||||||
|
if (activeNestedNestedStep) {
|
||||||
|
const nestedDraftValue = nestedLoopState.draft[activeNestedNestedStep.key];
|
||||||
|
this.renderLoopNestedStep(
|
||||||
|
activeNestedNestedStep,
|
||||||
|
nestedRightPane,
|
||||||
|
nestedLoopKey,
|
||||||
|
nestedDraftValue,
|
||||||
|
(fieldId, value) => {
|
||||||
|
// Update state without re-rendering to preserve focus
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = setDraftField(currentNestedState, fieldId, value);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
// Update parent draft without re-rendering
|
||||||
|
const parentLoopState = this.state.loopRuntimeStates.get(loopKey);
|
||||||
|
if (parentLoopState) {
|
||||||
|
const updatedParentDraft = {
|
||||||
|
...parentLoopState.draft,
|
||||||
|
[nestedStep.key]: newState.items,
|
||||||
|
};
|
||||||
|
const updatedParentState = setDraftField(parentLoopState, nestedStep.key, newState.items);
|
||||||
|
this.state.loopRuntimeStates.set(loopKey, updatedParentState);
|
||||||
|
}
|
||||||
|
// Do NOT call renderStep() here - it causes focus loss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation buttons for nested loop
|
||||||
|
const nestedNav = nestedRightPane.createEl("div", {
|
||||||
|
cls: "nested-loop-navigation",
|
||||||
|
});
|
||||||
|
nestedNav.style.display = "flex";
|
||||||
|
nestedNav.style.gap = "0.5em";
|
||||||
|
nestedNav.style.marginTop = "1em";
|
||||||
|
nestedNav.style.justifyContent = "space-between";
|
||||||
|
|
||||||
|
// Left: Back/Next
|
||||||
|
const nestedNavLeft = nestedNav.createEl("div");
|
||||||
|
nestedNavLeft.style.display = "flex";
|
||||||
|
nestedNavLeft.style.gap = "0.5em";
|
||||||
|
|
||||||
|
const nestedBackBtn = nestedNavLeft.createEl("button", {
|
||||||
|
text: "← Zurück",
|
||||||
|
});
|
||||||
|
nestedBackBtn.disabled = activeNestedStepIndex === 0;
|
||||||
|
nestedBackBtn.onclick = () => {
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = itemPrevStep(currentNestedState);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nestedNextBtn = nestedNavLeft.createEl("button", {
|
||||||
|
text: "Weiter →",
|
||||||
|
});
|
||||||
|
nestedNextBtn.disabled = activeNestedStepIndex >= nestedStep.items.length - 1;
|
||||||
|
nestedNextBtn.onclick = () => {
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = itemNextStep(currentNestedState, nestedStep.items.length);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right: Save/Clear
|
||||||
|
const nestedNavRight = nestedNav.createEl("div");
|
||||||
|
nestedNavRight.style.display = "flex";
|
||||||
|
nestedNavRight.style.gap = "0.5em";
|
||||||
|
|
||||||
|
const nestedSaveBtn = nestedNavRight.createEl("button", {
|
||||||
|
text: nestedLoopState.editIndex !== null ? "Speichern" : "Hinzufügen",
|
||||||
|
cls: "mod-cta",
|
||||||
|
});
|
||||||
|
nestedSaveBtn.onclick = () => {
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState && isDraftDirty(currentNestedState.draft)) {
|
||||||
|
const newState = commitDraft(currentNestedState);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
// Update parent draft
|
||||||
|
onFieldChange(nestedStep.key, newState.items);
|
||||||
|
// Re-render to show updated item list
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nestedLoopState.editIndex !== null || isDraftDirty(nestedLoopState.draft)) {
|
||||||
|
const nestedClearBtn = nestedNavRight.createEl("button", {
|
||||||
|
text: "Löschen",
|
||||||
|
});
|
||||||
|
nestedClearBtn.onclick = () => {
|
||||||
|
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||||
|
if (currentNestedState) {
|
||||||
|
const newState = clearDraft(currentNestedState);
|
||||||
|
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||||
|
this.renderStep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user