Compare commits
1 Commits
e50be2ee66
...
43f69066af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f69066af |
@@ -193,8 +193,13 @@ class AssetMenu {
|
||||
return normalizeApiCategory(key, node.softwareType || key, payload.data);
|
||||
}
|
||||
|
||||
// Non-dispatching populate (matches the wireEvents version). The
|
||||
// load path below explicitly walks supplier -> type -> model ->
|
||||
// unit in order using saved node.* values, so auto-dispatched
|
||||
// change events (which previously cascaded through wireEvents'
|
||||
// listeners and double-populated everything) are no longer needed.
|
||||
function populate(selectEl, items = [], selectedValue, mapFn, placeholderText = 'Select...') {
|
||||
const previous = selectEl.value;
|
||||
if (!selectEl) return;
|
||||
const mapper = typeof mapFn === 'function'
|
||||
? mapFn
|
||||
: (value) => ({ value, label: value });
|
||||
@@ -227,9 +232,6 @@ class AssetMenu {
|
||||
} else {
|
||||
selectEl.value = '';
|
||||
}
|
||||
if (selectEl.value !== previous) {
|
||||
selectEl.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
const categoryKey = resolveCategoryKey();
|
||||
@@ -305,6 +307,28 @@ class AssetMenu {
|
||||
getEventInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset event wiring for ${nodeName}
|
||||
//
|
||||
// The supplier -> type -> model -> unit chain is a strict downward
|
||||
// cascade: each select rebuilds the next based on the currently
|
||||
// selected value above it. Two earlier bugs in this code:
|
||||
//
|
||||
// 1. populate() auto-dispatched a synthetic 'change' event whenever
|
||||
// the value of the rebuilt select differed from before the
|
||||
// rebuild. That triggered the *child* select's listener mid-way
|
||||
// through the *parent* listener, which then continued and
|
||||
// blindly overwrote the child select with empty content. Net
|
||||
// effect: model dropdown showed 'Awaiting Type Selection' even
|
||||
// though a type was clearly selected.
|
||||
//
|
||||
// 2. Each downstream wipe ran unconditionally inside the parent
|
||||
// handler, instead of being driven by the actual current value
|
||||
// of the child select.
|
||||
//
|
||||
// Fix: populate() no longer dispatches change. Cascade is explicit
|
||||
// via cascadeFromSupplier() / cascadeFromType() / cascadeFromModel()
|
||||
// which are called from each handler. The same helpers run on
|
||||
// initial load so behaviour is identical whether the user picked the
|
||||
// value or it came from a saved node.
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) {
|
||||
const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
|
||||
const categories = menuAsset.categories || {};
|
||||
@@ -316,11 +340,17 @@ class AssetMenu {
|
||||
unit: document.getElementById('node-input-unit')
|
||||
};
|
||||
|
||||
function populate(selectEl, items = [], selectedValue, mapFn, placeholderText = 'Select...') {
|
||||
const previous = selectEl.value;
|
||||
// populate(): rebuild a <select> with a placeholder + items.
|
||||
// No change-event dispatch — cascading is done explicitly by the
|
||||
// caller via cascadeFrom*() so the order of operations is
|
||||
// predictable.
|
||||
function populate(selectEl, items, selectedValue, mapFn, placeholderText) {
|
||||
if (!selectEl) return;
|
||||
if (!Array.isArray(items)) items = [];
|
||||
if (!placeholderText) placeholderText = 'Select...';
|
||||
const mapper = typeof mapFn === 'function'
|
||||
? mapFn
|
||||
: (value) => ({ value, label: value });
|
||||
: (value) => ({ value: value, label: value });
|
||||
|
||||
selectEl.innerHTML = '';
|
||||
|
||||
@@ -331,11 +361,9 @@ class AssetMenu {
|
||||
placeholder.selected = true;
|
||||
selectEl.appendChild(placeholder);
|
||||
|
||||
items.forEach((item) => {
|
||||
items.forEach(function (item) {
|
||||
const option = mapper(item);
|
||||
if (!option || typeof option.value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (!option || typeof option.value === 'undefined') return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
@@ -344,111 +372,112 @@ class AssetMenu {
|
||||
|
||||
if (selectedValue) {
|
||||
selectEl.value = selectedValue;
|
||||
if (!selectEl.value) {
|
||||
selectEl.value = '';
|
||||
}
|
||||
if (!selectEl.value) selectEl.value = '';
|
||||
} else {
|
||||
selectEl.value = '';
|
||||
}
|
||||
if (selectEl.value !== previous) {
|
||||
selectEl.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
const resolveCategoryKey = () => {
|
||||
if (node.softwareType && categories[node.softwareType]) {
|
||||
return node.softwareType;
|
||||
}
|
||||
if (node.category && categories[node.category]) {
|
||||
return node.category;
|
||||
}
|
||||
function resolveCategoryKey() {
|
||||
if (node.softwareType && categories[node.softwareType]) return node.softwareType;
|
||||
if (node.category && categories[node.category]) return node.category;
|
||||
return defaultCategory;
|
||||
};
|
||||
|
||||
const getActiveCategory = () => {
|
||||
}
|
||||
function getActiveCategory() {
|
||||
const key = resolveCategoryKey();
|
||||
return key ? categories[key] : null;
|
||||
};
|
||||
}
|
||||
|
||||
node.category = resolveCategoryKey();
|
||||
|
||||
elems.supplier.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => String(item.id || item.name) === String(elems.supplier.value)
|
||||
)
|
||||
: null;
|
||||
// Lookup helpers — read from the *currently selected* values in the
|
||||
// DOM, not from node.* (which may not yet be in sync).
|
||||
function findSupplier() {
|
||||
const cat = getActiveCategory();
|
||||
if (!cat || !Array.isArray(cat.suppliers)) return null;
|
||||
const id = String(elems.supplier.value);
|
||||
return cat.suppliers.find(function (s) {
|
||||
return String(s.id || s.name) === id;
|
||||
}) || null;
|
||||
}
|
||||
function findType(supplier) {
|
||||
if (!supplier || !Array.isArray(supplier.types)) return null;
|
||||
const id = String(elems.type.value);
|
||||
return supplier.types.find(function (t) {
|
||||
return String(t.id || t.name) === id;
|
||||
}) || null;
|
||||
}
|
||||
function findModel(type) {
|
||||
if (!type || !Array.isArray(type.models)) return null;
|
||||
const id = String(elems.model.value);
|
||||
return type.models.find(function (m) {
|
||||
return String(m.id || m.name) === id;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
// === Cascade rebuild functions ==========================
|
||||
// Each one rebuilds the dropdown for the *level it owns* plus all
|
||||
// levels below it, using the current values in the DOM. Called by
|
||||
// the corresponding change handler AND by initial load so both
|
||||
// paths produce identical state.
|
||||
|
||||
function cascadeFromSupplier() {
|
||||
const supplier = findSupplier();
|
||||
const types = supplier ? supplier.types : [];
|
||||
populate(
|
||||
elems.type,
|
||||
types,
|
||||
node.assetType,
|
||||
(type) => ({ value: type.id || type.name, label: type.name }),
|
||||
function (t) { return { value: t.id || t.name, label: t.name }; },
|
||||
supplier ? 'Select...' : 'Awaiting Supplier Selection'
|
||||
);
|
||||
node.modelMetadata = null;
|
||||
populate(elems.model, [], '', undefined, 'Awaiting Type Selection');
|
||||
populate(elems.unit, [], '', undefined, 'Awaiting Type Selection');
|
||||
});
|
||||
// After repopulating type, propagate down. cascadeFromType()
|
||||
// will read the new elems.type.value (which was set by populate
|
||||
// to either the saved node.assetType or '') and rebuild model.
|
||||
cascadeFromType();
|
||||
}
|
||||
|
||||
elems.type.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => String(item.id || item.name) === String(elems.supplier.value)
|
||||
)
|
||||
: null;
|
||||
const type = supplier
|
||||
? supplier.types.find(
|
||||
(item) => String(item.id || item.name) === String(elems.type.value)
|
||||
)
|
||||
: null;
|
||||
function cascadeFromType() {
|
||||
const supplier = findSupplier();
|
||||
const type = findType(supplier);
|
||||
const models = type ? type.models : [];
|
||||
populate(
|
||||
elems.model,
|
||||
models,
|
||||
node.model,
|
||||
(model) => ({ value: model.id || model.name, label: model.name }),
|
||||
function (m) { return { value: m.id || m.name, label: m.name }; },
|
||||
type ? 'Select...' : 'Awaiting Type Selection'
|
||||
);
|
||||
node.modelMetadata = null;
|
||||
populate(
|
||||
elems.unit,
|
||||
[],
|
||||
'',
|
||||
undefined,
|
||||
type ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
|
||||
);
|
||||
});
|
||||
cascadeFromModel();
|
||||
}
|
||||
|
||||
elems.model.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => String(item.id || item.name) === String(elems.supplier.value)
|
||||
)
|
||||
: null;
|
||||
const type = supplier
|
||||
? supplier.types.find(
|
||||
(item) => String(item.id || item.name) === String(elems.type.value)
|
||||
)
|
||||
: null;
|
||||
const model = type
|
||||
? type.models.find(
|
||||
(item) => String(item.id || item.name) === String(elems.model.value)
|
||||
)
|
||||
: null;
|
||||
function cascadeFromModel() {
|
||||
const supplier = findSupplier();
|
||||
const type = findType(supplier);
|
||||
const model = findModel(type);
|
||||
node.modelMetadata = model;
|
||||
node.modelName = model ? model.name : '';
|
||||
populate(
|
||||
elems.unit,
|
||||
model ? model.units || [] : [],
|
||||
model ? (model.units || []) : [],
|
||||
node.unit,
|
||||
(unit) => ({ value: unit, label: unit }),
|
||||
model ? 'Select...' : type ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
|
||||
function (u) { return { value: u, label: u }; },
|
||||
model ? 'Select...' : (type ? 'Awaiting Model Selection' : 'Awaiting Type Selection')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
elems.supplier.addEventListener('change', cascadeFromSupplier);
|
||||
elems.type.addEventListener('change', cascadeFromType);
|
||||
elems.model.addEventListener('change', cascadeFromModel);
|
||||
|
||||
// Expose the cascades so loadData() (or future code) can re-run
|
||||
// them after async data arrives without duplicating logic.
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu._cascade = {
|
||||
fromSupplier: cascadeFromSupplier,
|
||||
fromType: cascadeFromType,
|
||||
fromModel: cascadeFromModel,
|
||||
};
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user