2355 lines
74 KiB
HTML
2355 lines
74 KiB
HTML
<!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 d’aide",
|
||
addCurrentStep:"Ajouter l’API actuelle comme étape",
|
||
insertExample:"Insérer un exemple",
|
||
runScript:"Exécuter le script",
|
||
clearLog:"Effacer le journal",
|
||
resultDefault:"Aucun script n’a encore été exécuté.",
|
||
note:"Les exemples et les textes d’aide 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> |