feat(bases): add dynamic template generation with user-specific filters and properties

- Generate task filters based on user's task identification method (tag or property)
- Include property ordering from defaultVisibleProperties setting
- Use correct YAML object notation for Bases filters
- Support combined filters for project-subtasks view
- Maintain backward compatibility with legacy static templates
This commit is contained in:
callumalpass 2025-11-04 13:41:13 +11:00
parent 5630db157c
commit 262a9c5fe6
2 changed files with 272 additions and 7 deletions

View file

@ -24,7 +24,7 @@ import {
import { TaskNotesSettings } from "./types/settings";
import { DEFAULT_SETTINGS } from "./settings/defaults";
import { TaskNotesSettingTab } from "./settings/TaskNotesSettingTab";
import { DEFAULT_BASES_FILES } from "./templates/defaultBasesFiles";
import { DEFAULT_BASES_FILES, generateBasesFileTemplate } from "./templates/defaultBasesFiles";
import {
MINI_CALENDAR_VIEW_TYPE,
ADVANCED_CALENDAR_VIEW_TYPE,
@ -1884,14 +1884,21 @@ export default class TaskNotesPlugin extends Plugin {
continue;
}
const template = DEFAULT_BASES_FILES[commandId];
// Generate template with user settings
const template = generateBasesFileTemplate(commandId, this.settings);
if (!template) {
skipped.push(rawPath);
continue;
// Fall back to legacy template if generation fails
const legacyTemplate = DEFAULT_BASES_FILES[commandId];
if (!legacyTemplate) {
skipped.push(rawPath);
continue;
}
// eslint-disable-next-line no-await-in-loop
await this.app.vault.create(normalizedPath, legacyTemplate);
} else {
// eslint-disable-next-line no-await-in-loop
await this.app.vault.create(normalizedPath, template);
}
// eslint-disable-next-line no-await-in-loop
await this.app.vault.create(normalizedPath, template);
created.push(rawPath);
}
} catch (error) {

View file

@ -3,6 +3,264 @@
* These are created in TaskNotes/Views/ directory when the user first uses the commands
*/
import type { TaskNotesSettings } from "../types/settings";
/**
* Generate a task filter expression based on the task identification method
* Returns the filter condition string (not the full YAML structure)
*/
function generateTaskFilterCondition(settings: TaskNotesSettings): string {
if (settings.taskIdentificationMethod === "tag") {
// Filter by tag using hasTag method
const taskTag = settings.taskTag || "task";
return `file.hasTag("${taskTag}")`;
} else {
// Filter by property
const propertyName = settings.taskPropertyName;
const propertyValue = settings.taskPropertyValue;
if (!propertyName) {
// No property name specified, fall back to tag-based filtering
const taskTag = settings.taskTag || "task";
return `file.hasTag("${taskTag}")`;
}
if (propertyValue) {
// Check property has specific value
return `note.${propertyName} == "${propertyValue}"`;
} else {
// Just check property exists (is not empty)
return `note.${propertyName} && note.${propertyName} != "" && note.${propertyName} != null`;
}
}
}
/**
* Format filter condition(s) as YAML object notation
*/
function formatFilterAsYAML(conditions: string | string[]): string {
const conditionArray = Array.isArray(conditions) ? conditions : [conditions];
const formattedConditions = conditionArray.map(c => ` - ${c}`).join('\n');
return `filters:
and:
${formattedConditions}`;
}
/**
* Map internal TaskNotes property names to Bases property names
*/
function mapPropertyToBasesProperty(property: string, settings: TaskNotesSettings): string {
const fieldMapping = settings.fieldMapping;
switch (property) {
case "status":
return fieldMapping.status;
case "priority":
return fieldMapping.priority;
case "due":
return fieldMapping.due;
case "scheduled":
return fieldMapping.scheduled;
case "contexts":
return fieldMapping.contexts;
case "projects":
return fieldMapping.projects;
case "tags":
return "file.tags";
case "timeEstimate":
return fieldMapping.timeEstimate;
case "blocked":
case "blockedBy":
return fieldMapping.blockedBy;
case "blocking":
// Blocking is a computed property, use blockedBy as the source
return fieldMapping.blockedBy;
case "recurrence":
return fieldMapping.recurrence;
case "complete_instances":
case "completeInstances":
return fieldMapping.completeInstances;
case "completedDate":
return fieldMapping.completedDate;
case "dateCreated":
return "file.ctime";
case "dateModified":
return "file.mtime";
case "title":
return "file.name";
default:
return property;
}
}
/**
* Generate the order array from defaultVisibleProperties
*/
function generateOrderArray(settings: TaskNotesSettings): string[] {
const visibleProperties = settings.defaultVisibleProperties || [
"status",
"priority",
"due",
"scheduled",
"projects",
"contexts",
"tags",
];
// Map to Bases property names
const basesProperties = visibleProperties.map(prop =>
mapPropertyToBasesProperty(prop, settings)
);
// Add essential properties that should always be in the order
const essentialProperties = [
"file.name", // title
mapPropertyToBasesProperty("recurrence", settings),
mapPropertyToBasesProperty("complete_instances", settings),
];
// Combine, removing duplicates while preserving order
const allProperties: string[] = [];
const seen = new Set<string>();
// Add visible properties first
for (const prop of basesProperties) {
if (!seen.has(prop)) {
allProperties.push(prop);
seen.add(prop);
}
}
// Add essential properties
for (const prop of essentialProperties) {
if (!seen.has(prop)) {
allProperties.push(prop);
seen.add(prop);
}
}
return allProperties;
}
/**
* Format the order array as YAML
*/
function formatOrderArray(orderArray: string[]): string {
return orderArray.map(prop => ` - ${prop}`).join('\n');
}
/**
* Generate a Bases file template for a specific command with user settings
*/
export function generateBasesFileTemplate(commandId: string, settings: TaskNotesSettings): string {
const taskFilterCondition = generateTaskFilterCondition(settings);
const orderArray = generateOrderArray(settings);
const orderYaml = formatOrderArray(orderArray);
switch (commandId) {
case 'open-kanban-view':
return `# Kanban Board
# Automatically filters for tasks based on your settings
${formatFilterAsYAML(taskFilterCondition)}
views:
- type: tasknotesKanban
name: "Kanban Board"
order:
${orderYaml}
options:
columnWidth: 280
hideEmptyColumns: false
`;
case 'open-tasks-view':
return `# All Tasks
# Automatically filters for tasks based on your settings
${formatFilterAsYAML(taskFilterCondition)}
views:
- type: tasknotesTaskList
name: "All Tasks"
order:
${orderYaml}
sort:
- column: due
direction: ASC
`;
case 'open-advanced-calendar-view':
return `# Calendar
# Automatically filters for tasks based on your settings
${formatFilterAsYAML(taskFilterCondition)}
views:
- type: tasknotesCalendar
name: "Calendar"
order:
${orderYaml}
options:
showScheduled: true
showDue: true
showRecurring: true
showTimeEntries: true
showTimeblocks: true
showPropertyBasedEvents: true
calendarView: "timeGridWeek"
customDayCount: 3
firstDay: 0
slotMinTime: "06:00:00"
slotMaxTime: "22:00:00"
slotDuration: "00:30:00"
`;
case 'open-agenda-view':
return `# Agenda
# Automatically filters for tasks based on your settings
${formatFilterAsYAML(taskFilterCondition)}
views:
- type: tasknotesCalendar
name: "Agenda"
order:
${orderYaml}
calendarView: "listWeek"
startDateProperty: file.ctime
listDayCount: 7
titleProperty: file.basename
`;
case 'project-subtasks':
// This view needs a special filter that combines task filter AND project filter
const projectFilter = `note.${settings.fieldMapping.projects}.contains(this.file.asLink())`;
return `# Project Subtasks
# This view shows all tasks that reference the current file in their projects field
# Uses the 'this' keyword to reference the current file dynamically
${formatFilterAsYAML([taskFilterCondition, projectFilter])}
views:
- type: tasknotesTaskList
name: "Subtasks"
order:
${orderYaml}
sort:
- column: priority
direction: DESC
`;
default:
return '';
}
}
/**
* Legacy static templates for backward compatibility
* These are used as fallbacks when settings are not available
*/
export const DEFAULT_BASES_FILES: Record<string, string> = {
'open-kanban-view': `views:
- type: tasknotesKanban