diff --git a/source/compose.manager/Compose.page b/source/compose.manager/Compose.page
index aeb2f41..8f81e0e 100644
--- a/source/compose.manager/Compose.page
+++ b/source/compose.manager/Compose.page
@@ -3,6 +3,7 @@ Type="xmenu"
Title="Docker Compose"
Tag="fa-cubes"
Code="f1b3"
+Lock="true"
Nchan="docker_load"
Cond="$var['fsState'] == 'Started' && exec('/etc/rc.d/rc.docker status | grep -v \"not\"') && exec(\"grep '^SHOW_COMPOSE_IN_HEADER_MENU=' /boot/config/plugins/compose.manager/compose.manager.cfg 2>/dev/null | grep 'true'\")"
---
diff --git a/source/compose.manager/compose.manager.page b/source/compose.manager/compose.manager.page
index 93856ea..fa48101 100644
--- a/source/compose.manager/compose.manager.page
+++ b/source/compose.manager/compose.manager.page
@@ -2,6 +2,7 @@ Author="dcflachs"
Title="Compose"
Type="php"
Menu="Docker:2"
+Lock="true"
Nchan="docker_load"
Cond="$var['fsState'] == 'Started' && exec('/etc/rc.d/rc.docker status | grep -v \"not\"') && (!file_exists('/boot/config/plugins/compose.manager/compose.manager.cfg') ? true : exec(\"grep '^SHOW_COMPOSE_IN_HEADER_MENU=' /boot/config/plugins/compose.manager/compose.manager.cfg 2>/dev/null | grep -v 'true'\"))"
---
diff --git a/source/compose.manager/compose.manager.settings.page b/source/compose.manager/compose.manager.settings.page
index 2b68f9c..8ce82e0 100755
--- a/source/compose.manager/compose.manager.settings.page
+++ b/source/compose.manager/compose.manager.settings.page
@@ -3,6 +3,7 @@ Icon="cubes"
Author="mstrhakr"
Title="Compose Manager Plus"
Type="xmenu"
+Lock="true"
---
_(Loading stacks...)_ | ');
$.post('/plugins/compose.manager/php/exec.php', {
@@ -1574,6 +1637,8 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
off_label: ''
});
});
+
+ initUpdatesSortable();
// Sync toggle-all state
updateToggleAllState();
}, 'json').fail(function() {
@@ -1641,6 +1706,8 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
// Initialize Updates tab when it's shown
$(function() {
+ updateLockButtonUI();
+
// Apply default schedule/time to all rows
$('#apply-default').on('click', function() {
var s = $('#default-schedule').val();
diff --git a/source/compose.manager/javascript/composeSortable.js b/source/compose.manager/javascript/composeSortable.js
new file mode 100644
index 0000000..6094566
--- /dev/null
+++ b/source/compose.manager/javascript/composeSortable.js
@@ -0,0 +1,162 @@
+/**
+ * Compose Manager – Stack sortable (drag-and-drop reordering)
+ *
+ * Depends on globals provided by the page:
+ * caURL – AJAX endpoint for exec.php
+ * composeClientDebug – debug/logging helper (common.js)
+ * $.cookie / $.removeCookie – jquery.cookie plugin
+ * $.fn.sortable – jQuery UI Sortable
+ */
+
+// ── Sort-mode state ────────────────────────────────────────────────
+
+function isComposeSortModeEnabled() {
+ return $.cookie('lockbutton') != null;
+}
+
+// ── Lock / Unlock button UI ────────────────────────────────────────
+
+function updateComposeLockButtonUI() {
+ var unlocked = isComposeSortModeEnabled();
+ var $button = $('div.nav-item.LockButton');
+ if (!$button.length) {
+ return;
+ }
+
+ if (unlocked) {
+ $button.find('a').prop('title', 'Lock sortable items');
+ $button.find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
+ $button.find('span').text('Lock sortable items');
+ } else {
+ $button.find('a').prop('title', 'Unlock sortable items');
+ $button.find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
+ $button.find('span').text('Unlock sortable items');
+ }
+}
+
+// ── Persist sort order ─────────────────────────────────────────────
+
+function saveComposeSortOrder() {
+ var projects = $('#compose_list > tr.compose-sortable').map(function() {
+ return $(this).data('project');
+ }).get();
+
+ return $.post(caURL, {
+ action: 'saveStackOrder',
+ projects: projects
+ }).fail(function(xhr) {
+ composeClientDebug('[saveComposeSortOrder] failed', {
+ status: xhr.status,
+ response: xhr.responseText
+ }, 'daemon', 'error');
+ });
+}
+
+// ── Details-row helpers (detach during drag, reattach on drop) ─────
+
+function getComposeDetailsRowForItem($item) {
+ if (!$item || !$item.length) {
+ return $();
+ }
+
+ var rowId = $item.attr('id') || '';
+ if (rowId.indexOf('stack-row-') !== 0) {
+ return $();
+ }
+
+ return $('#details-row-' + rowId.replace('stack-row-', ''));
+}
+
+function reattachComposeDetailsRow($item) {
+ var $detailsRow = $item.data('compose-details-row');
+ if ($detailsRow && $detailsRow.length) {
+ $detailsRow.insertAfter($item);
+ $item.removeData('compose-details-row');
+ }
+}
+
+// ── jQuery UI Sortable initialisation ──────────────────────────────
+
+function initComposeSortable() {
+ var $tbody = $('#compose_list');
+ if (!$tbody.length) {
+ return;
+ }
+
+ if ($tbody.hasClass('ui-sortable')) {
+ $tbody.sortable('destroy');
+ }
+
+ if (!isComposeSortModeEnabled()) {
+ $tbody.removeClass('compose-sort-enabled');
+ return;
+ }
+
+ $tbody.addClass('compose-sort-enabled');
+ $tbody.sortable({
+ helper: 'clone',
+ items: '> tr.compose-sortable',
+ cursor: 'grab',
+ axis: 'y',
+ containment: 'parent',
+ cancel: '[data-stackid], .compose-updatecolumn a, .compose-updatecolumn .exec, .auto_start, .switchButton, a, button, input',
+ delay: 100,
+ opacity: 0.5,
+ zIndex: 9999,
+ forcePlaceholderSize: true,
+ start: function(event, ui) {
+ var $detailsRow = getComposeDetailsRowForItem(ui.item);
+ if ($detailsRow.length) {
+ ui.item.data('compose-details-row', $detailsRow.detach());
+ }
+ },
+ update: function() {
+ saveComposeSortOrder();
+ },
+ stop: function(event, ui) {
+ reattachComposeDetailsRow(ui.item);
+ }
+ });
+}
+
+// ── Sync all sort-mode UI (icons, class, sortable instance) ────────
+
+function syncComposeSortModeUI() {
+ var unlocked = isComposeSortModeEnabled();
+ $('#compose_stacks tr.compose-sortable').each(function() {
+ var $row = $(this);
+ $row.find('.expand-icon').toggle(!unlocked);
+ $row.find('.mover').toggle(unlocked);
+ });
+
+ $('#compose_list').toggleClass('compose-sort-enabled', unlocked);
+ updateComposeLockButtonUI();
+ initComposeSortable();
+}
+
+// ── Global entry-point called by Unraid navigation lock button ─────
+// When embedded in the Docker tab, DockerContainers.page defines its own
+// LockButton(). We save the previous implementation (if any) and chain
+// to it so both Docker containers AND Compose stacks react to the button.
+// IMPORTANT: We use window.LockButton assignment (not a function declaration)
+// to avoid hoisting — a hoisted function declaration would overwrite the
+// global LockButton before we can capture it.
+
+var _composePrevLockButton = window.LockButton || null;
+
+window.LockButton = function LockButton() {
+ if (_composePrevLockButton) {
+ // Let the Docker (or other) handler run first — it toggles the
+ // cookie and manages its own sortable / UI state.
+ _composePrevLockButton();
+ } else {
+ // Standalone page (header-menu mode) — we own the cookie.
+ if (isComposeSortModeEnabled()) {
+ $.removeCookie('lockbutton');
+ } else {
+ $.cookie('lockbutton', 'lockbutton');
+ }
+ }
+
+ syncComposeSortModeUI();
+};
diff --git a/source/compose.manager/php/compose_list.php b/source/compose.manager/php/compose_list.php
index 548a4cb..1b537d5 100755
--- a/source/compose.manager/php/compose_list.php
+++ b/source/compose.manager/php/compose_list.php
@@ -192,6 +192,7 @@
// Arrow column
$o .= "";
$o .= "";
+ $o .= "";
$o .= " | ";
// Icon column
diff --git a/source/compose.manager/php/compose_manager_main.php b/source/compose.manager/php/compose_manager_main.php
index a8f096f..9b4f4cc 100755
--- a/source/compose.manager/php/compose_manager_main.php
+++ b/source/compose.manager/php/compose_manager_main.php
@@ -100,6 +100,19 @@ function compose_manager_cpu_spec_count($cpuSpec)
padding: 0;
}
+ #compose_stacks td.col-arrow {
+ text-align: center;
+ white-space: nowrap;
+ }
+
+ #compose_stacks td.col-arrow i {
+ vertical-align: middle;
+ }
+
+ #compose_list.compose-sort-enabled tr.compose-sortable {
+ cursor: move;
+ }
+
#compose_stacks thead th.col-icon {
width: 30px;
padding: 0;
@@ -261,6 +274,7 @@ function compose_manager_cpu_spec_count($cpuSpec)
+