api-chain-runner 2.5.5__tar.gz → 2.5.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {api_chain_runner-2.5.5/api_chain_runner.egg-info → api_chain_runner-2.5.6}/PKG-INFO +1 -1
  2. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/__init__.py +1 -1
  3. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/server.py +97 -3
  4. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/flow.js +87 -7
  5. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/style.css +44 -0
  6. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/templates/flow.html +25 -1
  7. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/templates/icons.html +4 -1
  8. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6/api_chain_runner.egg-info}/PKG-INFO +1 -1
  9. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/.gitignore +0 -0
  10. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/MANIFEST.in +0 -0
  11. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/PYPI_README.md +0 -0
  12. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/README.md +0 -0
  13. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/__main__.py +0 -0
  14. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/executor.py +0 -0
  15. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/generator.py +0 -0
  16. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/logger.py +0 -0
  17. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/models.py +0 -0
  18. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/pause.py +0 -0
  19. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/resolver.py +0 -0
  20. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/runner.py +0 -0
  21. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/store.py +0 -0
  22. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/__init__.py +0 -0
  23. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/create.js +0 -0
  24. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/docs.js +0 -0
  25. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/sidebar.js +0 -0
  26. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/static/theme.js +0 -0
  27. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/templates/docs.html +0 -0
  28. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/templates/editor.html +0 -0
  29. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner/ui/templates/index.html +0 -0
  30. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner.egg-info/SOURCES.txt +0 -0
  31. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner.egg-info/dependency_links.txt +0 -0
  32. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner.egg-info/entry_points.txt +0 -0
  33. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner.egg-info/requires.txt +0 -0
  34. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/api_chain_runner.egg-info/top_level.txt +0 -0
  35. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/pyproject.toml +0 -0
  36. {api_chain_runner-2.5.5 → api_chain_runner-2.5.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-chain-runner
3
- Version: 2.5.5
3
+ Version: 2.5.6
4
4
  Summary: Execute chained API calls defined in YAML configuration files
5
5
  Author-email: Sambit Saha <creativesambit@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  """API Chain Runner — execute chained API calls with dynamic reference resolution."""
2
2
 
3
- __version__ = "2.5.5"
3
+ __version__ = "2.5.6"
4
4
 
5
5
  from api_chain_runner.runner import ChainRunner
6
6
  from api_chain_runner.generator import UniqueDataGenerator
@@ -124,7 +124,7 @@ def _parse_chain(filepath: str) -> dict:
124
124
  "url": step.get("url", ""),
125
125
  "continue_on_error": step.get("continue_on_error", True),
126
126
  "delay": step.get("delay", 0),
127
- "has_polling": "polling" in step,
127
+ "has_polling": isinstance(step.get("polling"), dict),
128
128
  "has_payload": "payload" in step,
129
129
  "has_files": "files" in step,
130
130
  "has_unique_fields": "unique_fields" in step,
@@ -144,7 +144,7 @@ def _parse_chain(filepath: str) -> dict:
144
144
  "failure_message": step.get("failure_message", ""),
145
145
  "retry": step.get("retry"),
146
146
  }
147
- if "polling" in step:
147
+ if isinstance(step.get("polling"), dict):
148
148
  p = step["polling"]
149
149
  step_info["polling"] = {
150
150
  "key_path": p.get("key_path", ""),
@@ -167,6 +167,25 @@ def _read_raw_yaml(filepath: str) -> str:
167
167
  return f.read()
168
168
 
169
169
 
170
+ def _validate_step_names(raw: dict) -> None:
171
+ """Validate only the required and unique names in a candidate chain."""
172
+ chain = raw.get("chain") if isinstance(raw, dict) else None
173
+ if not isinstance(chain, list) or not chain:
174
+ raise ValueError("Configuration must contain a non-empty 'chain' list.")
175
+
176
+ seen = set()
177
+ for index, step in enumerate(chain):
178
+ if not isinstance(step, dict):
179
+ raise ValueError(f"Step at index {index} must be a mapping.")
180
+ name = step.get("name")
181
+ if not isinstance(name, str) or not name.strip():
182
+ raise ValueError(f"Step at index {index} must have a non-empty name.")
183
+ normalized = name.strip()
184
+ if normalized in seen:
185
+ raise ValueError(f"Duplicate step name '{normalized}'. Step names must be unique.")
186
+ seen.add(normalized)
187
+
188
+
170
189
  def _format_yaml_for_readability(yaml_str: str) -> str:
171
190
  """Post-process YAML to add blank lines before each step for better readability."""
172
191
  # Add newline before '- name:' if it's not already preceded by a blank line
@@ -504,7 +523,7 @@ def api_step_update(flow_path, step_index):
504
523
  return jsonify({"error": str(exc)}), 400
505
524
 
506
525
  # Apply updates to allowed fields
507
- allowed = {"payload", "headers", "url", "unique_fields", "delay", "continue_on_error", "method", "files", "print_keys", "polling", "eval_keys", "eval_condition", "success_message", "failure_message", "manual", "instruction", "print_ref", "retry", "condition"}
526
+ allowed = {"name", "payload", "headers", "url", "unique_fields", "delay", "continue_on_error", "method", "files", "print_keys", "polling", "eval_keys", "eval_condition", "success_message", "failure_message", "manual", "instruction", "print_ref", "retry", "condition"}
508
527
  for key, value in updates.items():
509
528
  if key not in allowed:
510
529
  continue
@@ -519,6 +538,11 @@ def api_step_update(flow_path, step_index):
519
538
  else:
520
539
  step[key] = value
521
540
 
541
+ try:
542
+ _validate_step_names(raw)
543
+ except ValueError as exc:
544
+ return jsonify({"error": str(exc)}), 400
545
+
522
546
  # Write back preserving original formatting as much as possible
523
547
  dumped = yaml.dump(
524
548
  raw,
@@ -536,6 +560,76 @@ def api_step_update(flow_path, step_index):
536
560
  return jsonify({"error": str(exc)}), 500
537
561
 
538
562
 
563
+ @app.route("/api/flow/<path:flow_path>/step/<int:step_index>", methods=["DELETE"])
564
+ def api_step_delete(flow_path, step_index):
565
+ """Delete one step after checking the remaining step names."""
566
+ abs_path = os.path.join(_flow_dir, flow_path)
567
+ if not os.path.isfile(abs_path):
568
+ return jsonify({"error": "not found"}), 404
569
+
570
+ try:
571
+ with open(abs_path, "r", encoding="utf-8") as f:
572
+ raw = yaml.safe_load(f)
573
+ chain = raw.get("chain", []) if isinstance(raw, dict) else []
574
+ if step_index < 0 or step_index >= len(chain):
575
+ return jsonify({"error": "step index out of range"}), 400
576
+
577
+ candidate = dict(raw)
578
+ candidate["chain"] = list(chain)
579
+ candidate["chain"].pop(step_index)
580
+ _validate_step_names(candidate)
581
+
582
+ dumped = yaml.dump(
583
+ candidate,
584
+ default_flow_style=False,
585
+ allow_unicode=True,
586
+ sort_keys=False,
587
+ default_style='"',
588
+ )
589
+ with open(abs_path, "w", encoding="utf-8") as f:
590
+ f.write(_format_yaml_for_readability(dumped))
591
+ return jsonify({"success": True})
592
+ except ValueError as exc:
593
+ return jsonify({"error": str(exc)}), 400
594
+ except Exception as exc:
595
+ return jsonify({"error": str(exc)}), 500
596
+
597
+
598
+ @app.route("/api/flow/<path:flow_path>/step", methods=["POST"])
599
+ def api_step_add(flow_path):
600
+ """Append one validated step to an existing flow."""
601
+ abs_path = os.path.join(_flow_dir, flow_path)
602
+ if not os.path.isfile(abs_path):
603
+ return jsonify({"error": "not found"}), 404
604
+
605
+ data = request.get_json(silent=True) or {}
606
+ new_step = data.get("step", data.get("updates"))
607
+ if not isinstance(new_step, dict):
608
+ return jsonify({"error": "step must be a mapping"}), 400
609
+
610
+ try:
611
+ with open(abs_path, "r", encoding="utf-8") as f:
612
+ raw = yaml.safe_load(f)
613
+ if not isinstance(raw, dict) or not isinstance(raw.get("chain"), list):
614
+ return jsonify({"error": "Configuration must contain a top-level 'chain' list."}), 400
615
+ candidate = dict(raw)
616
+ candidate["chain"] = list(raw["chain"]) + [dict(new_step)]
617
+ _validate_step_names(candidate)
618
+
619
+ dumped = yaml.dump(
620
+ candidate,
621
+ default_flow_style=False,
622
+ allow_unicode=True,
623
+ sort_keys=False,
624
+ default_style='"',
625
+ )
626
+ with open(abs_path, "w", encoding="utf-8") as f:
627
+ f.write(_format_yaml_for_readability(dumped))
628
+ return jsonify({"success": True})
629
+ except Exception as exc:
630
+ return jsonify({"error": str(exc)}), 400
631
+
632
+
539
633
  @app.route("/api/flow/create", methods=["POST"])
540
634
  def api_flow_create():
541
635
  """Create a new flow YAML file with initial steps."""
@@ -9,16 +9,24 @@
9
9
  const detailName = document.getElementById("detail-name");
10
10
  const detailBody = document.getElementById("detail-body");
11
11
  const detailClose = document.getElementById("detail-close");
12
+ const detailDelete = document.getElementById("detail-delete");
13
+ const deleteConfirmModal = document.getElementById("delete-confirm-modal");
14
+ const deleteConfirmStepName = document.getElementById("delete-confirm-step-name");
15
+ const deleteConfirmClose = document.getElementById("delete-confirm-close");
16
+ const deleteConfirmCancel = document.getElementById("delete-confirm-cancel");
17
+ const deleteConfirmOk = document.getElementById("delete-confirm-ok");
12
18
  const detailSave = document.getElementById("detail-save");
13
19
  const detailSaveStatus = document.getElementById("detail-save-status");
14
20
  const responsePanel = document.getElementById("response-panel");
15
21
  const responseList = document.getElementById("response-list");
16
22
  const responseClose = document.getElementById("response-close");
17
23
  const responseDownloadCsv = document.getElementById("response-download-csv");
24
+ const addStepBtn = document.getElementById("add-step-btn");
18
25
 
19
26
  const steps = CHAIN_DATA.steps;
20
27
  const stepBoxes = [];
21
28
  let currentStepIndex = -1;
29
+ let isCreatingStep = false;
22
30
  let currentResponseResults = [];
23
31
 
24
32
  // ── Render VERTICAL flow ─────────────────────────────────
@@ -82,9 +90,12 @@
82
90
  }
83
91
 
84
92
  // ── Detail drawer with editable fields ───────────────────
85
- function showDetail(step, index) {
93
+ function showDetail(step, index, isNew = false) {
86
94
  currentStepIndex = index;
87
- detailName.textContent = step.name;
95
+ isCreatingStep = isNew;
96
+ detailName.textContent = isNew ? "Add Step" : step.name;
97
+ detailDelete.classList.toggle("hidden", isNew);
98
+ detailSave.textContent = isNew ? "Save Step" : "Save Changes";
88
99
  detailSaveStatus.textContent = "";
89
100
  detailSaveStatus.className = "detail-save-status";
90
101
 
@@ -95,6 +106,7 @@
95
106
  if (isManual) {
96
107
  // ── Manual step UI ──
97
108
  html += `<div class="detail-row"><div class="detail-label">Type</div><div class="detail-value">Manual Step</div></div>`;
109
+ html += inputRow("name", "Step Name", step.name || "");
98
110
  html += editableRow("instruction", "Instruction", step.instruction || "");
99
111
  html += buildListField("print_ref", "Print References", step.print_ref || [], "e.g. create-lead.leadId");
100
112
  html += buildToggleSection("condition", "Conditions", (step.condition || []).length ? step.condition : null, buildConditionFields);
@@ -102,6 +114,7 @@
102
114
  html += dropdownRow("continue_on_error", "Continue on Error", step.continue_on_error);
103
115
  } else {
104
116
  // ── API step UI ──
117
+ html += inputRow("name", "Step Name", step.name || "");
105
118
  const methods = ["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"];
106
119
  html += `<div class="detail-row"><div class="detail-label">Method</div>
107
120
  <select class="detail-select" data-field="method">${methods.map(m => `<option${m===curMethod?" selected":""}>${m}</option>`).join("")}</select></div>`;
@@ -483,7 +496,7 @@
483
496
 
484
497
  // ── Save step changes from drawer ────────────────────────
485
498
  detailSave.addEventListener("click", async () => {
486
- if (currentStepIndex < 0) return;
499
+ if (!isCreatingStep && currentStepIndex < 0) return;
487
500
  const editables = detailBody.querySelectorAll(".detail-editable, .detail-editable-input, .detail-select");
488
501
  const updates = {};
489
502
 
@@ -494,7 +507,7 @@
494
507
 
495
508
  if (!raw && !["method", "url", "delay", "continue_on_error", "instruction"].includes(field)) continue;
496
509
 
497
- if (["url", "method", "instruction", "eval_condition", "success_message", "failure_message"].includes(field)) {
510
+ if (["name", "url", "method", "instruction", "eval_condition", "success_message", "failure_message"].includes(field)) {
498
511
  updates[field] = raw;
499
512
  } else if (field === "delay") {
500
513
  updates[field] = parseInt(raw) || 0;
@@ -511,6 +524,14 @@
511
524
  }
512
525
  }
513
526
 
527
+ if (!updates.name || !updates.name.trim()) {
528
+ detailSaveStatus.textContent = "Step name is required";
529
+ detailSaveStatus.className = "detail-save-status error";
530
+ detailBody.querySelector('[data-field="name"]')?.focus();
531
+ return;
532
+ }
533
+ updates.name = updates.name.trim();
534
+
514
535
  // Collect list fields (print_keys, print_ref)
515
536
  detailBody.querySelectorAll(".list-field").forEach(container => {
516
537
  const field = container.dataset.listField;
@@ -604,14 +625,18 @@
604
625
  detailSaveStatus.className = "detail-save-status";
605
626
 
606
627
  try {
607
- const res = await fetch(`/api/flow/${FLOW_PATH}/step/${currentStepIndex}`, {
628
+ const endpoint = isCreatingStep
629
+ ? `/api/flow/${FLOW_PATH}/step`
630
+ : `/api/flow/${FLOW_PATH}/step/${currentStepIndex}`;
631
+ const body = isCreatingStep ? { step: updates } : { updates };
632
+ const res = await fetch(endpoint, {
608
633
  method: "POST",
609
634
  headers: { "Content-Type": "application/json" },
610
- body: JSON.stringify({ updates }),
635
+ body: JSON.stringify(body),
611
636
  });
612
637
  const data = await res.json();
613
638
  if (data.success) {
614
- detailSaveStatus.textContent = "✓ Saved";
639
+ detailSaveStatus.textContent = isCreatingStep ? "✓ Step added" : "✓ Saved";
615
640
  detailSaveStatus.className = "detail-save-status success";
616
641
  setTimeout(() => location.reload(), 1200);
617
642
  } else {
@@ -624,6 +649,61 @@
624
649
  }
625
650
  });
626
651
 
652
+ detailDelete.addEventListener("click", () => {
653
+ if (isCreatingStep || currentStepIndex < 0) return;
654
+ deleteConfirmStepName.textContent = steps[currentStepIndex].name;
655
+ deleteConfirmModal.classList.remove("hidden");
656
+ deleteConfirmOk.focus();
657
+ });
658
+
659
+ function closeDeleteConfirmation() {
660
+ deleteConfirmModal.classList.add("hidden");
661
+ }
662
+
663
+ deleteConfirmClose.addEventListener("click", closeDeleteConfirmation);
664
+ deleteConfirmCancel.addEventListener("click", closeDeleteConfirmation);
665
+ deleteConfirmModal.addEventListener("click", (event) => {
666
+ if (event.target === deleteConfirmModal) closeDeleteConfirmation();
667
+ });
668
+
669
+ deleteConfirmOk.addEventListener("click", async () => {
670
+ if (isCreatingStep || currentStepIndex < 0) return;
671
+ closeDeleteConfirmation();
672
+ detailDelete.disabled = true;
673
+ deleteConfirmOk.disabled = true;
674
+ detailSaveStatus.textContent = "Deleting...";
675
+ detailSaveStatus.className = "detail-save-status";
676
+ try {
677
+ const res = await fetch(`/api/flow/${FLOW_PATH}/step/${currentStepIndex}`, { method: "DELETE" });
678
+ const data = await res.json();
679
+ if (!data.success) throw new Error(data.error || "Failed to delete step");
680
+ detailSaveStatus.textContent = "✓ Deleted";
681
+ detailSaveStatus.className = "detail-save-status success";
682
+ setTimeout(() => location.reload(), 700);
683
+ } catch (err) {
684
+ detailDelete.disabled = false;
685
+ deleteConfirmOk.disabled = false;
686
+ detailSaveStatus.textContent = "✗ " + err.message;
687
+ detailSaveStatus.className = "detail-save-status error";
688
+ }
689
+ });
690
+
691
+ addStepBtn.addEventListener("click", () => {
692
+ showDetail({
693
+ name: "",
694
+ method: "GET",
695
+ url: "",
696
+ manual: false,
697
+ continue_on_error: true,
698
+ delay: 0,
699
+ headers: {},
700
+ print_keys: [],
701
+ condition: [],
702
+ retry: false,
703
+ }, -1, true);
704
+ detailBody.querySelector('[data-field="name"]')?.focus();
705
+ });
706
+
627
707
  // ── Run chain ────────────────────────────────────────────
628
708
  let pollTimer = null;
629
709
  let currentRunId = null;
@@ -136,6 +136,38 @@ body > svg[style*="display:none"] { position: absolute; width: 0; height: 0; ove
136
136
  /* ── Flow Canvas (VERTICAL) ───────────────────────────────── */
137
137
  .flow-canvas-wrapper { overflow: auto; padding-bottom: 1rem; margin-bottom: 1rem; }
138
138
  .flow-canvas { display: flex; flex-direction: column; align-items: center; gap: 0; padding: 1.5rem 1rem; }
139
+ .add-step-btn {
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ gap: 0.35rem;
144
+ margin: 0 auto 1rem;
145
+ padding: 0.15rem 0.5rem;
146
+ border: 0;
147
+ background: transparent;
148
+ color: var(--accent);
149
+ cursor: pointer;
150
+ }
151
+ .add-step-btn:hover { color: var(--accent-hover); }
152
+ .add-step-plus {
153
+ width: 35px;
154
+ height: 35px;
155
+ display: inline-flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ flex-shrink: 0;
159
+ border: 2px solid rgba(255,255,255,0.18);
160
+ border-radius: 50%;
161
+ background: var(--accent);
162
+ color: #fff;
163
+ font-size: 1.3rem;
164
+ font-weight: 500;
165
+ line-height: 1;
166
+ box-shadow: 0 3px 10px rgba(124,143,255,0.25);
167
+ transition: transform 0.15s, background 0.15s;
168
+ }
169
+ .add-step-btn:hover .add-step-plus { background: var(--accent-hover); transform: scale(1.06); }
170
+ .add-step-label { font-size: 0.72rem; font-weight: 700; line-height: 1; }
139
171
  .step-node { display: flex; flex-direction: column; align-items: center; position: relative; }
140
172
  .step-node > .step-index { width: 280px; text-align: center; }
141
173
  .step-row {
@@ -544,6 +576,11 @@ body > svg[style*="display:none"] { position: absolute; width: 0; height: 0; ove
544
576
  .create-status { margin-top: 0.75rem; font-size: 0.78rem; }
545
577
  .create-status.success { color: var(--green); }
546
578
  .create-status.error { color: var(--red); }
579
+ .delete-confirm-panel { width: 420px; }
580
+ .delete-confirm-warning { margin-top: 0.5rem; color: var(--text-muted); font-size: 0.8rem; }
581
+ .delete-confirm-btn { background: var(--red); color: #fff; border-color: var(--red); }
582
+ .delete-confirm-btn:hover { filter: brightness(1.08); }
583
+ .delete-confirm-btn:disabled { opacity: 0.6; cursor: not-allowed; }
547
584
 
548
585
  /* ── Flow Documentation ───────────────────────────────────── */
549
586
  .doc-header-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; margin-bottom: 1.5rem; }
@@ -796,6 +833,10 @@ body > svg[style*="display:none"] { position: absolute; width: 0; height: 0; ove
796
833
  .response-pre-wrap .bounded-value-actions .value-expand::after {
797
834
  display: none;
798
835
  }
836
+ .response-pre-wrap .bounded-value-actions .value-expand {
837
+ width: 30px;
838
+ height: 30px;
839
+ }
799
840
 
800
841
  scroll owner while header, footer, close, and save controls stay reachable. */
801
842
  .step-detail-panel {
@@ -813,6 +854,9 @@ body > svg[style*="display:none"] { position: absolute; width: 0; height: 0; ove
813
854
  max-width: 100%;
814
855
  }
815
856
  .detail-header { gap: 0.75rem; }
857
+ .detail-header-actions { display: flex; align-items: center; gap: 0.35rem; }
858
+ .detail-delete { color: var(--red); transform: translateX(20px); }
859
+ .detail-delete:hover { background: var(--red-bg); color: var(--red); }
816
860
  .detail-header h3 {
817
861
  min-width: 0;
818
862
  overflow-wrap: anywhere;
@@ -87,6 +87,10 @@
87
87
  </div>
88
88
  <div class="flow-canvas-wrapper">
89
89
  <div id="flow-canvas" class="flow-canvas"></div>
90
+ <button id="add-step-btn" type="button" class="add-step-btn" aria-label="Add Step">
91
+ <span class="add-step-plus" aria-hidden="true">+</span>
92
+ <span class="add-step-label">Add Step</span>
93
+ </button>
90
94
  </div>
91
95
 
92
96
  <div id="response-panel" class="response-panel hidden">
@@ -114,7 +118,10 @@
114
118
  <div class="step-detail-panel">
115
119
  <div class="detail-header">
116
120
  <h3 id="detail-name"></h3>
117
- <button id="detail-close" type="button" class="btn-icon-only" aria-label="Close step details"><svg class="icon"><use href="#i-x"/></svg></button>
121
+ <div class="detail-header-actions">
122
+ <button id="detail-delete" type="button" class="btn-icon-only detail-delete hidden" aria-label="Delete step" data-tooltip="Delete step"><svg class="icon"><use href="#i-trash"/></svg></button>
123
+ <button id="detail-close" type="button" class="btn-icon-only" aria-label="Close step details"><svg class="icon"><use href="#i-x"/></svg></button>
124
+ </div>
118
125
  </div>
119
126
  <div id="detail-body" class="detail-body"></div>
120
127
  <div class="detail-footer">
@@ -126,6 +133,23 @@
126
133
  </div>
127
134
  </div>
128
135
 
136
+ <div id="delete-confirm-modal" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title">
137
+ <div class="modal-panel delete-confirm-panel">
138
+ <div class="modal-header">
139
+ <h3 id="delete-confirm-title">Delete Step</h3>
140
+ <button id="delete-confirm-close" type="button" class="btn-icon-only" aria-label="Close delete confirmation"><svg class="icon"><use href="#i-x"/></svg></button>
141
+ </div>
142
+ <div class="modal-body">
143
+ <p>Are you sure you want to delete <strong id="delete-confirm-step-name"></strong>?</p>
144
+ <p class="delete-confirm-warning">This action cannot be undone.</p>
145
+ </div>
146
+ <div class="modal-footer">
147
+ <button id="delete-confirm-cancel" type="button" class="btn btn-ghost btn-sm">Cancel</button>
148
+ <button id="delete-confirm-ok" type="button" class="btn btn-sm delete-confirm-btn">Delete Step</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
129
153
  <script id="chain-json" type="application/json">{{ chain | tojson | safe }}</script>
130
154
  <script>
131
155
  const CHAIN_DATA = JSON.parse(document.getElementById('chain-json').textContent);
@@ -34,7 +34,10 @@
34
34
  </symbol>
35
35
  <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
36
36
  </symbol>
37
- <symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
37
+ <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+ <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5M14 11v5"/><path d="M9 6V3h6v3"/>
39
+ </symbol>
40
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
38
41
  <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
39
42
  <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
40
43
  <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-chain-runner
3
- Version: 2.5.5
3
+ Version: 2.5.6
4
4
  Summary: Execute chained API calls defined in YAML configuration files
5
5
  Author-email: Sambit Saha <creativesambit@gmail.com>
6
6
  License: MIT