fix: simple prompt editing (save + restore original only), live users search, straight responsive admin rows
This commit is contained in:
parent
e7599128ec
commit
f1ac24a17d
3 changed files with 70 additions and 182 deletions
|
|
@ -1186,11 +1186,18 @@ textarea.full-input{resize:vertical;}
|
|||
.btn-bio-login:active{transform:translateY(0);}
|
||||
.btn-bio-login i{font-size:18px;}
|
||||
|
||||
/* Admin settings rows: aligned label/control pairs with consistent boxes. */
|
||||
.admin-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
||||
.admin-row-label { font-size:13px; font-weight:600; color:var(--g700); min-width:130px; }
|
||||
.admin-control { font-size:13px; font-family:inherit; padding:6px 10px; border:1px solid var(--g300); border-radius:6px; flex:1; max-width:420px; min-width:180px; background:white; color:var(--g800); }
|
||||
/* Admin settings rows: straight aligned label/control pairs with consistent boxes. */
|
||||
.admin-row { display:grid; grid-template-columns:minmax(140px,200px) minmax(0,1fr) auto; gap:10px; align-items:center; }
|
||||
.admin-row-label { font-size:13px; font-weight:600; color:var(--g700); text-align:right; }
|
||||
.admin-control { width:100%; font-size:13px; font-family:inherit; padding:6px 10px; border:1px solid var(--g300); border-radius:6px; max-width:420px; background:white; color:var(--g800); }
|
||||
.admin-control:focus { outline:none; border-color:var(--blue); box-shadow:0 0 0 2px var(--blue-light); }
|
||||
.cms-prompt-family > label { display:grid; grid-template-columns:minmax(140px,200px) minmax(0,1fr); gap:10px; align-items:center; }
|
||||
@media (max-width:640px) {
|
||||
.admin-row { grid-template-columns:1fr; }
|
||||
.admin-row-label { text-align:left; }
|
||||
.admin-control { max-width:none; }
|
||||
.cms-prompt-family > label { grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
/* One consistent font across the whole Admin panel. */
|
||||
#admin-tab select, #admin-tab input, #admin-tab textarea, #admin-tab button,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ function adminTabActive() {
|
|||
.catch(function() { allUsers = []; tbody.innerHTML = adminTableMessage(5, 'var(--red)', 'Request failed'); });
|
||||
}
|
||||
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target && e.target.id === 'admin-users-search') filterUsers();
|
||||
});
|
||||
|
||||
function renderUsers(users) {
|
||||
var tbody = document.getElementById('admin-users-body');
|
||||
if (!tbody) return;
|
||||
|
|
@ -589,69 +593,47 @@ function adminTabActive() {
|
|||
} finally { promptsLoading = false; }
|
||||
}
|
||||
|
||||
// Compact per-family editor: one prompt select, one draft textarea, one status
|
||||
// line, save/restore/history actions and a compact revision list for the
|
||||
// SELECTED prompt. Drafts and revision baselines are kept per key in memory,
|
||||
// so switching prompts (or families) never loses another prompt's unsaved draft.
|
||||
// Simple prompt editor: pick a prompt, edit it, save it, or restore the
|
||||
// shipped default. Revision tracking stays internal only so the server
|
||||
// optimistic concurrency contract keeps working; no history UI is shown.
|
||||
function createPromptFamilyEditor(family, prompts) {
|
||||
var editor = document.createElement('div');
|
||||
editor.className = 'cms-prompt-family';
|
||||
editor.dataset.family = family;
|
||||
editor.style.cssText = 'display:flex;flex-direction:column;gap:10px;';
|
||||
// Only static markup is parsed; all catalogue/revision content is assigned as text.
|
||||
// Only static markup is parsed; all catalogue content is assigned as text.
|
||||
editor.innerHTML =
|
||||
'<label style="font-size:13px;font-weight:600;">Prompt' +
|
||||
'<select class="prompt-select" style="display:block;margin-top:4px;max-width:100%;font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;"></select></label>' +
|
||||
'<select class="prompt-select" style="display:block;margin-top:4px;max-width:100%;font-size:13px;font-family:inherit;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;"></select></label>' +
|
||||
'<textarea class="prompt-draft" rows="8" style="display:block;width:100%;box-sizing:border-box;font-family:inherit;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;"></textarea>' +
|
||||
'<p class="prompt-baseline" style="margin:0;font-size:12px;color:var(--g500);"></p>' +
|
||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;">' +
|
||||
'<button type="button" class="btn-sm btn-primary" data-prompt-action="save">Save prompt</button>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="reset">Restore original</button>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="history">History (latest 100)</button></div>' +
|
||||
'<p class="prompt-status" role="status" style="margin:0;font-size:12px;color:var(--g600);"></p>' +
|
||||
'<div class="prompt-history" hidden>' +
|
||||
'<label>Saved revisions<select class="prompt-revisions" style="display:block;max-width:100%;"></select></label>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="view">View revision</button>' +
|
||||
'<textarea class="prompt-revision-text" rows="4" readonly style="display:block;width:100%;box-sizing:border-box;font-family:inherit;font-size:12px;"></textarea>' +
|
||||
'<p class="prompt-revision-meta"></p>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="restore">Restore viewed revision</button>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="baseline">Keep draft; use viewed current revision as save baseline</button></div>';
|
||||
'<button type="button" class="btn-sm btn-ghost" data-prompt-action="reset">Restore original</button></div>' +
|
||||
'<p class="prompt-status" role="status" style="margin:0;font-size:12px;color:var(--g600);"></p>';
|
||||
var select = editor.querySelector('.prompt-select');
|
||||
var text = editor.querySelector('.prompt-draft');
|
||||
var status = editor.querySelector('.prompt-status');
|
||||
var history = editor.querySelector('.prompt-history');
|
||||
var revisions = editor.querySelector('.prompt-revisions');
|
||||
var preview = editor.querySelector('.prompt-revision-text');
|
||||
var meta = editor.querySelector('.prompt-revision-meta');
|
||||
var buttons = {};
|
||||
editor.querySelectorAll('[data-prompt-action]').forEach(function(button) { buttons[button.dataset.promptAction] = button; });
|
||||
|
||||
// Per-key state: server value, optimistic save baseline, current server
|
||||
// revision (from history) and the live unsaved draft.
|
||||
// Per-key state: server value, revision baseline (internal only) and live draft.
|
||||
var states = new Map();
|
||||
prompts.forEach(function(p) {
|
||||
var option = document.createElement('option');
|
||||
option.value = p.dbKey;
|
||||
option.textContent = p.key;
|
||||
select.appendChild(option);
|
||||
states.set(p.dbKey, { value: p.value, revision: p.revision, currentRevision: p.revision, draft: p.value });
|
||||
states.set(p.dbKey, { value: p.value, revision: p.revision, draft: p.value });
|
||||
});
|
||||
var key = select.options.length ? select.options[0].value : null;
|
||||
var viewedRevision = null;
|
||||
var busy = false;
|
||||
if (key) text.value = states.get(key).draft;
|
||||
|
||||
function state() { return key ? states.get(key) : null; }
|
||||
function controls() {
|
||||
Object.values(buttons).forEach(function(button) { button.disabled = busy; });
|
||||
buttons.save.disabled = busy;
|
||||
buttons.reset.disabled = busy;
|
||||
select.disabled = busy;
|
||||
revisions.disabled = busy;
|
||||
buttons.view.disabled = busy || !revisions.value;
|
||||
buttons.restore.disabled = busy || !viewedRevision;
|
||||
buttons.baseline.disabled = busy || !viewedRevision || !state() || viewedRevision.id !== state().currentRevision;
|
||||
var st = state();
|
||||
editor.querySelector('.prompt-baseline').textContent = st ?
|
||||
'Save baseline: revision ' + st.revision + (st.revision === 0 ? ' (no history yet).' : '.') : '';
|
||||
}
|
||||
async function run(task) {
|
||||
if (busy) return;
|
||||
|
|
@ -660,85 +642,31 @@ function adminTabActive() {
|
|||
catch (error) { status.textContent = error.message + ' Unsaved edits are preserved.'; }
|
||||
finally { busy = false; controls(); }
|
||||
}
|
||||
function clearPreview() {
|
||||
viewedRevision = null; preview.value = ''; meta.textContent = ''; controls();
|
||||
}
|
||||
function revisionLabel(item) {
|
||||
return '#' + item.id + ' — ' + item.createdAt + ' — ' + (item.createdBy == null ? 'unknown actor' : item.createdBy) +
|
||||
(item.wasDefault ? ' — default snapshot' : '') + (item.restoredFrom == null ? '' : ' — restored from #' + item.restoredFrom);
|
||||
}
|
||||
function base() { return '/api/admin/config/prompts/' + encodeURIComponent(key); }
|
||||
async function mutate(action, revisionId) {
|
||||
async function mutate(action) {
|
||||
var targetKey = key;
|
||||
var st = states.get(targetKey);
|
||||
var submitted = text.value;
|
||||
st.draft = submitted;
|
||||
var body = { expectedRevision: st.revision };
|
||||
if (action === 'save') body.value = submitted;
|
||||
if (action === 'restore') body.revisionId = revisionId;
|
||||
var data = await promptRequest(action === 'save' ? '/api/admin/config/' + encodeURIComponent(targetKey) : base() + '/' + action,
|
||||
var data = await promptRequest(action === 'save' ? '/api/admin/config/' + encodeURIComponent(targetKey) : '/api/admin/config/prompts/' + encodeURIComponent(targetKey) + '/reset',
|
||||
action === 'save' ? 'PUT' : 'POST', body);
|
||||
st.value = data.value;
|
||||
st.revision = data.revision;
|
||||
st.currentRevision = data.revision;
|
||||
// Typing while a request is pending must not be overwritten by its response;
|
||||
// switching away must not leak the response into another prompt's editor.
|
||||
if (select.value === targetKey && text.value === submitted) {
|
||||
text.value = data.value;
|
||||
st.draft = data.value;
|
||||
}
|
||||
history.hidden = true; revisions.replaceChildren(); clearPreview();
|
||||
status.textContent = 'Saved revision ' + data.revision + (text.value === data.value ? '.' : '. Newer draft edits are still unsaved.');
|
||||
if (text.value === submitted) text.value = data.value;
|
||||
status.textContent = action === 'save' ? 'Saved.' : 'Restored shipped default.';
|
||||
}
|
||||
select.onchange = function() {
|
||||
// Stash the outgoing draft before switching: per-key drafts survive switches.
|
||||
select.addEventListener('change', function() {
|
||||
if (key) states.get(key).draft = text.value;
|
||||
key = select.value;
|
||||
var st = state();
|
||||
text.value = st ? st.draft : '';
|
||||
viewedRevision = null; preview.value = ''; meta.textContent = '';
|
||||
history.hidden = true; revisions.replaceChildren();
|
||||
text.value = states.get(key).draft;
|
||||
status.textContent = '';
|
||||
controls();
|
||||
};
|
||||
buttons.save.onclick = function() { run(function() { return mutate('save'); }); };
|
||||
buttons.reset.onclick = function() {
|
||||
showConfirm('Reset only this prompt to its shipped default? This replaces this editor’s draft and creates a revision; other editors are unchanged.', function() {
|
||||
});
|
||||
buttons.save.addEventListener('click', function() { run(function() { return mutate('save'); }); });
|
||||
buttons.reset.addEventListener('click', function() {
|
||||
showConfirm('Restore only this prompt to its shipped default? This replaces the current value.', function() {
|
||||
run(function() { return mutate('reset'); });
|
||||
});
|
||||
};
|
||||
buttons.history.onclick = function() { run(async function() {
|
||||
var st = states.get(key);
|
||||
var data = await promptRequest(base() + '/history?limit=100');
|
||||
st.currentRevision = data.revision;
|
||||
revisions.replaceChildren(); clearPreview();
|
||||
(data.revisions || []).forEach(function(item) {
|
||||
var option = document.createElement('option'); option.value = item.id; option.textContent = revisionLabel(item); revisions.appendChild(option);
|
||||
});
|
||||
history.hidden = false;
|
||||
status.textContent = 'Current server revision: ' + st.currentRevision + '. ' + (revisions.options.length ? 'Select a revision to view. Your draft is unchanged.' : 'No saved revisions yet.');
|
||||
}); };
|
||||
revisions.onchange = clearPreview;
|
||||
buttons.view.onclick = function() { run(async function() {
|
||||
var data = await promptRequest(base() + '/revisions/' + encodeURIComponent(revisions.value));
|
||||
viewedRevision = data.revision;
|
||||
preview.value = viewedRevision.value;
|
||||
meta.textContent = revisionLabel(viewedRevision);
|
||||
status.textContent = 'Viewing revision ' + viewedRevision.id + '. Your draft is unchanged.';
|
||||
}); };
|
||||
buttons.restore.onclick = function() {
|
||||
var id = viewedRevision && viewedRevision.id;
|
||||
if (!id) return;
|
||||
showConfirm('Restore revision ' + id + ' for only this prompt? This replaces this editor’s draft and creates a new revision.', function() {
|
||||
run(function() { return mutate('restore', id); });
|
||||
});
|
||||
};
|
||||
buttons.baseline.onclick = function() {
|
||||
var st = state();
|
||||
if (!viewedRevision || !st || viewedRevision.id !== st.currentRevision) return;
|
||||
st.revision = st.currentRevision; controls();
|
||||
status.textContent = 'Draft kept. The next save will use revision ' + st.revision + ' as its baseline; review your edits before saving.';
|
||||
};
|
||||
});
|
||||
controls();
|
||||
return editor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ test('three prompt sections keep Scribe, Clinical and Learning separate with ine
|
|||
assert.equal(selectOf(scribe).value, catalogue[0].dbKey, 'first prompt selected by default');
|
||||
assert.equal(selectOf(scribe).options[0].textContent, 'SCRIBE_0');
|
||||
assert.equal(draftOf(scribe).value, unsafe);
|
||||
for (const name of ['save', 'history', 'view', 'restore', 'reset', 'baseline']) assert.ok(actionOf(scribe, name));
|
||||
for (const name of ['save', 'reset']) assert.ok(actionOf(scribe, name)); assert.equal(scribe.querySelector('.prompt-history'), null, 'no revision history UI');
|
||||
for (const p of [catalogue[0], catalogue[1], ...catalogue.slice(-2)]) {
|
||||
const editor = sectionForPrompt(ui, p);
|
||||
await choosePrompt(editor, p.dbKey);
|
||||
|
|
@ -90,82 +90,59 @@ test('three prompt sections keep Scribe, Clinical and Learning separate with ine
|
|||
assert.equal(call.url, '/api/admin/config/' + p.dbKey);
|
||||
assert.equal(call.options.method, 'PUT');
|
||||
assert.deepEqual(call.body, { value: '\n' + unsafe + '\n', expectedRevision: p.revision });
|
||||
assert.match(statusOf(editor).textContent, /Saved revision 11/);
|
||||
assert.match(statusOf(editor).textContent, /Saved/);
|
||||
}
|
||||
await choosePrompt(scribe, catalogue[2].dbKey);
|
||||
assert.equal(draftOf(scribe).value, unsafe, 'other prompts unchanged');
|
||||
});
|
||||
|
||||
test('history/view/restore/reset use exact per-key APIs for every family and never replace other drafts', async t => {
|
||||
const historical = { id: 5, value: unsafe, createdAt: '2026-01-02T03:04:05Z', createdBy: '<img onerror=bad>', restoredFrom: 2, wasDefault: true };
|
||||
test('restore original resets only the selected prompt and never replaces other drafts', async t => {
|
||||
const ui = await browser(t, 'admin', (url, options) => {
|
||||
if (url.endsWith('/history?limit=100')) return json({ success: true, revision: 10, revisions: [historical] });
|
||||
if (url.endsWith('/revisions/5')) return json({ success: true, revision: historical });
|
||||
if (url.endsWith('/restore')) return json({ success: true, value: unsafe, revision: 11 });
|
||||
if (url.endsWith('/reset')) return json({ success: true, value: 'new shipped default', revision: 12 });
|
||||
});
|
||||
const scribe = sectionOf(ui, 'scribe');
|
||||
const clinical = sectionOf(ui, 'clinical');
|
||||
await choosePrompt(clinical, 'clinical_assistant.system_behavior');
|
||||
draftOf(clinical).value = 'Unsaved clinical edits';
|
||||
await choosePrompt(scribe, catalogue[1].dbKey);
|
||||
draftOf(scribe).value = 'Unrelated unsaved Scribe draft';
|
||||
await choosePrompt(scribe, catalogue[0].dbKey);
|
||||
for (const p of [catalogue[0], ...catalogue.slice(-2)]) {
|
||||
const f = sectionForPrompt(ui, p);
|
||||
await choosePrompt(f, p.dbKey);
|
||||
const base = '/api/admin/config/prompts/' + p.dbKey;
|
||||
draftOf(f).value = 'Unsaved clinical/Scribe edits';
|
||||
await clickAction(f, 'history');
|
||||
assert.equal(ui.calls.at(-1).url, base + '/history?limit=100');
|
||||
assert.match(f.querySelector('.prompt-revisions option').textContent, /default snapshot.*restored from #2/);
|
||||
await clickAction(f, 'view');
|
||||
assert.equal(ui.calls.at(-1).url, base + '/revisions/5');
|
||||
assert.equal(f.querySelector('.prompt-revision-text').value, unsafe);
|
||||
assert.equal(f.querySelector('.prompt-revision-text').readOnly, true);
|
||||
assert.equal(draftOf(f).value, 'Unsaved clinical/Scribe edits');
|
||||
assert.equal(actionOf(f, 'baseline').disabled, true, 'an old revision cannot rebase a stale save');
|
||||
assert.equal(f.querySelector('script, img, [onerror]'), null);
|
||||
await clickAction(f, 'restore');
|
||||
assert.equal(ui.calls.at(-1).url, base + '/restore');
|
||||
assert.equal(ui.calls.at(-1).options.method, 'POST');
|
||||
assert.deepEqual(ui.calls.at(-1).body, { revisionId: 5, expectedRevision: 10 });
|
||||
assert.equal(draftOf(f).value, unsafe);
|
||||
await clickAction(f, 'reset');
|
||||
assert.equal(ui.calls.at(-1).url, base + '/reset');
|
||||
assert.deepEqual(ui.calls.at(-1).body, { expectedRevision: 11 });
|
||||
assert.equal(draftOf(f).value, 'new shipped default');
|
||||
}
|
||||
await choosePrompt(clinical, 'clinical_assistant.image_behavior');
|
||||
draftOf(clinical).value = 'Unsaved image edits';
|
||||
await clickAction(clinical, 'reset');
|
||||
assert.equal(ui.calls.at(-1).url, '/api/admin/config/prompts/clinical_assistant.image_behavior/reset');
|
||||
assert.equal(ui.calls.at(-1).options.method, 'POST');
|
||||
assert.deepEqual(ui.calls.at(-1).body, { expectedRevision: 10 });
|
||||
assert.equal(draftOf(clinical).value, 'new shipped default');
|
||||
await choosePrompt(scribe, catalogue[1].dbKey);
|
||||
assert.equal(draftOf(scribe).value, 'Unrelated unsaved Scribe draft', 'switch never discards another prompt\'s draft');
|
||||
assert.equal(draftOf(scribe).value, 'Unrelated unsaved Scribe draft', 'reset never touches another prompt');
|
||||
await choosePrompt(clinical, 'clinical_assistant.system_behavior');
|
||||
assert.equal(draftOf(clinical).value, 'Unsaved clinical edits', 'reset never touches another prompt in the same section');
|
||||
});
|
||||
|
||||
test('409s preserve drafts; explicit review/rebase resolves conflicts without an automatic overwrite', async t => {
|
||||
test('409s preserve drafts; revisiting the section refreshes the baseline for a clean save', async t => {
|
||||
let conflicting = true;
|
||||
const ui = await browser(t, 'admin', (url, options) => {
|
||||
if (url.endsWith('/history?limit=100')) return json({ success: true, revision: 14, revisions: [{ id: 14, createdAt: 'now' }] });
|
||||
if (url.endsWith('/revisions/14')) return json({ success: true, revision: { id: 14, value: 'Concurrent edit', createdAt: 'now' } });
|
||||
if (options.method === 'PUT' || url.endsWith('/reset') || url.endsWith('/restore')) {
|
||||
if (options.method === 'PUT' || url.endsWith('/reset')) {
|
||||
if (conflicting) return json({ error: 'Stale revision' }, 409);
|
||||
return json({ success: true, value: JSON.parse(options.body).value, revision: 15 });
|
||||
}
|
||||
});
|
||||
const f = sectionOf(ui, 'clinical');
|
||||
await choosePrompt(f, 'clinical_assistant.system_behavior');
|
||||
draftOf(f).value = 'Keep this draft';
|
||||
await clickAction(f, 'save');
|
||||
assert.match(statusOf(f).textContent, /Conflict.*draft is unchanged/);
|
||||
await clickAction(f, 'reset');
|
||||
assert.equal(draftOf(f).value, 'Keep this draft');
|
||||
await clickAction(f, 'history'); await clickAction(f, 'view');
|
||||
await clickAction(f, 'restore');
|
||||
assert.match(statusOf(f).textContent, /Conflict/);
|
||||
assert.equal(draftOf(f).value, 'Keep this draft');
|
||||
assert.match(f.querySelector('.prompt-baseline').textContent, /revision 10/);
|
||||
await clickAction(f, 'baseline');
|
||||
assert.equal(draftOf(f).value, 'Keep this draft');
|
||||
assert.match(f.querySelector('.prompt-baseline').textContent, /revision 14/);
|
||||
const clinical = sectionOf(ui, 'clinical');
|
||||
await choosePrompt(clinical, 'clinical_assistant.system_behavior');
|
||||
draftOf(clinical).value = 'Keep this draft';
|
||||
await clickAction(clinical, 'save');
|
||||
assert.match(statusOf(clinical).textContent, /Conflict.*draft is unchanged/);
|
||||
assert.equal(draftOf(clinical).value, 'Keep this draft');
|
||||
conflicting = false;
|
||||
await clickAction(f, 'save');
|
||||
assert.deepEqual(ui.calls.at(-1).body, { value: 'Keep this draft', expectedRevision: 14 });
|
||||
assert.match(statusOf(f).textContent, /Saved revision 15/);
|
||||
// Revisiting the tab reloads the catalogue and refreshes the internal baseline.
|
||||
ui.document.dispatchEvent(new ui.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
|
||||
await tick();
|
||||
await choosePrompt(clinical, 'clinical_assistant.system_behavior');
|
||||
draftOf(clinical).value = 'Keep this draft';
|
||||
await clickAction(clinical, 'save');
|
||||
assert.deepEqual(ui.calls.at(-1).body, { value: 'Keep this draft', expectedRevision: 10 });
|
||||
assert.match(statusOf(clinical).textContent, /Saved/);
|
||||
});
|
||||
|
||||
test('failed loads can retry; transport failures and in-flight saves preserve newer and unrelated drafts', async t => {
|
||||
|
|
@ -186,8 +163,6 @@ test('failed loads can retry; transport failures and in-flight saves preserve ne
|
|||
const f = sectionOf(ui, 'clinical');
|
||||
await choosePrompt(f, 'clinical_assistant.image_behavior');
|
||||
draftOf(f).value = 'Preserved draft';
|
||||
await clickAction(f, 'history');
|
||||
assert.match(statusOf(f).textContent, /Synthetic offline failure/);
|
||||
await clickAction(f, 'save');
|
||||
assert.equal(draftOf(f).value, 'Preserved draft');
|
||||
assert.match(statusOf(f).textContent, /Migration unavailable/);
|
||||
|
|
@ -198,32 +173,12 @@ test('failed loads can retry; transport failures and in-flight saves preserve ne
|
|||
draftOf(f).value = 'Newer draft typed during save';
|
||||
release(); await tick();
|
||||
assert.equal(draftOf(f).value, 'Newer draft typed during save');
|
||||
assert.match(statusOf(f).textContent, /Newer draft edits are still unsaved/);
|
||||
assert.match(statusOf(f).textContent, /Saved/);
|
||||
// Resetting non-prompt CMS settings also must not reload prompt editors.
|
||||
ui.document.getElementById('btn-reset-all-defaults').click(); await tick();
|
||||
assert.equal(draftOf(f).value, 'Newer draft typed during save');
|
||||
});
|
||||
|
||||
test('empty history and failed revision viewing leave drafts intact and restore unavailable', async t => {
|
||||
let empty = true;
|
||||
const ui = await browser(t, 'admin', url => {
|
||||
if (url.endsWith('/history?limit=100')) return json({ success: true, revision: empty ? 0 : 4, revisions: empty ? [] : [{ id: 4, createdAt: 'now' }] });
|
||||
if (url.endsWith('/revisions/4')) return json({ error: 'Revision unavailable' }, 404);
|
||||
});
|
||||
const f = sectionOf(ui, 'scribe');
|
||||
await choosePrompt(f, catalogue[1].dbKey);
|
||||
draftOf(f).value = 'Keep even when no history is available';
|
||||
await clickAction(f, 'history');
|
||||
assert.match(statusOf(f).textContent, /No saved revisions yet/);
|
||||
assert.equal(actionOf(f, 'view').disabled, true);
|
||||
assert.equal(actionOf(f, 'restore').disabled, true);
|
||||
empty = false;
|
||||
await clickAction(f, 'history'); await clickAction(f, 'view');
|
||||
assert.match(statusOf(f).textContent, /Revision unavailable/);
|
||||
assert.equal(actionOf(f, 'restore').disabled, true);
|
||||
assert.equal(draftOf(f).value, 'Keep even when no history is available');
|
||||
});
|
||||
|
||||
async function loadChat(ui) {
|
||||
const load = ui.document.querySelector('[data-assistant-load-chat="1"]');
|
||||
assert.ok(load); load.click(); await tick();
|
||||
|
|
@ -467,7 +422,6 @@ test('assistant settings retries leave global prompt drafts/history and starter
|
|||
});
|
||||
const f = sectionOf(ui, 'scribe');
|
||||
draftOf(f).value = 'Unsaved global prompt';
|
||||
f.querySelector('.prompt-revision-text').value = 'Previously viewed revision';
|
||||
setting(ui, 'prompt-pool-snapshots').innerHTML = '<option value="7">Existing snapshot</option>';
|
||||
const before = f.outerHTML;
|
||||
const promptCalls = ui.calls.filter(c => c.url.includes('/config/prompts')).length;
|
||||
|
|
@ -477,7 +431,6 @@ test('assistant settings retries leave global prompt drafts/history and starter
|
|||
available = true;
|
||||
adminVisit(ui); await tick();
|
||||
assert.equal(draftOf(f).value, 'Unsaved global prompt');
|
||||
assert.equal(f.querySelector('.prompt-revision-text').value, 'Previously viewed revision');
|
||||
assert.equal(ui.calls.filter(c => c.url.includes('/config/prompts')).length, promptCalls);
|
||||
assert.equal(ui.calls.some(c => c.options.method === 'POST' || c.options.method === 'PUT'), false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue