From 4ef3fc482cd328207d6c550c67adcc2267de0fb3 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 30 Nov 2025 17:16:16 +1100 Subject: [PATCH] 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 --- docs/releases/unreleased.md | 35 ++++++++ src/bases/CalendarView.ts | 89 ++++++++++++------- src/modals/TaskModal.ts | 12 +-- src/services/ICSNoteService.ts | 9 +- src/services/NaturalLanguageParser.ts | 2 +- src/templates/defaultBasesFiles.ts | 55 ++++++++++++ src/ui/TaskCard.ts | 6 ++ styles/calendar-view.css | 12 +-- styles/task-card-bem.css | 10 +++ .../services/NaturalLanguageParser.test.ts | 7 ++ 10 files changed, 187 insertions(+), 50 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 1cfcf41f..f603b954 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -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 + diff --git a/src/bases/CalendarView.ts b/src/bases/CalendarView.ts index 39b50d69..187c0730 100644 --- a/src/bases/CalendarView.ts +++ b/src/bases/CalendarView.ts @@ -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) diff --git a/src/modals/TaskModal.ts b/src/modals/TaskModal.ts index e83b6e2d..a005836a 100644 --- a/src/modals/TaskModal.ts +++ b/src/modals/TaskModal.ts @@ -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({ diff --git a/src/services/ICSNoteService.ts b/src/services/ICSNoteService.ts index 92775e98..71181256 100644 --- a/src/services/ICSNoteService.ts +++ b/src/services/ICSNoteService.ts @@ -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 = { 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( diff --git a/src/services/NaturalLanguageParser.ts b/src/services/NaturalLanguageParser.ts index 07a9fe7e..6adf34a3 100644 --- a/src/services/NaturalLanguageParser.ts +++ b/src/services/NaturalLanguageParser.ts @@ -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) { diff --git a/src/templates/defaultBasesFiles.ts b/src/templates/defaultBasesFiles.ts index d88a638b..43edcc1e 100644 --- a/src/templates/defaultBasesFiles.ts +++ b/src/templates/defaultBasesFiles.ts @@ -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" diff --git a/src/ui/TaskCard.ts b/src/ui/TaskCard.ts index 5226c32c..b931190c 100644 --- a/src/ui/TaskCard.ts +++ b/src/ui/TaskCard.ts @@ -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"; diff --git a/styles/calendar-view.css b/styles/calendar-view.css index e9086229..e9c15c81 100644 --- a/styles/calendar-view.css +++ b/styles/calendar-view.css @@ -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; } diff --git a/styles/task-card-bem.css b/styles/task-card-bem.css index 96eca4fe..edf05b9a 100644 --- a/styles/task-card-bem.css +++ b/styles/task-card-bem.css @@ -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); diff --git a/tests/unit/services/NaturalLanguageParser.test.ts b/tests/unit/services/NaturalLanguageParser.test.ts index c0ec7e6d..4b5103e9 100644 --- a/tests/unit/services/NaturalLanguageParser.test.ts +++ b/tests/unit/services/NaturalLanguageParser.test.ts @@ -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');