Skip to content

Commit cf87e3a

Browse files
committed
Merge branch 'main' of https://github.com/NotePlan/plugins
2 parents 95a3da2 + 3b994b7 commit cf87e3a

15 files changed

Lines changed: 447 additions & 43 deletions

File tree

‎dwertheimer.Forms/src/NPTemplateForm.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,11 @@ export async function restoreFormFromAutosave(autosaveFilename?: string): Promis
10331033
}
10341034

10351035
const templateData = templateNote.content || ''
1036-
const { _, frontmatterAttributes } = await DataStore.invokePluginCommandByName('renderFrontmatter', 'np.Templating', [templateData])
1036+
const { frontmatterBody, frontmatterAttributes } = await DataStore.invokePluginCommandByName('renderFrontmatter', 'np.Templating', [templateData])
1037+
if (frontmatterBody == null) {
1038+
logDebug(pluginJson, 'restoreFormFromAutosave: renderFrontmatter returned null (user likely cancelled); not opening form')
1039+
return
1040+
}
10371041

10381042
// Add form fields, template info, and default values
10391043
frontmatterAttributes.formFields = formFields

‎dwertheimer.Forms/src/formSubmission.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,13 @@ async function processCreateNew(data: any, formFields: Array<Object>): Promise<F
949949
try {
950950
// Use templating plugin to render the title (it contains template tags like <%- Contact_Name %>)
951951
const renderedTitleResult = await DataStore.invokePluginCommandByName('render', 'np.Templating', [newNoteTitleToUse, templatingContext])
952+
if (renderedTitleResult == null) {
953+
logDebug(pluginJson, 'processCreateNew: render returned null for newNoteTitle (user likely cancelled); aborting')
954+
return {
955+
success: false,
956+
formSubmissionError: 'Template rendering was cancelled.',
957+
}
958+
}
952959
if (renderedTitleResult && typeof renderedTitleResult === 'string') {
953960
renderedNewNoteTitle = renderedTitleResult
954961
logDebug(`processCreateNew: Rendered newNoteTitle from "${newNoteTitleToUse}" to "${renderedNewNoteTitle}"`)
@@ -1098,6 +1105,13 @@ async function processCreateNew(data: any, formFields: Array<Object>): Promise<F
10981105
if (folderPath && typeof folderPath === 'string' && (folderPath.includes('<%') || folderPath.includes('${'))) {
10991106
try {
11001107
const renderedFolderResult = await DataStore.invokePluginCommandByName('render', 'np.Templating', [folderPath, templatingContext])
1108+
if (renderedFolderResult == null) {
1109+
logDebug(pluginJson, 'processCreateNew: render returned null for folder (user likely cancelled); aborting')
1110+
return {
1111+
success: false,
1112+
formSubmissionError: 'Template rendering was cancelled.',
1113+
}
1114+
}
11011115
if (renderedFolderResult && typeof renderedFolderResult === 'string') {
11021116
folderPath = renderedFolderResult
11031117
logDebug(`processCreateNew: Rendered folder to "${folderPath}"`)

‎helpers/NPEditor.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ export async function checkAndProcessFolderAndNewNoteTitle(templateNote: TNote,
113113
// Don't pass the unrendered title - let templateNew extract it from rendered content
114114
const argsArray = [templateTitle, folderToUse === '/' ? '' : folderToUse, '', frontmatterAttributes]
115115
logDebug(`checkAndProcessFolderAndNewNoteTitle: invoking templateNew because theFolder:"${theFolder}" hasTitle:${hasTitle} with argsArray:${JSON.stringify(argsArray)}`)
116-
await DataStore.invokePluginCommandByName('templateNew', 'np.Templating', argsArray)
116+
const templateNewResult = await DataStore.invokePluginCommandByName('templateNew', 'np.Templating', argsArray)
117+
if (templateNewResult == null || (typeof templateNewResult === 'string' && templateNewResult.length === 0)) {
118+
logDebug(
119+
`checkAndProcessFolderAndNewNoteTitle: templateNew returned no filename (user likely cancelled a prompt); not trashing empty note "${emptyNoteFilename}"`,
120+
)
121+
return true
122+
}
117123
// move the empty note to the trash
118124
// await DataStore.moveNote(emptyNoteFilename, '@Trash')
119125
await DataStore.trashNote(emptyNoteFilename)

‎jgclark.DailyJournal/src/templatesStartEnd.js‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,16 @@ async function renderAndInsertTemplate(
7676
): Promise<void> {
7777
// Render the template, using recommended decoupled method of invoking a different plugin
7878
const result = await DataStore.invokePluginCommandByName('renderTemplate', 'np.Templating', [templateTitle])
79-
// TEST: turning off error message for now, as it fires on Templates that only do background work.
80-
// if (result == null || result === '') {
81-
// throw new Error(`No result from running Template '${templateTitle}'. Stopping.`)
82-
// }
83-
79+
// np.Templating: user may cancel a prompt — then **null** (see 2.4.5+). Do not insert.
80+
if (result == null) {
81+
logDebug(commandName, `renderTemplate returned null (user likely cancelled); not inserting for "${templateTitle}"`)
82+
return
83+
}
84+
if (typeof result !== 'string') {
85+
logDebug(commandName, `renderTemplate returned non-string (${typeof result}); not inserting for "${templateTitle}"`)
86+
return
87+
}
88+
8489
// Work out where to insert it in the note, by reading the template, and checking
8590
// the frontmatter attributes for a 'location' field (append/insert/cursor)
8691
const attrs = getAttributes(templateData, true)

‎np.MeetingNotes/src/NPMeetingNotes.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export async function insertNoteTemplate(origFileName: string, dailyNoteDate: Da
4848

4949
logDebug(pluginJson, 'calling renderFrontmatter() to pre-render template')
5050
const { frontmatterBody: templateBody, frontmatterAttributes } = await DataStore.invokePluginCommandByName('renderFrontmatter', 'np.Templating', [templateContent])
51+
if (templateBody == null) {
52+
logDebug(pluginJson, `insertNoteTemplate: renderFrontmatter returned null (prompt cancelled); aborting`)
53+
return
54+
}
5155
if (Editor.type !== 'Calendar') {
5256
// Check if the template wants the note to be created in a folder (or with a new title) and if so, move the empty note to the trash and create a new note in the folder
5357
logDebug(pluginJson, `insertNoteTemplate: about to checkAndProcessFolderAndNewNoteTitle`)
@@ -56,6 +60,10 @@ export async function insertNoteTemplate(origFileName: string, dailyNoteDate: Da
5660
logDebug(pluginJson, `render() template with frontmatterAttributes: [${Object.keys(frontmatterAttributes).join(', ')}]`)
5761

5862
const result = await DataStore.invokePluginCommandByName('render', 'np.Templating', [templateBody, frontmatterAttributes])
63+
if (result == null) {
64+
logDebug(pluginJson, `insertNoteTemplate: render returned null (user likely cancelled a prompt); not modifying editor or calendar note`)
65+
return
66+
}
5967

6068
if (dailyNoteDate) {
6169
logDebug(pluginJson, `apply rendered template to daily note with date ${String(dailyNoteDate)}`)
@@ -202,6 +210,11 @@ async function renderTemplateForEvent(selectedEvent, templateFilename): Object {
202210
)
203211
const { frontmatterBody, frontmatterAttributes } = await DataStore.invokePluginCommandByName('renderFrontmatter', 'np.Templating', [templateContent, templateVariables])
204212

213+
if (frontmatterBody == null) {
214+
logDebug(pluginJson, `${timer(scriptLoad)} - renderTemplateForEvent: renderFrontmatter returned null (prompt cancelled)`)
215+
return { result: null, attrs: frontmatterAttributes }
216+
}
217+
205218
clo(frontmatterBody, 'renderTemplateForEvent frontmatterBody:')
206219
clo(frontmatterAttributes, 'renderTemplateForEvent frontmatterAttributes:')
207220

@@ -345,6 +358,10 @@ export async function newMeetingNote(_selectedEvent?: TCalendarItem, _templateFi
345358
const { selectedEvent, templateFilename } = await selectEventAndTemplate(_selectedEvent, _templateFilename)
346359
logDebug(pluginJson, `${timer(scriptLoad)} - newMeetingNote: got selectedEvent and templateFilename; calling renderTemplateForEvent()`)
347360
const { result, attrs } = await renderTemplateForEvent(selectedEvent, templateFilename)
361+
if (result == null) {
362+
logDebug(pluginJson, `${timer(scriptLoad)} - newMeetingNote: render cancelled (null result); not creating note`)
363+
return
364+
}
348365
logDebug(pluginJson, `${timer(scriptLoad)} - newMeetingNote: rendered template`)
349366
clo(result, 'rendered template:')
350367
await createNoteAndLinkEvent(selectedEvent, result, attrs, forceNewNote)

‎np.Templating/CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,38 @@ See Plugin [Documentation](https://noteplan.co/templates/docs) for details on av
66

77
DBW: REMEMBER THAT IF YOU ADDED ANY HELPERS IMPORTS, ADD THEM TO THE HELPER MODULE TO GIVE SCRIPTS ACCESS TO THEM ALSO
88

9+
## [2.4.5] 2026-05-17 @dwertheimer
10+
11+
### Fixed
12+
13+
- **Prompt cancel aborts the whole template run:** When the user cancels a Command Bar prompt (including **batched** `showForm`), `render` / `renderFrontmatter` processing now returns **`null`** instead of an empty string. **`templateNew`** and related commands stop before `DataStore.newNote`; **`checkAndProcessFolderAndNewNoteTitle`** does not trash the empty starter note when **`templateNew`** returns no filename. **`insertNoteTemplate`** (Meeting Notes) and **Forms** `processCreateNew` bail out when **`render`** is cancelled.
14+
15+
### Changed
16+
17+
- **API:** `NPTemplating.render`, exported `render` / `renderTemplate`, and Template Runner `renderTemplate` may resolve to **`null`** on user cancel (callers should treat **`null`** as abort, distinct from **`''`**).
18+
19+
### Edited in this release
20+
21+
- `np.Templating/lib/rendering/templateProcessor.js`, `np.Templating/lib/NPTemplating.js`, `np.Templating/src/Templating.js`, `np.Templating/src/NPTemplateRunner.js`
22+
- `helpers/NPEditor.js`, `np.MeetingNotes/src/NPMeetingNotes.js`, `dwertheimer.Forms/src/formSubmission.js`
23+
- `np.Templating/__tests__/prompt-cancellation.test.js`
24+
- `np.Templating/CHANGELOG.md`, `np.Templating/plugin.json` — version **2.4.5**
25+
26+
## [2.4.4] 2026-05-17 @dwertheimer
27+
28+
### Fixed
29+
30+
- **Command Bar batched `prompt('Question?', […])` fields:** `parseParameters` treats the question as `varName` and the choice list as `promptMessage`, so `showForm` used **Answer** for `title`/`label`. Batch field labels now take the first quoted argument when `promptMessage` is an array or when it equals the comma-joined choice list (the same mis-parse as a string). The first-argument scanner reads quoted strings properly so labels include apostrophes inside double-quoted text (e.g. `"What's the priority?"`) and commas inside the question.
31+
32+
### Added
33+
34+
- **Tests (`promptFormBatch.test.js`):** Batched form labels for `prompt(varName, 'Message?', […])`, double-quoted questions, `<%` execution tags, **apostrophes inside `"…"`**, and **commas inside the question** string.
35+
36+
### Edited in this release
37+
38+
- `np.Templating/lib/support/modules/prompts/promptFormBatch.js`, `np.Templating/__tests__/promptFormBatch.test.js`
39+
- `np.Templating/CHANGELOG.md`, `np.Templating/plugin.json` — version **2.4.4**
40+
941
## [2.4.3] 2026-05-14 @dwertheimer
1042

1143
### Fixed

‎np.Templating/__tests__/prompt-cancellation.test.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('Prompt Cancellation Tests', () => {
3434
<%- var3 %>
3535
`
3636
const result = await NPTemplating.render(template)
37-
expect(result).toBe('')
37+
expect(result).toBeNull()
3838
})
3939

4040
test('should stop template rendering when a prompt is cancelled', async () => {
@@ -44,7 +44,7 @@ describe('Prompt Cancellation Tests', () => {
4444
<%- var3 %>
4545
`
4646
const result = await NPTemplating.render(template)
47-
expect(result).toBe('')
47+
expect(result).toBeNull()
4848
})
4949

5050
test('should handle frontmatter prompts cancellation', async () => {
@@ -56,7 +56,7 @@ var2: <%- var2 %>
5656
Content here
5757
`
5858
const result = await NPTemplating.render(template)
59-
expect(result).toBe('')
59+
expect(result).toBeNull()
6060
})
6161

6262
test('should handle mixed prompt types cancellation', async () => {
@@ -68,6 +68,6 @@ var1: <%- prompt('var1', 'This prompt will be cancelled') %>
6868
<%- prompt('var3', 'This prompt will be cancelled') %>
6969
`
7070
const result = await NPTemplating.render(template)
71-
expect(result).toBe('')
71+
expect(result).toBeNull()
7272
})
7373
})

‎np.Templating/__tests__/promptFormBatch.test.js‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,106 @@ describe('prompt form batch (CommandBar.showForm)', () => {
115115
expect(result.sessionData.city).toBe('Chicago')
116116
})
117117

118+
test('batched prompt(question, [choices]) uses the question as showForm label (not Answer)', async () => {
119+
global.CommandBar.showForm.mockResolvedValue({
120+
submitted: true,
121+
values: { Abstände_Review: '1w', Priority_prompt: 'A' },
122+
})
123+
124+
const template =
125+
"<%- prompt('Abstände Review?',['1w','2w']) %>" + "<%- prompt('Priority prompt?',['A','B']) %>"
126+
127+
const result = await processPrompts(template, {})
128+
expect(result).not.toBe(false)
129+
if (result === false) return
130+
expect(global.CommandBar.showForm).toHaveBeenCalledTimes(1)
131+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
132+
expect(formArg.fields[0].title).toBe('Abstände Review?')
133+
expect(formArg.fields[0].label).toBe('Abstände Review?')
134+
expect(formArg.fields[1].title).toBe('Priority prompt?')
135+
expect(formArg.fields[1].label).toBe('Priority prompt?')
136+
})
137+
138+
test('batched prompt(varName, message, [choices]) uses message as showForm label (not varName)', async () => {
139+
global.CommandBar.showForm.mockResolvedValue({
140+
submitted: true,
141+
values: { pick: 'A', Rhythm: '1w' },
142+
})
143+
144+
const template =
145+
"<%- prompt('pick', 'Pick one?', ['A','B']) %>" + "<%- prompt('Rhythm?', ['1w','2w']) %>"
146+
const result = await processPrompts(template, {})
147+
expect(result).not.toBe(false)
148+
if (result === false) return
149+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
150+
expect(formArg.fields[0].title).toBe('Pick one?')
151+
expect(formArg.fields[0].label).toBe('Pick one?')
152+
expect(formArg.fields[1].title).toBe('Rhythm?')
153+
})
154+
155+
test('batched prompt with double-quoted question uses that string as label', async () => {
156+
global.CommandBar.showForm.mockResolvedValue({
157+
submitted: true,
158+
values: { Double_quoted: 'a', pick: 'B' },
159+
})
160+
161+
const template = '<%- prompt("Double quoted?", ["a","b"]) %>' + "<%- prompt('pick', 'Second?', ['B','C']) %>"
162+
const result = await processPrompts(template, {})
163+
expect(result).not.toBe(false)
164+
if (result === false) return
165+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
166+
expect(formArg.fields[0].title).toBe('Double quoted?')
167+
expect(formArg.fields[1].title).toBe('Second?')
168+
})
169+
170+
test('batched <% execution %> prompt(question, [choices]) labels match <%- output %> form', async () => {
171+
global.CommandBar.showForm.mockResolvedValue({
172+
submitted: true,
173+
values: { Q: 'a', Y: 'c' },
174+
})
175+
176+
const template = "<% prompt('Q?', ['a','b']) %><% prompt('Y?', ['c','d']) %>"
177+
const result = await processPrompts(template, {})
178+
expect(result).not.toBe(false)
179+
if (result === false) return
180+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
181+
expect(formArg.fields[0].title).toBe('Q?')
182+
expect(formArg.fields[1].title).toBe('Y?')
183+
})
184+
185+
test('batched prompt("What\'s the priority?", […]) uses full question as label (apostrophe inside double quotes)', async () => {
186+
global.CommandBar.showForm.mockResolvedValue({
187+
submitted: true,
188+
values: { What_s_the_priority: 'a', Other: 'c' },
189+
})
190+
191+
const template =
192+
`<%- prompt("What's the priority?", ['a','b']) %>` + "<%- prompt('Other?', ['c','d']) %>"
193+
const result = await processPrompts(template, {})
194+
expect(result).not.toBe(false)
195+
if (result === false) return
196+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
197+
expect(formArg.fields[0].title).toBe("What's the priority?")
198+
expect(formArg.fields[0].label).toBe("What's the priority?")
199+
expect(formArg.fields[1].title).toBe('Other?')
200+
})
201+
202+
test('batched prompt(\'Comma, in, question?\', […]) uses full first argument as label', async () => {
203+
global.CommandBar.showForm.mockResolvedValue({
204+
submitted: true,
205+
values: { One_two_three: 'a', Z: 'x' },
206+
})
207+
208+
const template =
209+
"<%- prompt('One, two, three?', ['a','b']) %>" + "<%- prompt('Z?', ['x','y']) %>"
210+
const result = await processPrompts(template, {})
211+
expect(result).not.toBe(false)
212+
if (result === false) return
213+
const formArg = global.CommandBar.showForm.mock.calls[0][0]
214+
expect(formArg.fields[0].title).toBe('One, two, three?')
215+
expect(formArg.fields[1].title).toBe('Z?')
216+
})
217+
118218
test('non-prompt tag between prompts breaks contiguity — no showForm', async () => {
119219
global.CommandBar.showForm.mockResolvedValue({
120220
submitted: true,

‎np.Templating/lib/NPTemplating.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,9 @@ class NPTemplating {
202202
* @param {string} inputTemplateData - The template content to render
203203
* @param {any} [userData={}] - User data to use in template rendering
204204
* @param {any} [userOptions={}] - Options for template rendering
205-
* @returns {Promise<string>} A promise that resolves to the rendered template content
205+
* @returns {Promise<string | null>} Rendered template, or **null** if the user cancels a prompt
206206
*/
207-
static async render(inputTemplateData: string, userData: any = {}, userOptions: any = {}): Promise<string> {
207+
static async render(inputTemplateData: string, userData: any = {}, userOptions: any = {}): Promise<string | null> {
208208
try {
209209
await this.setup()
210210
return render(inputTemplateData, userData, userOptions, this.templateConfig)
@@ -221,9 +221,9 @@ class NPTemplating {
221221
* @param {string} [templateName=''] - The name of the template to render
222222
* @param {any} [userData={}] - User data to use in template rendering
223223
* @param {any} [userOptions={}] - Options for template rendering
224-
* @returns {Promise<string>} A promise that resolves to the rendered template content
224+
* @returns {Promise<string | null>}
225225
*/
226-
static async renderTemplate(templateName: string = '', userData: any = {}, userOptions: any = {}): Promise<string> {
226+
static async renderTemplate(templateName: string = '', userData: any = {}, userOptions: any = {}): Promise<string | null> {
227227
try {
228228
await this.setup()
229229
return renderTemplateByName(templateName, userData, userOptions)

0 commit comments

Comments
 (0)