diff --git a/TEMPLATING_CONTEXT_OPTIONS.md b/TEMPLATING_CONTEXT_OPTIONS.md new file mode 100644 index 000000000..e1ceebc3c --- /dev/null +++ b/TEMPLATING_CONTEXT_OPTIONS.md @@ -0,0 +1,101 @@ +# Options for Providing Templating Context to TemplateJS Blocks + +## Current Architecture + +### How np.Templating Creates Render Context + +1. **`TemplatingEngine.getRenderDataWithMethods()`** - Main function that builds context + - Creates module instances: `date`, `time`, `note`, `tasks`, `frontmatter`, `helpers`, `web`, `utility`, `system` + - Merges globals from `loadGlobalHelpers()` (which loads from `globals.js`) + - Merges userData + - Adds prompt error handlers + - Applies custom plugin modules + - Returns full renderData object + +2. **`loadGlobalHelpers()`** - Adds globals to sessionData.methods + - Loads from `globals.js` (moment, affirmation, advice, etc.) + - Not exported (private function) + +3. **Dependencies:** + - `TemplatingEngine` needs `templateConfig` (from settings) + - Module constructors need `templateConfig` for initialization + - Custom plugin modules are registered per instance + +## Options + +### Option 1: Import and Create Minimal Context (Recommended) +**Pros:** +- Reuses templating code (globals, modules) +- No changes to np.Templating +- Full access to all templating features + +**Cons:** +- Need to import module classes and create instances +- Need templateConfig (can duplicate minimal config or get via command) +- Some complexity in setup + +**Implementation:** +```javascript +// Import what we can +import TemplatingEngine from '../../np.Templating/lib/TemplatingEngine' +import globals from '../../np.Templating/lib/globals' +// Or get templateConfig via invokePluginCommandByName if available +// Create minimal TemplatingEngine instance +const engine = new TemplatingEngine(templateConfig, '', []) +const renderContext = await engine.getRenderDataWithMethods('', context) +// Use renderContext in Function constructor +``` + +### Option 2: Add New Command to np.Templating +**Pros:** +- Clean API +- No duplication +- Always uses latest templating logic + +**Cons:** +- Requires editing np.Templating (user said not to) +- Adds new command to maintain + +**Implementation:** +```javascript +// In np.Templating, add: +export async function getRenderContext(userData: any = {}): Promise { + await NPTemplating.setup() + const engine = new TemplatingEngine(NPTemplating.templateConfig, '', []) + return await engine.getRenderDataWithMethods('', userData) +} + +// In Forms: +const renderContext = await DataStore.invokePluginCommandByName('getRenderContext', 'np.Templating', [context]) +``` + +### Option 3: Duplicate Context Building Logic +**Pros:** +- No dependencies on np.Templating internals +- Full control + +**Cons:** +- Lots of code to duplicate (~100+ lines) +- Must keep in sync with templating changes +- Risk of divergence + +### Option 4: Use render() with JSON Wrapper (What We Tried) +**Pros:** +- Uses full templating system +- No duplication + +**Cons:** +- Returns strings, need to parse JSON +- More complex error handling +- Performance overhead of full render pipeline + +## Recommendation + +**Option 1** - Import and create minimal context: +1. Import `TemplatingEngine` and `globals` directly +2. Get or create minimal `templateConfig` (can duplicate minimal config) +3. Create `TemplatingEngine` instance +4. Call `getRenderDataWithMethods()` to get full context +5. Merge into Function constructor parameters + +This reuses the templating code without requiring changes to np.Templating. diff --git a/dwertheimer.Forms/CHANGELOG.md b/dwertheimer.Forms/CHANGELOG.md index c602f6b2d..49996a1d8 100644 --- a/dwertheimer.Forms/CHANGELOG.md +++ b/dwertheimer.Forms/CHANGELOG.md @@ -4,6 +4,46 @@ See Plugin [README](https://github.com/NotePlan/plugins/blob/main/dwertheimer.Forms/README.md) for details on available commands and use case. +## [1.0.10] 2026-01-14 @dwertheimer + +### Added +- **Comprehensive Tailwind CSS color palette support**: Added full Tailwind color mapping (gray, red, orange, yellow, green, blue, indigo, purple, pink with shades 50-950) to `helpers/colors.js` +- **Color support for chooser icons and descriptions**: NoteChooser and SpaceChooser now display colored icons and short descriptions matching `chooseNoteV2` behavior +- **New `getColorStyle()` helper function**: Centralized color conversion utility that handles CSS variables, Tailwind color names, and direct hex/rgb colors with proper fallbacks + +### Fixed +- Fixed empty label showing "?" in read-only text elements (now shows empty string) +- Fixed SpaceChooser using incorrect icons - now uses `fa-regular fa-cube` for teamspaces and `fa-solid fa-user` for private (matching Dashboard, Filer, NoteHelpers) +- Fixed teamspace colors appearing gray - now correctly displays green using `--teamspace-color` CSS variable with proper fallback +- Fixed default comment field not appearing when creating a new form - now explicitly passes `isNewForm: true` to FormBuilder (works for both command bar and FormBrowserView creation) +- Fixed NoteChooser calendar picker displaying filename (e.g., "20260117.md") instead of ISO date format (e.g., "2026-01-17") - now displays ISO 8601 format (YYYY-MM-DD) in the field + +### Changed +- **SpaceChooser**: Updated to use proper Font Awesome icon classes (`TEAMSPACE_FA_ICON`, `PRIVATE_FA_ICON`) instead of generic icon names +- **Color system**: All Tailwind color names (e.g., `gray-500`, `blue-500`, `orange-500`, `green-700`) now automatically convert to their hex values via comprehensive palette mapping +- **Special color mappings**: `green-700` and `green-800` prefer `--teamspace-color` CSS variable when available, with Tailwind hex fallback +- **SearchableChooser**: Hide short description when it's identical to the label text to avoid redundant display + +## [1.0.9] 2026-01-13 @dwertheimer + +### Added +- Auto-focus first field when form opens for faster data entry +- Enter key reopens dropdown when closed (allows changing selection after initial choice) +- Tab key closes dropdown and moves to next field when dropdown is open +- ResizeObserver for portal dropdown positioning to handle dynamic form height changes + +### Fixed +- Fixed click selection not working after programmatic refocus (dropdown was reopening immediately) +- Fixed tab navigation blocked when dropdown is open +- Fixed portal dropdown position when form layout shifts due to async data loading +- Fixed ContainedMultiSelectChooser preventing "is:checked" from being saved as a value +- Fixed bottom element clipping in scrolling dialogs (added extra padding) + +### Changed +- Improved ContainedMultiSelectChooser header: narrower filter field (40% reduction), icon-only buttons (All/None/Filter/New) +- Refactored template-form CSS to use nested namespace selectors for better maintainability +- Improved compact mode label alignment using CSS variables for customizable widths + ## [1.0.8] 2026-01-12 @dwertheimer ### Fixed diff --git a/dwertheimer.Forms/plugin.json b/dwertheimer.Forms/plugin.json index 734cb9252..1b4afcb41 100644 --- a/dwertheimer.Forms/plugin.json +++ b/dwertheimer.Forms/plugin.json @@ -3,11 +3,11 @@ "macOS.minVersion": "10.13.0", "noteplan.minAppVersion": "3.4.0", "plugin.id": "dwertheimer.Forms", - "plugin.name": "📝 Forms", - "plugin.version": "1.0.5", + "plugin.name": "📝 Template Forms", + "plugin.version": "1.0.10", "plugin.releaseStatus": "beta", - "plugin.lastUpdateInfo": "Added folder-chooser and note-chooser field types with smart path truncation", - "plugin.description": "Dynamic Forms for NotePlan using Templating -- fill out a form and have the data sent to a template for processing", + "plugin.lastUpdateInfo": "Improved form UX: auto-focus, tab navigation, and chooser controls", + "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"], "plugin.dependsOn": [], diff --git a/dwertheimer.Forms/src/NPTemplateForm.js b/dwertheimer.Forms/src/NPTemplateForm.js index 73054a74f..547e4455d 100644 --- a/dwertheimer.Forms/src/NPTemplateForm.js +++ b/dwertheimer.Forms/src/NPTemplateForm.js @@ -272,16 +272,22 @@ export async function openTemplateForm(templateTitle?: string): Promise { // These contain template tags that reference form field values and should not be processed during form opening if (templateNote) { if (templateNote) { + // Remove customCSS from frontmatter (it should only come from codeblock) + delete frontmatterAttributes.customCSS + // Load templateBody from codeblock const templateBodyFromCodeblock = await loadTemplateBodyFromTemplate(templateNote) if (templateBodyFromCodeblock) { frontmatterAttributes.templateBody = templateBodyFromCodeblock } - // Load custom CSS from codeblock + // Load custom CSS from codeblock (always use codeblock, not frontmatter) const customCSSFromCodeblock = await loadCustomCSSFromTemplate(templateNote) if (customCSSFromCodeblock) { frontmatterAttributes.customCSS = customCSSFromCodeblock + } else { + // Ensure it's empty if codeblock doesn't exist + frontmatterAttributes.customCSS = '' } // Load TemplateRunner args from codeblock @@ -362,6 +368,7 @@ export async function openFormBuilder(templateTitle?: string): Promise { let selectedTemplate let formFields: Array = [] let templateNote = null + let isNewForm = false // Track if this is a newly created form const receivingTemplateTitle: string = '' // Track receiving template title for newly created forms if (templateTitle?.trim().length) { @@ -521,6 +528,7 @@ export async function openFormBuilder(templateTitle?: string): Promise { } selectedTemplate = filename + isNewForm = true // Mark this as a new form so default comment field is added logDebug(pluginJson, `openFormBuilder: Set frontmatter and selectedTemplate = ${selectedTemplate}, receivingTemplateTitle = "${receivingTemplateTitle}"`) // Generate processing template link if receiving template exists @@ -731,6 +739,7 @@ export async function openFormBuilder(templateTitle?: string): Promise { templateFilename: selectedTemplate, templateTitle: templateNote?.title || '', initialReceivingTemplateTitle: initialReceivingTemplateTitle, + isNewForm: isNewForm, // Pass isNewForm flag so default comment field is added }) logDebug(pluginJson, `openFormBuilder: openFormBuilderWindow call completed`) } catch (error) { diff --git a/dwertheimer.Forms/src/components/FieldEditor.jsx b/dwertheimer.Forms/src/components/FieldEditor.jsx index d907b7819..426e1922c 100644 --- a/dwertheimer.Forms/src/components/FieldEditor.jsx +++ b/dwertheimer.Forms/src/components/FieldEditor.jsx @@ -27,6 +27,16 @@ function isValidCSSWidth(value: string): boolean { return cssWidthRegex.test(value.trim()) } +/** + * Check if a field type is NOT in the excluded types array + * @param {string} fieldType - The field type to check + * @param {Array} excludedTypes - Array of field types to exclude + * @returns {boolean} - True if field type is NOT in excluded types + */ +function shouldDisplayFieldType(fieldType: string, excludedTypes: Array): boolean { + return !excludedTypes.some((excludedType) => fieldType === excludedType) +} + export function FieldEditor({ field, allFields, onSave, onCancel, requestFromPlugin }: FieldEditorProps): Node { const [editedField, setEditedField] = useState({ ...field }) const [calendars, setCalendars] = useState>([]) @@ -37,6 +47,7 @@ export function FieldEditor({ field, allFields, onSave, onCancel, requestFromPlu const reminderListsLoadingRef = useRef(false) const requestFromPluginRef = useRef(requestFromPlugin) const [widthError, setWidthError] = useState('') + const [templateJSError, setTemplateJSError] = useState('') // Track previous field key to detect actual field changes const prevFieldKeyRef = useRef(field.key) @@ -198,7 +209,8 @@ export function FieldEditor({ field, allFields, onSave, onCancel, requestFromPlu onSave(editedField) } - const needsKey = editedField.type !== 'separator' && editedField.type !== 'heading' && editedField.type !== 'autosave' && editedField.type !== 'table-of-contents' + // templatejs-block fields don't need keys - they're auto-generated at execution time + const needsKey = shouldDisplayFieldType(editedField.type, ['separator', 'heading', 'autosave', 'table-of-contents', 'comment', 'templatejs-block']) // Construct header title with label, key, and type const headerTitle = needsKey && editedField.key ? `Editing ${editedField.type}: ${editedField.label || ''} (${editedField.key})` : `Editing: ${editedField.type}` @@ -237,7 +249,7 @@ export function FieldEditor({ field, allFields, onSave, onCancel, requestFromPlu )} - {editedField.type !== 'separator' && editedField.type !== 'heading' && editedField.type !== 'table-of-contents' && editedField.type !== 'calendarpicker' && editedField.type !== 'autosave' && ( + {shouldDisplayFieldType(editedField.type, ['separator', 'heading', 'table-of-contents', 'calendarpicker', 'autosave', 'comment']) && (
)} - {editedField.type !== 'separator' && ( + {shouldDisplayFieldType(editedField.type, ['separator', 'comment']) && (