Skip to content

Commit d597383

Browse files
authored
Forms 1.0.12 (#721)
* chore: clear 1.0.12 changelog for new changes * Fix GenericDatePicker infinite loop and prevent Enter key form submission - Add 'calendarpicker' to ENTER_CONSUMING_FIELD_TYPES to prevent form submission on Enter - Add onKeyDown handler in GenericDatePicker to prevent Enter key from submitting form - Fix infinite loop by tracking last received date and only updating when actually different - Improve useEffect to compare normalized dates instead of just inputValue strings - Only call onSelectDate when value actually changed to prevent unnecessary parent updates - Add data-field-type attribute to calendarpicker container for Enter key detection * WIP looking for infinite loop * Update forms to use new sideBarView object * Refactor: Move openFormBrowser to windowManagement.js for consistency - Moved openFormBrowser function from NPTemplateForm.js to windowManagement.js - All window-opening functions (openFormWindow, openFormBuilderWindow, openFormBrowser) are now centralized in windowManagement.js - Updated exports in index.js to reflect the new location - Removed unused generateCSSFromTheme import from NPTemplateForm.js - All calls to openReactWindow and showInMainWindow are now in windowManagement.js * Update dark mode text bug * darkmode fix for forms
1 parent e713da5 commit d597383

13 files changed

Lines changed: 396 additions & 178 deletions

File tree

‎dwertheimer.Forms/CHANGELOG.md‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,13 @@ See Plugin [README](https://github.com/NotePlan/plugins/blob/main/dwertheimer.Fo
77
## [1.0.12] 2026-01-17 @dwertheimer
88

99
### Added
10-
- **EventChooser Preloading**: Added support for preloading events data when `preloadChooserData: true` is set in form frontmatter
11-
- **All Choosers Support Preloading**: All chooser types (FolderChooser, NoteChooser, SpaceChooser, MentionChooser, TagChooser, EventChooser) now support preloading data for static HTML testing
1210

1311
### Fixed
14-
- Fixed chooser width wrapping in compact mode - removed `min-width` for percentage and viewport unit widths to prevent wrapping issues
15-
- Choosers with `width="100%"` or other percentage/viewport units now properly respect container boundaries without wrapping
1612

1713
### Changed
18-
- **SearchableChooser**: Width prop now only sets `min-width` for fixed pixel values, allowing flexible sizing for percentages and viewport units
19-
- **Code Refactoring**: Refactored `getPluginData` function into smaller, modular helper functions for better maintainability:
20-
- `detectFieldRequirements()` - Detects which chooser types are needed
21-
- `ensureAutosaveField()` - Adds autosave field if needed
22-
- `preloadAllChooserData()` - Orchestrates all preloading operations
23-
- Individual preload functions for each chooser type (folders, notes, teamspaces, mentions, hashtags, events)
24-
- `initializeEmptyChooserData()` - Initializes empty arrays when not preloading
14+
- **FormView**: Set text color of dialog to main color for better readability
15+
Under the hood changes to move all window opening code to the windowManagement.js file.
16+
2517

2618
## [1.0.11] 2026-01-17 @dwertheimer
2719

‎dwertheimer.Forms/plugin.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
],
6060
"description": "Browse and select template forms in a two-column resizable layout",
6161
"jsFunction": "openFormBrowser",
62+
"sidebarView": {
63+
"windowID": "form-browser-window main",
64+
"title": "Form Browser",
65+
"icon": "list",
66+
"iconColor": "blue-500"
67+
},
6268
"arguments": [
6369
{
6470
"name": "showFloating",

‎dwertheimer.Forms/src/NPTemplateForm.js‎

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { getNoteByFilename } from '@helpers/note'
1717
import { validateObjectString, parseObjectString } from '@helpers/stringTransforms'
1818
import { updateFrontMatterVars, ensureFrontmatter, noteHasFrontMatter, getFrontmatterAttributes } from '@helpers/NPFrontMatter'
1919
import { loadCodeBlockFromNote } from '@helpers/codeBlocks'
20-
import { generateCSSFromTheme } from '@helpers/NPThemeToCSS'
2120
import { parseTeamspaceFilename } from '@helpers/teamspace'
2221
import { getFolderFromFilename } from '@helpers/folders'
2322
import { displayTitle } from '@helpers/paragraph'
@@ -806,89 +805,7 @@ export async function openFormBuilder(templateTitle?: string): Promise<void> {
806805
*/
807806
// openFormWindow is now imported from windowManagement.js
808807

809-
/**
810-
* Open Form Browser window - browse and select template forms
811-
* @param {Object} argObj - Options object
812-
* @param {boolean} argObj.showFloating - If true, use showReactWindow instead of showInMainWindow
813-
* @returns {Promise<void>}
814-
*/
815-
export async function openFormBrowser(_showFloating: boolean = false): Promise<void> {
816-
try {
817-
logDebug(pluginJson, `openFormBrowser: Starting, showFloating=${String(_showFloating)}`)
818-
819-
// Make sure we have np.Shared plugin which has the core react code
820-
await DataStore.installOrUpdatePluginsByID(['np.Shared'], false, false, true)
821-
logDebug(pluginJson, `openFormBrowser: installOrUpdatePluginsByID ['np.Shared'] completed`)
822-
823-
const startTime = new Date()
824-
const ENV_MODE = 'development'
825-
const showFloating = _showFloating === true || (typeof _showFloating === 'string' && /true/i.test(_showFloating))
826-
827-
// Create plugin data
828-
// Note: requestFromPlugin is now implemented in FormBrowserView using the dispatch pattern
829-
// (functions can't be serialized when passed through HTML/JSON)
830-
// Generate unique window ID based on whether it's floating or main window
831-
const windowId = showFloating ? getFormBrowserWindowId('floating') : getFormBrowserWindowId('main')
832-
const pluginData = {
833-
platform: NotePlan.environment.platform,
834-
windowId: windowId, // Store window ID in pluginData so React can send it in requests
835-
showFloating: showFloating, // Pass showFloating flag so React knows whether to show header
836-
}
837-
838-
// Create data object to pass to React
839-
const dataToPass: PassedData = {
840-
pluginData,
841-
title: 'Form Browser',
842-
logProfilingMessage: false,
843-
debug: false, // Enable debug mode to show test buttons
844-
ENV_MODE,
845-
returnPluginCommand: { id: pluginJson['plugin.id'], command: 'onFormBrowserAction' },
846-
componentPath: `../dwertheimer.Forms/react.c.FormBrowserView.bundle.dev.js`,
847-
startTime,
848-
}
849-
850-
// CSS tags for styling
851-
const cssTagsString = `
852-
<link rel="stylesheet" href="../np.Shared/css.w3.css">
853-
<link href="../np.Shared/fontawesome.css" rel="stylesheet">
854-
<link href="../np.Shared/regular.min.flat4NP.css" rel="stylesheet">
855-
<link href="../np.Shared/solid.min.flat4NP.css" rel="stylesheet">
856-
<link href="../np.Shared/light.min.flat4NP.css" rel="stylesheet">\n`
857-
858-
// Use the same windowOptions for both floating and main window
859-
const windowOptions = {
860-
savedFilename: `../../${pluginJson['plugin.id']}/form_browser_output.html`,
861-
headerTags: cssTagsString,
862-
windowTitle: 'Form Browser',
863-
width: 1200,
864-
height: 800,
865-
customId: windowId, // Use unique window ID instead of constant
866-
shouldFocus: true,
867-
generalCSSIn: generateCSSFromTheme(),
868-
postBodyScript: `
869-
<script type="text/javascript" >
870-
// Set DataStore.settings so default logDebug etc. logging works in React
871-
let DataStore = { settings: {_logLevel: "${DataStore.settings._logLevel}" } };
872-
</script>
873-
`,
874-
// Options for showInMainWindow (main window mode)
875-
splitView: false,
876-
icon: 'list',
877-
iconColor: 'blue-500',
878-
autoTopPadding: true,
879-
}
880-
881-
// Choose the appropriate command based on whether it's floating or main window
882-
const windowType = showFloating ? 'openReactWindow' : 'showInMainWindow'
883-
logDebug(pluginJson, `openFormBrowser: Using ${windowType} (${showFloating ? 'floating' : 'main'} window)`)
884-
await DataStore.invokePluginCommandByName(windowType, 'np.Shared', [dataToPass, windowOptions])
885-
886-
logDebug(pluginJson, `openFormBrowser: Completed after ${timer(startTime)}`)
887-
} catch (error) {
888-
logError(pluginJson, `openFormBrowser: Error: ${JSP(error)}`)
889-
await showMessage(`Error opening form browser: ${error.message}`)
890-
}
891-
}
808+
// openFormBrowser is now imported from windowManagement.js
892809

893810
/**
894811
* Plugin entrypoint for triggerOpenForm

‎dwertheimer.Forms/src/components/FormView.css‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
/* Template Form Dialog Centering - apply same centering logic as DynamicDialog.css */
88
/* Only apply centering if no explicit x,y positioning is set via inline styles */
99
.dynamic-dialog.template-form {
10+
11+
/* set the text color of the dialog to the main color */
12+
color: var(--fg-main-color);
13+
& input, & textarea, & select {
14+
color: var(--fg-main-color);
15+
}
16+
1017
/* Ensure proper vertical centering - same as DynamicDialog.css */
1118
/* Center vertically: position top edge at viewport center + toolbar offset, then translateY(-50%) centers it */
1219
/* Account for toolbar offset: center of available space = 50vh + toolbar-height/2 */
@@ -618,3 +625,48 @@
618625
word-wrap: break-word;
619626
overflow-wrap: break-word;
620627
}
628+
629+
/* Form Submitting Overlay */
630+
/* Positioned at root level to appear above all content including modals */
631+
.form-submitting-overlay {
632+
position: fixed;
633+
top: 0;
634+
left: 0;
635+
right: 0;
636+
bottom: 0;
637+
background-color: rgba(0, 0, 0, 0.6); /* Slightly darker for better visibility */
638+
display: flex;
639+
align-items: center;
640+
justify-content: center;
641+
z-index: 99999 !important; /* Very high z-index to appear above everything (modals are 100-101) */
642+
pointer-events: auto; /* Block all interactions during submission */
643+
}
644+
645+
.form-submitting-message {
646+
background-color: var(--tint-color, #007aff);
647+
color: white;
648+
padding: 1.5rem 2.5rem;
649+
border-radius: 12px;
650+
font-size: 1.2rem;
651+
font-weight: 600;
652+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
653+
animation: pulse 2s ease-in-out infinite;
654+
text-align: center;
655+
pointer-events: auto; /* Make message interactive (though not needed) */
656+
min-width: 250px;
657+
max-width: 90vw;
658+
}
659+
660+
.form-submitting-message > div {
661+
display: block;
662+
line-height: 1.5;
663+
}
664+
665+
@keyframes pulse {
666+
0%, 100% {
667+
opacity: 1;
668+
}
669+
50% {
670+
opacity: 0.8;
671+
}
672+
}

‎dwertheimer.Forms/src/components/FormView.jsx‎

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Props = {
3232
****************************************************************************************************************************/
3333

3434
import React, { useEffect, useRef, useState, useCallback, useMemo, type Node } from 'react'
35+
import { createPortal } from 'react-dom'
3536
import { type PassedData } from '../shared/types.js'
3637
import { AppProvider } from './AppContext.jsx'
3738
import DynamicDialog from '@helpers/react/DynamicDialog'
@@ -422,22 +423,47 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
422423
}
423424

424425
const handleCancel = () => {
426+
setIsSubmitting(false) // Hide overlay if canceling during submission
427+
setFormSubmitted(false) // Reset submitted state
425428
sendActionToPlugin(onSubmitOrCancelCallFunctionNamed, { type: 'cancel' })
426429
closeDialog()
427430
}
428431

429432
// Track if form was submitted to handle delayed closing
430433
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
434+
// Track if form is currently submitting (for showing loading overlay)
435+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
436+
437+
// Debug counters (visible on-screen during freeze)
438+
// Use refs for counters to avoid re-renders, only update state when needed (e.g., during submission)
439+
const debugCountersRef = useRef<{ renders: number, setDataReceived: number, handleSaveCalls: number }>({ renders: 0, setDataReceived: 0, handleSaveCalls: 0 })
440+
const [debugCounters, setDebugCounters] = useState({ renders: 0, setDataReceived: 0, handleSaveCalls: 0 })
441+
const handleSaveCallCountRef = useRef<number>(0)
442+
const setDataReceivedCountRef = useRef<number>(0)
431443

432444
// Close dialog after submission if there's no AI analysis result
445+
// GUARD: Use ref to prevent this effect from running multiple times with same data
446+
const formSubmittedEffectRunRef = useRef<string>('')
433447
useEffect(() => {
434448
if (formSubmitted) {
449+
// Create a signature for this effect run to prevent duplicate processing
450+
const signature = `${String(!!pluginData?.aiAnalysisResult)}-${String(!!pluginData?.formSubmissionError)}`
451+
if (formSubmittedEffectRunRef.current === signature) {
452+
// Already processed this state, skip to prevent loops
453+
return
454+
}
455+
formSubmittedEffectRunRef.current = signature
435456
// Check if there's an AI analysis result
436457
const hasAiAnalysis = pluginData?.aiAnalysisResult && typeof pluginData.aiAnalysisResult === 'string' && pluginData.aiAnalysisResult.includes('==**Templating Error Found**')
437458
logDebug('FormView', `[AI ANALYSIS] formSubmitted=${String(formSubmitted)}, hasAiAnalysis=${String(hasAiAnalysis)}, aiAnalysisResult exists=${String(!!pluginData?.aiAnalysisResult)}, length=${pluginData?.aiAnalysisResult?.length || 0}`)
438459

439460
const hasFormSubmissionError = pluginData?.formSubmissionError && typeof pluginData.formSubmissionError === 'string'
440461

462+
// Hide submitting overlay if we received an error or AI analysis result
463+
if (hasAiAnalysis || hasFormSubmissionError) {
464+
setIsSubmitting(false)
465+
}
466+
441467
if (!hasAiAnalysis && !hasFormSubmissionError) {
442468
// No AI analysis result and no form submission error - close the dialog after a short delay to allow data to update
443469
logDebug('FormView', `[AI ANALYSIS] No AI analysis result, no form submission error, will close dialog after 500ms delay`)
@@ -447,6 +473,7 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
447473
const stillNoFormError = !pluginData?.formSubmissionError
448474
logDebug('FormView', `[AI ANALYSIS] After 500ms delay, stillNoAiAnalysis=${String(stillNoAiAnalysis)}, stillNoFormError=${String(stillNoFormError)}, closing dialog`)
449475
if (stillNoAiAnalysis && stillNoFormError) {
476+
setIsSubmitting(false) // Hide overlay before closing
450477
closeDialog()
451478
setFormSubmitted(false)
452479
}
@@ -461,8 +488,22 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
461488
}, [formSubmitted, pluginData?.aiAnalysisResult, pluginData?.formSubmissionError])
462489

463490
const handleSave = (formValues: Object, windowId?: string) => {
491+
// GUARD: Prevent multiple submissions (critical for preventing loops)
492+
if (isSubmitting || formSubmitted) {
493+
logDebug('FormView', `[GUARD] handleSave: Already submitting (isSubmitting=${String(isSubmitting)}, formSubmitted=${String(formSubmitted)}), ignoring duplicate call`)
494+
return
495+
}
496+
497+
handleSaveCallCountRef.current += 1
498+
debugCountersRef.current.handleSaveCalls = handleSaveCallCountRef.current
499+
// Update state to show in overlay
500+
setDebugCounters({ ...debugCountersRef.current })
501+
464502
clo(formValues, 'DynamicDialog: handleSave: formValues')
503+
logDebug('FormView', `[FRONT-END] handleSave called (#${handleSaveCallCountRef.current}) - form submission starting, isSubmitting=${String(isSubmitting)}, formSubmitted=${String(formSubmitted)}`)
465504
setFormSubmitted(true) // Mark form as submitted
505+
setIsSubmitting(true) // Show submitting overlay
506+
logDebug('FormView', `[FRONT-END] Sending onSubmitClick to back-end (plugin)`)
466507
sendActionToPlugin(onSubmitOrCancelCallFunctionNamed, {
467508
type: 'submit',
468509
formValues,
@@ -677,19 +718,41 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
677718
// Diagnostic: Log render timing (runs on every render to track re-renders)
678719
// Note: Re-renders from autosave completion are expected (autosave updates state → re-render)
679720
const renderCountRef = useRef<number>(0)
721+
const lastPluginDataRef = useRef<string>('')
680722
useEffect(() => {
681723
renderCountRef.current += 1
682724
const renderStartTime = performance.now()
725+
726+
// Track pluginData changes to detect SET_DATA messages from back-end
727+
const currentPluginDataStr = JSON.stringify(pluginData)
728+
const pluginDataChanged = currentPluginDataStr !== lastPluginDataRef.current
729+
if (pluginDataChanged) {
730+
lastPluginDataRef.current = currentPluginDataStr
731+
setDataReceivedCountRef.current += 1
732+
debugCountersRef.current.setDataReceived = setDataReceivedCountRef.current
733+
// Don't update state here - would cause infinite loop. Only update when handleSave is called.
734+
logDebug('FormView', `[FRONT-END] SET_DATA received from back-end (#${setDataReceivedCountRef.current}) - pluginData changed, triggering re-render #${renderCountRef.current}`)
735+
// Log what changed
736+
const hasError = pluginData?.formSubmissionError || pluginData?.aiAnalysisResult
737+
if (hasError) {
738+
logDebug('FormView', `[FRONT-END] SET_DATA contains error: formSubmissionError=${!!pluginData?.formSubmissionError}, aiAnalysisResult=${!!pluginData?.aiAnalysisResult}`)
739+
}
740+
}
741+
742+
// Update render counter in ref only (don't trigger state update - that would cause infinite loop!)
743+
debugCountersRef.current.renders = renderCountRef.current
744+
683745
// Only log first few renders and then periodically to reduce noise
684-
const shouldLog = renderCountRef.current <= 3 || renderCountRef.current % 10 === 0
746+
// But always log when pluginData changes (back-end activity)
747+
const shouldLog = pluginDataChanged || renderCountRef.current <= 3 || renderCountRef.current % 10 === 0
685748
if (shouldLog) {
686-
logDebug('FormView', `[DIAG] FormView RENDER #${renderCountRef.current}: formFields=${formFields.length}, folders=${folders.length}, notes=${notes.length}`)
749+
logDebug('FormView', `[FRONT-END] FormView RENDER #${renderCountRef.current}: formFields=${formFields.length}, folders=${folders.length}, notes=${notes.length}, pluginDataChanged=${String(pluginDataChanged)}`)
687750
}
688751

689752
requestAnimationFrame(() => {
690753
const renderElapsed = performance.now() - renderStartTime
691754
if (shouldLog) {
692-
logDebug('FormView', `[DIAG] FormView RENDER #${renderCountRef.current} AFTER RAF: elapsed=${renderElapsed.toFixed(2)}ms`)
755+
logDebug('FormView', `[FRONT-END] FormView RENDER #${renderCountRef.current} AFTER RAF: elapsed=${renderElapsed.toFixed(2)}ms`)
693756
}
694757
})
695758
})
@@ -760,6 +823,7 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
760823
}, [formSubmissionError])
761824

762825
return (
826+
<>
763827
<AppProvider
764828
sendActionToPlugin={sendActionToPlugin}
765829
sendToPlugin={sendToPlugin}
@@ -856,6 +920,37 @@ export function FormView({ data, dispatch, reactSettings, setReactSettings, onSu
856920
</div>
857921
{/* end of replace */}
858922
</div>
923+
{/* Submitting overlay - rendered via portal to document.body to appear above everything */}
924+
{isSubmitting && typeof document !== 'undefined' && document.body
925+
? createPortal(
926+
<div className="form-submitting-overlay">
927+
<div className="form-submitting-message">
928+
<div>Submitting Form...</div>
929+
{/* Debug counters visible during freeze - read from ref to avoid triggering re-renders */}
930+
<div style={{ marginTop: '1rem', fontSize: '0.9rem', opacity: 0.8 }}>
931+
Renders: {debugCountersRef.current.renders} | SET_DATA: {debugCountersRef.current.setDataReceived} | handleSave: {debugCountersRef.current.handleSaveCalls}
932+
</div>
933+
</div>
934+
</div>,
935+
document.body,
936+
)
937+
: null}
859938
</AppProvider>
939+
{/* Submitting overlay - rendered via portal to document.body to appear above everything */}
940+
{isSubmitting && typeof document !== 'undefined' && document.body
941+
? createPortal(
942+
<div className="form-submitting-overlay">
943+
<div className="form-submitting-message">
944+
<div>Submitting Form...</div>
945+
{/* Debug counters visible during freeze - read from ref to avoid triggering re-renders */}
946+
<div style={{ marginTop: '1rem', fontSize: '0.9rem', opacity: 0.8 }}>
947+
Renders: {debugCountersRef.current.renders} | SET_DATA: {debugCountersRef.current.setDataReceived} | handleSave: {debugCountersRef.current.handleSaveCalls}
948+
</div>
949+
</div>
950+
</div>,
951+
document.body,
952+
)
953+
: null}
954+
</>
860955
)
861956
}

0 commit comments

Comments
 (0)