Fix default base date formula guards

This commit is contained in:
callumalpass 2026-05-16 19:55:49 +10:00
parent e1559d3164
commit 3625fb12e8
5 changed files with 142 additions and 91 deletions

View file

@ -51,6 +51,7 @@ Example:
## Fixed
- ([#1693](https://github.com/callumalpass/tasknotes/issues/1693)) Fixed generated default Bases formulas so empty due and scheduled dates are checked with `.isEmpty()` instead of date-field truthiness. Thanks to @benoitjadinon for reporting this.
- ([#1702](https://github.com/callumalpass/tasknotes/issues/1702)) Removed the forced bottom gap under TaskNotes custom Bases views, so calendar and other TaskNotes views can fill the available pane height. Thanks to @AudreyLooby for reporting this.
- ([#1719](https://github.com/callumalpass/tasknotes/issues/1719)) Fixed task cards in reading mode so Obsidian 1.12.x header changes no longer place the card above the title and properties or drop it after returning to a tab. Thanks to @tholbrook9 for reporting this and tracing the DOM change.
- ([#1722](https://github.com/callumalpass/tasknotes/issues/1722)) Saved the source note immediately after converting an inline checkbox task to a TaskNote, so Kanban views do not keep showing the stale inline task alongside the new task file. Thanks to @literallydope for reporting this and sharing screenshots.

View file

@ -32,8 +32,8 @@ The formula set is broad so views can reuse shared computed properties without c
| Formula | Description | Expression |
|---------|-------------|------------|
| `daysUntilDue` | Days until due date (negative = overdue, positive = days remaining, null if no due date) | `if(due, ((number(date(due)) - number(today())) / 86400000).floor(), null)` |
| `daysUntilScheduled` | Days until scheduled date (negative = past, positive = days remaining, null if no scheduled date) | `if(scheduled, ((number(date(scheduled)) - number(today())) / 86400000).floor(), null)` |
| `daysUntilDue` | Days until due date (negative = overdue, positive = days remaining, null if no due date) | `if((due.isEmpty() == false), ((number(date(due)) - number(today())) / 86400000).floor(), null)` |
| `daysUntilScheduled` | Days until scheduled date (negative = past, positive = days remaining, null if no scheduled date) | `if((scheduled.isEmpty() == false), ((number(date(scheduled)) - number(today())) / 86400000).floor(), null)` |
| `daysSinceCreated` | Number of days since the task file was created | `((number(now()) - number(file.ctime)) / 86400000).floor()` |
| `daysSinceModified` | Number of days since the task file was last modified | `((number(now()) - number(file.mtime)) / 86400000).floor()` |
@ -41,10 +41,10 @@ The formula set is broad so views can reuse shared computed properties without c
| Formula | Description | Expression |
|---------|-------------|------------|
| `isOverdue` | True if task has a past due date and is not completed | `due && date(due) < today() && status != "done"` |
| `isDueToday` | True if task is due today | `due && date(due).date() == today()` |
| `isDueThisWeek` | True if task is due within the next 7 days | `due && date(due).date() >= today() && date(due).date() <= today() + "7d"` |
| `isScheduledToday` | True if task is scheduled for today | `scheduled && date(scheduled).date() == today()` |
| `isOverdue` | True if task has a past due date and is not completed | `(due.isEmpty() == false) && date(due) < today() && status != "done"` |
| `isDueToday` | True if task is due today | `(due.isEmpty() == false) && date(due).date() == today()` |
| `isDueThisWeek` | True if task is due within the next 7 days | `(due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d"` |
| `isScheduledToday` | True if task is scheduled for today | `(scheduled.isEmpty() == false) && date(scheduled).date() == today()` |
| `isRecurring` | True if task has a recurrence rule | `recurrence && !recurrence.isEmpty()` |
| `hasTimeEstimate` | True if task has a time estimate > 0 | `timeEstimate && timeEstimate > 0` |
@ -63,11 +63,11 @@ These formulas return string values useful for grouping tasks in views:
| Formula | Description | Example values | Expression |
|---------|-------------|----------------|------------|
| `dueMonth` | Due date as year-month | "2025-01", "No due date" | `if(due, date(due).format("YYYY-MM"), "No due date")` |
| `dueWeek` | Due date as year-week | "2025-W01", "No due date" | `if(due, date(due).format("YYYY-[W]WW"), "No due date")` |
| `scheduledMonth` | Scheduled date as year-month | "2025-01", "Not scheduled" | `if(scheduled, date(scheduled).format("YYYY-MM"), "Not scheduled")` |
| `scheduledWeek` | Scheduled date as year-week | "2025-W01", "Not scheduled" | `if(scheduled, date(scheduled).format("YYYY-[W]WW"), "Not scheduled")` |
| `dueDateCategory` | Human-readable due date bucket | "Overdue", "Today", "Tomorrow", "This week", "Later", "No due date" | `if(!due, "No due date", if(date(due) < today(), "Overdue", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() <= today() + "7d", "This week", "Later")))))` |
| `dueMonth` | Due date as year-month | "2025-01", "No due date" | `if((due.isEmpty() == false), date(due).format("YYYY-MM"), "No due date")` |
| `dueWeek` | Due date as year-week | "2025-W01", "No due date" | `if((due.isEmpty() == false), date(due).format("YYYY-[W]WW"), "No due date")` |
| `scheduledMonth` | Scheduled date as year-month | "2025-01", "Not scheduled" | `if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-MM"), "Not scheduled")` |
| `scheduledWeek` | Scheduled date as year-week | "2025-W01", "Not scheduled" | `if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-[W]WW"), "Not scheduled")` |
| `dueDateCategory` | Human-readable due date bucket | "Overdue", "Today", "Tomorrow", "This week", "Later", "No due date" | `if(due.isEmpty(), "No due date", if(date(due) < today(), "Overdue", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() <= today() + "7d", "This week", "Later")))))` |
| `timeEstimateCategory` | Task size by time estimate | "No estimate", "Quick (<30m)", "Medium (30m-2h)", "Long (>2h)" | `if(!timeEstimate \|\| timeEstimate == 0 \|\| timeEstimate == null, "No estimate", if(timeEstimate < 30, "Quick (<30m)", if(timeEstimate <= 120, "Medium (30m-2h)", "Long (>2h)")))` |
| `ageCategory` | Task age bucket | "Today", "This week", "This month", "Older" | `if(((number(now()) - number(file.ctime)) / 86400000) < 1, "Today", if(((number(now()) - number(file.ctime)) / 86400000) < 7, "This week", if(((number(now()) - number(file.ctime)) / 86400000) < 30, "This month", "Older")))` |
| `createdMonth` | Creation date as year-month | "2025-01" | `file.ctime.format("YYYY-MM")` |
@ -83,28 +83,28 @@ These formulas work with either due date or scheduled date, useful for finding t
| Formula | Description | Example values | Expression |
|---------|-------------|----------------|------------|
| `nextDate` | The earlier of due or scheduled date | Date value or null | `if(due && scheduled, if(date(due) < date(scheduled), due, scheduled), if(due, due, scheduled))` |
| `daysUntilNext` | Days until next date (due or scheduled, whichever is sooner) | -2, 0, 5, null | `if(due && scheduled, min(formula.daysUntilDue, formula.daysUntilScheduled), if(due, formula.daysUntilDue, formula.daysUntilScheduled))` |
| `hasDate` | True if task has either a due or scheduled date | true, false | `due \|\| scheduled` |
| `isToday` | True if due OR scheduled today | true, false | `(due && date(due).date() == today()) \|\| (scheduled && date(scheduled).date() == today())` |
| `isThisWeek` | True if due OR scheduled within 7 days | true, false | `(due && date(due).date() >= today() && date(due).date() <= today() + "7d") \|\| (scheduled && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")` |
| `nextDateCategory` | Human-readable bucket for next date | "Overdue/Past", "Today", "Tomorrow", "This week", "Later", "No date" | `if(!due && !scheduled, "No date", if((due && date(due) < today()) \|\| (scheduled && date(scheduled) < today()), "Overdue/Past", if((due && date(due).date() == today()) \|\| (scheduled && date(scheduled).date() == today()), "Today", if((due && date(due).date() == today() + "1d") \|\| (scheduled && date(scheduled).date() == today() + "1d"), "Tomorrow", if((due && date(due).date() <= today() + "7d") \|\| (scheduled && date(scheduled).date() <= today() + "7d"), "This week", "Later")))))` |
| `nextDateMonth` | Next date as year-month | "2025-01", "No date" | `if(due && scheduled, if(date(due) < date(scheduled), date(due).format("YYYY-MM"), date(scheduled).format("YYYY-MM")), if(due, date(due).format("YYYY-MM"), if(scheduled, date(scheduled).format("YYYY-MM"), "No date")))` |
| `nextDateWeek` | Next date as year-week | "2025-W01", "No date" | `if(due && scheduled, if(date(due) < date(scheduled), date(due).format("YYYY-[W]WW"), date(scheduled).format("YYYY-[W]WW")), if(due, date(due).format("YYYY-[W]WW"), if(scheduled, date(scheduled).format("YYYY-[W]WW"), "No date")))` |
| `nextDate` | The earlier of due or scheduled date | Date value or null | `if((due.isEmpty() == false) && (scheduled.isEmpty() == false), if(date(due) < date(scheduled), due, scheduled), if((due.isEmpty() == false), due, scheduled))` |
| `daysUntilNext` | Days until next date (due or scheduled, whichever is sooner) | -2, 0, 5, null | `if((due.isEmpty() == false) && (scheduled.isEmpty() == false), min(formula.daysUntilDue, formula.daysUntilScheduled), if((due.isEmpty() == false), formula.daysUntilDue, formula.daysUntilScheduled))` |
| `hasDate` | True if task has either a due or scheduled date | true, false | `(due.isEmpty() == false) \|\| (scheduled.isEmpty() == false)` |
| `isToday` | True if due OR scheduled today | true, false | `((due.isEmpty() == false) && date(due).date() == today()) \|\| ((scheduled.isEmpty() == false) && date(scheduled).date() == today())` |
| `isThisWeek` | True if due OR scheduled within 7 days | true, false | `((due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d") \|\| ((scheduled.isEmpty() == false) && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")` |
| `nextDateCategory` | Human-readable bucket for next date | "Overdue/Past", "Today", "Tomorrow", "This week", "Later", "No date" | `if(due.isEmpty() && scheduled.isEmpty(), "No date", if(((due.isEmpty() == false) && date(due) < today()) \|\| ((scheduled.isEmpty() == false) && date(scheduled) < today()), "Overdue/Past", if(((due.isEmpty() == false) && date(due).date() == today()) \|\| ((scheduled.isEmpty() == false) && date(scheduled).date() == today()), "Today", if(((due.isEmpty() == false) && date(due).date() == today() + "1d") \|\| ((scheduled.isEmpty() == false) && date(scheduled).date() == today() + "1d"), "Tomorrow", if(((due.isEmpty() == false) && date(due).date() <= today() + "7d") \|\| ((scheduled.isEmpty() == false) && date(scheduled).date() <= today() + "7d"), "This week", "Later")))))` |
| `nextDateMonth` | Next date as year-month | "2025-01", "No date" | `if((due.isEmpty() == false) && (scheduled.isEmpty() == false), if(date(due) < date(scheduled), date(due).format("YYYY-MM"), date(scheduled).format("YYYY-MM")), if((due.isEmpty() == false), date(due).format("YYYY-MM"), if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-MM"), "No date")))` |
| `nextDateWeek` | Next date as year-week | "2025-W01", "No date" | `if((due.isEmpty() == false) && (scheduled.isEmpty() == false), if(date(due) < date(scheduled), date(due).format("YYYY-[W]WW"), date(scheduled).format("YYYY-[W]WW")), if((due.isEmpty() == false), date(due).format("YYYY-[W]WW"), if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-[W]WW"), "No date")))` |
### Sorting
| Formula | Description | Expression |
|---------|-------------|------------|
| `priorityWeight` | Numeric weight for priority sorting (lower = higher priority) | `if(priority=="none",0,if(priority=="low",1,if(priority=="normal",2,if(priority=="high",3,999))))` |
| `urgencyScore` | Combines priority, next date proximity, and time-of-day (due or scheduled, higher = more urgent) | `if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))` |
| `urgencyScore` | Combines priority, next date proximity, and time-of-day (due or scheduled, higher = more urgent) | `if(due.isEmpty() && scheduled.isEmpty(), formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))` |
### Display formulas
| Formula | Description | Example values | Expression |
|---------|-------------|----------------|------------|
| `timeTrackedFormatted` | Total time tracked as readable text | "2h 30m", "45m", "0m" | `if(timeEntries, if(list(timeEntries).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) >= 60, (list(timeEntries).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) / 60).floor() + "h " + (list(timeEntries).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) % 60).round() + "m", list(timeEntries).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0).round() + "m"), "0m")` |
| `dueDateDisplay` | Due date as relative text | "Today", "Tomorrow", "Yesterday", "3d ago", "Mon", "Dec 15" | `if(!due, "", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() == today() - "1d", "Yesterday", if(date(due) < today(), formula.daysUntilDue * -1 + "d ago", if(date(due).date() <= today() + "7d", date(due).format("ddd"), date(due).format("MMM D")))))))` |
| `dueDateDisplay` | Due date as relative text | "Today", "Tomorrow", "Yesterday", "3d ago", "Mon", "Dec 15" | `if(due.isEmpty(), "", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() == today() - "1d", "Yesterday", if(date(due) < today(), formula.daysUntilDue * -1 + "d ago", if(date(due).date() <= today() + "7d", date(due).format("ddd"), date(due).format("MMM D")))))))` |
## Mini Calendar
@ -122,17 +122,17 @@ filters:
formulas:
# Sorting
priorityWeight: 'if(priority=="none",0,if(priority=="low",1,if(priority=="normal",2,if(priority=="high",3,999))))'
urgencyScore: 'if(!due, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilDue))'
urgencyScore: 'if(due.isEmpty(), formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilDue))'
# Date calculations
daysUntilDue: 'if(due, ((number(date(due)) - number(today())) / 86400000).floor(), null)'
daysUntilScheduled: 'if(scheduled, ((number(date(scheduled)) - number(today())) / 86400000).floor(), null)'
daysUntilDue: 'if((due.isEmpty() == false), ((number(date(due)) - number(today())) / 86400000).floor(), null)'
daysUntilScheduled: 'if((scheduled.isEmpty() == false), ((number(date(scheduled)) - number(today())) / 86400000).floor(), null)'
daysSinceCreated: '((number(now()) - number(file.ctime)) / 86400000).floor()'
daysSinceModified: '((number(now()) - number(file.mtime)) / 86400000).floor()'
# Booleans
isOverdue: 'due && date(due) < today() && status != "done"'
isDueToday: 'due && date(due).date() == today()'
isDueThisWeek: 'due && date(due).date() >= today() && date(due).date() <= today() + "7d"'
isScheduledToday: 'scheduled && date(scheduled).date() == today()'
isOverdue: '(due.isEmpty() == false) && date(due) < today() && status != "done"'
isDueToday: '(due.isEmpty() == false) && date(due).date() == today()'
isDueThisWeek: '(due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d"'
isScheduledToday: '(scheduled.isEmpty() == false) && date(scheduled).date() == today()'
isRecurring: 'recurrence && !recurrence.isEmpty()'
hasTimeEstimate: 'timeEstimate && timeEstimate > 0'
# Time tracking
@ -142,11 +142,11 @@ formulas:
timeTrackedToday: 'if(timeEntries, list(timeEntries).filter(value.endTime && date(value.startTime).date() == today()).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0).round(), 0)'
timeTrackedFormatted: '...' # Formats total tracked time as "Xh Ym"
# Grouping
dueMonth: 'if(due, date(due).format("YYYY-MM"), "No due date")'
dueWeek: 'if(due, date(due).format("YYYY-[W]WW"), "No due date")'
scheduledMonth: 'if(scheduled, date(scheduled).format("YYYY-MM"), "Not scheduled")'
scheduledWeek: 'if(scheduled, date(scheduled).format("YYYY-[W]WW"), "Not scheduled")'
dueDateCategory: 'if(!due, "No due date", if(date(due) < today(), "Overdue", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() <= today() + "7d", "This week", "Later")))))'
dueMonth: 'if((due.isEmpty() == false), date(due).format("YYYY-MM"), "No due date")'
dueWeek: 'if((due.isEmpty() == false), date(due).format("YYYY-[W]WW"), "No due date")'
scheduledMonth: 'if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-MM"), "Not scheduled")'
scheduledWeek: 'if((scheduled.isEmpty() == false), date(scheduled).format("YYYY-[W]WW"), "Not scheduled")'
dueDateCategory: 'if(due.isEmpty(), "No due date", if(date(due) < today(), "Overdue", if(date(due).date() == today(), "Today", if(date(due).date() == today() + "1d", "Tomorrow", if(date(due).date() <= today() + "7d", "This week", "Later")))))'
dueDateDisplay: '...' # Shows "Today", "Tomorrow", "3d ago", "Mon", "Dec 15"
timeEstimateCategory: 'if(!timeEstimate || timeEstimate == 0 || timeEstimate == null, "No estimate", if(timeEstimate < 30, "Quick (<30m)", if(timeEstimate <= 120, "Medium (30m-2h)", "Long (>2h)")))'
ageCategory: 'if(((number(now()) - number(file.ctime)) / 86400000) < 1, "Today", if(((number(now()) - number(file.ctime)) / 86400000) < 7, "This week", if(((number(now()) - number(file.ctime)) / 86400000) < 30, "This month", "Older")))'
@ -157,11 +157,11 @@ formulas:
contextCount: 'if(!contexts || list(contexts).length == 0, "No contexts", if(list(contexts).length == 1, "Single context", "Multiple contexts"))'
trackingStatus: 'if(!timeEstimate || timeEstimate == 0 || timeEstimate == null, "No estimate", if(!timeEntries || list(timeEntries).length == 0, "Not started", if(formula.efficiencyRatio < 100, "Under estimate", "Over estimate")))'
# Combined due/scheduled
nextDate: 'if(due && scheduled, if(date(due) < date(scheduled), due, scheduled), if(due, due, scheduled))'
daysUntilNext: 'if(due && scheduled, min(formula.daysUntilDue, formula.daysUntilScheduled), if(due, formula.daysUntilDue, formula.daysUntilScheduled))'
hasDate: 'due || scheduled'
isToday: '(due && date(due).date() == today()) || (scheduled && date(scheduled).date() == today())'
isThisWeek: '(due && date(due).date() >= today() && date(due).date() <= today() + "7d") || (scheduled && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")'
nextDate: 'if((due.isEmpty() == false) && (scheduled.isEmpty() == false), if(date(due) < date(scheduled), due, scheduled), if((due.isEmpty() == false), due, scheduled))'
daysUntilNext: 'if((due.isEmpty() == false) && (scheduled.isEmpty() == false), min(formula.daysUntilDue, formula.daysUntilScheduled), if((due.isEmpty() == false), formula.daysUntilDue, formula.daysUntilScheduled))'
hasDate: '(due.isEmpty() == false) || (scheduled.isEmpty() == false)'
isToday: '((due.isEmpty() == false) && date(due).date() == today()) || ((scheduled.isEmpty() == false) && date(scheduled).date() == today())'
isThisWeek: '((due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d") || ((scheduled.isEmpty() == false) && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")'
nextDateCategory: '...' # "Overdue/Past", "Today", "Tomorrow", "This week", "Later", "No date"
nextDateMonth: '...' # YYYY-MM format for next date
nextDateWeek: '...' # YYYY-[W]WW format for next date

View file

@ -269,6 +269,10 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
const scheduledProperty = getPropertyName(mapPropertyToBasesProperty('scheduled', plugin));
const recurrenceProperty = getPropertyName(mapPropertyToBasesProperty('recurrence', plugin));
const dueIsEmpty = `${dueProperty}.isEmpty()`;
const scheduledIsEmpty = `${scheduledProperty}.isEmpty()`;
const dueHasValue = `(${dueIsEmpty} == false)`;
const scheduledHasValue = `(${scheduledIsEmpty} == false)`;
return {
// Priority weight for sorting (lower = higher priority)
@ -276,10 +280,10 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
// Days until due (negative = overdue, positive = days remaining)
// Convert dates to ms (via number()) before subtracting to get numeric difference
daysUntilDue: `if(${dueProperty}, ((number(date(${dueProperty})) - number(today())) / 86400000).floor(), null)`,
daysUntilDue: `if(${dueHasValue}, ((number(date(${dueProperty})) - number(today())) / 86400000).floor(), null)`,
// Days until scheduled (negative = past, positive = days remaining)
daysUntilScheduled: `if(${scheduledProperty}, ((number(date(${scheduledProperty})) - number(today())) / 86400000).floor(), null)`,
daysUntilScheduled: `if(${scheduledHasValue}, ((number(date(${scheduledProperty})) - number(today())) / 86400000).floor(), null)`,
// Days since the task was created
daysSinceCreated: '((number(now()) - number(file.ctime)) / 86400000).floor()',
@ -290,16 +294,16 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
// === BOOLEAN FORMULAS ===
// Boolean: is this task overdue?
isOverdue: `${dueProperty} && date(${dueProperty}) < today() && ${completedStatusCheck}`,
isOverdue: `${dueHasValue} && date(${dueProperty}) < today() && ${completedStatusCheck}`,
// Boolean: is this task due today?
isDueToday: `${dueProperty} && date(${dueProperty}).date() == today()`,
isDueToday: `${dueHasValue} && date(${dueProperty}).date() == today()`,
// Boolean: is this task due within the next 7 days?
isDueThisWeek: `${dueProperty} && date(${dueProperty}).date() >= today() && date(${dueProperty}).date() <= today() + "7d"`,
isDueThisWeek: `${dueHasValue} && date(${dueProperty}).date() >= today() && date(${dueProperty}).date() <= today() + "7d"`,
// Boolean: is this task scheduled for today?
isScheduledToday: `${scheduledProperty} && date(${scheduledProperty}).date() == today()`,
isScheduledToday: `${scheduledHasValue} && date(${scheduledProperty}).date() == today()`,
// Boolean: is this a recurring task?
isRecurring: `${recurrenceProperty} && !${recurrenceProperty}.isEmpty()`,
@ -325,19 +329,19 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
// === GROUPING FORMULAS ===
// Due date formatted as "YYYY-MM" for grouping by month
dueMonth: `if(${dueProperty}, date(${dueProperty}).format("YYYY-MM"), "No due date")`,
dueMonth: `if(${dueHasValue}, date(${dueProperty}).format("YYYY-MM"), "No due date")`,
// Due date formatted as "YYYY-[W]WW" for grouping by week
dueWeek: `if(${dueProperty}, date(${dueProperty}).format("YYYY-[W]WW"), "No due date")`,
dueWeek: `if(${dueHasValue}, date(${dueProperty}).format("YYYY-[W]WW"), "No due date")`,
// Scheduled date formatted as "YYYY-MM" for grouping by month
scheduledMonth: `if(${scheduledProperty}, date(${scheduledProperty}).format("YYYY-MM"), "Not scheduled")`,
scheduledMonth: `if(${scheduledHasValue}, date(${scheduledProperty}).format("YYYY-MM"), "Not scheduled")`,
// Scheduled date formatted as "YYYY-[W]WW" for grouping by week
scheduledWeek: `if(${scheduledProperty}, date(${scheduledProperty}).format("YYYY-[W]WW"), "Not scheduled")`,
scheduledWeek: `if(${scheduledHasValue}, date(${scheduledProperty}).format("YYYY-[W]WW"), "Not scheduled")`,
// Due date category for grouping: Overdue, Today, Tomorrow, This Week, Later, No Due Date
dueDateCategory: `if(!${dueProperty}, "No due date", if(date(${dueProperty}) < today(), "Overdue", if(date(${dueProperty}).date() == today(), "Today", if(date(${dueProperty}).date() == today() + "1d", "Tomorrow", if(date(${dueProperty}).date() <= today() + "7d", "This week", "Later")))))`,
dueDateCategory: `if(${dueIsEmpty}, "No due date", if(date(${dueProperty}) < today(), "Overdue", if(date(${dueProperty}).date() == today(), "Today", if(date(${dueProperty}).date() == today() + "1d", "Tomorrow", if(date(${dueProperty}).date() <= today() + "7d", "This week", "Later")))))`,
// Time estimate category for grouping
timeEstimateCategory: `if(!${timeEstimateProperty} || ${timeEstimateProperty} == 0 || ${timeEstimateProperty} == null, "No estimate", if(${timeEstimateProperty} < 30, "Quick (<30m)", if(${timeEstimateProperty} <= 120, "Medium (30m-2h)", "Long (>2h)")))`,
@ -366,35 +370,35 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
// === COMBINED DUE/SCHEDULED FORMULAS ===
// Next date: the earlier of due or scheduled (useful for "what's coming up")
nextDate: `if(${dueProperty} && ${scheduledProperty}, if(date(${dueProperty}) < date(${scheduledProperty}), ${dueProperty}, ${scheduledProperty}), if(${dueProperty}, ${dueProperty}, ${scheduledProperty}))`,
nextDate: `if(${dueHasValue} && ${scheduledHasValue}, if(date(${dueProperty}) < date(${scheduledProperty}), ${dueProperty}, ${scheduledProperty}), if(${dueHasValue}, ${dueProperty}, ${scheduledProperty}))`,
// Days until next date (due or scheduled, whichever is sooner)
daysUntilNext: `if(${dueProperty} && ${scheduledProperty}, min(formula.daysUntilDue, formula.daysUntilScheduled), if(${dueProperty}, formula.daysUntilDue, formula.daysUntilScheduled))`,
daysUntilNext: `if(${dueHasValue} && ${scheduledHasValue}, min(formula.daysUntilDue, formula.daysUntilScheduled), if(${dueHasValue}, formula.daysUntilDue, formula.daysUntilScheduled))`,
// Boolean: has any date (due or scheduled)
hasDate: `${dueProperty} || ${scheduledProperty}`,
hasDate: `${dueHasValue} || ${scheduledHasValue}`,
// Boolean: is due or scheduled today
isToday: `(${dueProperty} && date(${dueProperty}).date() == today()) || (${scheduledProperty} && date(${scheduledProperty}).date() == today())`,
isToday: `(${dueHasValue} && date(${dueProperty}).date() == today()) || (${scheduledHasValue} && date(${scheduledProperty}).date() == today())`,
// Boolean: is due or scheduled this week
isThisWeek: `(${dueProperty} && date(${dueProperty}).date() >= today() && date(${dueProperty}).date() <= today() + "7d") || (${scheduledProperty} && date(${scheduledProperty}).date() >= today() && date(${scheduledProperty}).date() <= today() + "7d")`,
isThisWeek: `(${dueHasValue} && date(${dueProperty}).date() >= today() && date(${dueProperty}).date() <= today() + "7d") || (${scheduledHasValue} && date(${scheduledProperty}).date() >= today() && date(${scheduledProperty}).date() <= today() + "7d")`,
// Next date category for grouping (combines due and scheduled)
nextDateCategory: `if(!${dueProperty} && !${scheduledProperty}, "No date", if((${dueProperty} && date(${dueProperty}) < today()) || (${scheduledProperty} && date(${scheduledProperty}) < today()), "Overdue/Past", if((${dueProperty} && date(${dueProperty}).date() == today()) || (${scheduledProperty} && date(${scheduledProperty}).date() == today()), "Today", if((${dueProperty} && date(${dueProperty}).date() == today() + "1d") || (${scheduledProperty} && date(${scheduledProperty}).date() == today() + "1d"), "Tomorrow", if((${dueProperty} && date(${dueProperty}).date() <= today() + "7d") || (${scheduledProperty} && date(${scheduledProperty}).date() <= today() + "7d"), "This week", "Later")))))`,
nextDateCategory: `if(${dueIsEmpty} && ${scheduledIsEmpty}, "No date", if((${dueHasValue} && date(${dueProperty}) < today()) || (${scheduledHasValue} && date(${scheduledProperty}) < today()), "Overdue/Past", if((${dueHasValue} && date(${dueProperty}).date() == today()) || (${scheduledHasValue} && date(${scheduledProperty}).date() == today()), "Today", if((${dueHasValue} && date(${dueProperty}).date() == today() + "1d") || (${scheduledHasValue} && date(${scheduledProperty}).date() == today() + "1d"), "Tomorrow", if((${dueHasValue} && date(${dueProperty}).date() <= today() + "7d") || (${scheduledHasValue} && date(${scheduledProperty}).date() <= today() + "7d"), "This week", "Later")))))`,
// Next date as month for grouping
nextDateMonth: `if(${dueProperty} && ${scheduledProperty}, if(date(${dueProperty}) < date(${scheduledProperty}), date(${dueProperty}).format("YYYY-MM"), date(${scheduledProperty}).format("YYYY-MM")), if(${dueProperty}, date(${dueProperty}).format("YYYY-MM"), if(${scheduledProperty}, date(${scheduledProperty}).format("YYYY-MM"), "No date")))`,
nextDateMonth: `if(${dueHasValue} && ${scheduledHasValue}, if(date(${dueProperty}) < date(${scheduledProperty}), date(${dueProperty}).format("YYYY-MM"), date(${scheduledProperty}).format("YYYY-MM")), if(${dueHasValue}, date(${dueProperty}).format("YYYY-MM"), if(${scheduledHasValue}, date(${scheduledProperty}).format("YYYY-MM"), "No date")))`,
// Next date as week for grouping
nextDateWeek: `if(${dueProperty} && ${scheduledProperty}, if(date(${dueProperty}) < date(${scheduledProperty}), date(${dueProperty}).format("YYYY-[W]WW"), date(${scheduledProperty}).format("YYYY-[W]WW")), if(${dueProperty}, date(${dueProperty}).format("YYYY-[W]WW"), if(${scheduledProperty}, date(${scheduledProperty}).format("YYYY-[W]WW"), "No date")))`,
nextDateWeek: `if(${dueHasValue} && ${scheduledHasValue}, if(date(${dueProperty}) < date(${scheduledProperty}), date(${dueProperty}).format("YYYY-[W]WW"), date(${scheduledProperty}).format("YYYY-[W]WW")), if(${dueHasValue}, date(${dueProperty}).format("YYYY-[W]WW"), if(${scheduledHasValue}, date(${scheduledProperty}).format("YYYY-[W]WW"), "No date")))`,
// === SORTING/SCORING FORMULAS ===
// Urgency score: combines priority weight, days until next date (due or scheduled), and time-of-day.
// Higher = more urgent. The 0..1 time-of-day term ranks earlier-in-day tasks above later same-day
// tasks at the same priority. Date-only values fall back to midnight.
urgencyScore: `if(!${dueProperty} && !${scheduledProperty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))`,
urgencyScore: `if(${dueIsEmpty} && ${scheduledIsEmpty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))`,
// === DISPLAY FORMULAS ===
@ -402,7 +406,7 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record<string, string> {
timeTrackedFormatted: `if(${timeEntriesProperty}, if(list(${timeEntriesProperty}).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) >= 60, (list(${timeEntriesProperty}).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) / 60).floor() + "h " + (list(${timeEntriesProperty}).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0) % 60).round() + "m", list(${timeEntriesProperty}).filter(value.endTime).map((number(date(value.endTime)) - number(date(value.startTime))) / 60000).reduce(acc + value, 0).round() + "m"), "0m")`,
// Due date as human-readable relative text
dueDateDisplay: `if(!${dueProperty}, "", if(date(${dueProperty}).date() == today(), "Today", if(date(${dueProperty}).date() == today() + "1d", "Tomorrow", if(date(${dueProperty}).date() == today() - "1d", "Yesterday", if(date(${dueProperty}) < today(), formula.daysUntilDue * -1 + "d ago", if(date(${dueProperty}).date() <= today() + "7d", date(${dueProperty}).format("ddd"), date(${dueProperty}).format("MMM D")))))))`,
dueDateDisplay: `if(${dueIsEmpty}, "", if(date(${dueProperty}).date() == today(), "Today", if(date(${dueProperty}).date() == today() + "1d", "Tomorrow", if(date(${dueProperty}).date() == today() - "1d", "Yesterday", if(date(${dueProperty}) < today(), formula.daysUntilDue * -1 + "d ago", if(date(${dueProperty}).date() <= today() + "7d", date(${dueProperty}).format("ddd"), date(${dueProperty}).format("MMM D")))))))`,
};
}

View file

@ -1,36 +1,65 @@
/**
* Reproduction tests for issue #1693.
*
* Reported behavior:
* - Bang (!) negation on date properties in Bases formula expressions does
* not work as expected. `!due` does not evaluate as truthy when due is
* empty/null. Users must use `.isEmpty()` instead.
*/
import { generateBasesFileTemplate } from "../../../src/templates/defaultBasesFiles";
describe('Issue #1693: Bang negation on date properties in base formulas', () => {
it.skip('reproduces issue #1693 - bang negation on empty date property', () => {
// Simulate how date property values might be represented in the Bases
// formula context. If empty dates are passed as empty strings or
// wrapper objects rather than null/undefined, ! won't work as expected.
const createMockPlugin = () => {
const fieldMapping = {
status: "status",
priority: "priority",
due: "due",
scheduled: "scheduled",
projects: "projects",
contexts: "contexts",
recurrence: "recurrence",
completeInstances: "complete_instances",
blockedBy: "blockedBy",
sortOrder: "sort_order",
timeEstimate: "timeEstimate",
timeEntries: "timeEntries",
};
// Possible representations of an empty date property:
const emptyDateAsNull = null;
const emptyDateAsUndefined = undefined;
const emptyDateAsEmptyString = '';
const emptyDateAsObject = {}; // Possible Bases Value wrapper
return {
settings: {
taskTag: "task",
taskIdentificationMethod: "tag",
customPriorities: [
{ value: "high", label: "High", weight: 0 },
{ value: "normal", label: "Normal", weight: 1 },
],
customStatuses: [
{ value: "open", label: "Open", isCompleted: false },
{ value: "done", label: "Done", isCompleted: true },
],
defaultVisibleProperties: ["status", "priority", "due", "scheduled"],
userFields: [],
fieldMapping,
},
fieldMapper: {
toUserField: jest.fn((key: keyof typeof fieldMapping) => fieldMapping[key] ?? key),
getMapping: jest.fn(() => fieldMapping),
},
};
};
// JavaScript truthiness with bang:
expect(!emptyDateAsNull).toBe(true); // Works correctly
expect(!emptyDateAsUndefined).toBe(true); // Works correctly
expect(!emptyDateAsEmptyString).toBe(true); // Works correctly
expect(!emptyDateAsObject).toBe(false); // BUG: empty object is truthy
describe("Issue #1693: bang negation on date properties in base formulas", () => {
it("generates default formulas using date isEmpty checks instead of date truthiness", () => {
const template = generateBasesFileTemplate("open-tasks-view", createMockPlugin() as any);
// If Bases provides date values as objects (even when empty),
// the bang operator will always evaluate to false.
// This means `if (!due && !scheduled, ...)` would never match
// the "both empty" case if dates are represented as objects.
expect(template).toContain(
`urgencyScore: 'if(due.isEmpty() && scheduled.isEmpty(), formula.priorityWeight`
);
expect(template).toContain(`dueDateCategory: 'if(due.isEmpty(), "No due date"`);
expect(template).toContain(`dueDateDisplay: 'if(due.isEmpty(), ""`);
expect(template).toContain(
`nextDate: 'if((due.isEmpty() == false) && (scheduled.isEmpty() == false)`
);
expect(template).toContain(
`hasDate: '(due.isEmpty() == false) || (scheduled.isEmpty() == false)'`
);
// The .isEmpty() method would check the internal value correctly,
// which is why the workaround works.
expect(template).not.toContain("if(!due");
expect(template).not.toContain("!due && !scheduled");
expect(template).not.toContain("if(due,");
expect(template).not.toContain("if(scheduled,");
expect(template).not.toContain("due && date(due)");
expect(template).not.toContain("scheduled && date(scheduled)");
});
});

View file

@ -133,10 +133,10 @@ describe("defaultBasesFiles", () => {
// Pin full bodies of the affected formulas so a regression in any single
// clause (lower bound, upper bound, due half, scheduled half) breaks the test.
expect(template).toContain(
`isDueThisWeek: 'due && date(due).date() >= today() && date(due).date() <= today() + "7d"'`
`isDueThisWeek: '(due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d"'`
);
expect(template).toContain(
`isThisWeek: '(due && date(due).date() >= today() && date(due).date() <= today() + "7d") || (scheduled && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")'`
`isThisWeek: '((due.isEmpty() == false) && date(due).date() >= today() && date(due).date() <= today() + "7d") || ((scheduled.isEmpty() == false) && date(scheduled).date() >= today() && date(scheduled).date() <= today() + "7d")'`
);
// Negative guards against any reappearance of the time-naive shape on a
@ -159,7 +159,7 @@ describe("defaultBasesFiles", () => {
const template = generateBasesFileTemplate("open-tasks-view", createMockPlugin() as any);
expect(template).toContain(
`urgencyScore: 'if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))'`
`urgencyScore: 'if(due.isEmpty() && scheduled.isEmpty(), formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))'`
);
// Guard against the time-naive form returning
@ -168,6 +168,23 @@ describe("defaultBasesFiles", () => {
);
});
it("uses isEmpty guards instead of date property truthiness in generated formulas", () => {
const template = generateBasesFileTemplate("open-tasks-view", createMockPlugin() as any);
expect(template).toContain("due.isEmpty()");
expect(template).toContain("scheduled.isEmpty()");
expect(template).toContain(
`nextDateCategory: 'if(due.isEmpty() && scheduled.isEmpty(), "No date"`
);
expect(template).toContain(
`hasDate: '(due.isEmpty() == false) || (scheduled.isEmpty() == false)'`
);
expect(template).not.toContain("!due");
expect(template).not.toContain("!scheduled");
expect(template).not.toContain("if(due,");
expect(template).not.toContain("if(scheduled,");
});
it("time-of-day boost is monotonic and bounded in [0, 1]", () => {
// Verifies the math invariant the formula relies on, independent of YAML shape.
// boost = 1 - fractional_day(ms_since_epoch). A given Date earlier in its day