Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
0ccb13a
Remove the <details> and <summary> parts, to have just one list.
jgclark Feb 22, 2026
23eab84
Allow project tags to be shown in col2 or col3 in table display
jgclark Feb 23, 2026
9a61a10
Remove single-section header
jgclark Feb 23, 2026
9e9bc35
Tweak to window/custom IDs in showHTMLV2() identified by Cursor
jgclark Feb 24, 2026
7432a64
b2 - add basic demo mode
jgclark Feb 24, 2026
288bae1
b3 refactoring reviews.js
jgclark Feb 25, 2026
55f790f
TaskAutomations 3.1.1 - fix weeks calculation to honor NP week
dwertheimer Feb 23, 2026
8e4e5cc
Make new endOfPreambleSection() helper by taking preamble detection o…
jgclark Feb 26, 2026
c027b79
Merge P+R v1.3.1 into 1.4 work branch
jgclark Feb 26, 2026
ceabfa5
b4: Add automatic refresh + setting autoUpdateAfterIdleTime
jgclark Feb 27, 2026
dc8f887
b5 roject metadata can now be fully stored in frontmatter as well as …
jgclark Mar 4, 2026
2a8fc8f
Merge remote-tracking branch 'origin/main' into projects-new-layout
jgclark Mar 11, 2026
bd07839
generateProjectsWeeklyProgressLines() now uses full folder paths cons…
jgclark Mar 12, 2026
b343605
b6 weekly project progress heatmaps
jgclark Mar 12, 2026
a463603
b7 improvements to lozenges + some leftovers from b6.
jgclark Mar 13, 2026
af8dca7
Remove webfonts from build as no longer necessary.
jgclark Mar 13, 2026
bbbd61c
b7 mostly lozenge UI
jgclark Mar 15, 2026
a384a20
b8 metadata improvements for FM and body of notes
jgclark Mar 16, 2026
9a265a2
b8 fix some issues on migration of metadata from body to frontmatter
jgclark Mar 18, 2026
d4b7501
b9 layout changes to use bordered project rows
jgclark Mar 20, 2026
5e99dd6
b9 Modernise layout
jgclark Mar 21, 2026
9599362
b10 add 'order by' control to top bar
jgclark Mar 21, 2026
901bf0c
Move Order control into dropdown and adjust its layout and that of to…
jgclark Mar 21, 2026
2764aeb
b11 - update changelog for first public beta of v2.0
jgclark Mar 21, 2026
a8fa3d7
improve multi-column layout
jgclark Mar 22, 2026
c317aae
b12 WIP remove 2 experimental config items about layout
jgclark Mar 22, 2026
5257226
WIP documentation updates for v2
jgclark Mar 22, 2026
59ef99d
b12 streamline CSS definitions and function names
jgclark Mar 23, 2026
0874489
Improve case insensitivity of updateFrontMatterVars()
jgclark Mar 26, 2026
5c782ea
b13 improvements to Frontmatter handling
jgclark Mar 26, 2026
3935c6f
b14 default frontmatter date and interval metadata to be in separate …
jgclark Mar 27, 2026
c9f7a5b
b14
jgclark Mar 27, 2026
30902b1
Add new getHashtagsFromString() helper
jgclark Mar 27, 2026
1538f95
b15 Adds '(first) Project tag' as a sort order.
jgclark Mar 29, 2026
581773d
Missed one
jgclark Mar 29, 2026
f433a9e
Harden getContentFromBrackets() helper from invalid data
jgclark Mar 29, 2026
4d18719
Improve jsdoc and add a test for ensureFrontmatter() helper
jgclark Mar 29, 2026
d5ed443
b16 WIP layout + metadata handling improvements
jgclark Mar 31, 2026
56725af
Dashboard b22 WIP +b23 window visibility check
jgclark Mar 31, 2026
b6a0a6d
change sorting order for "(first) project tag" to come in the order t…
jgclark Apr 11, 2026
8bbc641
now pauses/unpauses the auto refresh timers when the rich window is h…
jgclark Apr 12, 2026
eebb0f7
Add 'commandBarForms' as a new usersVersionHasI() check
jgclark Apr 11, 2026
a41e12f
b18 - now adds metadata migration in Constructor
jgclark Apr 15, 2026
79c5d2a
b19 various fixes
jgclark Apr 16, 2026
10c9270
b20 fixes
jgclark Apr 17, 2026
d27b1ea
Handle empty brackets better in getContentFromBrackets()
jgclark Apr 18, 2026
03de9c6
updateSettingData() now gathers defaults where a setting key is not a…
jgclark Apr 18, 2026
b93a2e6
b13 small fixes and logging improvements
jgclark Apr 18, 2026
6f22f78
Merge branch 'projects-new-layout' of https://github.com/NotePlan/plu…
jgclark Apr 18, 2026
e047f22
b20 fixes
jgclark Apr 18, 2026
0a7cd1f
b21 added caching and some smaller Project constructor optimisiations
jgclark Apr 19, 2026
76be32e
b21 update for beta release
jgclark Apr 19, 2026
5bdc9aa
Turn down logging in helpers.
jgclark Apr 20, 2026
32d809e
b22: fix to startReview. Other tidy up. Try (and mostly fail) to be s…
jgclark Apr 20, 2026
5a1c4c7
Move numberOfOpenItemsInString() from note.js to NPnote.js
jgclark Apr 28, 2026
d80ba19
b24 WIP
jgclark Apr 28, 2026
957cec2
b25 metadata migration fixes
jgclark Apr 29, 2026
a3eaa8a
b26
jgclark Apr 30, 2026
c10731e
b27 refactoring + add count to topbar
jgclark Apr 30, 2026
a557926
b28 WIP new 'migrate all projects' and 'convert to project' commands
jgclark May 1, 2026
e987c8c
b28
jgclark May 1, 2026
3cd93e3
Improve split view handling by moving from x-callback to internal call
jgclark May 2, 2026
72db367
b29 fix circular dependency
jgclark May 2, 2026
72b082e
b29 fix "finish review" operations failing to find the project note o…
jgclark May 2, 2026
6e20557
Standardise shown counts
jgclark May 2, 2026
52b7166
b30 fix some counts + reduce paused project opacity
jgclark May 3, 2026
1f83bfc
b31: documentation, offer to migrate on upgrade/install
jgclark May 10, 2026
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
8 changes: 8 additions & 0 deletions __mocks__/Note.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { textWithoutSyncedCopyTag } from '@helpers/syncedCopies'
export class Note {
// Explicitly define properties that are dynamically assigned
content: string
/** Full note markdown when tests construct notes with `{ rawContent }` only */
rawContent: string = ''
paragraphs: any[]
// Properties
backlinks: any[] = [] /* sample: [ SOMETHING ], */
Expand Down Expand Up @@ -237,6 +239,12 @@ export class Note {
this._content = value
this.paragraphs = makeParagraphsFromContent(this._content)
}
} else if (key === 'rawContent') {
this.rawContent = value
if (typeof value === 'string' && value !== '' && (data.content === undefined || data.content === '')) {
this._content = value
this.paragraphs = makeParagraphsFromContent(this._content)
}
} else {
this[key] = data[key]
}
Expand Down
4 changes: 3 additions & 1 deletion helpers/HTMLView.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import showdown from 'showdown' // for Markdown -> HTML from https://github.com/
import { hasFrontMatter } from '@helpers/NPFrontMatter'
import { getFolderFromFilename } from '@helpers/folders'
import { clo, logDebug, logError, logInfo, logWarn, JSP, timer } from '@helpers/dev'
import { getStoredWindowRect, getWindowFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
import { getStoredWindowRect, getWindowFromCustomId, getWindowIdFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
import { isTermInEventLinkHiddenPart, isTermInNotelinkOrURI, isTermInMarkdownPath } from '@helpers/paragraph'
import { RE_EVENT_LINK, RE_SYNC_MARKER, formRegExForUsersOpenTasks } from '@helpers/regex'
Expand Down Expand Up @@ -146,6 +146,7 @@ export async function getNoteContentAsHTML(content: string, note: TNote): Promis
// Make some necessary changes before conversion to HTML
for (let i = 0; i < lines.length; i++) {
// remove any sync link markers (blockIds)
// TODO: there's a helper function for this, I think.
lines[i] = lines[i].replace(/\^[A-z0-9]{6}([^A-z0-9]|$)/g, '').trimRight()

// change open tasks to GFM-flavoured task syntax
Expand Down Expand Up @@ -716,6 +717,7 @@ export async function sendToHTMLWindow(windowId: string, actionType: string, dat

const windowExists = isHTMLWindowOpen(windowId)
if (!windowExists) logWarn(`sendToHTMLWindow`, `Window ${windowId} does not exist; setting NPWindowID = undefined`)
// TEST: Not sure the comment about iphone/ipad is still relevant, but leaving it in for now.
const windowIdToSend = windowExists ? windowId : undefined // for iphone/ipad you have to send undefined

const dataWithUpdated = {
Expand Down
58 changes: 43 additions & 15 deletions helpers/NPConfiguration.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,55 +55,79 @@ export async function initConfiguration(pluginJsonData: any): Promise<any> {
return migrateData
}

/**
* Return the Default for a setting from plugin.json.
* Only return empty string when default is null/undefined, so false and 0 are preserved.
* @param {any} setting - single entry from plugin.settings
* @returns {any}
*/
function getDefaultValueForNewSetting(setting: any): any {
const d = setting?.default
return d === undefined || d === null ? '' : d
}

/**
* Add new top-level keys to setting.json's data in the event plugin.settings object has been updated in a Plugin's plugin.json file.
* @author @codedungeon
* @param {any} pluginJsonData - plugin.json data for which plugin is being migrated
* @return {number} update result (1 settings updated, 0 no update necessary, -1 update failed)
* @return {number} update result (1 settings updated, 0 no update necessary, -1 update failed or invalid pluginJsonData)
*/
export function updateSettingData(pluginJsonData: any): number {
let updateResult = 0

if (pluginJsonData == null || typeof pluginJsonData !== 'object' || Array.isArray(pluginJsonData)) {
logWarn(
'NPConfiguration/updateSettingData',
'Invalid pluginJsonData: expected a non-null object (not an array). Skipping settings migration.',
)
return -1
}

const newSettings = {}
const currentSettingData = DataStore.settings

const pluginSettings = pluginJsonData.hasOwnProperty('plugin.settings') ? pluginJsonData['plugin.settings'] : []
// clo(pluginSettings, `pluginSettings`)
pluginSettings.forEach((setting) => {
const key: any = setting?.key || null
if (key) {
const key: any = setting?.key
const hasValidKey = key != null && key !== ''
// For each setting with a key (i.e. ignoring headings) check if it is in the current settings, and if not, add it with the default value.
if (hasValidKey) {
if (!currentSettingData.hasOwnProperty(key)) {
newSettings[key] = setting?.default || ''
newSettings[key] = getDefaultValueForNewSetting(setting)
logInfo('updateSettingData', `- Added new setting: ${key} = ${newSettings[key]}`)
updateResult = 1 // we have made at least one update, change result code accordingly
} else {
newSettings[key] = currentSettingData[key]
}
}
})

// logDebug(`NPConfiguration/updateSettingData: Object.keys(DataStore): ${Object.keys(DataStore).join(',')}`)
// logDebug('currentSettingData:', JSP(currentSettingData, 2))
// logDebug('newSettings:', JSP(newSettings, 2))
// logDebug('DataStore.settings:', JSP(DataStore.settings, 2))
try {
if (DataStore && typeof DataStore.settings === 'object' && updateResult > 0) {
if (updateResult > 0 && DataStore && typeof DataStore.settings === 'object') {
// WARNING: @jgclark at least once saw an 'undefined is not an object' error, which appeared to be for this line.
// dbw added the following logging to try to track it down but it looks like, JS thinks that DataStore is not an object at times.
// And yet, somehow the migration actually does work and migrates new settings. So, I'm not sure what's going on here.
// We are going to leave this alone for the time being, but if you see this error again, please uncomment the following to keep hunting.
logDebug(
`NPConfiguration/updateSettingData for ${pluginJsonData['plugin.id']} updateResult: ${updateResult}`,
`typeof DataStore: ${typeof DataStore} isArray:${String(
Array.isArray(DataStore),
)} typeof DataStore.settings: ${typeof DataStore?.settings} typeof newSettings: ${typeof newSettings}`,
)
// logDebug(
// `NPConfiguration/updateSettingData for ${pluginJsonData['plugin.id']} updateResult: ${updateResult}`,
// `typeof DataStore: ${typeof DataStore} isArray:${String(
// Array.isArray(DataStore),
// )} typeof DataStore.settings: ${typeof DataStore?.settings} typeof newSettings: ${typeof newSettings}`,
// )
logDebug(
'NPConfiguration/updateSettingData',
`About to update DataStore.settings to newSettings after an update. If you see a TypeError right after this, please ignore it. It's a known NP bug that doesn't seem to matter.`,
)
DataStore.settings = newSettings
}
} catch (error) {
console.log(
'NPConfiguration/updateSettingData/Plugin Settings Migration Failed. Was not able to automatically migrate your plugin settings to the new version. Please open the plugin settings and save in order to update your settings.',
logError('updateSettingData',
'Plugin Settings Migration Failed. Was not able to automatically migrate your plugin settings to the new version. Please open the plugin settings, check them, and then save in order to update your settings.',
)
updateResult = -1
}
Expand Down Expand Up @@ -568,6 +592,7 @@ export async function getSettingFromAnotherPlugin(pluginID: string, settingName:

/**
* Backup the settings.json file for 'pluginID' to a dated version in the plugin data folder.
* Note: this fails if the file is not valid JSON, unfortunately. @jgclark can't find a way around this.
* @author @jgclark
* @param {string} pluginID
* @param {string} reason
Expand All @@ -579,12 +604,15 @@ export async function backupSettings(pluginID: string, reason: string = 'backup'
const pluginSettings = await DataStore.loadJSON(`../${pluginID}/settings.json`)
const backupFilename = `settings_${reason}_${moment().format('YYYYMMDDHHmmss')}.json`
const backupPath = `../${pluginID}/${backupFilename}`
await DataStore.saveJSON(pluginSettings, backupPath)
const res = await DataStore.saveJSON(pluginSettings, backupPath)
if (!res) {
throw new Error(`Error saving backup to ${backupPath}`)
}
if (!suppressMessage) {
await showMessage(`Backup of ${pluginID} settings saved to ${backupPath}`, 'OK', `${pluginID} Settings Backup`)
}
logInfo('backupSettings', `Backup of ${pluginID} settings saved to ${backupPath}`)
return true
return res
} catch (error) {
if (!suppressMessage) {
await showMessage(`Error trying to Backup ${pluginID} settings. Please see Plugin Console log for details.`, 'OK', `${pluginID} Settings Backup`)
Expand Down
49 changes: 42 additions & 7 deletions helpers/NPEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,22 +262,57 @@ export function isNoteOpenInEditor(filename: string): boolean {
/**
* Returns the first open Editor window that matches a given filename (if any).
* If 'getLastOpenEditor' is true, then return the last matching open Editor window (which is the most recently opened one) instead.
* TEST: Changes 30.3.2026
* @author @jgclark
* @param {string} openNoteFilename to find in list of open Editor windows
* @param {boolean} getLastOpenEditor - whether to return the last open Editor window (which is the most recently opened one) instead of the first one that matches the filename (the default)
* @returns {TEditor | false} the matching open Editor window or false if not found
*/
export function getOpenEditorFromFilename(openNoteFilename: string, getLastOpenEditor: boolean = false): TEditor | false {
const allEditorWindows = NotePlan.editors
const matchingEditorWindows = allEditorWindows.filter(ew => ew.filename === openNoteFilename)
if (matchingEditorWindows.length === 0) {
logDebug('getOpenEditorFromFilename', `No open Editor window found for filename '${openNoteFilename}'`)
try {
const allEditorWindows = NotePlan.editors
const matchingEditorWindows = allEditorWindows?.filter(ew => ew.filename === openNoteFilename) ?? []
if (matchingEditorWindows.length === 0) {
logDebug('getOpenEditorFromFilename', `No open Editor window found for filename '${openNoteFilename}'`)
return false
}
if (getLastOpenEditor) {
return matchingEditorWindows[matchingEditorWindows.length - 1]
}
return matchingEditorWindows[0]
} catch (error) {
logError('getOpenEditorFromFilename', error.message)
return false
}
if (getLastOpenEditor) {
return matchingEditorWindows[matchingEditorWindows.length - 1]
}

/**
* Find a regular (folder) note that is open in some Editor pane — not only the focused one.
* Prefers the globally focused `Editor` when its note is already type 'Notes'; otherwise scans
* `NotePlan.editors` so split views with a calendar note focused still expose an open project note.
* @author @jgclark
* @returns {?TNote} the note, or null if no Editor pane shows a regular note
*/
export function getFirstRegularNoteAmongOpenEditors(): ?TNote {
try {
const focusedNote = Editor?.note
if (focusedNote && focusedNote.type === 'Notes') {
return focusedNote
}
const allEditorWindows = NotePlan.editors ?? []
for (const thisEditorWindow of allEditorWindows) {
const candidate = thisEditorWindow?.note
if (candidate && candidate.type === 'Notes') {
logDebug('getFirstRegularNoteAmongOpenEditors', `Using open editor pane for '${candidate.filename || candidate.title || '?'}' (focused pane was not a regular note)`)
return candidate
}
}
logDebug('getFirstRegularNoteAmongOpenEditors', `No open Editor pane contains a regular (Notes) note`)
return null
} catch (error) {
logError('getFirstRegularNoteAmongOpenEditors', error.message)
return null
}
return matchingEditorWindows[0]
}

/**
Expand Down
Loading