Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- Markdown parsing and sanitization -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.0.0/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
<script src="src/logger.js"></script>
<script src="src/utils.js"></script>
<script src="src/event-bus.js"></script>
<script src="src/notifications.js"></script>
Expand Down Expand Up @@ -426,6 +427,12 @@ <h3 class="font-semibold text-gray-900 column-title">Done</h3>
</button>
</div>
</div>
<div class="column-content px-4 pt-3 -mb-1">
<button id="archive-all-done-btn" class="hidden w-full flex items-center justify-center space-x-2 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-50 border border-gray-200 rounded-md py-1.5 transition-colors" title="Archive all issues in the Done column">
<i class="fas fa-archive"></i>
<span>Archive all</span>
</button>
</div>
<div id="done" class="p-4 space-y-3 min-h-[64px] column-content">
</div>
</div>
Expand Down
38 changes: 38 additions & 0 deletions src/kanban.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,21 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});

updateArchiveAllButton();
}

// Show the Done column's "Archive all" button only when signed in and there
// is at least one GitHub issue card available to archive.
function updateArchiveAllButton() {
const btn = document.getElementById('archive-all-done-btn');
const doneColumn = document.getElementById('done');
if (!btn || !doneColumn) {
return;
}
const authed = Boolean(window.GitHub?.isGitHubAuthed?.());
const issueCount = doneColumn.querySelectorAll('.bg-white.border[data-issue-number]').length;
btn.classList.toggle('hidden', !(authed && issueCount > 0));
}

// Expose updateColumnCounts globally for modal use
Expand Down Expand Up @@ -310,6 +325,28 @@ document.addEventListener('DOMContentLoaded', function() {
}
});

// Archive every GitHub issue currently in the Done column at once.
document.addEventListener('click', function(e) {
if (!e.target.closest('#archive-all-done-btn')) {
return;
}
const doneColumn = document.getElementById('done');
if (!doneColumn) {
return;
}
const issueCards = Array.from(doneColumn.querySelectorAll('.bg-white.border[data-issue-number]'));
if (issueCards.length === 0) {
return;
}
const plural = issueCards.length === 1 ? 'issue' : 'issues';
if (!window.confirm(`Archive all ${issueCards.length} ${plural} in Done?`)) {
return;
}
issueCards.forEach(card => {
window.GitHub.archiveGitHubIssue(card.getAttribute('data-issue-number'), card);
});
});

// Add click to open issue modal functionality
document.addEventListener('click', function(e) {
// Only handle clicks on issue cards with GitHub issue numbers
Expand Down Expand Up @@ -476,6 +513,7 @@ document.addEventListener('DOMContentLoaded', function() {
const testAPI = {
createTaskElement,
updateColumnCounts,
updateArchiveAllButton,
hideModal,
addTaskBtn,
addTaskModal,
Expand Down
17 changes: 17 additions & 0 deletions src/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Quiets Dashban's routine console output.
//
// The app modules log a lot of informational/lifecycle detail via console.log.
// Loading this script first (before the other src modules) replaces console.log
// with a version that stays silent unless debugging is explicitly enabled by
// setting `window.DASHBAN_DEBUG = true` (e.g. from devtools). console.error and
// console.warn are left untouched, so genuine problems still surface.
(function () {
'use strict';

const original = console.log.bind(console);
console.log = function () {
if (globalThis.DASHBAN_DEBUG) {
original.apply(null, arguments);
}
};
})();
18 changes: 17 additions & 1 deletion src/status-cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,25 @@ document.addEventListener('DOMContentLoaded', function() {
}

async function fetchCoverageStatus() {
// Remember which repo this request is for. If the user switches repos
// while the request is in flight, a slow/late response must not overwrite
// the now-current repo's coverage (switching to a repo with no coverage
// and back was leaving dashban stuck on "unknown").
const { OWNER, REPO } = getCurrentRepoConfig();
const isStale = () => {
const current = getCurrentRepoConfig();
return current.OWNER !== OWNER || current.REPO !== REPO;
};

try {
// The coverage badge URL already carries a query (?branch=main), so
// join the cache-buster with & rather than ?.
const badgeUrl = `${buildBadgeUrl('coverage')}&t=${Date.now()}`;

const svgText = await fetch(badgeUrl).then(r => r.text());
if (isStale()) {
return;
}
const coverage = parseCoverageFromSVG(svgText);

const coverageData = {
Expand All @@ -234,7 +247,10 @@ document.addEventListener('DOMContentLoaded', function() {
updateCoverageStatusUI(coverageData);
} catch (error) {
console.error('Error fetching coverage status:', error);


if (isStale()) {
return;
}
const fallbackData = {
coverage: 'unknown',
updatedAt: new Date(),
Expand Down
136 changes: 136 additions & 0 deletions tests/kanban.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,142 @@ describe('Kanban Board Core Functionality', () => {
});
});

describe('archive all in the Done column', () => {
function addArchiveAllButton() {
const btn = document.createElement('button');
btn.id = 'archive-all-done-btn';
btn.className = 'hidden';
document.body.appendChild(btn);
return btn;
}

function addDoneIssueCard(number) {
const card = document.createElement('div');
card.className = 'bg-white border';
card.setAttribute('data-issue-number', String(number));
document.getElementById('done').appendChild(card);
return card;
}

beforeEach(() => {
global.window.GitHub.githubAuth = { isAuthenticated: true, mode: 'clerk', user: { login: 'octocat' } };
global.window.GitHub.archiveGitHubIssue = jest.fn();
global.window.confirm = jest.fn(() => true);
});

describe('updateArchiveAllButton', () => {
test('shows the button when signed in and Done has issue cards', () => {
const btn = addArchiveAllButton();
addDoneIssueCard(1);
api.updateArchiveAllButton();
expect(btn.classList.contains('hidden')).toBe(false);
});

test('hides the button when Done has no issue cards', () => {
const btn = addArchiveAllButton();
api.updateArchiveAllButton();
expect(btn.classList.contains('hidden')).toBe(true);
});

test('hides the button when not signed in', () => {
const btn = addArchiveAllButton();
addDoneIssueCard(1);
global.window.GitHub.githubAuth = { isAuthenticated: false, mode: null, user: null };
api.updateArchiveAllButton();
expect(btn.classList.contains('hidden')).toBe(true);
});

test('returns quietly when the button is absent', () => {
expect(() => api.updateArchiveAllButton()).not.toThrow();
});

test('returns quietly when the Done column is absent', () => {
const btn = addArchiveAllButton();
document.getElementById('done').remove();
expect(() => api.updateArchiveAllButton()).not.toThrow();
expect(btn.classList.contains('hidden')).toBe(true);
});

test('treats a missing GitHub integration as signed out', () => {
const btn = addArchiveAllButton();
addDoneIssueCard(1);
const saved = global.window.GitHub;
delete global.window.GitHub;
try {
api.updateArchiveAllButton();
expect(btn.classList.contains('hidden')).toBe(true);
} finally {
global.window.GitHub = saved;
}
});

test('treats GitHub without isGitHubAuthed as signed out', () => {
const btn = addArchiveAllButton();
addDoneIssueCard(1);
const saved = global.window.GitHub;
global.window.GitHub = {};
try {
api.updateArchiveAllButton();
expect(btn.classList.contains('hidden')).toBe(true);
} finally {
global.window.GitHub = saved;
}
});
});

describe('archive-all click handler', () => {
test('archives every issue card in Done after confirmation', () => {
addArchiveAllButton();
const c1 = addDoneIssueCard(1);
const c2 = addDoneIssueCard(2);

document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));

expect(global.window.confirm).toHaveBeenCalledWith('Archive all 2 issues in Done?');
expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('1', c1);
expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('2', c2);
});

test('uses the singular noun for a single issue', () => {
addArchiveAllButton();
addDoneIssueCard(7);

document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));

expect(global.window.confirm).toHaveBeenCalledWith('Archive all 1 issue in Done?');
expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('7', expect.any(Object));
});

test('does nothing when the user cancels', () => {
global.window.confirm = jest.fn(() => false);
addArchiveAllButton();
addDoneIssueCard(1);

document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));

expect(global.window.GitHub.archiveGitHubIssue).not.toHaveBeenCalled();
});

test('does not prompt when Done has no issue cards', () => {
addArchiveAllButton();

document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));

expect(global.window.confirm).not.toHaveBeenCalled();
expect(global.window.GitHub.archiveGitHubIssue).not.toHaveBeenCalled();
});

test('does nothing when the Done column is missing', () => {
addArchiveAllButton();
document.getElementById('done').remove();

document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));

expect(global.window.confirm).not.toHaveBeenCalled();
});
});
});

describe('double-click edit functionality', () => {
test('should handle double-click events on tasks', () => {
const taskElement = document.createElement('div');
Expand Down
59 changes: 59 additions & 0 deletions tests/logger.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Tests for src/logger.js — the console.log gate that keeps the browser console
* quiet unless window.DASHBAN_DEBUG is set.
*/
describe('logger (console.log gate)', () => {
let originalLog;
let originalError;
let originalWarn;

beforeEach(() => {
jest.resetModules();
originalLog = console.log;
originalError = console.error;
originalWarn = console.warn;
delete globalThis.DASHBAN_DEBUG;
});

afterEach(() => {
console.log = originalLog;
console.error = originalError;
console.warn = originalWarn;
delete globalThis.DASHBAN_DEBUG;
});

test('suppresses console.log by default (debugging off)', () => {
const sink = jest.fn();
console.log = sink; // stand in for the real console.log
require('../src/logger.js'); // wraps console.log, capturing `sink` as the original

console.log('hidden');

expect(sink).not.toHaveBeenCalled();
});

test('forwards console.log when DASHBAN_DEBUG is enabled', () => {
const sink = jest.fn();
console.log = sink;
require('../src/logger.js');

globalThis.DASHBAN_DEBUG = true;
console.log('shown', 1);

expect(sink).toHaveBeenCalledWith('shown', 1);
});

test('leaves console.error and console.warn untouched', () => {
const errSink = jest.fn();
const warnSink = jest.fn();
console.error = errSink;
console.warn = warnSink;
require('../src/logger.js');

console.error('boom');
console.warn('careful');

expect(errSink).toHaveBeenCalledWith('boom');
expect(warnSink).toHaveBeenCalledWith('careful');
});
});
33 changes: 31 additions & 2 deletions tests/status-cards.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -966,11 +966,40 @@ describe('Status Cards Functions', () => {

test('should handle coverage fetch errors', async () => {
global.fetch.mockRejectedValue(new Error('Coverage fetch error'));

await statusAPI.fetchCoverageStatus();

expect(console.error).toHaveBeenCalledWith('Error fetching coverage status:', expect.any(Error));
});

test('drops a stale coverage response when the repo changed mid-request', async () => {
setupGitHubAuth('super3', 'dashban');
const coverageEl = document.querySelector('[data-coverage-status]');
coverageEl.innerHTML = 'SENTINEL';
// The user switches repos while the request is in flight.
global.fetch.mockImplementation(async () => {
global.window.GitHubAuth.GITHUB_CONFIG.repo = 'other-repo';
return { text: async () => '<svg><text>85%</text></svg>' };
});

await statusAPI.fetchCoverageStatus();

expect(coverageEl.innerHTML).toBe('SENTINEL');
});

test('drops a stale coverage error when the repo changed mid-request', async () => {
setupGitHubAuth('super3', 'dashban');
const coverageEl = document.querySelector('[data-coverage-status]');
coverageEl.innerHTML = 'SENTINEL';
global.fetch.mockImplementation(async () => {
global.window.GitHubAuth.GITHUB_CONFIG.repo = 'other-repo';
throw new Error('network');
});

await statusAPI.fetchCoverageStatus();

expect(coverageEl.innerHTML).toBe('SENTINEL');
});
});

describe('refreshAllStatuses', () => {
Expand Down
Loading