Skip to content

Commit 5282b9e

Browse files
Fix wizard smart-assign and improve setup result dialogs
1 parent f53530c commit 5282b9e

7 files changed

Lines changed: 136 additions & 20 deletions

File tree

archive/folderview.plus-2026.03.21.07.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+
03dd6c7b905ff28f85774baddbad3b2b636ecca970130516614aa4da972c0d43 folderview.plus-2026.03.21.36.txz

folderview.plus.plg

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
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.21.35">
10-
<!ENTITY md5 "112679bd972e73656eccb2b89967fd9d">
9+
<!ENTITY version "2026.03.21.36">
10+
<!ENTITY md5 "569ac9456eac5448c1484a050aa5f0f7">
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.21.36
17+
- Fix: Restored wizard template smart-assign execution by exporting `utils.normalizeFolderMembers`, so detected Docker/VM items move into newly created starter folders.
18+
- UX: Redesigned the setup apply confirmation modal with structured summary rows, warning blocks, and clearer apply intent.
19+
- UX: Redesigned the partial-failure modal with grouped outcomes and a focused failed-task list to make retry decisions clearer.
20+
- Quality: Added a utils contract test to prevent regression where member normalization helpers are used but not exported.
21+
22+
1623
###2026.03.21.35
1724
- UX: Rewrote plugin-tab description and quick-start copy so the install summary explains core FolderView Plus capabilities more clearly.
1825
- Compatibility: Updated `folderviewplus-desc` locale text across all shipped language packs for consistent plugin-tab rendering.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,7 @@
19281928
createIdleTaskQueue,
19291929
createBatchedStorageWriter,
19301930
normalizeFolderMap,
1931+
normalizeFolderMembers,
19311932
normalizeAppColumnWidth,
19321933
normalizeDashboardLayout,
19331934
normalizeThemeCompatibilityMode,

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.wizard.js

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3285,6 +3285,76 @@ const formatSetupAssistantSavedAt = (value) => {
32853285
return formatted || raw;
32863286
};
32873287

3288+
const toSetupAssistantDisplayText = (value, fallback = '-') => {
3289+
const text = String(value ?? '').trim();
3290+
return text || fallback;
3291+
};
3292+
3293+
const renderSetupAssistantSwalSummaryHtml = ({
3294+
metaRows = [],
3295+
detailRows = [],
3296+
warningLines = [],
3297+
failureLines = [],
3298+
footerText = ''
3299+
} = {}) => {
3300+
const rowDivider = 'border-bottom:1px solid rgba(148, 170, 196, 0.18);';
3301+
const metaHtml = metaRows.length
3302+
? `
3303+
<div style="display:flex;flex-wrap:wrap;gap:6px 8px;margin-bottom:10px;">
3304+
${metaRows.map((row) => `
3305+
<span style="display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;border:1px solid rgba(148,170,196,0.3);background:rgba(18,28,42,0.72);font-size:12px;line-height:1.2;">
3306+
<strong style="margin-right:5px;">${escapeHtml(row.label)}:</strong>${escapeHtml(row.value)}
3307+
</span>
3308+
`).join('')}
3309+
</div>
3310+
`
3311+
: '';
3312+
const detailHtml = detailRows.length
3313+
? `
3314+
<div style="border:1px solid rgba(148,170,196,0.28);border-radius:10px;background:rgba(9,16,24,0.74);overflow:hidden;">
3315+
${detailRows.map((row, index) => `
3316+
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:7px 10px;${index < detailRows.length - 1 ? rowDivider : ''}">
3317+
<span style="font-weight:600;color:#d9e5f6;">${escapeHtml(row.label)}</span>
3318+
<span style="text-align:right;color:#eaf2ff;">${escapeHtml(row.value)}</span>
3319+
</div>
3320+
`).join('')}
3321+
</div>
3322+
`
3323+
: '';
3324+
const warningHtml = warningLines.length
3325+
? `
3326+
<div style="margin-top:10px;padding:8px 10px;border-radius:9px;border:1px solid rgba(255,193,94,0.42);background:rgba(255,193,94,0.1);text-align:left;">
3327+
<div style="font-weight:700;margin-bottom:4px;color:#ffd494;">Warnings</div>
3328+
<ul style="margin:0 0 0 18px;padding:0;line-height:1.35;">
3329+
${warningLines.map((line) => `<li style="margin:2px 0;">${escapeHtml(line)}</li>`).join('')}
3330+
</ul>
3331+
</div>
3332+
`
3333+
: '';
3334+
const failureHtml = failureLines.length
3335+
? `
3336+
<div style="margin-top:10px;padding:8px 10px;border-radius:9px;border:1px solid rgba(255,116,116,0.48);background:rgba(255,116,116,0.1);text-align:left;">
3337+
<div style="font-weight:700;margin-bottom:4px;color:#ffb4b4;">Failed tasks</div>
3338+
<ul style="margin:0 0 0 18px;padding:0;line-height:1.35;">
3339+
${failureLines.map((line) => `<li style="margin:2px 0;">${escapeHtml(line)}</li>`).join('')}
3340+
</ul>
3341+
</div>
3342+
`
3343+
: '';
3344+
const footerHtml = footerText
3345+
? `<div style="margin-top:10px;font-weight:600;color:#d7e6fb;">${escapeHtml(footerText)}</div>`
3346+
: '';
3347+
return `
3348+
<div style="max-width:560px;margin:0 auto;text-align:left;line-height:1.35;">
3349+
${metaHtml}
3350+
${detailHtml}
3351+
${warningHtml}
3352+
${failureHtml}
3353+
${footerHtml}
3354+
</div>
3355+
`;
3356+
};
3357+
32883358
const buildSetupAssistantVerificationReport = (importOutcomes, templateOutcomes, ruleOutcomes, validationWarnings = []) => {
32893359
const checks = [];
32903360
const register = (label, ok, detail = '') => {
@@ -3550,24 +3620,28 @@ const confirmSetupAssistantApply = async (impactSummary, reviewValidation) => (
35503620
const prefsTotal = Number(impactSummary?.prefs?.totalChanges) || 0;
35513621
const rulesTotal = Number(impactSummary?.rules?.creatable) || 0;
35523622
const hasDeletes = Number(totals.deletes) > 0;
3553-
const lines = [
3554-
`Route: ${setupAssistantState.route}`,
3555-
`Mode: ${setupAssistantState.mode}`,
3556-
`Wizard detail: ${normalizeSetupAssistantExperienceMode(setupAssistantState.experienceMode)}`,
3557-
`Safety mode: ${normalizeSetupAssistantSafetyMode(setupAssistantState.applySafetyMode)}`,
3558-
`Imports: ${totals.totalOps} ops (create ${totals.creates}, update ${totals.updates}, delete ${totals.deletes})`,
3559-
`Starter folders: ${templateTotals.creatable} create (selected ${templateTotals.selected}, skip existing ${templateTotals.skippedExisting})`,
3560-
`Template auto-assign: ${templateTotals.autoAssignMatched} matched / ${templateTotals.autoAssignUnmatched} unmatched`,
3561-
`Settings: ${prefsTotal} changes`,
3562-
`Starter rules: ${rulesTotal}`,
3563-
`Dry run: ${setupAssistantState.dryRunOnly ? 'ON' : 'OFF'}`
3564-
];
3565-
if (reviewValidation?.warnings?.length) {
3566-
lines.push(`Warnings: ${reviewValidation.warnings.length}`);
3567-
}
3623+
const html = renderSetupAssistantSwalSummaryHtml({
3624+
metaRows: [
3625+
{ label: 'Route', value: toSetupAssistantDisplayText(setupAssistantState.route) },
3626+
{ label: 'Mode', value: toSetupAssistantDisplayText(setupAssistantState.mode) },
3627+
{ label: 'Wizard detail', value: normalizeSetupAssistantExperienceMode(setupAssistantState.experienceMode) },
3628+
{ label: 'Safety mode', value: normalizeSetupAssistantSafetyMode(setupAssistantState.applySafetyMode) },
3629+
{ label: 'Dry run', value: setupAssistantState.dryRunOnly ? 'ON' : 'OFF' }
3630+
],
3631+
detailRows: [
3632+
{ label: 'Imports', value: `${totals.totalOps} ops (create ${totals.creates}, update ${totals.updates}, delete ${totals.deletes})` },
3633+
{ label: 'Starter folders', value: `${templateTotals.creatable} create (selected ${templateTotals.selected}, skip existing ${templateTotals.skippedExisting})` },
3634+
{ label: 'Template auto-assign', value: `${templateTotals.autoAssignMatched} matched / ${templateTotals.autoAssignUnmatched} unmatched` },
3635+
{ label: 'Settings', value: `${prefsTotal} changes` },
3636+
{ label: 'Starter rules', value: `${rulesTotal}` }
3637+
],
3638+
warningLines: Array.isArray(reviewValidation?.warnings) ? reviewValidation.warnings.slice(0, 5) : [],
3639+
footerText: 'Proceed to apply these changes?'
3640+
});
35683641
swal({
35693642
title: setupAssistantState.dryRunOnly ? 'Run setup dry run?' : 'Apply setup assistant changes?',
3570-
text: lines.join('\n'),
3643+
text: html,
3644+
html: true,
35713645
type: hasDeletes && setupAssistantState.dryRunOnly !== true ? 'warning' : 'info',
35723646
showCancelButton: true,
35733647
confirmButtonText: setupAssistantState.dryRunOnly ? 'Run dry run' : 'Apply now',
@@ -3923,9 +3997,31 @@ const applySetupAssistantPlan = async () => {
39233997

39243998
if (applyFailures.length > 0) {
39253999
const retryNow = await new Promise((resolve) => {
4000+
const failureSummaryHtml = renderSetupAssistantSwalSummaryHtml({
4001+
metaRows: [
4002+
{ label: 'Mode', value: toSetupAssistantDisplayText(setupAssistantState.mode) },
4003+
{ label: 'Route', value: toSetupAssistantDisplayText(setupAssistantState.route) },
4004+
{ label: 'Wizard detail', value: normalizeSetupAssistantExperienceMode(setupAssistantState.experienceMode) },
4005+
{ label: 'Safety mode', value: safetyMode }
4006+
],
4007+
detailRows: [
4008+
{ label: 'Docker imports', value: `${importOutcomes.docker}` },
4009+
{ label: 'VM imports', value: `${importOutcomes.vm}` },
4010+
{ label: 'Docker starter folders', value: `${templateOutcomes.docker.created} created, ${templateOutcomes.docker.skippedExisting} skipped, ${Number(templateOutcomes.docker.assignment?.matched) || 0} auto-assigned` },
4011+
{ label: 'VM starter folders', value: `${templateOutcomes.vm.created} created, ${templateOutcomes.vm.skippedExisting} skipped, ${Number(templateOutcomes.vm.assignment?.matched) || 0} auto-assigned` },
4012+
{ label: 'Docker starter rules', value: `${ruleOutcomes.docker.created} added` },
4013+
{ label: 'VM starter rules', value: `${ruleOutcomes.vm.created} added` },
4014+
{ label: 'Verification', value: `${verification.passed}/${verification.total} checks passed` },
4015+
{ label: 'Retryable failures', value: `${applyFailures.length}` }
4016+
],
4017+
warningLines: validationWarnings.slice(0, 5),
4018+
failureLines: applyFailures.slice(0, 8).map((entry) => `${String(entry.phase || '').toUpperCase()} ${String(entry.type || '').toUpperCase()}: ${String(entry.message || '').trim()}`),
4019+
footerText: 'Retry failed tasks now?'
4020+
});
39264021
swal({
39274022
title: 'Setup applied with partial failures',
3928-
text: `${summaryLines.join('\n')}\n\nRetry failed tasks now?`,
4023+
text: failureSummaryHtml,
4024+
html: true,
39294025
type: 'warning',
39304026
showCancelButton: true,
39314027
confirmButtonText: 'Retry failures',

tests/folderviewplus-utils.test.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ test('normalizeFolderMap trims ids and heals member lists', () => {
117117
assert.deepEqual(normalized.alpha.actions, []);
118118
});
119119

120+
test('normalizeFolderMembers is exported and normalizes arrays/objects', () => {
121+
assert.equal(typeof utils.normalizeFolderMembers, 'function');
122+
assert.deepEqual(
123+
utils.normalizeFolderMembers([' plex ', 'plex', '', 'sonarr']),
124+
['plex', 'sonarr']
125+
);
126+
assert.deepEqual(
127+
utils.normalizeFolderMembers({ ' qbittorrent ': true, '': true, bazarr: true }),
128+
['qbittorrent', 'bazarr']
129+
);
130+
});
131+
120132
test('summarizeImport reports creates updates and deletes for replace mode', () => {
121133
const existing = {
122134
a: { name: 'A', containers: ['x'] },

0 commit comments

Comments
 (0)