Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

282 строки
12KB

  1. /* kanban-settings.js — settings panel: add/rename/delete/reorder columns and lanes */
  2. (function () {
  3. 'use strict';
  4. var boardId = KANBAN.boardId;
  5. var panel = document.getElementById('settings-panel');
  6. var overlay = document.getElementById('settings-overlay');
  7. /* ── Panel open/close ────────────────────────────────────── */
  8. document.getElementById('btn-settings').addEventListener('click', openPanel);
  9. document.getElementById('btn-close-settings').addEventListener('click', closePanel);
  10. overlay.addEventListener('click', closePanel);
  11. function openPanel() {
  12. panel.classList.add('open');
  13. overlay.classList.remove('d-none');
  14. }
  15. function closePanel() {
  16. panel.classList.remove('open');
  17. overlay.classList.add('d-none');
  18. }
  19. /* ── Helpers ─────────────────────────────────────────────── */
  20. function post(url, data, cb) {
  21. var params = new URLSearchParams();
  22. Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
  23. fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() })
  24. .then(function (r) { return r.json(); })
  25. .then(cb)
  26. .catch(function (e) { console.error(url, e); });
  27. }
  28. function postJson(url, payload, cb) {
  29. fetch(url, {
  30. method: 'POST',
  31. headers: { 'Content-Type': 'application/json' },
  32. body: JSON.stringify(payload)
  33. }).then(function (r) { return r.json(); }).then(cb)
  34. .catch(function (e) { console.error(url, e); });
  35. }
  36. function collectOrder(listId) {
  37. return Array.from(document.querySelectorAll('#' + listId + ' li')).map(function (li, idx) {
  38. return { id: parseInt(li.dataset.id), position: idx };
  39. });
  40. }
  41. function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle) {
  42. var li = document.createElement('li');
  43. li.className = 'list-group-item d-flex align-items-center gap-2 py-2';
  44. li.dataset.id = id;
  45. li.innerHTML =
  46. '<i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i>' +
  47. '<span class="flex-grow-1 ' + labelClass + '">' + esc(name) + '</span>' +
  48. (countToggle ?
  49. '<div class="form-check form-switch m-0" title="Show card count in column header">' +
  50. '<input class="form-check-input col-count-toggle" type="checkbox" role="switch">' +
  51. '</div>' : '') +
  52. '<button class="btn btn-sm btn-link p-0 text-secondary ' + editClass + '" title="Rename"><i class="bi bi-pencil"></i></button>' +
  53. '<button class="btn btn-sm btn-link p-0 text-danger ' + deleteClass + '" title="Delete"><i class="bi bi-trash"></i></button>';
  54. return li;
  55. }
  56. function esc(s) {
  57. return String(s)
  58. .replace(/&/g, '&amp;').replace(/</g, '&lt;')
  59. .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  60. }
  61. /* ── Sortable reorder ─────────────────────────────────────── */
  62. function initSortable(listId, reorderUrl) {
  63. var el = document.getElementById(listId);
  64. Sortable.create(el, {
  65. handle: '.drag-handle',
  66. animation: 150,
  67. onEnd: function () {
  68. postJson(reorderUrl, collectOrder(listId), function (res) {
  69. if (!res.ok) console.error('Reorder failed', res);
  70. });
  71. }
  72. });
  73. }
  74. initSortable('col-list', '/columns/reorder');
  75. initSortable('lane-list', '/swimlanes/reorder');
  76. /* ═══════════════════════════════════════════════════════════
  77. COLUMNS
  78. ═══════════════════════════════════════════════════════════ */
  79. /* ── Add column ───────────────────────────────────────────── */
  80. document.getElementById('btn-add-column').addEventListener('click', function () {
  81. document.getElementById('col-add-form').classList.remove('d-none');
  82. document.getElementById('col-add-input').focus();
  83. });
  84. document.getElementById('btn-col-add-cancel').addEventListener('click', function () {
  85. document.getElementById('col-add-form').classList.add('d-none');
  86. document.getElementById('col-add-input').value = '';
  87. });
  88. document.getElementById('btn-col-add-save').addEventListener('click', function () {
  89. var name = document.getElementById('col-add-input').value.trim();
  90. if (!name) return;
  91. post('/columns', { board_id: boardId, name: name }, function (res) {
  92. if (!res.ok) { alert(res.error || 'Failed'); return; }
  93. document.getElementById('col-add-form').classList.add('d-none');
  94. document.getElementById('col-add-input').value = '';
  95. var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true);
  96. document.getElementById('col-list').appendChild(li);
  97. bindColItem(li);
  98. window.KanbanBoard.addColumn(res);
  99. });
  100. });
  101. /* ── Bind edit/delete on existing column items ─────────────── */
  102. function bindColItem(li) {
  103. li.querySelector('.btn-edit-col').addEventListener('click', function () {
  104. startRename(li, '.col-label-text', function (newName, done) {
  105. post('/columns/' + li.dataset.id, { name: newName }, function (res) {
  106. if (res.ok) {
  107. done(true);
  108. window.KanbanBoard.renameColumn(li.dataset.id, newName);
  109. } else {
  110. done(false);
  111. alert(res.error || 'Rename failed');
  112. }
  113. });
  114. });
  115. });
  116. li.querySelector('.btn-delete-col').addEventListener('click', function () {
  117. if (!confirm('Delete this column and all its cards?')) return;
  118. post('/columns/' + li.dataset.id + '/delete', {}, function (res) {
  119. if (res.ok) {
  120. window.KanbanBoard.removeColumn(li.dataset.id);
  121. li.remove();
  122. } else {
  123. alert(res.error || 'Delete failed');
  124. }
  125. });
  126. });
  127. var countToggle = li.querySelector('.col-count-toggle');
  128. if (countToggle) {
  129. countToggle.addEventListener('change', function () {
  130. var show = countToggle.checked;
  131. post('/columns/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
  132. if (res.ok) {
  133. window.KanbanBoard.setColumnShowCount(li.dataset.id, show);
  134. } else {
  135. countToggle.checked = !show;
  136. alert(res.error || 'Update failed');
  137. }
  138. });
  139. });
  140. }
  141. }
  142. document.querySelectorAll('#col-list li').forEach(bindColItem);
  143. /* ═══════════════════════════════════════════════════════════
  144. SWIM LANES
  145. ═══════════════════════════════════════════════════════════ */
  146. /* ── Add lane ─────────────────────────────────────────────── */
  147. document.getElementById('btn-add-lane').addEventListener('click', function () {
  148. document.getElementById('lane-add-form').classList.remove('d-none');
  149. document.getElementById('lane-add-input').focus();
  150. });
  151. document.getElementById('btn-lane-add-cancel').addEventListener('click', function () {
  152. document.getElementById('lane-add-form').classList.add('d-none');
  153. document.getElementById('lane-add-input').value = '';
  154. });
  155. document.getElementById('btn-lane-add-save').addEventListener('click', function () {
  156. var name = document.getElementById('lane-add-input').value.trim();
  157. if (!name) return;
  158. post('/swimlanes', { board_id: boardId, name: name }, function (res) {
  159. if (!res.ok) { alert(res.error || 'Failed'); return; }
  160. document.getElementById('lane-add-form').classList.add('d-none');
  161. document.getElementById('lane-add-input').value = '';
  162. var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text');
  163. document.getElementById('lane-list').appendChild(li);
  164. bindLaneItem(li);
  165. window.KanbanBoard.addLane(res);
  166. });
  167. });
  168. /* ── Bind edit/delete on existing lane items ──────────────── */
  169. function bindLaneItem(li) {
  170. li.querySelector('.btn-edit-lane').addEventListener('click', function () {
  171. startRename(li, '.lane-label-text', function (newName, done) {
  172. post('/swimlanes/' + li.dataset.id, { name: newName }, function (res) {
  173. if (res.ok) {
  174. done(true);
  175. window.KanbanBoard.renameLane(li.dataset.id, newName);
  176. } else {
  177. done(false);
  178. alert(res.error || 'Rename failed');
  179. }
  180. });
  181. });
  182. });
  183. li.querySelector('.btn-delete-lane').addEventListener('click', function () {
  184. if (!confirm('Delete this swim lane and all its cards?')) return;
  185. post('/swimlanes/' + li.dataset.id + '/delete', {}, function (res) {
  186. if (res.ok) {
  187. window.KanbanBoard.removeLane(li.dataset.id);
  188. li.remove();
  189. } else {
  190. alert(res.error || 'Delete failed');
  191. }
  192. });
  193. });
  194. }
  195. document.querySelectorAll('#lane-list li').forEach(bindLaneItem);
  196. /* ═══════════════════════════════════════════════════════════
  197. CARD AGE
  198. ═══════════════════════════════════════════════════════════ */
  199. var cardAgeTooltipToggle = document.getElementById('card-age-tooltip-toggle');
  200. var cardAgeWarningDays = document.getElementById('card-age-warning-days');
  201. function saveCardAgeSettings() {
  202. var showCardAge = cardAgeTooltipToggle.checked;
  203. var warningDays = parseInt(cardAgeWarningDays.value, 10);
  204. if (isNaN(warningDays) || warningDays < 0) warningDays = 0;
  205. cardAgeWarningDays.value = warningDays;
  206. post('/boards/' + boardId + '/card-age-settings', {
  207. show_card_age: showCardAge ? '1' : '0',
  208. card_age_warning_days: warningDays
  209. }, function (res) {
  210. if (!res.ok) {
  211. alert(res.error || 'Update failed');
  212. return;
  213. }
  214. window.KanbanBoard.setCardAgeSettings(res.show_card_age, res.card_age_warning_days);
  215. });
  216. }
  217. cardAgeTooltipToggle.addEventListener('change', saveCardAgeSettings);
  218. cardAgeWarningDays.addEventListener('change', saveCardAgeSettings);
  219. /* ── Inline rename helper ─────────────────────────────────── */
  220. function startRename(li, labelSel, saveCb) {
  221. var span = li.querySelector(labelSel);
  222. var oldName = span.textContent.trim();
  223. var input = document.createElement('input');
  224. input.type = 'text';
  225. input.className = 'form-control form-control-sm inline-rename flex-grow-1';
  226. input.value = oldName;
  227. span.replaceWith(input);
  228. input.focus();
  229. input.select();
  230. function commit() {
  231. var newName = input.value.trim();
  232. if (!newName || newName === oldName) {
  233. abort();
  234. return;
  235. }
  236. saveCb(newName, function (ok) {
  237. var replacement = document.createElement('span');
  238. replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
  239. replacement.textContent = ok ? newName : oldName;
  240. input.replaceWith(replacement);
  241. });
  242. }
  243. function abort() {
  244. var replacement = document.createElement('span');
  245. replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
  246. replacement.textContent = oldName;
  247. input.replaceWith(replacement);
  248. }
  249. input.addEventListener('blur', commit);
  250. input.addEventListener('keydown', function (e) {
  251. if (e.key === 'Enter') { e.preventDefault(); commit(); }
  252. if (e.key === 'Escape') { e.preventDefault(); abort(); }
  253. });
  254. }
  255. })();

Powered by TurnKey Linux.