Files
IMSAPI_Modul_REST/index.html
2026-05-15 20:07:54 +02:00

2355 lines
74 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>IMSAPI Workbench Keys/Filters/Values (iOS Style)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* ======================================
iOS-STYLE COLORS & TYPO
====================================== */
:root{
--bg:#0f1115;
--panel:#151822;
--panel2:#191d28;
--text:#f2f5f9;
--muted:#a6b0c2;
--accent:#34c759; /* iOS Grün */
--accent-2:#0a84ff; /* iOS Blau */
--warn:#ff9f0a;
--error:#ff453a;
--ok:#30d158;
--chip:#1f2430;
--border:#232836;
--shadow: 0 8px 24px rgba(0,0,0,0.35), 0 2px 6px rgba(0,0,0,0.25);
/* Required */
--req-bg:#2f2812;
--req-border:#cda33a;
--req-text:#ffe8a3;
--radius: 14px;
--radius-sm: 10px;
--radius-pill: 999px;
}
*{box-sizing:border-box}
html, body { height:100%; }
body{
margin:0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, Arial, "Helvetica Neue", sans-serif;
background:linear-gradient(180deg, #0f1115 0%, #0c0e13 100%);
color:var(--text);
display:flex;
height:100vh;
}
/* SIDEBAR */
#sidebar{
width:320px;
background:linear-gradient(180deg, #121522 0%, #0f1220 100%);
padding:16px;
border-right:1px solid var(--border);
overflow:auto;
box-shadow: var(--shadow);
}
h1,h2{margin:0 0 10px}
h1{font-weight:700; letter-spacing:.2px;}
.item{
cursor:pointer;
padding:10px 12px;
margin:6px 0;
border-radius:var(--radius-sm);
color:#ccd3e0;
transition: all .18s ease;
}
.item:hover{
background:#1a2130;
color:#fff;
}
.item.group{
font-weight:700;
background:#131827;
border:1px solid var(--border);
margin-top:12px;
color:#e7ecf7;
box-shadow: var(--shadow);
}
/* CONTENT */
#content{
flex:1;
padding:24px;
overflow:auto;
}
.card {
background:linear-gradient(180deg, #161a27 0%, #121725 100%);
border:1px solid var(--border);
border-radius:var(--radius);
box-shadow: var(--shadow);
}
.param{
margin:12px 0;
padding:12px;
background:linear-gradient(180deg, #191f2e 0%, #171c2a 100%);
border:1px solid var(--border);
border-radius:var(--radius-sm);
}
.param label{
display:block;
font-size:12px;
color:var(--muted);
margin-bottom:6px;
}
/* INPUT */
.param-input{
width:100%;
padding:12px 14px;
border-radius:12px;
border:1px solid #2a3040;
background:#0f1420;
color:var(--text);
outline:none;
transition: border .2s ease, box-shadow .2s;
}
.param-input:focus{
border-color:#3856ff66;
box-shadow:0 0 0 6px rgba(10,132,255,0.08);
}
/* BUTTONS */
.btn{
display:inline-flex;
align-items:center;
gap:8px;
padding:10px 14px;
border:1px solid #2a3040;
background:linear-gradient(180deg, #1b2232 0%, #181f2f 100%);
color:var(--text);
border-radius:12px;
cursor:pointer;
transition: all .16s ease;
}
.btn:hover{
border-color:#3856ff66;
color:#eaf2ff;
transform: translateY(-1px);
}
.btn-primary{
background:linear-gradient(180deg, #36d36f 0%, #2fbf63 100%);
color:#061108;
border-color:transparent;
box-shadow: 0 6px 16px rgba(52,199,89,0.25);
}
.btn-primary:hover{filter:brightness(1.06)}
.btn-small{padding:8px 12px; font-size:13px}
/* CHIPS */
.pill{
display:inline-flex;
align-items:center;
padding:6px 10px;
border-radius:var(--radius-pill);
background:#1a2232;
border:1px solid #2a3146;
color:#e1e7f5;
font-size:12px;
cursor:pointer;
user-select:none;
transition: all .12s ease;
}
.pill:hover{ filter:brightness(1.06); }
.pill.active{
background:#0f2c27;
border-color:#2c7a69;
color:#b7ffec;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15);
}
.pill.req{
background:var(--req-bg);
border-color:var(--req-border);
color:var(--req-text);
}
/* MODAL (iOS Sheet-Style) */
.modal-backdrop{
position:fixed;
inset:0;
background:rgba(0,0,0,0.45);
display:none;
align-items:flex-end; /* iOS sheet von unten */
justify-content:center;
padding:20px;
z-index:30;
backdrop-filter: blur(2px);
}
.modal{
width:min(940px,100vw);
max-height:85vh;
overflow:auto;
background:linear-gradient(180deg, #141a28 0%, #101624 100%);
border:1px solid #273049;
border-radius:22px 22px 0 0; /* Sheet oben rund */
box-shadow: 0 -8px 30px rgba(0,0,0,0.45);
}
.modal header{
display:flex;
justify-content:space-between;
align-items:center;
padding:16px 18px;
border-bottom:1px solid #273049;
font-weight:700;
letter-spacing:.2px;
}
.modal .content{padding:16px 18px}
.modal .actions{
padding:14px 18px;
border-top:1px solid #273049;
display:flex;
justify-content:flex-end;
gap:10px;
}
.grid{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
gap:10px;
}
.sep{height:1px;background:#273049;margin:12px 0}
#result{
background:#0e1220;
border:1px solid #262d44;
padding:14px;
border-radius:12px;
white-space:pre-wrap;
margin-top:12px;
box-shadow: var(--shadow);
}
/* LOGIN PANEL */
#loginPanel{
background:linear-gradient(180deg, #161c2b 0%, #121826 100%);
padding:12px;
border:1px solid var(--border);
border-radius:var(--radius);
margin-bottom:12px;
box-shadow: var(--shadow);
}
.sidebar-search{
margin:10px 0 12px;
position:sticky;
top:0;
z-index:2;
}
.api-sub{
margin-left:12px;
display:none;
}
.api-no-results{
color:var(--muted);
font-size:13px;
padding:10px 12px;
display:none;
}
/* SCRIPTEDITOR */
.script-panel-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:12px;
margin-bottom:8px;
}
.script-panel-title{
margin:0;
}
.script-language-menu{
display:flex;
align-items:center;
gap:8px;
margin-left:auto;
white-space:nowrap;
}
.script-language-menu label{
color:var(--muted);
font-size:12px;
}
@media (max-width: 720px){
.script-panel-header{
flex-direction:column;
align-items:stretch;
}
.script-language-menu{
margin-left:0;
justify-content:space-between;
}
.script-language-menu .script-select{
flex:1;
}
}
.script-toolbar{
display:flex;
flex-wrap:wrap;
gap:8px;
margin:10px 0;
}
.script-select{
min-width:170px;
padding:8px 12px;
border-radius:12px;
border:1px solid #2a3040;
background:#0f1420;
color:var(--text);
outline:none;
}
.script-select:focus{
border-color:#3856ff66;
box-shadow:0 0 0 6px rgba(10,132,255,0.08);
}
.script-example-note{
color:var(--muted);
font-size:12px;
line-height:1.45;
margin:6px 0 0;
}
#scriptEditor{
width:100%;
min-height:260px;
resize:vertical;
padding:12px 14px;
border-radius:12px;
border:1px solid #2a3040;
background:#0f1420;
color:var(--text);
outline:none;
font-family:"SF Mono", Consolas, "Liberation Mono", monospace;
font-size:13px;
line-height:1.45;
}
#scriptEditor:focus{
border-color:#3856ff66;
box-shadow:0 0 0 6px rgba(10,132,255,0.08);
}
.script-hint{
color:var(--muted);
font-size:12px;
line-height:1.45;
margin:8px 0 0;
}
#scriptResult{
background:#0e1220;
border:1px solid #262d44;
padding:12px;
border-radius:12px;
white-space:pre-wrap;
margin-top:10px;
max-height:280px;
overflow:auto;
box-shadow: var(--shadow);
}
h3.section {
margin: 18px 0 10px;
font-size: 13px;
color: var(--muted);
font-weight: 600;
letter-spacing: .2px;
}
</style>
</head>
<body>
<!-- SIDEBAR -->
<div id="sidebar">
<h1>IMSAPI</h1>
<div id="loginPanel">
<label>Server URL:</label>
<input id="serverUrl" class="param-input" placeholder="http://192.168.21.55">
<label>Port:</label>
<input id="serverPort" class="param-input" placeholder="8080">
<label>Station Number:</label>
<input id="stationNumber" class="param-input" placeholder="RVZ01">
<label>Client:</label>
<input id="client" class="param-input" placeholder="01">
<label>Reg Type:</label>
<input id="regType" class="param-input" placeholder="S">
<script>
document.getElementById("regType").value = "S" || "";
document.getElementById("client").value = "01" || "";
</script>
<button id="btnLogin" class="btn btn-primary" style="margin-top:10px;">Login</button>
<div id="loginStatus" class="hint" style="margin-top:8px; color:#c9d3e5;">Nicht eingeloggt</div>
</div>
<input id="apiSearch" class="param-input sidebar-search" placeholder="API / Funktion filtern..." autocomplete="off">
<div id="apiTree"></div>
</div>
<!-- MAIN -->
<div id="content">
<div class="card" id="scriptPanel" style="padding:18px; margin-bottom:16px;">
<div class="script-panel-header">
<h1 id="scriptPanelTitle" class="script-panel-title">Scripteditor</h1>
<div class="script-language-menu">
<label id="scriptLanguageLabel" for="scriptExampleLanguage">Sprache:</label>
<select id="scriptExampleLanguage" class="script-select" title="Sprache für Beispieltexte">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="zh">中文</option>
</select>
</div>
</div>
<div id="scriptHint" class="script-hint">
Mehrere APIs können hier als JSON-Script nacheinander ausgeführt werden. ResultKeys/ResultValues werden automatisch als Variablen exportiert, z. B. <code>{{vars.workorderNumber}}</code> oder <code>{{vars.getStationSetting.WORKORDER_NUMBER}}</code>. Login-Werte sind ebenfalls nutzbar, z. B. <code>{{login.stationNumber}}</code>; <code>sessionContext</code> wird im sichtbaren Payload immer automatisch aus dem Login als erstes Objekt gesetzt. Leere <code>stationNumber</code>-Felder werden ebenfalls aus dem Login ergänzt. Manuelle Platzhalter wie <code>{{row:stepId:resultValues:0:KEY}}</code>, <code>{{col:stepId:resultValues:KEY}}</code> und <code>{"$rows":{...}}</code> funktionieren weiterhin.
</div>
<div class="script-toolbar">
<button id="btnAddCurrentStep" class="btn btn-small">Aktuelle API als Step hinzufügen</button>
<button id="btnScriptExample" class="btn btn-small">Beispiel einfügen</button>
<button id="btnRunScript" class="btn btn-small btn-primary">Script ausführen</button>
<button id="btnClearScriptLog" class="btn btn-small" style="background:#101624;border-color:#273049;">Log leeren</button>
</div>
<div id="scriptExampleNote" class="script-example-note">Beispieltexte werden in der gewählten Sprache eingefügt. Die ausführbaren JSON-Felder bleiben unverändert.</div>
<textarea id="scriptEditor" spellcheck="false" placeholder='{"steps":[]}'></textarea>
<pre id="scriptResult">Noch kein Script ausgeführt.</pre>
</div>
<div class="card" style="padding:18px;">
<h1 style="margin-bottom:12px;">API Details</h1>
<div id="form"></div>
<h3 class="section">Payload</h3>
<pre id="result"></pre>
</div>
</div>
<!-- MODAL -->
<div id="backdrop" class="modal-backdrop">
<div class="modal">
<header>
<span id="modalTitle">Titel</span>
<button id="modalClose" class="btn btn-small" style="background:#101624;border-color:#273049;"></button>
</header>
<div id="modalContent" class="content"></div>
<div class="actions">
<button id="modalCancel" class="btn btn-small" style="background:#101624;border-color:#273049;">Abbrechen</button>
<button id="modalOk" class="btn btn-small btn-primary">OK</button>
</div>
</div>
</div>
<script>
/* ============================================================
DEDUPE HELPERS (optional, falls benötigt)
============================================================ */
function uniqueOptionsByKeyName(options) {
const seen = new Set();
const out = [];
for (const o of options || []) {
const key = String(o?.keyName || o?.name || "").toLowerCase();
if (!key || seen.has(key)) continue;
seen.add(key);
out.push(o);
}
return out;
}
function uniqueFiltersByName(filters) {
const seen = new Set();
const out = [];
for (const f of filters || []) {
// Support plain strings and objects
const key = typeof f === "string" ? f.toLowerCase() : String(f?.filterName || f?.name || "").toLowerCase();
if (!key || seen.has(key)) continue;
seen.add(key);
out.push(f);
}
return out;
}
function uniqueParamsByName(params) {
const seen = new Set();
const out = [];
for (const p of params || []) {
const key = String(p?.name || "").toLowerCase();
if (!seen.has(key)) {
seen.add(key);
out.push(p);
}
}
return out;
}
/* ============================================================
GLOBAL STATE
============================================================ */
let globalLogin = null;
const state = {
currentFn: null,
selections: {}
};
function ensureFnState(fn){
if(!state.selections[fn]){
state.selections[fn]={keys:{},filters:{},values:{},filterValues:{}};
}
if(!state.selections[fn].filterValues){
state.selections[fn].filterValues={};
}
return state.selections[fn];
}
/* ============================================================
HELPERS
============================================================ */
function $(id){return document.getElementById(id);}
function extractPrefix(fnName){
let prefix="";
for(let i=0;i<fnName.length;i++){
const c=fnName[i];
if(c.toUpperCase()===c && c.toLowerCase()!==c) break;
prefix+=c;
}
return prefix.toLowerCase();
}
function chip(label,active=false,className=""){
const el=document.createElement("div");
el.className=`pill ${className} ${active?"active":""}`;
el.textContent=label;
return el;
}
function toggleSet(set, value) {
if (set.has(value)) {
set.delete(value);
} else {
set.add(value);
}
}
/* ============================================================
MODAL SYSTEM (iOS Sheet)
============================================================ */
const backdrop=$("backdrop");
const modalTitle=$("modalTitle");
const modalContent=$("modalContent");
const modalOk=$("modalOk");
const modalCancel=$("modalCancel");
const modalClose=$("modalClose");
function openModal({title,contentNode,onOk}){
modalTitle.textContent=title;
modalContent.innerHTML="";
if(contentNode) modalContent.appendChild(contentNode);
backdrop.style.display="flex";
function close(){
backdrop.style.display="none";
modalOk.onclick=null;
modalCancel.onclick=null;
modalClose.onclick=null;
}
modalOk.onclick=async()=>{
await onOk?.();
close();
};
modalCancel.onclick=close;
modalClose.onclick=close;
}
/* ============================================================
KEY/FILTER CHIPS
============================================================ */
function createKeyChip(option,active){
const required = option.isRequired==="true";
const name = option.keyName;
const el=chip(name + (required?" *":""), active, required?"req":"");
return {el,required};
}
function createFilterChip(option,active){
const required = option.isRequired==="true";
const name = option.filterName;
const el=chip(name + (required?" *":""), active, required?"req":"");
return {el,required};
}
/* ============================================================
LOGIN (regLogin)
============================================================ */
async function doLogin(){
$("loginStatus").textContent="Verbinde...";
const server = $("serverUrl").value.replace(/\/$/,"");
const port = $("serverPort").value;
const baseUrl = `${server}:${port}`;
const payload ={
sessionValidationStruct:{
stationNumber:$("stationNumber").value,
stationPassword:"",
user:"",
password:"",
client:$("client").value,
registrationType:$("regType").value,
systemIdentifier:"Webclient"
}
};
try{
const resp=await fetch(`${baseUrl}/mes/imsapi/rest/actions/regLogin`,{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(payload)
});
if(!resp.ok) throw new Error("HTTP "+resp.status);
const data=await resp.json();
if(data.result.return_value!==0){
$("loginStatus").textContent="Login fehlgeschlagen";
return;
}
globalLogin = data;
$("loginStatus").textContent="Session OK: "+data.result.sessionContext.sessionId;
}catch(e){
$("loginStatus").textContent="Fehler: "+e;
}
}
$("btnLogin").onclick=doLogin;
/* ============================================================
LOAD DATA
============================================================ */
let templates={};
let paramInfo={};
async function loadData(){
templates = await (await fetch("./rest_api_templates.json")).json();
paramInfo = await (await fetch("./api_params.json")).json();
// Metadaten säubern
Object.keys(paramInfo).forEach(fn => {
paramInfo[fn] = uniqueParamsByName(paramInfo[fn]).map(p => {
const q = { ...p };
if (Array.isArray(q.options)) q.options = uniqueOptionsByKeyName(q.options);
if (Array.isArray(q.filters)) q.filters = uniqueFiltersByName(q.filters);
return q;
});
});
buildTree();
}
loadData();
/* ============================================================
BUILD API TREE
============================================================ */
function buildTree(){
const tree = $("apiTree");
tree.innerHTML="";
const groups={};
Object.keys(templates).forEach(fn=>{
const prefix=extractPrefix(fn);
if(!groups[prefix]) groups[prefix]=[];
groups[prefix].push(fn);
});
Object.keys(groups)
.sort()
.forEach(prefix=>{
const wrap=document.createElement("div");
wrap.className="api-group";
wrap.dataset.prefix=prefix.toLowerCase();
const g=document.createElement("div");
g.className="item group";
g.textContent=prefix;
g.dataset.prefix=prefix.toLowerCase();
const sub=document.createElement("div");
sub.className="api-sub";
g.onclick=()=>{
const willOpen=sub.style.display==="none"||!sub.style.display;
sub.style.display=willOpen?"block":"none";
sub.dataset.userOpened=willOpen?"1":"";
};
groups[prefix].sort().forEach(fn=>{
const e=document.createElement("div");
e.className="item api-fn";
e.textContent=fn;
e.style.marginLeft="18px";
e.dataset.fn=fn.toLowerCase();
e.dataset.prefix=prefix.toLowerCase();
e.onclick=(ev)=>{
ev.stopPropagation();
loadForm(fn);
};
sub.appendChild(e);
});
wrap.appendChild(g);
wrap.appendChild(sub);
tree.appendChild(wrap);
});
const noResults=document.createElement("div");
noResults.id="apiNoResults";
noResults.className="api-no-results";
noResults.textContent="Keine Funktion gefunden.";
tree.appendChild(noResults);
filterApiTree();
}
function initApiFilter(){
const search=$("apiSearch");
if(!search) return;
search.oninput=filterApiTree;
}
function filterApiTree(){
const search=$("apiSearch");
const term=(search?.value||"").trim().toLowerCase();
const groups=document.querySelectorAll("#apiTree .api-group");
let visibleGroups=0;
groups.forEach(wrap=>{
const group=wrap.querySelector(".item.group");
const sub=wrap.querySelector(".api-sub");
const prefix=wrap.dataset.prefix||"";
let visibleFns=0;
sub.querySelectorAll(".api-fn").forEach(item=>{
const fn=item.dataset.fn||"";
const match=!term || fn.includes(term) || prefix.includes(term);
item.style.display=match?"block":"none";
if(match) visibleFns++;
});
const groupMatches=!term || prefix.includes(term) || visibleFns>0;
wrap.style.display=groupMatches?"":"none";
if(groupMatches) visibleGroups++;
if(term){
sub.style.display=visibleFns>0?"block":"none";
}else if(!sub.dataset.userOpened){
sub.style.display="none";
}
});
const noResults=$("apiNoResults");
if(noResults) noResults.style.display=visibleGroups?"none":"block";
}
initApiFilter();
/* ============================================================
FORM BUILDER (Size-Felder werden ausgelassen)
============================================================ */
function loadForm(fn){
state.currentFn=fn;
const fnState=ensureFnState(fn);
const form=$("form");
form.innerHTML="";
const tpl=templates[fn].bodyTemplate;
const params=paramInfo[fn]||[];
const title=document.createElement("h2");
title.textContent=fn;
form.appendChild(title);
// PARAM LIST (skip *.Size)
const ordered = Object.keys(tpl).filter(p=>!p.toLowerCase().endsWith("size"));
ordered.forEach(paramName=>{
const info=params.find(p=>p.name===paramName);
const isArray=Array.isArray(tpl[paramName]);
// **Auto-Preselect Required Keys/Filters in State**, damit Values sofort möglich sind
if (info?.options?.length) {
autoPreselectRequiredKeys(fn, paramName, info);
form.appendChild(renderKeysParam(fn, paramName, info, tpl));
return;
}
// 2) Filters-Param
if (info?.filters?.length) {
autoPreselectRequiredFilters(fn, paramName, info);
form.appendChild(renderFiltersParam(fn, paramName, info));
return;
}
// 3) ResultValues -> unabhängig, eigenes Modal (KEINE Key-Abhängigkeit)
if (paramName.toLowerCase().endsWith("resultvalues")) {
form.appendChild(renderResultValuesParam(fn, paramName));
return;
}
// 4) UploadValues -> abhängig von UploadKeys (wie bisher)
if (paramName.toLowerCase().endsWith("values")) {
form.appendChild(renderValuesParam(fn, paramName, info, tpl));
return;
}
// 5) Fallbacks
if (isArray) form.appendChild(renderArrayFallback(paramName));
else form.appendChild(renderSingleFallback(paramName, info));
});
const row=document.createElement("div");
row.className="row";
row.style.marginTop="12px";
const gen=document.createElement("button");
gen.className="btn btn-primary";
gen.textContent="JSON erzeugen";
gen.onclick=()=>generatePayload(fn);
row.appendChild(gen);
const run=document.createElement("button");
run.className="btn";
run.textContent="API ausführen";
run.onclick=()=>executeCall(fn);
row.appendChild(run);
form.appendChild(row);
// DOKU
const doc=document.createElement("div");
doc.className="card";
doc.style.padding="12px";
doc.style.marginTop="12px";
doc.innerHTML="<h3 class='section' style='margin-top:0'>Dokumentation</h3>";
params.forEach(p=>{
const line=document.createElement("div");
const extra=[];
if(p.options?.length) extra.push(`options:${p.options.length}`);
if(p.filters?.length) extra.push(`filters:${p.filters.length}`);
line.textContent=`${p.name}${p.type}${extra.length?" ("+extra.join(", ")+")":""}`;
doc.appendChild(line);
});
form.appendChild(doc);
}
function renderResultValuesParam(fn, paramName) {
const block = document.createElement("div");
block.className = "param";
const label = document.createElement("label");
label.innerHTML = `<b>${paramName}</b> (ResultValues)`;
block.appendChild(label);
const btn = document.createElement("button");
btn.className = "btn btn-small";
btn.textContent = "ResultValues eingeben";
btn.onclick = () => {
openResultValuesModal(fn, paramName, btn);
};
block.appendChild(btn);
return block;
}
function openResultValuesModal(fn, paramName, btn) {
const fnState = ensureFnState(fn);
const content = document.createElement("div");
// KEINE Key-Abhängigkeit frei eingeben/strukturieren:
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
const tbody = document.createElement("tbody");
// Bestehende Werte laden (falls vorhanden)
const current = fnState.values[paramName] || [];
// Helfer zum Hinzufügen einer Zeile
const addRow = (value = "") => {
const tr = document.createElement("tr");
tr.style.borderBottom = "1px solid #273049";
const tdIdx = document.createElement("td");
tdIdx.style.padding = "8px";
tdIdx.style.width = "48px";
tdIdx.style.textAlign = "right";
const tdVal = document.createElement("td");
tdVal.style.padding = "8px";
const tdActions = document.createElement("td");
tdActions.style.padding = "8px";
tdActions.style.width = "80px";
tdActions.style.textAlign = "right";
const inp = document.createElement("input");
inp.className = "param-input";
inp.value = value;
const del = document.createElement("button");
del.className = "btn btn-small";
del.style.background = "#101624";
del.style.borderColor = "#273049";
del.textContent = "";
del.title = "Zeile löschen";
del.onclick = () => {
tbody.removeChild(tr);
reindexRows();
};
tdVal.appendChild(inp);
tdActions.appendChild(del);
tr.appendChild(tdIdx);
tr.appendChild(tdVal);
tr.appendChild(tdActions);
tbody.appendChild(tr);
reindexRows();
};
const reindexRows = () => {
Array.from(tbody.children).forEach((tr, i) => {
tr.firstChild.textContent = String(i + 1);
});
};
// Bestehende Werte in Zeilen darstellen
if (current.length) {
current.forEach(v => addRow(v));
} else {
addRow(""); // mindestens eine Zeile
}
table.appendChild(tbody);
content.appendChild(table);
// Toolbar: + hinzufügen
const toolbar = document.createElement("div");
toolbar.className = "row";
toolbar.style.marginTop = "10px";
const addBtn = document.createElement("button");
addBtn.className = "btn btn-small";
addBtn.textContent = "+ hinzufügen";
addBtn.onclick = () => addRow("");
toolbar.appendChild(addBtn);
content.appendChild(toolbar);
openModal({
title: `${fn}.${paramName} ResultValues`,
contentNode: content,
onOk: () => {
const vals = [];
tbody.querySelectorAll("input").forEach(i => vals.push(i.value));
fnState.values[paramName] = vals;
btn.textContent = `ResultValues (${vals.length})`;
}
});
}
/* === Auto-Preselect Required === */
function autoPreselectRequiredKeys(fn, paramName, info){
const fnState=ensureFnState(fn);
const current = new Set(fnState.keys[paramName]||[]);
const required = (info.options||[]).filter(o=>o.isRequired==="true").map(o=>o.keyName);
if (required.length){
required.forEach(k=>current.add(k));
fnState.keys[paramName] = [...current];
}
}
function normalizeFilters(filters){
return (filters||[]).map(f=>{
if(typeof f === "string") return {filterName:f, isRequired:"false"};
return {filterName: f.filterName||f.name||String(f), isRequired: f.isRequired||"false"};
});
}
function autoPreselectRequiredFilters(fn, paramName, info){
const fnState=ensureFnState(fn);
const current = new Set(fnState.filters[paramName]||[]);
// filters can be plain strings (no isRequired) or objects
const normalized = normalizeFilters(info.filters||[]);
const required = normalized.filter(o=>o.isRequired==="true").map(o=>o.filterName);
if (required.length){
required.forEach(k=>current.add(k));
fnState.filters[paramName] = [...current];
}
}
/* ============================================================
RENDER KEYS
============================================================ */
function renderKeysParam(fn,paramName,info,tpl){
const fnState=ensureFnState(fn);
const selected=new Set(fnState.keys[paramName]||[]);
const block=document.createElement("div");
block.className="param";
const label=document.createElement("label");
label.innerHTML=`<b>${paramName}</b> (Keys)`;
block.appendChild(label);
const btn=document.createElement("button");
btn.className="btn btn-small";
btn.textContent=selected.size
? `Keys (${selected.size})`
: `Keys auswählen`;
btn.onclick=()=>{
openKeysModal(fn,paramName,info.options,selected,(finalSel)=>{
fnState.keys[paramName]=finalSel;
btn.textContent=finalSel.length
? `Keys (${finalSel.length})`
: `Keys auswählen`;
});
};
block.appendChild(btn);
return block;
}
function openKeysModal(fn,paramName,options,selectedSet,onApply){
const content=document.createElement("div");
const grid=document.createElement("div");
grid.className="grid";
// Arbeitsmenge: vorbesetzen mit bisherigen Selektionen
const working=new Set([...selectedSet]);
// **Automatisch Required-Keys hinzufügen**, falls noch nicht drin:
options.forEach(opt=>{
if (opt.isRequired==="true") working.add(opt.keyName);
});
options.forEach(opt=>{
const active = working.has(opt.keyName);
const {el}=createKeyChip(opt, active);
el.onclick=()=>{
// Required bleibt immer gesetzt
if (opt.isRequired==="true") return;
toggleSet(working,opt.keyName);
el.classList.toggle("active");
};
grid.appendChild(el);
});
content.appendChild(grid);
openModal({
title:`${fn}.${paramName} Keys`,
contentNode:content,
onOk:()=>onApply([...working])
});
}
/* ============================================================
RENDER FILTERS
============================================================ */
function renderFiltersParam(fn,paramName,info){
const fnState=ensureFnState(fn);
const selected=new Set(fnState.filters[paramName]||[]);
const block=document.createElement("div");
block.className="param";
const label=document.createElement("label");
label.innerHTML=`<b>${paramName}</b> (Filters)`;
block.appendChild(label);
const row=document.createElement("div");
row.style.display="flex";
row.style.gap="8px";
row.style.alignItems="center";
row.style.flexWrap="wrap";
const filterBtn=document.createElement("button");
filterBtn.className="btn btn-small";
filterBtn.textContent=selected.size
? `Filters (${selected.size})`
: `Filters auswählen`;
// Values-Button (zeigt wieviele befüllte Values existieren)
const valBtn=document.createElement("button");
valBtn.className="btn btn-small";
const existingVals=fnState.filterValues[paramName]||{};
const filledCount=Object.values(existingVals).filter(v=>v&&v.trim()).length;
valBtn.textContent=filledCount?`Filter-Values (${filledCount})`:`Filter-Values eingeben`;
valBtn.style.borderColor=filledCount?"#34c759":"";
valBtn.style.color=filledCount?"#34c759":"";
filterBtn.onclick=()=>{
openFiltersModal(fn,paramName,info.filters,selected,(finalSel)=>{
finalSel.forEach(k=>selected.add(k));
// Remove deselected from selected set
[...selected].forEach(k=>{ if(!finalSel.includes(k)) selected.delete(k); });
fnState.filters[paramName]=finalSel;
filterBtn.textContent=finalSel.length?`Filters (${finalSel.length})`:`Filters auswählen`;
// Update valBtn
const vals=fnState.filterValues[paramName]||{};
const fc=Object.values(vals).filter(v=>v&&v.trim()).length;
valBtn.textContent=fc?`Filter-Values (${fc})`:`Filter-Values eingeben`;
});
};
valBtn.onclick=()=>{
openFilterKeyValuesModal(fn,paramName,valBtn);
};
row.appendChild(filterBtn);
row.appendChild(valBtn);
block.appendChild(row);
return block;
}
function openFiltersModal(fn,paramName,filters,selectedSet,onApply){
const normalized = normalizeFilters(filters);
const content=document.createElement("div");
const grid=document.createElement("div");
grid.className="grid";
const working=new Set([...selectedSet]);
// Automatisch Required-Filter hinzufügen
normalized.forEach(f=>{
if (f.isRequired==="true") working.add(f.filterName);
});
normalized.forEach(f=>{
const active = working.has(f.filterName);
const {el}=createFilterChip(f, active);
el.onclick=()=>{
if (f.isRequired==="true") return;
toggleSet(working,f.filterName);
el.classList.toggle("active");
};
grid.appendChild(el);
});
content.appendChild(grid);
openModal({
title:`${fn}.${paramName} Filters`,
contentNode:content,
onOk:()=>onApply([...working])
});
}
/* ============================================================
FILTER KEY-VALUE MODAL
Zeigt alle gewählten Filter als Zeile mit Textfeld für den Value.
Im JSON werden nur Einträge ausgegeben, die einen nicht-leeren Value haben.
============================================================ */
function openFilterKeyValuesModal(fn, paramName, btn){
const fnState=ensureFnState(fn);
const chosenFilters=fnState.filters[paramName]||[];
const current=fnState.filterValues[paramName]||{};
const content=document.createElement("div");
if(!chosenFilters.length){
const warn=document.createElement("div");
warn.style.color="var(--warn)";
warn.style.padding="8px 0";
warn.textContent="Keine Filter ausgewählt. Bitte zuerst Filter auswählen.";
content.appendChild(warn);
openModal({title:`${fn}.${paramName} Filter Values`,contentNode:content,onOk:()=>{}});
return;
}
const hint=document.createElement("div");
hint.style.color="var(--muted)";
hint.style.fontSize="12px";
hint.style.marginBottom="10px";
hint.textContent="Nur Felder mit eingetragenem Wert werden im JSON ausgegeben.";
content.appendChild(hint);
const table=document.createElement("table");
table.style.width="100%";
table.style.borderCollapse="collapse";
const thead=document.createElement("thead");
thead.innerHTML=`<tr>
<th style="text-align:left;padding:6px 8px;color:var(--muted);font-size:12px;border-bottom:1px solid #273049;">#</th>
<th style="text-align:left;padding:6px 8px;color:var(--muted);font-size:12px;border-bottom:1px solid #273049;">Filter Key</th>
<th style="text-align:left;padding:6px 8px;color:var(--muted);font-size:12px;border-bottom:1px solid #273049;">Value</th>
</tr>`;
table.appendChild(thead);
const tbody=document.createElement("tbody");
const inputMap={};
chosenFilters.forEach((filterKey,i)=>{
const tr=document.createElement("tr");
tr.style.borderBottom="1px solid #1e2436";
const c1=document.createElement("td");
c1.style.padding="8px";
c1.style.color="var(--muted)";
c1.style.fontSize="13px";
c1.style.width="36px";
c1.textContent=i+1;
const c2=document.createElement("td");
c2.style.padding="8px";
c2.style.fontWeight="600";
c2.style.fontSize="13px";
c2.textContent=filterKey;
const c3=document.createElement("td");
c3.style.padding="8px";
const inp=document.createElement("input");
inp.className="param-input";
inp.placeholder=`Wert für ${filterKey}`;
inp.value=current[filterKey]||"";
inputMap[filterKey]=inp;
c3.appendChild(inp);
tr.appendChild(c1);
tr.appendChild(c2);
tr.appendChild(c3);
tbody.appendChild(tr);
});
table.appendChild(tbody);
content.appendChild(table);
openModal({
title:`${fn}.${paramName} Filter Values`,
contentNode:content,
onOk:()=>{
const vals={};
Object.entries(inputMap).forEach(([k,inp])=>{
vals[k]=inp.value; // leere Werte bleiben, werden beim Payload-Build gefiltert
});
fnState.filterValues[paramName]=vals;
const filledCount=Object.values(vals).filter(v=>v&&v.trim()).length;
btn.textContent=filledCount?`Filter-Values (${filledCount})`:`Filter-Values eingeben`;
btn.style.borderColor=filledCount?"#34c759":"";
btn.style.color=filledCount?"#34c759":"";
}
});
}
/* ============================================================
RENDER VALUES
============================================================ */
function renderValuesParam(fn,paramName,info,tpl){
const block=document.createElement("div");
block.className="param";
const label=document.createElement("label");
label.innerHTML=`<b>${paramName}</b> (Values)`;
block.appendChild(label);
const btn=document.createElement("button");
btn.className="btn btn-small";
btn.textContent="Values eingeben";
btn.onclick=()=>{
openValuesModal(fn,paramName,btn);
};
block.appendChild(btn);
return block;
}
function normalizeValueRows(current, keyCount){
if(Array.isArray(current) && current.length && Array.isArray(current[0])){
return current.map(row=>{
const out=[];
for(let i=0;i<keyCount;i++) out.push(row[i] ?? "");
return out;
});
}
if(Array.isArray(current) && current.length){
const rows=[];
for(let i=0;i<current.length;i+=keyCount){
const row=current.slice(i,i+keyCount);
while(row.length<keyCount) row.push("");
rows.push(row);
}
return rows;
}
return [Array.from({length:keyCount},()=>"")];
}
function openValuesModal(fn,paramName,btn){
const fnState=ensureFnState(fn);
const keysParamName = paramName.replace(/Values$/i,"Keys");
const keys = fnState.keys[keysParamName]||[];
const content=document.createElement("div");
if(!keys.length){
const warn=document.createElement("div");
warn.style.color="var(--warn)";
warn.textContent="Keine Keys gewählt. (Required-Keys werden automatisch selektiert, bitte Keys prüfen.)";
content.appendChild(warn);
openModal({
title:`${fn}.${paramName} Values`,
contentNode:content,
onOk:()=>{}
});
return;
}
const hint=document.createElement("div");
hint.style.color="var(--muted)";
hint.style.fontSize="12px";
hint.style.marginBottom="10px";
hint.textContent="Jede Zeile ist ein Datensatz. Die Ausgabe wird zeilenweise in der Reihenfolge der gewählten Keys als flaches Values-Array erzeugt.";
content.appendChild(hint);
const table=document.createElement("table");
table.style.width="100%";
table.style.borderCollapse="collapse";
const thead=document.createElement("thead");
const headRow=document.createElement("tr");
const idxHead=document.createElement("th");
idxHead.textContent="#";
idxHead.style.textAlign="left";
idxHead.style.padding="6px 8px";
idxHead.style.color="var(--muted)";
idxHead.style.fontSize="12px";
idxHead.style.borderBottom="1px solid #273049";
idxHead.style.width="48px";
headRow.appendChild(idxHead);
keys.forEach(k=>{
const th=document.createElement("th");
th.textContent=k;
th.style.textAlign="left";
th.style.padding="6px 8px";
th.style.color="var(--muted)";
th.style.fontSize="12px";
th.style.borderBottom="1px solid #273049";
headRow.appendChild(th);
});
const actHead=document.createElement("th");
actHead.textContent="";
actHead.style.borderBottom="1px solid #273049";
actHead.style.width="70px";
headRow.appendChild(actHead);
thead.appendChild(headRow);
table.appendChild(thead);
const tbody=document.createElement("tbody");
const reindexRows=()=>{
Array.from(tbody.children).forEach((tr,i)=>{
const idx=tr.querySelector("td");
if(idx) idx.textContent=String(i+1);
});
};
const addRow=(rowValues=[])=>{
const tr=document.createElement("tr");
tr.style.borderBottom="1px solid #273049";
const cIdx=document.createElement("td");
cIdx.style.padding="8px";
cIdx.style.color="var(--muted)";
cIdx.style.fontSize="13px";
tr.appendChild(cIdx);
keys.forEach((k,idx)=>{
const td=document.createElement("td");
td.style.padding="8px";
const inp=document.createElement("input");
inp.className="param-input";
inp.value=rowValues[idx]||"";
inp.placeholder=k;
inp.dataset.keyName=k;
td.appendChild(inp);
tr.appendChild(td);
});
const cActions=document.createElement("td");
cActions.style.padding="8px";
cActions.style.textAlign="right";
const del=document.createElement("button");
del.className="btn btn-small";
del.style.background="#101624";
del.style.borderColor="#273049";
del.textContent="";
del.title="Zeile löschen";
del.onclick=()=>{
if(tbody.children.length>1){
tbody.removeChild(tr);
reindexRows();
}else{
tr.querySelectorAll("input").forEach(i=>i.value="");
}
};
cActions.appendChild(del);
tr.appendChild(cActions);
tbody.appendChild(tr);
reindexRows();
};
const rows=normalizeValueRows(fnState.values[paramName]||[], keys.length);
rows.forEach(row=>addRow(row));
table.appendChild(tbody);
content.appendChild(table);
const toolbar=document.createElement("div");
toolbar.className="row";
toolbar.style.marginTop="10px";
const addBtn=document.createElement("button");
addBtn.className="btn btn-small";
addBtn.textContent="+ Array hinzufügen";
addBtn.title="Weiteren Datensatz hinzufügen";
addBtn.onclick=()=>addRow(Array.from({length:keys.length},()=>""));
toolbar.appendChild(addBtn);
content.appendChild(toolbar);
openModal({
title:`${fn}.${paramName} Values`,
contentNode:content,
onOk:()=>{
const rows=[];
tbody.querySelectorAll("tr").forEach(tr=>{
const row=[];
tr.querySelectorAll("input").forEach(i=>row.push(i.value));
if(row.some(v=>String(v).trim()!=="")) rows.push(row);
});
fnState.values[paramName]=rows;
const flatCount=rows.length*keys.length;
btn.textContent=rows.length
? `Values (${rows.length} Zeile${rows.length===1?"":"n"} / ${flatCount})`
: "Values eingeben";
}
});
}
/* ============================================================
FALLBACK FORM FIELDS
============================================================ */
function renderArrayFallback(paramName){
const block=document.createElement("div");
block.className="param";
const label=document.createElement("label");
label.innerHTML=`<b>${paramName}</b> (Array)`;
block.appendChild(label);
const list=document.createElement("div");
block.appendChild(list);
const add=document.createElement("button");
add.className="btn btn-small";
add.textContent="+ hinzufügen";
add.onclick=()=>{
const inp=document.createElement("input");
inp.className="param-input";
inp.dataset.paramName=paramName+"[]";
list.appendChild(inp);
};
block.appendChild(add);
return block;
}
function renderSingleFallback(paramName,info){
const block=document.createElement("div");
block.className="param";
const label=document.createElement("label");
label.innerHTML=`<b>${paramName}</b>`;
block.appendChild(label);
const inp=document.createElement("input");
inp.className="param-input";
inp.dataset.paramName=paramName;
block.appendChild(inp);
return block;
}
/* ============================================================
PAYLOAD BUILDER (sessionContext + ohne Size)
============================================================ */
function generatePayload(fn, render=true){
const payload={};
const fnState=ensureFnState(fn);
// Single / Array inputs
document.querySelectorAll("#form [data-param-name]").forEach(inp=>{
const name=inp.dataset.paramName;
if(name.endsWith("[]")){
const base=name.slice(0,-2);
if(!payload[base]) payload[base]=[];
if(inp.value.trim()) payload[base].push(inp.value.trim());
} else {
if(inp.value.trim()) payload[name]=inp.value.trim();
}
});
// Keys
Object.entries(fnState.keys).forEach(([key,arr])=>{
payload[key]=[...arr];
});
// Filters: als [{key, value}] Array nur Einträge mit befülltem Value
Object.entries(fnState.filters).forEach(([paramName,arr])=>{
const filterVals=fnState.filterValues[paramName]||{};
const hasAnyValue=arr.some(k=>filterVals[k]&&filterVals[k].trim());
if(hasAnyValue){
payload[paramName]=arr
.filter(k=>filterVals[k]&&filterVals[k].trim())
.map(k=>({key:k,value:filterVals[k].trim()}));
} else {
// Keine Values nur die Keys als Array ausgeben (wie bisher)
payload[paramName]=[...arr];
}
});
// Values: UploadValues können mehrere Zeilen besitzen.
// Für die REST-API werden sie als flaches Array in Key-Reihenfolge ausgegeben:
// [row1_key1, row1_key2, ..., row2_key1, row2_key2, ...]
Object.entries(fnState.values).forEach(([key,arr])=>{
const keysParamName=key.replace(/Values$/i,"Keys");
const keys=fnState.keys[keysParamName]||[];
if(Array.isArray(arr) && arr.length && Array.isArray(arr[0]) && keys.length){
payload[key]=arr.flatMap(row=>keys.map((_,idx)=>row[idx] ?? ""));
}else if(Array.isArray(arr)){
payload[key]=[...arr];
}else{
payload[key]=[];
}
});
// Size-Parameter entfernen (Sicherheit)
Object.keys(payload).forEach(k=>{
if (k.toLowerCase().endsWith("size")) delete payload[k];
});
// Login-Defaults
if(globalLogin){
payload.sessionContext={
sessionId:globalLogin.result.sessionContext.sessionId,
persId:globalLogin.result.sessionContext.persId,
locale:"en_EN"
};
if(apiTemplateHasParam(fn,"stationNumber") && isEmptyRuntimeValue(payload.stationNumber)){
payload.stationNumber=$("stationNumber")?.value || "";
}
}
const orderedPayload=orderPayloadFields(payload);
if(render) $("result").textContent=JSON.stringify(orderedPayload,null,2);
return orderedPayload;
}
/* ============================================================
API EXECUTION
============================================================ */
function getSessionContextPayload(){
if(!globalLogin) return null;
return {
sessionId:globalLogin.result.sessionContext.sessionId,
persId:globalLogin.result.sessionContext.persId,
locale:"en_EN"
};
}
function getLoginRuntimeVars(){
return {
stationNumber: $("stationNumber")?.value || "",
client: $("client")?.value || "",
regType: $("regType")?.value || "",
serverUrl: $("serverUrl")?.value || "",
serverPort: $("serverPort")?.value || "",
sessionContext: getSessionContextPayload()
};
}
function isEmptyRuntimeValue(value){
return value === undefined || value === null || value === "" ||
(Array.isArray(value) && value.length === 0);
}
function apiTemplateHasParam(fn, paramName){
return !!(templates?.[fn]?.bodyTemplate && Object.prototype.hasOwnProperty.call(templates[fn].bodyTemplate, paramName));
}
function cloneJson(value){
return value == null ? value : JSON.parse(JSON.stringify(value));
}
function orderPayloadFields(payload){
const src=payload || {};
const out={};
// Für Endanwender lesbarer: Laufzeit-/Login-Kontext steht immer oben.
if(Object.prototype.hasOwnProperty.call(src, "sessionContext")){
out.sessionContext=src.sessionContext;
}
if(Object.prototype.hasOwnProperty.call(src, "stationNumber")){
out.stationNumber=src.stationNumber;
}
Object.entries(src).forEach(([k,v])=>{
if(k==="sessionContext" || k==="stationNumber") return;
out[k]=v;
});
return out;
}
function injectRuntimeDefaults(payload, fn){
const out=cloneJson(payload || {});
const login=getLoginRuntimeVars();
const ctx=login.sessionContext;
if(ctx && isEmptyRuntimeValue(out.sessionContext)){
out.sessionContext=ctx;
}
if(apiTemplateHasParam(fn,"stationNumber") && isEmptyRuntimeValue(out.stationNumber)){
out.stationNumber=login.stationNumber;
}
return orderPayloadFields(out);
}
function getBaseUrl(){
const server=$("serverUrl").value.replace(/\/$/,"");
const port=$("serverPort").value;
return `${server}:${port}`;
}
async function callApi(fn, payload){
const finalPayload=injectRuntimeDefaults(payload, fn);
const resp=await fetch(`${getBaseUrl()}/mes/imsapi/rest/actions/${fn}`,{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(finalPayload)
});
if(!resp.ok) throw new Error("HTTP "+resp.status);
return await resp.json();
}
async function executeCall(fn){
const payload=generatePayload(fn);
if(!globalLogin){
alert("Bitte zuerst Login durchführen.");
return;
}
try{
console.log("Payload "+fn, payload);
const data=await callApi(fn,payload);
console.log(data);
$("result").textContent="Ergebnis:\n\n"+JSON.stringify(data,null,2);
}
catch(e){
$("result").textContent="Error:\n"+e;
}
}
// === FilterValues (Integrated) ===
function renderFilterValuesParam(fn, paramName, info){
const block=document.createElement('div');
block.className='param';
const label=document.createElement('label');
label.innerHTML=`<b>${paramName}</b> (FilterValues)`;
block.appendChild(label);
const btn=document.createElement('button');
btn.className='btn btn-small';
btn.textContent='FilterValues eingeben';
btn.onclick=()=>openFilterValuesModal(fn,paramName,btn);
block.appendChild(btn);
return block;
}
function openFilterValuesModal(fn,paramName,btn){
const fnState=ensureFnState(fn);
const filtersParamName = paramName.replace(/Values$/i,"");
const filters = fnState.filters[filtersParamName] || [];
const current = fnState.values[paramName] || [];
const content=document.createElement('div');
const table=document.createElement('table');
table.style.width='100%';
const tbody=document.createElement('tbody');
if(!filters.length){
const warn=document.createElement('div');
warn.style.color='var(--warn)';
warn.textContent='Keine Filter gewählt.';
content.appendChild(warn);
openModal({title:`${fn}.${paramName}`,contentNode:content,onOk:()=>{}});
return;
}
filters.forEach((f,i)=>{
const tr=document.createElement('tr');
const c1=document.createElement('td'); c1.textContent=i+1;
const c2=document.createElement('td'); c2.textContent=f;
const c3=document.createElement('td');
const inp=document.createElement('input'); inp.className='param-input'; inp.placeholder=f; inp.value=current[i]||'';
c3.appendChild(inp);
tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3);
tbody.appendChild(tr);
});
table.appendChild(tbody);
content.appendChild(table);
openModal({
title:`${fn}.${paramName} FilterValues`,
contentNode:content,
onOk:()=>{
const vals=[];
tbody.querySelectorAll('input').forEach(i=>vals.push(i.value));
fnState.values[paramName]=vals;
btn.textContent=`FilterValues (${vals.length})`;
}
});
}
/* ============================================================
SCRIPTEDITOR
JSON-DSL zum seriellen Kombinieren mehrerer API Calls.
Unterstützte Platzhalter:
- {{vars.name}}
- {{login.stationNumber}}, {{login.client}}, {{login.sessionContext}}
- {{steps.stepId.response.result.return_value}}
- {{row:stepId:resultValues:0:KEY}}
- {{row:stepId:resultKeys:resultValues:0:KEY}} // explizite Keys
- {{col:stepId:resultValues:KEY}}
- {{flat:stepId:resultValues}}
- {"$concat":[ ... ]}
- {"$rows":{"from":"stepId","values":"resultValues","columns":["A","B"]}}
Auto-Export nach jedem Step:
- ResultKeys + ResultValues werden automatisch in ctx.vars geschrieben.
- Beispiel: WORKORDER_NUMBER -> vars.WORKORDER_NUMBER und vars.workorderNumber
- Zusätzlich: vars.stepId.WORKORDER_NUMBER und vars.stepId.rows.<valuesParam>
============================================================ */
function getScriptEditor(){ return $("scriptEditor"); }
function getScriptLog(){ return $("scriptResult"); }
function appendScriptLog(text){
const log=getScriptLog();
if(!log) return;
const old=(log.textContent||"").trim();
log.textContent=(old && old!=="Noch kein Script ausgeführt." ? old+"\n" : "")+text;
log.scrollTop=log.scrollHeight;
}
function readScriptJson(){
const editor=getScriptEditor();
const raw=(editor?.value||"").trim();
if(!raw) return {steps:[]};
const parsed=JSON.parse(raw);
if(Array.isArray(parsed)) return {steps:parsed};
if(!Array.isArray(parsed.steps)) parsed.steps=[];
return parsed;
}
function writeScriptJson(obj){
const editor=getScriptEditor();
if(editor) editor.value=JSON.stringify(obj,null,2);
}
function makeStepId(fn){
const base=String(fn||"step").replace(/[^a-zA-Z0-9_]/g,"_");
const script=readScriptJson();
const existing=new Set((script.steps||[]).map(s=>s.id));
let id=base;
let i=2;
while(existing.has(id)) id=`${base}_${i++}`;
return id;
}
function addCurrentApiAsScriptStep(){
if(!state.currentFn){
alert("Bitte zuerst eine API auswählen.");
return;
}
const script=readScriptJson();
const payload=injectRuntimeDefaults(generatePayload(state.currentFn,false), state.currentFn);
script.steps=script.steps||[];
script.steps.push({
id:makeStepId(state.currentFn),
api:state.currentFn,
payload
});
writeScriptJson(script);
appendScriptLog(`Step hinzugefügt: ${state.currentFn}`);
}
const SCRIPT_EXAMPLE_TEXTS={
de:{
language:"Deutsch",
title:"Beispiel: Stationseinstellungen lesen und Werte weiterverwenden",
description:"Dieses Beispiel liest WORKORDER_NUMBER und WORKSTEP_NUMBER. Diese ResultValues werden nach dem ersten Step automatisch als Variablen bereitgestellt.",
stationStep:"Liest die Stationseinstellungen. sessionContext und stationNumber werden aus dem Login übernommen.",
nextStep:"Beispiel für den nächsten API-Aufruf: workOrderNumber und workStepNumber kommen automatisch aus Step 1.",
disabledHint:"Dieser Step ist deaktiviert, damit das Beispiel nicht versehentlich eine weitere API aufruft. Setze enabled auf true und ändere api/payload passend.",
usage:{
workorderNumber:"{{vars.workorderNumber}} → Wert aus WORKORDER_NUMBER, z. B. Demo1",
workstepNumber:"{{vars.workstepNumber}} → Wert aus WORKSTEP_NUMBER, z. B. 6",
stepScoped:"{{vars.getStationSetting.workorderNumber}} → konfliktfreie Variante mit Step-ID",
explicitRow:"{{row:getStationSetting:stationSettingResultValues:0:WORKORDER_NUMBER}} → expliziter Zugriff über Keys/Values"
},
logInserted:"Deutsches Beispiel eingefügt. Der zweite Step ist enabled:false und wird nicht ausgeführt, bis du ihn aktivierst."
},
en:{
language:"English",
title:"Example: read station settings and reuse values",
description:"This example reads WORKORDER_NUMBER and WORKSTEP_NUMBER. After the first step, these ResultValues are exported automatically as variables.",
stationStep:"Reads the station settings. sessionContext and stationNumber are taken from the login.",
nextStep:"Example for the next API call: workOrderNumber and workStepNumber come automatically from step 1.",
disabledHint:"This step is disabled so the example does not accidentally call another API. Set enabled to true and adapt api/payload as needed.",
usage:{
workorderNumber:"{{vars.workorderNumber}} → value from WORKORDER_NUMBER, e.g. Demo1",
workstepNumber:"{{vars.workstepNumber}} → value from WORKSTEP_NUMBER, e.g. 6",
stepScoped:"{{vars.getStationSetting.workorderNumber}} → conflict-safe variant using the step ID",
explicitRow:"{{row:getStationSetting:stationSettingResultValues:0:WORKORDER_NUMBER}} → explicit access through keys/values"
},
logInserted:"English example inserted. The second step is enabled:false and will not run until you enable it."
},
fr:{
language:"Français",
title:"Exemple : lire les paramètres de station et réutiliser les valeurs",
description:"Cet exemple lit WORKORDER_NUMBER et WORKSTEP_NUMBER. Après la première étape, ces ResultValues sont exportées automatiquement comme variables.",
stationStep:"Lit les paramètres de la station. sessionContext et stationNumber sont repris depuis le login.",
nextStep:"Exemple pour l'appel API suivant : workOrderNumber et workStepNumber proviennent automatiquement de l'étape 1.",
disabledHint:"Cette étape est désactivée afin que l'exemple n'appelle pas une autre API par erreur. Mets enabled à true et adapte api/payload selon le besoin.",
usage:{
workorderNumber:"{{vars.workorderNumber}} → valeur de WORKORDER_NUMBER, par ex. Demo1",
workstepNumber:"{{vars.workstepNumber}} → valeur de WORKSTEP_NUMBER, par ex. 6",
stepScoped:"{{vars.getStationSetting.workorderNumber}} → variante sûre contre les conflits avec l'ID de l'étape",
explicitRow:"{{row:getStationSetting:stationSettingResultValues:0:WORKORDER_NUMBER}} → accès explicite via keys/values"
},
logInserted:"Exemple français inséré. La deuxième étape est enabled:false et ne sera pas exécutée tant que tu ne l'actives pas."
},
zh:{
language:"中文",
title:"示例:读取工位设置并复用结果值",
description:"此示例读取 WORKORDER_NUMBER 和 WORKSTEP_NUMBER。第一个步骤执行后这些 ResultValues 会自动导出为变量。",
stationStep:"读取工位设置。sessionContext 和 stationNumber 会从登录信息中获取。",
nextStep:"下一个 API 调用示例workOrderNumber 和 workStepNumber 自动来自步骤 1。",
disabledHint:"此步骤已禁用,避免示例意外调用其他 API。需要执行时将 enabled 改为 true并按需调整 api/payload。",
usage:{
workorderNumber:"{{vars.workorderNumber}} → 来自 WORKORDER_NUMBER 的值,例如 Demo1",
workstepNumber:"{{vars.workstepNumber}} → 来自 WORKSTEP_NUMBER 的值,例如 6",
stepScoped:"{{vars.getStationSetting.workorderNumber}} → 使用步骤 ID 的无冲突写法",
explicitRow:"{{row:getStationSetting:stationSettingResultValues:0:WORKORDER_NUMBER}} → 通过 keys/values 显式访问"
},
logInserted:"已插入中文示例。第二个步骤为 enabled:false启用之前不会执行。"
}
};
const SCRIPT_UI_TEXTS={
de:{
panelTitle:"Scripteditor",
languageLabel:"Sprache:",
languageTitle:"Sprache für Beispieltexte und Hilfetexte",
addCurrentStep:"Aktuelle API als Step hinzufügen",
insertExample:"Beispiel einfügen",
runScript:"Script ausführen",
clearLog:"Log leeren",
resultDefault:"Noch kein Script ausgeführt.",
note:"Beispieltexte und Hilfetexte werden in der gewählten Sprache angezeigt. Die ausführbaren JSON-Felder bleiben unverändert.",
hintHtml:`Mehrere APIs können hier als JSON-Script nacheinander ausgeführt werden. <code>ResultKeys</code>/<code>ResultValues</code> werden automatisch als Variablen exportiert, z. B. <code>{{vars.workorderNumber}}</code> oder <code>{{vars.getStationSetting.WORKORDER_NUMBER}}</code>. Login-Werte sind ebenfalls nutzbar, z. B. <code>{{login.stationNumber}}</code>; <code>sessionContext</code> wird im sichtbaren Payload immer automatisch aus dem Login als erstes Objekt gesetzt. Leere <code>stationNumber</code>-Felder werden ebenfalls aus dem Login ergänzt. Manuelle Platzhalter wie <code>{{row:stepId:resultValues:0:KEY}}</code>, <code>{{col:stepId:resultValues:KEY}}</code> und <code>{"$rows":{...}}</code> funktionieren weiterhin.`
},
en:{
panelTitle:"Script Editor",
languageLabel:"Language:",
languageTitle:"Language for examples and help texts",
addCurrentStep:"Add current API as step",
insertExample:"Insert example",
runScript:"Run script",
clearLog:"Clear log",
resultDefault:"No script has been run yet.",
note:"Example texts and help texts are displayed in the selected language. The executable JSON fields remain unchanged.",
hintHtml:`Multiple APIs can be executed here sequentially as a JSON script. <code>ResultKeys</code>/<code>ResultValues</code> are automatically exported as variables, e.g. <code>{{vars.workorderNumber}}</code> or <code>{{vars.getStationSetting.WORKORDER_NUMBER}}</code>. Login values can also be used, e.g. <code>{{login.stationNumber}}</code>; in the visible payload, <code>sessionContext</code> is always automatically inserted from the login as the first object. Empty <code>stationNumber</code> fields are also filled from the login. Manual placeholders such as <code>{{row:stepId:resultValues:0:KEY}}</code>, <code>{{col:stepId:resultValues:KEY}}</code>, and <code>{"$rows":{...}}</code> continue to work.`
},
fr:{
panelTitle:"Éditeur de script",
languageLabel:"Langue :",
languageTitle:"Langue des exemples et des textes daide",
addCurrentStep:"Ajouter lAPI actuelle comme étape",
insertExample:"Insérer un exemple",
runScript:"Exécuter le script",
clearLog:"Effacer le journal",
resultDefault:"Aucun script na encore été exécuté.",
note:"Les exemples et les textes daide sont affichés dans la langue sélectionnée. Les champs JSON exécutables restent inchangés.",
hintHtml:`Plusieurs API peuvent être exécutées ici successivement sous forme de script JSON. Les <code>ResultKeys</code>/<code>ResultValues</code> sont automatiquement exportés comme variables, par exemple <code>{{vars.workorderNumber}}</code> ou <code>{{vars.getStationSetting.WORKORDER_NUMBER}}</code>. Les valeurs de connexion peuvent également être utilisées, par exemple <code>{{login.stationNumber}}</code>; dans le payload visible, <code>sessionContext</code> est toujours automatiquement inséré depuis la connexion en tant que premier objet. Les champs <code>stationNumber</code> vides sont également complétés à partir de la connexion. Les placeholders manuels comme <code>{{row:stepId:resultValues:0:KEY}}</code>, <code>{{col:stepId:resultValues:KEY}}</code> et <code>{"$rows":{...}}</code> continuent de fonctionner.`
},
zh:{
panelTitle:"脚本编辑器",
languageLabel:"语言:",
languageTitle:"示例和帮助文本的语言",
addCurrentStep:"将当前 API 添加为步骤",
insertExample:"插入示例",
runScript:"执行脚本",
clearLog:"清空日志",
resultDefault:"尚未执行任何脚本。",
note:"示例文本和帮助文本会以所选语言显示。可执行的 JSON 字段保持不变。",
hintHtml:`这里可以按顺序执行多个 API格式为 JSON 脚本。<code>ResultKeys</code>/<code>ResultValues</code> 会自动导出为变量,例如 <code>{{vars.workorderNumber}}</code> 或 <code>{{vars.getStationSetting.WORKORDER_NUMBER}}</code>。也可以使用登录信息中的值,例如 <code>{{login.stationNumber}}</code>;在可见的 payload 中,<code>sessionContext</code> 会始终自动从登录信息中作为第一个对象插入。空的 <code>stationNumber</code> 字段也会自动从登录信息中补充。手动占位符仍然可用,例如 <code>{{row:stepId:resultValues:0:KEY}}</code>、<code>{{col:stepId:resultValues:KEY}}</code> 和 <code>{"$rows":{...}}</code>。`
}
};
function getSelectedScriptExampleLanguage(){
return $("scriptExampleLanguage")?.value || "de";
}
function scriptExample(lang=getSelectedScriptExampleLanguage()){
const t=SCRIPT_EXAMPLE_TEXTS[lang] || SCRIPT_EXAMPLE_TEXTS.de;
return {
_language:t.language,
_title:t.title,
_description:t.description,
_usage:t.usage,
vars:{},
steps:[
{
id:"getStationSetting",
_description:t.stationStep,
api:"trGetStationSetting",
payload:{
sessionContext:getSessionContextPayload() || [],
stationNumber:$("stationNumber")?.value || "J816352",
stationSettingResultKeys:["WORKORDER_NUMBER","WORKSTEP_NUMBER"],
stationSettingResultValues:[]
}
},
{
id:"useStationSettingDemo",
_description:t.nextStep,
_hint:t.disabledHint,
enabled:false,
api:"deineNaechsteApi",
payload:{
sessionContext:getSessionContextPayload() || [],
stationNumber:$("stationNumber")?.value || "J816352",
workOrderNumber:"{{vars.workorderNumber}}",
workStepNumber:"{{vars.workstepNumber}}"
}
}
]
};
}
function insertScriptExample(){
const lang=getSelectedScriptExampleLanguage();
const t=SCRIPT_EXAMPLE_TEXTS[lang] || SCRIPT_EXAMPLE_TEXTS.de;
writeScriptJson(scriptExample(lang));
appendScriptLog(t.logInserted);
}
function updateScriptExampleNote(){
const lang=getSelectedScriptExampleLanguage();
const t=SCRIPT_EXAMPLE_TEXTS[lang] || SCRIPT_EXAMPLE_TEXTS.de;
const ui=SCRIPT_UI_TEXTS[lang] || SCRIPT_UI_TEXTS.de;
const title=$("scriptPanelTitle");
if(title) title.textContent=ui.panelTitle;
const label=$("scriptLanguageLabel");
if(label) label.textContent=ui.languageLabel;
const langSel=$("scriptExampleLanguage");
if(langSel) langSel.title=ui.languageTitle;
const hint=$("scriptHint");
if(hint) hint.innerHTML=ui.hintHtml;
const addBtn=$("btnAddCurrentStep");
if(addBtn) addBtn.textContent=ui.addCurrentStep;
const exampleBtn=$("btnScriptExample");
if(exampleBtn) exampleBtn.textContent=ui.insertExample;
const runBtn=$("btnRunScript");
if(runBtn) runBtn.textContent=ui.runScript;
const clearBtn=$("btnClearScriptLog");
if(clearBtn) clearBtn.textContent=ui.clearLog;
const note=$("scriptExampleNote");
if(note) note.textContent=`${t.language}: ${ui.note} ${t.description}`;
const result=$("scriptResult");
const allDefaultTexts=Object.values(SCRIPT_UI_TEXTS).map(x=>x.resultDefault).concat(["Noch kein Script ausgeführt."]);
if(result && (!result.textContent.trim() || allDefaultTexts.includes(result.textContent.trim()))){
result.textContent=ui.resultDefault;
}
document.documentElement.lang = lang === "zh" ? "zh-CN" : lang;
try{ localStorage.setItem("imsapiScriptExampleLanguage", lang); }catch(e){}
}
function initLanguageDropdown(){
const langSel=$("scriptExampleLanguage");
if(!langSel) return;
try{
const stored=localStorage.getItem("imsapiScriptExampleLanguage");
if(stored && SCRIPT_EXAMPLE_TEXTS[stored]) langSel.value=stored;
}catch(e){}
langSel.onchange=updateScriptExampleNote;
updateScriptExampleNote();
}
function pathParts(path){
return String(path||"")
.replace(/\[(\d+)\]/g,".$1")
.split(".")
.filter(Boolean);
}
function getPathValue(root,path){
let cur=root;
for(const part of pathParts(path)){
if(cur==null) return undefined;
cur=cur[part];
}
return cur;
}
function inferKeysParam(valuesParam){
const s=String(valuesParam||"");
if(/ResultValues$/i.test(s)) return s.replace(/ResultValues$/i,"ResultKeys");
if(/Values$/i.test(s)) return s.replace(/Values$/i,"Keys");
if(/Result$/i.test(s)) return s.replace(/Result$/i,"Keys");
return s+"Keys";
}
function readStepParam(step,paramName){
if(!step) return undefined;
return getPathValue(step.response,`result.${paramName}`) ??
getPathValue(step.response,paramName) ??
getPathValue(step.payload,paramName);
}
function camelCaseKey(key){
const parts=String(key||"")
.toLowerCase()
.split(/[^a-z0-9]+/i)
.filter(Boolean);
if(!parts.length) return "";
return parts[0]+parts.slice(1).map(p=>p.charAt(0).toUpperCase()+p.slice(1)).join("");
}
function setVarAlias(target,key,value){
if(!key) return;
target[key]=value;
const alias=camelCaseKey(key);
if(alias && alias!==key) target[alias]=value;
}
function isExportableValuesParam(paramName,value){
return /Values$/i.test(paramName) && Array.isArray(value);
}
function autoExportStepVars(ctx, stepId, stepData, options={}){
if(options.enabled===false) return [];
const result=getPathValue(stepData.response,"result") || {};
const exported=[];
if(!ctx.vars[stepId] || typeof ctx.vars[stepId]!=="object") ctx.vars[stepId]={};
if(!ctx.vars[stepId].rows) ctx.vars[stepId].rows={};
Object.entries(result).forEach(([paramName,value])=>{
if(!isExportableValuesParam(paramName,value)) return;
const keysParam=inferKeysParam(paramName);
const keys=readStepParam(stepData,keysParam);
if(!Array.isArray(keys) || !keys.length) return;
const rows=rowsFromStep(ctx,stepId,paramName);
if(!rows.length) return;
ctx.vars[stepId].rows[paramName]=rows.map(row=>{
const copy={...row};
delete copy._values;
return copy;
});
keys.forEach(key=>{
const column=rows.map(row=>row[key] ?? "");
const first=column[0] ?? "";
const alias=camelCaseKey(key);
// Step-Namespace: immer vorhanden und konfliktfrei nutzbar.
setVarAlias(ctx.vars[stepId],key,first);
if(alias) ctx.vars[stepId][alias+"List"]=column;
ctx.vars[stepId][key+"_LIST"]=column;
// Globale Kurzvariablen: bequem für den nächsten Step.
// Bei mehreren Steps mit gleichen Keys gewinnt der zuletzt ausgeführte Step.
if(options.globals!==false){
setVarAlias(ctx.vars,key,first);
if(alias) ctx.vars[alias+"List"]=column;
ctx.vars[key+"_LIST"]=column;
}
});
exported.push(`${paramName}: ${keys.join(", ")}`);
});
return exported;
}
function rowsFromStep(ctx, stepId, keysParam, valuesParam){
const step=ctx.steps[stepId];
if(!step) return [];
const realKeysParam=valuesParam ? keysParam : inferKeysParam(keysParam);
const realValuesParam=valuesParam || keysParam;
let keys=readStepParam(step,realKeysParam);
let values=readStepParam(step,realValuesParam);
if(!Array.isArray(keys)) keys=[];
if(!Array.isArray(values)) values = values == null ? [] : [values];
const width=keys.length || 1;
const rows=[];
for(let i=0;i<values.length;i+=width){
const row={_index:rows.length,_values:values.slice(i,i+width)};
if(keys.length){
keys.forEach((k,j)=>row[k]=values[i+j] ?? "");
}else{
row.VALUE=values[i] ?? "";
}
rows.push(row);
}
return rows;
}
function resolveToken(token,ctx){
const t=String(token||"").trim();
if(t.startsWith("vars.")) return getPathValue(ctx.vars,t.slice(5));
if(t.startsWith("login.")) return getPathValue(ctx.login,t.slice(6));
if(t.startsWith("steps.")){
const parts=t.split(".");
const stepId=parts[1];
const rest=parts.slice(2).join(".");
return getPathValue(ctx.steps[stepId],rest);
}
if(t.startsWith("row:")){
const parts=t.split(":");
// row:stepId:valuesParam:rowIndex:key
if(parts.length>=5){
const [,stepId,a,b,c,...rest]=parts;
if(/^\d+$/.test(b)){
const rows=rowsFromStep(ctx,stepId,a);
return rows[Number(b)]?.[c] ?? "";
}
// row:stepId:keysParam:valuesParam:rowIndex:key
const rows=rowsFromStep(ctx,stepId,a,b);
return rows[Number(c)]?.[rest.join(":")] ?? "";
}
}
if(t.startsWith("col:")){
const parts=t.split(":");
// col:stepId:valuesParam:key
if(parts.length>=4){
const [,stepId,valuesParam,...keyParts]=parts;
const key=keyParts.join(":");
return rowsFromStep(ctx,stepId,valuesParam).map(r=>r[key] ?? "");
}
}
if(t.startsWith("flat:")){
const parts=t.split(":");
// flat:stepId:valuesParam
if(parts.length>=3){
const [,stepId,valuesParam]=parts;
const step=ctx.steps[stepId];
const values=readStepParam(step,valuesParam);
return Array.isArray(values) ? values : (values == null ? [] : [values]);
}
}
if(t.startsWith("join:")){
const parts=t.split(":");
// join:stepId:valuesParam:key:separator
if(parts.length>=5){
const [,stepId,valuesParam,key,...sepParts]=parts;
const sep=sepParts.join(":");
return rowsFromStep(ctx,stepId,valuesParam).map(r=>r[key] ?? "").join(sep);
}
}
return getPathValue(ctx.vars,t) ?? "";
}
function resolveStringTemplate(str,ctx){
const whole=String(str).match(/^\s*\{\{\s*([^}]+?)\s*\}\}\s*$/);
if(whole) return resolveToken(whole[1],ctx);
return String(str).replace(/\{\{\s*([^}]+?)\s*\}\}/g,(_,token)=>{
const value=resolveToken(token,ctx);
if(value == null) return "";
if(typeof value === "object") return JSON.stringify(value);
return String(value);
});
}
function resolveScriptValue(value,ctx){
if(typeof value === "string") return resolveStringTemplate(value,ctx);
if(Array.isArray(value)) return value.map(v=>resolveScriptValue(v,ctx));
if(value && typeof value === "object"){
if(Object.prototype.hasOwnProperty.call(value,"$concat")){
const parts=Array.isArray(value.$concat) ? value.$concat : [];
return parts.flatMap(part=>{
const resolved=resolveScriptValue(part,ctx);
return Array.isArray(resolved) ? resolved : [resolved];
});
}
if(Object.prototype.hasOwnProperty.call(value,"$rows")){
const cfg=value.$rows || {};
const rows=rowsFromStep(ctx,cfg.from,cfg.keys || cfg.values, cfg.keys ? cfg.values : undefined);
const columns=cfg.columns || [];
return rows.flatMap(row=>columns.map(col=>row[col] ?? ""));
}
const out={};
Object.entries(value).forEach(([k,v])=>out[k]=resolveScriptValue(v,ctx));
return out;
}
return value;
}
async function runScript(){
if(!globalLogin){
alert("Bitte zuerst Login durchführen.");
return;
}
let script;
try{
script=readScriptJson();
}catch(e){
appendScriptLog("Script JSON ist ungültig: "+e.message);
return;
}
const ctx={vars:{...(script.vars||{})},steps:{},login:getLoginRuntimeVars()};
const summary=[];
appendScriptLog("Script gestartet...");
for(const [idx,step] of (script.steps||[]).entries()){
if(step.enabled===false){
appendScriptLog(`Step ${idx+1} übersprungen: ${step.id||step.api} (enabled:false)`);
continue;
}
if(!step.api){
appendScriptLog(`Step ${idx+1} hat keine api-Angabe.`);
continue;
}
const id=step.id || `step${idx+1}`;
const payload=injectRuntimeDefaults(resolveScriptValue(step.payload || {},ctx), step.api);
try{
appendScriptLog(`${id}: ${step.api}`);
const response=await callApi(step.api,payload);
ctx.steps[id]={id,api:step.api,payload,response};
const exported=autoExportStepVars(ctx,id,ctx.steps[id],{
enabled: step.autoExports !== false && script.autoExports !== false,
globals: step.autoExportGlobals !== false && script.autoExportGlobals !== false
});
if(exported.length){
appendScriptLog(` Auto-Export: ${exported.join(" | ")}`);
}
if(step.exports && typeof step.exports==="object"){
Object.entries(step.exports).forEach(([name,expr])=>{
ctx.vars[name]=resolveScriptValue(expr,ctx);
});
}
const ret=getPathValue(response,"result.return_value");
appendScriptLog(`${id} fertig${ret!==undefined?` (return_value=${ret})`:""}`);
summary.push({id,api:step.api,return_value:ret,response});
}catch(e){
appendScriptLog(`${id} Fehler: ${e.message || e}`);
summary.push({id,api:step.api,error:String(e)});
if(step.continueOnError!==true) break;
}
}
const output={vars:ctx.vars,steps:summary};
$("result").textContent="Script Ergebnis:\n\n"+JSON.stringify(output,null,2);
appendScriptLog("Script beendet.");
}
function initScriptEditor(){
const editor=getScriptEditor();
if(editor && !editor.value.trim()) writeScriptJson({vars:{},steps:[]});
const add=$("btnAddCurrentStep");
if(add) add.onclick=()=>{
try{ addCurrentApiAsScriptStep(); }
catch(e){ appendScriptLog("Step konnte nicht hinzugefügt werden: "+e.message); }
};
initLanguageDropdown();
const ex=$("btnScriptExample");
if(ex) ex.onclick=insertScriptExample;
const run=$("btnRunScript");
if(run) run.onclick=runScript;
const clear=$("btnClearScriptLog");
if(clear) clear.onclick=()=>{ getScriptLog().textContent=""; };
}
initScriptEditor();
</script>
</body>
</html>