mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
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:
parent
1b921bed6e
commit
4ef3fc482c
10 changed files with 187 additions and 50 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue