|
- /* kanban-settings.js — settings panel: add/rename/delete/reorder columns and lanes */
- (function () {
- 'use strict';
-
- var boardId = KANBAN.boardId;
- var panel = document.getElementById('settings-panel');
- var overlay = document.getElementById('settings-overlay');
-
- /* ── Panel open/close ────────────────────────────────────── */
- document.getElementById('btn-settings').addEventListener('click', openPanel);
- document.getElementById('btn-close-settings').addEventListener('click', closePanel);
- overlay.addEventListener('click', closePanel);
-
- function openPanel() {
- panel.classList.add('open');
- overlay.classList.remove('d-none');
- }
- function closePanel() {
- panel.classList.remove('open');
- overlay.classList.add('d-none');
- }
-
- /* ── Helpers ─────────────────────────────────────────────── */
- function post(url, data, cb) {
- var params = new URLSearchParams();
- Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
- fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() })
- .then(function (r) { return r.json(); })
- .then(cb)
- .catch(function (e) { console.error(url, e); });
- }
-
- function postJson(url, payload, cb) {
- fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
- }).then(function (r) { return r.json(); }).then(cb)
- .catch(function (e) { console.error(url, e); });
- }
-
- function collectOrder(listId) {
- return Array.from(document.querySelectorAll('#' + listId + ' li')).map(function (li, idx) {
- return { id: parseInt(li.dataset.id), position: idx };
- });
- }
-
- function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle, agePrefix) {
- var li = document.createElement('li');
- li.className = 'list-group-item py-2';
- li.dataset.id = id;
- li.innerHTML =
- '<div class="d-flex align-items-center gap-2">' +
- '<i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i>' +
- '<span class="flex-grow-1 ' + labelClass + '">' + esc(name) + '</span>' +
- (countToggle ?
- '<div class="form-check form-switch m-0" title="Show card count in header">' +
- '<input class="form-check-input ' + agePrefix + '-count-toggle" type="checkbox" role="switch">' +
- '</div>' : '') +
- '<div class="form-check form-switch m-0" title="Show export button on board">' +
- '<input class="form-check-input ' + agePrefix + '-export-toggle" type="checkbox" role="switch">' +
- '</div>' +
- '<button class="btn btn-sm btn-link p-0 text-secondary btn-toggle-' + agePrefix + '-age" title="Card age settings"><i class="bi bi-clock-history"></i></button>' +
- '<button class="btn btn-sm btn-link p-0 text-secondary ' + editClass + '" title="Rename"><i class="bi bi-pencil"></i></button>' +
- '<button class="btn btn-sm btn-link p-0 text-danger ' + deleteClass + '" title="Delete"><i class="bi bi-trash"></i></button>' +
- '</div>' +
- '<div class="' + agePrefix + '-age-settings d-none mt-2 ps-4">' +
- '<div class="form-check form-switch mb-1">' +
- '<input class="form-check-input ' + agePrefix + '-age-toggle" type="checkbox" role="switch">' +
- '<label class="form-check-label small">Show "time in cell" tooltip & mark overdue</label>' +
- '</div>' +
- '<div class="input-group input-group-sm" style="max-width: 140px;">' +
- '<input type="number" min="0" step="1" class="form-control ' + agePrefix + '-age-days" value="0" placeholder="0">' +
- '<span class="input-group-text">days</span>' +
- '</div>' +
- '<div class="form-text">Overdue after this many days. 0 = no overdue marking.</div>' +
- '</div>';
- return li;
- }
-
- /* ── Card age settings (per column / per swim lane) ────────── */
- function bindAgeSettings(li, prefix, urlBase, setter) {
- var toggleBtn = li.querySelector('.btn-toggle-' + prefix + '-age');
- var settingsDiv = li.querySelector('.' + prefix + '-age-settings');
- if (toggleBtn && settingsDiv) {
- toggleBtn.addEventListener('click', function () {
- settingsDiv.classList.toggle('d-none');
- });
- }
-
- var ageToggle = li.querySelector('.' + prefix + '-age-toggle');
- var ageDays = li.querySelector('.' + prefix + '-age-days');
- if (!ageToggle || !ageDays) return;
-
- function save() {
- var show = ageToggle.checked;
- var days = parseInt(ageDays.value, 10);
- if (isNaN(days) || days < 0) days = 0;
- ageDays.value = days;
-
- post(urlBase + li.dataset.id + '/card-age-settings', {
- show_card_age: show ? '1' : '0',
- card_age_warning_days: days
- }, function (res) {
- if (!res.ok) {
- alert(res.error || 'Update failed');
- return;
- }
- setter(li.dataset.id, res.show_card_age, res.card_age_warning_days);
- });
- }
-
- ageToggle.addEventListener('change', save);
- ageDays.addEventListener('change', save);
- }
-
- function esc(s) {
- return String(s)
- .replace(/&/g, '&').replace(/</g, '<')
- .replace(/>/g, '>').replace(/"/g, '"');
- }
-
- /* ── Sortable reorder ─────────────────────────────────────── */
- function initSortable(listId, reorderUrl) {
- var el = document.getElementById(listId);
- Sortable.create(el, {
- handle: '.drag-handle',
- animation: 150,
- onEnd: function () {
- postJson(reorderUrl, collectOrder(listId), function (res) {
- if (!res.ok) console.error('Reorder failed', res);
- });
- }
- });
- }
-
- initSortable('col-list', '/columns/reorder');
- initSortable('lane-list', '/swimlanes/reorder');
-
- /* ═══════════════════════════════════════════════════════════
- COLUMNS
- ═══════════════════════════════════════════════════════════ */
-
- /* ── Add column ───────────────────────────────────────────── */
- document.getElementById('btn-add-column').addEventListener('click', function () {
- document.getElementById('col-add-form').classList.remove('d-none');
- document.getElementById('col-add-input').focus();
- });
- document.getElementById('btn-col-add-cancel').addEventListener('click', function () {
- document.getElementById('col-add-form').classList.add('d-none');
- document.getElementById('col-add-input').value = '';
- });
- document.getElementById('btn-col-add-save').addEventListener('click', function () {
- var name = document.getElementById('col-add-input').value.trim();
- if (!name) return;
- post('/columns', { board_id: boardId, name: name }, function (res) {
- if (!res.ok) { alert(res.error || 'Failed'); return; }
- document.getElementById('col-add-form').classList.add('d-none');
- document.getElementById('col-add-input').value = '';
- var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true, 'col');
- document.getElementById('col-list').appendChild(li);
- bindColItem(li);
- window.KanbanBoard.addColumn(res);
- });
- });
-
- /* ── Bind edit/delete on existing column items ─────────────── */
- function bindColItem(li) {
- li.querySelector('.btn-edit-col').addEventListener('click', function () {
- startRename(li, '.col-label-text', function (newName, done) {
- post('/columns/' + li.dataset.id, { name: newName }, function (res) {
- if (res.ok) {
- done(true);
- window.KanbanBoard.renameColumn(li.dataset.id, newName);
- } else {
- done(false);
- alert(res.error || 'Rename failed');
- }
- });
- });
- });
- li.querySelector('.btn-delete-col').addEventListener('click', function () {
- if (!confirm('Delete this column and all its cards?')) return;
- post('/columns/' + li.dataset.id + '/delete', {}, function (res) {
- if (res.ok) {
- window.KanbanBoard.removeColumn(li.dataset.id);
- li.remove();
- } else {
- alert(res.error || 'Delete failed');
- }
- });
- });
- var countToggle = li.querySelector('.col-count-toggle');
- if (countToggle) {
- countToggle.addEventListener('change', function () {
- var show = countToggle.checked;
- post('/columns/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
- if (res.ok) {
- window.KanbanBoard.setColumnShowCount(li.dataset.id, show);
- } else {
- countToggle.checked = !show;
- alert(res.error || 'Update failed');
- }
- });
- });
- }
- var colExportToggle = li.querySelector('.col-export-toggle');
- if (colExportToggle) {
- colExportToggle.addEventListener('change', function () {
- var show = colExportToggle.checked;
- post('/columns/' + li.dataset.id + '/toggle-export', { show_export_button: show ? '1' : '0' }, function (res) {
- if (res.ok) {
- window.KanbanBoard.setColumnShowExport(li.dataset.id, show);
- } else {
- colExportToggle.checked = !show;
- alert(res.error || 'Update failed');
- }
- });
- });
- }
- bindAgeSettings(li, 'col', '/columns/', function (id, show, days) {
- window.KanbanBoard.setColumnCardAge(id, show, days);
- });
- }
-
- document.querySelectorAll('#col-list li').forEach(bindColItem);
-
- /* ═══════════════════════════════════════════════════════════
- SWIM LANES
- ═══════════════════════════════════════════════════════════ */
-
- /* ── Add lane ─────────────────────────────────────────────── */
- document.getElementById('btn-add-lane').addEventListener('click', function () {
- document.getElementById('lane-add-form').classList.remove('d-none');
- document.getElementById('lane-add-input').focus();
- });
- document.getElementById('btn-lane-add-cancel').addEventListener('click', function () {
- document.getElementById('lane-add-form').classList.add('d-none');
- document.getElementById('lane-add-input').value = '';
- });
- document.getElementById('btn-lane-add-save').addEventListener('click', function () {
- var name = document.getElementById('lane-add-input').value.trim();
- if (!name) return;
- post('/swimlanes', { board_id: boardId, name: name }, function (res) {
- if (!res.ok) { alert(res.error || 'Failed'); return; }
- document.getElementById('lane-add-form').classList.add('d-none');
- document.getElementById('lane-add-input').value = '';
- var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text', true, 'lane');
- document.getElementById('lane-list').appendChild(li);
- bindLaneItem(li);
- window.KanbanBoard.addLane(res);
- });
- });
-
- /* ── Bind edit/delete on existing lane items ──────────────── */
- function bindLaneItem(li) {
- li.querySelector('.btn-edit-lane').addEventListener('click', function () {
- startRename(li, '.lane-label-text', function (newName, done) {
- post('/swimlanes/' + li.dataset.id, { name: newName }, function (res) {
- if (res.ok) {
- done(true);
- window.KanbanBoard.renameLane(li.dataset.id, newName);
- } else {
- done(false);
- alert(res.error || 'Rename failed');
- }
- });
- });
- });
- li.querySelector('.btn-delete-lane').addEventListener('click', function () {
- if (!confirm('Delete this swim lane and all its cards?')) return;
- post('/swimlanes/' + li.dataset.id + '/delete', {}, function (res) {
- if (res.ok) {
- window.KanbanBoard.removeLane(li.dataset.id);
- li.remove();
- } else {
- alert(res.error || 'Delete failed');
- }
- });
- });
- var laneCountToggle = li.querySelector('.lane-count-toggle');
- if (laneCountToggle) {
- laneCountToggle.addEventListener('change', function () {
- var show = laneCountToggle.checked;
- post('/swimlanes/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
- if (res.ok) {
- window.KanbanBoard.setLaneShowCount(li.dataset.id, show);
- } else {
- laneCountToggle.checked = !show;
- alert(res.error || 'Update failed');
- }
- });
- });
- }
- var laneExportToggle = li.querySelector('.lane-export-toggle');
- if (laneExportToggle) {
- laneExportToggle.addEventListener('change', function () {
- var show = laneExportToggle.checked;
- post('/swimlanes/' + li.dataset.id + '/toggle-export', { show_export_button: show ? '1' : '0' }, function (res) {
- if (res.ok) {
- window.KanbanBoard.setLaneShowExport(li.dataset.id, show);
- } else {
- laneExportToggle.checked = !show;
- alert(res.error || 'Update failed');
- }
- });
- });
- }
- bindAgeSettings(li, 'lane', '/swimlanes/', function (id, show, days) {
- window.KanbanBoard.setLaneCardAge(id, show, days);
- });
- }
-
- document.querySelectorAll('#lane-list li').forEach(bindLaneItem);
-
- /* ── Inline rename helper ─────────────────────────────────── */
- function startRename(li, labelSel, saveCb) {
- var span = li.querySelector(labelSel);
- var oldName = span.textContent.trim();
- var input = document.createElement('input');
- input.type = 'text';
- input.className = 'form-control form-control-sm inline-rename flex-grow-1';
- input.value = oldName;
- span.replaceWith(input);
- input.focus();
- input.select();
-
- function commit() {
- var newName = input.value.trim();
- if (!newName || newName === oldName) {
- abort();
- return;
- }
- saveCb(newName, function (ok) {
- var replacement = document.createElement('span');
- replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
- replacement.textContent = ok ? newName : oldName;
- input.replaceWith(replacement);
- });
- }
- function abort() {
- var replacement = document.createElement('span');
- replacement.className = labelSel.replace('.', '') + ' flex-grow-1';
- replacement.textContent = oldName;
- input.replaceWith(replacement);
- }
- input.addEventListener('blur', commit);
- input.addEventListener('keydown', function (e) {
- if (e.key === 'Enter') { e.preventDefault(); commit(); }
- if (e.key === 'Escape') { e.preventDefault(); abort(); }
- });
- }
-
- })();
|