Enhance nested loop functionality in Interview Wizard Modal
- Added support for tracking active loop paths, enabling nested loop navigation. - Implemented breadcrumb navigation to display loop hierarchy and allow users to navigate back to parent loops. - Updated rendering logic for nested loops to support fullscreen mode and improved item management with edit, delete, and navigation options. - Enhanced user experience with context headers showing parent loop item details and improved item editing capabilities.
This commit is contained in:
parent
5a171987b2
commit
c3357a784b
|
|
@ -9,6 +9,7 @@ export interface WizardState {
|
|||
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
|
||||
activeLoopPath: string[]; // Stack of loop keys representing current nesting level (e.g. ["items", "item_list"])
|
||||
}
|
||||
|
||||
export interface Patch {
|
||||
|
|
@ -36,6 +37,7 @@ export function createWizardState(profile: InterviewProfile): WizardState {
|
|||
loopContexts: new Map(), // Keep for backwards compatibility
|
||||
loopRuntimeStates: new Map(),
|
||||
patches: [],
|
||||
activeLoopPath: [], // Start at top level
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
TextAreaComponent,
|
||||
TextComponent,
|
||||
} from "obsidian";
|
||||
import type { InterviewProfile, InterviewStep } from "../interview/types";
|
||||
import type { InterviewProfile, InterviewStep, LoopStep } from "../interview/types";
|
||||
import {
|
||||
type WizardState,
|
||||
createWizardState,
|
||||
|
|
@ -596,6 +596,24 @@ export class InterviewWizardModal extends Modal {
|
|||
renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void {
|
||||
if (step.type !== "loop") return;
|
||||
|
||||
// Check if we're in a nested loop (fullscreen mode)
|
||||
const currentLoopKey = this.state.activeLoopPath.length > 0
|
||||
? this.state.activeLoopPath[this.state.activeLoopPath.length - 1]
|
||||
: null;
|
||||
|
||||
// If we're in a nested loop, find which nested step it is
|
||||
if (currentLoopKey && currentLoopKey.startsWith(step.key + ".")) {
|
||||
// We're in a nested loop of this step - render it in fullscreen mode
|
||||
this.renderNestedLoopFullscreen(step, containerEl, currentLoopKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, render normal loop (or top-level loop)
|
||||
if (this.state.activeLoopPath.length > 0 && this.state.activeLoopPath[0] !== step.key) {
|
||||
// We're in a different loop's nested loop, don't render this one
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize or get loop runtime state
|
||||
let loopState = this.state.loopRuntimeStates.get(step.key);
|
||||
if (!loopState) {
|
||||
|
|
@ -619,8 +637,14 @@ export class InterviewWizardModal extends Modal {
|
|||
nestedStepsCount: step.items.length,
|
||||
editIndex: loopState.editIndex,
|
||||
commitMode: commitMode,
|
||||
activeLoopPath: this.state.activeLoopPath,
|
||||
});
|
||||
|
||||
// Breadcrumb navigation if in nested context
|
||||
if (this.state.activeLoopPath.length > 0) {
|
||||
this.renderBreadcrumb(containerEl, step);
|
||||
}
|
||||
|
||||
// Title
|
||||
containerEl.createEl("h2", {
|
||||
text: step.label || "Loop",
|
||||
|
|
@ -954,6 +978,9 @@ export class InterviewWizardModal extends Modal {
|
|||
onFieldChange: (fieldId: string, value: unknown) => void
|
||||
): void {
|
||||
const existingValue = draftValue !== undefined ? String(draftValue) : "";
|
||||
// Use unique key for preview mode tracking in nested loops
|
||||
const previewKey = `${loopKey}.${nestedStep.key}`;
|
||||
const isPreviewMode = this.previewMode.get(previewKey) || false;
|
||||
|
||||
if (nestedStep.type === "capture_text") {
|
||||
// Field container
|
||||
|
|
@ -977,16 +1004,31 @@ export class InterviewWizardModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
// Editor container
|
||||
// Container for editor/preview
|
||||
const editorContainer = fieldContainer.createEl("div", {
|
||||
cls: "markdown-editor-container",
|
||||
});
|
||||
editorContainer.style.width = "100%";
|
||||
editorContainer.style.position = "relative";
|
||||
|
||||
// Preview container (hidden by default)
|
||||
const previewContainer = editorContainer.createEl("div", {
|
||||
cls: "markdown-preview-container",
|
||||
});
|
||||
previewContainer.style.display = isPreviewMode ? "block" : "none";
|
||||
previewContainer.style.width = "100%";
|
||||
previewContainer.style.minHeight = "240px";
|
||||
previewContainer.style.padding = "1em";
|
||||
previewContainer.style.border = "1px solid var(--background-modifier-border)";
|
||||
previewContainer.style.borderRadius = "4px";
|
||||
previewContainer.style.background = "var(--background-primary)";
|
||||
previewContainer.style.overflowY = "auto";
|
||||
|
||||
// Editor container
|
||||
const textEditorContainer = editorContainer.createEl("div", {
|
||||
cls: "markdown-editor-wrapper",
|
||||
});
|
||||
textEditorContainer.style.display = isPreviewMode ? "none" : "block";
|
||||
textEditorContainer.style.width = "100%";
|
||||
|
||||
const textSetting = new Setting(textEditorContainer);
|
||||
|
|
@ -999,10 +1041,18 @@ export class InterviewWizardModal extends Modal {
|
|||
settingNameEl.style.display = "none";
|
||||
}
|
||||
|
||||
let textareaRef: HTMLTextAreaElement | null = null;
|
||||
|
||||
textSetting.addTextArea((text) => {
|
||||
textareaRef = text.inputEl;
|
||||
text.setValue(existingValue);
|
||||
text.onChange((value) => {
|
||||
onFieldChange(nestedStep.key, value);
|
||||
// Update preview if in preview mode
|
||||
const currentPreviewMode = this.previewMode.get(previewKey) || false;
|
||||
if (currentPreviewMode) {
|
||||
this.updatePreview(previewContainer, value);
|
||||
}
|
||||
});
|
||||
text.inputEl.rows = 8;
|
||||
text.inputEl.style.width = "100%";
|
||||
|
|
@ -1010,14 +1060,34 @@ export class InterviewWizardModal extends Modal {
|
|||
text.inputEl.style.boxSizing = "border-box";
|
||||
});
|
||||
|
||||
// Add toolbar
|
||||
// Add toolbar with preview toggle
|
||||
setTimeout(() => {
|
||||
const textarea = textEditorContainer.querySelector("textarea");
|
||||
if (textarea) {
|
||||
const itemToolbar = createMarkdownToolbar(textarea);
|
||||
const itemToolbar = createMarkdownToolbar(
|
||||
textarea,
|
||||
() => {
|
||||
// Get current value from textarea before toggling
|
||||
const currentValue = textarea.value;
|
||||
// Update draft with current value
|
||||
onFieldChange(nestedStep.key, currentValue);
|
||||
|
||||
// Toggle preview mode
|
||||
const newPreviewMode = !this.previewMode.get(previewKey);
|
||||
this.previewMode.set(previewKey, newPreviewMode);
|
||||
|
||||
// Re-render to show/hide preview
|
||||
this.renderStep();
|
||||
}
|
||||
);
|
||||
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
// Render preview if in preview mode
|
||||
if (isPreviewMode && existingValue) {
|
||||
this.updatePreview(previewContainer, existingValue);
|
||||
}
|
||||
} else if (nestedStep.type === "capture_text_line") {
|
||||
// Field container
|
||||
const fieldContainer = containerEl.createEl("div", {
|
||||
|
|
@ -1112,12 +1182,11 @@ export class InterviewWizardModal extends Modal {
|
|||
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
|
||||
// Nested loop: render as a button to enter fullscreen mode
|
||||
const nestedLoopItems = Array.isArray(draftValue) ? draftValue : [];
|
||||
const nestedLoopKey = `${loopKey}.${nestedStep.key}`;
|
||||
|
||||
// Get or create nested loop state
|
||||
const nestedLoopKey = `${loopKey}.${nestedStep.key}`;
|
||||
let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (!nestedLoopState) {
|
||||
nestedLoopState = {
|
||||
|
|
@ -1129,55 +1198,322 @@ export class InterviewWizardModal extends Modal {
|
|||
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",
|
||||
// Field container
|
||||
const fieldContainer = containerEl.createEl("div", {
|
||||
cls: "mindnet-field",
|
||||
});
|
||||
nestedLoopContainer.style.border = "1px solid var(--background-modifier-border)";
|
||||
nestedLoopContainer.style.borderRadius = "4px";
|
||||
nestedLoopContainer.style.padding = "1em";
|
||||
nestedLoopContainer.style.marginTop = "0.5em";
|
||||
|
||||
// Label
|
||||
if (nestedStep.label) {
|
||||
const labelEl = nestedLoopContainer.createEl("div", {
|
||||
const labelEl = fieldContainer.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",
|
||||
// Show item count
|
||||
const countText = nestedLoopState.items.length > 0
|
||||
? `${nestedLoopState.items.length} ${nestedLoopState.items.length === 1 ? "Eintrag" : "Einträge"}`
|
||||
: "Keine Einträge";
|
||||
const countEl = fieldContainer.createEl("div", {
|
||||
cls: "mindnet-field__desc",
|
||||
text: countText,
|
||||
});
|
||||
nestedPaneContainer.style.display = "flex";
|
||||
nestedPaneContainer.style.gap = "1em";
|
||||
nestedPaneContainer.style.minHeight = "200px";
|
||||
countEl.style.marginBottom = "0.5em";
|
||||
|
||||
// Left pane: Items list (narrower for nested loops)
|
||||
const nestedLeftPane = nestedPaneContainer.createEl("div", {
|
||||
cls: "nested-loop-items-pane",
|
||||
// Button to enter nested loop (fullscreen mode)
|
||||
const enterBtn = fieldContainer.createEl("button", {
|
||||
text: nestedLoopState.items.length > 0 ? "Bearbeiten" : "Hinzufügen",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
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";
|
||||
enterBtn.style.width = "100%";
|
||||
enterBtn.onclick = () => {
|
||||
// Enter nested loop: add to activeLoopPath
|
||||
this.state.activeLoopPath.push(nestedLoopKey);
|
||||
this.renderStep();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const nestedItemsTitle = nestedLeftPane.createEl("div", {
|
||||
/**
|
||||
* Render breadcrumb navigation showing loop hierarchy.
|
||||
*/
|
||||
private renderBreadcrumb(containerEl: HTMLElement, currentStep: InterviewStep): void {
|
||||
const breadcrumbContainer = containerEl.createEl("div", {
|
||||
cls: "loop-breadcrumb",
|
||||
});
|
||||
breadcrumbContainer.style.display = "flex";
|
||||
breadcrumbContainer.style.alignItems = "center";
|
||||
breadcrumbContainer.style.gap = "0.5em";
|
||||
breadcrumbContainer.style.marginBottom = "1em";
|
||||
breadcrumbContainer.style.padding = "0.5em";
|
||||
breadcrumbContainer.style.background = "var(--background-secondary)";
|
||||
breadcrumbContainer.style.borderRadius = "4px";
|
||||
|
||||
// Build breadcrumb path
|
||||
const path: Array<{ key: string; label: string }> = [];
|
||||
|
||||
// Find all parent loops
|
||||
for (let i = 0; i < this.state.activeLoopPath.length; i++) {
|
||||
const loopKey = this.state.activeLoopPath[i];
|
||||
if (!loopKey) continue;
|
||||
|
||||
// Extract step key from loop key (e.g., "items.item_list" -> "item_list")
|
||||
const parts = loopKey.split(".");
|
||||
const stepKey = parts[parts.length - 1];
|
||||
if (!stepKey) continue;
|
||||
|
||||
// Find the step in the profile
|
||||
const step = this.findStepByKey(stepKey);
|
||||
if (step && step.type === "loop") {
|
||||
path.push({ key: loopKey, label: step.label || stepKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Render breadcrumb
|
||||
path.forEach((item, index) => {
|
||||
if (index > 0) {
|
||||
breadcrumbContainer.createEl("span", { text: "›" });
|
||||
}
|
||||
|
||||
const breadcrumbItem = breadcrumbContainer.createEl("button", {
|
||||
text: item.label,
|
||||
cls: "breadcrumb-item",
|
||||
});
|
||||
breadcrumbItem.style.background = "transparent";
|
||||
breadcrumbItem.style.border = "none";
|
||||
breadcrumbItem.style.cursor = "pointer";
|
||||
breadcrumbItem.style.textDecoration = index < path.length - 1 ? "underline" : "none";
|
||||
|
||||
if (index < path.length - 1) {
|
||||
breadcrumbItem.onclick = () => {
|
||||
// Navigate to this level
|
||||
this.state.activeLoopPath = this.state.activeLoopPath.slice(0, index + 1);
|
||||
this.renderStep();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Back button to parent level
|
||||
if (this.state.activeLoopPath.length > 0) {
|
||||
const backBtn = breadcrumbContainer.createEl("button", {
|
||||
text: "← Zurück",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
backBtn.style.marginLeft = "auto";
|
||||
backBtn.onclick = () => {
|
||||
this.state.activeLoopPath.pop();
|
||||
this.renderStep();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a step by its key in the profile (recursive search).
|
||||
*/
|
||||
private findStepByKey(key: string): InterviewStep | null {
|
||||
const searchInSteps = (steps: InterviewStep[]): InterviewStep | null => {
|
||||
for (const step of steps) {
|
||||
if (step.key === key) {
|
||||
return step;
|
||||
}
|
||||
if (step.type === "loop") {
|
||||
const found = searchInSteps(step.items);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return searchInSteps(this.state.profile.steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a nested loop in fullscreen mode (uses full width).
|
||||
*/
|
||||
private renderNestedLoopFullscreen(
|
||||
parentStep: InterviewStep,
|
||||
containerEl: HTMLElement,
|
||||
nestedLoopKey: string
|
||||
): void {
|
||||
// Extract the nested step from parent step
|
||||
const nestedStepKey = nestedLoopKey.split(".").pop();
|
||||
if (!nestedStepKey) return;
|
||||
|
||||
// parentStep must be a LoopStep to have items
|
||||
if (parentStep.type !== "loop") return;
|
||||
|
||||
const loopStep = parentStep as LoopStep;
|
||||
const nestedStep = loopStep.items.find((s: InterviewStep) => s.key === nestedStepKey);
|
||||
if (!nestedStep || nestedStep.type !== "loop") return;
|
||||
|
||||
// Get nested loop state
|
||||
let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (!nestedLoopState) {
|
||||
nestedLoopState = {
|
||||
items: [],
|
||||
draft: {},
|
||||
editIndex: null,
|
||||
activeItemStepIndex: 0,
|
||||
};
|
||||
this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState);
|
||||
}
|
||||
|
||||
// Breadcrumb
|
||||
this.renderBreadcrumb(containerEl, nestedStep);
|
||||
|
||||
// Context header: Show parent loop item context
|
||||
const contextHeader = containerEl.createEl("div", {
|
||||
cls: "nested-loop-context",
|
||||
});
|
||||
contextHeader.style.padding = "1em";
|
||||
contextHeader.style.background = "var(--background-secondary)";
|
||||
contextHeader.style.borderRadius = "6px";
|
||||
contextHeader.style.marginBottom = "1.5em";
|
||||
contextHeader.style.border = "1px solid var(--background-modifier-border)";
|
||||
|
||||
// Find parent loop context
|
||||
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
||||
const parentLoopState = this.state.loopRuntimeStates.get(parentLoopKey);
|
||||
|
||||
if (parentLoopState) {
|
||||
// Find the top-level loop step
|
||||
const topLevelLoopKey = nestedLoopKey.split(".")[0];
|
||||
if (!topLevelLoopKey) {
|
||||
// Fallback if no top-level key found
|
||||
const contextTitle = contextHeader.createEl("div", {
|
||||
cls: "context-title",
|
||||
text: "📍 Kontext: " + (parentStep.label || "Parent Loop"),
|
||||
});
|
||||
contextTitle.style.fontWeight = "bold";
|
||||
contextTitle.style.marginBottom = "0.5em";
|
||||
contextTitle.style.fontSize = "0.9em";
|
||||
contextTitle.style.color = "var(--text-muted)";
|
||||
} else {
|
||||
const topLevelLoopState = this.state.loopRuntimeStates.get(topLevelLoopKey);
|
||||
const topLevelStep = this.findStepByKey(topLevelLoopKey);
|
||||
|
||||
// Build context path: top-level loop > nested loop (current)
|
||||
const contextPath: string[] = [];
|
||||
if (topLevelStep && topLevelStep.type === "loop") {
|
||||
contextPath.push(topLevelStep.label || topLevelLoopKey);
|
||||
}
|
||||
// Add the nested loop label (the one we're currently in)
|
||||
if (nestedStep && nestedStep.type === "loop") {
|
||||
contextPath.push(nestedStep.label || nestedStepKey || "");
|
||||
}
|
||||
|
||||
const contextTitle = contextHeader.createEl("div", {
|
||||
cls: "context-title",
|
||||
});
|
||||
contextTitle.style.fontWeight = "bold";
|
||||
contextTitle.style.marginBottom = "0.5em";
|
||||
contextTitle.style.fontSize = "0.9em";
|
||||
contextTitle.style.color = "var(--text-muted)";
|
||||
|
||||
if (contextPath.length > 0) {
|
||||
contextTitle.textContent = "📍 Kontext: " + contextPath.join(" › ");
|
||||
} else {
|
||||
contextTitle.textContent = "📍 Kontext: " + (parentStep.label || "Parent Loop");
|
||||
}
|
||||
}
|
||||
|
||||
// Show which parent item we're editing
|
||||
if (parentLoopState.editIndex !== null) {
|
||||
const parentItem = parentLoopState.items[parentLoopState.editIndex];
|
||||
if (parentItem && typeof parentItem === "object") {
|
||||
const parentItemObj = parentItem as Record<string, unknown>;
|
||||
const contextInfo = contextHeader.createEl("div", {
|
||||
cls: "context-info",
|
||||
});
|
||||
contextInfo.style.fontSize = "0.85em";
|
||||
contextInfo.style.color = "var(--text-normal)";
|
||||
|
||||
// Show parent item fields (excluding the nested loop field itself)
|
||||
const parentFields: string[] = [];
|
||||
for (const [key, value] of Object.entries(parentItemObj)) {
|
||||
if (nestedStepKey && key !== nestedStepKey && value && typeof value === "string" && value.trim() !== "") {
|
||||
const step = loopStep.items.find(s => s.key === key);
|
||||
const label = step?.label || key;
|
||||
parentFields.push(`${label}: ${value.trim().substring(0, 60)}${value.trim().length > 60 ? "..." : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentFields.length > 0) {
|
||||
contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} - ${parentFields.join(" | ")}`;
|
||||
} else {
|
||||
contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} (bearbeiten)`;
|
||||
}
|
||||
} else {
|
||||
const contextInfo = contextHeader.createEl("div", {
|
||||
cls: "context-info",
|
||||
text: `Item ${parentLoopState.editIndex + 1} (bearbeiten)`,
|
||||
});
|
||||
contextInfo.style.fontSize = "0.85em";
|
||||
contextInfo.style.color = "var(--text-normal)";
|
||||
}
|
||||
} else {
|
||||
// New item in parent loop
|
||||
const contextInfo = contextHeader.createEl("div", {
|
||||
cls: "context-info",
|
||||
text: "Neues Item (wird erstellt)",
|
||||
});
|
||||
contextInfo.style.fontSize = "0.85em";
|
||||
contextInfo.style.color = "var(--text-muted)";
|
||||
contextInfo.style.fontStyle = "italic";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
containerEl.createEl("h2", {
|
||||
text: nestedStep.label || "Verschachtelter Loop",
|
||||
});
|
||||
|
||||
// Show editing indicator
|
||||
if (nestedLoopState.editIndex !== null) {
|
||||
const indicator = containerEl.createEl("div", {
|
||||
cls: "loop-editing-indicator",
|
||||
text: `✏️ Editing item ${nestedLoopState.editIndex + 1}`,
|
||||
});
|
||||
indicator.style.padding = "0.5em";
|
||||
indicator.style.background = "var(--background-modifier-border-hover)";
|
||||
indicator.style.borderRadius = "4px";
|
||||
indicator.style.marginBottom = "1em";
|
||||
}
|
||||
|
||||
// Full-width 2-pane container
|
||||
const paneContainer = containerEl.createEl("div", {
|
||||
cls: "loop-pane-container",
|
||||
});
|
||||
paneContainer.style.display = "flex";
|
||||
paneContainer.style.gap = "1em";
|
||||
paneContainer.style.width = "100%";
|
||||
|
||||
// Left pane: Items list (30% for fullscreen mode)
|
||||
const leftPane = paneContainer.createEl("div", {
|
||||
cls: "loop-items-pane",
|
||||
});
|
||||
leftPane.style.width = "30%";
|
||||
leftPane.style.borderRight = "1px solid var(--background-modifier-border)";
|
||||
leftPane.style.paddingRight = "1em";
|
||||
leftPane.style.maxHeight = "70vh";
|
||||
leftPane.style.overflowY = "auto";
|
||||
|
||||
const itemsTitle = leftPane.createEl("h3", {
|
||||
text: "Einträge",
|
||||
cls: "mindnet-field__label",
|
||||
});
|
||||
nestedItemsTitle.style.marginBottom = "0.5em";
|
||||
itemsTitle.style.marginBottom = "0.5em";
|
||||
|
||||
// Render items list
|
||||
// Render items list (same as normal loop)
|
||||
nestedLoopState.items.forEach((item, i) => {
|
||||
const itemEl = nestedLeftPane.createEl("div", {
|
||||
cls: "nested-loop-item",
|
||||
const itemEl = leftPane.createEl("div", {
|
||||
cls: "loop-item",
|
||||
});
|
||||
itemEl.style.padding = "0.5em";
|
||||
itemEl.style.marginBottom = "0.25em";
|
||||
itemEl.style.padding = "0.75em";
|
||||
itemEl.style.marginBottom = "0.5em";
|
||||
itemEl.style.background = "var(--background-secondary)";
|
||||
itemEl.style.borderRadius = "4px";
|
||||
itemEl.style.cursor = "pointer";
|
||||
|
|
@ -1186,7 +1522,6 @@ export class InterviewWizardModal extends Modal {
|
|||
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();
|
||||
|
|
@ -1196,209 +1531,177 @@ export class InterviewWizardModal extends Modal {
|
|||
} else if (item) {
|
||||
itemText = String(item);
|
||||
}
|
||||
itemEl.textContent = itemText.length > 40 ? itemText.substring(0, 40) + "..." : itemText;
|
||||
itemEl.textContent = itemText.length > 50 ? itemText.substring(0, 50) + "..." : itemText;
|
||||
|
||||
// Edit button
|
||||
const editBtn = itemEl.createEl("button", {
|
||||
text: "✏️",
|
||||
cls: "nested-edit-btn",
|
||||
// Action buttons (same as normal loop)
|
||||
const buttonContainer = itemEl.createEl("div", {
|
||||
cls: "loop-item-actions",
|
||||
});
|
||||
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);
|
||||
buttonContainer.style.display = "flex";
|
||||
buttonContainer.style.gap = "0.25em";
|
||||
buttonContainer.style.marginTop = "0.5em";
|
||||
buttonContainer.style.justifyContent = "flex-end";
|
||||
|
||||
const editBtn = buttonContainer.createEl("button", { text: "✏️ Edit" });
|
||||
editBtn.onclick = () => {
|
||||
let newState = startEdit(nestedLoopState!, 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";
|
||||
const moveUpBtn = buttonContainer.createEl("button", { text: "↑" });
|
||||
moveUpBtn.disabled = i === 0;
|
||||
moveUpBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentNestedState) {
|
||||
const newState = moveItemUp(currentNestedState, i);
|
||||
moveUpBtn.onclick = () => {
|
||||
const newState = moveItemUp(nestedLoopState!, 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";
|
||||
const moveDownBtn = buttonContainer.createEl("button", { text: "↓" });
|
||||
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);
|
||||
moveDownBtn.onclick = () => {
|
||||
const newState = moveItemDown(nestedLoopState!, 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);
|
||||
const deleteBtn = buttonContainer.createEl("button", { text: "🗑️" });
|
||||
deleteBtn.onclick = () => {
|
||||
const newState = deleteItem(nestedLoopState!, 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",
|
||||
// Right pane: Editor (70% for fullscreen mode)
|
||||
const rightPane = paneContainer.createEl("div", {
|
||||
cls: "loop-editor-pane",
|
||||
});
|
||||
nestedRightPane.style.width = "75%";
|
||||
nestedRightPane.style.flex = "1";
|
||||
rightPane.style.width = "70%";
|
||||
rightPane.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}`
|
||||
// Subwizard header
|
||||
const itemTitle = rightPane.createEl("h3");
|
||||
const itemTitleText = nestedLoopState.editIndex !== null
|
||||
? `Item: ${nestedLoopState.editIndex + 1} (editing)`
|
||||
: "Item: New";
|
||||
const stepCounter = nestedStep.items.length > 0
|
||||
? ` - Step ${nestedLoopState.activeItemStepIndex + 1}/${nestedStep.items.length}`
|
||||
: "";
|
||||
nestedEditorTitle.textContent = nestedTitleText + nestedStepCounter;
|
||||
nestedEditorTitle.style.marginBottom = "0.5em";
|
||||
itemTitle.textContent = itemTitleText + stepCounter;
|
||||
|
||||
// Render active nested step
|
||||
if (nestedStep.items.length > 0) {
|
||||
const activeNestedStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1);
|
||||
const activeNestedNestedStep = nestedStep.items[activeNestedStepIndex];
|
||||
const activeStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1);
|
||||
const activeNestedStep = nestedStep.items[activeStepIndex];
|
||||
|
||||
// Recursively render nested step (supports arbitrary nesting depth)
|
||||
if (activeNestedNestedStep) {
|
||||
const nestedDraftValue = nestedLoopState.draft[activeNestedNestedStep.key];
|
||||
if (activeNestedStep) {
|
||||
const editorContainer = rightPane.createEl("div", {
|
||||
cls: "loop-item-editor",
|
||||
});
|
||||
|
||||
const draftValue = nestedLoopState.draft[activeNestedStep.key];
|
||||
this.renderLoopNestedStep(
|
||||
activeNestedNestedStep,
|
||||
nestedRightPane,
|
||||
activeNestedStep,
|
||||
editorContainer,
|
||||
nestedLoopKey,
|
||||
nestedDraftValue,
|
||||
draftValue,
|
||||
(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);
|
||||
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentState) {
|
||||
const newState = setDraftField(currentState, fieldId, value);
|
||||
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||
// Update parent draft without re-rendering
|
||||
const parentLoopState = this.state.loopRuntimeStates.get(loopKey);
|
||||
if (parentLoopState) {
|
||||
// Update parent draft
|
||||
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
||||
if (parentLoopKey) {
|
||||
const parentState = this.state.loopRuntimeStates.get(parentLoopKey);
|
||||
if (parentState && nestedStepKey) {
|
||||
const updatedParentDraft = {
|
||||
...parentLoopState.draft,
|
||||
[nestedStep.key]: newState.items,
|
||||
...parentState.draft,
|
||||
[nestedStepKey]: newState.items,
|
||||
};
|
||||
const updatedParentState = setDraftField(parentLoopState, nestedStep.key, newState.items);
|
||||
this.state.loopRuntimeStates.set(loopKey, updatedParentState);
|
||||
const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items);
|
||||
this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do NOT call renderStep() here - it causes focus loss
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Navigation buttons for nested loop
|
||||
const nestedNav = nestedRightPane.createEl("div", {
|
||||
cls: "nested-loop-navigation",
|
||||
// Navigation buttons (same as normal loop)
|
||||
const subwizardNav = rightPane.createEl("div", {
|
||||
cls: "loop-subwizard-navigation",
|
||||
});
|
||||
nestedNav.style.display = "flex";
|
||||
nestedNav.style.gap = "0.5em";
|
||||
nestedNav.style.marginTop = "1em";
|
||||
nestedNav.style.justifyContent = "space-between";
|
||||
subwizardNav.style.display = "flex";
|
||||
subwizardNav.style.gap = "0.5em";
|
||||
subwizardNav.style.marginTop = "1em";
|
||||
subwizardNav.style.justifyContent = "space-between";
|
||||
|
||||
// Left: Back/Next
|
||||
const nestedNavLeft = nestedNav.createEl("div");
|
||||
nestedNavLeft.style.display = "flex";
|
||||
nestedNavLeft.style.gap = "0.5em";
|
||||
const itemNavLeft = subwizardNav.createEl("div");
|
||||
itemNavLeft.style.display = "flex";
|
||||
itemNavLeft.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);
|
||||
const itemBackBtn = itemNavLeft.createEl("button", { text: "← Item Back" });
|
||||
itemBackBtn.disabled = activeStepIndex === 0;
|
||||
itemBackBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentState) {
|
||||
const newState = itemPrevStep(currentState);
|
||||
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);
|
||||
const itemNextBtn = itemNavLeft.createEl("button", { text: "Item Next →" });
|
||||
itemNextBtn.disabled = activeStepIndex >= nestedStep.items.length - 1;
|
||||
itemNextBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentState) {
|
||||
const newState = itemNextStep(currentState, 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 itemNavRight = subwizardNav.createEl("div");
|
||||
itemNavRight.style.display = "flex";
|
||||
itemNavRight.style.gap = "0.5em";
|
||||
|
||||
const nestedSaveBtn = nestedNavRight.createEl("button", {
|
||||
text: nestedLoopState.editIndex !== null ? "Speichern" : "Hinzufügen",
|
||||
const doneBtn = itemNavRight.createEl("button", {
|
||||
text: nestedLoopState.editIndex !== null ? "Save Item" : "Done",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
nestedSaveBtn.onclick = () => {
|
||||
const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentNestedState && isDraftDirty(currentNestedState.draft)) {
|
||||
const newState = commitDraft(currentNestedState);
|
||||
doneBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentState && isDraftDirty(currentState.draft)) {
|
||||
const newState = commitDraft(currentState);
|
||||
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||
// Update parent draft
|
||||
onFieldChange(nestedStep.key, newState.items);
|
||||
// Re-render to show updated item list
|
||||
const lastDotIndex = nestedLoopKey.lastIndexOf(".");
|
||||
if (lastDotIndex > 0 && nestedStepKey) {
|
||||
const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex);
|
||||
if (parentLoopKey) {
|
||||
const parentState = this.state.loopRuntimeStates.get(parentLoopKey);
|
||||
if (parentState) {
|
||||
const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items);
|
||||
this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
const clearBtn = itemNavRight.createEl("button", { text: "Clear" });
|
||||
clearBtn.onclick = () => {
|
||||
const currentState = this.state.loopRuntimeStates.get(nestedLoopKey);
|
||||
if (currentState) {
|
||||
const newState = clearDraft(currentState);
|
||||
this.state.loopRuntimeStates.set(nestedLoopKey, newState);
|
||||
this.renderStep();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user