Skip to content

Commit cd219f5

Browse files
Fix dashboard legacy layout switching
1 parent ef95a82 commit cd219f5

6 files changed

Lines changed: 165 additions & 43 deletions

File tree

archive/folderview.plus-2026.03.22.34.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8fd27849fe03d2b5895c3787234d9ced92b721aaabd8be01730c3f0a34e8824c folderview.plus-2026.03.23.06.txz

folderview.plus.plg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.03.23.05">
10-
<!ENTITY md5 "127b706e598c5d4fe5ae754e0be01ff0">
9+
<!ENTITY version "2026.03.23.06">
10+
<!ENTITY md5 "c717f290c3bd116b976961b6ba528534">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.03.23.06
17+
- Fix: Reworked Dashboard widget `Legacy` transitions to restore native rows locally instead of forcing a full `loadlist()` refresh, removing the extra delay when cycling in and out of bypass mode.
18+
- Fix: Prevented one widget entering `Legacy` from short-circuiting the other widget's folder render path by scoping dashboard rebuilds to the affected type.
19+
- Test: Expanded dashboard layout regression coverage for the local `Legacy` rerender path and type-scoped widget rebuilds.
20+
21+
1622
###2026.03.23.05
1723
- Feature: Added `Legacy` as a Dashboard widget layout for Docker and VM widgets so they can show native Unraid rows without FolderView Plus folder grouping.
1824
- Quality: Persisted the new `Legacy` layout across settings, runtime normalization, and manifest-backed preferences, with a safe widget reload when switching in or out of bypass mode.

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

Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,101 @@ const writeDashboardCompactDensityStateForType = (type, enabled) => {
282282
};
283283
const getDashboardStartedOnlySelectorForType = (type) => (type === 'vm' ? 'input#vms' : 'input#apps');
284284
const isDashboardLegacyLayoutForType = (type) => normalizeDashboardPrefsForType(type).layout === 'legacy';
285+
const getDashboardNativeRowSelectorForType = (type) => (
286+
type === 'vm'
287+
? 'span.outer.vms:not(.folder-vm)'
288+
: 'span.outer.apps:not(.folder-docker)'
289+
);
290+
const getDashboardNativeRowName = ($row) => String(
291+
$row?.find('span.inner').contents().first().text() || ''
292+
).trim();
293+
const stripDashboardFolderizedStateFromRow = ($row) => {
294+
if (!$row || !$row.length) {
295+
return;
296+
}
297+
$row.find('span.fv-dashboard-member-actions').remove();
298+
$row.removeClass((_, className = '') => className
299+
.split(/\s+/)
300+
.filter((token) => {
301+
if (!token) {
302+
return false;
303+
}
304+
if (token === 'folder-element-docker' || token === 'folder-element-vm' || token === 'autostart') {
305+
return true;
306+
}
307+
return /^folder-[A-Za-z0-9._-]+-element$/.test(token);
308+
})
309+
.join(' '));
310+
};
311+
const readDashboardNativeOrderSnapshotForType = async (type) => {
312+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
313+
const existingReq = Array.isArray(folderReq?.[resolvedType]) ? folderReq[resolvedType][3] : null;
314+
if (existingReq && typeof existingReq.then === 'function') {
315+
try {
316+
return Object.values(JSON.parse(await existingReq));
317+
} catch (_error) {
318+
// Fall through to a fresh fetch.
319+
}
320+
}
321+
try {
322+
const payload = await $.get(`/plugins/folderview.plus/server/read_unraid_order.php?type=${resolvedType}`).promise();
323+
return Object.values(JSON.parse(payload));
324+
} catch (_error) {
325+
return [];
326+
}
327+
};
328+
const restoreDashboardNativeRowsForType = async (type) => {
329+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
330+
const $container = resolveDashboardWidgetInlineHostForType(resolvedType).first();
331+
if (!$container.length) {
332+
return;
333+
}
334+
const selector = getDashboardNativeRowSelectorForType(resolvedType);
335+
const rowEntries = [];
336+
$container.find(selector).each((_, node) => {
337+
const $row = $(node);
338+
if ($row.closest('.fv-dashboard-layout-inline-host').length) {
339+
return;
340+
}
341+
rowEntries.push({
342+
$row,
343+
name: getDashboardNativeRowName($row)
344+
});
345+
});
346+
rowEntries.forEach((entry) => {
347+
stripDashboardFolderizedStateFromRow(entry.$row);
348+
entry.$row.detach();
349+
});
350+
$container.children('.folder-showcase-outer').remove();
351+
const orderSnapshot = await readDashboardNativeOrderSnapshotForType(resolvedType);
352+
const rowMap = new Map();
353+
const appended = new Set();
354+
for (const entry of rowEntries) {
355+
if (entry.name && !rowMap.has(entry.name)) {
356+
rowMap.set(entry.name, entry);
357+
}
358+
}
359+
for (const name of orderSnapshot) {
360+
const key = String(name || '').trim();
361+
if (!key || !rowMap.has(key) || appended.has(key)) {
362+
continue;
363+
}
364+
appended.add(key);
365+
$container.append(rowMap.get(key).$row);
366+
}
367+
for (const entry of rowEntries) {
368+
const key = String(entry.name || '').trim();
369+
if (key && appended.has(key)) {
370+
continue;
371+
}
372+
$container.append(entry.$row);
373+
}
374+
if (resolvedType === 'vm') {
375+
globalFolders.vms = {};
376+
} else {
377+
globalFolders.docker = {};
378+
}
379+
};
285380
const isDashboardStartedOnlyEnabledForType = (type) => {
286381
const selector = getDashboardStartedOnlySelectorForType(type);
287382
const $toggle = $(selector).first();
@@ -620,6 +715,19 @@ const saveDashboardLayoutPrefForType = async (type, prefsPayload) => {
620715
}).promise();
621716
return parseJsonPayloadSafe(payload);
622717
};
718+
const rerenderDashboardWidgetStructureForType = async (type) => {
719+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
720+
await restoreDashboardNativeRowsForType(resolvedType);
721+
if (isDashboardLegacyLayoutForType(resolvedType)) {
722+
scheduleDashboardLayoutApplyForType(resolvedType);
723+
syncDashboardWidgetLayoutQuickControlForType(resolvedType);
724+
scheduleDashboardWidgetVisibilitySyncForType(resolvedType, 0);
725+
return;
726+
}
727+
prepareDashboardFolderRequestsForType(resolvedType);
728+
await createFolders([resolvedType]);
729+
scheduleDashboardWidgetVisibilitySyncForType(resolvedType, 0);
730+
};
623731
const handleDashboardWidgetLayoutQuickSwitch = async (type, value) => {
624732
const resolvedType = type === 'vm' ? 'vm' : 'docker';
625733
const nextLayout = normalizeDashboardLayoutMode(value);
@@ -653,9 +761,8 @@ const handleDashboardWidgetLayoutQuickSwitch = async (type, value) => {
653761
throw new Error(response?.error || 'Failed to save dashboard preferences.');
654762
}
655763
folderTypePrefs[resolvedType] = utils.normalizePrefs(response.prefs || nextPrefs);
656-
if (requiresStructureReload && typeof window.loadlist === 'function') {
657-
loadedFolder = false;
658-
window.loadlist();
764+
if (requiresStructureReload) {
765+
await rerenderDashboardWidgetStructureForType(resolvedType);
659766
return;
660767
}
661768
scheduleDashboardLayoutApplyForType(resolvedType);
@@ -1368,13 +1475,17 @@ let createFoldersQueued = false;
13681475
/**
13691476
* Handles the creation of all folders
13701477
*/
1371-
const createFolders = async () => {
1478+
const createFolders = async (types = ['docker', 'vm']) => {
1479+
const renderTypes = new Set(
1480+
(Array.isArray(types) ? types : [types])
1481+
.map((type) => (type === 'vm' ? 'vm' : 'docker'))
1482+
);
13721483
// ########################################
13731484
// ########## DOCKER ##########
13741485
// ########################################
13751486

13761487
// if docker is enabled
1377-
if($('tbody#docker_view').length > 0) {
1488+
if (renderTypes.has('docker') && $('tbody#docker_view').length > 0) {
13781489
showDashboardRuntimeLoadingRow('docker');
13791490
try {
13801491
let prom = await Promise.all(folderReq.docker);
@@ -1402,9 +1513,7 @@ const createFolders = async () => {
14021513
globalFolders.docker = {};
14031514
scheduleDashboardLayoutApplyForType('docker');
14041515
syncDashboardWidgetLayoutQuickControlForType('docker');
1405-
return;
1406-
}
1407-
1516+
} else {
14081517
// Filter the order to get the container that aren't in the order, this happen when a new container is created
14091518
let newOnes = order.filter(x => !unraidOrder.includes(x));
14101519

@@ -1569,6 +1678,7 @@ const createFolders = async () => {
15691678
// Assing the folder done to the global object
15701679
globalFolders.docker = foldersDone;
15711680
scheduleDashboardLayoutApplyForType('docker');
1681+
}
15721682
} finally {
15731683
hideDashboardRuntimeLoadingRow('docker');
15741684
}
@@ -1580,7 +1690,7 @@ const createFolders = async () => {
15801690
// ########################################
15811691

15821692
// if vm is enabled
1583-
if($('tbody#vm_view').length > 0) {
1693+
if (renderTypes.has('vm') && $('tbody#vm_view').length > 0) {
15841694
showDashboardRuntimeLoadingRow('vm');
15851695
try {
15861696
const prom = await Promise.all(folderReq.vm);
@@ -1608,9 +1718,7 @@ const createFolders = async () => {
16081718
globalFolders.vms = {};
16091719
scheduleDashboardLayoutApplyForType('vm');
16101720
syncDashboardWidgetLayoutQuickControlForType('vm');
1611-
return;
1612-
}
1613-
1721+
} else {
16141722
// Filter the webui order to get the container that aren't in the order, this happen when a new container is created
16151723
let newOnes = order.filter(x => !unraidOrder.includes(x));
16161724

@@ -1774,6 +1882,7 @@ const createFolders = async () => {
17741882

17751883
globalFolders.vms = foldersDone;
17761884
scheduleDashboardLayoutApplyForType('vm');
1885+
}
17771886
} finally {
17781887
hideDashboardRuntimeLoadingRow('vm');
17791888
}
@@ -3401,44 +3510,45 @@ const queueCreateFoldersRender = () => {
34013510
}
34023511
});
34033512
};
3404-
3405-
// Patching the original function to make sure the containers are rendered before insering the folder
3406-
window.loadlist_original = loadlist;
3407-
window.loadlist = (x) => {
3408-
loadedFolder = false;
3409-
if($('tbody#docker_view').length > 0) {
3513+
const prepareDashboardFolderRequestsForType = (type) => {
3514+
const resolvedType = type === 'vm' ? 'vm' : 'docker';
3515+
const hasWidget = resolvedType === 'vm'
3516+
? $('tbody#vm_view').length > 0
3517+
: $('tbody#docker_view').length > 0;
3518+
if (!hasWidget) {
3519+
folderReq[resolvedType] = [];
3520+
return [];
3521+
}
3522+
if (resolvedType === 'docker') {
34103523
const safeDockerPrefsReq = $.get('/plugins/folderview.plus/server/prefs.php?type=docker')
34113524
.then((data) => data, () => JSON.stringify({ ok: false, prefs: {} }));
34123525
folderReq.docker = [
3413-
// Get the folders
34143526
$.get('/plugins/folderview.plus/server/read.php?type=docker').promise(),
3415-
// Get the order as unraid sees it
34163527
$.get('/plugins/folderview.plus/server/read_order.php?type=docker').promise(),
3417-
// Get the info on containers, needed for autostart, update and started
34183528
$.get('/plugins/folderview.plus/server/read_info.php?type=docker').promise(),
3419-
// Get the order that is shown in the webui
34203529
$.get('/plugins/folderview.plus/server/read_unraid_order.php?type=docker').promise(),
3421-
// Get sort and auto-assignment preferences
34223530
safeDockerPrefsReq
34233531
];
3532+
return folderReq.docker;
34243533
}
3534+
const safeVmPrefsReq = $.get('/plugins/folderview.plus/server/prefs.php?type=vm')
3535+
.then((data) => data, () => JSON.stringify({ ok: false, prefs: {} }));
3536+
folderReq.vm = [
3537+
$.get('/plugins/folderview.plus/server/read.php?type=vm').promise(),
3538+
$.get('/plugins/folderview.plus/server/read_order.php?type=vm').promise(),
3539+
$.get('/plugins/folderview.plus/server/read_info.php?type=vm').promise(),
3540+
$.get('/plugins/folderview.plus/server/read_unraid_order.php?type=vm').promise(),
3541+
safeVmPrefsReq
3542+
];
3543+
return folderReq.vm;
3544+
};
34253545

3426-
if($('tbody#vm_view').length > 0) {
3427-
const safeVmPrefsReq = $.get('/plugins/folderview.plus/server/prefs.php?type=vm')
3428-
.then((data) => data, () => JSON.stringify({ ok: false, prefs: {} }));
3429-
folderReq.vm = [
3430-
// Get the folders
3431-
$.get('/plugins/folderview.plus/server/read.php?type=vm').promise(),
3432-
// Get the order as unraid sees it
3433-
$.get('/plugins/folderview.plus/server/read_order.php?type=vm').promise(),
3434-
// Get the info on VMs, needed for autostart and started
3435-
$.get('/plugins/folderview.plus/server/read_info.php?type=vm').promise(),
3436-
// Get the order that is shown in the webui
3437-
$.get('/plugins/folderview.plus/server/read_unraid_order.php?type=vm').promise(),
3438-
// Get sort and auto-assignment preferences
3439-
safeVmPrefsReq
3440-
];
3441-
}
3546+
// Patching the original function to make sure the containers are rendered before insering the folder
3547+
window.loadlist_original = loadlist;
3548+
window.loadlist = (x) => {
3549+
loadedFolder = false;
3550+
prepareDashboardFolderRequestsForType('docker');
3551+
prepareDashboardFolderRequestsForType('vm');
34423552
loadlist_original(x);
34433553
};
34443554

tests/dashboard-layout-regression.test.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ test('server normalizes compact matrix dashboard layout', () => {
8484
test('dashboard runtime supports layout classes, accordion guards, and overflow metadata', () => {
8585
assert.match(dashboardScript, /const DASHBOARD_LAYOUT_MODES = \['classic', 'legacy', 'fullwidth', 'accordion', 'inset', 'compactmatrix'\]/);
8686
assert.match(dashboardScript, /const isDashboardLegacyLayoutForType = \(type\) =>/);
87+
assert.match(dashboardScript, /const restoreDashboardNativeRowsForType = async \(type\) =>/);
88+
assert.match(dashboardScript, /const rerenderDashboardWidgetStructureForType = async \(type\) =>/);
89+
assert.match(dashboardScript, /const prepareDashboardFolderRequestsForType = \(type\) =>/);
8790
assert.match(dashboardScript, /const ensureDashboardWidgetLayoutQuickSwitchForType = \(type\) =>/);
8891
assert.match(dashboardScript, /const resolveDashboardWidgetInlineHostForType = \(type\) =>/);
8992
assert.match(dashboardScript, /const isDashboardWidgetCollapsedForType = \(type\) =>/);
@@ -114,10 +117,13 @@ test('dashboard runtime supports layout classes, accordion guards, and overflow
114117
assert.match(dashboardScript, /prefsResponse = parseJsonPayloadSafe\(prom\[4\]\);/);
115118
assert.match(dashboardScript, /ensureQuickAction\('layout-cycle', 'fa-columns', 'Cycle layout view', 'fv-dashboard-layout-quick'\)/);
116119
assert.match(dashboardScript, /const normalizeDashboardOverflowMode = \(value\) =>/);
120+
assert.match(dashboardScript, /const createFolders = async \(types = \['docker', 'vm'\]\) =>/);
121+
assert.match(dashboardScript, /if \(renderTypes\.has\('docker'\) && \$\('tbody#docker_view'\)\.length > 0\) \{/);
122+
assert.match(dashboardScript, /if \(renderTypes\.has\('vm'\) && \$\('tbody#vm_view'\)\.length > 0\) \{/);
117123
assert.match(dashboardScript, /const applyDashboardLayoutStateForType = \(type\) =>/);
118124
assert.match(dashboardScript, /const scheduleDashboardLayoutApplyForType = \(type\) =>/);
119125
assert.match(dashboardScript, /const requiresStructureReload = previousDashboard\.layout === 'legacy' \|\| nextLayout === 'legacy';/);
120-
assert.match(dashboardScript, /if \(requiresStructureReload && typeof window\.loadlist === 'function'\) \{/);
126+
assert.match(dashboardScript, /await rerenderDashboardWidgetStructureForType\(resolvedType\);/);
121127
assert.match(dashboardScript, /if \(isDashboardLegacyLayoutForType\('docker'\)\) \{/);
122128
assert.match(dashboardScript, /if \(isDashboardLegacyLayoutForType\('vm'\)\) \{/);
123129
assert.match(dashboardScript, /\|\| isDashboardLegacyLayoutForType\(resolvedType\)/);

0 commit comments

Comments
 (0)