1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
{# Board view: columns (dropzones) and task cards. Expects: project, project_url, board_name, board_url, columns [ { status, label, tasks } ] #}
<template id="board-card-template">
{% include "board_card.html" without context %}
</template>
<h1 class="page-title d-flex align-items-center gap-2">
<a href="{{ project_url }}" class="text-reset">{{ project }}</a>
<span class="text-muted">/</span>
<span>{{ board_name }} board</span>
</h1>
<div class="board-columns d-flex gap-3 overflow-auto pb-2" style="min-height: 70vh;">
{% for col in columns %}
<div class="board-column flex-shrink-0 rounded border p-2 dropzone"
data-status="{{ col.status }}"
style="min-width: 280px; width: 280px;">
<div class="d-flex align-items-center justify-content-between mb-2">
<h3 class="h6 mb-0 text-muted">{{ col.label }}</h3>
<button type="button" class="badge bg-secondary border-0 board-column-add-card" aria-label="Add card">{{ col.tasks | length }}</button>
</div>
<div class="board-column-cards d-flex flex-column">
{% for task in col.tasks %}
{% include "board_card.html" %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<script>
(function() {
var template = document.getElementById('board-card-template');
var addCardButtons = document.querySelectorAll('.board-column-add-card');
function addDefaultCard(ev) {
if (!template || !template.content) return;
var column = ev.currentTarget.closest('.board-column');
var cardsContainer = column && column.querySelector('.board-column-cards');
if (!cardsContainer) return;
var clone = template.content.cloneNode(true);
var card = clone.querySelector && clone.querySelector('.board-card');
if (card) {
var titleEl = card.querySelector('.board-card-title');
if (titleEl) titleEl.textContent = 'New task';
}
cardsContainer.appendChild(clone);
var badge = column.querySelector('.board-column-add-card');
if (badge) {
var n = cardsContainer.querySelectorAll('.board-card').length;
badge.textContent = n;
}
}
addCardButtons.forEach(function(btn) {
btn.addEventListener('click', addDefaultCard);
});
var boardColumns = document.querySelector('.board-columns');
var dropzones = document.querySelectorAll('.board-column.dropzone');
var draggedCard = null;
function updateColumnBadge(column) {
var cardsContainer = column && column.querySelector('.board-column-cards');
var badge = column && column.querySelector('.board-column-add-card');
if (badge && cardsContainer) {
badge.textContent = cardsContainer.querySelectorAll('.board-card').length;
}
}
function handleDragStart(e) {
var card = e.target.closest && e.target.closest('.board-card');
if (!card) return;
draggedCard = card;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', card.dataset.taskRef || '');
card.classList.add('opacity-50');
}
function handleDragEnd(e) {
var card = e.target.closest && e.target.closest('.board-card');
if (card) card.classList.remove('opacity-50');
draggedCard = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.currentTarget.classList.add('border-primary');
}
function handleDragLeave(e) {
e.currentTarget.classList.remove('border-primary');
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('border-primary');
if (!draggedCard) return;
var targetColumn = e.currentTarget.closest && e.currentTarget.closest('.board-column');
var targetCards = targetColumn && targetColumn.querySelector('.board-column-cards');
if (!targetCards) return;
var sourceColumn = draggedCard.closest && draggedCard.closest('.board-column');
if (sourceColumn === targetColumn) return;
draggedCard.dataset.taskStatus = targetColumn.dataset.status || '';
targetCards.appendChild(draggedCard);
updateColumnBadge(sourceColumn);
updateColumnBadge(targetColumn);
// TODO: POST to update task status on server (e.g. /project/.../tasks/update?task=... with body { status })
}
if (boardColumns) {
boardColumns.addEventListener('dragstart', handleDragStart);
boardColumns.addEventListener('dragend', handleDragEnd);
}
dropzones.forEach(function(el) {
el.addEventListener('dragover', handleDragOver);
el.addEventListener('dragleave', handleDragLeave);
el.addEventListener('drop', handleDrop);
});
})();
</script>