pygitweb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygitweb/__init__.py +22 -0
- pygitweb/__meta__.py +9 -0
- pygitweb/actions.py +1303 -0
- pygitweb/addproject_permissions_test.py +114 -0
- pygitweb/api/__init__.py +0 -0
- pygitweb/api/action.py +20 -0
- pygitweb/api/plugins/__init__.py +0 -0
- pygitweb/api/plugins/project_zip.py +71 -0
- pygitweb/api/project.py +67 -0
- pygitweb/api/subpage.py +22 -0
- pygitweb/auth.py +387 -0
- pygitweb/auth.schema.json +106 -0
- pygitweb/auth_config.py +243 -0
- pygitweb/auth_oauth.py +343 -0
- pygitweb/auth_test.py +390 -0
- pygitweb/boards_permissions_test.py +113 -0
- pygitweb/change_queue.py +110 -0
- pygitweb/change_queue_routes_test.py +146 -0
- pygitweb/change_queue_test.py +91 -0
- pygitweb/comments_permissions_test.py +140 -0
- pygitweb/config.py +185 -0
- pygitweb/conftest.py +43 -0
- pygitweb/dependencies.py +87 -0
- pygitweb/formatting.py +182 -0
- pygitweb/formatting_test.py +68 -0
- pygitweb/git_helpers.py +454 -0
- pygitweb/git_helpers_test.py +223 -0
- pygitweb/gravatar.py +16 -0
- pygitweb/gravatar_test.py +10 -0
- pygitweb/hooks_install.py +278 -0
- pygitweb/hooks_install_test.py +363 -0
- pygitweb/hooks_permissions_test.py +116 -0
- pygitweb/main.py +887 -0
- pygitweb/merge_requests.py +230 -0
- pygitweb/merge_requests_test.py +85 -0
- pygitweb/mr_permissions_test.py +148 -0
- pygitweb/password_hash.py +74 -0
- pygitweb/password_hash_test.py +20 -0
- pygitweb/permissions.py +209 -0
- pygitweb/permissions_test.py +86 -0
- pygitweb/plugin_loader.py +83 -0
- pygitweb/projects.py +213 -0
- pygitweb/projects_test.py +186 -0
- pygitweb/sessions.py +73 -0
- pygitweb/settings.py +412 -0
- pygitweb/settings.schema.json +73 -0
- pygitweb/settings_config.py +122 -0
- pygitweb/settings_config_test.py +101 -0
- pygitweb/settings_permissions_test.py +76 -0
- pygitweb/static/blob-view.js +133 -0
- pygitweb/static/board.js +839 -0
- pygitweb/static/hook-install.js +53 -0
- pygitweb/static/icons/cloud-down.svg +1 -0
- pygitweb/static/icons/cloud-exclamation.svg +1 -0
- pygitweb/static/icons/cloud-filled.svg +1 -0
- pygitweb/static/icons/cloud-storm.svg +1 -0
- pygitweb/static/icons/cloud-up.svg +1 -0
- pygitweb/static/icons/cloud.svg +1 -0
- pygitweb/static/logo.svg +44 -0
- pygitweb/static/main.css +1210 -0
- pygitweb/static/navbar-auth.js +82 -0
- pygitweb/static/readme-render.js +174 -0
- pygitweb/static/search-page.js +139 -0
- pygitweb/static/summary-ref-switcher.js +130 -0
- pygitweb/static/themes/solarized.css +34 -0
- pygitweb/tasks.py +595 -0
- pygitweb/tasks_permissions_test.py +137 -0
- pygitweb/tasks_routes_test.py +430 -0
- pygitweb/templates/addproject.html +174 -0
- pygitweb/templates/blobdiff.html +57 -0
- pygitweb/templates/board.html +31 -0
- pygitweb/templates/board_card.html +44 -0
- pygitweb/templates/commitdiff.html +57 -0
- pygitweb/templates/hooks_summary_cell.html +37 -0
- pygitweb/templates/login.html +37 -0
- pygitweb/templates/merge_request_summary_cell.html +18 -0
- pygitweb/templates/merge_request_tag.html +90 -0
- pygitweb/templates/pagination.html +44 -0
- pygitweb/templates/preamble.html +89 -0
- pygitweb/templates/project_settings.html +13 -0
- pygitweb/templates/pygit2_settings.html +12 -0
- pygitweb/templates/pygitweb_settings.html +12 -0
- pygitweb/templates/search.html +77 -0
- pygitweb/templates/settings_form_field.html +27 -0
- pygitweb/templates/table.html +15 -0
- pygitweb/templates/user_profile.html +10 -0
- pygitweb/templates_env.py +21 -0
- pygitweb/timeline_cache.py +243 -0
- pygitweb/validation.py +75 -0
- pygitweb/validation_test.py +167 -0
- pygitweb-0.1.0.dist-info/METADATA +165 -0
- pygitweb-0.1.0.dist-info/RECORD +95 -0
- pygitweb-0.1.0.dist-info/WHEEL +5 -0
- pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
- pygitweb-0.1.0.dist-info/top_level.txt +1 -0
pygitweb/static/board.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var template = document.getElementById('board-card-template');
|
|
3
|
+
var addCardButtons = document.querySelectorAll('.board-column-add-card');
|
|
4
|
+
|
|
5
|
+
var ICONS_BASE = '/static/icons/';
|
|
6
|
+
var ICON_LIST = ['cloud.svg', 'cloud-filled.svg', 'cloud-storm.svg', 'cloud-down.svg', 'cloud-up.svg', 'cloud-exclamation.svg'];
|
|
7
|
+
var boardMarkdownConverter = null;
|
|
8
|
+
|
|
9
|
+
function whenShowdownReady(cb) {
|
|
10
|
+
if (typeof showdown !== 'undefined') {
|
|
11
|
+
cb();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
var script = document.getElementById('Showdown-script');
|
|
15
|
+
if (!script) return;
|
|
16
|
+
function run() {
|
|
17
|
+
if (typeof showdown !== 'undefined') cb();
|
|
18
|
+
}
|
|
19
|
+
script.addEventListener('load', run);
|
|
20
|
+
if (script.getAttribute('data-loaded')) run();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBoardMarkdownConverter() {
|
|
24
|
+
if (!boardMarkdownConverter && typeof showdown !== 'undefined') {
|
|
25
|
+
boardMarkdownConverter = new showdown.Converter({ ghCompatibleHeaderId: true });
|
|
26
|
+
}
|
|
27
|
+
return boardMarkdownConverter;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function markdownToHtml(source) {
|
|
31
|
+
var conv = getBoardMarkdownConverter();
|
|
32
|
+
if (!conv) return null;
|
|
33
|
+
return conv.makeHtml(source || '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function htmlToPlainPreview(html) {
|
|
37
|
+
var tmp = document.createElement('div');
|
|
38
|
+
tmp.innerHTML = html;
|
|
39
|
+
return (tmp.textContent || '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function markdownPlainPreview(source, maxLen) {
|
|
43
|
+
var md = source || '';
|
|
44
|
+
var html = markdownToHtml(md);
|
|
45
|
+
var text = html !== null ? htmlToPlainPreview(html) : md.replace(/\s+/g, ' ').trim();
|
|
46
|
+
if (maxLen && text.length > maxLen) return text.slice(0, maxLen) + '\u2026';
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMarkdownFromElement(el) {
|
|
51
|
+
if (!el) return '';
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(el.dataset, 'boardMarkdown')) {
|
|
53
|
+
return el.dataset.boardMarkdown;
|
|
54
|
+
}
|
|
55
|
+
return (el.textContent || '').trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setMarkdownElement(el, source) {
|
|
59
|
+
if (!el) return;
|
|
60
|
+
var md = source || '';
|
|
61
|
+
el.dataset.boardMarkdown = md;
|
|
62
|
+
el.classList.add('markdown-body', 'board-card-markdown');
|
|
63
|
+
function apply() {
|
|
64
|
+
var html = markdownToHtml(md);
|
|
65
|
+
if (html !== null) {
|
|
66
|
+
el.innerHTML = html;
|
|
67
|
+
} else {
|
|
68
|
+
el.textContent = md;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof showdown !== 'undefined') {
|
|
72
|
+
apply();
|
|
73
|
+
} else {
|
|
74
|
+
el.textContent = md;
|
|
75
|
+
whenShowdownReady(apply);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function syncCompactDescription(card, source) {
|
|
80
|
+
var preview = card.querySelector('.board-card-description-preview');
|
|
81
|
+
var full = card.querySelector('.board-card-description-full');
|
|
82
|
+
var md = source !== undefined ? source : getMarkdownFromElement(full);
|
|
83
|
+
if (preview) {
|
|
84
|
+
preview.textContent = md ? markdownPlainPreview(md, 120) : '';
|
|
85
|
+
preview.dataset.boardMarkdown = md;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function preloadIcons() {
|
|
90
|
+
ICON_LIST.forEach(function(name) {
|
|
91
|
+
var img = new Image();
|
|
92
|
+
img.src = ICONS_BASE + name;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setCardIcon(card, iconName) {
|
|
97
|
+
var icon = card && card.querySelector && card.querySelector('.board-card-icon');
|
|
98
|
+
if (icon) icon.setAttribute('data-icon', iconName);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assignCloudIconToCard(card) {
|
|
102
|
+
setCardIcon(card, 'cloud');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assignCloudIconToAllCards() {
|
|
106
|
+
document.querySelectorAll('.board-card').forEach(assignCloudIconToCard);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
preloadIcons();
|
|
110
|
+
function initBoard() {
|
|
111
|
+
assignCloudIconToAllCards();
|
|
112
|
+
loadTasksForBoard();
|
|
113
|
+
}
|
|
114
|
+
if (document.readyState === 'loading') {
|
|
115
|
+
document.addEventListener('DOMContentLoaded', initBoard);
|
|
116
|
+
} else {
|
|
117
|
+
initBoard();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getBoardContext() {
|
|
121
|
+
var boardColumns = document.querySelector('.board-columns');
|
|
122
|
+
var project = boardColumns && boardColumns.dataset.project;
|
|
123
|
+
var board = boardColumns && boardColumns.dataset.board;
|
|
124
|
+
if (!project && location.pathname) {
|
|
125
|
+
var m = location.pathname.match(/^\/project\/(.+?)\/board\/?$/);
|
|
126
|
+
if (m) project = m[1];
|
|
127
|
+
}
|
|
128
|
+
if (!board) board = (new URLSearchParams(location.search)).get('b') || 'Tasks';
|
|
129
|
+
return { project: project || '', board: board || 'Tasks' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createCardFromTask(task) {
|
|
133
|
+
if (!template || !template.content) return null;
|
|
134
|
+
var clone = template.content.cloneNode(true);
|
|
135
|
+
var card = clone.querySelector && clone.querySelector('.board-card');
|
|
136
|
+
if (!card) return null;
|
|
137
|
+
var status = task.status || 'TODO';
|
|
138
|
+
card.dataset.taskRef = task.ref || '';
|
|
139
|
+
card.dataset.taskStatus = status;
|
|
140
|
+
var titleEl = card.querySelector('.board-card-title');
|
|
141
|
+
if (titleEl) titleEl.textContent = task.title || 'Untitled';
|
|
142
|
+
var descFull = card.querySelector('.board-card-description-full');
|
|
143
|
+
if (descFull) setMarkdownElement(descFull, task.description || '');
|
|
144
|
+
syncCompactDescription(card, task.description || '');
|
|
145
|
+
setCardIcon(card, 'cloud-filled');
|
|
146
|
+
var meta = card.querySelector('.board-card-meta');
|
|
147
|
+
if (meta) {
|
|
148
|
+
var badge = meta.querySelector('.badge');
|
|
149
|
+
var assigneeSpan = meta.querySelectorAll('.text-truncate')[0];
|
|
150
|
+
var commentsSpan = meta.querySelector('.board-card-comments-count-meta') || meta.querySelector('.ms-auto');
|
|
151
|
+
if (badge) {
|
|
152
|
+
badge.textContent = task.priority || 'LOW';
|
|
153
|
+
badge.className = 'badge badge-outline text-' + (task.priority === 'CRITICAL' ? 'danger' : task.priority === 'HIGH' ? 'warning' : 'secondary');
|
|
154
|
+
}
|
|
155
|
+
if (assigneeSpan) assigneeSpan.textContent = task.assignee || 'None';
|
|
156
|
+
if (commentsSpan) commentsSpan.textContent = commentCountLabel(task.comments_count || 0);
|
|
157
|
+
}
|
|
158
|
+
var details = card.querySelector('.board-card-details');
|
|
159
|
+
if (details) {
|
|
160
|
+
var dueDate = details.querySelector('.board-card-due-date');
|
|
161
|
+
var updatedAt = details.querySelector('.board-card-updated-at');
|
|
162
|
+
var statusEl = details.querySelector('.board-card-status');
|
|
163
|
+
var priorityEl = details.querySelector('.board-card-priority');
|
|
164
|
+
var assigneeEl = details.querySelector('.board-card-assignee');
|
|
165
|
+
if (dueDate) dueDate.textContent = task.due_date || 'None';
|
|
166
|
+
if (updatedAt) updatedAt.textContent = task.updated_at || 'None';
|
|
167
|
+
if (statusEl) statusEl.textContent = status;
|
|
168
|
+
if (priorityEl) priorityEl.textContent = task.priority || 'LOW';
|
|
169
|
+
if (assigneeEl) assigneeEl.textContent = task.assignee || 'None';
|
|
170
|
+
updateCardCommentCount(card, task.comments_count || 0);
|
|
171
|
+
}
|
|
172
|
+
return clone;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function groupTasksByStatus(tasks) {
|
|
176
|
+
var byStatus = {};
|
|
177
|
+
if (!Array.isArray(tasks)) return byStatus;
|
|
178
|
+
tasks.forEach(function(task) {
|
|
179
|
+
var s = task.status || 'TODO';
|
|
180
|
+
if (!byStatus[s]) byStatus[s] = [];
|
|
181
|
+
byStatus[s].push(task);
|
|
182
|
+
});
|
|
183
|
+
return byStatus;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function loadTasksForBoard() {
|
|
187
|
+
var ctx = getBoardContext();
|
|
188
|
+
if (!ctx.project) return;
|
|
189
|
+
var listUrl = '/tasks/list?' + new URLSearchParams({ project: ctx.project, board: ctx.board }).toString();
|
|
190
|
+
fetch(listUrl).then(function(res) {
|
|
191
|
+
if (!res.ok) return;
|
|
192
|
+
return res.json();
|
|
193
|
+
}).then(function(tasks) {
|
|
194
|
+
if (!Array.isArray(tasks)) return;
|
|
195
|
+
var byStatus = groupTasksByStatus(tasks);
|
|
196
|
+
var columns = document.querySelectorAll('.board-column.dropzone');
|
|
197
|
+
columns.forEach(function(column) {
|
|
198
|
+
var status = column.dataset.status || 'TODO';
|
|
199
|
+
var cardsContainer = column.querySelector('.board-column-cards');
|
|
200
|
+
if (!cardsContainer) return;
|
|
201
|
+
cardsContainer.innerHTML = '';
|
|
202
|
+
(byStatus[status] || []).forEach(function(task) {
|
|
203
|
+
var frag = createCardFromTask(task);
|
|
204
|
+
if (frag) cardsContainer.appendChild(frag);
|
|
205
|
+
});
|
|
206
|
+
updateColumnBadge(column);
|
|
207
|
+
});
|
|
208
|
+
}).catch(function() {});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function fetchBoardTasksJson() {
|
|
212
|
+
var ctx = getBoardContext();
|
|
213
|
+
if (!ctx.project) return Promise.resolve(null);
|
|
214
|
+
var listUrl = '/tasks/list?' + new URLSearchParams({ project: ctx.project, board: ctx.board }).toString();
|
|
215
|
+
return fetch(listUrl).then(function(res) {
|
|
216
|
+
return res.ok ? res.json() : null;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function reorderBoardDomToTaskList(tasks) {
|
|
221
|
+
if (!Array.isArray(tasks)) return;
|
|
222
|
+
var byStatus = groupTasksByStatus(tasks);
|
|
223
|
+
document.querySelectorAll('.board-column.dropzone').forEach(function(column) {
|
|
224
|
+
var status = column.dataset.status || 'TODO';
|
|
225
|
+
var cardsContainer = column.querySelector('.board-column-cards');
|
|
226
|
+
if (!cardsContainer) return;
|
|
227
|
+
var ordered = byStatus[status] || [];
|
|
228
|
+
ordered.forEach(function(task) {
|
|
229
|
+
if (!task.ref) return;
|
|
230
|
+
var card = findBoardCardByTaskRef(task.ref);
|
|
231
|
+
if (card) cardsContainer.appendChild(card);
|
|
232
|
+
});
|
|
233
|
+
updateColumnBadge(column);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function addDefaultCard(ev) {
|
|
238
|
+
if (!template || !template.content) return;
|
|
239
|
+
var column = ev.currentTarget.closest('.board-column');
|
|
240
|
+
var cardsContainer = column && column.querySelector('.board-column-cards');
|
|
241
|
+
var boardColumns = document.querySelector('.board-columns');
|
|
242
|
+
if (!cardsContainer || !boardColumns) return;
|
|
243
|
+
var ctx = getBoardContext();
|
|
244
|
+
var project = ctx.project;
|
|
245
|
+
var board = ctx.board;
|
|
246
|
+
if (!project) return;
|
|
247
|
+
var clone = template.content.cloneNode(true);
|
|
248
|
+
var card = clone.querySelector && clone.querySelector('.board-card');
|
|
249
|
+
if (!card) return;
|
|
250
|
+
var titleEl = card.querySelector('.board-card-title');
|
|
251
|
+
if (titleEl) titleEl.textContent = 'New task';
|
|
252
|
+
assignCloudIconToCard(card);
|
|
253
|
+
cardsContainer.appendChild(clone);
|
|
254
|
+
var badge = column.querySelector('.board-column-add-card');
|
|
255
|
+
if (badge) {
|
|
256
|
+
badge.textContent = cardsContainer.querySelectorAll('.board-card').length;
|
|
257
|
+
}
|
|
258
|
+
var status = column.dataset.status || 'TODO';
|
|
259
|
+
setCardIcon(card, 'cloud-up');
|
|
260
|
+
var createUrl = '/tasks/create?' + new URLSearchParams({
|
|
261
|
+
project: project,
|
|
262
|
+
board: board,
|
|
263
|
+
title: 'New task',
|
|
264
|
+
description: '',
|
|
265
|
+
status: status,
|
|
266
|
+
priority: 'LOW',
|
|
267
|
+
assignee: '',
|
|
268
|
+
due_date: ''
|
|
269
|
+
}).toString();
|
|
270
|
+
fetch(createUrl, { method: 'POST' }).then(function(res) {
|
|
271
|
+
if (res.ok) {
|
|
272
|
+
return res.json().then(function(body) {
|
|
273
|
+
setCardIcon(card, 'cloud-filled');
|
|
274
|
+
if (body.ref) card.dataset.taskRef = body.ref;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (res.status >= 400 && res.status < 600) {
|
|
278
|
+
setCardIcon(card, 'cloud-exclamation');
|
|
279
|
+
}
|
|
280
|
+
}).catch(function() {
|
|
281
|
+
setCardIcon(card, 'cloud-exclamation');
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
addCardButtons.forEach(function(btn) {
|
|
286
|
+
btn.addEventListener('click', addDefaultCard);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
var boardColumns = document.querySelector('.board-columns');
|
|
290
|
+
var draggedCard = null;
|
|
291
|
+
var didDrag = false;
|
|
292
|
+
var boardAuth = { auth_enabled: false, username: null };
|
|
293
|
+
|
|
294
|
+
function loadBoardAuthStatus() {
|
|
295
|
+
return fetch('/auth/status', { credentials: 'same-origin' })
|
|
296
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
297
|
+
.then(function(s) {
|
|
298
|
+
if (!s) return;
|
|
299
|
+
boardAuth.auth_enabled = !!s.auth_enabled;
|
|
300
|
+
boardAuth.username = s.username || null;
|
|
301
|
+
})
|
|
302
|
+
.catch(function() {});
|
|
303
|
+
}
|
|
304
|
+
loadBoardAuthStatus();
|
|
305
|
+
|
|
306
|
+
function canAddComment() {
|
|
307
|
+
return !boardAuth.auth_enabled || !!boardAuth.username;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function commentCountLabel(n) {
|
|
311
|
+
return n + ' comment' + (n !== 1 ? 's' : '');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function updateCardCommentCount(card, count) {
|
|
315
|
+
var n = count || 0;
|
|
316
|
+
var meta = card.querySelector('.board-card-meta');
|
|
317
|
+
if (meta) {
|
|
318
|
+
var commentsSpan = meta.querySelector('.board-card-comments-count-meta') || meta.querySelector('.ms-auto');
|
|
319
|
+
if (commentsSpan) commentsSpan.textContent = commentCountLabel(n);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function commentAuthorInitials(author) {
|
|
324
|
+
var parts = (author || '').trim().split(/\s+/).filter(Boolean);
|
|
325
|
+
if (!parts.length) return '?';
|
|
326
|
+
if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
|
|
327
|
+
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function commentAvatarHtml(comment) {
|
|
331
|
+
var wrap = document.createElement('span');
|
|
332
|
+
wrap.className = 'board-card-comment-avatar';
|
|
333
|
+
if (comment.gravatar_url) {
|
|
334
|
+
var img = document.createElement('img');
|
|
335
|
+
img.src = comment.gravatar_url;
|
|
336
|
+
img.alt = '';
|
|
337
|
+
img.width = 32;
|
|
338
|
+
img.height = 32;
|
|
339
|
+
img.onerror = function() {
|
|
340
|
+
wrap.replaceChildren();
|
|
341
|
+
var fb = document.createElement('span');
|
|
342
|
+
fb.className = 'board-card-comment-avatar-fallback';
|
|
343
|
+
fb.textContent = commentAuthorInitials(comment.author);
|
|
344
|
+
wrap.appendChild(fb);
|
|
345
|
+
};
|
|
346
|
+
wrap.appendChild(img);
|
|
347
|
+
} else {
|
|
348
|
+
var fb = document.createElement('span');
|
|
349
|
+
fb.className = 'board-card-comment-avatar-fallback';
|
|
350
|
+
fb.textContent = commentAuthorInitials(comment.author);
|
|
351
|
+
wrap.appendChild(fb);
|
|
352
|
+
}
|
|
353
|
+
return wrap;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderCommentsList(card, comments) {
|
|
357
|
+
var list = card.querySelector('.board-card-comments-list');
|
|
358
|
+
if (!list) return;
|
|
359
|
+
list.innerHTML = '';
|
|
360
|
+
if (!Array.isArray(comments) || !comments.length) return;
|
|
361
|
+
comments.forEach(function(comment) {
|
|
362
|
+
var row = document.createElement('div');
|
|
363
|
+
row.className = 'board-card-comment-item';
|
|
364
|
+
row.appendChild(commentAvatarHtml(comment));
|
|
365
|
+
var body = document.createElement('div');
|
|
366
|
+
body.className = 'board-card-comment-body';
|
|
367
|
+
var authorEl = document.createElement('div');
|
|
368
|
+
authorEl.className = 'board-card-comment-author';
|
|
369
|
+
authorEl.textContent = comment.author || 'Unknown';
|
|
370
|
+
var contentEl = document.createElement('div');
|
|
371
|
+
contentEl.className = 'board-card-comment-content';
|
|
372
|
+
setMarkdownElement(contentEl, comment.content || '');
|
|
373
|
+
body.appendChild(authorEl);
|
|
374
|
+
body.appendChild(contentEl);
|
|
375
|
+
row.appendChild(body);
|
|
376
|
+
list.appendChild(row);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function syncCommentFormVisibility(card) {
|
|
381
|
+
var form = card.querySelector('.board-card-comment-form');
|
|
382
|
+
var hint = card.querySelector('.board-card-comment-signin-hint');
|
|
383
|
+
if (!form || !hint) return;
|
|
384
|
+
if (canAddComment()) {
|
|
385
|
+
form.classList.remove('d-none');
|
|
386
|
+
hint.classList.add('d-none');
|
|
387
|
+
} else {
|
|
388
|
+
form.classList.add('d-none');
|
|
389
|
+
hint.classList.remove('d-none');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function loadCommentsForCard(card) {
|
|
394
|
+
var taskRef = card.dataset.taskRef;
|
|
395
|
+
if (!taskRef) return Promise.resolve();
|
|
396
|
+
var ctx = getBoardContext();
|
|
397
|
+
if (!ctx.project) return Promise.resolve();
|
|
398
|
+
var listUrl = '/comments/list?' + new URLSearchParams({
|
|
399
|
+
project: ctx.project,
|
|
400
|
+
board: ctx.board,
|
|
401
|
+
task: taskRef
|
|
402
|
+
}).toString();
|
|
403
|
+
return fetch(listUrl, { credentials: 'same-origin' })
|
|
404
|
+
.then(function(res) { return res.ok ? res.json() : []; })
|
|
405
|
+
.then(function(comments) {
|
|
406
|
+
if (!Array.isArray(comments)) comments = [];
|
|
407
|
+
renderCommentsList(card, comments);
|
|
408
|
+
updateCardCommentCount(card, comments.length);
|
|
409
|
+
return comments;
|
|
410
|
+
})
|
|
411
|
+
.catch(function() { return []; });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function setupCommentForm(popoutCard) {
|
|
415
|
+
var form = popoutCard.querySelector('.board-card-comment-form');
|
|
416
|
+
if (!form || form.dataset.bound === '1') return;
|
|
417
|
+
form.dataset.bound = '1';
|
|
418
|
+
syncCommentFormVisibility(popoutCard);
|
|
419
|
+
form.addEventListener('submit', function(e) {
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
if (!canAddComment()) return;
|
|
422
|
+
var taskRef = popoutCard.dataset.taskRef;
|
|
423
|
+
if (!taskRef) return;
|
|
424
|
+
var input = form.querySelector('.board-card-comment-input');
|
|
425
|
+
var content = (input && input.value) ? input.value.trim() : '';
|
|
426
|
+
if (!content) return;
|
|
427
|
+
var ctx = getBoardContext();
|
|
428
|
+
if (!ctx.project) return;
|
|
429
|
+
var createUrl = '/comments/create?' + new URLSearchParams({
|
|
430
|
+
project: ctx.project,
|
|
431
|
+
board: ctx.board,
|
|
432
|
+
task: taskRef,
|
|
433
|
+
content: content
|
|
434
|
+
}).toString();
|
|
435
|
+
fetch(createUrl, { method: 'POST', credentials: 'same-origin' })
|
|
436
|
+
.then(function(res) {
|
|
437
|
+
if (!res.ok) {
|
|
438
|
+
var msg = (res.status === 401 || res.status === 403)
|
|
439
|
+
? 'Not authorized to add comments (sign in again if needed).'
|
|
440
|
+
: 'Could not post comment.';
|
|
441
|
+
showBoardError(msg);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (input) input.value = '';
|
|
445
|
+
return loadCommentsForCard(popoutCard);
|
|
446
|
+
})
|
|
447
|
+
.catch(function() {
|
|
448
|
+
showBoardError('Could not post comment.');
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function findBoardCardByTaskRef(taskRef) {
|
|
454
|
+
if (!taskRef) return null;
|
|
455
|
+
var cards = document.querySelectorAll('.board-card');
|
|
456
|
+
for (var i = 0; i < cards.length; i++) {
|
|
457
|
+
if (cards[i].dataset.taskRef === taskRef) return cards[i];
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function clearColumnDropHighlight() {
|
|
463
|
+
document.querySelectorAll('.board-column.dropzone.border-primary').forEach(function(el) {
|
|
464
|
+
el.classList.remove('border-primary');
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function dragTransferLooksLikeTask(e) {
|
|
469
|
+
if (draggedCard) return true;
|
|
470
|
+
var types = e.dataTransfer && e.dataTransfer.types;
|
|
471
|
+
if (!types || !types.length) return false;
|
|
472
|
+
for (var i = 0; i < types.length; i++) {
|
|
473
|
+
if (types[i] === 'text/plain' || types[i] === 'Text') return true;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function showBoardError(message) {
|
|
479
|
+
var anchor = boardColumns && boardColumns.parentNode;
|
|
480
|
+
if (!anchor) return;
|
|
481
|
+
var existing = document.querySelector('.board-task-error-hint');
|
|
482
|
+
if (existing) existing.remove();
|
|
483
|
+
var el = document.createElement('div');
|
|
484
|
+
el.className = 'alert board-task-error-hint py-2 px-3 mb-2';
|
|
485
|
+
el.setAttribute('role', 'alert');
|
|
486
|
+
el.textContent = message;
|
|
487
|
+
anchor.insertBefore(el, boardColumns);
|
|
488
|
+
window.setTimeout(function() {
|
|
489
|
+
if (el.parentNode) el.remove();
|
|
490
|
+
}, 8000);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function updateColumnBadge(column) {
|
|
494
|
+
var cardsContainer = column && column.querySelector('.board-column-cards');
|
|
495
|
+
var badge = column && column.querySelector('.board-column-add-card');
|
|
496
|
+
if (badge && cardsContainer) {
|
|
497
|
+
badge.textContent = cardsContainer.querySelectorAll('.board-card').length;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function handleDragStart(e) {
|
|
502
|
+
var card = e.target.closest && e.target.closest('.board-card');
|
|
503
|
+
if (!card) return;
|
|
504
|
+
didDrag = true;
|
|
505
|
+
draggedCard = card;
|
|
506
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
507
|
+
try {
|
|
508
|
+
e.dataTransfer.setData('text/plain', card.dataset.taskRef || '');
|
|
509
|
+
} catch (err) {
|
|
510
|
+
/* ignore setData failures in restricted environments */
|
|
511
|
+
}
|
|
512
|
+
card.classList.add('opacity-50');
|
|
513
|
+
}
|
|
514
|
+
function handleDragEnd(e) {
|
|
515
|
+
var card = (draggedCard && draggedCard.classList.contains('opacity-50') ? draggedCard : null) ||
|
|
516
|
+
(e.target.closest && e.target.closest('.board-card'));
|
|
517
|
+
if (card) card.classList.remove('opacity-50');
|
|
518
|
+
clearColumnDropHighlight();
|
|
519
|
+
window.setTimeout(function() {
|
|
520
|
+
draggedCard = null;
|
|
521
|
+
didDrag = false;
|
|
522
|
+
}, 0);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Parse display date text to YYYY-MM-DD for input[type=date]. Returns '' if none. */
|
|
526
|
+
function parseDateForInput(text) {
|
|
527
|
+
if (!text || text.trim() === 'None') return '';
|
|
528
|
+
var s = text.trim();
|
|
529
|
+
var m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
530
|
+
return m ? m[0] : '';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function setupPopoutEditables(popoutCard) {
|
|
534
|
+
var titleEl = popoutCard.querySelector('.board-card-title');
|
|
535
|
+
var descEl = popoutCard.querySelector('.board-card-description-full');
|
|
536
|
+
var dueEl = popoutCard.querySelector('.board-card-due-date');
|
|
537
|
+
var statusEl = popoutCard.querySelector('.board-card-status');
|
|
538
|
+
var priorityEl = popoutCard.querySelector('.board-card-priority');
|
|
539
|
+
|
|
540
|
+
function pushTaskUpdate() {
|
|
541
|
+
var taskRef = popoutCard.dataset.taskRef;
|
|
542
|
+
if (!taskRef) return;
|
|
543
|
+
var ctx = getBoardContext();
|
|
544
|
+
if (!ctx.project) return;
|
|
545
|
+
var tEl = popoutCard.querySelector('.board-card-title');
|
|
546
|
+
var dEl = popoutCard.querySelector('.board-card-description-full');
|
|
547
|
+
var dueEl_ = popoutCard.querySelector('.board-card-due-date');
|
|
548
|
+
var sEl = popoutCard.querySelector('.board-card-status');
|
|
549
|
+
var pEl = popoutCard.querySelector('.board-card-priority');
|
|
550
|
+
var aEl = popoutCard.querySelector('.board-card-assignee');
|
|
551
|
+
var dueVal = (dueEl_ && dueEl_.textContent) ? dueEl_.textContent.trim() : '';
|
|
552
|
+
var statusDisplay = (sEl && sEl.textContent) ? sEl.textContent.trim() : 'TODO';
|
|
553
|
+
var priorityDisplay = (pEl && pEl.textContent) ? pEl.textContent.trim() : 'LOW';
|
|
554
|
+
var body = {
|
|
555
|
+
title: (tEl && tEl.textContent) ? tEl.textContent.trim() || 'Untitled' : 'Untitled',
|
|
556
|
+
description: getMarkdownFromElement(dEl),
|
|
557
|
+
status: statusDisplay ? statusDisplay.replace(/\s+/g, '_').toUpperCase() : 'TODO',
|
|
558
|
+
priority: priorityDisplay ? priorityDisplay.replace(/\s+/g, '_').toUpperCase() : 'LOW',
|
|
559
|
+
assignee: (aEl && aEl.textContent && aEl.textContent.trim() !== 'None') ? aEl.textContent.trim() : null,
|
|
560
|
+
due_date: (dueVal && dueVal !== 'None') ? dueVal : null
|
|
561
|
+
};
|
|
562
|
+
fetch('/tasks/update?' + new URLSearchParams({ project: ctx.project, board: ctx.board, task: taskRef }).toString(), {
|
|
563
|
+
method: 'POST',
|
|
564
|
+
headers: { 'Content-Type': 'application/json' },
|
|
565
|
+
body: JSON.stringify(body)
|
|
566
|
+
}).catch(function() {});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (titleEl) titleEl.classList.add('board-card-editable');
|
|
570
|
+
function commitTitle() {
|
|
571
|
+
var input = popoutCard.querySelector('.board-card-title-edit');
|
|
572
|
+
if (!input) return;
|
|
573
|
+
var val = (input.value || '').trim() || 'Untitled';
|
|
574
|
+
titleEl.textContent = val;
|
|
575
|
+
titleEl.classList.remove('d-none');
|
|
576
|
+
input.remove();
|
|
577
|
+
pushTaskUpdate();
|
|
578
|
+
}
|
|
579
|
+
if (titleEl) {
|
|
580
|
+
titleEl.addEventListener('click', function () {
|
|
581
|
+
if (popoutCard.querySelector('.board-card-title-edit')) return;
|
|
582
|
+
var val = (titleEl.textContent || '').trim();
|
|
583
|
+
var input = document.createElement('input');
|
|
584
|
+
input.type = 'text';
|
|
585
|
+
input.className = 'form-control form-control-sm board-card-title-edit';
|
|
586
|
+
input.value = val;
|
|
587
|
+
titleEl.classList.add('d-none');
|
|
588
|
+
titleEl.parentNode.insertBefore(input, titleEl);
|
|
589
|
+
input.focus();
|
|
590
|
+
input.addEventListener('blur', commitTitle);
|
|
591
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); input.blur(); } });
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function commitDescription() {
|
|
596
|
+
var textarea = popoutCard.querySelector('.board-card-description-edit');
|
|
597
|
+
if (!textarea) return;
|
|
598
|
+
var val = (textarea.value || '').trim();
|
|
599
|
+
setMarkdownElement(descEl, val);
|
|
600
|
+
syncCompactDescription(popoutCard, val);
|
|
601
|
+
descEl.classList.remove('d-none');
|
|
602
|
+
textarea.remove();
|
|
603
|
+
pushTaskUpdate();
|
|
604
|
+
}
|
|
605
|
+
if (descEl) {
|
|
606
|
+
descEl.addEventListener('click', function () {
|
|
607
|
+
if (popoutCard.querySelector('.board-card-description-edit')) return;
|
|
608
|
+
var val = getMarkdownFromElement(descEl);
|
|
609
|
+
var textarea = document.createElement('textarea');
|
|
610
|
+
textarea.className = 'form-control form-control-sm board-card-description-edit mt-1';
|
|
611
|
+
textarea.rows = 3;
|
|
612
|
+
textarea.value = val;
|
|
613
|
+
descEl.classList.add('d-none');
|
|
614
|
+
descEl.parentNode.insertBefore(textarea, descEl);
|
|
615
|
+
textarea.focus();
|
|
616
|
+
textarea.addEventListener('blur', commitDescription);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function commitDueDate() {
|
|
621
|
+
var input = popoutCard.querySelector('.board-card-due-date-edit');
|
|
622
|
+
if (!input) return;
|
|
623
|
+
var val = (input.value || '').trim();
|
|
624
|
+
dueEl.textContent = val || 'None';
|
|
625
|
+
dueEl.classList.remove('d-none');
|
|
626
|
+
input.remove();
|
|
627
|
+
pushTaskUpdate();
|
|
628
|
+
}
|
|
629
|
+
if (dueEl) {
|
|
630
|
+
dueEl.addEventListener('click', function () {
|
|
631
|
+
if (popoutCard.querySelector('.board-card-due-date-edit')) return;
|
|
632
|
+
var val = parseDateForInput(dueEl.textContent);
|
|
633
|
+
var input = document.createElement('input');
|
|
634
|
+
input.type = 'date';
|
|
635
|
+
input.className = 'form-control form-control-sm board-card-due-date-edit mt-1';
|
|
636
|
+
input.value = val;
|
|
637
|
+
dueEl.classList.add('d-none');
|
|
638
|
+
dueEl.parentNode.insertBefore(input, dueEl);
|
|
639
|
+
input.focus();
|
|
640
|
+
input.addEventListener('blur', commitDueDate);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function commitStatus() {
|
|
645
|
+
var sel = popoutCard.querySelector('.board-card-status-edit');
|
|
646
|
+
if (!sel) return;
|
|
647
|
+
var display = (sel.options[sel.selectedIndex] && sel.options[sel.selectedIndex].text) ? sel.options[sel.selectedIndex].text : (sel.value || 'TODO');
|
|
648
|
+
statusEl.textContent = display || 'TODO';
|
|
649
|
+
statusEl.classList.remove('d-none');
|
|
650
|
+
sel.remove();
|
|
651
|
+
var statusValue = display ? display.replace(/\s+/g, '_').toUpperCase() : 'TODO';
|
|
652
|
+
popoutCard.dataset.taskStatus = statusValue;
|
|
653
|
+
pushTaskUpdate();
|
|
654
|
+
}
|
|
655
|
+
if (statusEl) {
|
|
656
|
+
statusEl.addEventListener('click', function () {
|
|
657
|
+
if (popoutCard.querySelector('.board-card-status-edit')) return;
|
|
658
|
+
var current = (statusEl.textContent || '').trim() || 'TODO';
|
|
659
|
+
var sel = document.createElement('select');
|
|
660
|
+
sel.className = 'form-select form-select-sm board-card-status-edit mt-1';
|
|
661
|
+
sel.innerHTML = '';
|
|
662
|
+
var opt = document.createElement('option');
|
|
663
|
+
opt.value = current;
|
|
664
|
+
opt.textContent = current;
|
|
665
|
+
opt.selected = true;
|
|
666
|
+
sel.appendChild(opt);
|
|
667
|
+
statusEl.classList.add('d-none');
|
|
668
|
+
statusEl.parentNode.insertBefore(sel, statusEl);
|
|
669
|
+
sel.focus();
|
|
670
|
+
sel.addEventListener('blur', commitStatus);
|
|
671
|
+
sel.addEventListener('change', commitStatus);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function commitPriority() {
|
|
676
|
+
var sel = popoutCard.querySelector('.board-card-priority-edit');
|
|
677
|
+
if (!sel) return;
|
|
678
|
+
var display = (sel.options[sel.selectedIndex] && sel.options[sel.selectedIndex].text) ? sel.options[sel.selectedIndex].text : (sel.value || 'LOW');
|
|
679
|
+
priorityEl.textContent = display || 'LOW';
|
|
680
|
+
priorityEl.classList.remove('d-none');
|
|
681
|
+
sel.remove();
|
|
682
|
+
pushTaskUpdate();
|
|
683
|
+
}
|
|
684
|
+
if (priorityEl) {
|
|
685
|
+
priorityEl.addEventListener('click', function () {
|
|
686
|
+
if (popoutCard.querySelector('.board-card-priority-edit')) return;
|
|
687
|
+
var current = (priorityEl.textContent || '').trim() || 'LOW';
|
|
688
|
+
var sel = document.createElement('select');
|
|
689
|
+
sel.className = 'form-select form-select-sm board-card-priority-edit mt-1';
|
|
690
|
+
sel.innerHTML = '';
|
|
691
|
+
var opt = document.createElement('option');
|
|
692
|
+
opt.value = current;
|
|
693
|
+
opt.textContent = current;
|
|
694
|
+
opt.selected = true;
|
|
695
|
+
sel.appendChild(opt);
|
|
696
|
+
priorityEl.classList.add('d-none');
|
|
697
|
+
priorityEl.parentNode.insertBefore(sel, priorityEl);
|
|
698
|
+
sel.focus();
|
|
699
|
+
sel.addEventListener('blur', commitPriority);
|
|
700
|
+
sel.addEventListener('change', commitPriority);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function showCardPopout(card) {
|
|
706
|
+
var cardsContainer = card.parentNode;
|
|
707
|
+
var nextSibling = card.nextSibling;
|
|
708
|
+
card.classList.add('board-card-popout');
|
|
709
|
+
card.draggable = false;
|
|
710
|
+
card.removeAttribute('draggable');
|
|
711
|
+
var details = card.querySelector('.board-card-details');
|
|
712
|
+
if (details) details.classList.remove('d-none');
|
|
713
|
+
setupPopoutEditables(card);
|
|
714
|
+
setupCommentForm(card);
|
|
715
|
+
var descFull = card.querySelector('.board-card-description-full');
|
|
716
|
+
if (descFull && !Object.prototype.hasOwnProperty.call(descFull.dataset, 'boardMarkdown')) {
|
|
717
|
+
setMarkdownElement(descFull, getMarkdownFromElement(descFull));
|
|
718
|
+
syncCompactDescription(card);
|
|
719
|
+
}
|
|
720
|
+
loadCommentsForCard(card);
|
|
721
|
+
var overlay = document.createElement('div');
|
|
722
|
+
overlay.className = 'board-card-popout-overlay';
|
|
723
|
+
overlay.appendChild(card);
|
|
724
|
+
function close() {
|
|
725
|
+
overlay.removeEventListener('click', closeOnBackdrop);
|
|
726
|
+
document.body.removeChild(overlay);
|
|
727
|
+
card.classList.remove('board-card-popout');
|
|
728
|
+
card.setAttribute('draggable', 'true');
|
|
729
|
+
card.draggable = true;
|
|
730
|
+
if (details) details.classList.add('d-none');
|
|
731
|
+
if (nextSibling) {
|
|
732
|
+
cardsContainer.insertBefore(card, nextSibling);
|
|
733
|
+
} else {
|
|
734
|
+
cardsContainer.appendChild(card);
|
|
735
|
+
}
|
|
736
|
+
updateColumnBadge(cardsContainer.closest('.board-column'));
|
|
737
|
+
}
|
|
738
|
+
function closeOnBackdrop(e) {
|
|
739
|
+
if (e.target === overlay) close();
|
|
740
|
+
}
|
|
741
|
+
overlay.addEventListener('click', closeOnBackdrop);
|
|
742
|
+
document.body.appendChild(overlay);
|
|
743
|
+
updateColumnBadge(cardsContainer.closest('.board-column'));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function handleCardClick(e) {
|
|
747
|
+
if (didDrag) return;
|
|
748
|
+
var card = e.target.closest && e.target.closest('.board-card');
|
|
749
|
+
if (!card || card.classList.contains('board-card-popout')) return;
|
|
750
|
+
e.preventDefault();
|
|
751
|
+
showCardPopout(card);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function handleBoardDragOver(e) {
|
|
755
|
+
if (!boardColumns || !boardColumns.contains(e.target)) return;
|
|
756
|
+
var col = e.target.closest && e.target.closest('.board-column.dropzone');
|
|
757
|
+
if (!col || !dragTransferLooksLikeTask(e)) return;
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
e.dataTransfer.dropEffect = 'move';
|
|
760
|
+
clearColumnDropHighlight();
|
|
761
|
+
col.classList.add('border-primary');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function pushTaskStatusUpdate(card, newStatus) {
|
|
765
|
+
var taskRef = card && card.dataset.taskRef;
|
|
766
|
+
if (!taskRef) return Promise.resolve({ ok: false, status: 0 });
|
|
767
|
+
var ctx = getBoardContext();
|
|
768
|
+
if (!ctx.project) return Promise.resolve({ ok: false, status: 0 });
|
|
769
|
+
return fetch('/tasks/update?' + new URLSearchParams({ project: ctx.project, board: ctx.board, task: taskRef }).toString(), {
|
|
770
|
+
method: 'POST',
|
|
771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
772
|
+
body: JSON.stringify({ status: newStatus || 'TODO' })
|
|
773
|
+
}).then(function(res) {
|
|
774
|
+
return { ok: res.ok, status: res.status };
|
|
775
|
+
}).catch(function() {
|
|
776
|
+
return { ok: false, status: 0 };
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function handleBoardDrop(e) {
|
|
781
|
+
var col = e.target.closest && e.target.closest('.board-column.dropzone');
|
|
782
|
+
if (!col || !boardColumns || !boardColumns.contains(e.target)) return;
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
e.stopPropagation();
|
|
785
|
+
clearColumnDropHighlight();
|
|
786
|
+
var taskRef = '';
|
|
787
|
+
try {
|
|
788
|
+
taskRef = e.dataTransfer.getData('text/plain') || '';
|
|
789
|
+
} catch (err2) {
|
|
790
|
+
taskRef = '';
|
|
791
|
+
}
|
|
792
|
+
var card = draggedCard || findBoardCardByTaskRef(taskRef);
|
|
793
|
+
if (!card) return;
|
|
794
|
+
var targetColumn = col;
|
|
795
|
+
var targetCards = targetColumn.querySelector('.board-column-cards');
|
|
796
|
+
if (!targetCards) return;
|
|
797
|
+
var sourceColumn = card.closest && card.closest('.board-column');
|
|
798
|
+
if (sourceColumn === targetColumn) return;
|
|
799
|
+
var newStatus = targetColumn.dataset.status || 'TODO';
|
|
800
|
+
var prevStatus = card.dataset.taskStatus || 'TODO';
|
|
801
|
+
var sourceCards = sourceColumn && sourceColumn.querySelector('.board-column-cards');
|
|
802
|
+
var nextSibling = card.nextSibling;
|
|
803
|
+
card.dataset.taskStatus = newStatus;
|
|
804
|
+
targetCards.appendChild(card);
|
|
805
|
+
updateColumnBadge(sourceColumn);
|
|
806
|
+
updateColumnBadge(targetColumn);
|
|
807
|
+
pushTaskStatusUpdate(card, newStatus).then(function(result) {
|
|
808
|
+
if (result.ok) {
|
|
809
|
+
fetchBoardTasksJson().then(function(tasks) {
|
|
810
|
+
if (tasks) reorderBoardDomToTaskList(tasks);
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
card.dataset.taskStatus = prevStatus;
|
|
815
|
+
if (sourceCards) {
|
|
816
|
+
if (nextSibling && nextSibling.parentNode === sourceCards) {
|
|
817
|
+
sourceCards.insertBefore(card, nextSibling);
|
|
818
|
+
} else {
|
|
819
|
+
sourceCards.appendChild(card);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
updateColumnBadge(sourceColumn);
|
|
823
|
+
updateColumnBadge(targetColumn);
|
|
824
|
+
var st = result.status;
|
|
825
|
+
var msg = (st === 401 || st === 403)
|
|
826
|
+
? 'Not authorized to update tasks (sign in again if needed). The card was moved back.'
|
|
827
|
+
: 'Could not save the task move. The card was moved back.';
|
|
828
|
+
showBoardError(msg);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (boardColumns) {
|
|
833
|
+
boardColumns.addEventListener('dragstart', handleDragStart);
|
|
834
|
+
boardColumns.addEventListener('dragend', handleDragEnd);
|
|
835
|
+
boardColumns.addEventListener('click', handleCardClick);
|
|
836
|
+
boardColumns.addEventListener('dragover', handleBoardDragOver, true);
|
|
837
|
+
boardColumns.addEventListener('drop', handleBoardDrop, true);
|
|
838
|
+
}
|
|
839
|
+
})();
|