fix: multiple bug fixes and UI enhancements

- (#904) Add visual highlighting for overdue/past dates on task cards
- (#1128) Allow slashes in NLP context names for hierarchical contexts
- (#1151) Increase mini calendar heatmap color intensity for better visibility
- (#1164) Add priorityWeight formula to default Bases templates
- (#1170) Fix dateCreated/dateModified using custom property names in ICS notes
- (#1171) Fix calendar event toggles not responding to changes after initial load
- (#1198) Fix project links resolving relative to wrong file in Bases views
This commit is contained in:
callumalpass 2025-11-30 17:16:16 +11:00
parent 1b921bed6e
commit 4ef3fc482c
10 changed files with 187 additions and 50 deletions

View file

@ -69,6 +69,17 @@ Example:
- Prevents accidental data loss from clicking outside the modal or pressing Escape
- Thanks to @renatomen for the PR and @0-BSCode for the feature request
- (#904) Added visual highlighting for overdue and past dates on task cards
- Overdue due dates now display in red text
- Past scheduled dates now display in blue text
- Thanks to @ras0q for the suggestion
- (#1164) Added `priorityWeight` formula to all default Bases templates for priority-based sorting
- Formula maps each priority value to a numeric weight based on your custom priority configuration
- Sort by `formula.priorityWeight` in ascending order to get highest priority tasks first
- Example: `if(priority=="high",0,if(priority=="normal",1,if(priority=="low",2,999)))`
- Thanks to @jhedlund for the suggestion
## Changed
- Improved inline task conversion to merge TasksPlugin and NLP parsing results
@ -83,6 +94,11 @@ Example:
- Fixed subtask chevron vertical alignment with status dot
- Reduced swimlane label column width in Kanban view
- (#1151) Increased mini calendar heatmap color intensity for better visibility
- Days with notes now show more noticeable colors, especially with low note counts
- Intensity levels increased from 10/25/45/65% to 25/40/55/70%
- Thanks to @arreme for the suggestion
## Fixed
- (#1157) Fixed inline task embeds breaking layout when placed in indented bullet lists
@ -136,3 +152,22 @@ Example:
- Selecting "completion" as the recurrence anchor now correctly persists to the task frontmatter
- Thanks to @blaxcky for reporting
- (#1128) Fixed NLP parser not allowing slashes in context names
- Contexts like `@shopping/groceries` were being split into `@shopping` and `/groceries`
- Hierarchical context names now work the same as tags and projects
- Thanks to @wealthychef1 for reporting
- (#1170) Fixed `dateCreated` and `dateModified` not using custom property names when creating notes from calendar events
- Notes created from ICS events now respect the field mapping configured in settings
- Thanks to @maddie-m for reporting
- (#1171) Fixed calendar event toggles not responding to changes after initial load
- Toggling "Show scheduled tasks", "Show due tasks", and other event filters now correctly updates the calendar
- Previously, changing these toggles had no effect until the view was reloaded
- Thanks to @hangryscribe3 for reporting
- (#1198) Fixed project links being created relative to the wrong file when editing from Bases views
- Relative paths in project links are now correctly resolved relative to the task note file
- Previously, editing a task from a Bases view would create project links relative to the `.base` file
- Thanks to @minchinweb for reporting

View file

@ -261,8 +261,58 @@ export class CalendarView extends BasesViewBase {
return defaultValue;
}
/**
* Read event toggle options from config.
* These should be re-read on every render to respond to toggle changes.
*/
private readEventToggles(): void {
// Guard: config may not be set yet if called too early
if (!this.config || typeof this.config.get !== 'function') {
return;
}
try {
this.viewOptions.showScheduled = this.config.get('showScheduled') ?? this.viewOptions.showScheduled;
this.viewOptions.showDue = this.config.get('showDue') ?? this.viewOptions.showDue;
this.viewOptions.showRecurring = this.config.get('showRecurring') ?? this.viewOptions.showRecurring;
this.viewOptions.showTimeEntries = this.config.get('showTimeEntries') ?? this.viewOptions.showTimeEntries;
this.viewOptions.showTimeblocks = this.config.get('showTimeblocks') ?? this.viewOptions.showTimeblocks;
this.viewOptions.showPropertyBasedEvents = this.config.get('showPropertyBasedEvents') ?? this.viewOptions.showPropertyBasedEvents;
// ICS calendar toggles
if (this.plugin.icsSubscriptionService) {
const subscriptions = this.plugin.icsSubscriptionService.getSubscriptions();
for (const sub of subscriptions) {
const key = `showICS_${sub.id}`;
this.icsCalendarToggles.set(sub.id, this.config.get(key) ?? true);
}
}
// Google calendar toggles
if (this.plugin.googleCalendarService) {
const calendars = this.plugin.googleCalendarService.getAvailableCalendars();
for (const cal of calendars) {
const key = `showGoogleCalendar_${cal.id}`;
this.googleCalendarToggles.set(cal.id, this.config.get(key) ?? true);
}
}
// Microsoft calendar toggles
if (this.plugin.microsoftCalendarService) {
const calendars = this.plugin.microsoftCalendarService.getAvailableCalendars();
for (const cal of calendars) {
const key = `showMicrosoftCalendar_${cal.id}`;
this.microsoftCalendarToggles.set(cal.id, this.config.get(key) ?? true);
}
}
} catch (e) {
console.error("[TaskNotes][CalendarView] Error reading event toggles:", e);
}
}
/**
* Read view configuration options from BasesViewConfig.
* Layout options are only read once to avoid resetting the view on toggle changes.
*/
private readViewOptions(): void {
// Guard: config may not be set yet if called too early
@ -271,13 +321,8 @@ export class CalendarView extends BasesViewBase {
}
try {
// Events
this.viewOptions.showScheduled = this.config.get('showScheduled') ?? this.viewOptions.showScheduled;
this.viewOptions.showDue = this.config.get('showDue') ?? this.viewOptions.showDue;
this.viewOptions.showRecurring = this.config.get('showRecurring') ?? this.viewOptions.showRecurring;
this.viewOptions.showTimeEntries = this.config.get('showTimeEntries') ?? this.viewOptions.showTimeEntries;
this.viewOptions.showTimeblocks = this.config.get('showTimeblocks') ?? this.viewOptions.showTimeblocks;
this.viewOptions.showPropertyBasedEvents = this.config.get('showPropertyBasedEvents') ?? this.viewOptions.showPropertyBasedEvents;
// Always read event toggles
this.readEventToggles();
// Date navigation
this.viewOptions.initialDate = this.config.get('initialDate') ?? this.viewOptions.initialDate;
@ -326,33 +371,6 @@ export class CalendarView extends BasesViewBase {
this.viewOptions.endDateProperty = this.config.get('endDateProperty') ?? this.viewOptions.endDateProperty;
this.viewOptions.titleProperty = this.config.get('titleProperty') ?? this.viewOptions.titleProperty;
// ICS calendar toggles
if (this.plugin.icsSubscriptionService) {
const subscriptions = this.plugin.icsSubscriptionService.getSubscriptions();
for (const sub of subscriptions) {
const key = `showICS_${sub.id}`;
this.icsCalendarToggles.set(sub.id, this.config.get(key) ?? true);
}
}
// Google calendar toggles
if (this.plugin.googleCalendarService) {
const calendars = this.plugin.googleCalendarService.getAvailableCalendars();
for (const cal of calendars) {
const key = `showGoogleCalendar_${cal.id}`;
this.googleCalendarToggles.set(cal.id, this.config.get(key) ?? true);
}
}
// Microsoft calendar toggles
if (this.plugin.microsoftCalendarService) {
const calendars = this.plugin.microsoftCalendarService.getAvailableCalendars();
for (const cal of calendars) {
const key = `showMicrosoftCalendar_${cal.id}`;
this.microsoftCalendarToggles.set(cal.id, this.config.get(key) ?? true);
}
}
// Read enableSearch toggle (default: false for backward compatibility)
const enableSearchValue = this.config.get('enableSearch');
this.enableSearch = (enableSearchValue as boolean) ?? false;
@ -376,6 +394,9 @@ export class CalendarView extends BasesViewBase {
// Ensure view options are read (in case config wasn't available in onload)
if (!this.configLoaded && this.config) {
this.readViewOptions();
} else if (this.config) {
// Always re-read event toggles to respond to toggle changes
this.readEventToggles();
}
// Now that config is loaded, setup search (idempotent: will only create once)

View file

@ -1645,10 +1645,7 @@ export abstract class TaskModal extends Modal {
}
protected updateProjectsFromFiles(): void {
// Convert selected files to markdown links using generateMarkdownLink
const currentFile = this.app.workspace.getActiveFile();
const sourcePath = currentFile?.path || "";
// Update the projects string from selected items
this.projects = this.selectedProjectItems.map((item) => item.link).join(", ");
}
@ -1668,6 +1665,9 @@ export abstract class TaskModal extends Modal {
// This handles both old plain string projects and new [[link]] format
this.selectedProjectItems = [];
// Use the task's path as the source for resolving relative links
const sourcePath = this.getCurrentTaskPath() || "";
for (const projectString of projects) {
// Skip null, undefined, or empty strings
if (
@ -1682,7 +1682,7 @@ export abstract class TaskModal extends Modal {
const linkMatch = projectString.match(/^\[\[([^\]]+)\]\]$/);
if (linkMatch) {
const linkPath = linkMatch[1];
const file = this.resolveLink(linkPath, "");
const file = this.resolveLink(linkPath, sourcePath);
if (file) {
// Resolved link
this.selectedProjectItems.push({
@ -1704,7 +1704,7 @@ export abstract class TaskModal extends Modal {
const markdownMatch = projectString.match(/^\[([^\]]*)\]\(([^)]+)\)$/);
if (markdownMatch) {
const linkPath = parseLinkToPath(projectString);
const file = this.resolveLink(linkPath, "");
const file = this.resolveLink(linkPath, sourcePath);
if (file) {
// Resolved markdown link
this.selectedProjectItems.push({

View file

@ -196,10 +196,12 @@ export class ICSNoteService {
};
// Process template if provided
const dateCreatedField = this.plugin.fieldMapper.toUserField("dateCreated");
const dateModifiedField = this.plugin.fieldMapper.toUserField("dateModified");
let frontmatter: Record<string, any> = {
title: noteTitle,
dateCreated: getCurrentTimestamp(),
dateModified: getCurrentTimestamp(),
[dateCreatedField]: getCurrentTimestamp(),
[dateModifiedField]: getCurrentTimestamp(),
tags: [this.plugin.fieldMapper.toUserField("icsEventTag")],
[this.plugin.fieldMapper.toUserField("icsEventId")]: [icsEvent.id],
};
@ -350,7 +352,8 @@ export class ICSNoteService {
}
frontmatter[icsEventIdField] = existingIds;
frontmatter.dateModified = getCurrentTimestamp();
const dateModifiedField = this.plugin.fieldMapper.toUserField("dateModified");
frontmatter[dateModifiedField] = getCurrentTimestamp();
});
new Notice(

View file

@ -266,7 +266,7 @@ export class NaturalLanguageParser {
const escapedTrigger = this.escapeRegex(trigger);
// Use Unicode-aware pattern to support non-ASCII characters (accented, Cyrillic, CJK, etc.)
const contextPattern = new RegExp(`${escapedTrigger}[\\p{L}\\p{N}\\p{M}_]+`, "gu");
const contextPattern = new RegExp(`${escapedTrigger}[\\p{L}\\p{N}\\p{M}_/-]+`, "gu");
const contextMatches = text.match(contextPattern);
if (contextMatches) {

View file

@ -170,6 +170,48 @@ function formatOrderArray(orderArray: string[]): string {
return orderArray.map(prop => ` - ${prop}`).join('\n');
}
/**
* Generate a priorityWeight formula based on user's custom priorities.
* Creates nested if() statements that map priority values to their weights.
* Lower weight = higher priority, so tasks sort correctly in ascending order.
*
* Example output: if(priority=="high",0,if(priority=="normal",1,if(priority=="low",2,999)))
*/
function generatePriorityWeightFormula(plugin: TaskNotesPlugin): string {
const settings = plugin.settings;
const priorityProperty = getPropertyName(mapPropertyToBasesProperty('priority', plugin));
// Sort priorities by weight (ascending - lower weight = higher priority)
const sortedPriorities = [...settings.customPriorities].sort((a, b) => a.weight - b.weight);
if (sortedPriorities.length === 0) {
// No priorities configured, return a constant
return '999';
}
// Build nested if statements from the inside out
// Start with the fallback value (for tasks with no priority or unknown priority)
let formula = '999';
// Work backwards through priorities to build nested ifs
for (let i = sortedPriorities.length - 1; i >= 0; i--) {
const priority = sortedPriorities[i];
// Use the index as the weight value (0 = highest priority)
formula = `if(${priorityProperty}=="${priority.value}",${i},${formula})`;
}
return formula;
}
/**
* Generate the formulas section YAML including priorityWeight
*/
function generateFormulasSection(plugin: TaskNotesPlugin): string {
const priorityWeightFormula = generatePriorityWeightFormula(plugin);
return `formulas:
priorityWeight: '${priorityWeightFormula}'`;
}
/**
* Generate a Bases file template for a specific command with user settings
*/
@ -178,6 +220,7 @@ export function generateBasesFileTemplate(commandId: string, plugin: TaskNotesPl
const taskFilterCondition = generateTaskFilterCondition(settings);
const orderArray = generateOrderArray(plugin);
const orderYaml = formatOrderArray(orderArray);
const formulasSection = generateFormulasSection(plugin);
switch (commandId) {
case 'open-calendar-view': {
@ -188,6 +231,8 @@ export function generateBasesFileTemplate(commandId: string, plugin: TaskNotesPl
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesMiniCalendar
name: "Due"
@ -215,6 +260,8 @@ ${orderYaml}
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesKanban
name: "Kanban Board"
@ -258,6 +305,8 @@ ${orderYaml}
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesTaskList
name: "All Tasks"
@ -392,6 +441,8 @@ ${orderYaml}
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesCalendar
name: "Calendar"
@ -417,6 +468,8 @@ ${orderYaml}
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesCalendar
name: "Agenda"
@ -441,6 +494,8 @@ ${orderYaml}
${formatFilterAsYAML([taskFilterCondition])}
${formulasSection}
views:
- type: tasknotesKanban
name: "Subtasks"

View file

@ -1167,6 +1167,9 @@ function renderDueDateProperty(
element.textContent = dueDateText;
element.classList.add("task-card__metadata-date", "task-card__metadata-date--due");
if (isDueOverdue) {
element.classList.add("task-card__metadata-date--overdue");
}
element.dataset.tnAction = "edit-date";
element.dataset.tnDateType = "due";
@ -1213,6 +1216,9 @@ function renderScheduledDateProperty(
element.textContent = scheduledDateText;
element.classList.add("task-card__metadata-date", "task-card__metadata-date--scheduled");
if (isScheduledPast) {
element.classList.add("task-card__metadata-date--past");
}
element.dataset.tnAction = "edit-date";
element.dataset.tnDateType = "scheduled";

View file

@ -1044,19 +1044,19 @@
}
.tasknotes-plugin .mini-calendar-view__day--intensity-low {
background: color-mix(in srgb, var(--tn-interactive-accent) 10%, var(--tn-bg-primary)) !important;
}
.tasknotes-plugin .mini-calendar-view__day--intensity-medium {
background: color-mix(in srgb, var(--tn-interactive-accent) 25%, var(--tn-bg-primary)) !important;
}
.tasknotes-plugin .mini-calendar-view__day--intensity-medium {
background: color-mix(in srgb, var(--tn-interactive-accent) 40%, var(--tn-bg-primary)) !important;
}
.tasknotes-plugin .mini-calendar-view__day--intensity-high {
background: color-mix(in srgb, var(--tn-interactive-accent) 45%, var(--tn-bg-primary)) !important;
background: color-mix(in srgb, var(--tn-interactive-accent) 55%, var(--tn-bg-primary)) !important;
}
.tasknotes-plugin .mini-calendar-view__day--intensity-very-high {
background: color-mix(in srgb, var(--tn-interactive-accent) 65%, var(--tn-bg-primary)) !important;
background: color-mix(in srgb, var(--tn-interactive-accent) 70%, var(--tn-bg-primary)) !important;
color: var(--tn-text-on-accent);
font-weight: 600;
}

View file

@ -660,6 +660,16 @@
color: var(--tn-color-info);
}
/* Overdue due dates - use the hover background color for text */
.tasknotes-plugin .task-card__metadata-date--overdue {
color: var(--tn-color-error);
}
/* Past scheduled dates - use the hover background color for text */
.tasknotes-plugin .task-card__metadata-date--past {
color: var(--tn-color-info);
}
/* Project links */
.tasknotes-plugin .task-card__project-link {
color: var(--interactive-accent);

View file

@ -109,6 +109,13 @@ describe('NaturalLanguageParser', () => {
expect(result.title).toBe('Meeting with team');
});
it('should extract nested contexts with forward slashes', () => {
const result = parser.parseInput('Buy milk @shopping/groceries @home/kitchen');
expect(result.contexts).toEqual(['shopping/groceries', 'home/kitchen']);
expect(result.title).toBe('Buy milk');
});
it('should handle mixed tags and contexts', () => {
const result = parser.parseInput('Fix #bug @home in #codebase @weekend');