From 3dd110929979ca5a4a0f5a7caf2e536167e1e290 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 10:36:08 -0800 Subject: [PATCH 01/12] Fix SearchableChooser: filter templating fields and fix manual entry indicator - Add default filter to screen out options containing templating fields ('<%') - Fix pencil icon appearing incorrectly for empty/blank values - Update plugin version to 1.0.17 --- dwertheimer.Forms/CHANGELOG.md | 6 ++++++ dwertheimer.Forms/plugin.json | 4 ++-- .../react/DynamicDialog/SearchableChooser.jsx | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/dwertheimer.Forms/CHANGELOG.md b/dwertheimer.Forms/CHANGELOG.md index eadb8417f..8b13fed6f 100644 --- a/dwertheimer.Forms/CHANGELOG.md +++ b/dwertheimer.Forms/CHANGELOG.md @@ -4,6 +4,12 @@ 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. + ## [1.0.16] 2026-01-19 @dwertheimer ### Added diff --git a/dwertheimer.Forms/plugin.json b/dwertheimer.Forms/plugin.json index f1137aaf7..ff9517d8c 100644 --- a/dwertheimer.Forms/plugin.json +++ b/dwertheimer.Forms/plugin.json @@ -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"], diff --git a/helpers/react/DynamicDialog/SearchableChooser.jsx b/helpers/react/DynamicDialog/SearchableChooser.jsx index 8417d5014..17b91ab5b 100644 --- a/helpers/react/DynamicDialog/SearchableChooser.jsx +++ b/helpers/react/DynamicDialog/SearchableChooser.jsx @@ -179,7 +179,7 @@ export function SearchableChooser({ // } // }, [items, isOpen, filteredItems.length, debugLogging, fieldType, getDisplayValue]) - // Filter items: first apply itemFilter (if provided), then apply search filter + // Filter items: first apply itemFilter (if provided), then apply default templating filter, then apply search filter useEffect(() => { // Apply itemFilter first (if provided) - this filters items regardless of search term let preFilteredItems = items @@ -187,6 +187,13 @@ export function SearchableChooser({ preFilteredItems = items.filter((item: any) => itemFilter(item)) } + // Apply default filter to screen out templating fields (containing "<%") + // This prevents templating syntax from appearing in option lists + preFilteredItems = preFilteredItems.filter((item: any) => { + const optionText = getOptionText(item) + return !optionText.includes('<%') + }) + // Then apply search filter if there's a search term if (!searchTerm.trim()) { setFilteredItems(preFilteredItems) @@ -194,7 +201,7 @@ export function SearchableChooser({ const filtered = preFilteredItems.filter((item: any) => filterFn(item, searchTerm)) setFilteredItems(filtered) } - }, [searchTerm, items, filterFn, itemFilter]) + }, [searchTerm, items, filterFn, itemFilter, getOptionText]) // Scroll highlighted item into view when hoveredIndex changes useEffect(() => { @@ -501,8 +508,12 @@ export function SearchableChooser({ let isManualEntryValue = false // Check if current value is a manual entry - if (allowManualEntry && displayValue && isManualEntry) { - isManualEntryValue = isManualEntry(displayValue, items) + // Don't show manual entry indicator for empty/blank values + if (allowManualEntry && displayValue && displayValue.trim() !== '' && isManualEntry) { + // Don't show manual entry indicator if items list is empty (still loading) + if (items && items.length > 0) { + isManualEntryValue = isManualEntry(displayValue, items) + } } if (displayValue && items && items.length > 0 && !isManualEntryValue) { From 73559124c3874097d9c33637343b1b3168235ce9 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:19:41 -0800 Subject: [PATCH 02/12] Fix templating syntax filtering and manual entry indicator - Filter templating syntax values (containing '<%') at source in getFrontmatterKeyValues to prevent templating errors when forms load - Add comprehensive debug logging to SearchableChooser for manual entry indicator diagnosis - Update changelog with all recent changes for version 1.0.17 --- dwertheimer.Forms/CHANGELOG.md | 5 +++ dwertheimer.Forms/src/requestHandlers.js | 10 ++++- .../react/DynamicDialog/SearchableChooser.jsx | 43 +++++++++++++++++-- .../getFrontmatterKeyValues.js | 10 ++++- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/dwertheimer.Forms/CHANGELOG.md b/dwertheimer.Forms/CHANGELOG.md index 8b13fed6f..9cdb0ce20 100644 --- a/dwertheimer.Forms/CHANGELOG.md +++ b/dwertheimer.Forms/CHANGELOG.md @@ -9,6 +9,11 @@ See Plugin [README](https://github.com/NotePlan/plugins/blob/main/dwertheimer.Fo ### 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. + +### 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. ## [1.0.16] 2026-01-19 @dwertheimer diff --git a/dwertheimer.Forms/src/requestHandlers.js b/dwertheimer.Forms/src/requestHandlers.js index b9012fae5..6261b2e66 100644 --- a/dwertheimer.Forms/src/requestHandlers.js +++ b/dwertheimer.Forms/src/requestHandlers.js @@ -221,7 +221,15 @@ export async function getFrontmatterKeyValues(params: { const values = await getValuesForFrontmatterTag(params.frontmatterKey, noteType, caseSensitive, folderString, fullPathMatch) // Convert all values to strings (frontmatter values can be various types) - const stringValues = values.map((v: any) => String(v)) + let stringValues = values.map((v: any) => String(v)) + + // Filter out templating syntax values (containing "<%") - these are template code, not actual values + // This prevents templating errors when forms load and process frontmatter + const beforeFilterCount = stringValues.length + stringValues = stringValues.filter((v: string) => !v.includes('<%')) + if (beforeFilterCount !== stringValues.length) { + logDebug(pluginJson, `[DIAG] getFrontmatterKeyValues: Filtered out ${beforeFilterCount - stringValues.length} templating syntax values`) + } const totalElapsed: number = Date.now() - startTime logDebug(pluginJson, `[DIAG] getFrontmatterKeyValues COMPLETE: totalElapsed=${totalElapsed}ms, found=${stringValues.length} values for key "${params.frontmatterKey}"`) diff --git a/helpers/react/DynamicDialog/SearchableChooser.jsx b/helpers/react/DynamicDialog/SearchableChooser.jsx index 17b91ab5b..d85097ae7 100644 --- a/helpers/react/DynamicDialog/SearchableChooser.jsx +++ b/helpers/react/DynamicDialog/SearchableChooser.jsx @@ -508,11 +508,48 @@ export function SearchableChooser({ let isManualEntryValue = false // Check if current value is a manual entry - // Don't show manual entry indicator for empty/blank values - if (allowManualEntry && displayValue && displayValue.trim() !== '' && isManualEntry) { + // Don't show manual entry indicator for empty/blank values or placeholder text + const trimmedDisplayValue = displayValue ? displayValue.trim() : '' + const isPlaceholderValue = placeholder && trimmedDisplayValue === placeholder.trim() + + if (allowManualEntry && trimmedDisplayValue !== '' && !isPlaceholderValue && isManualEntry) { // Don't show manual entry indicator if items list is empty (still loading) if (items && items.length > 0) { - isManualEntryValue = isManualEntry(displayValue, items) + // DEBUG: Log manual entry check details + console.log(`[SearchableChooser:${fieldType}] Manual entry check:`, { + value: `"${value}"`, + displayValue: `"${displayValue}"`, + trimmedDisplayValue: `"${trimmedDisplayValue}"`, + isPlaceholderValue, + placeholder: `"${placeholder}"`, + allowManualEntry, + hasIsManualEntryFn: !!isManualEntry, + itemsLength: items.length, + isOpen, + }) + const manualEntryResult = isManualEntry(trimmedDisplayValue, items) + console.log(`[SearchableChooser:${fieldType}] isManualEntry returned:`, manualEntryResult) + isManualEntryValue = manualEntryResult + console.log(`[SearchableChooser:${fieldType}] Final isManualEntryValue:`, isManualEntryValue, `(will show pencil: ${isManualEntryValue && !isOpen})`) + } else { + console.log(`[SearchableChooser:${fieldType}] Skipping manual entry check - items list empty or loading:`, { + itemsLength: items?.length || 0, + hasItems: !!items, + }) + } + } else { + // DEBUG: Log why manual entry check was skipped + if (allowManualEntry && isManualEntry) { + console.log(`[SearchableChooser:${fieldType}] Skipping manual entry check:`, { + allowManualEntry, + hasDisplayValue: !!displayValue, + displayValue: `"${displayValue}"`, + trimmedDisplayValue: trimmedDisplayValue || 'N/A', + isEmpty: trimmedDisplayValue === '', + isPlaceholderValue, + placeholder: `"${placeholder}"`, + hasIsManualEntryFn: !!isManualEntry, + }) } } diff --git a/np.Shared/src/requestHandlers/getFrontmatterKeyValues.js b/np.Shared/src/requestHandlers/getFrontmatterKeyValues.js index 4fb005ad7..cd6f7415d 100644 --- a/np.Shared/src/requestHandlers/getFrontmatterKeyValues.js +++ b/np.Shared/src/requestHandlers/getFrontmatterKeyValues.js @@ -56,7 +56,15 @@ export async function getFrontmatterKeyValues( const values = await getValuesForFrontmatterTag(params.frontmatterKey, noteType, caseSensitive, folderString, fullPathMatch) // Convert all values to strings (frontmatter values can be various types) - const stringValues = values.map((v: any) => String(v)) + let stringValues = values.map((v: any) => String(v)) + + // Filter out templating syntax values (containing "<%") - these are template code, not actual values + // This prevents templating errors when forms load and process frontmatter + const beforeFilterCount = stringValues.length + stringValues = stringValues.filter((v: string) => !v.includes('<%')) + if (beforeFilterCount !== stringValues.length) { + logDebug(pluginJson, `[np.Shared/requestHandlers] getFrontmatterKeyValues: Filtered out ${beforeFilterCount - stringValues.length} templating syntax values`) + } const totalElapsed: number = Date.now() - startTime logDebug(pluginJson, `[np.Shared/requestHandlers] getFrontmatterKeyValues COMPLETE: totalElapsed=${totalElapsed}ms, found=${stringValues.length} values for key "${params.frontmatterKey}"`) From 7e9b6693531879eb895cd716bb90d67e36a58736 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:31:44 -0800 Subject: [PATCH 03/12] Fix form initialization: parse frontmatter without rendering templating syntax - Changed from renderFrontmatter() to getFrontmatterAttributes() to parse frontmatter without rendering - This prevents templating errors during form initialization when frontmatter contains templating syntax referencing form fields - Templating syntax in frontmatter attributes will be rendered later when form is submitted with actual values --- dwertheimer.Forms/src/NPTemplateForm.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dwertheimer.Forms/src/NPTemplateForm.js b/dwertheimer.Forms/src/NPTemplateForm.js index 541f4d366..0aa5e7444 100644 --- a/dwertheimer.Forms/src/NPTemplateForm.js +++ b/dwertheimer.Forms/src/NPTemplateForm.js @@ -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' @@ -273,9 +273,18 @@ export async function openTemplateForm(templateTitle?: string): Promise { } } - //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 From cfa2ef261b2c2a496709642c7074362583541b3f Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:35:41 -0800 Subject: [PATCH 04/12] Fix create mode not showing when all items filtered out - Changed condition from filteredItems.length > 0 to items.length > 0 - This allows create mode to show even when all items are filtered out (e.g., by templating syntax filter) - Users can now create new items when typing something not in the list, even if existing items were filtered out --- .../DynamicDialog/ContainedMultiSelectChooser.jsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/helpers/react/DynamicDialog/ContainedMultiSelectChooser.jsx b/helpers/react/DynamicDialog/ContainedMultiSelectChooser.jsx index ea44b3d65..860936045 100644 --- a/helpers/react/DynamicDialog/ContainedMultiSelectChooser.jsx +++ b/helpers/react/DynamicDialog/ContainedMultiSelectChooser.jsx @@ -264,9 +264,18 @@ export function ContainedMultiSelectChooser({ // Show create mode automatically when search has no matches and allowCreate is true // Skip create mode when "is:checked" filter is active useEffect(() => { - logDebug('ContainedMultiSelectChooser', `[CREATE MODE] Effect triggered: searchTerm="${searchTerm}", displayItems.length=${displayItems.length}, filteredItems.length=${filteredItems.length}, showCreateMode=${String(showCreateMode)}, showCheckedOnly=${String(showCheckedOnly)}`) + logDebug('ContainedMultiSelectChooser', `[CREATE MODE] Effect triggered: searchTerm="${searchTerm}", displayItems.length=${displayItems.length}, filteredItems.length=${filteredItems.length}, items.length=${items.length}, showCreateMode=${String(showCreateMode)}, showCheckedOnly=${String(showCheckedOnly)}`) - if (allowCreate && searchTerm.trim() && searchTerm.toLowerCase() !== 'is:checked' && !showCheckedOnly && displayItems.length === 0 && filteredItems.length > 0) { + // Allow create mode when: + // 1. allowCreate is true + // 2. There's a search term (not empty) + // 3. Search term is not "is:checked" + // 4. "is:checked" filter is not active + // 5. No display items match the search (displayItems.length === 0) + // 6. There are items available OR allowCreate is true (allow creation even if all items were filtered out) + // Note: We check items.length > 0 instead of filteredItems.length > 0 to allow creation even when + // all items are filtered out (e.g., by templating syntax filter) + if (allowCreate && searchTerm.trim() && searchTerm.toLowerCase() !== 'is:checked' && !showCheckedOnly && displayItems.length === 0 && items.length > 0) { // No matches found for the search term, show create mode with the search term pre-filled if (!showCreateMode) { logDebug('ContainedMultiSelectChooser', `[CREATE MODE] Auto-showing create mode with searchTerm="${searchTerm.trim()}"`) @@ -279,7 +288,7 @@ export function ContainedMultiSelectChooser({ setShowCreateMode(false) setCreateValue('') } - }, [displayItems.length, searchTerm, filteredItems.length, allowCreate, showCreateMode, showCheckedOnly]) + }, [displayItems.length, searchTerm, filteredItems.length, items.length, allowCreate, showCreateMode, showCheckedOnly]) // Handle checkbox toggle (multi-select) or item selection (single-value) const handleToggle = (itemName: string) => { From 5d9edd993ff3f8deb981619329841c3972decc5c Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:41:35 -0800 Subject: [PATCH 05/12] Restore missing features: raw mode defaults, newNoteFrontmatter field, remove replacers - Added defaultRawMode and hideRawToggle props to TemplateTagEditor - Removed displayTextWithSpaces function (no more and replacers) - Show text directly with whitespace preserved - Added newNoteFrontmatter field to ProcessingMethodSection (create-new section) - Set newNoteTitle and newNoteFrontmatter to use raw mode by default with hidden toggle - Calendar picker auto-close already implemented in NoteChooser --- .../src/components/FormBuilder.jsx | 1 + .../components/ProcessingMethodSection.jsx | 82 +++++++++++++++++++ .../src/components/TemplateTagEditor.jsx | 69 ++++++++-------- 3 files changed, 120 insertions(+), 32 deletions(-) diff --git a/dwertheimer.Forms/src/components/FormBuilder.jsx b/dwertheimer.Forms/src/components/FormBuilder.jsx index 344369533..eeb6140e8 100644 --- a/dwertheimer.Forms/src/components/FormBuilder.jsx +++ b/dwertheimer.Forms/src/components/FormBuilder.jsx @@ -146,6 +146,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) diff --git a/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx b/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx index 7cb389a20..ee317c438 100644 --- a/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx +++ b/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx @@ -468,6 +468,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={ <> + + + } + /> +
+ Frontmatter content for the new note. Use template tags like <%- fieldKey %> for form fields. This will be saved to the ```template:ignore newNoteFrontmatter``` codeblock. +
+ )} diff --git a/dwertheimer.Forms/src/components/TemplateTagEditor.jsx b/dwertheimer.Forms/src/components/TemplateTagEditor.jsx index 2c4c6a3b0..0bdd5e22b 100644 --- a/dwertheimer.Forms/src/components/TemplateTagEditor.jsx +++ b/dwertheimer.Forms/src/components/TemplateTagEditor.jsx @@ -28,6 +28,8 @@ export type TemplateTagEditorProps = { className?: string, style?: { [key: string]: any }, actionButtons?: React$Node, // Buttons to display in the toggle area + defaultRawMode?: boolean, // If true, start in raw mode (default: false) + hideRawToggle?: boolean, // If true, hide the raw mode toggle switch (default: false) } /** @@ -127,26 +129,7 @@ function reconstructText(pills: Array): string { return pills.map((pill) => (pill.type === 'tag' ? pill.content : pill.content)).join('') } -/** - * Replace leading and trailing spaces with visible space indicator for display - * Also replaces newlines with indicators - * Only shows space indicators at the start or end of text, not in the middle - * @param {string} text - The text to process - * @returns {string} Text with leading/trailing spaces replaced by "" and newlines by "" - */ -function displayTextWithSpaces(text: string): string { - // First, replace all newlines with - let processedText = text.replace(/\n/g, '') - - // Then replace leading spaces and trailing spaces separately - // Leading spaces: ^\s+ - // Trailing spaces: \s+$ - // Middle spaces are left as-is - processedText = processedText.replace(/^(\s+)/, (match) => ''.repeat(match.length)) - processedText = processedText.replace(/(\s+)$/, (match) => ''.repeat(match.length)) - - return processedText -} +// Removed displayTextWithSpaces function - now showing text directly with whitespace preserved /** * TemplateTagEditor Component @@ -164,8 +147,10 @@ export function TemplateTagEditor({ className = '', style = {}, actionButtons, + defaultRawMode = false, + hideRawToggle = false, }: TemplateTagEditorProps): React$Node { - const [showRaw, setShowRaw] = useState(false) + const [showRaw, setShowRaw] = useState(defaultRawMode) const [pills, setPills] = useState>([]) const [selectedPillId, setSelectedPillId] = useState(null) const [editingTextIndex, setEditingTextIndex] = useState(null) @@ -670,7 +655,7 @@ export function TemplateTagEditor({ style={{ cursor: isDragging ? 'text' : 'text', fontFamily: 'Menlo, monospace' }} > {showDropIndicatorBefore &&
} - {displayTextWithSpaces(pill.content)} + {pill.content} {showDropIndicatorAfter &&
}
{/* Drop zone for between pills - also clickable for text input */} @@ -724,9 +709,34 @@ export function TemplateTagEditor({ return (
- {/* Toggle switch for raw mode */} -
- {actionButtons && ( + {/* Toggle switch for raw mode - hidden if hideRawToggle is true */} + {!hideRawToggle && ( +
+ {actionButtons && ( +
{ + // Ensure button clicks work + e.stopPropagation() + }} + onMouseDown={(e) => { + // Ensure button clicks work + e.stopPropagation() + }} + > + {actionButtons} +
+ )} + +
+ )} + {/* Show action buttons even when toggle is hidden */} + {hideRawToggle && actionButtons && ( +
{ @@ -740,13 +750,8 @@ export function TemplateTagEditor({ > {actionButtons}
- )} - -
+
+ )} {/* Raw mode - simple textarea */} {showRaw ? ( From 2fb33f5eface52a09ba2c92e09ad5b3100440a3e Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:55:01 -0800 Subject: [PATCH 06/12] FormBuilder: Reorder fields and fix SelectableChooser create mode - Move New Note Frontmatter field above Content to Insert in create-new mode - Change 'Content to Insert' label to 'New Note Body Content' in create-new mode - Fix ContainedMultiSelectChooser to allow creating new items even when list is empty - Remove items.length > 0 requirement from create mode condition --- dwertheimer.Forms/src/NPTemplateForm.js | 8 +- dwertheimer.Forms/src/ProcessingTemplate.js | 1 + .../src/components/FormBuilder.jsx | 4 + .../src/components/FormBuilderView.jsx | 1 + .../components/ProcessingMethodSection.jsx | 8 +- dwertheimer.Forms/src/formBuilderHandlers.js | 9 + dwertheimer.Forms/src/formSubmission.js | 28 +- dwertheimer.Forms/src/templateIO.js | 66 ++- dwertheimer.Forms/src/windowManagement.js | 8 +- .../ContainedMultiSelectChooser.jsx | 6 +- .../react/DynamicDialog/GenericDatePicker.jsx | 33 +- np.Shared/src/installPluginVersion.js | 498 ++++++++++++++++++ 12 files changed, 653 insertions(+), 17 deletions(-) create mode 100644 np.Shared/src/installPluginVersion.js diff --git a/dwertheimer.Forms/src/NPTemplateForm.js b/dwertheimer.Forms/src/NPTemplateForm.js index 0aa5e7444..083180e44 100644 --- a/dwertheimer.Forms/src/NPTemplateForm.js +++ b/dwertheimer.Forms/src/NPTemplateForm.js @@ -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' @@ -308,6 +308,12 @@ export async function openTemplateForm(templateTitle?: string): Promise { 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) { diff --git a/dwertheimer.Forms/src/ProcessingTemplate.js b/dwertheimer.Forms/src/ProcessingTemplate.js index 9d9d67efd..6da339687 100644 --- a/dwertheimer.Forms/src/ProcessingTemplate.js +++ b/dwertheimer.Forms/src/ProcessingTemplate.js @@ -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) diff --git a/dwertheimer.Forms/src/components/FormBuilder.jsx b/dwertheimer.Forms/src/components/FormBuilder.jsx index eeb6140e8..711fe1b88 100644 --- a/dwertheimer.Forms/src/components/FormBuilder.jsx +++ b/dwertheimer.Forms/src/components/FormBuilder.jsx @@ -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, @@ -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 = '', @@ -156,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) diff --git a/dwertheimer.Forms/src/components/FormBuilderView.jsx b/dwertheimer.Forms/src/components/FormBuilderView.jsx index f8bab03bd..e245d0927 100644 --- a/dwertheimer.Forms/src/components/FormBuilderView.jsx +++ b/dwertheimer.Forms/src/components/FormBuilderView.jsx @@ -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} diff --git a/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx b/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx index ee317c438..7312fc628 100644 --- a/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx +++ b/dwertheimer.Forms/src/components/ProcessingMethodSection.jsx @@ -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={ <> From 7fcaff9d9ddde064b662acc3b68838c13a8067a7 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 11:58:51 -0800 Subject: [PATCH 09/12] Add CSS highlighting for create confirm button --- .../DynamicDialog/ContainedMultiSelectChooser.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/helpers/react/DynamicDialog/ContainedMultiSelectChooser.css b/helpers/react/DynamicDialog/ContainedMultiSelectChooser.css index 036ad458b..7efb3123a 100644 --- a/helpers/react/DynamicDialog/ContainedMultiSelectChooser.css +++ b/helpers/react/DynamicDialog/ContainedMultiSelectChooser.css @@ -201,6 +201,21 @@ background-color: var(--bg-disabled-color, #f5f5f5); } +/* Highlighted confirm button when in create mode */ +.contained-multi-select-create-confirm-btn-highlighted:not(:disabled) { + background-color: var(--tint-color, #dc8a78); + color: white; + border-color: var(--tint-color, #dc8a78); + font-weight: bold; + box-shadow: 0 0 0 2px rgba(220, 138, 120, 0.2); +} + +.contained-multi-select-create-confirm-btn-highlighted:not(:disabled):hover { + background-color: var(--tint-color, #dc8a78); + color: white; + box-shadow: 0 0 0 3px rgba(220, 138, 120, 0.3); +} + /* Select All/None buttons */ .contained-multi-select-select-all-btn, .contained-multi-select-select-none-btn { From 7ce648cfe04548125920fe1f46e2f492688a6658 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 12:01:13 -0800 Subject: [PATCH 10/12] Remove all console.log statements from SearchableChooser --- .../react/DynamicDialog/SearchableChooser.jsx | 99 +------------------ 1 file changed, 4 insertions(+), 95 deletions(-) diff --git a/helpers/react/DynamicDialog/SearchableChooser.jsx b/helpers/react/DynamicDialog/SearchableChooser.jsx index d85097ae7..ecf9bf8e6 100644 --- a/helpers/react/DynamicDialog/SearchableChooser.jsx +++ b/helpers/react/DynamicDialog/SearchableChooser.jsx @@ -366,9 +366,6 @@ export function SearchableChooser({ suppressOpenOnFocusRef.current = false return } - if (debugLogging) { - console.log(`${fieldType}: Input focused, opening dropdown. items=${items.length}, filteredItems=${filteredItems.length}`) - } if (!isOpen && onOpen) { onOpen() // Trigger lazy loading callback } @@ -515,113 +512,42 @@ export function SearchableChooser({ if (allowManualEntry && trimmedDisplayValue !== '' && !isPlaceholderValue && isManualEntry) { // Don't show manual entry indicator if items list is empty (still loading) if (items && items.length > 0) { - // DEBUG: Log manual entry check details - console.log(`[SearchableChooser:${fieldType}] Manual entry check:`, { - value: `"${value}"`, - displayValue: `"${displayValue}"`, - trimmedDisplayValue: `"${trimmedDisplayValue}"`, - isPlaceholderValue, - placeholder: `"${placeholder}"`, - allowManualEntry, - hasIsManualEntryFn: !!isManualEntry, - itemsLength: items.length, - isOpen, - }) const manualEntryResult = isManualEntry(trimmedDisplayValue, items) - console.log(`[SearchableChooser:${fieldType}] isManualEntry returned:`, manualEntryResult) isManualEntryValue = manualEntryResult - console.log(`[SearchableChooser:${fieldType}] Final isManualEntryValue:`, isManualEntryValue, `(will show pencil: ${isManualEntryValue && !isOpen})`) - } else { - console.log(`[SearchableChooser:${fieldType}] Skipping manual entry check - items list empty or loading:`, { - itemsLength: items?.length || 0, - hasItems: !!items, - }) - } - } else { - // DEBUG: Log why manual entry check was skipped - if (allowManualEntry && isManualEntry) { - console.log(`[SearchableChooser:${fieldType}] Skipping manual entry check:`, { - allowManualEntry, - hasDisplayValue: !!displayValue, - displayValue: `"${displayValue}"`, - trimmedDisplayValue: trimmedDisplayValue || 'N/A', - isEmpty: trimmedDisplayValue === '', - isPlaceholderValue, - placeholder: `"${placeholder}"`, - hasIsManualEntryFn: !!isManualEntry, - }) } } if (displayValue && items && items.length > 0 && !isManualEntryValue) { - if (debugLogging) { - console.log(`${fieldType}: Looking up display value for stored value: "${value}"`) - console.log(`${fieldType}: Items available: ${items.length}, first item type:`, typeof items[0]) - if (items.length > 0 && typeof items[0] === 'object') { - console.log(`${fieldType}: First item keys:`, Object.keys(items[0])) - } - } - // Try to find the item that matches this value // For notes, we need to match by filename; for folders, by path const foundItem = items.find((item: any) => { // Check if this item's value matches our stored value // For note objects, compare filename; for folder strings, compare the string itself if (typeof item === 'string') { - const matches = item === displayValue - if (debugLogging && matches) { - console.log(`${fieldType}: Matched string item: "${item}" === "${displayValue}"`) - } - return matches + return item === displayValue } else if (item && typeof item === 'object' && 'filename' in item) { // It's a note object, match by filename - const matches = item.filename === displayValue - if (debugLogging && matches) { - console.log(`${fieldType}: Matched note item by filename: "${item.filename}" === "${displayValue}", title: "${item.title}"`) - } - return matches + return item.filename === displayValue } else if (item && typeof item === 'object' && 'id' in item) { // It's an object with an id property (event, space, etc.), match by id first const matchesById = item.id === displayValue if (matchesById) { - if (debugLogging) { - console.log(`${fieldType}: Matched item by id: "${item.id}" === "${displayValue}", title: "${item.title || ''}"`) - } return true } // If id doesn't match, also check display value as fallback // This handles cases where value is a display string (e.g., "Private") instead of id (e.g., "") const displayVal = getDisplayValue(item) - const matchesByDisplay = displayVal === displayValue - if (debugLogging && matchesByDisplay) { - console.log(`${fieldType}: Matched item by display value: "${displayVal}" === "${displayValue}", id: "${item.id || ''}"`) - } - return matchesByDisplay + return displayVal === displayValue } // For other object types, try to match by comparing getDisplayValue result // or by checking if the item itself is the value const displayVal = getDisplayValue(item) - const matches = item === displayValue || displayVal === displayValue - if (debugLogging && matches) { - console.log(`${fieldType}: Matched object item:`, item) - } - return matches + return item === displayValue || displayVal === displayValue }) if (foundItem) { // Use the display label from the found item - const originalDisplayValue = displayValue displayValue = getDisplayValue(foundItem) - if (debugLogging) { - console.log(`${fieldType}: Found item! Original value: "${originalDisplayValue}" -> Display value: "${displayValue}"`) - } - } else { - if (debugLogging) { - console.log(`${fieldType}: No item found for value "${value}", will display value directly`) - // Show a few examples of what we're searching through - if (items.length > 0) { - const examples = items.slice(0, 3).map((item: any) => { - if (typeof item === 'string') return item if (item && typeof item === 'object' && 'filename' in item) return `{title: "${item.title}", filename: "${item.filename}"}` return String(item) }) @@ -792,12 +718,6 @@ export function SearchableChooser({ data-debug-items-count={items.length} data-debug-isloading={String(isLoading)} > - {debugLogging && - console.log( - `${fieldType}: Rendering dropdown, isOpen=${String(isOpen)}, isLoading=${String(isLoading)}, items.length=${items.length}, filteredItems.length=${ - filteredItems.length - }`, - )} {isLoading ? (
{ // Show all items if maxResults is undefined, otherwise limit to maxResults const itemsToShow = maxResults != null && maxResults > 0 ? filteredItems.slice(0, maxResults) : filteredItems - if (debugLogging) { - console.log(`${fieldType}: Rendering ${itemsToShow.length} options (filtered from ${filteredItems.length} total, maxResults=${maxResults || 'unlimited'})`) - } // Check if any items have icons or shortDescriptions (calculate once for all items) const hasIconsOrDescriptions = itemsToShow.some((item: any) => { @@ -846,14 +763,6 @@ export function SearchableChooser({ // For shorter items, let CSS handle truncation based on actual width const truncatedText = optionText.length > dropdownMaxLength ? truncateDisplay(optionText, dropdownMaxLength) : optionText const optionTitle = getOptionTitle(item) - if (debugLogging && index < 3) { - const jsTruncated = optionText.length > dropdownMaxLength - console.log( - `${fieldType}: Dropdown option[${index}]: original="${optionText}", length=${optionText.length}, truncated="${truncatedText}", length=${ - truncatedText.length - }, maxLength=${dropdownMaxLength}, jsTruncated=${String(jsTruncated)}`, - ) - } const optionIcon = getOptionIcon ? getOptionIcon(item) : null const optionColor = getOptionColor ? getOptionColor(item) : null let optionShortDesc = getOptionShortDescription ? getOptionShortDescription(item) : null From dbea69babc21659fff7068929a052cd7b8402cce Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 12:17:43 -0800 Subject: [PATCH 11/12] Ensure title is preserved in new note frontmatter When processing form submissions with newNoteFrontmatter: - Check if frontmatter already has a title field; if so, do nothing - If no title field exists, check if body content first line contains '# <%- newNoteTitle %>' - If body doesn't have the title heading, add 'title: <%- newNoteTitle %>' (or use original template tag) to the top of frontmatter - This ensures the title is always set and not overwritten by frontmatter or body content --- dwertheimer.Forms/src/formSubmission.js | 43 ++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/dwertheimer.Forms/src/formSubmission.js b/dwertheimer.Forms/src/formSubmission.js index 7acd1ee4e..443695427 100644 --- a/dwertheimer.Forms/src/formSubmission.js +++ b/dwertheimer.Forms/src/formSubmission.js @@ -933,9 +933,50 @@ async function processCreateNew(data: any, reactWindowData: PassedData): Promise // Step 7: Build template body (DO NOT insert templatejs blocks - they're already executed) // Get new note frontmatter and body content (templateBody) - const newNoteFrontmatter = reactWindowData?.pluginData?.newNoteFrontmatter || data?.newNoteFrontmatter || '' + let newNoteFrontmatter = reactWindowData?.pluginData?.newNoteFrontmatter || data?.newNoteFrontmatter || '' const templateBody = reactWindowData?.pluginData?.templateBody || data?.templateBody || '' + // Ensure title is preserved: if frontmatter exists, check if it has a title field + // If not, and body doesn't start with "# <%- newNoteTitle %>", add title to frontmatter + if (newNoteFrontmatter && newNoteFrontmatter.trim()) { + // Parse frontmatter to check for title field + const frontmatterLines = newNoteFrontmatter.trim().split('\n') + let hasTitleField = false + + for (const line of frontmatterLines) { + const trimmedLine = line.trim() + // Check if line matches "title:" (case-insensitive, with optional whitespace) + if (trimmedLine.match(/^title\s*:/i)) { + hasTitleField = true + break + } + } + + // If no title field exists, check body content + if (!hasTitleField) { + const bodyFirstLine = templateBody.trim().split('\n')[0] || '' + const hasTitleHeading = bodyFirstLine.trim() === '# <%- newNoteTitle %>' + + // If body doesn't have the title heading, add title to frontmatter + if (!hasTitleHeading) { + // Use the original newNoteTitle template tag if it contains template syntax, + // otherwise use the newNoteTitle variable (which will be available in template context) + const originalNewNoteTitle = newNoteTitleToUse || reactWindowData?.pluginData?.newNoteTitle || data?.newNoteTitle || '' + + // If newNoteTitle contains template tags, use them directly; otherwise reference newNoteTitle variable + let titleTemplateTag = '<%- newNoteTitle %>' + if (originalNewNoteTitle && typeof originalNewNoteTitle === 'string' && originalNewNoteTitle.includes('<%')) { + // Use the original template tag (e.g., "<%- Contact_Name %>") + titleTemplateTag = originalNewNoteTitle + } + + // Add title field to the top of frontmatter + newNoteFrontmatter = `title: ${titleTemplateTag}\n${newNoteFrontmatter.trim()}` + logDebug(pluginJson, `processCreateNew: Added title field to frontmatter to preserve title from being overwritten: title: ${titleTemplateTag}`) + } + } + } + let finalTemplateBody = '' // If we have frontmatter, combine it with templateBody using -- delimiters From cbc1369c9139cf3141232e5dcd3140e2e52e01ca Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sun, 25 Jan 2026 12:18:57 -0800 Subject: [PATCH 12/12] Fix syntax error in SearchableChooser - remove orphaned code from console.log removal --- helpers/react/DynamicDialog/SearchableChooser.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/helpers/react/DynamicDialog/SearchableChooser.jsx b/helpers/react/DynamicDialog/SearchableChooser.jsx index ecf9bf8e6..090827478 100644 --- a/helpers/react/DynamicDialog/SearchableChooser.jsx +++ b/helpers/react/DynamicDialog/SearchableChooser.jsx @@ -548,11 +548,6 @@ export function SearchableChooser({ if (foundItem) { // Use the display label from the found item displayValue = getDisplayValue(foundItem) - if (item && typeof item === 'object' && 'filename' in item) return `{title: "${item.title}", filename: "${item.filename}"}` - return String(item) - }) - } - } } }