mirror of
https://github.com/heroblackink/ultimate-todoist-sync-for-obsidian.git
synced 2026-07-22 07:40:27 +00:00
fix(sync): pull Todoist labels/priority to vault and align link generation
This commit is contained in:
parent
fa8a69b40e
commit
eacd2c9e3c
4 changed files with 130 additions and 13 deletions
|
|
@ -272,8 +272,8 @@ export class UltimateTodoistSyncSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Use Desktop URIs')
|
||||
.setDesc('Open Todoist tasks in desktop app (todoist://) instead of browser (https://).')
|
||||
.setName('Use App URI Scheme')
|
||||
.setDesc('When enabled, generated task links use todoist:// (desktop app). When disabled, links use https://app.todoist.com/app/task/... (web).')
|
||||
.addToggle(component =>
|
||||
component
|
||||
.setValue(this.plugin.settings.useAppURI)
|
||||
|
|
|
|||
|
|
@ -135,6 +135,16 @@ export class TodoistToObsidianSync {
|
|||
await this.plugin.fileOperation.syncTaskDueDateToFile(taskId, task.due?.date || "");
|
||||
this.plugin.logOperation?.log('FILE_TASK_DUEDATE_SYNCED', `Synced due date: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian');
|
||||
}
|
||||
|
||||
const prioritySynced = await this.plugin.fileOperation.syncTaskPriorityToFile(taskId, task.priority || 1);
|
||||
if (prioritySynced) {
|
||||
this.plugin.logOperation?.log('FILE_TASK_PRIORITY_SYNCED', `Synced priority: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian');
|
||||
}
|
||||
|
||||
const labelsSynced = await this.plugin.fileOperation.syncTaskLabelsToFile(taskId, task.labels || []);
|
||||
if (labelsSynced) {
|
||||
this.plugin.logOperation?.log('FILE_TASK_LABELS_SYNCED', `Synced labels: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian');
|
||||
}
|
||||
}
|
||||
|
||||
private async syncNotesToObsidian(
|
||||
|
|
|
|||
|
|
@ -1117,14 +1117,12 @@ export class TaskManagerModal extends Modal {
|
|||
// Invalidate file cache after content/date writes so tag/priority sync reads fresh data
|
||||
this._fileCache.delete(filePath);
|
||||
// Sync tags (labels) from Todoist to file
|
||||
const todoistLabels = task.labels || [];
|
||||
const todoistLabels = taskParser.normalizeLabelsForCompare([...(task.labels || []), 'todoist']);
|
||||
let currentLine = await this.getTaskLine(taskId, filePath);
|
||||
if (currentLine && todoistLabels.length > 0) {
|
||||
const existingTags = taskParser.getAllTagsFromLineText(currentLine);
|
||||
// Remove existing tags (except #todoist and project tags)
|
||||
let updatedLine = currentLine;
|
||||
for (const tag of existingTags) {
|
||||
if (tag === 'todoist') continue;
|
||||
updatedLine = updatedLine.replace(new RegExp(`#${tag}\\b`, 'g'), '');
|
||||
}
|
||||
// Add Todoist labels as tags before #todoist
|
||||
|
|
|
|||
|
|
@ -261,8 +261,10 @@ export class FileOperation {
|
|||
console.error(`Task ${taskID} not found in taskFileMapping`);
|
||||
continue;
|
||||
}
|
||||
const todoistTask = await this.plugin.todoistSyncAPI.GetTaskById(taskID)
|
||||
const todoistLink = todoistTask?.url || ''
|
||||
await this.plugin.todoistSyncAPI.GetTaskById(taskID)
|
||||
const todoistLink = this.plugin.settings.useAppURI
|
||||
? `todoist://task?id=${taskID}`
|
||||
: `https://app.todoist.com/app/task/${taskID}`
|
||||
const link = `[link](${todoistLink})`
|
||||
const newLine = this.plugin.taskParser.addTodoistLink(line,link)
|
||||
this.plugin.debugLog(newLine)
|
||||
|
|
@ -501,6 +503,109 @@ export class FileOperation {
|
|||
return modified;
|
||||
}
|
||||
|
||||
private normalizeLabelsForSync(labels: string[] | undefined): string[] {
|
||||
if (!labels || labels.length === 0) return [];
|
||||
const normalized = labels
|
||||
.map(label => (label || '').trim().replace(/^#/, ''))
|
||||
.filter(label => label.length > 0);
|
||||
return Array.from(new Set(normalized));
|
||||
}
|
||||
|
||||
async syncTaskPriorityToFile(taskId: string, newPriority: number): Promise<boolean> {
|
||||
const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId);
|
||||
if (!taskMapping) return false;
|
||||
const filepath = taskMapping.filePath;
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(filepath);
|
||||
const fileContent = await this.app.vault.read(file);
|
||||
const lines = fileContent.split('\n');
|
||||
let modified = false;
|
||||
|
||||
const numericPriority = Number(newPriority);
|
||||
const targetPriority = Number.isFinite(numericPriority) && numericPriority >= 1 && numericPriority <= 4
|
||||
? Math.floor(numericPriority)
|
||||
: 1;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.includes(taskId) || !this.plugin.taskParser.hasTodoistTag(line)) continue;
|
||||
|
||||
const currentPriority = this.plugin.taskParser.getTaskPriority(line);
|
||||
if (currentPriority === targetPriority) break;
|
||||
|
||||
const metadataIndex = line.indexOf('%%[todoist_id::');
|
||||
const prefix = (metadataIndex >= 0 ? line.slice(0, metadataIndex) : line)
|
||||
.replace(/\s!!([1-4])(?=\s|$)/g, '')
|
||||
.replace(/ {2,}/g, ' ')
|
||||
.trimEnd();
|
||||
const suffix = metadataIndex >= 0 ? line.slice(metadataIndex).trimStart() : '';
|
||||
|
||||
const nextPrefix = targetPriority > 1 ? `${prefix} !!${targetPriority}` : prefix;
|
||||
lines[i] = suffix ? `${nextPrefix} ${suffix}` : nextPrefix;
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
const newFileContent = lines.join('\n');
|
||||
await this.plugin.backupOperation?.backupFile(filepath);
|
||||
await this.app.vault.modify(file, newFileContent);
|
||||
this.plugin.logOperation?.log('FILE_TASK_PRIORITY_SYNCED', `Synced priority from Todoist: ${taskId}`, filepath, taskId, this.plugin.isSyncingFromTodoist ? 'todoist→obsidian' : 'obsidian→todoist');
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
async syncTaskLabelsToFile(taskId: string, newLabels: string[]): Promise<boolean> {
|
||||
const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId);
|
||||
if (!taskMapping) return false;
|
||||
const filepath = taskMapping.filePath;
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(filepath);
|
||||
const fileContent = await this.app.vault.read(file);
|
||||
const lines = fileContent.split('\n');
|
||||
let modified = false;
|
||||
|
||||
const todoistLabels = this.normalizeLabelsForSync(newLabels);
|
||||
const desiredLabels = this.plugin.taskParser.normalizeLabelsForCompare([...todoistLabels, 'todoist']);
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.includes(taskId) || !this.plugin.taskParser.hasTodoistTag(line)) continue;
|
||||
|
||||
const currentLabels = this.plugin.taskParser.normalizeLabelsForCompare(
|
||||
this.plugin.taskParser.getAllTagsFromLineText(line)
|
||||
);
|
||||
const isSame = currentLabels.length === desiredLabels.length
|
||||
&& currentLabels.every((label, idx) => label === desiredLabels[idx]);
|
||||
if (isSame) break;
|
||||
|
||||
const metadataIndex = line.indexOf('%%[todoist_id::');
|
||||
const prefix = metadataIndex >= 0 ? line.slice(0, metadataIndex) : line;
|
||||
const suffix = metadataIndex >= 0 ? line.slice(metadataIndex).trimStart() : '';
|
||||
|
||||
const prefixWithoutTags = prefix
|
||||
.replace(/#[\w\u4e00-\u9fa5-]+/g, '')
|
||||
.replace(/ {2,}/g, ' ')
|
||||
.trimEnd();
|
||||
const tagText = desiredLabels.map(label => `#${label}`).join(' ');
|
||||
const nextPrefix = tagText ? `${prefixWithoutTags} ${tagText}` : prefixWithoutTags;
|
||||
|
||||
lines[i] = suffix ? `${nextPrefix} ${suffix}` : nextPrefix;
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
const newFileContent = lines.join('\n');
|
||||
await this.plugin.backupOperation?.backupFile(filepath);
|
||||
await this.app.vault.modify(file, newFileContent);
|
||||
this.plugin.logOperation?.log('FILE_TASK_LABELS_SYNCED', `Synced labels from Todoist: ${taskId}`, filepath, taskId, this.plugin.isSyncingFromTodoist ? 'todoist→obsidian' : 'obsidian→todoist');
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
async syncTaskNoteToFile(taskId: string, noteContent: string, noteDate: string): Promise<boolean> {
|
||||
const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId);
|
||||
if (!taskMapping) return false;
|
||||
|
|
@ -663,10 +768,15 @@ export class FileOperation {
|
|||
hasChanges = true;
|
||||
}
|
||||
|
||||
// 3. Replace Web URL: https://todoist.com/app/task/oldId -> https://todoist.com/app/task/newId
|
||||
const oldWebUrlPattern = new RegExp(`https://todoist\\.com/app/task/${oldId}`, 'g');
|
||||
if (oldWebUrlPattern.test(line)) {
|
||||
line = line.replace(oldWebUrlPattern, `https://todoist.com/app/task/${newId}`);
|
||||
const oldWebUrlPatternLegacy = new RegExp(`https://todoist\\.com/app/task/${oldId}`, 'g');
|
||||
if (oldWebUrlPatternLegacy.test(line)) {
|
||||
line = line.replace(oldWebUrlPatternLegacy, `https://todoist.com/app/task/${newId}`);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const oldWebUrlPatternNew = new RegExp(`https://app\\.todoist\\.com/app/task/${oldId}`, 'g');
|
||||
if (oldWebUrlPatternNew.test(line)) {
|
||||
line = line.replace(oldWebUrlPatternNew, `https://app.todoist.com/app/task/${newId}`);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
|
|
@ -811,8 +921,7 @@ export class FileOperation {
|
|||
* @returns 标签数组(不带 # 前缀)
|
||||
*/
|
||||
private extractLabelsFromLine(line: string): string[] {
|
||||
return this.plugin.taskParser.getAllTagsFromLineText(line)
|
||||
.filter(l => l !== 'todoist');
|
||||
return this.plugin.taskParser.getAllTagsFromLineText(line);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue