/** * Generates the TqFormFileInput wrapper — the in-form upload field behind * ``. It is a pure controlled input over a page state * whose value is an ARRAY of PickedFile POJOs * `{ name, size, type, lastModified, dataURL }` — the exact shape the * `browser-pick-files` workflow node emits and `file-storage-upload` * consumes (dataURL→Blob). Nothing is uploaded at pick time: the * form-submit workflow reads the state via state-get-local-state and * uploads there. Commits go through the page-supplied `onChange` * (`event.target.value` = JSON) and are mirrored into a paired hidden * input (no `name`, so it never leaks into form payloads) that dispatches * a native `change` for the platform's element-trigger machinery — the * same bridge the signature widget uses. * * Bug 6 (encode race): `readAsDataURL` is async, so a form-submit workflow's * synchronous "is state. empty?" gate — evaluated the instant Submit is * clicked — used to read the OLD (empty) state if the user clicked Submit * before a large file's encode resolved. `handlePick` now commits an * IMMEDIATE placeholder entry (same shape, `dataURL: ''`) synchronously, in * the SAME native `change` event as the pick, so `.length` reflects "a file * is selected" before any later, separate click could possibly fire. The * placeholder is replaced with the fully-encoded entry once the async read * resolves (or rolled back to the pre-pick selection if every read fails). */ export const generateFormFileInputComponentCode = (): string => { return `import React, { useRef } from 'react' const readPickedFile = (file) => new Promise((resolve) => { const reader = new FileReader() reader.onload = () => resolve({ name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, dataURL: String(reader.result || ''), }) reader.onerror = () => resolve(null) reader.readAsDataURL(file) }) // stateKey (the GUI export's canonical prop) and state (the legacy/editor // attr name) are destructured ONLY to keep them off the DOM: neither is a // valid
attribute and both would otherwise leak through {...rest}. const TqFormFileInput = ({ id, stateKey, state, value = [], multiple = false, accept = '', label = '', maxFiles, maxFileSizeMb = 5, onChange, ...rest }) => { const pickerRef = useRef(null) const mirrorRef = useRef(null) const files = Array.isArray(value) ? value : [] const isMultiple = multiple === true || multiple === 'true' const fileCap = Number(maxFiles) > 0 ? Number(maxFiles) : isMultiple ? 10 : 1 const sizeCap = (Number(maxFileSizeMb) > 0 ? Number(maxFileSizeMb) : 5) * 1024 * 1024 const commit = (next) => { const serialized = JSON.stringify(next) if (mirrorRef.current) { mirrorRef.current.value = serialized mirrorRef.current.dispatchEvent(new Event('change', { bubbles: true })) } // React never synthesizes onChange from a hidden input's native change, // so the state setter is invoked directly with the same event shape the // generated handler reads (event.target.value). if (typeof onChange === 'function') { onChange({ target: { value: serialized } }) } } const handlePick = async (event) => { const picked = Array.from(event.target.files || []).filter((file) => file.size <= sizeCap) event.target.value = '' if (picked.length === 0) { return } // Commit a placeholder SYNCHRONOUSLY (dataURL: '' — never uploaded as-is; // file-storage-upload only ever sees the resolved entries below) so // state..length is already non-zero in this same event, closing the // async-encode race a fast Submit click could otherwise win. const placeholders = picked.map((file) => ({ name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, dataURL: '', })) commit(isMultiple ? files.concat(placeholders).slice(0, fileCap) : placeholders.slice(0, 1)) const read = (await Promise.all(picked.map(readPickedFile))).filter(Boolean) if (read.length === 0) { commit(files) // every read failed — roll back to the pre-pick selection return } const next = isMultiple ? files.concat(read).slice(0, fileCap) : read.slice(0, 1) commit(next) } const removeAt = (index) => { commit(files.filter((_entry, i) => i !== index)) } return (
{label ? {label} : null} {files.length > 0 ? (
{files.map((file, index) => (
{file && file.dataURL && String(file.type || '').indexOf('image/') === 0 ? ( {file.name ) : (
{(file && file.name) || 'file'}
)}
))}
) : null}
) } export default TqFormFileInput ` }