Skip to content

Commit 4d96e23

Browse files
author
FolderView Plus Test
committed
Fix VM detail row placement in folders
1 parent 6f6aaff commit 4d96e23

2 files changed

Lines changed: 136 additions & 2 deletions

File tree

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/vm.js

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,56 @@ const readFolderOwnerFromRow = (row) => {
685685
}
686686
return '';
687687
};
688+
const isVmFolderMemberPrimaryRow = (row) => !!(row && row.nodeType === 1 && row.matches('tr.fv-vm-member-row'));
689+
const isVmFolderBoundaryRow = (row) => !!(row && row.nodeType === 1 && (row.matches('tr.folder') || isVmFolderMemberPrimaryRow(row)));
690+
const collectVmMemberRowGroup = (row) => {
691+
if (!isVmFolderMemberPrimaryRow(row)) {
692+
return [];
693+
}
694+
const group = [row];
695+
let cursor = row.nextElementSibling;
696+
while (cursor && !isVmFolderBoundaryRow(cursor)) {
697+
group.push(cursor);
698+
cursor = cursor.nextElementSibling;
699+
}
700+
return group;
701+
};
702+
const applyVmMemberRowGroupOwnership = (row, folderId) => {
703+
const id = String(folderId || '').trim();
704+
if (!id || !isVmFolderMemberPrimaryRow(row)) {
705+
return [];
706+
}
707+
const ownerClass = `folder-${id}-element`;
708+
const group = collectVmMemberRowGroup(row);
709+
group.forEach((entry, index) => {
710+
if (!entry || entry.nodeType !== 1) {
711+
return;
712+
}
713+
entry.classList.add(ownerClass);
714+
if (index > 0) {
715+
entry.classList.add('fv-vm-member-detail-row');
716+
}
717+
});
718+
return group;
719+
};
720+
const placeVmManagedDetailRowsAfterOwner = (ownerRow, detailRows, folderId) => {
721+
if (!isVmFolderMemberPrimaryRow(ownerRow)) {
722+
return;
723+
}
724+
const id = String(folderId || readFolderOwnerFromRow(ownerRow) || '').trim();
725+
if (!id) {
726+
return;
727+
}
728+
let insertAfter = collectVmMemberRowGroup(ownerRow).slice(-1)[0] || ownerRow;
729+
for (const detailRow of detailRows) {
730+
if (!detailRow || detailRow.nodeType !== 1 || isVmFolderBoundaryRow(detailRow)) {
731+
continue;
732+
}
733+
detailRow.classList.add(`folder-${id}-element`, 'fv-vm-member-detail-row');
734+
$(insertAfter).after(detailRow);
735+
insertAfter = detailRow;
736+
}
737+
};
688738
const getFocusedFolderVisibleSet = (folderId) => {
689739
const id = String(folderId || '').trim();
690740
if (!id || !globalFolders[id]) {
@@ -1101,6 +1151,73 @@ const hideVmRuntimeLoadingRow = () => {
11011151
$('tbody#kvm_list tr.fv-runtime-loading-row').remove();
11021152
};
11031153

1154+
const rememberPendingVmDetailOwner = (row) => {
1155+
if (!isVmFolderMemberPrimaryRow(row)) {
1156+
vmPendingExpandedDetailOwner = null;
1157+
return;
1158+
}
1159+
vmPendingExpandedDetailOwner = {
1160+
row,
1161+
folderId: readFolderOwnerFromRow(row),
1162+
expiresAt: Date.now() + 1500
1163+
};
1164+
};
1165+
1166+
const readPendingVmDetailOwner = () => {
1167+
if (!vmPendingExpandedDetailOwner) {
1168+
return null;
1169+
}
1170+
if (vmPendingExpandedDetailOwner.expiresAt < Date.now()) {
1171+
vmPendingExpandedDetailOwner = null;
1172+
return null;
1173+
}
1174+
const row = vmPendingExpandedDetailOwner.row;
1175+
if (!isVmFolderMemberPrimaryRow(row) || !document.body.contains(row)) {
1176+
vmPendingExpandedDetailOwner = null;
1177+
return null;
1178+
}
1179+
return vmPendingExpandedDetailOwner;
1180+
};
1181+
1182+
const ensureVmFolderDetailObserver = () => {
1183+
if (vmFolderDetailObserver || typeof MutationObserver !== 'function') {
1184+
return;
1185+
}
1186+
const tbody = document.querySelector('tbody#kvm_list');
1187+
if (!tbody) {
1188+
return;
1189+
}
1190+
$(tbody)
1191+
.off('click.fvVmMemberDetailOwner')
1192+
.on('click.fvVmMemberDetailOwner', 'tr.fv-vm-member-row td.vm-name, tr.fv-vm-member-row td.vm-name a, tr.fv-vm-member-row td.vm-name button, tr.fv-vm-member-row td.vm-name span', function() {
1193+
rememberPendingVmDetailOwner(this.closest('tr'));
1194+
});
1195+
vmFolderDetailObserver = new MutationObserver((mutations) => {
1196+
const pendingOwner = readPendingVmDetailOwner();
1197+
if (!pendingOwner || !pendingOwner.folderId) {
1198+
return;
1199+
}
1200+
const addedRows = [];
1201+
for (const mutation of mutations) {
1202+
for (const node of mutation.addedNodes || []) {
1203+
if (!node || node.nodeType !== 1 || node.tagName !== 'TR') {
1204+
continue;
1205+
}
1206+
if (isVmFolderBoundaryRow(node) || readFolderOwnerFromRow(node)) {
1207+
continue;
1208+
}
1209+
addedRows.push(node);
1210+
}
1211+
}
1212+
if (!addedRows.length) {
1213+
return;
1214+
}
1215+
placeVmManagedDetailRowsAfterOwner(pendingOwner.row, addedRows, pendingOwner.folderId);
1216+
vmPendingExpandedDetailOwner = null;
1217+
});
1218+
vmFolderDetailObserver.observe(tbody, { childList: true });
1219+
};
1220+
11041221
let createFoldersInFlight = false;
11051222
let createFoldersQueued = false;
11061223

@@ -1112,6 +1229,7 @@ const createFolders = async () => {
11121229
showVmRuntimeLoadingRow();
11131230
setVmFatalBannerPhase('bootstrap-data');
11141231
try {
1232+
ensureVmFolderDetailObserver();
11151233
ensureVmExpandedStateLifecycleHooks();
11161234
markVmFatalBannerStep('VM runtime lifecycle hooks ready');
11171235
persistVmExpandedStateFromDom();
@@ -1557,7 +1675,9 @@ const createFolder = (folder, id, position, order, vmInfo, foldersDone, matchCac
15571675
let $vmTR = $('#kvm_list > tr.sortable').filter(function() {
15581676
return $(this).find('td.vm-name span.outer span.inner a').first().text().trim() === container;
15591677
}).first();
1560-
$(`tr.folder-id-${id} div.folder-storage`).append($vmTR.addClass(`folder-${id}-element`).addClass(`folder-element`).removeClass('sortable'));
1678+
$vmTR.addClass('fv-vm-member-row').addClass(`folder-${id}-element`).addClass('folder-element').removeClass('sortable');
1679+
const vmRowGroup = applyVmMemberRowGroupOwnership($vmTR.get(0), id);
1680+
$(`tr.folder-id-${id} div.folder-storage`).append(vmRowGroup.length ? $(vmRowGroup) : $vmTR);
15611681

15621682
if(folderDebugMode) {
15631683
vmDebugLog(`${newFolder[container].id}(${offsetIndex}, ${index}) => ${id}`);
@@ -2689,6 +2809,8 @@ let folderDebugMode = false;
26892809
let folderDebugModeWindow = [];
26902810
let folderReq = [];
26912811
let folderTypePrefs = utils.normalizePrefs({});
2812+
let vmPendingExpandedDetailOwner = null;
2813+
let vmFolderDetailObserver = null;
26922814
let liveRefreshTimer = null;
26932815
let liveRefreshMs = 0;
26942816
let liveRefreshInFlight = false;

tests/vm-runtime-docker-parity-guard.test.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ test('vm runtime includes docker-parity quick state actions and branch-aware fol
2222
assert.match(vmJs, /const cloneVmFolderFromMenu = async/);
2323
});
2424

25+
test('vm runtime keeps native expanded detail rows attached to their folder-managed VM rows', () => {
26+
assert.match(vmJs, /const collectVmMemberRowGroup = \(row\) => \{/);
27+
assert.match(vmJs, /const applyVmMemberRowGroupOwnership = \(row,\s*folderId\) => \{/);
28+
assert.match(vmJs, /const placeVmManagedDetailRowsAfterOwner = \(ownerRow,\s*detailRows,\s*folderId\) => \{/);
29+
assert.match(vmJs, /let vmPendingExpandedDetailOwner = null;/);
30+
assert.match(vmJs, /let vmFolderDetailObserver = null;/);
31+
assert.match(vmJs, /const ensureVmFolderDetailObserver = \(\) => \{/);
32+
assert.match(vmJs, /rememberPendingVmDetailOwner\(this\.closest\('tr'\)\);/);
33+
assert.match(vmJs, /placeVmManagedDetailRowsAfterOwner\(pendingOwner\.row,\s*addedRows,\s*pendingOwner\.folderId\);/);
34+
assert.match(vmJs, /\$vmTR\.addClass\('fv-vm-member-row'\)/);
35+
assert.match(vmJs, /const vmRowGroup = applyVmMemberRowGroupOwnership\(\$vmTR\.get\(0\),\s*id\);/);
36+
});
37+
2538
test('vm runtime context menu keeps focus/pin/lock and clone actions in vm folder menu', () => {
2639
assert.match(vmJs, /Focus folder/);
2740
assert.match(vmJs, /Pin folder/);
@@ -38,4 +51,3 @@ test('vm css includes parity selectors for quick action row and folder quick sta
3851
assert.match(vmCss, /\.fvplus-vm-context-menu > li\.fvplus-vm-quick-item/);
3952
assert.match(vmCss, /\.fvplus-vm-context-menu > li\.fvplus-vm-quick-clear/);
4053
});
41-

0 commit comments

Comments
 (0)