RenderWorkflow() was passing the route param (instance ID) as the entry token to the template JS. The advance API needs the actual entry token from the instance. Now resolves from inst.EntryToken and checks the ?token= query param from the landing page redirect. Also fix submitForm() early-return guard that blocked progressive forms using fieldsets (checked FORM_TPL.fields but not .fieldsets). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
632 lines
27 KiB
HTML
632 lines
27 KiB
HTML
{{define "workflow.html"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{.Data.WorkflowTitle}} — {{.InstanceName}}</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/favicon.svg?v={{.Version}}">
|
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}/favicon.ico?v={{.Version}}">
|
|
<link rel="stylesheet" href="{{.BasePath}}/css/variables.css?v={{.Version}}">
|
|
<link rel="stylesheet" href="{{.BasePath}}/css/primitives.css?v={{.Version}}">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: var(--font); background: var(--bg); color: var(--text); }
|
|
|
|
.wf-shell { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
|
|
|
|
.wf-header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-surface);
|
|
}
|
|
.wf-header h1 { font-size: 18px; font-weight: 600; }
|
|
.wf-header p { font-size: 13px; color: var(--text-2); margin-top: 4px; }
|
|
|
|
.wf-stage-info {
|
|
padding: 8px 24px; font-size: 12px; color: var(--text-2);
|
|
background: var(--bg-surface); border-bottom: 1px solid var(--border);
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
}
|
|
.wf-progress {
|
|
display: flex; gap: 4px;
|
|
}
|
|
.wf-progress-dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
background: var(--border);
|
|
}
|
|
.wf-progress-dot.active { background: var(--accent); }
|
|
.wf-progress-dot.done { background: var(--text-3); }
|
|
|
|
/* ── Form ───────────────────────── */
|
|
.wf-form {
|
|
flex: 1; overflow-y: auto; padding: 24px;
|
|
max-width: 640px; margin: 0 auto; width: 100%;
|
|
}
|
|
.wf-form-field { margin-bottom: 16px; }
|
|
.wf-form-field label {
|
|
display: block; font-size: 13px; font-weight: 600;
|
|
margin-bottom: 4px; color: var(--text);
|
|
}
|
|
.wf-form-field label .required { color: var(--danger, #e74c3c); margin-left: 2px; }
|
|
.wf-form-field input, .wf-form-field select, .wf-form-field textarea {
|
|
width: 100%; padding: 8px 12px; font-size: 14px;
|
|
background: var(--input-bg); color: var(--text);
|
|
border: 1px solid var(--border); border-radius: 6px;
|
|
font-family: var(--font);
|
|
}
|
|
.wf-form-field input:focus, .wf-form-field select:focus, .wf-form-field textarea:focus {
|
|
outline: none; border-color: var(--accent);
|
|
}
|
|
.wf-form-field textarea { resize: vertical; min-height: 80px; }
|
|
.wf-form-field .field-error {
|
|
font-size: 12px; color: var(--danger, #e74c3c); margin-top: 4px;
|
|
}
|
|
.wf-form-field.has-error input,
|
|
.wf-form-field.has-error select,
|
|
.wf-form-field.has-error textarea {
|
|
border-color: var(--danger, #e74c3c);
|
|
}
|
|
.wf-form-field .field-hint {
|
|
font-size: 12px; color: var(--text-3); margin-top: 2px;
|
|
}
|
|
.wf-form-check {
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.wf-form-check input[type="checkbox"] {
|
|
width: auto; margin: 0;
|
|
}
|
|
|
|
.wf-form-submit {
|
|
margin-top: 24px;
|
|
}
|
|
.wf-form-submit button {
|
|
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
padding: 10px 24px; font-weight: 600; cursor: pointer; font-size: 14px;
|
|
}
|
|
.wf-form-submit button:hover { background: var(--accent-hover); }
|
|
.wf-form-submit button:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
.wf-form-success {
|
|
text-align: center; padding: 40px 24px;
|
|
}
|
|
.wf-form-success h3 { font-size: 18px; margin-bottom: 8px; }
|
|
.wf-form-success p { color: var(--text-2); }
|
|
|
|
.wf-session-info {
|
|
padding: 8px 24px; font-size: 12px; color: var(--text-3);
|
|
border-top: 1px solid var(--border); background: var(--bg);
|
|
}
|
|
|
|
/* ── Unsupported mode fallback ─── */
|
|
.wf-unsupported {
|
|
flex: 1; display: flex; align-items: center; justify-content: center;
|
|
padding: 24px; text-align: center; color: var(--text-2);
|
|
}
|
|
|
|
/* ── Branding ────────────────────── */
|
|
.wf-branding-logo { max-height: 32px; margin-bottom: 4px; }
|
|
.wf-branding-tagline { font-size: 13px; color: var(--text-2); margin-top: 2px; }
|
|
|
|
/* ── Progressive Forms ────────── */
|
|
.wf-fieldset-nav {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
margin-bottom: 16px; padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.wf-fieldset-nav .step-label { font-size: 14px; font-weight: 600; }
|
|
.wf-fieldset-nav .step-counter { font-size: 12px; color: var(--text-3); }
|
|
.wf-fieldset-buttons { display: flex; gap: 8px; margin-top: 16px; }
|
|
.wf-fieldset-buttons button {
|
|
padding: 8px 20px; border-radius: 8px; font-size: 14px; cursor: pointer;
|
|
font-weight: 600; border: 1px solid var(--border);
|
|
background: var(--bg-surface); color: var(--text);
|
|
}
|
|
.wf-fieldset-buttons button.primary {
|
|
background: var(--accent); color: #fff; border-color: var(--accent);
|
|
}
|
|
.wf-fieldset-buttons button.primary:hover { background: var(--accent-hover); }
|
|
|
|
/* ── Conditional field (hidden) ── */
|
|
.wf-form-field.cond-hidden { display: none; }
|
|
</style>
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var p = JSON.parse(localStorage.getItem('sb-appearance') || '{}');
|
|
var mode = p.theme || 'system';
|
|
var resolved = mode;
|
|
if (mode === 'system') resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
document.documentElement.setAttribute('data-theme', resolved);
|
|
} catch(e) {}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="wf-shell">
|
|
<div class="wf-header" id="wfHeader">
|
|
<h1>{{.Data.WorkflowTitle}}</h1>
|
|
{{if .Data.WorkflowDescription}}<p>{{.Data.WorkflowDescription}}</p>{{end}}
|
|
</div>
|
|
|
|
{{if .Data.StageName}}
|
|
<div class="wf-stage-info">
|
|
<span>{{.Data.StageName}}</span>
|
|
{{if gt .Data.TotalStages 1}}
|
|
<div class="wf-progress" id="progressDots"></div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Stage surface: form, review, delegated (custom surface), or automated -->
|
|
|
|
{{if .Data.SurfacePkgID}}
|
|
<div id="customSurfaceMount" style="flex:1;overflow-y:auto"></div>
|
|
{{else if eq .Data.StageMode "form"}}
|
|
<div class="wf-form" id="formArea"></div>
|
|
{{else if eq .Data.StageMode "review"}}
|
|
<div id="reviewArea" style="flex:1;overflow-y:auto;display:flex">
|
|
<div id="reviewDataPanel" style="flex:1;overflow-y:auto;border-right:1px solid var(--border)"></div>
|
|
<div id="reviewActionPanel" style="flex:1;overflow-y:auto;display:flex;flex-direction:column"></div>
|
|
</div>
|
|
{{else if eq .Data.Status "completed"}}
|
|
<div class="wf-form-success" style="flex:1;display:flex;flex-direction:column;justify-content:center">
|
|
<h3>Completed</h3>
|
|
<p>This workflow has been completed. Thank you for your submission.</p>
|
|
</div>
|
|
{{else}}
|
|
<div class="wf-unsupported">
|
|
<p>This stage type ({{.Data.StageMode}}) does not have an interactive surface.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="wf-session-info">
|
|
{{if .Data.SurfacePkgID}}Session:{{else if eq .Data.StageMode "form"}}Submitting as{{else if eq .Data.StageMode "review"}}Reviewing as{{else}}Session:{{end}} <strong>{{.Data.SessionName}}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const BASE = '{{.BasePath}}';
|
|
const INSTANCE_ID = '{{.Data.InstanceID}}';
|
|
const ENTRY_TOKEN = '{{.Data.EntryToken}}';
|
|
const SESSION_ID = '{{.Data.SessionID}}';
|
|
const STAGE_MODE = '{{.Data.StageMode}}';
|
|
const TOTAL_STAGES = {{.Data.TotalStages}};
|
|
const CURRENT_STAGE = {{.Data.CurrentStage}};
|
|
const SURFACE_PKG_ID = '{{.Data.SurfacePkgID}}';
|
|
const STATUS = '{{.Data.Status}}';
|
|
|
|
// API calls use the entry token (public) or instance ID
|
|
const API_ID = ENTRY_TOKEN || INSTANCE_ID;
|
|
|
|
var FORM_TPL = null;
|
|
try { FORM_TPL = JSON.parse('{{.Data.FormTemplateJSON}}' || 'null'); } catch(e) {}
|
|
|
|
var BRANDING = null;
|
|
try { BRANDING = JSON.parse('{{.Data.BrandingJSON}}' || 'null'); } catch(e) {}
|
|
|
|
// Apply branding
|
|
(function() {
|
|
if (!BRANDING) return;
|
|
var header = document.getElementById('wfHeader');
|
|
if (!header) return;
|
|
if (BRANDING.accent_color) {
|
|
document.documentElement.style.setProperty('--accent', BRANDING.accent_color);
|
|
}
|
|
if (BRANDING.logo_url) {
|
|
var img = document.createElement('img');
|
|
img.src = BRANDING.logo_url;
|
|
img.className = 'wf-branding-logo';
|
|
img.alt = '';
|
|
header.insertBefore(img, header.firstChild);
|
|
}
|
|
if (BRANDING.tagline) {
|
|
var tag = document.createElement('div');
|
|
tag.className = 'wf-branding-tagline';
|
|
tag.textContent = BRANDING.tagline;
|
|
header.appendChild(tag);
|
|
}
|
|
})();
|
|
|
|
// ── Progress dots ──────────────────
|
|
(function() {
|
|
var dotsEl = document.getElementById('progressDots');
|
|
if (!dotsEl || TOTAL_STAGES <= 1) return;
|
|
for (var i = 0; i < TOTAL_STAGES; i++) {
|
|
var dot = document.createElement('div');
|
|
dot.className = 'wf-progress-dot';
|
|
if (i < CURRENT_STAGE) dot.className += ' done';
|
|
if (i === CURRENT_STAGE) dot.className += ' active';
|
|
dotsEl.appendChild(dot);
|
|
}
|
|
})();
|
|
|
|
// ── Form rendering (progressive forms + conditional fields) ──
|
|
var _fieldsetIndex = 0;
|
|
var _fieldsetCount = 0;
|
|
|
|
if (STAGE_MODE === 'form' && FORM_TPL) {
|
|
var formArea = document.getElementById('formArea');
|
|
if (FORM_TPL.fieldsets && FORM_TPL.fieldsets.length > 0) {
|
|
renderProgressiveForm(formArea, FORM_TPL);
|
|
} else if (FORM_TPL.fields && FORM_TPL.fields.length > 0) {
|
|
renderForm(formArea, FORM_TPL);
|
|
}
|
|
}
|
|
|
|
function renderForm(container, tpl) {
|
|
var html = '';
|
|
for (var i = 0; i < tpl.fields.length; i++) {
|
|
var f = tpl.fields[i];
|
|
html += renderField(f);
|
|
}
|
|
html += '<div class="wf-form-submit"><button id="formSubmitBtn" onclick="submitForm()">Submit</button></div>';
|
|
container.innerHTML = html;
|
|
applyConditionalFields(tpl.fields);
|
|
}
|
|
|
|
function renderProgressiveForm(container, tpl) {
|
|
_fieldsetCount = tpl.fieldsets.length;
|
|
_fieldsetIndex = 0;
|
|
var html = '';
|
|
for (var s = 0; s < tpl.fieldsets.length; s++) {
|
|
var fs = tpl.fieldsets[s];
|
|
var display = s === 0 ? '' : ' style="display:none"';
|
|
html += '<div class="wf-fieldset" id="fieldset_' + s + '"' + display + '>';
|
|
html += '<div class="wf-fieldset-nav"><span class="step-label">' + escHtml(fs.label) + '</span>';
|
|
html += '<span class="step-counter">Step ' + (s + 1) + ' of ' + _fieldsetCount + '</span></div>';
|
|
for (var i = 0; i < fs.fields.length; i++) {
|
|
html += renderField(fs.fields[i]);
|
|
}
|
|
html += '<div class="wf-fieldset-buttons">';
|
|
if (s > 0) html += '<button onclick="showFieldset(' + (s - 1) + ')">Back</button>';
|
|
if (s < tpl.fieldsets.length - 1) {
|
|
html += '<button class="primary" onclick="showFieldset(' + (s + 1) + ')">Next</button>';
|
|
} else {
|
|
html += '<button class="primary" id="formSubmitBtn" onclick="submitForm()">Submit</button>';
|
|
}
|
|
html += '</div></div>';
|
|
}
|
|
container.innerHTML = html;
|
|
for (var s = 0; s < tpl.fieldsets.length; s++) {
|
|
applyConditionalFields(tpl.fieldsets[s].fields);
|
|
}
|
|
}
|
|
|
|
window.showFieldset = function(idx) {
|
|
if (idx < 0 || idx >= _fieldsetCount) return;
|
|
document.getElementById('fieldset_' + _fieldsetIndex).style.display = 'none';
|
|
document.getElementById('fieldset_' + idx).style.display = '';
|
|
_fieldsetIndex = idx;
|
|
};
|
|
|
|
function applyConditionalFields(fields) {
|
|
for (var i = 0; i < fields.length; i++) {
|
|
var f = fields[i];
|
|
if (!f.condition) continue;
|
|
var when = f.condition.when;
|
|
var srcEl = document.getElementById('ff_' + when);
|
|
if (!srcEl) continue;
|
|
(function(field) {
|
|
var handler = function() { evaluateCondition(field); };
|
|
srcEl.addEventListener('change', handler);
|
|
srcEl.addEventListener('input', handler);
|
|
handler();
|
|
})(f);
|
|
}
|
|
}
|
|
|
|
function evaluateCondition(f) {
|
|
var cond = f.condition;
|
|
var srcEl = document.getElementById('ff_' + cond.when);
|
|
var tgtEl = document.querySelector('.wf-form-field[data-key="' + f.key + '"]');
|
|
if (!srcEl || !tgtEl) return;
|
|
|
|
var val = srcEl.type === 'checkbox' ? srcEl.checked : srcEl.value;
|
|
var op = cond.op || 'eq';
|
|
var visible = false;
|
|
|
|
switch (op) {
|
|
case 'exists': visible = val !== '' && val !== false; break;
|
|
case 'eq': visible = String(val) === String(cond.value); break;
|
|
case 'neq': visible = String(val) !== String(cond.value); break;
|
|
case 'in': visible = Array.isArray(cond.value) && cond.value.map(String).indexOf(String(val)) >= 0; break;
|
|
default: visible = true;
|
|
}
|
|
|
|
if (visible) {
|
|
tgtEl.classList.remove('cond-hidden');
|
|
} else {
|
|
tgtEl.classList.add('cond-hidden');
|
|
}
|
|
}
|
|
|
|
function renderField(f) {
|
|
var req = f.required ? '<span class="required">*</span>' : '';
|
|
var ph = f.placeholder ? ' placeholder="' + escAttr(f.placeholder) + '"' : '';
|
|
var inner = '';
|
|
|
|
switch (f.type) {
|
|
case 'text':
|
|
case 'email':
|
|
case 'date':
|
|
inner = '<input type="' + f.type + '" id="ff_' + f.key + '"' + ph + '>';
|
|
break;
|
|
case 'number':
|
|
var attrs = ph;
|
|
if (f.validation) {
|
|
if (f.validation.min != null) attrs += ' min="' + f.validation.min + '"';
|
|
if (f.validation.max != null) attrs += ' max="' + f.validation.max + '"';
|
|
if (f.validation.step != null) attrs += ' step="' + f.validation.step + '"';
|
|
}
|
|
inner = '<input type="number" id="ff_' + f.key + '"' + attrs + '>';
|
|
break;
|
|
case 'textarea':
|
|
inner = '<textarea id="ff_' + f.key + '"' + ph + ' rows="4"></textarea>';
|
|
break;
|
|
case 'select':
|
|
var opts = '<option value="">— Select —</option>';
|
|
if (f.options) {
|
|
for (var j = 0; j < f.options.length; j++) {
|
|
opts += '<option value="' + escAttr(f.options[j].value) + '">' + escHtml(f.options[j].label) + '</option>';
|
|
}
|
|
}
|
|
inner = '<select id="ff_' + f.key + '">' + opts + '</select>';
|
|
break;
|
|
case 'checkbox':
|
|
return '<div class="wf-form-field" data-key="' + f.key + '">' +
|
|
'<div class="wf-form-check">' +
|
|
'<input type="checkbox" id="ff_' + f.key + '">' +
|
|
'<label for="ff_' + f.key + '">' + escHtml(f.label) + req + '</label>' +
|
|
'</div>' +
|
|
'<div class="field-error" id="err_' + f.key + '"></div>' +
|
|
'</div>';
|
|
case 'file':
|
|
var accept = (f.validation && f.validation.accept) ? ' accept="' + escAttr(f.validation.accept) + '"' : '';
|
|
inner = '<input type="file" id="ff_' + f.key + '"' + accept + '>';
|
|
break;
|
|
default:
|
|
inner = '<input type="text" id="ff_' + f.key + '"' + ph + '>';
|
|
}
|
|
|
|
return '<div class="wf-form-field" data-key="' + f.key + '">' +
|
|
'<label for="ff_' + f.key + '">' + escHtml(f.label) + req + '</label>' +
|
|
inner +
|
|
'<div class="field-error" id="err_' + f.key + '"></div>' +
|
|
'</div>';
|
|
}
|
|
|
|
window.submitForm = async function() {
|
|
if (!FORM_TPL || (!FORM_TPL.fields && !FORM_TPL.fieldsets)) return;
|
|
var btn = document.getElementById('formSubmitBtn');
|
|
if (btn) btn.disabled = true;
|
|
|
|
// Clear previous errors
|
|
document.querySelectorAll('.wf-form-field').forEach(function(el) {
|
|
el.classList.remove('has-error');
|
|
});
|
|
document.querySelectorAll('.field-error').forEach(function(el) {
|
|
el.textContent = '';
|
|
});
|
|
|
|
// Collect form data (get all fields from fieldsets or top-level)
|
|
var allFields = FORM_TPL.fields || [];
|
|
if (FORM_TPL.fieldsets && FORM_TPL.fieldsets.length > 0) {
|
|
allFields = [];
|
|
for (var s = 0; s < FORM_TPL.fieldsets.length; s++) {
|
|
allFields = allFields.concat(FORM_TPL.fieldsets[s].fields);
|
|
}
|
|
}
|
|
var data = {};
|
|
for (var i = 0; i < allFields.length; i++) {
|
|
var f = allFields[i];
|
|
var el = document.getElementById('ff_' + f.key);
|
|
if (!el) continue;
|
|
// Skip hidden conditional fields
|
|
var fieldWrap = document.querySelector('.wf-form-field[data-key="' + f.key + '"]');
|
|
if (fieldWrap && fieldWrap.classList.contains('cond-hidden')) continue;
|
|
if (f.type === 'checkbox') {
|
|
data[f.key] = el.checked;
|
|
} else if (f.type === 'number') {
|
|
data[f.key] = el.value !== '' ? parseFloat(el.value) : null;
|
|
} else {
|
|
data[f.key] = el.value;
|
|
}
|
|
}
|
|
|
|
try {
|
|
var resp = await fetch(BASE + '/api/v1/public/workflows/advance/' + API_ID, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ data: data }),
|
|
});
|
|
var result = await resp.json();
|
|
if (!resp.ok) {
|
|
if (result.errors) {
|
|
showErrors(result.errors);
|
|
} else if (result.error) {
|
|
alert(result.error);
|
|
}
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
// Success
|
|
if (result.status === 'advanced' || result.status === 'completed') {
|
|
showFormSuccess(result.status === 'completed'
|
|
? 'Thank you! Your submission is complete.'
|
|
: 'Submitted! Moving to the next step...');
|
|
if (result.status === 'advanced') {
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
}
|
|
} else {
|
|
showFormSuccess('Your response has been submitted.');
|
|
}
|
|
} catch(e) {
|
|
alert('Network error: ' + e.message);
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
function showErrors(errors) {
|
|
for (var i = 0; i < errors.length; i++) {
|
|
var e = errors[i];
|
|
var errEl = document.getElementById('err_' + e.key);
|
|
if (errEl) errEl.textContent = e.message;
|
|
var fieldEl = document.querySelector('.wf-form-field[data-key="' + e.key + '"]');
|
|
if (fieldEl) fieldEl.classList.add('has-error');
|
|
}
|
|
}
|
|
|
|
function showFormSuccess(msg) {
|
|
var formArea = document.getElementById('formArea');
|
|
if (formArea) {
|
|
formArea.innerHTML = '<div class="wf-form-success"><h3>Submitted</h3><p>' + escHtml(msg) + '</p></div>';
|
|
}
|
|
}
|
|
|
|
function escHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function escAttr(s) {
|
|
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
// ── Custom surface (delegated mode) ──────────
|
|
if (SURFACE_PKG_ID) {
|
|
var mount = document.getElementById('customSurfaceMount');
|
|
if (mount) {
|
|
mount.innerHTML = '<div style="padding:24px;color:var(--danger)">Custom workflow surfaces require an updated package format. The legacy surface registry has been removed.</div>';
|
|
}
|
|
}
|
|
|
|
function loadScript(src) {
|
|
return new Promise(function(resolve, reject) {
|
|
var s = document.createElement('script');
|
|
s.src = src;
|
|
s.onload = resolve;
|
|
s.onerror = function() { reject(new Error('Failed to load ' + src)); };
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
// ── Review surface (structured review with side-by-side + comments) ──
|
|
if (STAGE_MODE === 'review') {
|
|
var dataPanel = document.getElementById('reviewDataPanel');
|
|
var actionPanel = document.getElementById('reviewActionPanel');
|
|
if (dataPanel && actionPanel) {
|
|
loadReviewSurface(dataPanel, actionPanel);
|
|
}
|
|
}
|
|
|
|
async function loadReviewSurface(dataPanel, actionPanel) {
|
|
dataPanel.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text-3)">Loading\u2026</div>';
|
|
|
|
// Left panel: structured data card
|
|
var html = '<div style="padding:24px">';
|
|
html += '<h3 style="margin-bottom:16px">Collected Data</h3>';
|
|
|
|
try {
|
|
var resp = await fetch(BASE + '/api/v1/public/workflows/resume/' + API_ID, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (resp.ok) {
|
|
var status = await resp.json();
|
|
if (status.stage_data && typeof status.stage_data === 'object') {
|
|
html += '<table style="width:100%;border-collapse:collapse">';
|
|
for (var key in status.stage_data) {
|
|
if (!status.stage_data.hasOwnProperty(key) || key.startsWith('_')) continue;
|
|
html += '<tr style="border-bottom:1px solid var(--border)">';
|
|
html += '<td style="padding:8px;font-weight:600;font-size:13px;width:30%;vertical-align:top">' + escHtml(key) + '</td>';
|
|
var val = status.stage_data[key];
|
|
var display = typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
|
|
html += '<td style="padding:8px;font-size:13px;white-space:pre-wrap">' + escHtml(display) + '</td>';
|
|
html += '</tr>';
|
|
}
|
|
html += '</table>';
|
|
} else {
|
|
html += '<p style="color:var(--text-3)">No data collected yet.</p>';
|
|
}
|
|
}
|
|
} catch(e) {
|
|
html += '<p style="color:var(--danger)">Failed to load review data.</p>';
|
|
}
|
|
html += '</div>';
|
|
dataPanel.innerHTML = html;
|
|
|
|
// Right panel: comment input + approve/reject buttons
|
|
var actHtml = '<div style="padding:24px;flex:1;display:flex;flex-direction:column">';
|
|
actHtml += '<h3 style="margin-bottom:16px">Review Actions</h3>';
|
|
actHtml += '<div style="margin-bottom:16px">';
|
|
actHtml += '<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">Add Comment</label>';
|
|
actHtml += '<textarea id="reviewComment" rows="3" style="width:100%;padding:8px;font-size:14px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text);font-family:var(--font);resize:vertical" placeholder="Optional comment\u2026"></textarea>';
|
|
actHtml += '</div>';
|
|
actHtml += '<div style="display:flex;gap:8px">';
|
|
actHtml += '<button id="reviewAdvanceBtn" style="background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Approve & Advance</button>';
|
|
actHtml += '<button id="reviewRejectBtn" style="background:var(--danger,#e74c3c);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Reject</button>';
|
|
actHtml += '</div>';
|
|
actHtml += '<div style="font-size:12px;color:var(--text-3);margin-top:8px">Ctrl+Enter: Approve · Ctrl+Shift+Enter: Reject</div>';
|
|
actHtml += '</div>';
|
|
actionPanel.innerHTML = actHtml;
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.ctrlKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (e.shiftKey) {
|
|
document.getElementById('reviewRejectBtn').click();
|
|
} else {
|
|
document.getElementById('reviewAdvanceBtn').click();
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('reviewAdvanceBtn').addEventListener('click', async function() {
|
|
this.disabled = true;
|
|
try {
|
|
var r = await fetch(BASE + '/api/v1/public/workflows/advance/' + API_ID, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ data: {} }),
|
|
});
|
|
if (r.ok) {
|
|
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Approved</h3><p style="color:var(--text-2)">Stage advanced.</p></div>';
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
} else {
|
|
var err = await r.json().catch(function() { return {}; });
|
|
alert('Failed: ' + (err.error || 'unknown'));
|
|
document.getElementById('reviewAdvanceBtn').disabled = false;
|
|
}
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
});
|
|
|
|
document.getElementById('reviewRejectBtn').addEventListener('click', async function() {
|
|
var comment = document.getElementById('reviewComment').value;
|
|
var reason = comment || prompt('Rejection reason:');
|
|
if (!reason) return;
|
|
this.disabled = true;
|
|
try {
|
|
var r = await fetch(BASE + '/api/v1/public/workflows/advance/' + API_ID, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ data: { _action: 'reject', reason: reason } }),
|
|
});
|
|
if (r.ok) {
|
|
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Rejected</h3><p style="color:var(--text-2)">Sent back for revision.</p></div>';
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
} else {
|
|
var err = await r.json().catch(function() { return {}; });
|
|
alert('Failed: ' + (err.error || 'unknown'));
|
|
document.getElementById('reviewRejectBtn').disabled = false;
|
|
}
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|