Skip to content

Commit 1490875

Browse files
Fix folder editor bulk move post-apply lockup
1 parent fac98e1 commit 1490875

7 files changed

Lines changed: 93 additions & 39 deletions

File tree

archive/folderview.plus-2026.04.04.14.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+
1298d40b68ffaa64600cc049530b05a964ba16cabffc6271db23b1e64283067d folderview.plus-2026.04.05.10.txz

folderview.plus.plg

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
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.04.05.09">
10-
<!ENTITY md5 "9ec5159372e5b36652993d96c01ea953">
9+
<!ENTITY version "2026.04.05.10">
10+
<!ENTITY md5 "3a4004b27877f1a3bc0698d9d12f9329">
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.04.05.10
17+
- UX: Folder editor flows, previews, and bootstrap behavior.
18+
19+
1620
###2026.04.05.09
1721
- Quality: Release automation, CI smoke coverage, and packaging guards.
1822
- Docs: Project documentation and support guidance.

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

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ let editorMode = 'basic';
388388
let activeEditorSection = 'general';
389389
let advancedSectionCollapsedState = {};
390390
let memberBulkMoveInFlight = false;
391+
let memberBulkMoveUndoState = null;
392+
let memberBulkMoveUndoInFlight = false;
391393
const SMART_DEFAULT_FIELD_NAMES = new Set([
392394
'icon',
393395
'preview',
@@ -4383,6 +4385,11 @@ function syncMemberSnapshotBaseline() {
43834385
updateChangeSummaryPanel();
43844386
}
43854387

4388+
function clearMemberBulkMoveUndoState() {
4389+
memberBulkMoveUndoState = null;
4390+
memberBulkMoveUndoInFlight = false;
4391+
}
4392+
43864393
const applyMemberBulkMoveResultLocally = (targetFolderId, movedNames = []) => {
43874394
const safeTargetFolderId = String(targetFolderId || '').trim();
43884395
const uniqueNames = Array.from(new Set((Array.isArray(movedNames) ? movedNames : []).map((name) => String(name || '').trim()).filter(Boolean)));
@@ -4451,42 +4458,37 @@ const restoreEditorBulkMoveBackup = async (backupName) => {
44514458
return response.restore || {};
44524459
};
44534460

4454-
const offerMemberBulkMoveUndo = async (backup, successMessage) => {
4455-
const backupName = String(backup?.name || '').trim();
4456-
if (!backupName) {
4461+
function setMemberBulkMoveUndoState(backup, successMessage) {
4462+
memberBulkMoveUndoState = {
4463+
backupName: String(backup?.name || '').trim(),
4464+
message: String(successMessage || '').trim() || 'Bulk move complete.'
4465+
};
4466+
memberBulkMoveUndoInFlight = false;
4467+
}
4468+
4469+
async function undoEditorMemberBulkMove() {
4470+
const backupName = String(memberBulkMoveUndoState?.backupName || '').trim();
4471+
if (!backupName || memberBulkMoveUndoInFlight) {
4472+
return false;
4473+
}
4474+
memberBulkMoveUndoInFlight = true;
4475+
updateMemberBulkMoveUi();
4476+
try {
4477+
await restoreEditorBulkMoveBackup(backupName);
4478+
suppressUnloadPrompt = true;
4479+
location.reload();
4480+
return true;
4481+
} catch (error) {
4482+
memberBulkMoveUndoInFlight = false;
4483+
updateMemberBulkMoveUi();
44574484
swal({
4458-
title: 'Bulk move complete',
4459-
text: successMessage,
4460-
type: 'success'
4485+
title: 'Undo failed',
4486+
text: extractAjaxErrorMessage(error, 'bulk move undo'),
4487+
type: 'error'
44614488
});
4462-
return;
4489+
return false;
44634490
}
4464-
swal({
4465-
title: 'Bulk move complete',
4466-
text: `${successMessage}\n\nBackup created: ${backupName}\nUndo will reload this editor.`,
4467-
type: 'success',
4468-
showCancelButton: true,
4469-
confirmButtonText: 'Undo',
4470-
cancelButtonText: 'Close',
4471-
closeOnConfirm: false,
4472-
showLoaderOnConfirm: true
4473-
}, async (confirmed) => {
4474-
if (!confirmed) {
4475-
return;
4476-
}
4477-
try {
4478-
await restoreEditorBulkMoveBackup(backupName);
4479-
suppressUnloadPrompt = true;
4480-
location.reload();
4481-
} catch (error) {
4482-
swal({
4483-
title: 'Undo failed',
4484-
text: extractAjaxErrorMessage(error, 'bulk move undo'),
4485-
type: 'error'
4486-
});
4487-
}
4488-
});
4489-
};
4491+
}
44904492

44914493
async function applyEditorMemberBulkMove() {
44924494
if (memberBulkMoveInFlight) {
@@ -4573,6 +4575,7 @@ async function applyEditorMemberBulkMove() {
45734575
return;
45744576
}
45754577
memberBulkMoveInFlight = true;
4578+
clearMemberBulkMoveUndoState();
45764579
updateMemberBulkMoveUi();
45774580
try {
45784581
const preludeLines = sharedApi.buildBulkAssignmentPreludeLines(plan, {
@@ -4596,13 +4599,14 @@ async function applyEditorMemberBulkMove() {
45964599
});
45974600
if (executionResult?.cancelled) {
45984601
$('#fvMemberBulkSummary').text(executionResult.summary || 'Bulk move is already running.');
4602+
swal.close();
45994603
return;
46004604
}
4605+
swal.close();
46014606
applyMemberBulkMoveResultLocally(plan.targetFolderId, executionResult?.lines?.filter((entry) => entry.status === 'success').map((entry) => entry.name) || []);
46024607
const successMessage = executionResult?.summary || `Moved ${plan.actionableNames.length} item${plan.actionableNames.length === 1 ? '' : 's'}.`;
4603-
$('#fvMemberBulkSummary').text(successMessage);
4604-
swal.close();
4605-
await offerMemberBulkMoveUndo(executionResult?.backup || null, successMessage);
4608+
setMemberBulkMoveUndoState(executionResult?.backup || null, successMessage);
4609+
updateMemberBulkMoveUi();
46064610
} catch (error) {
46074611
$('#fvMemberBulkSummary').text('Bulk move failed.');
46084612
swal({
@@ -4636,6 +4640,26 @@ function updateMemberBulkMoveUi() {
46364640
if (memberBulkMoveInFlight) {
46374641
return;
46384642
}
4643+
if (memberBulkMoveUndoState) {
4644+
const successText = escapeHtml(String(memberBulkMoveUndoState.message || 'Bulk move complete.'));
4645+
const backupName = escapeHtml(String(memberBulkMoveUndoState.backupName || '').trim());
4646+
if (memberBulkMoveUndoInFlight && backupName) {
4647+
summaryNode.html(`<span class="fv-member-bulk-summary-text">${successText} Restoring backup ${backupName}...</span>`);
4648+
return;
4649+
}
4650+
if (backupName) {
4651+
summaryNode.html(`
4652+
<span class="fv-member-bulk-summary-text">${successText} Backup created: ${backupName}.</span>
4653+
<button type="button" id="fvMemberBulkUndo" class="fv-member-bulk-inline-action">Undo</button>
4654+
`);
4655+
$('#fvMemberBulkUndo').off('click').on('click', () => {
4656+
void undoEditorMemberBulkMove();
4657+
});
4658+
return;
4659+
}
4660+
summaryNode.html(`<span class="fv-member-bulk-summary-text">${successText}</span>`);
4661+
return;
4662+
}
46394663
if ((scopeDetails.movableCount || 0) <= 0) {
46404664
summaryNode.text(scopeDetails.skippedRegexNames.length > 0
46414665
? 'Current scope only contains regex-controlled members. Those stay controlled by rules.'

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/folder.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,11 +1963,32 @@ body[data-fvplus-host-theme="white"] #fvEditorActionBar,
19631963
}
19641964

19651965
.fv-member-bulk-summary {
1966+
display: flex;
1967+
flex-wrap: wrap;
1968+
align-items: center;
1969+
gap: 0.55em;
19661970
color: var(--fv-editor-muted);
19671971
font-size: 0.96rem;
19681972
line-height: 1.35;
19691973
}
19701974

1975+
.fv-member-bulk-summary-text {
1976+
min-width: 0;
1977+
}
1978+
1979+
.fv-member-bulk-inline-action {
1980+
border: 0;
1981+
border-radius: 999px;
1982+
padding: 0.28em 0.9em;
1983+
background: linear-gradient(180deg, var(--fv-editor-button-bg-top), var(--fv-editor-button-bg-bottom));
1984+
color: var(--fv-editor-button-fg);
1985+
box-shadow: var(--fv-editor-button-shadow);
1986+
font-size: 0.86rem;
1987+
font-weight: 700;
1988+
letter-spacing: 0.08em;
1989+
text-transform: uppercase;
1990+
}
1991+
19711992
.fv-member-chip {
19721993
display: inline-flex;
19731994
align-items: center;

tests/folder-editor-member-bulk-move.test.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ test('folder editor members tab renders compact bulk move controls and blocks un
3636
assert.match(folderJs, /renderMemberBulkMoveTargets\(\);/);
3737
assert.match(folderJs, /applyMemberBulkMoveResultLocally\(plan\.targetFolderId/);
3838
assert.match(folderJs, /syncMemberSnapshotBaseline\(\);/);
39+
assert.match(folderJs, /memberBulkMoveUndoState = \{/);
40+
assert.match(folderJs, /id="fvMemberBulkUndo"/);
41+
assert.match(folderJs, /void undoEditorMemberBulkMove\(\);/);
42+
assert.doesNotMatch(folderJs, /title: 'Bulk move complete'/);
3943
});
4044

4145
test('folder editor styles include dedicated member bulk move layout hooks', () => {
4246
assert.match(folderCss, /\.fv-member-bulk-row\s*\{/);
4347
assert.match(folderCss, /\.fv-member-bulk-controls\s*\{/);
4448
assert.match(folderCss, /\.fv-member-bulk-summary\s*\{/);
49+
assert.match(folderCss, /\.fv-member-bulk-inline-action\s*\{/);
4550
});

0 commit comments

Comments
 (0)