Skip to content

Commit e04b547

Browse files
committed
b4 Fix to 'Generating' message getting stuck.
New 'display density' setting. UI polish to top bar, scrollbars and scrolling.
1 parent dcc2e39 commit e04b547

11 files changed

Lines changed: 402 additions & 41 deletions

File tree

‎helpers/react/DynamicDialog/DropdownSelect.css‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,37 @@
6868
box-shadow: 0 2px 5px var(--divider-color);
6969
z-index: 5;
7070
width: 100%;
71-
/* ensure that a right margin can be seen still - not sure why this is needed */
72-
padding-right: 6px;
71+
box-sizing: border-box;
72+
/* Include padding in width so it does not force a needless horizontal scrollbar */
73+
padding-right: 6px;
74+
/* thumb = grabable knob */
75+
scrollbar-color: var(--bg-alt-color, #e6e9ef) transparent;
76+
}
77+
78+
.dropdown-select-dropdiv::-webkit-scrollbar {
79+
width: 8px;
80+
height: 8px;
81+
}
82+
83+
.dropdown-select-dropdiv::-webkit-scrollbar-track {
84+
background: transparent;
85+
}
86+
87+
.dropdown-select-dropdiv::-webkit-scrollbar-thumb {
88+
background: var(--bg-alt-color, #e6e9ef);
89+
border-radius: 6px;
7390
}
7491

7592
/* The displayed options. */
7693
.dropdown-select-option {
7794
cursor: pointer;
7895
color: var(--fg-main-color);
79-
margin: 0rem 0.2rem;
80-
padding: 0.2rem;
96+
/* Inset via padding only - margin + width:100% overflowed the menu by a few pixels and forced a horizontal scrollbar */
97+
margin: 0;
98+
padding: 0.2rem 0.4rem;
8199
min-height: 0.9rem;
82100
width: 100%;
101+
box-sizing: border-box;
83102
}
84103

85104
/* The currently hovered-over or keyboard-highlighted option. */

‎jgclark.Dashboard/CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
# What's changed in 🎛 Dashboard plugin?
22
For more details see the [plugin's documentation](https://github.com/NotePlan/plugins/tree/main/jgclark.Dashboard/).
33

4+
<!-- "Not calling init(): Automatic plugin updates are disabled in preferences." -->
5+
46
## [2.5.0.b4] 2026-09-10
7+
- New: Add task / checklist dialogs (Today, Week, etc.) pre-select Under Heading by near-match to the active Perspective name, otherwise the first real heading (not top-of-note / insert-new pseudo options).
58
- New: Display Density setting (Normal / Compact). Compact tightens vertical spacing between items and truncates item text at half the Normal length (70 vs 140 characters).
9+
- Change: Top-bar Refresh, Hard Refresh, and Perspective dropdown share the same control height, with text/icons set 1px higher.
10+
- Change: Refresh icon spins during first generation ("Generating") as well as during refresh.
11+
- Fix: Perspective dropdown menu no longer shows a needless horizontal scrollbar (width:100% options plus horizontal margin/padding overflow).
12+
- Change: Horizontal overscroll bounce disabled. Vertical bounce applies only to Sections (header stays fixed).
13+
- Change: Main window and Perspective dropdown scrollbar thumbs use `--bg-alt-color`.
14+
- Fix: Generating no longer sticks after open when ForceInitialLoad is on - defer relative-dates Calendar bridge until after `firstRun` clears, and clear `firstRun` before done-count recount on that path.
615

716
## [2.5.0.b3] 2026-09-08
817
- Fix: If a task cannot be opened/highlighted from Dashboard, refresh that section and say so in the toast. The refresh now runs before the toast, so the banner cannot interrupt it.

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import {
99
filterParasByIgnoreTerms,
1010
filterParasByIncludedCalendarSections,
1111
filterParasByExcludedCalendarSections,
12+
isPseudoHeadingOption,
1213
isWinItem,
1314
makeDashboardParas,
1415
mergeSections,
16+
pickDefaultHeadingForAddTaskDialog,
17+
scoreHeadingPerspectiveMatch,
1518
} from '../dashboardHelpers.js'
1619
import { DataStore, Editor, CommandBar, NotePlan, Paragraph, Note, simpleFormatter } from '@mocks/index'
1720
import * as timeblocks from '@helpers/timeblocks'
@@ -78,6 +81,79 @@ describe(`${PLUGIN_NAME}`, () => {
7881
})
7982
})
8083

84+
describe('isPseudoHeadingOption()', () => {
85+
test('detects top / bottom / insert-new pseudo headings', () => {
86+
expect(isPseudoHeadingOption('⏫ (top of note)')).toBe(true)
87+
expect(isPseudoHeadingOption('⏬ (bottom of note)')).toBe(true)
88+
expect(isPseudoHeadingOption('➕#️⃣ (first insert new heading at the start of the note)')).toBe(true)
89+
expect(isPseudoHeadingOption('➕#️⃣ (first insert new heading under the title)')).toBe(true)
90+
expect(isPseudoHeadingOption('Home')).toBe(false)
91+
expect(isPseudoHeadingOption('### CCC')).toBe(false)
92+
})
93+
})
94+
95+
describe('scoreHeadingPerspectiveMatch()', () => {
96+
test('exact match scores highest', () => {
97+
expect(scoreHeadingPerspectiveMatch('CCC', 'CCC')).toBe(100)
98+
expect(scoreHeadingPerspectiveMatch('ccc', '### CCC')).toBe(100)
99+
})
100+
101+
test('close matches score above zero', () => {
102+
expect(scoreHeadingPerspectiveMatch('Home', 'Home tasks')).toBe(80)
103+
expect(scoreHeadingPerspectiveMatch('Wider Ministry', 'Ministry')).toBe(60)
104+
})
105+
106+
test('unrelated names score zero', () => {
107+
expect(scoreHeadingPerspectiveMatch('NotePlan', 'Journal')).toBe(0)
108+
expect(scoreHeadingPerspectiveMatch('-', 'Home')).toBe(0)
109+
})
110+
})
111+
112+
describe('pickDefaultHeadingForAddTaskDialog()', () => {
113+
const headings = [
114+
'⏫ (top of note)',
115+
'➕#️⃣ (first insert new heading at the start of the note)',
116+
'Home',
117+
'Ministry',
118+
'CCC',
119+
'Journal for 10.9.26',
120+
'⏬ (bottom of note)',
121+
]
122+
123+
test('picks exact perspective match among real headings', () => {
124+
const result = pickDefaultHeadingForAddTaskDialog(headings, 'CCC')
125+
expect(result.heading).toBe('CCC')
126+
expect(result.reason).toBe('perspective-match')
127+
expect(result.score).toBe(100)
128+
})
129+
130+
test('picks close match when no exact match', () => {
131+
const result = pickDefaultHeadingForAddTaskDialog(headings, 'Wider Ministry')
132+
expect(result.heading).toBe('Ministry')
133+
expect(result.reason).toBe('perspective-match')
134+
expect(result.score).toBe(60)
135+
})
136+
137+
test('falls back to first real heading when no near match', () => {
138+
const result = pickDefaultHeadingForAddTaskDialog(headings, 'NotePlan')
139+
expect(result.heading).toBe('Home')
140+
expect(result.reason).toBe('first-real-heading')
141+
expect(result.score).toBe(0)
142+
})
143+
144+
test('falls back to first real heading when perspective is -', () => {
145+
const result = pickDefaultHeadingForAddTaskDialog(headings, '-')
146+
expect(result.heading).toBe('Home')
147+
expect(result.reason).toBe('first-real-heading')
148+
})
149+
150+
test('returns empty when only pseudo headings exist', () => {
151+
const result = pickDefaultHeadingForAddTaskDialog(['⏫ (top of note)', '⏬ (bottom of note)'], 'CCC')
152+
expect(result.heading).toBe('')
153+
expect(result.reason).toBe('none')
154+
})
155+
})
156+
81157
describe('filterToOpenParagraphs()', () => {
82158
test('should return empty array when input is empty', () => {
83159
const result = filterToOpenParagraphs([], false, '')

‎jgclark.Dashboard/src/dashboardHelpers.js‎

Lines changed: 109 additions & 5 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-08-14 for v2.4.0.b63 by @jgclark + @CursorAI
4+
// Last updated 2026-09-10 for v2.5.0.b4 by @jgclark + @CursorAI
55
//-----------------------------------------------------------------------------
66

77
// import pluginJson from '../plugin.json'
@@ -10,7 +10,7 @@ import { normaliseDashboardNumberSettings } from './dashboardSettings'
1010
import { getDashboardSettingsDefaults } from './dashboardSettingsDefaults'
1111
import { loadDashboardPluginSettings, saveDashboardPluginSettings } from './dashboardPluginSettings'
1212
import { removeInvalidTagSections } from './dashboardSettingsClean'
13-
import { getCurrentlyAllowedFolders } from './perspectivesShared'
13+
import { getActivePerspectiveNameSync, getCurrentlyAllowedFolders } from './perspectivesShared'
1414
import { normalizePreferredWindowType } from './preferredWindowType'
1515
import { parseSettings, validateAndFlattenMessageObject } from './shared'
1616
import type { ValidatedData } from './shared'
@@ -793,11 +793,115 @@ export function isTBSectionEnabled(config: TDashboardSettings): boolean {
793793
}
794794

795795
/**
796-
* Default heading for new-task form fields; empty string when setting is <<carry forward>>.
796+
* Whether a heading option is a pseudo-option (top/bottom/insert-new) rather than a real note heading.
797+
* @param {string} heading
798+
* @returns {boolean}
799+
*/
800+
export function isPseudoHeadingOption(heading: string): boolean {
801+
if (!heading || typeof heading !== 'string') return true
802+
const h = heading.toLowerCase()
803+
return (
804+
h.includes('top of note') ||
805+
h.includes('bottom of note') ||
806+
h.includes('insert new heading') ||
807+
heading.includes('⏫') ||
808+
heading.includes('⏬') ||
809+
heading.includes('➕')
810+
)
811+
}
812+
813+
/**
814+
* Strip markdown heading markers and trim for comparison.
815+
* @param {string} heading
816+
* @returns {string}
817+
*/
818+
function normalizeHeadingTextForMatch(heading: string): string {
819+
return heading.replace(/^#{1,5}\s*/, '').trim()
820+
}
821+
822+
/**
823+
* Score how well a heading matches a perspective name (higher is better; 0 = no match).
824+
* @param {string} perspectiveName
825+
* @param {string} heading
826+
* @returns {number}
827+
*/
828+
export function scoreHeadingPerspectiveMatch(perspectiveName: string, heading: string): number {
829+
const p = perspectiveName.trim().toLowerCase()
830+
const h = normalizeHeadingTextForMatch(heading).toLowerCase()
831+
if (!p || !h || p === '-') return 0
832+
if (p === h) return 100
833+
// Starts-with either direction (e.g. "Home" / "Home tasks")
834+
if ((h.startsWith(p) || p.startsWith(h)) && Math.min(p.length, h.length) >= 2) return 80
835+
// Substring either direction when the shorter term is meaningful (e.g. "Wider Ministry" / "Ministry")
836+
const shorter = p.length <= h.length ? p : h
837+
const longer = p.length <= h.length ? h : p
838+
if (shorter.length >= 3 && longer.includes(shorter)) return 60
839+
// Token overlap (split on whitespace / common separators)
840+
const pTokens = p.split(/[\s/_:-]+/).filter((t) => t.length >= 3)
841+
const hTokens = h.split(/[\s/_:-]+/).filter((t) => t.length >= 3)
842+
if (pTokens.some((pt) => hTokens.includes(pt))) return 50
843+
return 0
844+
}
845+
846+
/**
847+
* Pick the best Under Heading pre-select for add-task dialogs: near-match to the active Perspective,
848+
* otherwise the first real (non-pseudo) heading.
849+
* @param {Array<string>} headings - options as shown in the dropdown (may include pseudo-headings)
850+
* @param {string} perspectiveName - active Perspective name (or '-' / empty)
851+
* @returns {{ heading: string, reason: string, score: number }}
852+
*/
853+
export function pickDefaultHeadingForAddTaskDialog(
854+
headings: Array<string>,
855+
perspectiveName: string,
856+
): { heading: string, reason: string, score: number } {
857+
const realHeadings = headings.filter((h) => h && String(h).trim() !== '' && !isPseudoHeadingOption(h))
858+
logInfo(
859+
'pickDefaultHeadingForAddTaskDialog',
860+
`Input: perspectiveName="${perspectiveName}", headings=[${headings.join(' | ')}], realHeadings=[${realHeadings.join(' | ')}]`,
861+
)
862+
863+
let bestHeading = ''
864+
let bestScore = 0
865+
if (perspectiveName && perspectiveName !== '-') {
866+
for (const heading of realHeadings) {
867+
const score = scoreHeadingPerspectiveMatch(perspectiveName, heading)
868+
if (score > bestScore) {
869+
bestScore = score
870+
bestHeading = heading
871+
}
872+
}
873+
}
874+
875+
if (bestHeading) {
876+
logInfo(
877+
'pickDefaultHeadingForAddTaskDialog',
878+
`Result: matched "${bestHeading}" (score ${String(bestScore)}) for perspective "${perspectiveName}"`,
879+
)
880+
return { heading: bestHeading, reason: 'perspective-match', score: bestScore }
881+
}
882+
883+
const fallback = realHeadings[0] || ''
884+
logInfo(
885+
'pickDefaultHeadingForAddTaskDialog',
886+
`Result: no near match for perspective "${perspectiveName}"; falling back to first real heading "${fallback}"`,
887+
)
888+
return { heading: fallback, reason: fallback ? 'first-real-heading' : 'none', score: 0 }
889+
}
890+
891+
/**
892+
* Default heading for new-task form fields.
893+
* When headings are provided, prefers a near-match to the active Perspective, else the first real heading.
894+
* Without headings, falls back to newTaskSectionHeading (empty when that setting is <<carry forward>>).
797895
* @param {TDashboardSettings} config
896+
* @param {Array<string>} headings - optional dropdown options from the destination note
798897
* @returns {string}
799898
*/
800-
export function getDefaultHeadingForNewTask(config: TDashboardSettings): string {
899+
export function getDefaultHeadingForNewTask(config: TDashboardSettings, headings: Array<string> = []): string {
900+
if (headings.length > 0) {
901+
const perspectiveName = getActivePerspectiveNameSync()
902+
const { heading } = pickDefaultHeadingForAddTaskDialog(headings, perspectiveName)
903+
if (heading) return heading
904+
}
801905
return config.newTaskSectionHeading !== '<<carry forward>>' ? config.newTaskSectionHeading : ''
802906
}
803907

@@ -810,7 +914,7 @@ export function getDefaultHeadingForNewTask(config: TDashboardSettings): string
810914
export function buildAddTaskFormFields(headings: Array<string>, config: TDashboardSettings): Array<TDialogSettingItem> {
811915
const formFieldsBase: Array<TDialogSettingItem> = [{ type: 'input', label: 'Task:', key: 'text', focus: true }]
812916
if (!headings.length) return formFieldsBase
813-
const defaultHeadingToAddTo = getDefaultHeadingForNewTask(config)
917+
const defaultHeadingToAddTo = getDefaultHeadingForNewTask(config, headings)
814918
return formFieldsBase.concat(
815919
([
816920
{

‎jgclark.Dashboard/src/perspectivesShared.js‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// Shared perspective-related utilities
44
// This file is reserved for shared perspective functions that don't belong
55
// in perspectiveHelpers.js (to avoid circular dependency).
6+
// Last updated 2026-09-10 for v2.5.0.b4 by @jgclark + @CursorAI
67
//-----------------------------------------------------------------------------
78

8-
import type { TDashboardSettings } from './types'
9+
import type { TDashboardSettings, TPerspectiveDef } from './types'
910
import { stringListOrArrayToArray } from '@helpers/dataManipulation'
1011
import { getFoldersMatching } from '@helpers/folders'
1112
import { logDebug } from '@helpers/dev'
@@ -28,3 +29,22 @@ export function getCurrentlyAllowedFolders(
2829
const folderListToUse = getFoldersMatching(includedFolderArr, true, excludedFolderArr)
2930
return folderListToUse
3031
}
32+
33+
/**
34+
* Sync read of the active Perspective name from DataStore.settings (or '-' if none).
35+
* Kept here so dashboardHelpers can call it without importing perspectiveHelpers (circular dependency).
36+
* @returns {string}
37+
*/
38+
export function getActivePerspectiveNameSync(): string {
39+
try {
40+
const raw = DataStore.settings?.perspectiveSettings
41+
if (raw == null) return '-'
42+
const defs: Array<TPerspectiveDef> = typeof raw === 'string' ? JSON.parse(raw) : raw
43+
if (!Array.isArray(defs)) return '-'
44+
const active = defs.find((p) => p && p.isActive === true)
45+
return active && typeof active.name === 'string' && active.name !== '' ? active.name : '-'
46+
} catch (error) {
47+
logDebug('getActivePerspectiveNameSync', `Failed to read active perspective: ${error.message}`)
48+
return '-'
49+
}
50+
}

‎jgclark.Dashboard/src/react/components/Dashboard.jsx‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//--------------------------------------------------------------------------
33
// Dashboard React component to aggregate data and layout for the dashboard
44
// Called by WebView component.
5-
// Last updated for 2026-08-21 for v2.4.1, @jgclark + @CursorAI
5+
// Last updated for 2026-09-14 for v2.5.0.b4, @jgclark + @CursorAI
66
//--------------------------------------------------------------------------
77

88
//--------------------------------------------------------------------------
@@ -232,19 +232,27 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
232232
logChanges('pluginData', pluginData)
233233
}, [pluginData])
234234

235-
// At Startup, request the Dashboard Sections content by telling the plugin that Dashboard is loaded
236-
// Sections starts out as empty array, so this is the first time it will be populated
235+
// At Startup, request the Dashboard Sections content by telling the plugin that Dashboard is loaded.
236+
// Sections starts out as empty array, so this is the first time it will be populated.
237237
useEffect(() => {
238238
// Note: This executes before globalSharedData is saved into state
239239
logInfo('Dashboard/useEffect [] (startup only)', `${sections.length} sections (${origSections.length} origSections): [${sections.map((s) => s.sectionCode).join(', ')}]`)
240240
logDebug('Dashboard', `React: sending reactWindowInitialisedSoStartGeneratingData command to plugin`)
241241
runPluginCommand('reactWindowInitialisedSoStartGeneratingData', 'jgclark.Dashboard', [''])
242-
// WebView Calendar.* calls often return Thenables during early startup. Re-apply week entries in the
243-
// NPdateTime relative-dates cache once the bridge is ready (correct week start vs moment fallback).
242+
// Do NOT call refreshRelativeDatesISOCache() here. Its Calendar/DataStore bridge traffic
243+
// contends on the same JSContext as generation and was aborting firstRun completion
244+
// ('generating' stuck while sections from ForceInitialLoad already looked done).
245+
}, [])
246+
247+
// After generation clears firstRun, refresh relative-week ISO cache once (bridge is quiet).
248+
const relativeDatesRefreshedAfterGenerationRef = useRef(false)
249+
useEffect(() => {
250+
if (pluginData.firstRun || relativeDatesRefreshedAfterGenerationRef.current) return
251+
relativeDatesRefreshedAfterGenerationRef.current = true
244252
refreshRelativeDatesISOCache().catch((err) => {
245253
logWarn('Dashboard', `refreshRelativeDatesISOCache failed: ${err?.message ?? String(err)}`)
246254
})
247-
}, [])
255+
}, [pluginData.firstRun])
248256

249257
// Keep the window/tab title in sync. In Main Window / Split View, NotePlan's chrome uses the
250258
// title from showInMainWindow and does not reliably follow document.title changes that append
@@ -449,7 +457,7 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
449457
style={dashboardContainerStyle}
450458
tabIndex={0}
451459
ref={containerRef}
452-
className={`${pluginData.platform ?? ''}${dashboardSettings?.displayDensity === 'Compact' ? ' density-compact' : ''}`.trim()}
460+
className={`dashboard-app ${pluginData.platform ?? ''}${dashboardSettings?.displayDensity === 'Compact' ? ' density-compact' : ''}`.trim()}
453461
>
454462
{autoUpdateEnabled && (
455463
<IdleTimer
@@ -464,7 +472,7 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
464472
)}
465473
<div className="dashboard">
466474
<Header lastFullRefresh={lastFullRefresh} onDropdownMenuOpenChange={setDropdownMenuOpen} />
467-
<main>
475+
<main className="dashboard-sections">
468476
{sections.map((section, index) => (
469477
<Section key={`${section.sectionCode}-${index}`} section={section} onButtonClick={handleSectionButtonClick} isViewVisible={isViewVisible} />
470478
))}

0 commit comments

Comments
 (0)