Skip to content

Commit 8617ade

Browse files
committed
b52 Change "Calendar note terms to include" now also matches task content, not only heading prefixes.
1 parent 8d35367 commit 8617ade

8 files changed

Lines changed: 208 additions & 24 deletions

File tree

‎jgclark.Dashboard/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ For more details see the [plugin's documentation](https://github.com/NotePlan/pl
99
- TODO: fix isNoteFromAllowedFolder() for teamspace or possibly 2025-W21.md
1010
-->
1111
## [2.4.0.b52] 2026-07-???
12+
- Change: **Calendar note terms to include** (was "Calendar note Sections to Include") now also matches task content, not only heading prefixes. Set a Perspective to `acme` or `#acme` to focus calendar and Overdue on that context without a new setting. When this is set and Hide Duplicates is on, matching items stay in calendar / Overdue sections rather than only in a TAG section. (Addresses #727 for @dwertheimer)
1213
- New: each Perspective can override which Apple Reminders lists to look in via **Reminder Lists to Include** (comma-separated). Blank keeps using NotePlan-enabled lists; named lists can include ones disabled in NotePlan.
1314
- Experimental Change: noteLink lozenges are right-aligned in task item rows (PROJ* sections unchanged).
1415
- Change: update set of available icons. dev: Use FA 7.0.1 instead of FA 6.0.0.b3.

‎jgclark.Dashboard/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ The settings in "What to Include and Exclude" control what Spaces, folders, Remi
239239

240240
- **Folders to Include**: Comma-separated list of folder(s) to include when searching for open or closed tasks/checklists. The matches ared partial, so 'Home' will include 'Home' and 'The Home Areas' etc. If left blank, all folders are included. Note: Calendar notes are always included, where relevant.
241241
- **Folders to Exclude**: Comma-separated list of folder(s) to ignore when searching for open or closed tasks/checklists. The matches are partial, so 'Work' will exclude 'Work' and 'Work/CompanyA' etc. Where there is a conflict, exclusions will take precedence over inclusions. To ignore notes at the top-level (not in a folder), include '/' in the list. (@Trash is always ignored, but other special folders need to be specified, e.g. @Archive, @Templates.)
242+
- **Calendar note terms to include**: Comma-separated list of terms for calendar notes only. A task is included if any heading above it starts with one of these values (case-insensitive prefix; e.g. `Wins` matches `Wins for 2026 Q2`), or if the task text itself contains the term (e.g. `#acme`, `@acme`, or `acme`). If left blank, all calendar items are included. Short terms can over-match. When this is set and Hide Duplicates is on, matching items stay in calendar / Overdue sections rather than only in a TAG section. Useful for Perspective focus (e.g. an ACME perspective with `acme` or `#acme`).
242243
- **Reminder Lists to Include**: Comma-separated list of Apple Reminders list names for this Perspective. Names must match exactly (after trimming). If left blank, Dashboard uses the lists enabled in NotePlan's Preferences → Calendars → Reminders. You can name lists that are disabled in NotePlan if this Perspective should include them.
243244
- **Ignore items in notes with these term(s)**: If set, open tasks/checklists with this word or tag will be ignored, and not counted as open or closed. (This check is not case sensitive.) This is useful for situations where completing the item is outside your control.
244245
- Apply to sections under headings in Calendar notes? If turned on, then all content in Calendar notes under headings that contains any of those phrases will be ignored. This applies to the preceding headings all the way up the H5->H1 hierarchy of section headings for that line. For example in the following note:

‎jgclark.Dashboard/src/__tests__/dashboardHelpers.test.js‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,75 @@ describe(`${PLUGIN_NAME}`, () => {
791791
expect(result[0]).toBe(para1)
792792
expect(result[1]).toBe(para3)
793793
})
794+
795+
test('should keep calendar paragraphs when content contains the term even if heading does not match', () => {
796+
const calendarNote = new Note({ filename: '2025-01-25.md', type: 'Calendar' })
797+
const para1 = new Paragraph({ type: 'open', content: 'Call client #acme', note: calendarNote })
798+
const para2 = new Paragraph({ type: 'open', content: 'Personal errand', note: calendarNote })
799+
const paras = [para1, para2]
800+
const dashboardSettings = { includedCalendarSections: 'acme' }
801+
const startTime = new Date()
802+
const functionName = 'testFunction'
803+
804+
jest.spyOn(headings, 'getHeadingHierarchyForThisPara').mockReturnValue(['Inbox'])
805+
806+
const result = filterParasByIncludedCalendarSections(paras, dashboardSettings, startTime, functionName)
807+
expect(result).toHaveLength(1)
808+
expect(result).toContain(para1)
809+
expect(result).not.toContain(para2)
810+
})
811+
812+
test('should match content terms case-insensitively including #tag form', () => {
813+
const calendarNote = new Note({ filename: '2025-01-25.md', type: 'Calendar' })
814+
const para1 = new Paragraph({ type: 'open', content: 'Ship #ACME release', note: calendarNote })
815+
const paras = [para1]
816+
const dashboardSettings = { includedCalendarSections: '#acme' }
817+
const startTime = new Date()
818+
const functionName = 'testFunction'
819+
820+
jest.spyOn(headings, 'getHeadingHierarchyForThisPara').mockReturnValue(['Other'])
821+
822+
const result = filterParasByIncludedCalendarSections(paras, dashboardSettings, startTime, functionName)
823+
expect(result).toHaveLength(1)
824+
expect(result).toContain(para1)
825+
})
826+
827+
test('should keep paragraph when heading matches even if content does not contain the term', () => {
828+
const calendarNote = new Note({ filename: '2025-01-25.md', type: 'Calendar' })
829+
const para1 = new Paragraph({ type: 'open', content: 'Untagged task under Acme heading', note: calendarNote })
830+
const paras = [para1]
831+
const dashboardSettings = { includedCalendarSections: 'Acme' }
832+
const startTime = new Date()
833+
const functionName = 'testFunction'
834+
835+
jest.spyOn(headings, 'getHeadingHierarchyForThisPara').mockReturnValue(['Acme Tasks'])
836+
837+
const result = filterParasByIncludedCalendarSections(paras, dashboardSettings, startTime, functionName)
838+
expect(result).toHaveLength(1)
839+
expect(result).toContain(para1)
840+
})
841+
842+
test('should keep paragraph when either heading or content matches (OR)', () => {
843+
const calendarNote = new Note({ filename: '2025-01-25.md', type: 'Calendar' })
844+
const paraHeading = new Paragraph({ type: 'open', content: 'No tag here', note: calendarNote })
845+
const paraContent = new Paragraph({ type: 'open', content: 'Tagged #acme elsewhere', note: calendarNote })
846+
const paraNeither = new Paragraph({ type: 'open', content: 'Home chore', note: calendarNote })
847+
const paras = [paraHeading, paraContent, paraNeither]
848+
const dashboardSettings = { includedCalendarSections: 'acme' }
849+
const startTime = new Date()
850+
const functionName = 'testFunction'
851+
852+
jest.spyOn(headings, 'getHeadingHierarchyForThisPara').mockImplementation((p) => {
853+
if (p === paraHeading) return ['acme']
854+
return ['Inbox']
855+
})
856+
857+
const result = filterParasByIncludedCalendarSections(paras, dashboardSettings, startTime, functionName)
858+
expect(result).toHaveLength(2)
859+
expect(result).toContain(paraHeading)
860+
expect(result).toContain(paraContent)
861+
expect(result).not.toContain(paraNeither)
862+
})
794863
})
795864

796865
describe('filterParasByExcludedCalendarSections()', () => {

‎jgclark.Dashboard/src/dashboardHelpers.js‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
//-----------------------------------------------------------------------------
33
// Dashboard plugin helper functions
4-
// Last updated 2026-07-16 for v2.4.0.b51 by @CursorAI
4+
// Last updated 2026-07-20 for v2.4.0.b52 by @CursorAI
55
//-----------------------------------------------------------------------------
66

77
// import pluginJson from '../plugin.json'
@@ -1126,11 +1126,15 @@ export function filterParasByIgnoreTerms(
11261126
}
11271127

11281128
/**
1129-
* Filter paragraphs to only include those from included calendar sections.
1130-
* Heading matching is case-insensitive prefix: each configured section name must match the start of a heading in the paragraph's hierarchy (see caseInsensitiveStartsWith with strictSubset false).
1129+
* Filter paragraphs to only include those matching included calendar note terms.
1130+
* Applies only to calendar notes (project notes always pass through).
1131+
* A calendar task is kept if either:
1132+
* - any heading in its hierarchy matches a term as a case-insensitive prefix, or
1133+
* - its content contains a term as a case-insensitive substring (so `#acme`, `@acme`, or `acme` all work).
1134+
* Blank / unset setting keeps all paragraphs.
11311135
* @tests in jest file
11321136
* @param {Array<TParagraph>} paras - paragraphs to filter
1133-
* @param {TDashboardSettings} dashboardSettings - dashboard settings containing included calendar sections
1137+
* @param {TDashboardSettings} dashboardSettings - dashboard settings containing included calendar sections/terms
11341138
* @param {Date} startTime - timer start time for logging
11351139
* @param {string} functionName - name of calling function for logging
11361140
* @returns {Array<TParagraph>} filtered paragraphs
@@ -1147,18 +1151,28 @@ export function filterParasByIncludedCalendarSections(
11471151
const includedCalendarSections = stringListOrArrayToArray(dashboardSettings.includedCalendarSections, ',')
11481152
.map((s) => s.trim())
11491153
.filter((s) => s !== '')
1154+
if (includedCalendarSections.length === 0) {
1155+
return paras
1156+
}
11501157

11511158
// TEST: this is where TB defeat happens for headings
11521159
const filteredParas = paras.filter((p) => {
11531160
// only apply to calendar notes
11541161
if (p.note?.type !== 'Calendar') return true
1155-
// Apply to all H4/H3/H2 headings in the hierarchy for this para
1162+
1163+
// Keep if task content contains any of the filter terms (e.g. #acme, @acme, or acme)
1164+
const contentLower = (p.content || '').toLowerCase()
1165+
if (includedCalendarSections.some((inc) => contentLower.includes(inc.toLowerCase()))) {
1166+
return true
1167+
}
1168+
1169+
// Or if any heading in the hierarchy matches as a case-insensitive prefix
11561170
const theseHeadings = getHeadingHierarchyForThisPara(p)
11571171
return theseHeadings.some((h) =>
11581172
includedCalendarSections.some((inc) => caseInsensitiveStartsWith(inc, h.trim(), false))
11591173
)
11601174
})
1161-
logTimer(functionName, startTime, `- ${filteredParas.length} paras after filtering out calendar headings`)
1175+
logTimer(functionName, startTime, `- ${filteredParas.length} paras after includedCalendarSections filter`)
11621176
return filteredParas
11631177
}
11641178

‎jgclark.Dashboard/src/dashboardSettings.js‎

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
//-----------------------------------------------------------------------------
33
// Settings for the dashboard - loaded/set in React Window
4-
// Last updated 2026-07-18 for v2.4.0.b52 by @jgclark + @CursorAI
4+
// Last updated 2026-07-20 for v2.4.0.b52 by @jgclark + @CursorAI
55
//-----------------------------------------------------------------------------
66

77
import { defaultSectionDisplayOrder } from './constants.js'
@@ -96,7 +96,7 @@ export const dashboardSettingDefs: Array<TSettingItem> = [
9696
type: 'heading',
9797
label: 'What to Include and Exclude',
9898
description:
99-
"These settings control what Spaces, folders, Reminder lists and items are included and excluded in Dashboard's many sections. It will first use only the included Spaces. Then, it includes the folders from the next setting, and removes any specified from the following setting. Then certain Sections within Calendar notes can be included. Reminder Lists to Include can override which Apple Reminders lists this Perspective looks in. Finally, individual lines in notes can be ignored by adding terms to the last setting.",
99+
"These settings control what Spaces, folders, Reminder lists and items are included and excluded in Dashboard's many sections. It will first use only the included Spaces. Then, it includes the folders from the next setting, and removes any specified from the following setting. Then calendar note terms (heading prefixes or task text) can focus which calendar items appear. Reminder Lists to Include can override which Apple Reminders lists this Perspective looks in. Finally, individual lines in notes can be ignored by adding terms to the last setting.",
100100
},
101101
{
102102
key: 'includedTeamspaces',
@@ -129,23 +129,13 @@ export const dashboardSettingDefs: Array<TSettingItem> = [
129129
},
130130
{
131131
key: 'includedCalendarSections',
132-
label: 'Calendar note Sections to Include',
132+
label: 'Calendar note terms to include',
133133
description:
134-
"Comma-separated list of calendar note section heading prefixes (case-insensitive). A task is included if any heading above it starts with one of these values (after trimming). For example, 'Wins' matches 'Wins' and 'Wins for 2026 Q2'. If left blank, all sections are included. Very short names can match more than one section if one heading is a prefix of another.",
134+
"Comma-separated list of terms for calendar notes only. A task is included if (1) any heading above it starts with one of these values (case-insensitive prefix; e.g. 'Wins' matches 'Wins for 2026 Q2'), or (2) the task text itself contains the term (e.g. #acme, @acme, or acme). If left blank, all calendar items are included. Short terms can over-match. When this is set and Hide Duplicates is on, matching items stay in calendar / Overdue sections rather than only in a TAG section.",
135135
type: 'input',
136136
default: '',
137137
compactDisplay: true,
138138
},
139-
// {
140-
// key: 'TODO: req',
141-
// label: 'In Calendar notes, task contains term(s) or is in named Section',
142-
// description:
143-
// '...',
144-
// type: 'input',
145-
// default: '',
146-
// compactDisplay: false,
147-
// dependsOnKey: '',
148-
// },
149139
{
150140
key: 'ignoreItemsWithTerms',
151141
label: 'Exclude items containing term(s)',

‎jgclark.Dashboard/src/react/components/Section/sectionHelpers.js‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
//--------------------------------------------------------------------------
33
// Helpers for the Section component.
4-
// Last updated 2026-07-10 for v2.4.0.b47 by @jgclark + @CursorAI
4+
// Last updated 2026-07-20 for v2.4.0.b52 by @jgclark + @CursorAI
55
//--------------------------------------------------------------------------
66

77
import type { TSection, TSectionItem, TDashboardSettings, TSectionCode, TSectionDetails, TSettingItem } from '../../../types.js'
@@ -172,6 +172,44 @@ function getUseFirstButVisible(useFirst: Array<TSectionCode>, dashboardSettings:
172172
return useFirstButVisible
173173
}
174174

175+
/**
176+
* When includedCalendarSections is set, prefer calendar period and Overdue sections over TAG
177+
* for Hide Duplicates, so focused tagged items stay in Today/Overdue rather than only in TAG.
178+
* Leaves the default order alone when the setting is blank.
179+
* @param {Array<TSectionCode>} useFirst - base dedupe priority order
180+
* @param {TDashboardSettings} dashboardSettings
181+
* @returns {Array<TSectionCode>} possibly reordered priority list
182+
*/
183+
export function adjustDedupPriorityForCalendarFocus(
184+
useFirst: Array<TSectionCode>,
185+
dashboardSettings: TDashboardSettings,
186+
): Array<TSectionCode> {
187+
if (!dashboardSettings?.includedCalendarSections) {
188+
return useFirst
189+
}
190+
const terms = String(dashboardSettings.includedCalendarSections)
191+
.split(',')
192+
.map((s) => s.trim())
193+
.filter((s) => s !== '')
194+
if (terms.length === 0) {
195+
return useFirst
196+
}
197+
198+
const preferBeforeTag: Array<TSectionCode> = ['DT', 'DY', 'DO', 'LW', 'W', 'M', 'Q', 'Y', 'OVERDUE']
199+
const tagIndex = useFirst.indexOf('TAG')
200+
if (tagIndex < 0) {
201+
return useFirst
202+
}
203+
204+
const beforeTag = useFirst.slice(0, tagIndex)
205+
const afterTag = useFirst.slice(tagIndex + 1)
206+
const preferredInOrder = useFirst.filter((code) => preferBeforeTag.includes(code))
207+
const restBefore = beforeTag.filter((code) => !preferBeforeTag.includes(code))
208+
const restAfter = afterTag.filter((code) => !preferBeforeTag.includes(code))
209+
210+
return [...restBefore, ...preferredInOrder, 'TAG', ...restAfter]
211+
}
212+
175213
/**
176214
* Removes duplicate items from sections based on specified fields and prioritizes sections based on a given order.
177215
* Note: This will be called multiple times for each section being displayed -- for all the other sections, it seems.
@@ -196,8 +234,11 @@ export function getSectionsWithoutDuplicateLines(
196234
// Deep copy the sections to avoid mutating the original data
197235
const sections = JSON.parse(JSON.stringify(_sections))
198236

237+
// When calendar focus terms are set, prefer calendar / Overdue over TAG for duplicates
238+
const adjustedUseFirst = adjustDedupPriorityForCalendarFocus(useFirst, dashboardSettings)
239+
199240
// Get ordered list of sectionCodes based on visibility and priority
200-
const useFirstVisibleOnly: Array<TSectionCode> = getUseFirstButVisible(useFirst, dashboardSettings, sections)
241+
const useFirstVisibleOnly: Array<TSectionCode> = getUseFirstButVisible(adjustedUseFirst, dashboardSettings, sections)
201242

202243
// Create an array of ordered sections based on the `useFirstVisibleOnly` priority list.
203244
// For each section code (`st`) in `useFirstVisibleOnly`, use `flatMap` to:
@@ -210,7 +251,7 @@ export function getSectionsWithoutDuplicateLines(
210251
// logDebug('getSectionsWithoutDuplicateLines', `Starting with useFirstVisibleOnly: ${useFirstVisibleOnly.join('-')} with ${totalItemsBeforeDedupe} items`)
211252

212253
// Include sections not listed in useFirst at the end of the array
213-
orderedSections.push(...sections.filter((section) => !useFirst.includes(section.sectionCode)))
254+
orderedSections.push(...sections.filter((section) => !adjustedUseFirst.includes(section.sectionCode)))
214255
// Map to track unique items
215256
const itemMap: any = new Map()
216257

0 commit comments

Comments
 (0)