fix(notebooks): resolve TDZ ReferenceError on updateModelsSummary and empty state rendering
This commit is contained in:
@@ -84,6 +84,8 @@ tests/*
|
||||
!tests/model_sources_hub.mjs
|
||||
!tests/shortcut_layout.mjs
|
||||
!tests/shortcut_organizer.mjs
|
||||
!tests/notebook_render_test.mjs
|
||||
!tests/notebook_save_roundtrip.mjs
|
||||
|
||||
# Local diagnostic/test artifacts
|
||||
grep_doctor.txt
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ DOM or live LiteGraph state.
|
||||
owns structured Prompt Notes (提示词笔记), integrated as a standard tool in the Toolbox
|
||||
with defensive workspace return state restoration, and redesigned with companion models
|
||||
(Base architecture, Main checkpoint, and LoRA stack) prominently positioned at the top of the editor,
|
||||
followed by the core prompt composer and material library archiving. `ui_recipe_detail.js`
|
||||
followed by the core prompt composer and material library archiving, guarded by Temporal Dead Zone (TDZ)
|
||||
safe summary initialization and responsive empty-state fallback rendering. `ui_recipe_detail.js`
|
||||
owns the Workflow Recipe detail workspace, including Overview prompt showcase (with `entry.text` fallback),
|
||||
the responsive Parameter Presets workspace (featuring default-expanded raw node parameter inspection,
|
||||
a `clamp(230px, 24vw, 290px)` sidebar with guarded card actions, uncluttered console action bars with deferred status feedback,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { fixture } from './ui_fixture.mjs';
|
||||
|
||||
const f = fixture();
|
||||
let mockNotebooks = [{ filename: '1.json', name: 'Test Note', data: { baseModel: 'SDXL', loras: [] } }];
|
||||
|
||||
f.fetch = async (url) => {
|
||||
if (String(url).includes('/anomalous/base_models')) {
|
||||
return { ok: true, json: async () => ({ base_models: ['SD 1.5', 'SDXL', 'Flux.1'] }) };
|
||||
}
|
||||
if (String(url).includes('/anomalous/compatible_models')) {
|
||||
return { ok: true, json: async () => ({ models: [] }) };
|
||||
}
|
||||
if (String(url).includes('/anomalous/notebooks')) {
|
||||
return { ok: true, json: async () => ({ notebooks: mockNotebooks }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({ status: 'success' }) };
|
||||
};
|
||||
|
||||
const notes = await f.module('ui_notebooks.js');
|
||||
|
||||
const owner = {
|
||||
currentNotebook: null,
|
||||
nbListEl: f.document.createElement('div'),
|
||||
nbEditor: f.document.createElement('div'),
|
||||
saveCurrentNotebook: async () => true,
|
||||
deleteCurrentNotebook: async () => {},
|
||||
sendNotebookToCanvas: () => {},
|
||||
fillNotebookGalleries: () => {},
|
||||
};
|
||||
owner.renderNotebookEditor = notes.renderNotebookEditor.bind(owner);
|
||||
owner.refreshNotebooks = notes.refreshNotebooks.bind(owner);
|
||||
|
||||
// 1. Test refreshNotebooks with autoOpenFirst=true
|
||||
await owner.refreshNotebooks(true);
|
||||
assert.ok(owner.currentNotebook, 'First notebook should be selected');
|
||||
assert.equal(owner.currentNotebook.filename, '1.json');
|
||||
assert.ok(owner.nbEditor.children.length >= 4, 'nbEditor should contain at least 4 main sections');
|
||||
|
||||
const fold = owner.nbEditor.querySelector('.anomalous-nb-models-fold');
|
||||
assert.ok(fold, 'Companion models fold should exist in nbEditor');
|
||||
assert.equal(fold.open, true, 'Companion models fold should be open by default');
|
||||
|
||||
const promptSection = owner.nbEditor.querySelector('.anomalous-nb-prompt-section');
|
||||
assert.ok(promptSection, 'Prompt section should exist');
|
||||
|
||||
// 2. Test refreshNotebooks when list is empty
|
||||
mockNotebooks = [];
|
||||
owner.currentNotebook = null;
|
||||
await owner.refreshNotebooks(false);
|
||||
assert.ok(owner.nbEditor.textContent.includes('提示词笔记') || owner.nbEditor.textContent.includes('Prompt Notes') || owner.nbEditor.textContent.includes('暂无笔记'), 'Empty state should be displayed when no notebooks exist');
|
||||
|
||||
console.log('notebook_render_test: passed successfully');
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const source = (await fs.readFile(new URL('../web/modules/ui_notebooks.js', import.meta.url), 'utf8'))
|
||||
.replace(/^import .*;$/gm, '')
|
||||
.replace("const t = (key, params) => translate(key, params);", 'const t = key => key;');
|
||||
const { saveCurrentNotebook } = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`);
|
||||
const calls = [];
|
||||
let release;
|
||||
globalThis.fetch = async (_, options) => {
|
||||
calls.push(JSON.parse(options.body));
|
||||
if (calls.length === 1) await new Promise(resolve => { release = resolve; });
|
||||
return { ok: true, json: async () => ({ status: 'success' }) };
|
||||
};
|
||||
const owner = { currentNotebook: { filename: 'one.json', data: { text: 'first' } }, refreshNotebooks: async () => {} };
|
||||
const first = saveCurrentNotebook.call(owner);
|
||||
owner.currentNotebook.data.text = 'second';
|
||||
const second = saveCurrentNotebook.call(owner);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(calls.length, 1);
|
||||
release();
|
||||
assert.deepEqual(await Promise.all([first, second]), [true, true]);
|
||||
assert.deepEqual(calls.map(item => item.data.text), ['first', 'second']);
|
||||
globalThis.document = { createElement: () => ({ setAttribute() {} }) };
|
||||
owner.nbEditor = { prepend(element) { element.isConnected = true; } };
|
||||
globalThis.fetch = async () => ({ ok: false, json: async () => ({ status: 'error' }) });
|
||||
assert.equal(await saveCurrentNotebook.call(owner), false);
|
||||
assert.equal(owner.nbSaveStatus.textContent, 'notebookSaveError');
|
||||
assert.equal(owner.currentNotebook.data.text, 'second');
|
||||
console.log('notebook save ordering and failure: ok');
|
||||
+45
-24
@@ -216,8 +216,18 @@ export async function refreshNotebooks(autoOpenFirst = false) {
|
||||
};
|
||||
this.nbListEl.appendChild(item);
|
||||
});
|
||||
} else if (this.nbEditor && (!this.currentNotebook || !data.notebooks?.length)) {
|
||||
this.nbEditor.innerHTML = `
|
||||
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:#94a3b8; text-align:center; gap:14px; padding:40px;">
|
||||
<span style="font-size:3.2rem;">📝</span>
|
||||
<h3 style="margin:0; color:#f1f5f9; font-size:1.1rem;">${t('promptNotes') || '提示词笔记'}</h3>
|
||||
<p style="margin:0; font-size:0.88rem; max-width:320px; line-height:1.5;">${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {
|
||||
console.error('[AMB] Error refreshing notebooks:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -270,6 +280,7 @@ export async function deleteCurrentNotebook(skipConfirm = false) {
|
||||
|
||||
|
||||
export function renderNotebookEditor() {
|
||||
try {
|
||||
this.nbEditor.innerHTML = '';
|
||||
if (!this.currentNotebook) return;
|
||||
|
||||
@@ -401,6 +412,33 @@ export function renderNotebookEditor() {
|
||||
modelsLabel.append(modelsTitleLeft, arrowIcon);
|
||||
modelsFold.append(modelsLabel, modelSection);
|
||||
|
||||
// Badges for main model and loras
|
||||
const mainSelectedBadge = document.createElement('span');
|
||||
mainSelectedBadge.className = 'anomalous-nb-selected-badge';
|
||||
mainSelectedBadge.style.fontSize = '0.8rem';
|
||||
mainSelectedBadge.style.color = '#c084fc';
|
||||
|
||||
const loraSelectedBadge = document.createElement('span');
|
||||
loraSelectedBadge.className = 'anomalous-nb-selected-badge';
|
||||
loraSelectedBadge.style.fontSize = '0.8rem';
|
||||
loraSelectedBadge.style.color = '#fbbf24';
|
||||
|
||||
const updateModelsSummary = () => {
|
||||
const arrow = modelsFold.open ? '▾' : '▸';
|
||||
arrowIcon.textContent = arrow;
|
||||
const baseInfo = data.baseModel || 'SDXL';
|
||||
const mainInfo = data.mainModel?.filename ? ` · ${data.mainModel.filename}` : '';
|
||||
const loraCount = data.loras?.length ? ` · ${data.loras.length} LoRA` : '';
|
||||
modelsBadge.textContent = `${baseInfo}${mainInfo}${loraCount}`;
|
||||
if (mainSelectedBadge) {
|
||||
mainSelectedBadge.textContent = data.mainModel?.filename ? `✓ ${data.mainModel.filename}` : (t('recipeDiffNone') || '未选择');
|
||||
}
|
||||
if (loraSelectedBadge) {
|
||||
loraSelectedBadge.textContent = data.loras?.length ? `✓ ${data.loras.length} LoRA` : (t('recipeDiffNone') || '未选择');
|
||||
}
|
||||
};
|
||||
this.updateNotebookModelsSummary = updateModelsSummary;
|
||||
|
||||
// Base Model
|
||||
const baseRow = document.createElement('div');
|
||||
baseRow.className = 'anomalous-nb-row anomalous-nb-base-row';
|
||||
@@ -464,10 +502,6 @@ export function renderNotebookEditor() {
|
||||
|
||||
const mainLabel = document.createElement('strong');
|
||||
mainLabel.textContent = `🎨 ${t('mainModel')}`;
|
||||
const mainSelectedBadge = document.createElement('span');
|
||||
mainSelectedBadge.className = 'anomalous-nb-selected-badge';
|
||||
mainSelectedBadge.style.fontSize = '0.8rem';
|
||||
mainSelectedBadge.style.color = '#c084fc';
|
||||
mainRow.append(mainLabel, mainSelectedBadge);
|
||||
|
||||
const mainGallery = document.createElement('div');
|
||||
@@ -488,10 +522,6 @@ export function renderNotebookEditor() {
|
||||
|
||||
const loraLabel = document.createElement('strong');
|
||||
loraLabel.textContent = `⚡ LoRA ${t('loras') || '模型'}`;
|
||||
const loraSelectedBadge = document.createElement('span');
|
||||
loraSelectedBadge.className = 'anomalous-nb-selected-badge';
|
||||
loraSelectedBadge.style.fontSize = '0.8rem';
|
||||
loraSelectedBadge.style.color = '#fbbf24';
|
||||
loraRow.append(loraLabel, loraSelectedBadge);
|
||||
|
||||
const loraGallery = document.createElement('div');
|
||||
@@ -504,21 +534,6 @@ export function renderNotebookEditor() {
|
||||
modelSection.appendChild(mainBox);
|
||||
modelSection.appendChild(loraBox);
|
||||
|
||||
const updateModelsSummary = () => {
|
||||
const arrow = modelsFold.open ? '▾' : '▸';
|
||||
arrowIcon.textContent = arrow;
|
||||
const baseInfo = data.baseModel || 'SDXL';
|
||||
const mainInfo = data.mainModel?.filename ? ` · ${data.mainModel.filename}` : '';
|
||||
const loraCount = data.loras?.length ? ` · ${data.loras.length} LoRA` : '';
|
||||
modelsBadge.textContent = `${baseInfo}${mainInfo}${loraCount}`;
|
||||
if (mainSelectedBadge) {
|
||||
mainSelectedBadge.textContent = data.mainModel?.filename ? `✓ ${data.mainModel.filename}` : (t('recipeDiffNone') || '未选择');
|
||||
}
|
||||
if (loraSelectedBadge) {
|
||||
loraSelectedBadge.textContent = data.loras?.length ? `✓ ${data.loras.length} LoRA` : (t('recipeDiffNone') || '未选择');
|
||||
}
|
||||
};
|
||||
this.updateNotebookModelsSummary = updateModelsSummary;
|
||||
updateModelsSummary();
|
||||
|
||||
// Prompt Section
|
||||
@@ -806,7 +821,13 @@ export function renderNotebookEditor() {
|
||||
if (modelsFold.open) {
|
||||
loadModels();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AMB] Error rendering notebook editor:', err);
|
||||
if (this.nbEditor) {
|
||||
this.nbEditor.innerHTML = `<div style="padding:20px; color:#ef4444;">Render error: ${escapeHtml(err?.message || String(err))}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -2741,7 +2741,7 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
}
|
||||
|
||||
.anomalous-nb-header-main h2 {
|
||||
display: none;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user