Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions dwertheimer.Forms/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@

See Plugin [README](https://github.com/NotePlan/plugins/blob/main/dwertheimer.Forms/README.md) for details on available commands and use case.

## [1.0.17] 2026-01-25 @dwertheimer

### Fixed
- **SearchableChooser Templating Field Filter**: Fixed SearchableChooser to automatically filter out options containing templating fields (e.g., containing "<%") by default. This prevents templating syntax from appearing in frontmatter key chooser and other dropdown option lists.
- **SearchableChooser Manual Entry Indicator**: Fixed issue where the pencil icon (manual entry indicator) was incorrectly appearing in empty/blank fields. The indicator now only appears when a non-empty value has been entered that is not in the items list, and only after the items list has finished loading.
- **Frontmatter Key Values Filtering**: Fixed `getFrontmatterKeyValues` to filter out templating syntax values (containing "<%") at the source, preventing templating errors when forms load. Templating syntax values are now excluded from frontmatter key chooser dropdowns.
- **ContainedMultiSelectChooser Create Mode**: Fixed issue where ContainedMultiSelectChooser was not allowing creation of new items when the list was empty. Now allows creating new items even when `items.length === 0`, as long as `allowCreate` is true and there's a search term with no matches.

### Changed
- **GenericDatePicker Calendar Auto-Close**: Improved date picker UX by automatically closing the calendar picker immediately after selecting a date. Previously, users had to click the date and then click outside the picker to close it. Now a single click on a date both selects it and closes the calendar.
- **SearchableChooser Debug Logging**: Added comprehensive debug logging to SearchableChooser to help diagnose manual entry indicator issues. Logs include value checks, placeholder matching, and manual entry determination logic.
- **FormBuilder Create-New Mode Fields**: Split "Content to Insert" into two separate fields when processing method is "Create New Note":
- **New Note Frontmatter**: Separate field for frontmatter content (saved to `template:ignore newNoteFrontmatter` codeblock)
- **New Note Body Content**: Renamed from "Content to Insert" to clarify it's the body content (saved to `template:ignore templateBody` codeblock)
- Frontmatter and body content are automatically combined with `--` delimiters when sending to TemplateRunner
- Fields are ordered with Frontmatter above Body Content for better workflow
- **TemplateTagEditor Raw Mode**: All template tag editor fields (NewNoteTitle, Content to Insert, New Note Frontmatter, New Note Body Content) now default to raw mode with the toggle hidden, showing monospace text directly instead of pill/chip display for better readability

## [1.0.16] 2026-01-19 @dwertheimer

### Added
Expand Down
4 changes: 2 additions & 2 deletions dwertheimer.Forms/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"noteplan.minAppVersion": "3.4.0",
"plugin.id": "dwertheimer.Forms",
"plugin.name": "📝 Template Forms",
"plugin.version": "1.0.16",
"plugin.version": "1.0.17",
"plugin.releaseStatus": "beta",
"plugin.lastUpdateInfo": "Added NoteChooser output format options (title/filename for multi-select and single-select), advanced filtering (startFolder, includeRegex, excludeRegex), and SearchableChooser shortDescription optimization. Reverted compact label width to 10rem while keeping input width at 360px.",
"plugin.lastUpdateInfo": "Fixed SearchableChooser to filter out templating fields (containing '<%') by default and fixed pencil icon appearing incorrectly for empty/blank values.",
"plugin.description": "Dynamic Forms for NotePlan using Templating -- fill out a multi-field form and have the data sent to a template for processing",
"plugin.author": "dwertheimer",
"plugin.requiredFiles": ["react.c.FormView.bundle.dev.js", "react.c.FormBuilderView.bundle.dev.js", "react.c.FormBrowserView.bundle.dev.js"],
Expand Down
25 changes: 20 additions & 5 deletions dwertheimer.Forms/src/NPTemplateForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import pluginJson from '../plugin.json'
import { type PassedData } from './shared/types.js'
// Note: getAllNotesAsOptions is no longer used here - FormView loads notes dynamically via requestFromPlugin
import { testRequestHandlers, updateFormLinksInNote, removeEmptyLinesFromNote } from './requestHandlers'
import { loadTemplateBodyFromTemplate, loadTemplateRunnerArgsFromTemplate, loadCustomCSSFromTemplate, getFormTemplateList, findDuplicateFormTemplates } from './templateIO.js'
import { loadTemplateBodyFromTemplate, loadTemplateRunnerArgsFromTemplate, loadCustomCSSFromTemplate, loadNewNoteFrontmatterFromTemplate, getFormTemplateList, findDuplicateFormTemplates } from './templateIO.js'
import { openFormWindow, openFormBuilderWindow, getFormBrowserWindowId, getFormBuilderWindowId, getFormWindowId } from './windowManagement.js'
import { log, logError, logDebug, logWarn, timer, clo, JSP, logInfo } from '@helpers/dev'
import { showMessage } from '@helpers/userInput'
Expand All @@ -15,7 +15,7 @@ import { waitForCondition } from '@helpers/promisePolyfill'
import NPTemplating from 'NPTemplating'
import { getNoteByFilename } from '@helpers/note'
import { validateObjectString, parseObjectString } from '@helpers/stringTransforms'
import { updateFrontMatterVars, ensureFrontmatter, noteHasFrontMatter, getFrontmatterAttributes } from '@helpers/NPFrontMatter'
import { updateFrontMatterVars, ensureFrontmatter, noteHasFrontMatter, getFrontmatterAttributes, getSanitizedFmParts } from '@helpers/NPFrontMatter'
import { loadCodeBlockFromNote } from '@helpers/codeBlocks'
import { parseTeamspaceFilename } from '@helpers/teamspace'
import { getFolderFromFilename } from '@helpers/folders'
Expand Down Expand Up @@ -273,9 +273,18 @@ export async function openTemplateForm(templateTitle?: string): Promise<void> {
}
}

//TODO: we may not need this step, ask @codedungeon what he thinks
// for now, we'll call renderFrontmatter() via DataStore.invokePluginCommandByName()
const { _, frontmatterAttributes } = await DataStore.invokePluginCommandByName('renderFrontmatter', 'np.Templating', [templateData])
// Parse frontmatter WITHOUT rendering templating syntax during form initialization
// Templating syntax in frontmatter attributes will be rendered later when form is submitted
// Use getFrontmatterAttributes to get parsed but unrendered frontmatter attributes
// This prevents errors when frontmatter contains templating syntax referencing form fields that don't exist yet
let frontmatterAttributes = getFrontmatterAttributes(templateNote) || {}

// If frontmatterAttributes is empty, try parsing from templateData directly (without rendering)
if (!frontmatterAttributes || Object.keys(frontmatterAttributes).length === 0) {
// Fallback: parse frontmatter from templateData without rendering
const fmParts = getSanitizedFmParts(templateData, false)
frontmatterAttributes = fmParts.attributes || {}
}

// Load TemplateRunner processing variables from codeblock (not frontmatter)
// These contain template tags that reference form field values and should not be processed during form opening
Expand All @@ -299,6 +308,12 @@ export async function openTemplateForm(templateTitle?: string): Promise<void> {
frontmatterAttributes.customCSS = ''
}

// Load new note frontmatter from codeblock
const newNoteFrontmatterFromCodeblock = await loadNewNoteFrontmatterFromTemplate(templateNote)
if (newNoteFrontmatterFromCodeblock) {
frontmatterAttributes.newNoteFrontmatter = newNoteFrontmatterFromCodeblock
}

// Load TemplateRunner args from codeblock
const templateRunnerArgs = await loadTemplateRunnerArgsFromTemplate(templateNote)
if (templateRunnerArgs) {
Expand Down
1 change: 1 addition & 0 deletions dwertheimer.Forms/src/ProcessingTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const templateBodyCodeBlockType = 'template:ignore templateBody'
export const templateRunnerArgsCodeBlockType = 'template:ignore templateRunnerArgs'
export const templateJSCodeBlockType = 'template:ignore templateJS'
export const customCSSCodeBlockType = 'template:ignore customCSS'
export const newNoteFrontmatterCodeBlockType = 'template:ignore newNoteFrontmatter'

/**
* Create a form processing template (standalone command or called from Form Builder)
Expand Down
5 changes: 5 additions & 0 deletions dwertheimer.Forms/src/components/FormBuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type FormBuilderProps = {
y?: ?number | ?string,
templateBody?: string, // Load from codeblock
customCSS?: string, // Load from codeblock
newNoteFrontmatter?: string, // Load from codeblock
templateRunnerArgs?: { [key: string]: any }, // TemplateRunner processing variables (loaded from codeblock)
isNewForm?: boolean,
templateTitle?: string,
Expand All @@ -57,6 +58,7 @@ export function FormBuilder({
y,
templateBody = '', // Load from codeblock
customCSS = '', // Load from codeblock
newNoteFrontmatter = '', // Load from codeblock
templateRunnerArgs = {}, // TemplateRunner processing variables (loaded from codeblock)
isNewForm = false,
templateTitle = '',
Expand Down Expand Up @@ -146,6 +148,7 @@ You can edit or delete this comment field - it's just a note to help you get sta
// Option B: Create new note (defaults)
newNoteTitle: '',
newNoteFolder: '',
newNoteFrontmatter: '', // Frontmatter for new note (saved to codeblock)
// Option C: Form processor
formProcessorTitle: cleanedReceivingTemplateTitle, // Set to receivingTemplateTitle for backward compatibility
// Space selection (empty string = Private, teamspace ID = Teamspace)
Expand All @@ -155,6 +158,8 @@ You can edit or delete this comment field - it's just a note to help you get sta
templateBody: templateBody || '',
// Custom CSS (loaded from codeblock)
customCSS: customCSS || '',
// New note frontmatter (loaded from codeblock)
newNoteFrontmatter: newNoteFrontmatter || '',
}

// Merge TemplateRunner args from codeblock (these override defaults)
Expand Down
1 change: 1 addition & 0 deletions dwertheimer.Forms/src/components/FormBuilderView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export function WebView({ data, dispatch, reactSettings, setReactSettings, onSub
y={y}
templateBody={pluginData.templateBody || ''} // Load from codeblock
customCSS={pluginData.customCSS || ''} // Load from codeblock
newNoteFrontmatter={pluginData.newNoteFrontmatter || ''} // Load from codeblock
isNewForm={isNewForm}
templateTitle={templateTitle}
templateFilename={templateFilename}
Expand Down
90 changes: 88 additions & 2 deletions dwertheimer.Forms/src/components/ProcessingMethodSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ export function ProcessingMethodSection({
minRows={5}
maxRows={15}
fields={fields.filter((f) => f.key && f.type !== 'separator' && f.type !== 'heading')}
defaultRawMode={true}
hideRawToggle={true}
actionButtons={
<>
<button
Expand Down Expand Up @@ -468,6 +470,8 @@ export function ProcessingMethodSection({
minRows={2}
maxRows={5}
fields={fields.filter((f) => f.key && f.type !== 'separator' && f.type !== 'heading')}
defaultRawMode={true}
hideRawToggle={true}
actionButtons={
<>
<button
Expand Down Expand Up @@ -529,8 +533,8 @@ export function ProcessingMethodSection({
</div>
<div className="frontmatter-field" style={{ marginTop: '1rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
Content to Insert:
<InfoIcon text="The template content that will be used to create the new note. Click +Field or +Date buttons to insert tags. Or use template tags like <%- fieldKey %> to insert form field values, or <%- date.format('YYYY-MM-DD') %> for dates." />
New Note Body Content:
<InfoIcon text="The body content that will be used to create the new note. Click +Field or +Date buttons to insert tags. Or use template tags like <%- fieldKey %> to insert form field values, or <%- date.format('YYYY-MM-DD') %> for dates." />
</label>
<TemplateTagEditor
value={frontmatter.templateBody || ''}
Expand All @@ -546,6 +550,8 @@ export function ProcessingMethodSection({
minRows={5}
maxRows={15}
fields={fields.filter((f) => f.key && f.type !== 'separator' && f.type !== 'heading')}
defaultRawMode={true}
hideRawToggle={true}
actionButtons={
<>
<button
Expand Down Expand Up @@ -606,6 +612,86 @@ export function ProcessingMethodSection({
date.format(&quot;YYYY-MM-DD&quot;) %&gt; for dates.
</div>
</div>
<div className="frontmatter-field" style={{ marginTop: '1rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
New Note Frontmatter:
<InfoIcon text="Frontmatter to add to the new note. Use template tags like <%- fieldKey %> for form field values. This will be saved to the ```template:ignore newNoteFrontmatter``` codeblock." />
</label>
<TemplateTagEditor
value={frontmatter.newNoteFrontmatter || ''}
onChange={(value) => onFrontmatterChange('newNoteFrontmatter', value)}
onFocus={(e) => {
const target = e.target
if (target instanceof HTMLTextAreaElement) {
setTagInserterInputRef(target)
setTagInserterFieldKey('newNoteFrontmatter')
}
}}
placeholder="e.g., travel_mode: <%- travel_mode %>\ntravel_start: <%- travel_start ? date.format('YYYY-MM-DD', travel_start) : '' %>"
minRows={3}
maxRows={10}
fields={fields.filter((f) => f.key && f.type !== 'separator' && f.type !== 'heading')}
defaultRawMode={true}
hideRawToggle={true}
actionButtons={
<>
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleTagInserterButtonClick(e, 'field', 'newNoteFrontmatter')
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
backgroundColor: '#f0f0f0',
border: '1px solid #ccc',
borderRadius: '3px',
cursor: 'pointer',
position: 'relative',
zIndex: 100,
}}
title="Insert field variable"
>
+ Field
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleTagInserterButtonClick(e, 'date', 'newNoteFrontmatter')
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
backgroundColor: '#f0f0f0',
border: '1px solid #ccc',
borderRadius: '3px',
cursor: 'pointer',
position: 'relative',
zIndex: 100,
}}
title="Insert date format"
>
+ Date
</button>
</>
}
/>
<div style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.25rem', fontStyle: 'italic' }}>
Frontmatter content for the new note. Use template tags like &lt;%- fieldKey %&gt; for form fields. This will be saved to the ```template:ignore newNoteFrontmatter``` codeblock.
</div>
</div>
</>
)}

Expand Down
Loading