Skip to content

Commit 9eaee35

Browse files
author
FolderView Plus Test
committed
Extract folder editor type layer batch 2
1 parent 66261d6 commit 9eaee35

13 files changed

Lines changed: 265 additions & 101 deletions
14 MB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
d3cedb221805687f4695027909d4a4bd9c843cb98ba116838d2c4b6bd10d56bd folderview.plus-2026.04.16.07.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.04.16.06">
10-
<!ENTITY md5 "85351d0a42fd4c3c889a245077704a9b">
9+
<!ENTITY version "2026.04.16.07">
10+
<!ENTITY md5 "e72c825a64d544dacf3b1a13054881a7">
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.16.07
17+
- Refactor: Moved Docker-only folder editor row registration into the Docker type module so VM no longer inherits Docker field grouping from the shared editor chrome.
18+
- Refactor: Moved Docker preview signal assembly and runtime member normalization into type modules, keeping the shared editor preview/runtime type-agnostic.
19+
- Fix: VM editor now hides Docker-only constrained fields through the VM type module instead of shared-runtime Docker checks.
20+
21+
1622
###2026.04.16.06
1723
- UX: Folder editor flows, previews, and bootstrap behavior.
1824
- Refactor: Shared runtime contracts, request plumbing, and cross-page foundations.

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

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
? sharedSectionMeta
2323
: FALLBACK_SECTION_META;
2424
const DEFAULT_FOLDER_ICON_PATH = '/plugins/folderview.plus/images/folder-icon.png';
25+
const pageType = String(root.FolderViewPlusFolderEditorPageType || '').trim().toLowerCase();
2526
const BASIC_MODE = 'basic';
2627
const ADVANCED_MODE = 'advanced';
2728
const pageReportFolderEditorBootstrap = typeof root.FolderViewPlusReportFolderEditorBootstrap === 'function'
@@ -30,6 +31,46 @@
3031
let currentMode = BASIC_MODE;
3132
let currentSection = 'general';
3233
let bootstrapWatchdogArmed = false;
34+
let folderEditorTypeApi = null;
35+
36+
const resolveFolderEditorTypeModule = () => {
37+
if (pageType === 'docker') {
38+
return root.FolderViewPlusFolderEditorTypeDocker || null;
39+
}
40+
if (pageType === 'vm') {
41+
return root.FolderViewPlusFolderEditorTypeVm || null;
42+
}
43+
return null;
44+
};
45+
46+
const getFolderEditorTypeApi = () => {
47+
if (folderEditorTypeApi) {
48+
return folderEditorTypeApi;
49+
}
50+
const typeModule = resolveFolderEditorTypeModule();
51+
if (!typeModule || typeof typeModule.createApi !== 'function') {
52+
return null;
53+
}
54+
try {
55+
folderEditorTypeApi = typeModule.createApi({});
56+
} catch (_error) {
57+
folderEditorTypeApi = null;
58+
}
59+
return folderEditorTypeApi;
60+
};
61+
62+
const mergeSectionRows = (baseRows, extraRows) => {
63+
const merged = { ...baseRows };
64+
const source = extraRows && typeof extraRows === 'object' ? extraRows : {};
65+
Object.entries(source).forEach(([sectionKey, rows]) => {
66+
const nextRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
67+
if (!nextRows.length) {
68+
return;
69+
}
70+
merged[sectionKey] = [...(merged[sectionKey] || []), ...nextRows];
71+
});
72+
return merged;
73+
};
3374

3475
const setBootstrapSurfaceState = ({
3576
summary = '',
@@ -413,7 +454,8 @@
413454
});
414455
};
415456

416-
const collectSectionRows = (form) => ({
457+
const collectSectionRows = (form) => {
458+
const baseRows = {
417459
general: [
418460
findBasicByFieldName(form, 'name'),
419461
findBasicByFieldName(form, 'parent_folder_id'),
@@ -427,21 +469,14 @@
427469
preview: [
428470
findBasicByFieldName(form, 'preview'),
429471
findBasicByFieldName(form, 'preview_hover'),
430-
findBasicByFieldName(form, 'preview_update'),
431472
findBasicByFieldName(form, 'preview_text_width'),
432473
findBasicByFieldName(form, 'preview_rows'),
433474
findBasicByFieldName(form, 'preview_grayscale'),
434-
findBasicByFieldName(form, 'preview_webui'),
435475
findBasicByFieldName(form, 'preview_logs'),
436-
findBasicByFieldName(form, 'preview_console'),
437476
findBasicByFieldName(form, 'preview_vertical_bars'),
438477
findBasicByFieldName(form, 'preview_vertical_bars_color'),
439478
findBasicByFieldName(form, 'preview_border'),
440-
findBasicByFieldName(form, 'preview_border_color'),
441-
findBasicByFieldName(form, 'context'),
442-
findBasicByFieldName(form, 'context_trigger'),
443-
findBasicByFieldName(form, 'context_graph'),
444-
findBasicByFieldName(form, 'context_graph_time')
479+
findBasicByFieldName(form, 'preview_border_color')
445480
],
446481
chevron: [
447482
findBasicByFieldName(form, 'dropdown_style'),
@@ -451,11 +486,6 @@
451486
findBasicByFieldName(form, 'folder_accent_enabled'),
452487
findBasicByFieldName(form, 'folder_accent_color'),
453488
findBasicByFieldName(form, 'status_color_started'),
454-
findBasicByFieldName(form, 'health_warn_stopped_percent'),
455-
findBasicByFieldName(form, 'health_critical_stopped_percent'),
456-
findBasicByFieldName(form, 'health_profile'),
457-
findBasicByFieldName(form, 'health_updates_mode'),
458-
findBasicByFieldName(form, 'health_all_stopped_mode'),
459489
findBasicByFieldName(form, 'status_warn_stopped_percent')
460490
],
461491
rules: [
@@ -465,14 +495,18 @@
465495
form.querySelector('.basic.custom-action-wrapper-parent')
466496
],
467497
advanced: [
468-
findBasicByFieldName(form, 'update_column'),
469498
findBasicByFieldName(form, 'override_default_actions'),
470499
findBasicByFieldName(form, 'default_action'),
471500
findBasicByFieldName(form, 'expand_tab'),
472501
findBasicByFieldName(form, 'expand_dashboard'),
473502
findBasicByFieldName(form, 'dashboard_overflow')
474503
]
475-
});
504+
};
505+
return mergeSectionRows(
506+
baseRows,
507+
getFolderEditorTypeApi()?.collectSectionRows?.({ form, findBasicByFieldName }) || null
508+
);
509+
};
476510

477511
const syncActionLaunchPlacement = (form) => {
478512
const actionsRow = form.querySelector('.basic.custom-action-wrapper-parent');

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.editor.preview-runtime.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@
5454
const normalizeParentFolderId = typeof deps.normalizeParentFolderId === 'function'
5555
? deps.normalizeParentFolderId
5656
: ((value) => String(value || '').trim());
57-
const isDockerUpdateAvailableInEditor = typeof deps.isDockerUpdateAvailableInEditor === 'function'
58-
? deps.isDockerUpdateAvailableInEditor
59-
: (() => false);
57+
const getPreviewSignals = typeof deps.getPreviewSignals === 'function'
58+
? deps.getPreviewSignals
59+
: (() => null);
60+
const applyTypePreviewConstraints = typeof deps.applyTypePreviewConstraints === 'function'
61+
? deps.applyTypePreviewConstraints
62+
: (() => {});
6063
const isFolderAccentEnabled = typeof deps.isFolderAccentEnabled === 'function'
6164
? deps.isFolderAccentEnabled
6265
: ((settings) => settings?.folder_accent_enabled === true);
@@ -98,7 +101,7 @@
98101
getDropdownStyleTokens,
99102
buildSampleMemberState,
100103
normalizeParentFolderId,
101-
isDockerUpdateAvailableInEditor,
104+
getPreviewSignals,
102105
escapeHtml,
103106
updateMemberStats,
104107
onAfterSummaryUpdate: () => {
@@ -186,9 +189,7 @@
186189
if (form.folder_webui?.checked === true) {
187190
$('[constraint*="folder-webui"]').show();
188191
}
189-
if (type !== 'docker') {
190-
$('[constraint*="docker"]').hide();
191-
}
192+
applyTypePreviewConstraints({ $, form });
192193

193194
$('div.canvas > form.folder-editor-form')
194195
.toggleClass('fv-preview-disabled', String(form.preview?.value) === '0')

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

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
const normalizeParentFolderId = typeof deps.normalizeParentFolderId === 'function'
4646
? deps.normalizeParentFolderId
4747
: ((value) => String(value || '').trim());
48-
const isDockerUpdateAvailableInEditor = typeof deps.isDockerUpdateAvailableInEditor === 'function'
49-
? deps.isDockerUpdateAvailableInEditor
50-
: (() => false);
48+
const getPreviewSignals = typeof deps.getPreviewSignals === 'function'
49+
? deps.getPreviewSignals
50+
: (() => null);
5151

5252
const renderLivePreviewCanvas = () => {
5353
if (!$ || !shouldRender()) {
@@ -196,19 +196,12 @@
196196

197197
const dockerSignalsShell = $('#fvDockerSignalsShell');
198198
const dockerSignals = $('#fvDockerSignals');
199-
if (deps.type === 'docker' && dockerSignals.length) {
200-
const composeProjects = Array.from(new Set(
201-
selectedMembers
202-
.map((member) => String(member?.ComposeProject || '').trim())
203-
.filter((value) => value !== '')
204-
));
205-
const updateCount = selectedMembers.filter((member) => isDockerUpdateAvailableInEditor(member)).length;
206-
const composeSummary = composeProjects.length === 0
207-
? 'Compose: none detected'
208-
: (composeProjects.length === 1 ? `Compose: ${composeProjects[0]}` : `Compose: ${composeProjects.length} projects`);
209-
const updateSummary = `Updates: ${updateCount}/${selectedMembers.length || 0}`;
210-
$('#fvDockerComposeSummary').text(composeSummary);
211-
$('#fvDockerUpdateSummary').text(updateSummary);
199+
const previewSignals = getPreviewSignals({ form, memberNames, selectedMembers });
200+
if (dockerSignals.length && previewSignals && Array.isArray(previewSignals.items) && previewSignals.items.length) {
201+
dockerSignalsShell.find('.fv-live-chip-panel-head').text(String(previewSignals.title || 'Signals'));
202+
dockerSignals.html(previewSignals.items.map((label) => (
203+
`<span class="fv-docker-signal-chip">${escapeHtml(label)}</span>`
204+
)).join(''));
212205
if (dockerSignalsShell.length) {
213206
dockerSignalsShell.show();
214207
} else {

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.editor.type-docker.js

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
root.FolderViewPlusFolderEditorTypeDockerModuleLoaded = true;
99
}(typeof globalThis !== 'undefined' ? globalThis : this, function() {
1010
const SYNC_ORDER_PATH = '/plugins/folderview.plus/server/sync_order.php';
11+
const EMPTY_SECTION_ROWS = Object.freeze({});
1112

1213
const createApi = (deps = {}) => {
1314
const normalizeFolderRecordForEditor = typeof deps.normalizeFolderRecordForEditor === 'function'
@@ -20,6 +21,15 @@
2021
? deps.securePost
2122
: (async () => {});
2223
const syncType = String(deps.syncType || 'docker').trim() || 'docker';
24+
const getFolderLabelValue = typeof deps.getFolderLabelValue === 'function'
25+
? deps.getFolderLabelValue
26+
: (() => '');
27+
const getComposeProjectFromLabels = typeof deps.getComposeProjectFromLabels === 'function'
28+
? deps.getComposeProjectFromLabels
29+
: (() => '');
30+
const isDockerUpdateAvailableInEditor = typeof deps.isDockerUpdateAvailableInEditor === 'function'
31+
? deps.isDockerUpdateAvailableInEditor
32+
: ((member) => member?.UpdateAvailable === true || member?.update === true);
2333

2434
const buildComparableFolder = (folderRecord) => {
2535
const normalized = normalizeFolderRecordForEditor(folderRecord || {});
@@ -67,10 +77,109 @@
6777
await securePost(SYNC_ORDER_PATH, { type: syncType });
6878
};
6979

80+
const mapRuntimeMember = (entry = {}) => {
81+
const labels = entry?.info?.Config?.Labels || {};
82+
const state = entry?.info?.State || entry?.State || {};
83+
const memberName = String(entry?.info?.Name || entry?.Name || '').trim();
84+
if (!memberName) {
85+
return null;
86+
}
87+
return {
88+
Name: memberName,
89+
Icon: labels['net.unraid.docker.icon'],
90+
Label: getFolderLabelValue(labels),
91+
ComposeProject: getComposeProjectFromLabels(labels),
92+
State: state,
93+
RawState: state,
94+
UpdateAvailable: state?.manager === 'dockerman' && state?.Updated === false
95+
};
96+
};
97+
98+
const collectSectionRows = ({ form, findBasicByFieldName } = {}) => {
99+
if (!form || typeof findBasicByFieldName !== 'function') {
100+
return EMPTY_SECTION_ROWS;
101+
}
102+
return {
103+
preview: [
104+
findBasicByFieldName(form, 'preview_update'),
105+
findBasicByFieldName(form, 'preview_webui'),
106+
findBasicByFieldName(form, 'preview_console'),
107+
findBasicByFieldName(form, 'context'),
108+
findBasicByFieldName(form, 'context_trigger'),
109+
findBasicByFieldName(form, 'context_graph'),
110+
findBasicByFieldName(form, 'context_graph_time')
111+
],
112+
status: [
113+
findBasicByFieldName(form, 'health_warn_stopped_percent'),
114+
findBasicByFieldName(form, 'health_critical_stopped_percent'),
115+
findBasicByFieldName(form, 'health_profile'),
116+
findBasicByFieldName(form, 'health_updates_mode'),
117+
findBasicByFieldName(form, 'health_all_stopped_mode')
118+
],
119+
advanced: [
120+
findBasicByFieldName(form, 'update_column')
121+
]
122+
};
123+
};
124+
125+
const applySectionTags = ({ markSection, markAdvanced } = {}) => {
126+
if (typeof markSection !== 'function' || typeof markAdvanced !== 'function') {
127+
return;
128+
}
129+
markSection('div.basic:has([name="preview_update"])', 'preview');
130+
markSection('div.basic:has([name="preview_webui"])', 'preview');
131+
markSection('div.basic:has([name="preview_console"])', 'preview');
132+
markSection('div.basic:has([name="context"])', 'preview');
133+
markSection('ul[constraint*="context-2"]', 'preview');
134+
markSection('div.basic:has([name="context_trigger"])', 'preview');
135+
markSection('div.basic:has([name="context_graph"])', 'preview');
136+
markSection('div.basic:has([name="context_graph_time"])', 'preview');
137+
markSection('div.basic:has([name="health_warn_stopped_percent"])', 'status');
138+
markSection('div.basic:has([name="health_critical_stopped_percent"])', 'status');
139+
markSection('div.basic:has([name="health_profile"])', 'status');
140+
markSection('div.basic:has([name="health_updates_mode"])', 'status');
141+
markSection('div.basic:has([name="health_all_stopped_mode"])', 'status');
142+
markSection('div.basic:has([name="update_column"])', 'advanced');
143+
144+
markAdvanced('div.basic:has([name="preview_update"])');
145+
markAdvanced('div.basic:has([name="health_warn_stopped_percent"])');
146+
markAdvanced('div.basic:has([name="health_critical_stopped_percent"])');
147+
markAdvanced('div.basic:has([name="health_profile"])');
148+
markAdvanced('div.basic:has([name="health_updates_mode"])');
149+
markAdvanced('div.basic:has([name="health_all_stopped_mode"])');
150+
markAdvanced('div.basic:has([name="update_column"])');
151+
};
152+
153+
const getPreviewSignals = ({ selectedMembers = [] } = {}) => {
154+
const members = Array.isArray(selectedMembers) ? selectedMembers : [];
155+
const composeProjects = Array.from(new Set(
156+
members
157+
.map((member) => String(member?.ComposeProject || '').trim())
158+
.filter(Boolean)
159+
));
160+
const updateCount = members.filter((member) => isDockerUpdateAvailableInEditor(member)).length;
161+
return {
162+
title: 'Docker signals',
163+
items: [
164+
composeProjects.length === 0
165+
? 'Compose: none detected'
166+
: (composeProjects.length === 1 ? `Compose: ${composeProjects[0]}` : `Compose: ${composeProjects.length} projects`),
167+
`Updates: ${updateCount}/${members.length || 0}`
168+
]
169+
};
170+
};
171+
172+
const applyPreviewConstraints = () => {};
173+
70174
return Object.freeze({
71175
buildComparableFolder,
72176
shouldSyncAfterSave,
73-
flushPostSaveSync
177+
flushPostSaveSync,
178+
mapRuntimeMember,
179+
collectSectionRows,
180+
applySectionTags,
181+
getPreviewSignals,
182+
applyPreviewConstraints
74183
});
75184
};
76185

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.editor.type-vm.js

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,39 @@
77
root.FolderViewPlusFolderEditorTypeVm = factory();
88
root.FolderViewPlusFolderEditorTypeVmModuleLoaded = true;
99
}(typeof globalThis !== 'undefined' ? globalThis : this, function() {
10-
const createApi = () => Object.freeze({
11-
shouldSyncAfterSave: () => false,
12-
flushPostSaveSync: async () => {}
13-
});
10+
const EMPTY_SECTION_ROWS = Object.freeze({});
11+
12+
const createApi = (deps = {}) => {
13+
const jq = deps.$ || (typeof globalThis !== 'undefined' ? globalThis.jQuery || globalThis.$ : null);
14+
15+
const mapRuntimeMember = (entry = {}) => {
16+
const memberName = String(entry?.name || entry?.Name || '').trim();
17+
if (!memberName) {
18+
return null;
19+
}
20+
return {
21+
Name: memberName,
22+
Icon: entry?.icon || entry?.Icon || '',
23+
Label: undefined
24+
};
25+
};
26+
27+
return Object.freeze({
28+
shouldSyncAfterSave: () => false,
29+
flushPostSaveSync: async () => {},
30+
mapRuntimeMember,
31+
collectSectionRows: () => EMPTY_SECTION_ROWS,
32+
applySectionTags: () => {},
33+
getPreviewSignals: () => null,
34+
applyPreviewConstraints: ({ $, form } = {}) => {
35+
const activeJq = $ || jq;
36+
if (!activeJq || !form) {
37+
return;
38+
}
39+
activeJq('[constraint*="docker"]').hide();
40+
}
41+
});
42+
};
1443

1544
return Object.freeze({
1645
createApi

0 commit comments

Comments
 (0)