|
|
|
@@ -8,6 +8,20 @@ import {toggleFullScreen} from '../utils.ts';
|
|
|
|
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
|
|
|
|
import {localUserSettings} from '../modules/user-settings.ts';
|
|
|
|
|
import {UserEventsSharedWorker} from '../modules/worker.ts';
|
|
|
|
|
import {showInfoToast, showWarningToast} from '../modules/toast.ts';
|
|
|
|
|
|
|
|
|
|
// issueRef returns a short human label for a card, preferring the
|
|
|
|
|
// rendered "#index" anchor text and falling back to the internal id.
|
|
|
|
|
function issueRef(card: HTMLElement | null, issueID: number): string {
|
|
|
|
|
const idx = card?.querySelector('.issue-card-title, .ref-issue, a[href*="/issues/"]')?.textContent?.trim();
|
|
|
|
|
const m = idx?.match(/#\d+/);
|
|
|
|
|
return m ? m[0] : `#${issueID}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function columnName(board: HTMLElement, columnID: number): string {
|
|
|
|
|
const t = board.querySelector<HTMLElement>(`.project-column[data-id="${columnID}"] .project-column-title-text`)?.textContent?.trim();
|
|
|
|
|
return t || `column ${columnID}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SESSION_TAG_HEADER = 'X-Session-Tag';
|
|
|
|
|
|
|
|
|
@@ -281,8 +295,10 @@ function handleCardMoved(board: HTMLElement, payload: CardMovedPayload): void {
|
|
|
|
|
// Card is not currently rendered (filtered out, or new since
|
|
|
|
|
// page load). A targeted column re-fetch is the safe fallback.
|
|
|
|
|
refetchColumn(board, payload.to_column_id);
|
|
|
|
|
showInfoToast(`#${payload.issue_id} → ${columnName(board, payload.to_column_id)}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const ref = issueRef(card, payload.issue_id);
|
|
|
|
|
const target = board.querySelector<HTMLElement>(`#board_${payload.to_column_id}`);
|
|
|
|
|
if (!target) return;
|
|
|
|
|
const fromColumn = card.parentElement;
|
|
|
|
@@ -293,6 +309,7 @@ function handleCardMoved(board: HTMLElement, payload: CardMovedPayload): void {
|
|
|
|
|
}
|
|
|
|
|
const toColumnEl = target.closest<HTMLElement>('.project-column');
|
|
|
|
|
if (toColumnEl) updateColumnCount(toColumnEl);
|
|
|
|
|
showInfoToast(`${ref} → ${columnName(board, payload.to_column_id)}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refetchColumn(board: HTMLElement, columnID: number): Promise<void> {
|
|
|
|
@@ -321,15 +338,18 @@ async function refetchColumn(board: HTMLElement, columnID: number): Promise<void
|
|
|
|
|
function handleCardLinked(board: HTMLElement, payload: CardLinkedPayload): void {
|
|
|
|
|
if (payload.session_tag && payload.session_tag === sessionTag) return;
|
|
|
|
|
refetchColumn(board, payload.column_id); // no await
|
|
|
|
|
showInfoToast(`#${payload.issue_id} added to ${columnName(board, payload.column_id)}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCardUnlinked(board: HTMLElement, payload: CardUnlinkedPayload): void {
|
|
|
|
|
if (payload.session_tag && payload.session_tag === sessionTag) return;
|
|
|
|
|
const card = board.querySelector<HTMLElement>(`.issue-card[data-issue="${payload.issue_id}"]`);
|
|
|
|
|
if (!card) return;
|
|
|
|
|
const ref = issueRef(card, payload.issue_id);
|
|
|
|
|
const colEl = card.closest<HTMLElement>('.project-column');
|
|
|
|
|
card.remove();
|
|
|
|
|
if (colEl) updateColumnCount(colEl);
|
|
|
|
|
showInfoToast(`${ref} removed from board`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleColumnCreated(): void {
|
|
|
|
@@ -341,7 +361,11 @@ function handleColumnUpdated(board: HTMLElement, payload: ColumnUpdatedPayload):
|
|
|
|
|
const colEl = board.querySelector<HTMLElement>(`.project-column[data-id="${payload.column_id}"]`);
|
|
|
|
|
if (!colEl) return;
|
|
|
|
|
const titleEl = colEl.querySelector<HTMLElement>('.project-column-title-text');
|
|
|
|
|
const oldTitle = titleEl?.textContent?.trim();
|
|
|
|
|
if (titleEl) titleEl.textContent = payload.title;
|
|
|
|
|
if (oldTitle && oldTitle !== payload.title) {
|
|
|
|
|
showInfoToast(`Column renamed to “${payload.title}”`);
|
|
|
|
|
}
|
|
|
|
|
if (payload.color) {
|
|
|
|
|
const textColor = contrastColor(payload.color);
|
|
|
|
|
colEl.style.setProperty('background', payload.color, 'important');
|
|
|
|
@@ -356,7 +380,10 @@ function handleColumnUpdated(board: HTMLElement, payload: ColumnUpdatedPayload):
|
|
|
|
|
|
|
|
|
|
function handleColumnDeleted(board: HTMLElement, payload: ColumnDeletedPayload): void {
|
|
|
|
|
const colEl = board.querySelector<HTMLElement>(`.project-column[data-id="${payload.column_id}"]`);
|
|
|
|
|
if (colEl) colEl.remove();
|
|
|
|
|
if (!colEl) return;
|
|
|
|
|
const name = colEl.querySelector<HTMLElement>('.project-column-title-text')?.textContent?.trim();
|
|
|
|
|
colEl.remove();
|
|
|
|
|
showInfoToast(name ? `Column “${name}” removed` : 'A column was removed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleColumnReordered(board: HTMLElement, payload: ColumnReorderedPayload): void {
|
|
|
|
@@ -383,13 +410,12 @@ function handleProjectDeleted(): void {
|
|
|
|
|
// The board lives at .../projects/{id}; the listing page is the
|
|
|
|
|
// parent. Falling back to the homepage on any URL we don't
|
|
|
|
|
// recognise is acceptable since this is a destructive event.
|
|
|
|
|
// Show a sticky warning first and delay the redirect briefly so the
|
|
|
|
|
// user understands why the page is about to change under them.
|
|
|
|
|
showWarningToast('This project was deleted — returning to the project list');
|
|
|
|
|
const parts = window.location.pathname.split('/');
|
|
|
|
|
if (parts.length > 1) {
|
|
|
|
|
parts.pop();
|
|
|
|
|
window.location.href = parts.join('/') || '/';
|
|
|
|
|
} else {
|
|
|
|
|
window.location.href = '/';
|
|
|
|
|
}
|
|
|
|
|
const dest = parts.length > 1 ? (parts.slice(0, -1).join('/') || '/') : '/';
|
|
|
|
|
setTimeout(() => { window.location.href = dest }, 1500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dispatchProjectEvent picks the right handler for an SSE payload.
|
|
|
|
|