mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add checklist progress task-card property with Bases support
This commit is contained in:
parent
ae3cd41151
commit
69b8ae2506
23 changed files with 459 additions and 216 deletions
|
|
@ -29,6 +29,13 @@ Example:
|
|||
- [Mdbase](https://mdbase.dev) type generation now emits `tn_role` annotations on schema fields, allowing external tools (e.g. [mtn CLI](https://github.com/callumalpass/mdbase-tasknotes)) to discover each field's semantic role regardless of custom frontmatter names
|
||||
- Mdbase type match rules now use tag or frontmatter property matching (based on task identification settings) instead of path glob, with automatic fallback to tag matching
|
||||
- Mdbase type status field now includes `tn_completed_values` annotation, listing which status values count as completed
|
||||
- (#1576) Added an optional `Checklist Progress` task-card property that renders a compact progress bar and `completed/total` count from top-level markdown checkboxes
|
||||
- Uses Obsidian `metadataCache.listItems` parsing (no vault body reads), minimizing rendering overhead
|
||||
- Nested checklist items are excluded so progress reflects first-level task steps only
|
||||
- Bases views map `tasks` (`file.tasks`) to `Checklist Progress` for TaskNotes cards
|
||||
- Existing `.base` files need `file.tasks` added to the view `order` YAML manually (it is not auto-added to pickers by default)
|
||||
- Default generated `.base` templates now include `file.tasks` in view `order` arrays so checklist progress is available out of the box and then appears in the picker
|
||||
- Thanks to @phortx for the initial PR and for opening #1576
|
||||
|
||||
## Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ These settings control the visual appearance of the plugin, including the calend
|
|||
|
||||
Use **Default visible properties** to decide what metadata appears on task cards without opening each task. This is the primary control for card density.
|
||||
|
||||
Checklist progress is available as a visible property in task cards. In Bases view `order` arrays, the corresponding source property is `file.tasks` (shown as `tasks` in Bases property pickers once present in the view `order` list).
|
||||
|
||||
## Display Formatting
|
||||
|
||||
Use **Time format** to switch between 12-hour and 24-hour display across all TaskNotes surfaces.
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- property: due
|
||||
direction: ASC
|
||||
|
|
@ -224,6 +225,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
groupBy:
|
||||
property: status
|
||||
direction: ASC
|
||||
|
|
@ -264,6 +266,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: due
|
||||
direction: ASC
|
||||
|
|
@ -299,6 +302,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: formula.urgencyScore
|
||||
direction: DESC
|
||||
|
|
@ -332,6 +336,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: formula.urgencyScore
|
||||
direction: DESC
|
||||
|
|
@ -363,6 +368,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: formula.urgencyScore
|
||||
direction: DESC
|
||||
|
|
@ -400,6 +406,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: formula.urgencyScore
|
||||
direction: DESC
|
||||
|
|
@ -432,6 +439,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
sort:
|
||||
- column: status
|
||||
direction: ASC
|
||||
|
|
@ -466,6 +474,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
options:
|
||||
showScheduled: true
|
||||
showDue: true
|
||||
|
|
@ -512,6 +521,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
options:
|
||||
showPropertyBasedEvents: false
|
||||
calendarView: "listWeek"
|
||||
|
|
@ -559,6 +569,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
groupBy:
|
||||
property: status
|
||||
direction: ASC
|
||||
|
|
@ -579,6 +590,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
- type: tasknotesTaskList
|
||||
name: "Blocked By"
|
||||
filters:
|
||||
|
|
@ -597,6 +609,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
- type: tasknotesKanban
|
||||
name: "Blocking"
|
||||
filters:
|
||||
|
|
@ -615,6 +628,7 @@ views:
|
|||
- file.name
|
||||
- recurrence
|
||||
- complete_instances
|
||||
- file.tasks
|
||||
groupBy:
|
||||
property: status
|
||||
direction: ASC
|
||||
|
|
@ -629,6 +643,7 @@ If you've customized your TaskNotes settings (e.g., renamed properties, added cu
|
|||
- **Custom priorities**: The `priorityWeight` formula will include all your configured priorities with their weights
|
||||
- **Property-based identification**: If you identify tasks by a property instead of a tag, the filters will use that property
|
||||
- **Custom visible properties**: The `order` arrays will include your configured visible properties
|
||||
- **Essential card properties**: `file.name`, recurrence, `complete_instances`, and `file.tasks` are always included in generated `order` arrays for TaskNotes card rendering
|
||||
After major settings changes, regenerate default files and diff against customized versions to merge template updates.
|
||||
|
||||
## Related
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ Each swimlane row includes:
|
|||
|
||||
Each task card displays information based on the visible properties configured in the Bases view. Standard task information includes title, priority, due date, and scheduled date.
|
||||
|
||||
To show checklist progress on cards, include `file.tasks` in the view `order` array.
|
||||
For existing `.base` files, add this in YAML manually first; after it is in `order`, it appears in the Bases picker as `tasks`.
|
||||
|
||||
Click a card to open the task file for editing. Right-click to access the context menu for task actions. Drag cards between columns or swimlane cells to update the task's properties.
|
||||
|
||||
## Column Operations
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ views:
|
|||
|
||||
**`name`**: Display name shown in the view header
|
||||
|
||||
**`order`**: Array of property names that control which task properties are visible in the task cards. Properties are referenced using their Bases property paths (e.g., `note.status`, `note.priority`, `note.due`).
|
||||
**`order`**: Array of property names that control which task properties are visible in the task cards. Properties are referenced using their Bases property paths (e.g., `note.status`, `note.priority`, `note.due`, `file.tasks`).
|
||||
For existing `.base` files, add `file.tasks` in YAML manually; once present in `order`, it appears in the Bases picker as `tasks`.
|
||||
|
||||
**`sort`**: Array of sort criteria. Tasks are sorted by the first criterion, with ties broken by subsequent criteria.
|
||||
- `column`: Property to sort by (e.g., `due`, `scheduled`, `priority`, `title`)
|
||||
|
|
@ -88,6 +89,7 @@ TaskNotes properties are accessed in Bases YAML using these paths:
|
|||
| Projects | `note.projects` | Associated projects |
|
||||
| Contexts | `note.contexts` | Task contexts |
|
||||
| Tags | `file.tags` | File tags |
|
||||
| Checklist progress | `file.tasks` | First-level markdown checkbox progress shown as the TaskNotes checklist progress bar |
|
||||
| Time estimate | `note.timeEstimate` | Estimated duration |
|
||||
| Recurrence | `note.recurrence` | Recurrence pattern |
|
||||
| Blocked by | `note.blockedBy` | Blocking dependencies |
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ import type { FrontmatterPropertyName, TaskCardPropertyId } from "../types";
|
|||
* 4. Return: "complete_instances" (NOT "completeInstances")
|
||||
* 5. TaskCard uses PROPERTY_EXTRACTORS["complete_instances"] ✓
|
||||
*
|
||||
* Special transformations (for computed properties):
|
||||
* - "timeEntries" → "totalTrackedTime" (show total instead of raw array)
|
||||
* - "blockedBy" → "blocked" (show status pill instead of array)
|
||||
* Special transformations (for computed properties):
|
||||
* - "timeEntries" → "totalTrackedTime" (show total instead of raw array)
|
||||
* - "blockedBy" → "blocked" (show status pill instead of array)
|
||||
* - "file.tasks" / "formula.checklistProgress" → "checklistProgress"
|
||||
*/
|
||||
export class PropertyMappingService {
|
||||
constructor(
|
||||
|
|
@ -90,20 +91,22 @@ export class PropertyMappingService {
|
|||
return this.applySpecialTransformations(stripped);
|
||||
}
|
||||
|
||||
if (basesPropertyId.startsWith("file.")) {
|
||||
// Map specific file properties to TaskInfo equivalents
|
||||
if (basesPropertyId === "file.ctime") return "dateCreated";
|
||||
if (basesPropertyId === "file.mtime") return "dateModified";
|
||||
if (basesPropertyId.startsWith("file.")) {
|
||||
// Map specific file properties to TaskInfo equivalents
|
||||
if (basesPropertyId === "file.ctime") return "dateCreated";
|
||||
if (basesPropertyId === "file.mtime") return "dateModified";
|
||||
if (basesPropertyId === "file.tasks") return "checklistProgress";
|
||||
|
||||
// Keep file.* prefix for computed file properties (backlinks, links, etc.)
|
||||
// This distinguishes them from note.* properties with the same name
|
||||
return basesPropertyId;
|
||||
}
|
||||
|
||||
// Keep file.* prefix for computed file properties (backlinks, links, etc.)
|
||||
// This distinguishes them from note.* properties with the same name
|
||||
return basesPropertyId;
|
||||
}
|
||||
|
||||
// Step 3: Keep formula properties unchanged
|
||||
if (basesPropertyId.startsWith("formula.")) {
|
||||
return basesPropertyId;
|
||||
}
|
||||
// Step 3: Keep formula properties unchanged
|
||||
if (basesPropertyId.startsWith("formula.")) {
|
||||
if (basesPropertyId === "formula.checklistProgress") return "checklistProgress";
|
||||
return basesPropertyId;
|
||||
}
|
||||
|
||||
// Step 4: Direct property (no prefix) - apply special transformations
|
||||
return this.applySpecialTransformations(basesPropertyId);
|
||||
|
|
@ -150,16 +153,21 @@ export class PropertyMappingService {
|
|||
* These are display-only transformations that don't affect data storage.
|
||||
* Transforms calculated properties to their display representations.
|
||||
*/
|
||||
private applySpecialTransformations(propId: string): string {
|
||||
// timeEntries → totalTrackedTime (show computed total instead of raw array)
|
||||
if (propId === "timeEntries") return "totalTrackedTime";
|
||||
|
||||
// blockedBy → blocked (show status pill instead of dependency array)
|
||||
if (propId === "blockedBy") return "blocked";
|
||||
|
||||
// Keep everything else unchanged
|
||||
return propId;
|
||||
}
|
||||
private applySpecialTransformations(propId: string): string {
|
||||
// timeEntries → totalTrackedTime (show computed total instead of raw array)
|
||||
if (propId === "timeEntries") return "totalTrackedTime";
|
||||
|
||||
// blockedBy → blocked (show status pill instead of dependency array)
|
||||
if (propId === "blockedBy") return "blocked";
|
||||
|
||||
// Keep only explicit Bases-facing checklist progress aliases.
|
||||
// "Tasks" (file.tasks) is selectable in Bases UI.
|
||||
if (propId === "file.tasks") return "checklistProgress";
|
||||
if (propId === "formula.checklistProgress") return "checklistProgress";
|
||||
|
||||
// Keep everything else unchanged
|
||||
return propId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for basesToTaskCardProperty() for backward compatibility.
|
||||
|
|
|
|||
|
|
@ -56,11 +56,14 @@ export function mapBasesPropertyToTaskCardProperty(
|
|||
* Transformations:
|
||||
* - timeEntries → totalTrackedTime (show computed total instead of raw array)
|
||||
* - blockedBy → blocked (show status pill instead of dependency list)
|
||||
* - file.tasks/formula.checklistProgress → checklistProgress
|
||||
* - All other properties pass through unchanged
|
||||
*/
|
||||
function applySpecialTransformations(propId: string): string {
|
||||
if (propId === "timeEntries") return "totalTrackedTime";
|
||||
if (propId === "blockedBy") return "blocked";
|
||||
if (propId === "file.tasks") return "checklistProgress";
|
||||
if (propId === "formula.checklistProgress") return "checklistProgress";
|
||||
return propId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const de: TranslationTree = {
|
|||
scheduled: "Planungsdatum",
|
||||
timeEstimate: "Zeitschätzung",
|
||||
totalTrackedTime: "Gesamte erfasste Zeit",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Wiederholung",
|
||||
completedDate: "Abschlussdatum",
|
||||
createdDate: "Erstellungsdatum",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const de: TranslationTree = {
|
|||
scheduledDate: "Planungsdatum",
|
||||
timeEstimate: "Zeitschätzung",
|
||||
totalTrackedTime: "Gesamte erfasste Zeit",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Wiederholung",
|
||||
completedDate: "Abschlussdatum",
|
||||
createdDate: "Erstellungsdatum",
|
||||
|
|
|
|||
|
|
@ -1212,6 +1212,7 @@ export const en: TranslationTree = {
|
|||
scheduled: "Scheduled Date",
|
||||
timeEstimate: "Time Estimate",
|
||||
totalTrackedTime: "Total Tracked Time",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recurrence",
|
||||
completedDate: "Completed Date",
|
||||
createdDate: "Created Date",
|
||||
|
|
@ -3110,6 +3111,7 @@ export const en: TranslationTree = {
|
|||
scheduledDate: "Scheduled Date",
|
||||
timeEstimate: "Time Estimate",
|
||||
totalTrackedTime: "Total Tracked Time",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recurrence",
|
||||
completedDate: "Completed Date",
|
||||
createdDate: "Created Date",
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const es: TranslationTree = {
|
|||
scheduled: "Fecha programada",
|
||||
timeEstimate: "Estimación de tiempo",
|
||||
totalTrackedTime: "Tiempo total rastreado",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recurrencia",
|
||||
completedDate: "Fecha de finalización",
|
||||
createdDate: "Fecha de creación",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const es: TranslationTree = {
|
|||
scheduledDate: "Fecha programada",
|
||||
timeEstimate: "Estimación de tiempo",
|
||||
totalTrackedTime: "Tiempo total rastreado",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recurrencia",
|
||||
completedDate: "Fecha de finalización",
|
||||
createdDate: "Fecha de creación",
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const fr: TranslationTree = {
|
|||
scheduled: "Date planifiée",
|
||||
timeEstimate: "Estimation de temps",
|
||||
totalTrackedTime: "Temps suivi total",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Récurrence",
|
||||
completedDate: "Date d'achèvement",
|
||||
createdDate: "Date de création",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const fr: TranslationTree = {
|
|||
scheduledDate: "Date planifiée",
|
||||
timeEstimate: "Estimation de temps",
|
||||
totalTrackedTime: "Temps suivi total",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Récurrence",
|
||||
completedDate: "Date d'achèvement",
|
||||
createdDate: "Date de création",
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const ja: TranslationTree = {
|
|||
scheduled: "予定日",
|
||||
timeEstimate: "時間見積もり",
|
||||
totalTrackedTime: "総追跡時間",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "繰り返し",
|
||||
completedDate: "完了日",
|
||||
createdDate: "作成日",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const ja: TranslationTree = {
|
|||
scheduledDate: "予定日",
|
||||
timeEstimate: "時間見積もり",
|
||||
totalTrackedTime: "総追跡時間",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "繰り返し",
|
||||
completedDate: "完了日",
|
||||
createdDate: "作成日",
|
||||
|
|
|
|||
|
|
@ -1149,6 +1149,7 @@ export const ko: TranslationTree = {
|
|||
scheduled: "예정일",
|
||||
timeEstimate: "시간 예상",
|
||||
totalTrackedTime: "총 기록 시간",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "반복",
|
||||
completedDate: "완료일",
|
||||
createdDate: "생성일",
|
||||
|
|
@ -2990,6 +2991,7 @@ export const ko: TranslationTree = {
|
|||
scheduledDate: "예정일",
|
||||
timeEstimate: "시간 예상",
|
||||
totalTrackedTime: "총 기록 시간",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "반복",
|
||||
completedDate: "완료일",
|
||||
createdDate: "생성일",
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,7 @@ export const pt: TranslationTree = {
|
|||
scheduled: "Data Agendada",
|
||||
timeEstimate: "Estimativa de Tempo",
|
||||
totalTrackedTime: "Tempo Total Registrado",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recorrência",
|
||||
completedDate: "Data de Conclusão",
|
||||
createdDate: "Data de Criação",
|
||||
|
|
@ -3054,6 +3055,7 @@ export const pt: TranslationTree = {
|
|||
scheduledDate: "Data Agendada",
|
||||
timeEstimate: "Estimativa de Tempo",
|
||||
totalTrackedTime: "Tempo Total Registrado",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Recorrência",
|
||||
completedDate: "Data de Conclusão",
|
||||
createdDate: "Data de Criação",
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const ru: TranslationTree = {
|
|||
scheduled: "Запланированная дата",
|
||||
timeEstimate: "Оценка времени",
|
||||
totalTrackedTime: "Общее отслеженное время",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Повторение",
|
||||
completedDate: "Дата завершения",
|
||||
createdDate: "Дата создания",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const ru: TranslationTree = {
|
|||
scheduledDate: "Запланированная дата",
|
||||
timeEstimate: "Оценка времени",
|
||||
totalTrackedTime: "Общее отслеженное время",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "Повторение",
|
||||
completedDate: "Дата завершения",
|
||||
createdDate: "Дата создания",
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ export const zh: TranslationTree = {
|
|||
scheduled: "安排日期",
|
||||
timeEstimate: "时间估计",
|
||||
totalTrackedTime: "总跟踪时间",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "重复",
|
||||
completedDate: "完成日期",
|
||||
createdDate: "创建日期",
|
||||
|
|
@ -3040,6 +3041,7 @@ export const zh: TranslationTree = {
|
|||
scheduledDate: "安排日期",
|
||||
timeEstimate: "时间估计",
|
||||
totalTrackedTime: "总跟踪时间",
|
||||
checklistProgress: "Checklist Progress",
|
||||
recurrence: "重复",
|
||||
completedDate: "完成日期",
|
||||
createdDate: "创建日期",
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ function mapPropertyToBasesProperty(property: string, plugin: TaskNotesPlugin):
|
|||
case "totalTrackedTime":
|
||||
// totalTrackedTime is computed from timeEntries, use the timeEntries property
|
||||
return fm.toUserField("timeEntries");
|
||||
case "checklistProgress":
|
||||
// checklistProgress is computed from markdown checklist items.
|
||||
// Use file.tasks as the selectable Bases source property.
|
||||
return "file.tasks";
|
||||
}
|
||||
|
||||
// Try to map using FieldMapper
|
||||
|
|
@ -145,6 +149,7 @@ function generateOrderArray(plugin: TaskNotesPlugin): string[] {
|
|||
"file.name", // title
|
||||
mapPropertyToBasesProperty("recurrence", plugin),
|
||||
mapPropertyToBasesProperty("complete_instances", plugin),
|
||||
mapPropertyToBasesProperty("checklistProgress", plugin),
|
||||
].filter((prop): prop is string => !!prop);
|
||||
|
||||
// Combine, removing duplicates while preserving order
|
||||
|
|
|
|||
|
|
@ -176,6 +176,13 @@ export class PropertyVisibilityDropdown {
|
|||
),
|
||||
category: "core" as const,
|
||||
},
|
||||
{
|
||||
id: "checklistProgress",
|
||||
name: this.plugin.i18n.translate(
|
||||
"components.propertyVisibilityDropdown.properties.checklistProgress"
|
||||
),
|
||||
category: "core" as const,
|
||||
},
|
||||
{
|
||||
id: "recurrence",
|
||||
name: this.plugin.i18n.translate(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-console */
|
||||
import { TFile, setIcon, Notice, Modal, App, setTooltip, parseLinktext, Menu } from "obsidian";
|
||||
import { TFile, setIcon, Notice, Modal, App, setTooltip, parseLinktext, Menu, type CachedMetadata } from "obsidian";
|
||||
import { TaskInfo } from "../types";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { TaskContextMenu } from "../components/TaskContextMenu";
|
||||
|
|
@ -454,6 +454,63 @@ function getDefaultVisibleProperties(plugin: TaskNotesPlugin): string[] {
|
|||
return convertInternalToUserProperties(internalDefaults, plugin);
|
||||
}
|
||||
|
||||
interface ChecklistProgress {
|
||||
completed: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checklist progress from Obsidian's metadata cache listItems.
|
||||
* Uses parsed metadata only (no vault body reads) for better performance.
|
||||
*/
|
||||
function getChecklistProgress(taskPath: string, plugin: TaskNotesPlugin): ChecklistProgress | null {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(taskPath);
|
||||
if (!(file instanceof TFile)) return null;
|
||||
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
return calculateChecklistProgress(fileCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate first-level checklist progress from cached list items.
|
||||
* Only top-level task list items are counted (nested subtasks are ignored).
|
||||
*/
|
||||
function calculateChecklistProgress(cache: CachedMetadata | null): ChecklistProgress | null {
|
||||
const listItems = cache?.listItems;
|
||||
if (!Array.isArray(listItems) || listItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
|
||||
for (const item of listItems) {
|
||||
if (!item || typeof item.task !== "string") continue;
|
||||
|
||||
// Obsidian uses parent >= 0 for nested list items.
|
||||
const isNested = typeof item.parent === "number" && item.parent >= 0;
|
||||
if (isNested) continue;
|
||||
|
||||
total += 1;
|
||||
// Count only explicit checked boxes as complete ([x] / [X]).
|
||||
// Other task markers (e.g. [-], [>]) are treated as not complete.
|
||||
if (item.task.toLowerCase() === "x") {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
percent: Math.round((completed / total) * 100),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Property value extractors for better type safety and error handling
|
||||
*/
|
||||
|
|
@ -479,6 +536,7 @@ const PROPERTY_EXTRACTORS: Record<string, (task: TaskInfo) => any> = {
|
|||
dateCreated: (task) => task.dateCreated,
|
||||
dateModified: (task) => task.dateModified,
|
||||
googleCalendarSync: (task) => task.path, // Used to check if task is synced via plugin settings
|
||||
checklistProgress: (task) => task.path, // Used to compute checklist progress from metadata cache listItems
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -921,6 +979,29 @@ const PROPERTY_RENDERERS: Record<string, PropertyRenderer> = {
|
|||
element.textContent = `Linked to ${value.length} calendar ${value.length === 1 ? "event" : "events"}`;
|
||||
}
|
||||
},
|
||||
checklistProgress: (element, _value, task, plugin) => {
|
||||
const progress = getChecklistProgress(task.path, plugin);
|
||||
if (!progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progressEl = element.createEl("span", { cls: "task-card__progress" });
|
||||
const progressBar = progressEl.createEl("span", { cls: "task-card__progress-bar" });
|
||||
const progressFill = progressBar.createEl("span", { cls: "task-card__progress-fill" });
|
||||
progressFill.style.width = `${progress.percent}%`;
|
||||
if (progress.percent > 0 && progress.percent < 5) {
|
||||
progressFill.style.minWidth = "2px";
|
||||
}
|
||||
|
||||
progressEl.createEl("span", {
|
||||
cls: "task-card__progress-label",
|
||||
text: `${progress.completed}/${progress.total}`,
|
||||
});
|
||||
|
||||
setTooltip(progressEl, `${progress.percent}% complete (${progress.completed}/${progress.total})`, {
|
||||
placement: "top",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export function getAvailableProperties(
|
|||
{ id: "scheduled", label: makeLabel("Scheduled Date", "scheduled") },
|
||||
{ id: "timeEstimate", label: makeLabel("Time Estimate", "timeEstimate") },
|
||||
{ id: "totalTrackedTime", label: "Total Tracked Time" }, // Computed property, not in FieldMapping
|
||||
{ id: "checklistProgress", label: "Checklist Progress" }, // Computed from metadata cache listItems
|
||||
{ id: "recurrence", label: makeLabel("Recurrence", "recurrence") },
|
||||
{ id: "completeInstances", label: makeLabel("Completed Instances", "completeInstances") },
|
||||
{ id: "skippedInstances", label: makeLabel("Skipped Instances", "skippedInstances") },
|
||||
|
|
|
|||
|
|
@ -170,6 +170,43 @@
|
|||
height: 12px;
|
||||
}
|
||||
|
||||
/* Checklist progress metadata */
|
||||
.tasknotes-plugin .task-card__metadata-property--checklistProgress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card__progress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card__progress-bar {
|
||||
width: 72px;
|
||||
height: 6px;
|
||||
background: color-mix(in srgb, var(--background-modifier-border) 70%, transparent);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card__progress-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--interactive-accent);
|
||||
border-radius: inherit;
|
||||
transition: width var(--tn-transition-fast);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card__progress-label {
|
||||
font-size: var(--tn-font-size-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--tn-text-muted);
|
||||
}
|
||||
|
||||
|
||||
.tasknotes-plugin .task-card__tag {
|
||||
background: var(--tn-bg-secondary);
|
||||
|
|
@ -1254,6 +1291,23 @@
|
|||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card--layout-inline .task-card__metadata-property--checklistProgress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card--layout-inline .task-card__progress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-card--layout-inline .task-card__progress-bar {
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
/* Metadata pills in inline mode */
|
||||
.tasknotes-plugin .task-card--layout-inline .task-card__metadata-pill {
|
||||
display: inline;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { PropertyMappingService } from "../../../src/bases/PropertyMappingService";
|
||||
|
||||
describe("Issue #1576 - Bases checklist progress aliases", () => {
|
||||
const fieldMapperStub = {
|
||||
isRecognizedProperty: jest.fn(() => false),
|
||||
getMapping: jest.fn(() => ({})),
|
||||
toUserField: jest.fn((key: string) => key),
|
||||
fromUserField: jest.fn(() => null),
|
||||
};
|
||||
|
||||
const mapper = new PropertyMappingService({} as any, fieldMapperStub as any);
|
||||
|
||||
it("maps file.tasks to checklistProgress", () => {
|
||||
expect(mapper.basesToTaskCardProperty("file.tasks")).toBe("checklistProgress");
|
||||
});
|
||||
|
||||
it("maps formula.checklistProgress to checklistProgress", () => {
|
||||
expect(mapper.basesToTaskCardProperty("formula.checklistProgress")).toBe("checklistProgress");
|
||||
});
|
||||
|
||||
it("does not map note/task tasks aliases", () => {
|
||||
expect(mapper.basesToTaskCardProperty("tasks")).toBe("tasks");
|
||||
expect(mapper.basesToTaskCardProperty("note.tasks")).toBe("tasks");
|
||||
expect(mapper.basesToTaskCardProperty("task.tasks")).toBe("tasks");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,211 +1,222 @@
|
|||
/**
|
||||
* Skipped reproduction tests for Issue #1576:
|
||||
* [FR]: Display Progress Bar
|
||||
*
|
||||
* Feature Description:
|
||||
* Inspired by Trello, display a small progress bar on each task card showing
|
||||
* the completion progress of first-level checkboxes in the task's details.
|
||||
*
|
||||
* For example, if a task's `details` field contains:
|
||||
* - [x] Buy groceries
|
||||
* - [ ] Clean the house
|
||||
* - [x] Walk the dog
|
||||
* - [ ] Read a book
|
||||
*
|
||||
* The progress bar should show 50% (2 of 4 completed).
|
||||
*
|
||||
* Key Implementation Points:
|
||||
* 1. Parse `task.details` for markdown checkboxes (- [ ] and - [x])
|
||||
* 2. Only count first-level checkboxes (not nested/indented ones)
|
||||
* 3. Calculate completion percentage
|
||||
* 4. Render a progress bar element on the task card (after title, before/in metadata)
|
||||
* 5. Show text like "2/4" alongside the bar
|
||||
* 6. Only show the progress bar when checkboxes exist in details
|
||||
*
|
||||
* Relevant Source Files:
|
||||
* - src/ui/TaskCard.ts (createTaskCard, updateTaskCard)
|
||||
* - src/types.ts (TaskInfo.details field)
|
||||
* - src/utils/TasksPluginParser.ts (CHECKBOX_PATTERN regex)
|
||||
* - styles/task-card-bem.css (BEM card styles)
|
||||
*
|
||||
* Suggested BEM Classes:
|
||||
* - .task-card__progress (container)
|
||||
* - .task-card__progress-bar (the bar track)
|
||||
* - .task-card__progress-fill (the filled portion)
|
||||
* - .task-card__progress-label (text like "2/4")
|
||||
*/
|
||||
import { createTaskCard, updateTaskCard } from "../../../src/ui/TaskCard";
|
||||
import { TaskFactory } from "../../helpers/mock-factories";
|
||||
import { App, MockObsidian } from "../../__mocks__/obsidian";
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
jest.mock("obsidian");
|
||||
|
||||
/**
|
||||
* Helper to count first-level markdown checkboxes in a string.
|
||||
* First-level means no leading whitespace (or minimal, non-nested indentation).
|
||||
* Returns { total, completed }.
|
||||
*/
|
||||
function countCheckboxes(text: string): { total: number; completed: number } {
|
||||
const lines = text.split('\n');
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
jest.mock("../../../src/utils/helpers", () => ({
|
||||
calculateTotalTimeSpent: jest.fn(() => 0),
|
||||
getEffectiveTaskStatus: jest.fn((task) => task.status || "open"),
|
||||
shouldUseRecurringTaskUI: jest.fn(() => false),
|
||||
getRecurringTaskCompletionText: jest.fn(() => "Not completed for this date"),
|
||||
getRecurrenceDisplayText: jest.fn(() => "Daily"),
|
||||
filterEmptyProjects: jest.fn((projects) => projects?.filter((p: string) => p && p.trim()) || []),
|
||||
}));
|
||||
|
||||
for (const line of lines) {
|
||||
// First-level checkboxes: no leading whitespace (or at most the list marker indent)
|
||||
const match = line.match(/^(?:[-*+]|\d+\.)\s+\[([ xX])\]/);
|
||||
if (match) {
|
||||
total++;
|
||||
if (match[1] === 'x' || match[1] === 'X') {
|
||||
completed++;
|
||||
}
|
||||
jest.mock("../../../src/utils/dateUtils", () => ({
|
||||
isTodayTimeAware: jest.fn(() => false),
|
||||
isOverdueTimeAware: jest.fn(() => false),
|
||||
formatDateTimeForDisplay: jest.fn(() => "Jan 15"),
|
||||
getDatePart: jest.fn(() => ""),
|
||||
getTimePart: jest.fn(() => null),
|
||||
formatDateForStorage: jest.fn((value: Date | string) => {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString().split("T")[0];
|
||||
}
|
||||
return value?.split("T")[0] || "";
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/components/TaskContextMenu", () => ({
|
||||
TaskContextMenu: jest.fn().mockImplementation(() => ({
|
||||
show: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("Issue #1576 - Display Progress Bar on task cards", () => {
|
||||
let app: App;
|
||||
let plugin: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MockObsidian.reset();
|
||||
|
||||
app = new App();
|
||||
plugin = {
|
||||
app,
|
||||
fieldMapper: {
|
||||
lookupMappingKey: jest.fn((propertyId: string) => {
|
||||
const mapped = new Set(["status", "priority", "due", "scheduled", "contexts", "projects"]);
|
||||
return mapped.has(propertyId) ? propertyId : null;
|
||||
}),
|
||||
isPropertyForField: jest.fn((propertyId: string, field: string) => propertyId === field),
|
||||
toUserField: jest.fn((field: string) => field),
|
||||
getMapping: jest.fn(() => ({
|
||||
status: "status",
|
||||
priority: "priority",
|
||||
due: "due",
|
||||
scheduled: "scheduled",
|
||||
contexts: "contexts",
|
||||
projects: "projects",
|
||||
})),
|
||||
},
|
||||
statusManager: {
|
||||
isCompletedStatus: jest.fn((status: string) => status === "done"),
|
||||
getStatusConfig: jest.fn((status: string) => ({
|
||||
value: status,
|
||||
label: status,
|
||||
color: "#666666",
|
||||
})),
|
||||
getNextStatus: jest.fn(() => "done"),
|
||||
getCompletedStatuses: jest.fn(() => ["done"]),
|
||||
},
|
||||
priorityManager: {
|
||||
getPriorityConfig: jest.fn((priority: string) => ({
|
||||
value: priority,
|
||||
label: priority,
|
||||
color: "#ff0000",
|
||||
})),
|
||||
},
|
||||
getActiveTimeSession: jest.fn(() => null),
|
||||
cacheManager: {
|
||||
getTaskInfo: jest.fn(),
|
||||
},
|
||||
updateTaskProperty: jest.fn(),
|
||||
getTaskByPath: jest.fn(),
|
||||
projectSubtasksService: {
|
||||
isTaskUsedAsProject: jest.fn().mockResolvedValue(false),
|
||||
isTaskUsedAsProjectSync: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
i18n: {
|
||||
translate: jest.fn((key: string) => key),
|
||||
},
|
||||
settings: {
|
||||
singleClickAction: "edit",
|
||||
doubleClickAction: "none",
|
||||
showExpandableSubtasks: true,
|
||||
subtaskChevronPosition: "right",
|
||||
hideCompletedFromOverdue: true,
|
||||
calendarViewSettings: {
|
||||
timeFormat: "24",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function listItem(taskChar: string, parent: number, line: number) {
|
||||
return {
|
||||
task: taskChar,
|
||||
parent,
|
||||
position: {
|
||||
start: { line, col: 0, offset: 0 },
|
||||
end: { line, col: 10, offset: 10 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { total, completed };
|
||||
}
|
||||
|
||||
describe('Issue #1576: Display Progress Bar on task cards', () => {
|
||||
|
||||
describe('checkbox counting logic', () => {
|
||||
it.skip('reproduces issue #1576 - should count first-level checkboxes in task details', () => {
|
||||
const details = [
|
||||
'- [x] Buy groceries',
|
||||
'- [ ] Clean the house',
|
||||
'- [x] Walk the dog',
|
||||
'- [ ] Read a book',
|
||||
].join('\n');
|
||||
|
||||
const result = countCheckboxes(details);
|
||||
expect(result.total).toBe(4);
|
||||
expect(result.completed).toBe(2);
|
||||
it("renders checklist progress and ignores nested checkboxes", () => {
|
||||
const task = TaskFactory.createTask({
|
||||
path: "tasks/progress-task.md",
|
||||
title: "Checklist task",
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - should ignore nested checkboxes (only first-level)', () => {
|
||||
const details = [
|
||||
'- [x] Main task 1',
|
||||
' - [x] Subtask 1a',
|
||||
' - [ ] Subtask 1b',
|
||||
'- [ ] Main task 2',
|
||||
' - [x] Deeply nested',
|
||||
].join('\n');
|
||||
|
||||
// Only first-level items should be counted (Main task 1, Main task 2)
|
||||
const result = countCheckboxes(details);
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.completed).toBe(1);
|
||||
MockObsidian.createTestFile(task.path, "# Checklist task");
|
||||
app.metadataCache.setCache(task.path, {
|
||||
frontmatter: { title: task.title },
|
||||
listItems: [
|
||||
listItem("x", -1, 10), // top-level complete
|
||||
listItem(" ", -1, 11), // top-level incomplete
|
||||
listItem("x", 10, 12), // nested complete (ignored)
|
||||
listItem(" ", 11, 13), // nested incomplete (ignored)
|
||||
],
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - should return zero counts when no checkboxes exist', () => {
|
||||
const details = 'This is a plain text description with no checkboxes.';
|
||||
const card = createTaskCard(task, plugin, ["checklistProgress"]);
|
||||
|
||||
const result = countCheckboxes(details);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.completed).toBe(0);
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - should handle all checkboxes completed', () => {
|
||||
const details = [
|
||||
'- [x] Task A',
|
||||
'- [X] Task B',
|
||||
'- [x] Task C',
|
||||
].join('\n');
|
||||
|
||||
const result = countCheckboxes(details);
|
||||
expect(result.total).toBe(3);
|
||||
expect(result.completed).toBe(3);
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - should handle numbered list checkboxes', () => {
|
||||
const details = [
|
||||
'1. [x] First item',
|
||||
'2. [ ] Second item',
|
||||
'3. [x] Third item',
|
||||
].join('\n');
|
||||
|
||||
const result = countCheckboxes(details);
|
||||
expect(result.total).toBe(3);
|
||||
expect(result.completed).toBe(2);
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - should handle empty details', () => {
|
||||
const result = countCheckboxes('');
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.completed).toBe(0);
|
||||
});
|
||||
const progressEl = card.querySelector(".task-card__progress");
|
||||
expect(progressEl).not.toBeNull();
|
||||
expect(card.querySelector(".task-card__progress-label")?.textContent).toBe("1/2");
|
||||
expect((card.querySelector(".task-card__progress-fill") as HTMLElement).style.width).toBe("50%");
|
||||
});
|
||||
|
||||
describe('progress bar rendering on task card', () => {
|
||||
it.skip('reproduces issue #1576 - task card should include a progress bar when details contain checkboxes', () => {
|
||||
// When createTaskCard() is called with a task that has checkboxes in details,
|
||||
// the card DOM should contain a .task-card__progress element.
|
||||
//
|
||||
// Expected DOM structure:
|
||||
// <div class="task-card__progress">
|
||||
// <div class="task-card__progress-bar">
|
||||
// <div class="task-card__progress-fill" style="width: 50%"></div>
|
||||
// </div>
|
||||
// <span class="task-card__progress-label">2/4</span>
|
||||
// </div>
|
||||
//
|
||||
// const task = TaskFactory.createTask({
|
||||
// details: '- [x] Done\n- [ ] Not done\n- [x] Also done\n- [ ] Pending'
|
||||
// });
|
||||
// const card = createTaskCard(task, mockPlugin);
|
||||
// const progressEl = card.querySelector('.task-card__progress');
|
||||
// expect(progressEl).not.toBeNull();
|
||||
// const fill = card.querySelector('.task-card__progress-fill') as HTMLElement;
|
||||
// expect(fill.style.width).toBe('50%');
|
||||
// const label = card.querySelector('.task-card__progress-label');
|
||||
// expect(label?.textContent).toBe('2/4');
|
||||
it("does not render checklist progress when only nested checkboxes exist", () => {
|
||||
const task = TaskFactory.createTask({
|
||||
path: "tasks/nested-only-task.md",
|
||||
title: "Nested only",
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - task card should NOT show progress bar when details have no checkboxes', () => {
|
||||
// When task.details contains no checkboxes, no progress bar should be rendered.
|
||||
//
|
||||
// const task = TaskFactory.createTask({
|
||||
// details: 'Just some notes about the task.'
|
||||
// });
|
||||
// const card = createTaskCard(task, mockPlugin);
|
||||
// const progressEl = card.querySelector('.task-card__progress');
|
||||
// expect(progressEl).toBeNull();
|
||||
MockObsidian.createTestFile(task.path, "# Nested only");
|
||||
app.metadataCache.setCache(task.path, {
|
||||
frontmatter: { title: task.title },
|
||||
listItems: [
|
||||
listItem("x", 5, 10),
|
||||
listItem(" ", 5, 11),
|
||||
],
|
||||
});
|
||||
|
||||
it.skip('reproduces issue #1576 - task card should NOT show progress bar when details are undefined', () => {
|
||||
// When task.details is undefined, no progress bar should be rendered.
|
||||
//
|
||||
// const task = TaskFactory.createTask({ details: undefined });
|
||||
// const card = createTaskCard(task, mockPlugin);
|
||||
// const progressEl = card.querySelector('.task-card__progress');
|
||||
// expect(progressEl).toBeNull();
|
||||
});
|
||||
const card = createTaskCard(task, plugin, ["checklistProgress"]);
|
||||
|
||||
it.skip('reproduces issue #1576 - progress bar should show 100% when all checkboxes are completed', () => {
|
||||
// When all checkboxes are checked, the progress fill should be at 100%.
|
||||
//
|
||||
// const task = TaskFactory.createTask({
|
||||
// details: '- [x] Task A\n- [x] Task B\n- [x] Task C'
|
||||
// });
|
||||
// const card = createTaskCard(task, mockPlugin);
|
||||
// const fill = card.querySelector('.task-card__progress-fill') as HTMLElement;
|
||||
// expect(fill.style.width).toBe('100%');
|
||||
// const label = card.querySelector('.task-card__progress-label');
|
||||
// expect(label?.textContent).toBe('3/3');
|
||||
});
|
||||
expect(card.querySelector(".task-card__progress")).toBeNull();
|
||||
const metadata = card.querySelector(".task-card__metadata") as HTMLElement;
|
||||
expect(metadata.style.display).toBe("none");
|
||||
});
|
||||
|
||||
describe('progress bar CSS', () => {
|
||||
it.skip('reproduces issue #1576 - task-card-bem.css should contain progress bar styles', () => {
|
||||
// The CSS should include styles for:
|
||||
// - .task-card__progress (container, flex row, aligned)
|
||||
// - .task-card__progress-bar (track, background, border-radius, height)
|
||||
// - .task-card__progress-fill (fill, transition, themed color)
|
||||
// - .task-card__progress-label (small font, muted color)
|
||||
//
|
||||
// const fs = require('fs');
|
||||
// const path = require('path');
|
||||
// const cssPath = path.resolve(__dirname, '../../../styles/task-card-bem.css');
|
||||
// const css = fs.readFileSync(cssPath, 'utf-8');
|
||||
// expect(css).toContain('.task-card__progress');
|
||||
// expect(css).toContain('.task-card__progress-bar');
|
||||
// expect(css).toContain('.task-card__progress-fill');
|
||||
// expect(css).toContain('.task-card__progress-label');
|
||||
it("updates checklist progress after metadata cache changes", () => {
|
||||
const task = TaskFactory.createTask({
|
||||
path: "tasks/update-progress-task.md",
|
||||
title: "Update progress",
|
||||
});
|
||||
|
||||
MockObsidian.createTestFile(task.path, "# Update progress");
|
||||
app.metadataCache.setCache(task.path, {
|
||||
frontmatter: { title: task.title },
|
||||
listItems: [
|
||||
listItem("x", -1, 10),
|
||||
listItem(" ", -1, 11),
|
||||
],
|
||||
});
|
||||
|
||||
const card = createTaskCard(task, plugin, ["checklistProgress"]);
|
||||
expect(card.querySelector(".task-card__progress-label")?.textContent).toBe("1/2");
|
||||
|
||||
app.metadataCache.setCache(task.path, {
|
||||
frontmatter: { title: task.title },
|
||||
listItems: [
|
||||
listItem("x", -1, 10),
|
||||
listItem("x", -1, 11),
|
||||
],
|
||||
});
|
||||
|
||||
updateTaskCard(card, task, plugin, ["checklistProgress"]);
|
||||
|
||||
expect(card.querySelector(".task-card__progress-label")?.textContent).toBe("2/2");
|
||||
expect((card.querySelector(".task-card__progress-fill") as HTMLElement).style.width).toBe("100%");
|
||||
});
|
||||
|
||||
it("treats non-x task markers as incomplete", () => {
|
||||
const task = TaskFactory.createTask({
|
||||
path: "tasks/custom-marker-task.md",
|
||||
title: "Custom marker",
|
||||
});
|
||||
|
||||
MockObsidian.createTestFile(task.path, "# Custom marker");
|
||||
app.metadataCache.setCache(task.path, {
|
||||
frontmatter: { title: task.title },
|
||||
listItems: [
|
||||
listItem("-", -1, 10), // not completed
|
||||
listItem("x", -1, 11), // completed
|
||||
],
|
||||
});
|
||||
|
||||
const card = createTaskCard(task, plugin, ["checklistProgress"]);
|
||||
expect(card.querySelector(".task-card__progress-label")?.textContent).toBe("1/2");
|
||||
expect((card.querySelector(".task-card__progress-fill") as HTMLElement).style.width).toBe("50%");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue