From 67ef8b3f5ba07b3acfebfb0dfbe0268e33f1206d Mon Sep 17 00:00:00 2001 From: jcconnects Date: Sun, 19 Jul 2026 15:44:27 -0300 Subject: [PATCH] feat: identify tasks by property key regardless of value When identifying tasks by a frontmatter property, allow leaving the task property value empty to mean "match any value": a note is treated as a task whenever the configured property key is present with a non-empty value, regardless of what that value is (e.g. identify tasks by `status` alone). Previously property-based identification required both a property name and an exact value, so a field like `status` whose value changes over time could not be used as the sole task marker. An empty value now maps to an existence check, which also aligns the detection path with the generated Bases filters that already treat an empty value as "exists / not empty". A present-but-empty value (empty string, whitespace, null, or empty array) does not count as a task, matching the Bases filter semantics. Providing a value keeps the existing exact-match behavior unchanged. Update the task property value setting description across all locales and add unit tests covering the match-any, exact-match, and empty/absent cases. --- i18n.manifest.json | 2 +- i18n.state.json | 32 +++++----- src/i18n/resources/de.ts | 2 +- src/i18n/resources/en.ts | 3 +- src/i18n/resources/es.ts | 2 +- src/i18n/resources/fr.ts | 2 +- src/i18n/resources/ja.ts | 2 +- src/i18n/resources/ko.ts | 2 +- src/i18n/resources/pt.ts | 2 +- src/i18n/resources/ru.ts | 2 +- src/i18n/resources/zh.ts | 2 +- src/utils/taskIdentification.ts | 27 ++++++++- .../unit/utils/TaskManager.isTaskFile.test.ts | 60 +++++++++++++++++++ 13 files changed, 113 insertions(+), 27 deletions(-) diff --git a/i18n.manifest.json b/i18n.manifest.json index a30b9bbec..fed873cee 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -586,7 +586,7 @@ "settings.general.taskIdentification.taskProperty.name": "fb00b60c99acd4529404e3ef8bd77017d9332da6", "settings.general.taskIdentification.taskProperty.description": "5ba9a14eff27529d0bbb5eb8c793ca728759623b", "settings.general.taskIdentification.taskPropertyValue.name": "258b4a08e80f7b9bec1e0ac3c2c5c3008676297d", - "settings.general.taskIdentification.taskPropertyValue.description": "062539e47430864ee4bf36d0b134ad3d7968f7a8", + "settings.general.taskIdentification.taskPropertyValue.description": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", "settings.general.folderManagement.header": "679a99627ec2eac7d867141c7d18b2d71804736c", "settings.general.folderManagement.excludedFolders.name": "956d3039f8ba3fd08869f0fffc6038f0a106fe9c", "settings.general.folderManagement.excludedFolders.description": "bef8a14860dc486cf46aad464c4c57652fc676d3", diff --git a/i18n.state.json b/i18n.state.json index 01d90a504..a71b57ee1 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -2349,8 +2349,8 @@ "translation": "fe54cfd0b371c0db30b573a782f874ee2ec83e29" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "fc00d383ed55b0705e1037b37fc96e92289ee44c" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "5057a36f03e94fde00663a85df93eb5ddecc8902" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -11215,8 +11215,8 @@ "translation": "9f5e7fff0b1991a28c067bb16b69a0773050a887" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "90db7e8f935b4220989250c0eaaf44845cbe8cd2" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "587bf866d614114162dae920446297defaa3d672" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -20081,8 +20081,8 @@ "translation": "a63ba99c5cff48c5527661c7bd7e759fef4abc0f" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "9c493294d3f02303ed2668bbfb126797915a3f9a" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "cb626729d872a1884710acaad8793e7b3660fd7d" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -28947,8 +28947,8 @@ "translation": "961d481dd7fd53b281a8da5c4bdb69744c71ef1e" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "991fd53856fb12c49f5ce1e1ecfcac319cb143a4" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "27104de141028d65c140a1e71658058938755090" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -37813,8 +37813,8 @@ "translation": "d9ab65c516df3680d7cb4cae79fce0b40377aaf6" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "ebd248aa2aaf679effff2cc85508ffc3e1a3f151" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "fe2fd597474dbc3938a9fa1f850a7844d26265c8" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -46679,8 +46679,8 @@ "translation": "9c8ae4810200c854572f20371f939318f3fe422c" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "17a81e641866202e2de4097f59de2b8d21ef7fd6" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "381eeb846e39311762463001511b5351587fc0e5" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -55545,8 +55545,8 @@ "translation": "a3fd42826f8635e08120459dc7ae119f273e42f3" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "d4961905d4456b7e71409da645f2b39d24fb2424" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "20b42216a409309bf498e5aacb3a580f914849d8" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", @@ -64411,8 +64411,8 @@ "translation": "64f4d0f6328db55b4c5e9eccb304f972bb2de5a8" }, "settings.general.taskIdentification.taskPropertyValue.description": { - "source": "062539e47430864ee4bf36d0b134ad3d7968f7a8", - "translation": "793d654a8377ba074d1297aa6517d53d4db1631f" + "source": "7ca741bd29f5fda2fafb7500ce9d8c15f8b208cc", + "translation": "ab3af390f38d944a2c1ba5a0c7bee73bf7f429f2" }, "settings.general.folderManagement.header": { "source": "679a99627ec2eac7d867141c7d18b2d71804736c", diff --git a/src/i18n/resources/de.ts b/src/i18n/resources/de.ts index 46a27f84c..23c9db1a8 100644 --- a/src/i18n/resources/de.ts +++ b/src/i18n/resources/de.ts @@ -915,7 +915,7 @@ export const de: TranslationTree = { }, taskPropertyValue: { name: "Aufgabeneigenschaftswert", - description: "Der Wert, der eine Notiz als Aufgabe identifiziert (z.B. \"task\")" + description: "Der Wert, der eine Notiz als Aufgabe identifiziert (z.B. \"task\"). Leer lassen, um jeden Wert zu akzeptieren, sodass eine Notiz eine Aufgabe ist, sobald die Eigenschaft vorhanden ist." } }, folderManagement: { diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index 0926250b6..3c2601506 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -960,7 +960,8 @@ export const en: TranslationTree = { }, taskPropertyValue: { name: "Task property value", - description: 'The value that identifies a note as a task (e.g., "task")', + description: + 'The value that identifies a note as a task (e.g., "task"). Leave empty to match any value, so a note is a task whenever the property is present.', }, }, folderManagement: { diff --git a/src/i18n/resources/es.ts b/src/i18n/resources/es.ts index 07d2caeaf..ef7f8c8c4 100644 --- a/src/i18n/resources/es.ts +++ b/src/i18n/resources/es.ts @@ -915,7 +915,7 @@ export const es: TranslationTree = { }, taskPropertyValue: { name: "Valor de propiedad de tarea", - description: "El valor que identifica una nota como tarea (ej. \"tarea\")" + description: "El valor que identifica una nota como tarea (ej. \"tarea\"). Déjelo vacío para que coincida con cualquier valor, de modo que una nota sea una tarea siempre que la propiedad esté presente." } }, folderManagement: { diff --git a/src/i18n/resources/fr.ts b/src/i18n/resources/fr.ts index 9d16e2657..a8b7b3d51 100644 --- a/src/i18n/resources/fr.ts +++ b/src/i18n/resources/fr.ts @@ -915,7 +915,7 @@ export const fr: TranslationTree = { }, taskPropertyValue: { name: "Valeur de la propriété de tâche", - description: "La valeur qui identifie une note comme une tâche (ex. \"task\")" + description: "La valeur qui identifie une note comme une tâche (ex. \"task\"). Laissez vide pour correspondre à n'importe quelle valeur, de sorte qu'une note soit une tâche dès que la propriété est présente." } }, folderManagement: { diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 1e3f4df6e..02a7f12e0 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -915,7 +915,7 @@ export const ja: TranslationTree = { }, taskPropertyValue: { name: "タスクプロパティ値", - description: "ノートをタスクとして識別する値(例:\"task\")" + description: "ノートをタスクとして識別する値(例:\"task\")。空欄にすると任意の値に一致し、プロパティが存在すればノートはタスクになります。" } }, folderManagement: { diff --git a/src/i18n/resources/ko.ts b/src/i18n/resources/ko.ts index ee54da2c2..a9d2903f3 100644 --- a/src/i18n/resources/ko.ts +++ b/src/i18n/resources/ko.ts @@ -915,7 +915,7 @@ export const ko: TranslationTree = { }, taskPropertyValue: { name: "작업 속성 값", - description: "노트를 작업으로 식별하는 값 (예: \"task\")" + description: "노트를 작업으로 식별하는 값 (예: \"task\"). 비워 두면 모든 값과 일치하여 속성이 존재하기만 하면 노트가 작업이 됩니다." } }, folderManagement: { diff --git a/src/i18n/resources/pt.ts b/src/i18n/resources/pt.ts index 7d377b4c7..78b328c0f 100644 --- a/src/i18n/resources/pt.ts +++ b/src/i18n/resources/pt.ts @@ -917,7 +917,7 @@ export const pt: TranslationTree = { }, taskPropertyValue: { name: "Valor da propriedade da tarefa", - description: "O valor que identifica uma nota como uma tarefa (ex: \"tarefa\")" + description: "O valor que identifica uma nota como uma tarefa (ex: \"tarefa\"). Deixe em branco para corresponder a qualquer valor, tornando a nota uma tarefa sempre que a propriedade estiver presente." } }, folderManagement: { diff --git a/src/i18n/resources/ru.ts b/src/i18n/resources/ru.ts index d0f75346a..6e369e66b 100644 --- a/src/i18n/resources/ru.ts +++ b/src/i18n/resources/ru.ts @@ -915,7 +915,7 @@ export const ru: TranslationTree = { }, taskPropertyValue: { name: "Значение свойства задачи", - description: "Значение, которое идентифицирует заметку как задачу (например, \"задача\")" + description: "Значение, которое идентифицирует заметку как задачу (например, \"задача\"). Оставьте пустым, чтобы соответствовать любому значению — заметка становится задачей, как только это свойство присутствует." } }, folderManagement: { diff --git a/src/i18n/resources/zh.ts b/src/i18n/resources/zh.ts index 0665e8e0c..d471225e1 100644 --- a/src/i18n/resources/zh.ts +++ b/src/i18n/resources/zh.ts @@ -915,7 +915,7 @@ export const zh: TranslationTree = { }, taskPropertyValue: { name: "任务属性值", - description: "识别笔记为任务的值(例如,\"task\")" + description: "识别笔记为任务的值(例如,\"task\")。留空可匹配任意值,只要存在该属性,笔记即视为任务。" } }, folderManagement: { diff --git a/src/utils/taskIdentification.ts b/src/utils/taskIdentification.ts index ae09569e7..14911f9a5 100644 --- a/src/utils/taskIdentification.ts +++ b/src/utils/taskIdentification.ts @@ -11,6 +11,23 @@ function isFrontmatterRecord(frontmatter: unknown): frontmatter is Record 0; + } + if (Array.isArray(value)) { + return value.some((item) => hasNonEmptyValue(item)); + } + return true; +} + export function compareTaskPropertyIdentifierValue( frontmatterValue: unknown, settingValue: string @@ -37,9 +54,17 @@ export function isTaskFrontmatter( if (settings.taskIdentificationMethod === "property") { const propName = settings.taskPropertyName; const propValue = settings.taskPropertyValue; - if (!propName || !propValue) return false; + if (!propName) return false; const frontmatterValue = frontmatter[propName]; + + // Empty configured value means "match any value": the note is a task as + // long as the property key exists with a non-empty value, regardless of + // what that value is (e.g. identify by `status` alone). + if (propValue.trim().length === 0) { + return hasNonEmptyValue(frontmatterValue); + } + if (frontmatterValue === undefined) return false; if (Array.isArray(frontmatterValue)) { diff --git a/tests/unit/utils/TaskManager.isTaskFile.test.ts b/tests/unit/utils/TaskManager.isTaskFile.test.ts index ffe0f6aa6..205d5dcaf 100644 --- a/tests/unit/utils/TaskManager.isTaskFile.test.ts +++ b/tests/unit/utils/TaskManager.isTaskFile.test.ts @@ -227,4 +227,64 @@ describe('TaskManager.isTaskFile - tag hash prefix handling', () => { expect(isTaskFile({ flags: ['note', true] }, settings)).toBe(true); }); }); + + describe('Property-based identification with empty value (match any value)', () => { + const anyValueSettings: IsTaskFileSettings = { + taskIdentificationMethod: 'property', + taskPropertyName: 'status', + taskPropertyValue: '', + taskTag: 'task', + }; + + it('should identify a task when the property exists with any value', () => { + expect(isTaskFile({ status: 'open' }, anyValueSettings)).toBe(true); + expect(isTaskFile({ status: 'done' }, anyValueSettings)).toBe(true); + expect(isTaskFile({ status: 'anything-at-all' }, anyValueSettings)).toBe(true); + }); + + it('should treat whitespace-only configured value as match-any', () => { + const settings: IsTaskFileSettings = { ...anyValueSettings, taskPropertyValue: ' ' }; + expect(isTaskFile({ status: 'open' }, settings)).toBe(true); + }); + + it('should identify a task for non-string scalar values', () => { + expect(isTaskFile({ status: 3 }, anyValueSettings)).toBe(true); + expect(isTaskFile({ status: true }, anyValueSettings)).toBe(true); + expect(isTaskFile({ status: false }, anyValueSettings)).toBe(true); + }); + + it('should identify a task when the property is a non-empty array', () => { + expect(isTaskFile({ status: ['open'] }, anyValueSettings)).toBe(true); + expect(isTaskFile({ status: ['', 'open'] }, anyValueSettings)).toBe(true); + }); + + it('should NOT identify a task when the property is absent', () => { + expect(isTaskFile({ type: 'note' }, anyValueSettings)).toBe(false); + expect(isTaskFile({}, anyValueSettings)).toBe(false); + }); + + it('should NOT identify a task when the property is present but empty', () => { + expect(isTaskFile({ status: '' }, anyValueSettings)).toBe(false); + expect(isTaskFile({ status: ' ' }, anyValueSettings)).toBe(false); + expect(isTaskFile({ status: null }, anyValueSettings)).toBe(false); + expect(isTaskFile({ status: [] }, anyValueSettings)).toBe(false); + expect(isTaskFile({ status: ['', ' '] }, anyValueSettings)).toBe(false); + }); + + it('should still exact-match when a value is configured', () => { + const settings: IsTaskFileSettings = { ...anyValueSettings, taskPropertyValue: 'done' }; + expect(isTaskFile({ status: 'done' }, settings)).toBe(true); + expect(isTaskFile({ status: 'open' }, settings)).toBe(false); + }); + + it('should return false when no property name is configured, even with match-any value', () => { + const settings: IsTaskFileSettings = { + taskIdentificationMethod: 'property', + taskPropertyName: '', + taskPropertyValue: '', + taskTag: 'task', + }; + expect(isTaskFile({ status: 'open' }, settings)).toBe(false); + }); + }); });