Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions commands/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,15 @@ async function showProject(api, projectName, options) {
archived: 'false'
});

const projectTasks = result.tasks.filter(task =>
task.projects && task.projects.includes(projectName)
);
// Filter tasks - handle both plain names and [[wikilink]] format
const projectTasks = result.tasks.filter(task => {
if (!task.projects || task.projects.length === 0) return false;
return task.projects.some(proj => {
if (!proj || typeof proj !== 'string') return false;
const cleanProj = proj.replace(/^\[\[|\]\]$/g, '');
return cleanProj === projectName || proj === projectName || cleanProj.includes(projectName) || projectName.includes(cleanProj);
});
});

spinner.succeed(`Found ${projectTasks.length} tasks for project`);

Expand Down Expand Up @@ -220,9 +226,15 @@ async function showProjectStats(api, projectName, options) {
project: projectName
});

const projectTasks = result.tasks.filter(task =>
task.projects && task.projects.includes(projectName)
);
// Filter tasks - handle both plain names and [[wikilink]] format
const projectTasks = result.tasks.filter(task => {
if (!task.projects || task.projects.length === 0) return false;
return task.projects.some(proj => {
if (!proj || typeof proj !== 'string') return false;
const cleanProj = proj.replace(/^\[\[|\]\]$/g, '');
return cleanProj === projectName || proj === projectName || cleanProj.includes(projectName) || projectName.includes(cleanProj);
});
});

spinner.succeed(`Stats calculated for ${projectTasks.length} tasks`);

Expand Down
187 changes: 169 additions & 18 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,160 @@ class TaskNotesAPI {
}

async listTasks(filters = {}) {
const params = new URLSearchParams();
// Use GET /api/tasks (no params) and filter client-side
// POST /api/tasks/query has a bug in the current plugin version
const result = await this.getAllTasks();
let tasks = result.tasks || [];
const total = tasks.length;

Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, value);
}
});

const queryString = params.toString();
const endpoint = `/api/tasks${queryString ? `?${queryString}` : ''}`;
// Apply client-side filters
if (filters.completed === 'true') {
tasks = tasks.filter(t => t.status === 'done');
} else if (filters.completed === 'false') {
tasks = tasks.filter(t => t.status !== 'done');
}

return this.request(endpoint);
if (filters.scheduled_after) {
const afterDate = filters.scheduled_after.split('T')[0];
tasks = tasks.filter(t => t.scheduled && t.scheduled >= afterDate);
}
if (filters.scheduled_before) {
const beforeDate = filters.scheduled_before.split('T')[0];
tasks = tasks.filter(t => t.scheduled && t.scheduled <= beforeDate);
}
if (filters.due_before) {
const beforeDate = filters.due_before.split('T')[0];
tasks = tasks.filter(t => t.due && t.due < beforeDate);
}

// Apply limit
const limit = filters.limit || 200;
if (tasks.length > limit) {
tasks = tasks.slice(0, limit);
}

return {
tasks,
total,
filtered: tasks.length,
vault: result.vault
};
}

async getAllTasks() {
// GET /api/tasks without parameters returns all tasks
return this.request('/api/tasks');
}

async queryTasks(filterQuery) {
return this.request('/api/tasks/query', {
method: 'POST',
body: JSON.stringify(filterQuery)
});
// Use getAllTasks and filter client-side since POST /api/tasks/query is buggy
const result = await this.getAllTasks();
let tasks = result.tasks || [];
const total = tasks.length;

// Handle FilterParser AST format (type: 'group', children: [...])
if (filterQuery.type === 'group' && filterQuery.children) {
tasks = tasks.filter(task => this.evaluateGroup(task, filterQuery));
}
// Handle simple conditions array format
else if (filterQuery.conditions && filterQuery.conditions.length > 0) {
tasks = tasks.filter(task => {
return filterQuery.conditions.every(condition =>
this.evaluateCondition(task, condition)
);
});
}

// Apply limit
const limit = filterQuery.limit || 200;
if (tasks.length > limit) {
tasks = tasks.slice(0, limit);
}

return {
tasks,
total,
filtered: tasks.length,
vault: result.vault
};
}

evaluateGroup(task, group) {
const { conjunction, children } = group;

if (!children || children.length === 0) {
return true;
}

if (conjunction === 'or') {
return children.some(child => this.evaluateNode(task, child));
} else {
// Default to AND
return children.every(child => this.evaluateNode(task, child));
}
}

evaluateNode(task, node) {
if (node.type === 'group') {
return this.evaluateGroup(task, node);
} else if (node.type === 'condition') {
return this.evaluateCondition(task, node);
}
return true;
}

evaluateCondition(task, condition) {
const { property, operator, value } = condition;
const taskValue = task[property];

switch (operator) {
case 'is':
if (Array.isArray(taskValue)) {
return taskValue.some(v => v.toLowerCase() === value.toLowerCase());
}
return taskValue && taskValue.toLowerCase() === value.toLowerCase();
case 'is-not':
if (Array.isArray(taskValue)) {
return !taskValue.some(v => v.toLowerCase() === value.toLowerCase());
}
return !taskValue || taskValue.toLowerCase() !== value.toLowerCase();
case 'contains':
if (Array.isArray(taskValue)) {
return taskValue.some(v => v.toLowerCase().includes(value.toLowerCase()));
}
return taskValue && taskValue.toLowerCase().includes(value.toLowerCase());
case 'does-not-contain':
if (Array.isArray(taskValue)) {
return !taskValue.some(v => v.toLowerCase().includes(value.toLowerCase()));
}
return !taskValue || !taskValue.toLowerCase().includes(value.toLowerCase());
case 'is-before':
case 'before':
return taskValue && taskValue < value;
case 'is-after':
case 'after':
return taskValue && taskValue > value;
case 'is-on-or-before':
case 'on-or-before':
return taskValue && taskValue <= value;
case 'is-on-or-after':
case 'on-or-after':
return taskValue && taskValue >= value;
case 'is-empty':
case 'empty':
return !taskValue || (Array.isArray(taskValue) && taskValue.length === 0);
case 'is-not-empty':
case 'not-empty':
return taskValue && (!Array.isArray(taskValue) || taskValue.length > 0);
case 'is-greater-than':
case 'greater-than':
return taskValue && parseFloat(taskValue) > parseFloat(value);
case 'is-less-than':
case 'less-than':
return taskValue && parseFloat(taskValue) < parseFloat(value);
default:
return true;
}
}

async getTask(taskId) {
Expand Down Expand Up @@ -139,10 +274,26 @@ class TaskNotesAPI {
}

async searchTasks(query) {
return this.listTasks({
limit: 50,
// Add search functionality when API supports it
});
// Search using title contains - client-side filtering
const result = await this.getAllTasks();
let tasks = result.tasks || [];
const queryLower = query.toLowerCase();

tasks = tasks.filter(t =>
t.title && t.title.toLowerCase().includes(queryLower)
);

// Limit to 50 results
if (tasks.length > 50) {
tasks = tasks.slice(0, 50);
}

return {
tasks,
total: result.tasks.length,
filtered: tasks.length,
vault: result.vault
};
}

async getFilterOptions() {
Expand Down