Changeset 0.31.2 (#205)
This commit is contained in:
@@ -173,6 +173,7 @@ Object.assign(UI, {
|
||||
if (tab === 'members') UI.loadTeamManageMembers(teamId);
|
||||
if (tab === 'providers') UI.loadTeamManageProviders(teamId);
|
||||
if (tab === 'personas') UI.loadTeamManagePersonas(teamId);
|
||||
if (tab === 'workflows') UI.loadTeamWorkflows(teamId);
|
||||
if (tab === 'usage') UI.loadTeamUsage();
|
||||
if (tab === 'activity') UI.loadTeamAuditLog(1);
|
||||
if (tab === 'settings') UI.loadTeamManageSettings(teamId);
|
||||
|
||||
@@ -117,3 +117,53 @@
|
||||
API.completeAssignment = function(id) {
|
||||
return this._post(`/api/v1/workflow-assignments/${id}/complete`, {});
|
||||
};
|
||||
|
||||
// ── Team-Scoped Workflow CRUD (v0.31.2) ──
|
||||
|
||||
API.listTeamWorkflows = function(teamId) {
|
||||
return this._get(`/api/v1/teams/${teamId}/workflows`);
|
||||
};
|
||||
|
||||
API.getTeamWorkflow = function(teamId, id) {
|
||||
return this._get(`/api/v1/teams/${teamId}/workflows/${id}`);
|
||||
};
|
||||
|
||||
API.createTeamWorkflow = function(teamId, data) {
|
||||
return this._post(`/api/v1/teams/${teamId}/workflows`, data);
|
||||
};
|
||||
|
||||
API.updateTeamWorkflow = function(teamId, id, patch) {
|
||||
return this._patch(`/api/v1/teams/${teamId}/workflows/${id}`, patch);
|
||||
};
|
||||
|
||||
API.deleteTeamWorkflow = function(teamId, id) {
|
||||
return this._del(`/api/v1/teams/${teamId}/workflows/${id}`);
|
||||
};
|
||||
|
||||
API.listTeamWorkflowStages = function(teamId, workflowId) {
|
||||
return this._get(`/api/v1/teams/${teamId}/workflows/${workflowId}/stages`);
|
||||
};
|
||||
|
||||
API.createTeamWorkflowStage = function(teamId, workflowId, data) {
|
||||
return this._post(`/api/v1/teams/${teamId}/workflows/${workflowId}/stages`, data);
|
||||
};
|
||||
|
||||
API.updateTeamWorkflowStage = function(teamId, workflowId, stageId, data) {
|
||||
return this._put(`/api/v1/teams/${teamId}/workflows/${workflowId}/stages/${stageId}`, data);
|
||||
};
|
||||
|
||||
API.deleteTeamWorkflowStage = function(teamId, workflowId, stageId) {
|
||||
return this._del(`/api/v1/teams/${teamId}/workflows/${workflowId}/stages/${stageId}`);
|
||||
};
|
||||
|
||||
API.reorderTeamWorkflowStages = function(teamId, workflowId, orderedIds) {
|
||||
return this._patch(`/api/v1/teams/${teamId}/workflows/${workflowId}/stages/reorder`, { ordered_ids: orderedIds });
|
||||
};
|
||||
|
||||
API.publishTeamWorkflow = function(teamId, workflowId) {
|
||||
return this._post(`/api/v1/teams/${teamId}/workflows/${workflowId}/publish`, {});
|
||||
};
|
||||
|
||||
API.getTeamWorkflowVersion = function(teamId, workflowId, version) {
|
||||
return this._get(`/api/v1/teams/${teamId}/workflows/${workflowId}/versions/${version}`);
|
||||
};
|
||||
|
||||
707
src/js/workflow-team-admin.js
Normal file
707
src/js/workflow-team-admin.js
Normal file
@@ -0,0 +1,707 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard — Team Workflow Builder (v0.31.2)
|
||||
// ==========================================
|
||||
// Team admin panel for managing team-scoped workflow definitions,
|
||||
// stages, and publishing. Adapted from workflow-admin.js using
|
||||
// team-scoped API endpoints. Loaded as a team admin tab.
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var esc = typeof window.esc === 'function' ? window.esc : function (s) {
|
||||
var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML;
|
||||
};
|
||||
|
||||
// ── Scaffold HTML ────────────────────────
|
||||
|
||||
var SCAFFOLD_HTML =
|
||||
'<div id="twfList" class="admin-list"></div>' +
|
||||
'<div id="twfDetail" style="display:none">' +
|
||||
'<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">' +
|
||||
'<button class="btn-small" id="twfBackBtn">← Back</button>' +
|
||||
'<h4 id="twfDetailName" style="margin:0;flex:1"></h4>' +
|
||||
'<span id="twfDetailVersion" class="badge"></span>' +
|
||||
'<span id="twfDetailStatus" class="badge"></span>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="settings-section" style="margin-bottom:16px">' +
|
||||
'<div class="form-row" style="gap:12px">' +
|
||||
'<div class="form-group" style="flex:2"><label>Name</label>' +
|
||||
'<input type="text" id="twfEditName" style="width:100%"></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>Slug</label>' +
|
||||
'<input type="text" id="twfEditSlug" style="width:100%" readonly></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
||||
'<div class="form-group" style="flex:2"><label>Description</label>' +
|
||||
'<textarea id="twfEditDesc" rows="2" style="width:100%"></textarea></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>Entry Mode</label>' +
|
||||
'<select id="twfEditEntry" style="width:100%">' +
|
||||
'<option value="team_only">Team Only</option>' +
|
||||
'<option value="public_link">Public Link</option>' +
|
||||
'</select></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="margin-top:8px;gap:12px;align-items:center">' +
|
||||
'<label class="toggle-label"><input type="checkbox" id="twfEditActive"><span class="toggle-track"></span><span>Active</span></label>' +
|
||||
'<button class="btn-small btn-primary" id="twfSaveBtn">Save Changes</button>' +
|
||||
'<button class="btn-small" id="twfPublishBtn">Publish</button>' +
|
||||
'<button class="btn-small btn-danger" id="twfDeleteBtn" style="margin-left:auto">Delete</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<h5 style="margin:0 0 8px">Stages</h5>' +
|
||||
'<div id="twfStageList" class="admin-list"></div>' +
|
||||
'<button class="btn-small" id="twfAddStageBtn" style="margin-top:8px">+ Add Stage</button>' +
|
||||
|
||||
'<div id="twfStageEditor" class="settings-section" style="display:none;margin-top:12px">' +
|
||||
'<div class="form-row" style="gap:12px">' +
|
||||
'<div class="form-group" style="flex:2"><label>Stage Name</label>' +
|
||||
'<input type="text" id="twfStageName" style="width:100%"></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>History Mode</label>' +
|
||||
'<select id="twfStageHistory" style="width:100%">' +
|
||||
'<option value="full">Full</option>' +
|
||||
'<option value="summary">Summary</option>' +
|
||||
'<option value="fresh">Fresh</option>' +
|
||||
'</select></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
||||
'<div class="form-group" style="flex:1"><label>Persona</label>' +
|
||||
'<select id="twfStagePersona" style="width:100%">' +
|
||||
'<option value="">None (no AI for this stage)</option>' +
|
||||
'</select></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
||||
'<div class="form-group" style="flex:1"><label>Stage Mode</label>' +
|
||||
'<select id="twfStageMode" style="width:100%">' +
|
||||
'<option value="chat_only">Chat Only</option>' +
|
||||
'<option value="form_only">Form Only (no AI)</option>' +
|
||||
'<option value="form_chat">Form + Chat</option>' +
|
||||
'<option value="review">Review (Human)</option>' +
|
||||
'</select></div>' +
|
||||
'</div>' +
|
||||
'<div id="twfFormBuilderWrap" style="display:none;margin-top:8px">' +
|
||||
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">' +
|
||||
'<label style="margin:0;font-weight:600">Form Fields</label>' +
|
||||
'<label class="toggle-label" style="font-size:12px"><input type="checkbox" id="twfFormJsonToggle"><span class="toggle-track"></span><span>Show JSON</span></label>' +
|
||||
'</div>' +
|
||||
'<div id="twfFormFields"></div>' +
|
||||
'<button class="btn-small" id="twfAddFieldBtn" style="margin-top:4px">+ Add Field</button>' +
|
||||
'<textarea id="twfStageForm" rows="4" style="display:none;width:100%;font-family:monospace;font-size:12px;margin-top:6px" placeholder=\'{"fields":[{"key":"name","type":"text","label":"Full Name","required":true}]}\'></textarea>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="margin-top:8px;gap:8px">' +
|
||||
'<button class="btn-small btn-primary" id="twfSaveStageBtn">Save Stage</button>' +
|
||||
'<button class="btn-small" id="twfCancelStageBtn">Cancel</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<h5 style="margin:16px 0 8px">Public URL</h5>' +
|
||||
'<div id="twfPublicUrl" class="text-muted" style="font-size:12px;word-break:break-all"></div>' +
|
||||
'</div>';
|
||||
|
||||
// ── State ───────────────────────────────
|
||||
|
||||
var _teamId = null;
|
||||
var _teamSlug = null;
|
||||
var _currentWf = null;
|
||||
var _editingStageId = null;
|
||||
var _personaCache = [];
|
||||
|
||||
function _getTeamId() {
|
||||
return _teamId || (UI && UI._managingTeamId) || null;
|
||||
}
|
||||
|
||||
// Load team personas into the stage editor dropdown.
|
||||
async function _twfLoadPersonaSelect(selectedId) {
|
||||
var sel = document.getElementById('twfStagePersona');
|
||||
if (!sel) return;
|
||||
|
||||
var teamId = _getTeamId();
|
||||
if (_personaCache.length === 0 && teamId) {
|
||||
try {
|
||||
var resp = await API._get('/api/v1/teams/' + teamId + '/personas');
|
||||
_personaCache = (resp.data || resp || []).map(function(p) {
|
||||
return { id: p.id, name: p.name, icon: p.icon || '' };
|
||||
});
|
||||
} catch (e) {
|
||||
// Fall back to user-visible personas
|
||||
try {
|
||||
var resp2 = await API.listPersonas();
|
||||
_personaCache = (resp2.data || resp2 || []).map(function(p) {
|
||||
return { id: p.id, name: p.name, icon: p.icon || '' };
|
||||
});
|
||||
} catch (e2) { /* no personas available */ }
|
||||
}
|
||||
}
|
||||
|
||||
sel.innerHTML = '<option value="">None (no AI for this stage)</option>';
|
||||
_personaCache.forEach(function(p) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = (p.icon ? p.icon + ' ' : '') + p.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
if (selectedId) sel.value = selectedId;
|
||||
}
|
||||
|
||||
// ── Loader (called from switchTeamTab) ──
|
||||
|
||||
UI.loadTeamWorkflows = async function(teamId) {
|
||||
_teamId = teamId;
|
||||
_teamSlug = null;
|
||||
_currentWf = null;
|
||||
_editingStageId = null;
|
||||
_personaCache = [];
|
||||
_twfWired = false;
|
||||
|
||||
// Fetch team slug for clean public URLs
|
||||
try {
|
||||
var teamResp = await API._get('/api/v1/admin/teams/' + teamId);
|
||||
_teamSlug = teamResp.slug || null;
|
||||
} catch (e) { /* fallback to team ID in URLs */ }
|
||||
|
||||
var target = document.getElementById('teamTabWorkflows');
|
||||
if (!target) return;
|
||||
|
||||
// Self-scaffold
|
||||
if (!document.getElementById('twfList')) {
|
||||
target.innerHTML = SCAFFOLD_HTML;
|
||||
_twfWireFormBuilder();
|
||||
}
|
||||
|
||||
var list = document.getElementById('twfList');
|
||||
var detail = document.getElementById('twfDetail');
|
||||
if (!list) return;
|
||||
|
||||
detail.style.display = 'none';
|
||||
list.style.display = '';
|
||||
|
||||
try {
|
||||
var resp = await API.listTeamWorkflows(teamId);
|
||||
var wfs = resp.data || resp || [];
|
||||
|
||||
if (wfs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="empty-hint">No workflows for this team</div>' +
|
||||
'<button class="btn-small btn-primary" style="margin-top:8px" ' +
|
||||
'data-action="_twfCreate">+ Create Workflow</button>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">' +
|
||||
'<span class="text-muted">' + wfs.length + ' workflow(s)</span>' +
|
||||
'<button class="btn-small btn-primary" data-action="_twfCreate">+ New</button>' +
|
||||
'</div>';
|
||||
html += '<table class="admin-table"><thead><tr>' +
|
||||
'<th>Name</th><th>Slug</th><th>Entry</th><th>Active</th><th>Version</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
for (var i = 0; i < wfs.length; i++) {
|
||||
var wf = wfs[i];
|
||||
html += '<tr style="cursor:pointer" data-action="_twfOpen" data-args=\'' + JSON.stringify([wf.id]) + '\'>' +
|
||||
'<td>' + esc(wf.name) + '</td>' +
|
||||
'<td><code>' + esc(wf.slug) + '</code></td>' +
|
||||
'<td>' + esc(wf.entry_mode || 'team_only') + '</td>' +
|
||||
'<td>' + (wf.is_active ? '✓' : '—') + '</td>' +
|
||||
'<td>v' + (wf.version || 1) + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
list.innerHTML = html;
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div class="empty-hint">Failed to load: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
};
|
||||
|
||||
// ── Create ──────────────────────────────
|
||||
|
||||
sb.register('_twfCreate', async function() {
|
||||
var teamId = _getTeamId();
|
||||
if (!teamId) return;
|
||||
var name = prompt('Workflow name:');
|
||||
if (!name) return;
|
||||
try {
|
||||
var wf = await API.createTeamWorkflow(teamId, { name: name });
|
||||
UI.toast('Workflow created', 'success');
|
||||
_twfOpen(wf.id);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Open Detail ─────────────────────────
|
||||
|
||||
async function _twfOpen(id) {
|
||||
var teamId = _getTeamId();
|
||||
var list = document.getElementById('twfList');
|
||||
var detail = document.getElementById('twfDetail');
|
||||
try {
|
||||
var wf = await API.getTeamWorkflow(teamId, id);
|
||||
_currentWf = wf;
|
||||
list.style.display = 'none';
|
||||
detail.style.display = '';
|
||||
|
||||
document.getElementById('twfDetailName').textContent = wf.name;
|
||||
document.getElementById('twfDetailVersion').textContent = 'v' + (wf.version || 1);
|
||||
document.getElementById('twfDetailStatus').textContent = wf.is_active ? 'Active' : 'Inactive';
|
||||
document.getElementById('twfDetailStatus').className = 'badge ' + (wf.is_active ? 'badge-ok' : 'badge-warn');
|
||||
|
||||
document.getElementById('twfEditName').value = wf.name;
|
||||
document.getElementById('twfEditSlug').value = wf.slug;
|
||||
document.getElementById('twfEditDesc').value = wf.description || '';
|
||||
document.getElementById('twfEditEntry').value = wf.entry_mode || 'team_only';
|
||||
document.getElementById('twfEditActive').checked = wf.is_active;
|
||||
|
||||
// Public URL (v0.31.2: use team slug for clean URLs)
|
||||
var scope = _teamSlug || wf.team_id || teamId;
|
||||
var base = location.origin + (window.__BASE__ || '');
|
||||
document.getElementById('twfPublicUrl').textContent =
|
||||
wf.entry_mode === 'public_link'
|
||||
? base + '/w/' + scope + '/' + wf.slug
|
||||
: 'Enable "Public Link" entry mode to generate a URL';
|
||||
|
||||
// Preload persona cache
|
||||
await _twfLoadPersonaSelect('');
|
||||
|
||||
// Load stages
|
||||
await _twfLoadStages(id);
|
||||
_twfWireEvents();
|
||||
} catch (e) {
|
||||
UI.toast('Failed to load workflow: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
sb.register('_twfOpen', function(id) { return _twfOpen(id); });
|
||||
|
||||
// ── Stage List ──────────────────────────
|
||||
|
||||
async function _twfLoadStages(wfId) {
|
||||
var teamId = _getTeamId();
|
||||
var el = document.getElementById('twfStageList');
|
||||
if (!el) return;
|
||||
try {
|
||||
var resp = await API.listTeamWorkflowStages(teamId, wfId);
|
||||
var stages = resp.data || resp || [];
|
||||
if (stages.length === 0) {
|
||||
el.innerHTML = '<div class="empty-hint">No stages — add one below</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
stages.forEach(function(s, i) {
|
||||
var personaLabel = '';
|
||||
if (s.persona_id) {
|
||||
var p = _personaCache.find(function(x) { return x.id === s.persona_id; });
|
||||
personaLabel = p ? '<span class="badge">' + esc((p.icon ? p.icon + ' ' : '') + p.name) + '</span>' :
|
||||
'<span class="badge">persona</span>';
|
||||
}
|
||||
var modeLabel = '';
|
||||
if (s.stage_mode === 'review') {
|
||||
modeLabel = '<span class="badge badge-warn">review</span>';
|
||||
} else if (s.stage_mode && s.stage_mode !== 'chat_only') {
|
||||
modeLabel = '<span class="badge badge-accent">' + esc(s.stage_mode.replace('_', ' ')) + '</span>';
|
||||
}
|
||||
html += '<div class="wf-stage-row" draggable="true" data-id="' + s.id + '" data-idx="' + i + '">' +
|
||||
'<span class="wf-stage-grip">⠿</span>' +
|
||||
'<span class="wf-stage-ord">' + i + '</span>' +
|
||||
'<span class="wf-stage-name">' + esc(s.name) + '</span>' +
|
||||
personaLabel + modeLabel +
|
||||
'<span class="badge">' + (s.history_mode || 'full') + '</span>' +
|
||||
'<button class="btn-small" data-action="_twfEditStage" data-args=\'' + JSON.stringify([s.id]) + '\'>Edit</button>' +
|
||||
'<button class="btn-small btn-danger" data-action="_twfDeleteStage" data-args=\'' + JSON.stringify([s.id]) + '\'>×</button>' +
|
||||
'</div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
_twfWireStageDnD(el, wfId);
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="empty-hint">Failed: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stage DnD ───────────────────────────
|
||||
|
||||
function _twfWireStageDnD(container, wfId) {
|
||||
var dragSrc = null;
|
||||
var teamId = _getTeamId();
|
||||
container.querySelectorAll('.wf-stage-row').forEach(function(row) {
|
||||
row.addEventListener('dragstart', function(e) {
|
||||
dragSrc = row;
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', row.dataset.id);
|
||||
});
|
||||
row.addEventListener('dragend', function() {
|
||||
row.classList.remove('dragging');
|
||||
container.querySelectorAll('.wf-stage-row').forEach(function(r) {
|
||||
r.classList.remove('drag-over');
|
||||
});
|
||||
dragSrc = null;
|
||||
});
|
||||
row.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
if (row !== dragSrc) row.classList.add('drag-over');
|
||||
});
|
||||
row.addEventListener('dragleave', function() {
|
||||
row.classList.remove('drag-over');
|
||||
});
|
||||
row.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
row.classList.remove('drag-over');
|
||||
if (!dragSrc || dragSrc === row) return;
|
||||
var rows = Array.from(container.querySelectorAll('.wf-stage-row'));
|
||||
var fromIdx = rows.indexOf(dragSrc);
|
||||
var toIdx = rows.indexOf(row);
|
||||
if (fromIdx < toIdx) {
|
||||
row.parentNode.insertBefore(dragSrc, row.nextSibling);
|
||||
} else {
|
||||
row.parentNode.insertBefore(dragSrc, row);
|
||||
}
|
||||
var orderedIDs = Array.from(container.querySelectorAll('.wf-stage-row')).map(function(r) {
|
||||
return r.dataset.id;
|
||||
});
|
||||
API.reorderTeamWorkflowStages(teamId, wfId, orderedIDs).then(function() {
|
||||
UI.toast('Stages reordered', 'success');
|
||||
_twfLoadStages(wfId);
|
||||
}).catch(function(err) {
|
||||
UI.toast('Reorder failed: ' + (err.message || err), 'error');
|
||||
_twfLoadStages(wfId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Wire Events ─────────────────────────
|
||||
|
||||
var _twfWired = false;
|
||||
function _twfWireEvents() {
|
||||
if (_twfWired) return;
|
||||
_twfWired = true;
|
||||
|
||||
document.getElementById('twfBackBtn').onclick = function() {
|
||||
_currentWf = null;
|
||||
_editingStageId = null;
|
||||
_personaCache = [];
|
||||
_twfWired = false;
|
||||
UI.loadTeamWorkflows(_getTeamId());
|
||||
};
|
||||
|
||||
document.getElementById('twfSaveBtn').onclick = async function() {
|
||||
if (!_currentWf) return;
|
||||
var teamId = _getTeamId();
|
||||
try {
|
||||
await API.updateTeamWorkflow(teamId, _currentWf.id, {
|
||||
name: document.getElementById('twfEditName').value.trim(),
|
||||
description: document.getElementById('twfEditDesc').value.trim(),
|
||||
entry_mode: document.getElementById('twfEditEntry').value,
|
||||
is_active: document.getElementById('twfEditActive').checked,
|
||||
});
|
||||
UI.toast('Saved', 'success');
|
||||
_twfOpen(_currentWf.id);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('twfPublishBtn').onclick = async function() {
|
||||
if (!_currentWf) return;
|
||||
var teamId = _getTeamId();
|
||||
try {
|
||||
var ver = await API.publishTeamWorkflow(teamId, _currentWf.id);
|
||||
UI.toast('Published v' + ver.version_number, 'success');
|
||||
_twfOpen(_currentWf.id);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('twfDeleteBtn').onclick = async function() {
|
||||
if (!_currentWf) return;
|
||||
if (!confirm('Delete workflow "' + _currentWf.name + '"? This cannot be undone.')) return;
|
||||
var teamId = _getTeamId();
|
||||
try {
|
||||
await API.deleteTeamWorkflow(teamId, _currentWf.id);
|
||||
UI.toast('Deleted', 'success');
|
||||
_currentWf = null;
|
||||
_personaCache = [];
|
||||
_twfWired = false;
|
||||
UI.loadTeamWorkflows(teamId);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('twfAddStageBtn').onclick = async function() {
|
||||
_editingStageId = null;
|
||||
document.getElementById('twfStageName').value = '';
|
||||
document.getElementById('twfStageHistory').value = 'full';
|
||||
document.getElementById('twfStagePersona').value = '';
|
||||
document.getElementById('twfStageMode').value = 'chat_only';
|
||||
document.getElementById('twfStageForm').value = '';
|
||||
document.getElementById('twfFormFields').innerHTML = '';
|
||||
_twfUpdateFormBuilderVisibility();
|
||||
await _twfLoadPersonaSelect('');
|
||||
document.getElementById('twfStageEditor').style.display = '';
|
||||
};
|
||||
|
||||
document.getElementById('twfCancelStageBtn').onclick = function() {
|
||||
document.getElementById('twfStageEditor').style.display = 'none';
|
||||
_editingStageId = null;
|
||||
};
|
||||
|
||||
document.getElementById('twfSaveStageBtn').onclick = async function() {
|
||||
if (!_currentWf) return;
|
||||
var teamId = _getTeamId();
|
||||
var name = document.getElementById('twfStageName').value.trim();
|
||||
if (!name) { UI.toast('Stage name required', 'error'); return; }
|
||||
|
||||
var stageMode = document.getElementById('twfStageMode').value;
|
||||
var formTemplate = null;
|
||||
|
||||
if (stageMode === 'form_only' || stageMode === 'form_chat') {
|
||||
var jsonToggle = document.getElementById('twfFormJsonToggle');
|
||||
if (jsonToggle && jsonToggle.checked) {
|
||||
var formRaw = document.getElementById('twfStageForm').value.trim();
|
||||
if (formRaw) {
|
||||
try { formTemplate = JSON.parse(formRaw); }
|
||||
catch (e) { UI.toast('Invalid JSON in form template', 'error'); return; }
|
||||
}
|
||||
} else {
|
||||
formTemplate = _twfBuildFormTemplateFromUI();
|
||||
}
|
||||
if (!formTemplate || !formTemplate.fields || formTemplate.fields.length === 0) {
|
||||
UI.toast('Form modes require at least one form field', 'error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
var formRaw = document.getElementById('twfStageForm').value.trim();
|
||||
if (formRaw) {
|
||||
try { formTemplate = JSON.parse(formRaw); }
|
||||
catch (e) { UI.toast('Invalid JSON in form template', 'error'); return; }
|
||||
}
|
||||
}
|
||||
|
||||
var personaId = document.getElementById('twfStagePersona').value || null;
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
history_mode: document.getElementById('twfStageHistory').value,
|
||||
stage_mode: stageMode,
|
||||
persona_id: personaId,
|
||||
form_template: formTemplate,
|
||||
};
|
||||
|
||||
try {
|
||||
if (_editingStageId) {
|
||||
await API.updateTeamWorkflowStage(teamId, _currentWf.id, _editingStageId, data);
|
||||
} else {
|
||||
data.ordinal = document.querySelectorAll('#twfStageList .wf-stage-row').length;
|
||||
await API.createTeamWorkflowStage(teamId, _currentWf.id, data);
|
||||
}
|
||||
UI.toast('Stage saved', 'success');
|
||||
document.getElementById('twfStageEditor').style.display = 'none';
|
||||
_editingStageId = null;
|
||||
await _twfLoadStages(_currentWf.id);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Edit / Delete Stage ─────────────────
|
||||
|
||||
sb.register('_twfEditStage', async function(stageId) {
|
||||
if (!_currentWf) return;
|
||||
var teamId = _getTeamId();
|
||||
try {
|
||||
var resp = await API.listTeamWorkflowStages(teamId, _currentWf.id);
|
||||
var stages = resp.data || resp || [];
|
||||
var stage = stages.find(function(s) { return s.id === stageId; });
|
||||
if (!stage) return;
|
||||
|
||||
_editingStageId = stageId;
|
||||
document.getElementById('twfStageName').value = stage.name;
|
||||
document.getElementById('twfStageHistory').value = stage.history_mode || 'full';
|
||||
document.getElementById('twfStageMode').value = stage.stage_mode || 'chat_only';
|
||||
await _twfLoadPersonaSelect(stage.persona_id || '');
|
||||
|
||||
var tpl = stage.form_template;
|
||||
document.getElementById('twfStageForm').value = tpl ? JSON.stringify(tpl, null, 2) : '';
|
||||
_twfPopulateFormBuilder(tpl);
|
||||
_twfUpdateFormBuilderVisibility();
|
||||
|
||||
document.getElementById('twfStageEditor').style.display = '';
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
sb.register('_twfDeleteStage', async function(stageId) {
|
||||
if (!_currentWf) return;
|
||||
if (!confirm('Delete this stage?')) return;
|
||||
var teamId = _getTeamId();
|
||||
try {
|
||||
await API.deleteTeamWorkflowStage(teamId, _currentWf.id, stageId);
|
||||
UI.toast('Stage deleted', 'success');
|
||||
await _twfLoadStages(_currentWf.id);
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Form Builder Helpers ────────────────
|
||||
|
||||
var FIELD_TYPES = ['text', 'email', 'number', 'date', 'textarea', 'select', 'checkbox', 'file'];
|
||||
|
||||
function _twfUpdateFormBuilderVisibility() {
|
||||
var mode = document.getElementById('twfStageMode').value;
|
||||
var wrap = document.getElementById('twfFormBuilderWrap');
|
||||
if (wrap) wrap.style.display = (mode === 'form_only' || mode === 'form_chat') ? '' : 'none';
|
||||
}
|
||||
|
||||
function _twfCreateFieldRow(field) {
|
||||
field = field || { key: '', type: 'text', label: '', required: false };
|
||||
var row = document.createElement('div');
|
||||
row.className = 'wf-field-row';
|
||||
row.innerHTML =
|
||||
'<input type="text" class="wf-fb-key" value="' + esc(field.key || '') + '" placeholder="key" style="width:80px">' +
|
||||
'<select class="wf-fb-type" style="width:90px">' +
|
||||
FIELD_TYPES.map(function(t) {
|
||||
return '<option value="' + t + '"' + (t === field.type ? ' selected' : '') + '>' + t + '</option>';
|
||||
}).join('') +
|
||||
'</select>' +
|
||||
'<input type="text" class="wf-fb-label" value="' + esc(field.label || '') + '" placeholder="Label" style="flex:1">' +
|
||||
'<label class="toggle-label" style="font-size:12px;white-space:nowrap"><input type="checkbox" class="wf-fb-required"' +
|
||||
(field.required ? ' checked' : '') + '><span class="toggle-track"></span><span>Req</span></label>' +
|
||||
'<button class="btn-small btn-danger wf-fb-del" style="padding:2px 6px">×</button>';
|
||||
|
||||
row.querySelector('.wf-fb-del').onclick = function() {
|
||||
row.remove();
|
||||
_twfSyncFormJSON();
|
||||
};
|
||||
|
||||
row.querySelector('.wf-fb-type').onchange = function() {
|
||||
var optWrap = row.querySelector('.wf-fb-options');
|
||||
if (this.value === 'select') {
|
||||
if (!optWrap) {
|
||||
optWrap = document.createElement('div');
|
||||
optWrap.className = 'wf-fb-options';
|
||||
optWrap.style.cssText = 'margin:4px 0 0 28px;font-size:12px';
|
||||
optWrap.innerHTML = '<input type="text" class="wf-fb-opts-input" placeholder="option1, option2, ..." style="width:100%;font-size:12px" value="' +
|
||||
esc((field.options || []).map(function(o) { return typeof o === 'string' ? o : o.value || o.label || ''; }).join(', ')) + '">';
|
||||
row.appendChild(optWrap);
|
||||
}
|
||||
optWrap.style.display = '';
|
||||
} else if (optWrap) {
|
||||
optWrap.style.display = 'none';
|
||||
}
|
||||
_twfSyncFormJSON();
|
||||
};
|
||||
|
||||
if (field.type === 'select') {
|
||||
var optWrap = document.createElement('div');
|
||||
optWrap.className = 'wf-fb-options';
|
||||
optWrap.style.cssText = 'margin:4px 0 0 28px;font-size:12px';
|
||||
optWrap.innerHTML = '<input type="text" class="wf-fb-opts-input" placeholder="option1, option2, ..." style="width:100%;font-size:12px" value="' +
|
||||
esc((field.options || []).map(function(o) { return typeof o === 'string' ? o : o.value || o.label || ''; }).join(', ')) + '">';
|
||||
row.appendChild(optWrap);
|
||||
}
|
||||
|
||||
row.querySelectorAll('input, select').forEach(function(el) {
|
||||
el.addEventListener('change', _twfSyncFormJSON);
|
||||
el.addEventListener('input', _twfSyncFormJSON);
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function _twfPopulateFormBuilder(tpl) {
|
||||
var container = document.getElementById('twfFormFields');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
if (!tpl || !tpl.fields || !Array.isArray(tpl.fields)) return;
|
||||
tpl.fields.forEach(function(f) {
|
||||
if (typeof f === 'string') {
|
||||
f = { key: f, type: 'text', label: f, required: false };
|
||||
}
|
||||
container.appendChild(_twfCreateFieldRow(f));
|
||||
});
|
||||
}
|
||||
|
||||
function _twfBuildFormTemplateFromUI() {
|
||||
var container = document.getElementById('twfFormFields');
|
||||
if (!container) return null;
|
||||
var rows = container.querySelectorAll('.wf-field-row');
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
var fields = [];
|
||||
rows.forEach(function(row) {
|
||||
var key = row.querySelector('.wf-fb-key').value.trim();
|
||||
var type = row.querySelector('.wf-fb-type').value;
|
||||
var label = row.querySelector('.wf-fb-label').value.trim();
|
||||
var required = row.querySelector('.wf-fb-required').checked;
|
||||
if (!key) return;
|
||||
|
||||
var field = { key: key, type: type, label: label || key, required: required };
|
||||
|
||||
if (type === 'select') {
|
||||
var optsInput = row.querySelector('.wf-fb-opts-input');
|
||||
if (optsInput && optsInput.value.trim()) {
|
||||
field.options = optsInput.value.split(',').map(function(s) {
|
||||
s = s.trim();
|
||||
return { value: s, label: s };
|
||||
}).filter(function(o) { return o.value; });
|
||||
}
|
||||
}
|
||||
fields.push(field);
|
||||
});
|
||||
|
||||
return fields.length > 0 ? { fields: fields } : null;
|
||||
}
|
||||
|
||||
function _twfSyncFormJSON() {
|
||||
var jsonToggle = document.getElementById('twfFormJsonToggle');
|
||||
if (!jsonToggle) return;
|
||||
var tpl = _twfBuildFormTemplateFromUI();
|
||||
document.getElementById('twfStageForm').value = tpl ? JSON.stringify(tpl, null, 2) : '';
|
||||
}
|
||||
|
||||
function _twfWireFormBuilder() {
|
||||
var modeSelect = document.getElementById('twfStageMode');
|
||||
if (modeSelect) {
|
||||
modeSelect.onchange = _twfUpdateFormBuilderVisibility;
|
||||
}
|
||||
|
||||
var addBtn = document.getElementById('twfAddFieldBtn');
|
||||
if (addBtn) {
|
||||
addBtn.onclick = function() {
|
||||
var container = document.getElementById('twfFormFields');
|
||||
if (container) {
|
||||
container.appendChild(_twfCreateFieldRow());
|
||||
_twfSyncFormJSON();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var jsonToggle = document.getElementById('twfFormJsonToggle');
|
||||
if (jsonToggle) {
|
||||
jsonToggle.onchange = function() {
|
||||
var textarea = document.getElementById('twfStageForm');
|
||||
if (this.checked) {
|
||||
_twfSyncFormJSON();
|
||||
textarea.style.display = '';
|
||||
} else {
|
||||
var raw = textarea.value.trim();
|
||||
if (raw) {
|
||||
try {
|
||||
var tpl = JSON.parse(raw);
|
||||
_twfPopulateFormBuilder(tpl);
|
||||
} catch (e) {
|
||||
UI.toast('Invalid JSON — fix before switching to visual mode', 'error');
|
||||
this.checked = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
textarea.style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user