Skip to content

Commit a4e5078

Browse files
Merge dev into main for 2026.03.30.19 release
2 parents d203205 + dd2bb55 commit a4e5078

9 files changed

Lines changed: 42 additions & 32 deletions

archive/folderview.plus-2026.03.29.19.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+
5cadfaa0509cb1d395137b3fe1533148619b7b4611ae3071bd963eaa4a0b612d folderview.plus-2026.03.30.19.txz

folderview.plus.plg

Lines changed: 8 additions & 3 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;/main/folderview.plus.plg">
9-
<!ENTITY version "2026.03.30.18">
10-
<!ENTITY md5 "f97e5b9f93849f7058f56b986536b224">
9+
<!ENTITY version "2026.03.30.19">
10+
<!ENTITY md5 "b40c1ce2317f502819d859321fabe642">
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.30.19
17+
- Fix: Resolve the remaining Docker page double-reload path during initial bootstrap when Unraid refreshes the staged request bundle while FolderView Plus is still rendering folders.
18+
- Fix: Replay queued Docker folder renders in place against the newest staged request bundle instead of forcing a second host `loadlist()` cycle after the page has already refreshed.
19+
- UX: Suppress the extra Docker runtime loading shell during that in-place replay so the Docker page no longer flashes a second reload.
20+
- Regression guard: Cover the primed-request `listview()`/`loadlist()` bootstrap path and the deferred runtime hydration path so Docker refreshes stay in-place.
21+
1622
###2026.03.30.18
1723
- Feature: Add optional per-folder `Accent color` support across Docker folders, VM folders, Dashboard cards, and the folder editor live preview, including smart-default and template inheritance plus the consolidated Accent card and live swatch UX.
1824
- Fix: Remove the extra Docker reload cycle during deferred runtime hydration by updating preview actions and tooltip payloads in place, suppressing the duplicate FolderView Plus loading shell, and routing runtime info reads through no-cache helpers so fresh state and WebUI metadata stay in sync.
@@ -23,7 +29,6 @@
2329
- Fix: Stop Docker advanced preview context popups configured for click from opening on the first hover interaction.
2430
- Quality: Add the dev version-bump push guard and expand regression coverage for hydration, accent color, folder actions, layout, and tooltip trigger mode changes.
2531

26-
2732
###2026.03.30.17
2833
- Fix: Stop Docker advanced preview context popups configured for click from eager-opening on the first hover interaction.
2934
- Quality: Trim the lazy tooltip trigger-mode fix so the Docker runtime stays inside the repo perf budget while preserving hover-mode eager-open behavior.

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

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,18 +2812,6 @@ const syncDockerVisibleFoldersFromRuntimeCache = () => {
28122812
applyDockerFocusedFolderState();
28132813
};
28142814

2815-
const buildDockerWebuiSignature = (source) => {
2816-
const map = source && typeof source === 'object' ? source : {};
2817-
const names = Object.keys(map).sort((a, b) => a.localeCompare(b));
2818-
if (!names.length) {
2819-
return '';
2820-
}
2821-
return names.map((name) => {
2822-
const state = map[name]?.info?.State && typeof map[name].info.State === 'object' ? map[name].info.State : {};
2823-
return `${name}:${getSafeWebuiUrl(state.WebUi)}|${getSafeWebuiUrl(state.TSWebUi)}|${state.Updated === false ? 'u' : (state.Updated === true ? 'c' : '?')}`;
2824-
}).join('|');
2825-
};
2826-
28272815
const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, fullInfoPromise = null) => {
28282816
const requestPromise = fullInfoPromise && typeof fullInfoPromise.then === 'function'
28292817
? fullInfoPromise
@@ -2846,18 +2834,12 @@ const queueDockerDeferredRuntimeInfoHydration = (generation, stateSignature, ful
28462834
if (!parsed || Object.keys(parsed).length <= 0) {
28472835
return;
28482836
}
2849-
const previousWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
28502837
dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap(parsed, dockerRuntimeInfoByName);
2851-
const nextWebuiSignature = buildDockerWebuiSignature(dockerRuntimeInfoByName);
28522838
if (stateSignature) {
28532839
lastLiveRefreshStateSignature = stateSignature;
28542840
}
28552841
markDockerFatalBannerStep('Docker runtime details hydrated');
28562842
recordDockerFatalBannerAction('Docker runtime details hydrated');
2857-
if (previousWebuiSignature !== nextWebuiSignature) {
2858-
queueLoadlistRefresh();
2859-
return;
2860-
}
28612843
syncDockerVisibleFoldersFromRuntimeCache();
28622844
})
28632845
.catch(() => {});
@@ -3185,7 +3167,10 @@ const queueCreateFoldersRender = () => {
31853167
createFoldersInFlight = false;
31863168
if (createFoldersQueued) {
31873169
createFoldersQueued = false;
3188-
queueLoadlistRefresh();
3170+
// If Unraid queued a newer request bundle mid-render, replay FolderView Plus
3171+
// against the current DOM instead of forcing another host loadlist() cycle.
3172+
nextDockerRenderSuppressLoadingUi = true;
3173+
queueCreateFoldersRender();
31893174
}
31903175
});
31913176
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
const repoRoot = path.resolve(process.cwd());
7+
const dockerJs = fs.readFileSync(
8+
path.join(repoRoot, 'src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js'),
9+
'utf8'
10+
);
11+
12+
test('docker queued bootstrap renders replay in place instead of forcing a second host loadlist', () => {
13+
assert.match(dockerJs, /\/\/ Prime requests for environments where loadlist isn't called first\.\s*folderReq = buildDockerFolderReq\(\);/);
14+
assert.match(dockerJs, /window\.loadlist = \(\) => \{[\s\S]*?loadedFolder = false;[\s\S]*?folderReq = buildDockerFolderReq\(\);/);
15+
assert.match(dockerJs, /window\.listview = \(\) => \{[\s\S]*?if \(!loadedFolder\) \{[\s\S]*?queueCreateFoldersRender\(\);[\s\S]*?loadedFolder = true;/);
16+
assert.match(dockerJs, /const queueCreateFoldersRender = \(\) => \{[\s\S]*?if \(createFoldersQueued\) \{\s*createFoldersQueued = false;[\s\S]*?nextDockerRenderSuppressLoadingUi = true;\s*queueCreateFoldersRender\(\);\s*\}[\s\S]*?\};/);
17+
assert.doesNotMatch(dockerJs, /const queueCreateFoldersRender = \(\) => \{[\s\S]*?if \(createFoldersQueued\) \{\s*createFoldersQueued = false;\s*queueLoadlistRefresh\(\);\s*\}[\s\S]*?\};/);
18+
});

tests/docker-folder-row-quick-actions.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ test('docker hydration refreshes existing preview actions in place instead of re
3939
assert.match(dockerScript, /const syncDockerLeafFolderPreviewActions = \(id,\s*folder,\s*runtimeContainers\) =>/);
4040
assert.match(dockerScript, /syncDockerLeafFolderPreviewActions\(id,\s*folder,\s*runtimeContainers\);/);
4141
assert.match(dockerScript, /\$preview\.find\('\[id\^="folder-preview-"\]'\)\.each\(\(_,\s*node\) => \{\s*\$\(node\)\.data\('fvTooltipLazyBuilt', false\);/s);
42+
assert.match(dockerScript, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?syncDockerVisibleFoldersFromRuntimeCache\(\);[\s\S]*?\}\)\s*\.catch\(\(\) => \{\}\);/);
43+
assert.doesNotMatch(dockerScript, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const previousWebuiSignature/);
4244
});
4345

4446
test('docker hydration refresh updates collapsed folder update columns from runtime cache', () => {

tests/docker-update-status-regression.test.mjs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ test('docker runtime preserves hydrated update flags when normalizing partial ru
1313
assert.match(dockerJs, /Updated:\s*sourceState\.Updated \?\? previousState\.Updated \?\? null/);
1414
});
1515

16-
test('deferred docker runtime hydration rerenders when update availability changes', () => {
17-
assert.match(dockerJs, /\|\$\{state\.Updated === false \? 'u' : \(state\.Updated === true \? 'c' : '\?'\)\}/);
18-
assert.match(dockerJs, /const previousWebuiSignature = buildDockerWebuiSignature\(dockerRuntimeInfoByName\);/);
19-
assert.match(dockerJs, /const nextWebuiSignature = buildDockerWebuiSignature\(dockerRuntimeInfoByName\);/);
20-
assert.match(dockerJs, /previousWebuiSignature !== nextWebuiSignature/);
21-
assert.match(dockerJs, /queueLoadlistRefresh\(\);\s*return;/);
16+
test('deferred docker runtime hydration refreshes visible folder state in place instead of reloading the page', () => {
17+
assert.match(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap\(parsed,\s*dockerRuntimeInfoByName\);[\s\S]*?markDockerFatalBannerStep\('Docker runtime details hydrated'\);[\s\S]*?recordDockerFatalBannerAction\('Docker runtime details hydrated'\);[\s\S]*?syncDockerVisibleFoldersFromRuntimeCache\(\);[\s\S]*?\}\)\s*\.catch\(\(\) => \{\}\);/);
18+
assert.doesNotMatch(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const previousWebuiSignature/);
19+
assert.doesNotMatch(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) => \{[\s\S]*?const nextWebuiSignature/);
2220
});
2321

2422
test('folder update-column renderer is reused across initial and synced folder state', () => {

tests/performance-optimizations.test.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,15 @@ test('runtime refresh uses lightweight state mode checks before re-rendering', (
126126
assert.match(dockerJs, /const queueCreateFoldersRender = \(\) =>/);
127127
assert.match(vmJs, /const queueCreateFoldersRender = \(\) =>/);
128128
assert.match(dashboardJs, /const queueCreateFoldersRender = \(\) =>/);
129+
assert.match(dockerJs, /if \(createFoldersQueued\) \{\s*createFoldersQueued = false;[\s\S]*?nextDockerRenderSuppressLoadingUi = true;\s*queueCreateFoldersRender\(\);\s*\}/s);
130+
assert.doesNotMatch(dockerJs, /if \(createFoldersQueued\) \{\s*createFoldersQueued = false;\s*queueLoadlistRefresh\(\);\s*\}/s);
129131
assert.match(dockerJs, /const readDockerHostOrderFromDom = \(\) =>/);
130132
assert.match(dockerJs, /const queueDockerDeferredRuntimeInfoHydration = \(generation,\s*stateSignature,\s*fullInfoPromise = null\) =>/);
131133
assert.match(dockerJs, /let dockerHostLoadOwnsLoadingUi = false;/);
132134
assert.match(dockerJs, /const shouldSuppressDockerRuntimeLoadingUi = \(\) => dockerHostLoadOwnsLoadingUi \|\| nextDockerRenderSuppressLoadingUi \|\| activeDockerRenderSuppressLoadingUi;/);
133-
assert.match(dockerJs, /const buildDockerWebuiSignature = \(source\) =>/);
134-
assert.match(dockerJs, /\|\$\{state\.Updated === false \? 'u' : \(state\.Updated === true \? 'c' : '\?'\)\}/);
135-
assert.match(dockerJs, /if \(previousWebuiSignature !== nextWebuiSignature\) \{\s*queueLoadlistRefresh\(\);\s*return;\s*\}/s);
135+
assert.match(dockerJs, /dockerRuntimeInfoByName = normalizeDockerRuntimeInfoMap\(parsed,\s*dockerRuntimeInfoByName\);[\s\S]*syncDockerVisibleFoldersFromRuntimeCache\(\);/);
136+
assert.doesNotMatch(dockerJs, /const buildDockerWebuiSignature = \(source\) =>/);
137+
assert.doesNotMatch(dockerJs, /if \(previousWebuiSignature !== nextWebuiSignature\) \{\s*queueLoadlistRefresh\(\);\s*return;\s*\}/s);
136138
assert.doesNotMatch(dockerJs, /applyDockerPinnedFolderIds\(Array\.isArray\(response\?\.prefs\?\.pinnedFolderIds\) \? response\.prefs\.pinnedFolderIds : nextPinned\);\s*syncDockerPinnedFolderUi\(\);\s*queueLoadlistRefresh\(/s);
137139
assert.match(dockerJs, /const yieldDockerRenderLoop = async \(processedCount,\s*totalCount\) =>/);
138140
assert.match(dockerJs, /dockerHostLoadOwnsLoadingUi = true;\s*if \(FOLDER_VIEW_DEBUG_MODE\) console\.log\('\[FV3_DEBUG\] Patched listview: loadedFolder is false\. Queueing createFolders render\.'/);

0 commit comments

Comments
 (0)