The TriliumNext Task-Hub plugin is now available for trial use. #9226
Replies: 5 comments 11 replies
-
|
This is great! I'll test it. |
Beta Was this translation helpful? Give feedback.
-
|
Hi, Two remarks:
|
Beta Was this translation helpful? Give feedback.
-
|
Update 4: You can now mark tasks as complete directly from the open tasks list. Details
/**
* ╔═══════════════════════════════════════════╗
* ║ Painel de Tarefas — TriliumNext ║
* ╚═══════════════════════════════════════════╝
*
* INTERAÇÕES:
* ☐ clicar no quadradinho → marca a tarefa como concluída na nota
* texto da tarefa → abre a nota que contém a tarefa
* título do grupo → abre a nota (comportamento original)
*/
(async function () {
const $root = $container;
$root.css({
padding: '28px 32px',
fontFamily: 'var(--detail-font-family, "Segoe UI", sans-serif)',
fontSize: '16px',
lineHeight: '1.5',
color: 'var(--main-text-color)',
boxSizing: 'border-box'
});
// =========================================================
// ESTILOS
// =========================================================
if (!document.getElementById('th-task-styles')) {
const style = document.createElement('style');
style.id = 'th-task-styles';
style.textContent = `
.th-task-check {
flex-shrink: 0;
width: 15px;
height: 15px;
margin-top: 3px;
border: 1.5px solid var(--main-border-color, #45475a);
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color .15s, background .15s, color .15s;
color: transparent;
font-size: 10px;
line-height: 1;
user-select: none;
}
.th-task-check:hover {
border-color: var(--main-text-color);
background: var(--accented-background-color, #313244);
color: var(--muted-text-color, #888);
}
.th-task-check.th-completing {
border-color: var(--active-item-background-color, #a6e3a1);
background: rgba(166, 227, 161, .15);
color: var(--active-item-background-color, #a6e3a1);
pointer-events: none;
}
.th-task-text {
cursor: pointer;
transition: color .15s;
}
.th-task-text:hover {
color: var(--link-color, var(--main-text-color));
text-decoration: underline;
}
.th-note-link {
cursor: pointer;
transition: color .15s;
}
.th-note-link:hover {
color: var(--main-text-color) !important;
}
.th-task-row {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 3px 0;
font-size: 16px;
line-height: 1.45;
}
`;
document.head.appendChild(style);
}
// =========================================================
// RENDER
// =========================================================
async function load() {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
⏳ Carregando tarefas…
</p>
`);
try {
// ── Backend: busca todas as notas de texto ──────────
const rawNotes = await api.runOnBackend(() => {
const rows = api.sql.getRows(`
SELECT noteId, title
FROM notes
WHERE isDeleted = 0 AND type = 'text'
ORDER BY title COLLATE NOCASE
`);
return rows.map(row => {
const note = api.getNote(row.noteId);
return {
noteId: row.noteId,
title: row.title || '(sem título)',
content: note ? note.getContent() : ''
};
});
});
// ── Frontend: processa checkboxes ───────────────────
const groups = [];
for (const row of rawNotes) {
if (!row.content) continue;
const $tmp = $('<div>').html(String(row.content));
const tasks = [];
let cbIndex = 0; // índice global de todos os checkboxes da nota
$tmp.find('input[type="checkbox"]').each((_, input) => {
const idx = cbIndex++; // captura antes de incrementar
// pula tarefas já concluídas
if (input.checked || input.hasAttribute('checked')) return;
let text = '';
const $span = $(input).next('span');
if ($span.length) {
text = $span.text();
} else {
text = $(input).parent().text();
}
text = text.replace(/\s+/g, ' ').trim();
if (text) {
tasks.push({ text, checkboxIndex: idx });
}
});
if (tasks.length > 0) {
groups.push({
noteId: row.noteId,
title: row.title,
count: tasks.length,
tasks
});
}
}
// ── Sem tarefas ─────────────────────────────────────
if (groups.length === 0) {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
✓ Nenhuma tarefa aberta encontrada.
</p>
`);
return;
}
// ── Monta HTML ──────────────────────────────────────
const totalTasks = groups.reduce((s, g) => s + g.count, 0);
const esc = s => String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
let html = `
<div style="
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid var(--main-border-color, #313244);
">
<span style="font-size:19px;font-weight:700;">Tarefas abertas</span>
<span class="th-total-count" style="font-size:14px;color:var(--muted-text-color,#888);">
${totalTasks} tarefa${totalTasks !== 1 ? 's' : ''}
</span>
</div>
`;
for (const group of groups) {
html += `
<div class="th-group" style="margin-bottom:20px;">
<div
class="th-note-link"
data-note-id="${esc(group.noteId)}"
style="
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted-text-color, #888);
"
>
${esc(group.title)}
<span
class="th-group-count"
style="
font-size: 13px;
background: var(--accented-background-color, #313244);
color: var(--muted-text-color, #888);
padding: 1px 7px;
border-radius: 10px;
"
>${group.count}</span>
</div>
<div
class="th-task-list"
style="
border-left: 2px solid var(--main-border-color, #313244);
padding-left: 14px;
"
>
${group.tasks.map(task => `
<div
class="th-task-row"
data-note-id="${esc(group.noteId)}"
data-cb-index="${task.checkboxIndex}"
>
<span
class="th-task-check"
title="Marcar como concluída"
>✓</span>
<span
class="th-task-text"
data-note-id="${esc(group.noteId)}"
title="Abrir nota"
>${esc(task.text)}</span>
</div>
`).join('')}
</div>
</div>
`;
}
$root.html(html);
} catch (err) {
console.error(err);
$root.html(`
<div style="
color: #f38ba8;
background: rgba(243,139,168,.08);
padding: 12px 14px;
border-radius: 8px;
font-size: 15px;
">
✗ Erro ao carregar tarefas<br><br>
${String(err.message || err)}
</div>
`);
}
}
await load();
// =========================================================
// EVENTOS (delegados ao $root para sobreviver ao re-render)
// =========================================================
// Título do grupo → abre nota
$root.on('click', '.th-note-link', function () {
api.activateNote($(this).data('noteId'));
});
// Texto da tarefa → abre nota
$root.on('click', '.th-task-text', function () {
api.activateNote($(this).data('noteId'));
});
// Quadradinho → marca como concluída
$root.on('click', '.th-task-check', async function () {
const $check = $(this);
if ($check.hasClass('th-completing')) return; // evita duplo clique
const $row = $check.closest('.th-task-row');
const noteId = String($row.data('noteId'));
const cbIndex = parseInt($row.data('cbIndex'), 10);
$check.addClass('th-completing');
try {
// ── Backend: aplica checked no HTML da nota ─────────
await api.runOnBackend((noteId, cbIndex) => {
const note = api.getNote(noteId);
let content = note.getContent();
let count = 0;
content = content.replace(
/<input\s+type="checkbox"([^>]*?)>/gi,
(match, attrs) => {
if (count++ === cbIndex) {
// adiciona checked apenas se ainda não estiver
if (/\bchecked\b/i.test(attrs)) return match;
return `<input type="checkbox"${attrs} checked>`;
}
return match;
}
);
note.setContent(content);
}, [noteId, cbIndex]);
// ── UI: remove a linha com animação ─────────────────
$row.fadeOut(250, function () {
const $group = $(this).closest('.th-group');
$(this).remove();
// atualiza contador do grupo
const remainingInGroup = $group.find('.th-task-row').length;
if (remainingInGroup === 0) {
$group.fadeOut(200, function () { $(this).remove(); });
} else {
$group.find('.th-group-count').text(remainingInGroup);
}
// atualiza contador global
const total = $root.find('.th-task-row').length;
$root.find('.th-total-count').text(
`${total} tarefa${total !== 1 ? 's' : ''}`
);
// estado vazio
if (total === 0) {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
✓ Nenhuma tarefa aberta encontrada.
</p>
`);
}
});
} catch (err) {
console.error(err);
$check.removeClass('th-completing'); // reverte visual em caso de erro
}
});
})(); |
Beta Was this translation helpful? Give feedback.
-
|
Let me also promote the plugin I developed: it features Gantt chart support
|
Beta Was this translation helpful? Give feedback.
-
|
@ricolandia I have modified the task management based on Journal according to your code. Journal Dashboard — Task & Inbox Widget for TriliumNext Turn your Journal into a task command center. Automatically pulls open checkboxes from daily notes and splits them into Scheduled (#task tagged, sorted by date, overdue in red) and Inbox (quick captures). One-click to complete, one-click to open the source note. Set your Journal ID and go.
CODE Details
/**
* ╔══════════════════════════════════════════════════════╗
* ║ Journal Dashboard — Task & Inbox Widget ║
* ║ for TriliumNext Notes ║
* ╚══════════════════════════════════════════════════════╝
*
* A focused dashboard that pulls open checkboxes from notes under
* a Journal parent and splits them into two views:
*
* Scheduled Notes tagged with #task, sorted by date.
* Overdue & today items are highlighted in red.
*
* Inbox Untagged items — quick thoughts, ideas,
* or anything not yet scheduled.
*
* ── QUICK SETUP ──────────────────────────────────────────
*
* 1. Set your Journal parent note ID below (JOURNAL_ID constant).
* Find it in the note's Properties panel → copy the Note ID.
*
* 2. Inside your daily journal notes, add checkboxes:
* - [ ] Write quarterly report #task
* - [ ] A random idea
*
*/
// ═══════════════════════════════════════════════════════════
// CONFIG — change this to your Journal parent note ID
// ═══════════════════════════════════════════════════════════
const JOURNAL_ID = 'uDnCdpocRt8o';
(async function () {
const $root = $container;
$root.css({
padding: '28px 32px',
fontFamily: 'var(--detail-font-family, "Segoe UI", sans-serif)',
fontSize: '16px',
lineHeight: '1.5',
color: 'var(--main-text-color)',
boxSizing: 'border-box'
});
// =========================================================
// STYLES
// =========================================================
if (!document.getElementById('th-task-styles')) {
const style = document.createElement('style');
style.id = 'th-task-styles';
style.textContent = `
.th-task-check {
flex-shrink: 0;
width: 15px;
height: 15px;
margin-top: 3px;
border: 1.5px solid var(--main-border-color, #45475a);
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color .15s, background .15s, color .15s;
color: transparent;
font-size: 10px;
line-height: 1;
user-select: none;
}
.th-task-check:hover {
border-color: var(--main-text-color);
background: var(--accented-background-color, #313244);
color: var(--muted-text-color, #888);
}
.th-task-check.th-completing {
border-color: var(--active-item-background-color, #a6e3a1);
background: rgba(166, 227, 161, .15);
color: var(--active-item-background-color, #a6e3a1);
pointer-events: none;
}
.th-task-text {
cursor: pointer;
transition: color .15s;
}
.th-task-text:hover {
color: var(--link-color, var(--main-text-color));
text-decoration: underline;
}
.th-task-overdue {
color: #e06c75 !important;
font-weight: 600;
}
.th-task-overdue:hover {
color: #f28b94 !important;
}
.th-task-today {
color: #e06c75 !important;
font-weight: 600;
}
.th-task-today:hover {
color: #f28b94 !important;
}
.th-note-link {
cursor: pointer;
transition: color .15s;
}
.th-note-link:hover {
color: var(--main-text-color) !important;
}
.th-task-row {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 3px 0;
font-size: 16px;
line-height: 1.45;
}
.th-section-header {
font-size: 21px;
font-weight: 700;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid var(--main-border-color, #313244);
}
.th-section {
margin-bottom: 28px;
}
`;
document.head.appendChild(style);
}
// =========================================================
// HELPERS
// =========================================================
const esc = s => String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
/** Extract date from dateNote / title / utcDateCreated, returns YYYY-MM-DD or null */
function extractDate(title, utcDateCreated, dateNote) {
// ① dateNote label (most reliable)
if (dateNote) {
const m = String(dateNote).match(/(\d{4})-(\d{2})-(\d{2})/);
if (m) return m[0];
}
// ② YYYY-MM-DD in title
let m = String(title).match(/(\d{4})-(\d{2})-(\d{2})/);
if (m) return m[0];
// ③ MM-DD → current year + extracted month-day
m = String(title).match(/\b(\d{2})-(\d{2})\b/);
if (m) {
const now = new Date();
return `${now.getFullYear()}-${m[1]}-${m[2]}`;
}
// ④ Title starts with DD (e.g. "18 - Thu") → current year-month
m = String(title).match(/^(\d{1,2})\b/);
if (m) {
const now = new Date();
const day = parseInt(m[1], 10);
let y = now.getFullYear();
let mo = now.getMonth() + 1;
// If the candidate is earlier than today, try next month
const candidate = `${y}-${String(mo).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
if (candidate < todayStr()) {
if (mo === 12) { mo = 1; y++; } else { mo++; }
return `${y}-${String(mo).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
}
return candidate;
}
// ⑤ Fallback: use note creation date UTC → local date
if (utcDateCreated) {
const d = new Date(utcDateCreated);
if (!isNaN(d.getTime())) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
}
return null;
}
/** Format dateNote attribute for display, e.g. "Jun 18 Thu" */
function formatDateDisplay(dateNote) {
if (!dateNote) return '';
const m = String(dateNote).match(/(\d{4})-(\d{2})-(\d{2})/);
if (!m) return '';
const d = new Date(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10));
if (isNaN(d.getTime())) return '';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return `${months[d.getMonth()]} ${d.getDate()} ${days[d.getDay()]}`;
}
/** Get today as YYYY-MM-DD */
function todayStr() {
const d = new Date();
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
// =========================================================
// RENDER
// =========================================================
async function load() {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
⏳ Loading tasks…
</p>
`);
try {
// ── Backend: fetch text notes under Journal ──
const rawNotes = await api.runOnBackend((journalId) => {
const rows = api.sql.getRows(`
WITH RECURSIVE subtree AS (
SELECT noteId FROM branches
WHERE parentNoteId = '${journalId}' AND isDeleted = 0
UNION ALL
SELECT b.noteId
FROM branches b
JOIN subtree s ON b.parentNoteId = s.noteId
WHERE b.isDeleted = 0
)
SELECT n.noteId, n.title, n.utcDateCreated
FROM notes n
JOIN subtree s ON n.noteId = s.noteId
WHERE n.isDeleted = 0 AND n.type = 'text'
ORDER BY n.title COLLATE NOCASE
`);
return rows.map(row => {
const note = api.getNote(row.noteId);
const dateNote = note ? (note.getLabelValue('dateNote') || '') : '';
return {
noteId: row.noteId,
title: row.title || '(untitled)',
utcDateCreated: row.utcDateCreated || '',
dateNote: dateNote,
content: note ? note.getContent() : ''
};
});
}, [JOURNAL_ID]);
// ── Frontend: split checkboxes into two categories ──
const today = todayStr();
const scheduledGroups = []; // with #task tag
const inboxGroups = []; // without #task tag
for (const row of rawNotes) {
if (!row.content) continue;
const $tmp = $('<div>').html(String(row.content));
const scheduledTasks = [];
const inboxTasks = [];
let cbIndex = 0;
$tmp.find('input[type="checkbox"]').each((_, input) => {
const idx = cbIndex++;
// skip already checked items
if (input.checked || input.hasAttribute('checked')) return;
let text = '';
const $span = $(input).next('span');
if ($span.length) {
text = $span.text();
} else {
text = $(input).parent().text();
}
text = text.replace(/\s+/g, ' ').trim();
if (!text) return;
// detect #task (case-insensitive)
const isScheduled = /#task\b/i.test(text);
// strip tag from display
const displayText = text.replace(/\s*#task\b\s*/gi, ' ').replace(/\s+/g, ' ').trim();
if (isScheduled) {
scheduledTasks.push({ text: displayText, checkboxIndex: idx });
} else {
inboxTasks.push({ text: displayText, checkboxIndex: idx });
}
});
// ── Add to Scheduled group ──
if (scheduledTasks.length > 0) {
const dateStr = extractDate(row.title, row.utcDateCreated, row.dateNote);
const displayTitle = formatDateDisplay(row.dateNote) || row.title;
let dateStatus = 'none'; // 'overdue' | 'today' | 'future' | 'none'
if (dateStr) {
if (dateStr < today) dateStatus = 'overdue';
else if (dateStr === today) dateStatus = 'today';
else dateStatus = 'future';
}
scheduledGroups.push({
noteId: row.noteId,
title: displayTitle,
count: scheduledTasks.length,
dateStr: dateStr || '9999-99-99', // no date → sent to bottom
dateStatus,
tasks: scheduledTasks
});
}
// ── Add to Inbox group ──
if (inboxTasks.length > 0) {
const displayTitle = formatDateDisplay(row.dateNote) || row.title;
inboxGroups.push({
noteId: row.noteId,
title: displayTitle,
count: inboxTasks.length,
tasks: inboxTasks
});
}
}
// ── Sort Scheduled by date (ascending) ──
scheduledGroups.sort((a, b) => a.dateStr.localeCompare(b.dateStr));
const totalScheduled = scheduledGroups.reduce((s, g) => s + g.count, 0);
const totalInbox = inboxGroups.reduce((s, g) => s + g.count, 0);
// ── No open tasks ──
if (scheduledGroups.length === 0 && inboxGroups.length === 0) {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
✓ No open tasks found.
</p>
`);
return;
}
// ── Helper: render a single task row ──
function taskRow(group, task, extraClass = '') {
return `
<div
class="th-task-row"
data-note-id="${esc(group.noteId)}"
data-cb-index="${task.checkboxIndex}"
>
<span
class="th-task-check"
title="Mark as done"
>✓</span>
<span
class="th-task-text ${extraClass}"
data-note-id="${esc(group.noteId)}"
title="Open note"
>${esc(task.text)}</span>
</div>
`;
}
// ── Helper: render an entire section ──
function renderSection(groups, sectionTitle, icon, extraTaskClass = '', extraStyle = '') {
if (groups.length === 0) return '';
const total = groups.reduce((s, g) => s + g.count, 0);
let html = `<div class="th-section" style="${extraStyle}">`;
html += `
<div class="th-section-header" style="display:flex;align-items:baseline;gap:10px;font-weight:700;margin-bottom:24px;padding-bottom:12px;border-bottom:1px solid var(--main-border-color,#313244);">
<span>${icon} ${sectionTitle}</span>
<span class="th-total-count" data-section="${sectionTitle}" style="font-size:14px;color:var(--muted-text-color,#888);">
${total} task${total !== 1 ? 's' : ''}
</span>
</div>
`;
for (const group of groups) {
html += `
<div class="th-group" style="margin-bottom:20px;">
<div
class="th-note-link"
data-note-id="${esc(group.noteId)}"
style="
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted-text-color, #888);
"
>
${esc(group.title)}
<span
class="th-group-count"
style="
font-size: 13px;
background: var(--accented-background-color, #313244);
color: var(--muted-text-color, #888);
padding: 1px 7px;
border-radius: 10px;
"
>${group.count}</span>
</div>
<div
class="th-task-list"
style="
border-left: 2px solid var(--main-border-color, #313244);
padding-left: 14px;
"
>
${group.tasks.map(t => taskRow(group, t, extraTaskClass)).join('')}
</div>
</div>
`;
}
html += `</div>`;
return html;
}
// ── Build final HTML ──
// Render Scheduled section with overdue/today highlighting
function renderScheduledWithOverdue() {
if (scheduledGroups.length === 0) return '';
const total = scheduledGroups.reduce((s, g) => s + g.count, 0);
let h = `<div class="th-section">`;
h += `
<div class="th-section-header" style="display:flex;align-items:baseline;gap:10px;font-weight:700;margin-bottom:24px;padding-bottom:12px;border-bottom:1px solid var(--main-border-color,#313244);">
<span>📅 Scheduled</span>
<span class="th-total-count" data-section="Scheduled" style="font-size:14px;color:var(--muted-text-color,#888);">
${total} task${total !== 1 ? 's' : ''}
</span>
</div>
`;
for (const group of scheduledGroups) {
h += `
<div class="th-group" style="margin-bottom:20px;">
<div
class="th-note-link"
data-note-id="${esc(group.noteId)}"
style="
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: ${group.dateStatus === 'overdue' || group.dateStatus === 'today' ? '#e06c75' : 'var(--muted-text-color, #888)'};
"
>
${esc(group.title)}
${group.dateStatus === 'overdue' ? '<span style="font-size:11px;background:#e06c75;color:#fff;padding:0 5px;border-radius:4px;margin-left:4px;">Overdue</span>' : ''}
${group.dateStatus === 'today' ? '<span style="font-size:11px;background:#e06c75;color:#fff;padding:0 5px;border-radius:4px;margin-left:4px;">Today</span>' : ''}
<span
class="th-group-count"
style="
font-size: 13px;
background: var(--accented-background-color, #313244);
color: var(--muted-text-color, #888);
padding: 1px 7px;
border-radius: 10px;
"
>${group.count}</span>
</div>
<div
class="th-task-list"
style="
border-left: 2px solid ${group.dateStatus === 'overdue' || group.dateStatus === 'today' ? '#e06c75' : 'var(--main-border-color, #313244)'};
padding-left: 14px;
"
>
${group.tasks.map(t => taskRow(group, t, group.dateStatus === 'overdue' ? 'th-task-overdue' : group.dateStatus === 'today' ? 'th-task-today' : '')).join('')}
</div>
</div>
`;
}
h += `</div>`;
return h;
}
// ① Scheduled
let html = renderScheduledWithOverdue();
// ② Inbox
html += renderSection(
inboxGroups,
'Inbox',
'💡',
'',
'margin-top:28px;'
);
$root.html(html);
} catch (err) {
console.error(err);
$root.html(`
<div style="
color: #f38ba8;
background: rgba(243,139,168,.08);
padding: 12px 14px;
border-radius: 8px;
font-size: 15px;
">
✗ Error loading tasks<br><br>
${String(err.message || err)}
</div>
`);
}
}
await load();
// =========================================================
// EVENTS (delegated to $root to survive re-renders)
// =========================================================
// Group title → open note
$root.on('click', '.th-note-link', function () {
api.activateNote($(this).data('noteId'));
});
// Task text → open note
$root.on('click', '.th-task-text', function () {
api.activateNote($(this).data('noteId'));
});
// Checkbox → mark as done
$root.on('click', '.th-task-check', async function () {
const $check = $(this);
if ($check.hasClass('th-completing')) return;
const $row = $check.closest('.th-task-row');
const noteId = String($row.data('noteId'));
const cbIndex = parseInt($row.data('cbIndex'), 10);
$check.addClass('th-completing');
try {
// ── Backend: check the checkbox in source note ──
await api.runOnBackend((noteId, cbIndex) => {
const note = api.getNote(noteId);
let content = note.getContent();
let count = 0;
content = content.replace(
/<input\s+type="checkbox"([^>]*?)>/gi,
(match, attrs) => {
if (count++ === cbIndex) {
if (/\bchecked\b/i.test(attrs)) return match;
return `<input type="checkbox"${attrs} checked>`;
}
return match;
}
);
note.setContent(content);
}, [noteId, cbIndex]);
// ── UI: remove row with animation ──
const $section = $row.closest('.th-section');
const sectionTitle = $section.find('.th-total-count').data('section') || '';
$row.fadeOut(250, function () {
const $group = $(this).closest('.th-group');
$(this).remove();
// update group counter
const remainingInGroup = $group.find('.th-task-row').length;
if (remainingInGroup === 0) {
$group.fadeOut(200, function () { $(this).remove(); });
// remove section if no groups remain
if ($section.find('.th-group').length === 0) {
$section.fadeOut(200, function () { $(this).remove(); });
}
} else {
$group.find('.th-group-count').text(remainingInGroup);
}
// update section counter
const totalInSection = $section.find('.th-task-row').length;
const $sectionCount = $section.find('.th-total-count');
$sectionCount.text(
`${totalInSection} task${totalInSection !== 1 ? 's' : ''}`
);
// global empty state
const totalGlobal = $root.find('.th-task-row').length;
if (totalGlobal === 0) {
$root.html(`
<p style="color:var(--muted-text-color,#888)">
✓ No open tasks found.
</p>
`);
}
});
} catch (err) {
console.error(err);
$check.removeClass('th-completing');
}
});
})(); |
Beta Was this translation helpful? Give feedback.





Uh oh!
There was an error while loading. Please reload this page.
-
It provides a clean task overview on the right panel, helping you collect todos scattered across different notes, including due dates, priorities, recurring tasks, and quick navigation.
Features
Who Is It For
This project is a good fit if you manage tasks in TriliumNext like this:
ZIP compressed file:task-hub0.1.0.zip
Github:https://github.com/ZangXincz/TriliumNext-Task-Hub
Beta Was this translation helpful? Give feedback.
All reactions