[CI] pgitBot
·
2026-02-26
pgit.js
JavaScript
1document.addEventListener('DOMContentLoaded', () => {
2 if (!document.querySelector('.issue-container') && !document.getElementById('new-issue-btn')) {
3 return;
4 }
5
6 // --- GLOBAL STATE ---
7 let isEditing = false; // Mutex to prevent multiple simultaneous edits
8 let confirmCallback = null;
9
10 // --- ELEMENTS ---
11 const aliasBtn = document.getElementById('alias-btn');
12 const aliasModal = document.getElementById('alias-modal');
13 const reactionPicker = document.getElementById('reaction-picker');
14 const replyTextarea = document.getElementById('reply-textarea');
15 const submitCommentBtn = document.getElementById('submit-comment-btn');
16 const newIssueBtn = document.getElementById('new-issue-btn');
17 const currentUserAliasSpan = document.getElementById('current-user-alias');
18 const newIssueModal = document.getElementById('new-issue-modal');
19 const submitNewIssueBtn = document.getElementById('submit-new-issue-btn');
20 const issueFilterInput = document.getElementById('issue-filter-input');
21 const issuesList = document.getElementById('issues-list');
22
23 // --- INITIALIZATION ---
24 updateAliasDisplay();
25 if (reactionPicker) {
26 populateReactionPicker();
27 }
28
29 // --- EVENT LISTENERS ---
30 document.body.addEventListener('click', handleBodyClick);
31 if (aliasBtn) aliasBtn.addEventListener('click', showAliasModal);
32 if (submitCommentBtn) submitCommentBtn.addEventListener('click', handleSubmitNewComment);
33 if (newIssueBtn) newIssueBtn.addEventListener('click', showNewIssueModal);
34 if (submitNewIssueBtn) submitNewIssueBtn.addEventListener('click', handleSubmitNewIssue);
35 if (issueFilterInput && issuesList) {
36 issueFilterInput.addEventListener('input', filterIssues);
37 }
38
39 // --- MODAL & POPUP HANDLING ---
40 function handleBodyClick(e) {
41 if (e.target.classList.contains('close-btn') || e.target.classList.contains('modal')) {
42 const modalId = e.target.dataset.modal || (e.target.closest('.modal')?.id);
43 if (modalId) {
44 document.getElementById(modalId).style.display = 'none';
45 }
46 }
47 if (e.target.id === 'save-alias-btn') {
48 saveAlias();
49 }
50 const actionBtn = e.target.closest('.action-btn');
51 if (actionBtn) {
52 handleTimelineAction(actionBtn);
53 }
54 if (e.target.parentElement?.id === 'reaction-picker') {
55 handleReactionSelection(e.target);
56 }
57 const reactionBadge = e.target.closest('.reaction-badge[data-action="unreact"]');
58 if (reactionBadge) {
59 handleUnreact(reactionBadge);
60 }
61 if (e.target.classList.contains('dropdown-toggle')) {
62 e.preventDefault();
63 toggleDropdown(e.target);
64 } else if (!e.target.closest('.dropdown')) {
65 document.querySelectorAll('.dropdown-menu').forEach(menu => menu.style.display = 'none');
66 }
67 if (e.target.classList.contains('mark-as-btn')) {
68 e.preventDefault();
69 handleMarkAs(e.target);
70 }
71 if (e.target.id === 'confirm-modal-ok-btn') {
72 if (typeof confirmCallback === 'function') {
73 confirmCallback();
74 }
75 document.getElementById('confirm-modal').style.display = 'none';
76 confirmCallback = null;
77 }
78 if (e.target.id === 'confirm-modal-cancel-btn' || e.target.dataset.modal === 'confirm-modal') {
79 document.getElementById('confirm-modal').style.display = 'none';
80 confirmCallback = null;
81 }
82 if (reactionPicker && !reactionPicker.contains(e.target) && !e.target.matches('[data-action="react"]')) {
83 reactionPicker.style.display = 'none';
84 }
85 }
86
87 function toggleDropdown(button) {
88 const dropdownMenu = button.nextElementSibling;
89 const isVisible = dropdownMenu.style.display === 'block';
90 document.querySelectorAll('.dropdown-menu').forEach(menu => menu.style.display = 'none');
91 dropdownMenu.style.display = isVisible ? 'none' : 'block';
92 }
93
94 function showAliasModal() {
95 if (aliasModal) {
96 document.getElementById('alias-input').value = getCookie('pgit_alias') || '';
97 aliasModal.style.display = 'block';
98 }
99 }
100
101 function showNewIssueModal(e) {
102 e.preventDefault();
103 if (newIssueModal) {
104 newIssueModal.style.display = 'block';
105 }
106 }
107
108 function showErrorModal(message) {
109 const modal = document.getElementById('error-modal');
110 if (modal) {
111 modal.querySelector('p').textContent = message;
112 modal.style.display = 'block';
113 } else {
114 console.error(message);
115 }
116 }
117
118 function showConfirmModal(message, onConfirm) {
119 const modal = document.getElementById('confirm-modal');
120 if (modal) {
121 modal.querySelector('#confirm-modal-message').textContent = message;
122 confirmCallback = onConfirm;
123 modal.style.display = 'block';
124 }
125 }
126
127 function showReactionPicker(button) {
128 if (!reactionPicker) return;
129 const rect = button.getBoundingClientRect();
130 reactionPicker.style.top = `${window.scrollY + rect.bottom}px`;
131 reactionPicker.style.left = `${window.scrollX + rect.left}px`;
132 reactionPicker.style.display = 'block';
133 const timelineItem = button.closest('.timeline-item');
134 reactionPicker.dataset.issueId = timelineItem.dataset.id;
135 reactionPicker.dataset.commentId = timelineItem.dataset.type === 'comment' ? timelineItem.dataset.id : '';
136 reactionPicker.dataset.type = timelineItem.dataset.type;
137 }
138
139 // --- CORE LOGIC ---
140 function handleTimelineAction(button) {
141 const action = button.dataset.action;
142 const item = button.closest('.timeline-item');
143 if (!item) return;
144
145 switch (action) {
146 case 'edit':
147 startEdit(item);
148 break;
149 case 'quote':
150 quoteText(item);
151 break;
152 case 'react':
153 showReactionPicker(button);
154 break;
155 }
156 }
157
158 function handleMarkAs(button) {
159 if (!PGB_EMAIL_ADDR) {
160 showErrorModal('Cannot perform this action: The repository contact email is not configured.');
161 return;
162 }
163 const issueId = button.dataset.issueId;
164 const status = button.dataset.status;
165
166 const subject = `${PGB_SUBJECT_TAG} Mark Issue #${issueId} as ${status}`;
167 const mailBody = `---pgitBot---\ncommand: mark-as\nissue-id: ${issueId}\nstatus: ${status}\n---pgitBot---`;
168 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
169
170 window.location.href = mailto;
171 }
172
173 function handleSubmitNewComment() {
174 const issueId = document.querySelector('.timeline-item[data-type="issue"]').dataset.id;
175 const body = replyTextarea.value;
176 if (!body.trim()) return;
177
178 const subject = `${PGB_SUBJECT_TAG} New Comment on Issue #${issueId}`;
179 const mailBody = `${body}\n\n---pgitBot---\ncommand: add-comment\nissue-id: ${issueId}\n---pgitBot---`;
180 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
181
182 window.location.href = mailto;
183 replyTextarea.value = '';
184 }
185
186 function handleSubmitNewIssue() {
187 const titleInput = document.getElementById('new-issue-title');
188 const bodyInput = document.getElementById('new-issue-body');
189 const title = titleInput.value.trim();
190 const body = bodyInput.value.trim();
191
192 if (!title) {
193 titleInput.style.borderColor = 'red';
194 return;
195 }
196 titleInput.style.borderColor = '';
197
198 const subject = `${PGB_SUBJECT_TAG} New Issue: ${title}`;
199 const mailBody = `${body}\n\n---pgitBot---\ncommand: create-issue\ntitle: ${title}\n---pgitBot---`;
200 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
201
202 window.location.href = mailto;
203
204 titleInput.value = '';
205 bodyInput.value = '';
206 if (newIssueModal) {
207 newIssueModal.style.display = 'none';
208 }
209 }
210
211 function startEdit(item) {
212 if (isEditing) {
213 showErrorModal("Please save or cancel your current edit before editing another item.");
214 return;
215 }
216
217 const author = item.dataset.author;
218 if (getCookie('pgit_author_email') !== author) {
219 showConfirmModal("You are trying to edit an item you may not have created. The bot will reject this if you are not the original author. Continue?", () => proceedWithEdit(item));
220 } else {
221 proceedWithEdit(item);
222 }
223 }
224
225 function proceedWithEdit(item) {
226 isEditing = true;
227
228 const bodyElement = item.querySelector('.comment-body');
229 const originalMarkdownElement = item.querySelector('.original-markdown');
230 const actionsElement = item.querySelector('.comment-actions');
231
232 bodyElement.style.display = 'none';
233 actionsElement.style.display = 'none';
234
235 const editorTextarea = document.createElement('textarea');
236 editorTextarea.className = 'comment-editor';
237 editorTextarea.value = originalMarkdownElement.textContent;
238 item.querySelector('.comment-box').insertBefore(editorTextarea, bodyElement.nextSibling);
239 editorTextarea.focus();
240
241 const controlsDiv = document.createElement('div');
242 controlsDiv.className = 'comment-edit-controls';
243
244 const saveBtn = document.createElement('button');
245 saveBtn.className = 'btn';
246 saveBtn.textContent = 'Save';
247
248 const cancelBtn = document.createElement('button');
249 cancelBtn.className = 'btn btn-secondary';
250 cancelBtn.textContent = 'Cancel';
251
252 controlsDiv.appendChild(cancelBtn);
253 controlsDiv.appendChild(saveBtn);
254 editorTextarea.insertAdjacentElement('afterend', controlsDiv);
255
256 const cancelEdit = () => {
257 editorTextarea.remove();
258 controlsDiv.remove();
259 bodyElement.style.display = 'block';
260 actionsElement.style.display = 'flex';
261 isEditing = false;
262 };
263
264 cancelBtn.addEventListener('click', cancelEdit);
265
266 saveBtn.addEventListener('click', () => {
267 const newBody = editorTextarea.value;
268 if (!newBody.trim()) return;
269
270 const type = item.dataset.type;
271 const id = item.dataset.id;
272 const issueId = (type === 'issue') ? id : item.closest('.issue-container').querySelector('.timeline-item[data-type="issue"]').dataset.id;
273
274 const subject = `${PGB_SUBJECT_TAG} Edit ${type} on Issue #${issueId}`;
275 let mailBody = `${newBody}\n\n---pgitBot---\ncommand: edit\nissue-id: ${issueId}\ntype: ${type}\n`;
276 if (type === 'comment') {
277 mailBody += `comment-id: ${id}\n`;
278 }
279 mailBody += `---pgitBot---`;
280 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
281 window.location.href = mailto;
282
283 cancelEdit();
284 });
285 }
286
287 function quoteText(item) {
288 if (!replyTextarea) return;
289 const originalMarkdownElement = item.querySelector('.original-markdown');
290 const author = item.querySelector('.author strong').textContent;
291 const selection = window.getSelection().toString();
292 const textToQuote = selection || originalMarkdownElement.textContent;
293
294 const quotedText = `> ${textToQuote.replace(/\n/g, '\n> ')}\n\n*Quoting ${author}*\n\n`;
295
296 replyTextarea.value = quotedText + replyTextarea.value;
297 replyTextarea.focus();
298 replyTextarea.setSelectionRange(replyTextarea.value.length, replyTextarea.value.length);
299 }
300
301 function handleReactionSelection(target) {
302 const reactionName = target.dataset.reaction;
303 if (!reactionName || !reactionPicker) return;
304
305 const issueId = reactionPicker.dataset.issueId;
306 const commentId = reactionPicker.dataset.commentId;
307 const type = reactionPicker.dataset.type;
308
309 const subject = `${PGB_SUBJECT_TAG} React to ${type} on Issue #${issueId}`;
310 let mailBody = `---pgitBot---\ncommand: react\nissue-id: ${issueId}\ntype: ${type}\nreaction: ${reactionName}\n`;
311 if (type === 'comment') {
312 mailBody += `comment-id: ${commentId}\n`;
313 }
314 mailBody += `---pgitBot---`;
315
316 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
317 window.location.href = mailto;
318
319 reactionPicker.style.display = 'none';
320 }
321
322 function handleUnreact(button) {
323 const reactionName = button.dataset.reactionName;
324 if (!reactionName) return;
325
326 const timelineItem = button.closest('.timeline-item');
327 if (!timelineItem) return;
328
329 showConfirmModal(`Send email to remove your "${reactionName}" reaction?`, () => {
330 const type = timelineItem.dataset.type;
331 const issueId = (type === 'issue') ? timelineItem.dataset.id : button.closest('.issue-container').querySelector('.timeline-item[data-type="issue"]').dataset.id;
332 const commentId = (type === 'comment') ? timelineItem.dataset.id : '';
333
334 const subject = `${PGB_SUBJECT_TAG} Un-react to ${type} on Issue #${issueId}`;
335 let mailBody = `---pgitBot---\ncommand: unreact\nissue-id: ${issueId}\ntype: ${type}\nreaction: ${reactionName}\n`;
336 if (type === 'comment') {
337 mailBody += `comment-id: ${commentId}\n`;
338 }
339 mailBody += `---pgitBot---`;
340
341 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
342 window.location.href = mailto;
343 });
344 }
345
346 function saveAlias() {
347 const aliasInput = document.getElementById('alias-input');
348 const newAlias = aliasInput.value.trim();
349
350 const subject = `${PGB_SUBJECT_TAG} Set Alias`;
351 const command = newAlias === '' ? 'unalias' : 'alias';
352 let mailBody = `---pgitBot---\ncommand: ${command}\n`;
353 if (command === 'alias') {
354 mailBody += `alias: ${newAlias}\n`;
355 }
356 mailBody += `---pgitBot---`;
357
358 const mailto = `mailto:${PGB_EMAIL_ADDR}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`;
359 window.location.href = mailto;
360
361 setCookie('pgit_alias', newAlias || 'anonymous', 365);
362 updateAliasDisplay();
363 if (aliasModal) aliasModal.style.display = 'none';
364 }
365
366 function updateAliasDisplay() {
367 if (currentUserAliasSpan) {
368 currentUserAliasSpan.textContent = `Commenting as ${getCookie('pgit_alias') || 'anonymous'}`;
369 }
370 }
371
372 function populateReactionPicker() {
373 if (!reactionPicker) return;
374 let html = '';
375 for (const name in PGB_REACTIONS) {
376 html += `<button class="reaction-picker-emoji" data-reaction="${name}" title="${name}">${PGB_REACTIONS[name]}</button>`;
377 }
378 reactionPicker.innerHTML = html;
379 }
380
381 function setCookie(name, value, days) {
382 let expires = "";
383 if (days) {
384 const date = new Date();
385 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
386 expires = "; expires=" + date.toUTCString();
387 }
388 document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
389 }
390
391 function getCookie(name) {
392 const nameEQ = name + "=";
393 const ca = document.cookie.split(';');
394 for (let i = 0; i < ca.length; i++) {
395 let c = ca[i];
396 while (c.charAt(0) == ' ') c = c.substring(1, c.length);
397 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
398 }
399 return null;
400 }
401
402 function filterIssues() {
403 const filterText = issueFilterInput.value.toLowerCase();
404 const issueItems = issuesList.querySelectorAll('.issue-list-item');
405 issueItems.forEach(item => {
406 const title = item.dataset.issueTitle.toLowerCase();
407 if (title.includes(filterText)) {
408 item.style.display = 'flex';
409 } else {
410 item.style.display = 'none';
411 }
412 });
413 }
414});