Skip to content

Commit 96a3ffa

Browse files
authored
Merge pull request #87 from mstrhakr:feat/sortable-stacks
Sortable stacks with lock functionality
2 parents 7704526 + 78387ad commit 96a3ffa

9 files changed

Lines changed: 406 additions & 0 deletions

File tree

source/compose.manager/Compose.page

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Type="xmenu"
33
Title="Docker Compose"
44
Tag="fa-cubes"
55
Code="f1b3"
6+
Lock="true"
67
Nchan="docker_load"
78
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'\")"
89
---

source/compose.manager/compose.manager.page

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Author="dcflachs"
22
Title="Compose"
33
Type="php"
44
Menu="Docker:2"
5+
Lock="true"
56
Nchan="docker_load"
67
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'\"))"
78
---

source/compose.manager/compose.manager.settings.page

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Icon="cubes"
33
Author="mstrhakr"
44
Title="Compose Manager Plus"
55
Type="xmenu"
6+
Lock="true"
67
---
78
<?php
89
include "/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php";
@@ -1520,6 +1521,68 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
15201521
}
15211522
}
15221523

1524+
function updateLockButtonUI() {
1525+
var unlocked = $.cookie('lockbutton') != null;
1526+
var $lockNav = $('div.nav-item.LockButton');
1527+
1528+
if (!$lockNav.length) return;
1529+
1530+
if (unlocked) {
1531+
$lockNav.find('a').prop('title', "_(Lock sortable items)_");
1532+
$lockNav.find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
1533+
$lockNav.find('span').text("_(Lock sortable items)_");
1534+
} else {
1535+
$lockNav.find('a').prop('title', "_(Unlock sortable items)_");
1536+
$lockNav.find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
1537+
$lockNav.find('span').text("_(Unlock sortable items)_");
1538+
}
1539+
}
1540+
1541+
function initUpdatesSortable() {
1542+
var $tbody = $('#updates-tbody');
1543+
if (!$tbody.length || typeof $.fn.sortable !== 'function') return;
1544+
1545+
if ($tbody.hasClass('ui-sortable')) {
1546+
$tbody.sortable('destroy');
1547+
}
1548+
1549+
var unlocked = $.cookie('lockbutton') != null;
1550+
if (!unlocked) {
1551+
$tbody.find('tr.sortable').css({
1552+
cursor: 'default'
1553+
});
1554+
return;
1555+
}
1556+
1557+
$tbody.find('tr.sortable').css({
1558+
cursor: 'move'
1559+
});
1560+
1561+
$tbody.sortable({
1562+
helper: 'clone',
1563+
items: 'tr.sortable',
1564+
cursor: 'grab',
1565+
axis: 'y',
1566+
containment: 'parent',
1567+
cancel: 'input,select,button,a,label,.switch-button-background',
1568+
delay: 100,
1569+
opacity: 0.5,
1570+
zIndex: 9999,
1571+
forcePlaceholderSize: true
1572+
});
1573+
}
1574+
1575+
function LockButton() {
1576+
if ($.cookie('lockbutton') == null) {
1577+
$.cookie('lockbutton', 'lockbutton');
1578+
} else {
1579+
$.removeCookie('lockbutton');
1580+
}
1581+
1582+
updateLockButtonUI();
1583+
initUpdatesSortable();
1584+
}
1585+
15231586
function loadUpdatesUI() {
15241587
$('#updates-tbody').html('<tr><td colspan="5" class="compose-text-muted" style="text-align:center;padding:12px;">_(Loading stacks...)_</td></tr>');
15251588
$.post('/plugins/compose.manager/php/exec.php', {
@@ -1574,6 +1637,8 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
15741637
off_label: ''
15751638
});
15761639
});
1640+
1641+
initUpdatesSortable();
15771642
// Sync toggle-all state
15781643
updateToggleAllState();
15791644
}, 'json').fail(function() {
@@ -1641,6 +1706,8 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
16411706

16421707
// Initialize Updates tab when it's shown
16431708
$(function() {
1709+
updateLockButtonUI();
1710+
16441711
// Apply default schedule/time to all rows
16451712
$('#apply-default').on('click', function() {
16461713
var s = $('#default-schedule').val();
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Compose Manager – Stack sortable (drag-and-drop reordering)
3+
*
4+
* Depends on globals provided by the page:
5+
* caURL – AJAX endpoint for exec.php
6+
* composeClientDebug – debug/logging helper (common.js)
7+
* $.cookie / $.removeCookie – jquery.cookie plugin
8+
* $.fn.sortable – jQuery UI Sortable
9+
*/
10+
11+
// ── Sort-mode state ────────────────────────────────────────────────
12+
13+
function isComposeSortModeEnabled() {
14+
return $.cookie('lockbutton') != null;
15+
}
16+
17+
// ── Lock / Unlock button UI ────────────────────────────────────────
18+
19+
function updateComposeLockButtonUI() {
20+
var unlocked = isComposeSortModeEnabled();
21+
var $button = $('div.nav-item.LockButton');
22+
if (!$button.length) {
23+
return;
24+
}
25+
26+
if (unlocked) {
27+
$button.find('a').prop('title', 'Lock sortable items');
28+
$button.find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
29+
$button.find('span').text('Lock sortable items');
30+
} else {
31+
$button.find('a').prop('title', 'Unlock sortable items');
32+
$button.find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
33+
$button.find('span').text('Unlock sortable items');
34+
}
35+
}
36+
37+
// ── Persist sort order ─────────────────────────────────────────────
38+
39+
function saveComposeSortOrder() {
40+
var projects = $('#compose_list > tr.compose-sortable').map(function() {
41+
return $(this).data('project');
42+
}).get();
43+
44+
return $.post(caURL, {
45+
action: 'saveStackOrder',
46+
projects: projects
47+
}).fail(function(xhr) {
48+
composeClientDebug('[saveComposeSortOrder] failed', {
49+
status: xhr.status,
50+
response: xhr.responseText
51+
}, 'daemon', 'error');
52+
});
53+
}
54+
55+
// ── Details-row helpers (detach during drag, reattach on drop) ─────
56+
57+
function getComposeDetailsRowForItem($item) {
58+
if (!$item || !$item.length) {
59+
return $();
60+
}
61+
62+
var rowId = $item.attr('id') || '';
63+
if (rowId.indexOf('stack-row-') !== 0) {
64+
return $();
65+
}
66+
67+
return $('#details-row-' + rowId.replace('stack-row-', ''));
68+
}
69+
70+
function reattachComposeDetailsRow($item) {
71+
var $detailsRow = $item.data('compose-details-row');
72+
if ($detailsRow && $detailsRow.length) {
73+
$detailsRow.insertAfter($item);
74+
$item.removeData('compose-details-row');
75+
}
76+
}
77+
78+
// ── jQuery UI Sortable initialisation ──────────────────────────────
79+
80+
function initComposeSortable() {
81+
var $tbody = $('#compose_list');
82+
if (!$tbody.length) {
83+
return;
84+
}
85+
86+
if ($tbody.hasClass('ui-sortable')) {
87+
$tbody.sortable('destroy');
88+
}
89+
90+
if (!isComposeSortModeEnabled()) {
91+
$tbody.removeClass('compose-sort-enabled');
92+
return;
93+
}
94+
95+
$tbody.addClass('compose-sort-enabled');
96+
$tbody.sortable({
97+
helper: 'clone',
98+
items: '> tr.compose-sortable',
99+
cursor: 'grab',
100+
axis: 'y',
101+
containment: 'parent',
102+
cancel: '[data-stackid], .compose-updatecolumn a, .compose-updatecolumn .exec, .auto_start, .switchButton, a, button, input',
103+
delay: 100,
104+
opacity: 0.5,
105+
zIndex: 9999,
106+
forcePlaceholderSize: true,
107+
start: function(event, ui) {
108+
var $detailsRow = getComposeDetailsRowForItem(ui.item);
109+
if ($detailsRow.length) {
110+
ui.item.data('compose-details-row', $detailsRow.detach());
111+
}
112+
},
113+
update: function() {
114+
saveComposeSortOrder();
115+
},
116+
stop: function(event, ui) {
117+
reattachComposeDetailsRow(ui.item);
118+
}
119+
});
120+
}
121+
122+
// ── Sync all sort-mode UI (icons, class, sortable instance) ────────
123+
124+
function syncComposeSortModeUI() {
125+
var unlocked = isComposeSortModeEnabled();
126+
$('#compose_stacks tr.compose-sortable').each(function() {
127+
var $row = $(this);
128+
$row.find('.expand-icon').toggle(!unlocked);
129+
$row.find('.mover').toggle(unlocked);
130+
});
131+
132+
$('#compose_list').toggleClass('compose-sort-enabled', unlocked);
133+
updateComposeLockButtonUI();
134+
initComposeSortable();
135+
}
136+
137+
// ── Global entry-point called by Unraid navigation lock button ─────
138+
// When embedded in the Docker tab, DockerContainers.page defines its own
139+
// LockButton(). We save the previous implementation (if any) and chain
140+
// to it so both Docker containers AND Compose stacks react to the button.
141+
// IMPORTANT: We use window.LockButton assignment (not a function declaration)
142+
// to avoid hoisting — a hoisted function declaration would overwrite the
143+
// global LockButton before we can capture it.
144+
145+
var _composePrevLockButton = window.LockButton || null;
146+
147+
window.LockButton = function LockButton() {
148+
if (_composePrevLockButton) {
149+
// Let the Docker (or other) handler run first — it toggles the
150+
// cookie and manages its own sortable / UI state.
151+
_composePrevLockButton();
152+
} else {
153+
// Standalone page (header-menu mode) — we own the cookie.
154+
if (isComposeSortModeEnabled()) {
155+
$.removeCookie('lockbutton');
156+
} else {
157+
$.cookie('lockbutton', 'lockbutton');
158+
}
159+
}
160+
161+
syncComposeSortModeUI();
162+
};

source/compose.manager/php/compose_list.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
// Arrow column
193193
$o .= "<td class='col-arrow'>";
194194
$o .= "<i class='fa fa-chevron-right expand-icon' id='expand-icon-$id' onclick='toggleStackDetails(\"$id\");event.stopPropagation();' style='cursor:pointer;'></i>";
195+
$o .= "<i class='fa fa-arrows-v mover orange-text' aria-hidden='true' style='display:none;cursor:move;'></i>";
195196
$o .= "</td>";
196197

197198
// Icon column

0 commit comments

Comments
 (0)