From 720e0f6729b8f488164522aba535ad58027e0cfc Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Fri, 19 Jun 2026 16:52:20 +0000 Subject: [PATCH 1/3] wip --- .../lib/__tests__/formPreviewBridge.test.ts | 30 ++ desktop/src/lib/formPreviewBridge.ts | 11 +- .../renderers/subObservationHelpers.test.ts | 29 ++ .../src/renderers/subObservationHelpers.ts | 15 +- .../assets/webview/FormulusInjectionScript.js | 2 +- formulus/assets/webview/formulus-api.js | 465 ++++++++---------- formulus/scripts/generateInjectionScript.ts | 17 +- .../src/webview/FormulusMessageHandlers.ts | 13 +- 8 files changed, 316 insertions(+), 266 deletions(-) diff --git a/desktop/src/lib/__tests__/formPreviewBridge.test.ts b/desktop/src/lib/__tests__/formPreviewBridge.test.ts index d56255572..4df7f2796 100644 --- a/desktop/src/lib/__tests__/formPreviewBridge.test.ts +++ b/desktop/src/lib/__tests__/formPreviewBridge.test.ts @@ -208,6 +208,36 @@ describe('handleFormPreviewBridgeMessage', () => { expect(payload.result).toBe('nested-synthetic-id'); }); + it('allocateSequence honors peek (nested in options) without advancing the counter', async () => { + const postMessage = vi.fn(); + const cw = { postMessage } as unknown as Window; + const iframe = { contentWindow: cw } as HTMLIFrameElement; + + const allocate = async ( + options: Record | undefined, + messageId: string, + ) => { + await handleFormPreviewBridgeMessage( + bridgeMessageFromIframe(iframe, { + type: 'allocateSequence', + messageId, + scopeKey: 'tb:peek-test:role:A:af', + options, + }), + { iframe, onFinalize: async () => ({ error: 'no' }) }, + ); + const calls = postMessage.mock.calls; + const payload = JSON.parse(calls[calls.length - 1][0] as string); + return payload.result as number; + }; + + // Consume -> 1, peek -> 2 (no advance), consume -> 2. + expect(await allocate(undefined, 'seq1')).toBe(1); + expect(await allocate({ peek: true }, 'seq2')).toBe(2); + expect(await allocate({ peek: true }, 'seq3')).toBe(2); + expect(await allocate(undefined, 'seq4')).toBe(2); + }); + it('stubs requestCamera with prefixed error', async () => { const postMessage = vi.fn(); const cw = { postMessage } as unknown as Window; diff --git a/desktop/src/lib/formPreviewBridge.ts b/desktop/src/lib/formPreviewBridge.ts index 1bb5583ab..adcf2e56a 100644 --- a/desktop/src/lib/formPreviewBridge.ts +++ b/desktop/src/lib/formPreviewBridge.ts @@ -386,9 +386,16 @@ export async function handleFormPreviewBridgeMessage( : typeof data.payload === 'string' ? data.payload : ''; + // Injection sends options nested under `options`; fall back to flat + // top-level fields for back-compat with older callers. + const opts = ( + data.options && typeof data.options === 'object' + ? (data.options as Record) + : data + ) as { startAt?: unknown; peek?: unknown }; const startAt = - typeof data.startAt === 'number' ? data.startAt : undefined; - const peek = Boolean(data.peek); + typeof opts.startAt === 'number' ? opts.startAt : undefined; + const peek = Boolean(opts.peek); const result = previewAllocateSequence(scopeKey, { startAt, peek }); reply('allocateSequence', { result }); return; diff --git a/formulus-formplayer/src/renderers/subObservationHelpers.test.ts b/formulus-formplayer/src/renderers/subObservationHelpers.test.ts index f7a4a428b..4779fbeed 100644 --- a/formulus-formplayer/src/renderers/subObservationHelpers.test.ts +++ b/formulus-formplayer/src/renderers/subObservationHelpers.test.ts @@ -40,6 +40,35 @@ describe('subObservationHelpers', () => { expect(merged.person_codigos).toEqual([]); }); + it('mergePreservingSubObsArrays keeps off-page scalar prefills and autoSequence fields', () => { + // SwipeLayout partial payload: only the current page's field is present. + const baseline = { + obsdate: '2026-06-19', + region: 'Bafata', + tb: 'TB01', + device_role: 'A', + num: 7, + }; + const incoming = { nome_chefe: 'Maria' }; + const merged = mergePreservingSubObsArrays(baseline, incoming); + expect(merged.nome_chefe).toBe('Maria'); + expect(merged.obsdate).toBe('2026-06-19'); + expect(merged.region).toBe('Bafata'); + expect(merged.tb).toBe('TB01'); + expect(merged.device_role).toBe('A'); + // Critical: a directly-used x-autoSequence value must survive partial + // payloads so applyAutoSequences does not re-allocate it. + expect(merged.num).toBe(7); + }); + + it('mergePreservingSubObsArrays lets on-page edits override baseline', () => { + const baseline = { nome_chefe: 'Maria', num: 7 }; + const incoming = { nome_chefe: 'João' }; + const merged = mergePreservingSubObsArrays(baseline, incoming); + expect(merged.nome_chefe).toBe('João'); + expect(merged.num).toBe(7); + }); + it('formDataJsonEqual compares stable JSON snapshots', () => { expect(formDataJsonEqual({ a: 1 }, { a: 1 })).toBe(true); expect(formDataJsonEqual({ a: 1 }, { a: 2 })).toBe(false); diff --git a/formulus-formplayer/src/renderers/subObservationHelpers.ts b/formulus-formplayer/src/renderers/subObservationHelpers.ts index a936df1e1..98e8c465f 100644 --- a/formulus-formplayer/src/renderers/subObservationHelpers.ts +++ b/formulus-formplayer/src/renderers/subObservationHelpers.ts @@ -132,16 +132,21 @@ export function formDataJsonEqual( } /** - * Keep embedded sub-obs arrays when JsonForms emits partial onChange payloads. - * SwipeLayout only mounts the current page, so controls on other pages (e.g. - * `quartos`) are absent from `incoming` even though they still live in our - * baseline draft state. + * Keep baseline values when JsonForms emits partial onChange payloads. + * SwipeLayout only mounts the current page, so controls on other pages are + * absent from `incoming` even though they still live in our baseline draft + * state. Starting from `{ ...baseline, ...incoming }` preserves off-page + * scalars (host prefills like cluster stamps / obsdate, and directly-used + * `x-autoSequence` fields) so they are not dropped and re-allocated. On-page + * edits (including clearing a field) still override baseline via `incoming`. + * Sub-observation arrays get extra protection below against shorter/partial + * payloads. */ export function mergePreservingSubObsArrays( baseline: Record, incoming: Record, ): Record { - const merged = { ...incoming }; + const merged = { ...baseline, ...incoming }; for (const key of PRESERVED_ARRAY_KEYS) { const baseArr = baseline[key]; if (!(key in incoming)) { diff --git a/formulus/assets/webview/FormulusInjectionScript.js b/formulus/assets/webview/FormulusInjectionScript.js index 7207daf0d..844fc7a2e 100644 --- a/formulus/assets/webview/FormulusInjectionScript.js +++ b/formulus/assets/webview/FormulusInjectionScript.js @@ -1,6 +1,6 @@ // Auto-generated from FormulusInterfaceDefinition.ts // Do not edit directly - this file will be overwritten -// Last generated: 2026-06-19T12:32:54.430Z +// Last generated: 2026-06-19T16:46:35.984Z (function () { // Enhanced API availability detection and recovery diff --git a/formulus/assets/webview/formulus-api.js b/formulus/assets/webview/formulus-api.js index 71dec8160..2c2ab2ed6 100644 --- a/formulus/assets/webview/formulus-api.js +++ b/formulus/assets/webview/formulus-api.js @@ -1,16 +1,16 @@ /** * Formulus API Interface (JavaScript Version) - * + * * This file provides type information and documentation for the Formulus API * that's available in the WebView context as `globalThis.formulus`. - * + * * This file is auto-generated from FormulusInterfaceDefinition.ts - * Last generated: 2026-06-19T12:32:54.875Z - * + * Last generated: 2026-06-19T16:46:36.433Z + * * @example * // In your JavaScript file: * /// - * + * * // Now you'll get autocompletion and type hints in IDEs that support JSDoc * globalThis.formulus.getVersion().then(version => { * console.log('Formulus version:', version); @@ -29,299 +29,266 @@ */ const FormulusAPI = { /** - * Get the current version of the Formulus bridge API (the interface contract - * version, e.g. for {@link isCompatibleVersion} checks). - * / - * @returns {Promise} The API version (semver) - */ - getVersion: function() {}, + * Get the current version of the Formulus bridge API (the interface contract + * version, e.g. for {@link isCompatibleVersion} checks). + * @returns {Promise} The API version (semver) + */ + getVersion: function () {}, /** - * Get a list of available forms - * / - * @returns {Promise} Array of form information objects - */ - getAvailableForms: function() {}, - - /** - * Open Formplayer with the specified form - * Reserved keys are not treated as observation data: - * `defaultData` (prefill), `theme`/`darkMode`/`themeColors` (theming), and - * `context` — a read-only **session context** object (device role, selected cluster, - * ...) that Formplayer never persists and exposes to extensions as - * `window.formulusSessionContext`. Draft bypass is not a param key — use - * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). - * / - * @param {string} formType - The identifier of the formtype to open - * @param {Object} params - Additional parameters for form initialization. - * @param {Object} savedData - Previously saved form data (for editing) - * @returns {Promise} Promise that resolves when the form is completed/closed with result details - */ - openFormplayer: function(formType, params, savedData, options) {}, + * Get a list of available forms + * @returns {Promise} Array of form information objects + */ + getAvailableForms: function () {}, /** - * Get observations for a specific form - * / - * @param {string} formType - The identifier of the formtype - * @returns {Promise} Array of form observations - */ - getObservations: function(formType, isDraft, includeDeleted) {}, + * Open Formplayer with the specified form + * Reserved keys are not treated as observation data: + * `defaultData` (prefill), `theme`/`darkMode`/`themeColors` (theming), and + * `context` — a read-only **session context** object (device role, selected cluster, + * ...) that Formplayer never persists and exposes to extensions as + * `window.formulusSessionContext`. Draft bypass is not a param key — use + * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). + * @param {string} formType - The identifier of the formtype to open + * @param {Object} params - Additional parameters for form initialization. + * @param {Object} savedData - Previously saved form data (for editing) + * @returns {Promise} Promise that resolves when the form is completed/closed with result details + */ + openFormplayer: function (formType, params, savedData, options) {}, /** - * Get observations with optional WHERE clause filtering (for dynamic choice lists). - * Supports format: data.field = 'value' AND data.other = 'value' - * Age filtering via age_from_dob(data.dob) is handled client-side in formplayer. - * / - * @returns {Promise} Array of filtered observations - */ - getObservationsByQuery: function(options) {}, + * Get observations for a specific form + * @param {string} formType - The identifier of the formtype + * @returns {Promise} Array of form observations + */ + getObservations: function (formType, isDraft, includeDeleted) {}, /** - * Submit a completed form - * / - * @param {string} formType - The identifier of the formtype - * @param {Object} finalData - The final form data to submit - * @returns {Promise} The observationId of the submitted form - */ - submitObservation: function(formType, finalData) {}, + * Get observations with optional WHERE clause filtering (for dynamic choice lists). + * Supports format: data.field = 'value' AND data.other = 'value' + * Age filtering via age_from_dob(data.dob) is handled client-side in formplayer. + * @returns {Promise} Array of filtered observations + */ + getObservationsByQuery: function (options) {}, /** - * Update an existing form - * / - * @param {string} observationId - The identifier of the observation - * @param {string} formType - The identifier of the formtype - * @param {Object} finalData - The final form data to update - * @returns {Promise} The observationId of the updated form - */ - updateObservation: function(observationId, formType, finalData) {}, + * Submit a completed form + * @param {string} formType - The identifier of the formtype + * @param {Object} finalData - The final form data to submit + * @returns {Promise} The observationId of the submitted form + */ + submitObservation: function (formType, finalData) {}, /** - * Request camera access for a field - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with camera result or rejects on error/cancellation - */ - requestCamera: function(fieldId) {}, + * Update an existing form + * @param {string} observationId - The identifier of the observation + * @param {string} formType - The identifier of the formtype + * @param {Object} finalData - The final form data to update + * @returns {Promise} The observationId of the updated form + */ + updateObservation: function (observationId, formType, finalData) {}, /** - * Request location for a field (captures into the form GPS field). - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with location result or rejects on error - */ - requestLocation: function(fieldId) {}, + * Request camera access for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with camera result or rejects on error/cancellation + */ + requestCamera: function (fieldId) {}, /** - * Return the best cached device fix (if any) without forcing a new GPS read. - * / - */ - getCachedLocation: function(fieldId) {}, + * Request location for a field (captures into the form GPS field). + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with location result or rejects on error + */ + requestLocation: function (fieldId) {}, /** - * Allocate the next monotonic integer in a device-local scope. - * Formulus prepends `device:{deviceId}:` to the app-authored `scopeKey` suffix. - * / - */ - allocateSequence: function(scopeKey, options) {}, + * Return the best cached device fix (if any) without forcing a new GPS read. + */ + getCachedLocation: function (fieldId) {}, /** - * Subscribe to location updates while the custom app WebView is active. - * Updates arrive as `locationWatchUpdate` window messages. - * / - */ - watchLocation: function(fieldId) {}, + * Allocate the next monotonic integer in a device-local scope. + * Formulus prepends `device:{deviceId}:` to the app-authored `scopeKey` suffix. + */ + allocateSequence: function (scopeKey, options) {}, /** - * /** Stop a prior `watchLocation` subscription for `fieldId`. */ - */ - stopWatchLocation: function(fieldId) {}, + * Subscribe to location updates while the custom app WebView is active. + * Updates arrive as `locationWatchUpdate` window messages. + */ + watchLocation: function (fieldId) {}, /** - * Request file selection for a field - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with file result or rejects on error/cancellation - */ - requestFile: function(fieldId) {}, + * Stop a prior `watchLocation` subscription for `fieldId`. + */ + stopWatchLocation: function (fieldId) {}, /** - * Launch an external intent - * / - * @param {string} fieldId - The ID of the field - * @param {Object} intentSpec - The intent specification - * @returns {Promise} - */ - launchIntent: function(fieldId, intentSpec) {}, + * Request file selection for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with file result or rejects on error/cancellation + */ + requestFile: function (fieldId) {}, /** - * Call a subform - * / - * @param {string} fieldId - The ID of the field - * @param {string} formType - The ID of the subform - * @param {Object} options - Additional options for the subform - * @returns {Promise} - */ - callSubform: function(fieldId, formType, options) {}, + * Launch an external intent + * @param {string} fieldId - The ID of the field + * @param {Object} intentSpec - The intent specification + * @returns {Promise} + */ + launchIntent: function (fieldId, intentSpec) {}, /** - * Request audio recording for a field - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with audio result or rejects on error/cancellation - */ - requestAudio: function(fieldId) {}, + * Call a subform + * @param {string} fieldId - The ID of the field + * @param {string} formType - The ID of the subform + * @param {Object} options - Additional options for the subform + * @returns {Promise} + */ + callSubform: function (fieldId, formType, options) {}, /** - * Request video recording for a field (camera / picker — host-defined). - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with video result or rejects on error/cancellation - */ - requestVideo: function(fieldId) {}, + * Request audio recording for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with audio result or rejects on error/cancellation + */ + requestAudio: function (fieldId) {}, /** - * Request QR code scanning for a field - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with QR code result or rejects on error/cancellation - */ - requestQrcode: function(fieldId) {}, + * Request video recording for a field (camera / picker — host-defined). + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with video result or rejects on error/cancellation + */ + requestVideo: function (fieldId) {}, /** - * Request biometric authentication - * / - * @param {string} fieldId - The ID of the field - * @returns {Promise} - */ - requestBiometric: function(fieldId) {}, + * Request QR code scanning for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with QR code result or rejects on error/cancellation + */ + requestQrcode: function (fieldId) {}, /** - * Request the current connectivity status - * / - * @returns {Promise} - */ - requestConnectivityStatus: function() {}, + * Request biometric authentication + * @param {string} fieldId - The ID of the field + * @returns {Promise} + */ + requestBiometric: function (fieldId) {}, /** - * Request the current sync status - * / - * @returns {Promise} - */ - requestSyncStatus: function() {}, - - /** - * Run a local ML model - * / - * @param {string} fieldId - The ID of the field - * @param {string} modelId - The ID of the model to run - * @param {Object} input - The input data for the model - * @returns {Promise} - */ - runLocalModel: function(fieldId, modelId, input) {}, - - /** - * Get information about the currently authenticated user. - * When no one is logged in, resolves with `{ username: '' }` (does not reject). - * / - * @returns {Promise<{username: string, displayName?: string, role?: 'read-only' | 'read-write' | 'admin'} - */ - getCurrentUser: function() {}, - - /** - * Get the current theme mode (System / Light / Dark) so custom apps can match the host app. - * / - * @returns {Promise<'light' | 'dark' | 'system'>} Current theme mode; 'system' means follow device preference. - */ - getThemeMode: function() {}, - - /** - * Resolve an attachment to a WebView-loadable URL (`file://`, `http(s):`, or host-specific). - * **String `fileName`:** basename only (e.g. `photo.filename`). Lookup order, first hit wins: - * 1. `attachments/draft/` — unsaved capture (formplayer preview) - * 2. `attachments/synced/` — canonical committed / downloaded copy - * 3. `attachments/pending/` — queued for upload (fallback only) - * Legacy locations (`attachments/` and `attachments/pending_upload/`) are also checked. - * Path segments and ".." are rejected. - * **`AttachmentDisplayDescriptor`:** `{ filename }` basename only (same lookup as a string argument). - * / - * @returns {Promise} Display URL, or `null` if none - */ - getAttachmentUri: function(fileName) {}, - - /** - * Base `file://` URL for the canonical attachments directory (trailing slash). - * Returns the `synced/` subfolder — only committed/downloaded files are - * iterable from here. Drafts and the upload queue are excluded by design so - * custom apps can safely list this directory. - * **Breaking change (v2 layout):** this used to return the `attachments/` - * parent directory, which mixed committed files with `draft/` and - * `pending_upload/` subfolders. Custom apps that iterate this URL will now - * see only fully-committed attachments. - * / - * @returns {Promise} e.g. `file:///.../attachments/synced/` - */ - getAttachmentsUri: function() {}, + * Request the current connectivity status + * @returns {Promise} + */ + requestConnectivityStatus: function () {}, /** - * Base `file://` URL for the custom app bundle root (`DocumentDirectory/app/`, trailing slash). - * / - * @returns {Promise} App directory URL for extensions, question_types, etc. - */ - getCustomAppUri: function() {}, + * Request the current sync status + * @returns {Promise} + */ + requestSyncStatus: function () {}, /** - * Primary `file://` URL for downloaded form specs (`DocumentDirectory/forms/`, trailing slash). - * Some bundles also use files under the custom app `forms/` subdirectory. - * / - * @returns {Promise} Forms directory URL - */ - getFormSpecsUri: function() {}, - - /** - * Persist an observation **without opening Formplayer** (headless write). - * Intended for custom apps that create or update records programmatically. - * Uses the same persistence path as a Formplayer submit, including promotion - * of any referenced draft attachments. Provide `observationId` to update an - * existing row; omit it to create a new one. - * / - * @since 1.3.0 - * @param {PersistObservationInput} input - The observation to persist - * @returns {Promise} The stored observation id and data - */ - persistObservation: function(input) {}, + * Run a local ML model + * @param {string} fieldId - The ID of the field + * @param {string} modelId - The ID of the model to run + * @param {Object} input - The input data for the model + * @returns {Promise} + */ + runLocalModel: function (fieldId, modelId, input) {}, /** - * Trigger a synchronization with Synkronus (pull + push). - * Resolves when the sync completes; rejects if a sync is already in progress - * or the sync fails. Attachments are excluded by default for speed. - * / - * @since 1.3.0 - * @returns {Promise} The final data version after sync - */ - sync: function(options) {}, + * Get information about the currently authenticated user. + * When no one is logged in, resolves with `{ username: '' }` (does not reject). + * @returns {Promise<{username: string, displayName?: string, role?: 'read-only' | 'read-write' | 'admin'} + */ + getCurrentUser: function () {}, /** - * Probe connectivity to the configured Synkronus server (`GET /health`). - * Never rejects for an offline device — returns `{ online: false }` so callers - * can branch on connectivity without try/catch. Suitable for the - * "verify subject details when online, fall back when offline" pattern. - * / - * @since 1.3.0 - * @returns {Promise} The current connectivity status - */ - getConnectivityStatus: function() {}, - + * Get the current theme mode (System / Light / Dark) so custom apps can match the host app. + * @returns {Promise<'light' | 'dark' | 'system'>} Current theme mode; 'system' means follow device preference. + */ + getThemeMode: function () {}, + /** - * Read the device's last-known Synkronus data revision (`current_version` - * from the most recent successful sync). Reflects server-stream alignment - * only — not unsynced local edits. Use with periodic polling or after - * {@link sync} to detect when another device has pushed changes. - * / - * @since 1.4.0 - * @returns {Promise} Non-negative revision count (0 if never synced) - */ - getCurrentDataRevisionCount: function() {}, - + * Resolve an attachment to a WebView-loadable URL (`file://`, `http(s):`, or host-specific). + * **String `fileName`:** basename only (e.g. `photo.filename`). Lookup order, first hit wins: + * 1. `attachments/draft/` — unsaved capture (formplayer preview) + * 2. `attachments/synced/` — canonical committed / downloaded copy + * 3. `attachments/pending/` — queued for upload (fallback only) + * Legacy locations (`attachments/` and `attachments/pending_upload/`) are also checked. + * Path segments and ".." are rejected. + * **`AttachmentDisplayDescriptor`:** `{ filename }` basename only (same lookup as a string argument). + * @returns {Promise} Display URL, or `null` if none + */ + getAttachmentUri: function (fileName) {}, + + /** + * Base `file://` URL for the canonical attachments directory (trailing slash). + * Returns the `synced/` subfolder — only committed/downloaded files are + * iterable from here. Drafts and the upload queue are excluded by design so + * custom apps can safely list this directory. + * **Breaking change (v2 layout):** this used to return the `attachments/` + * parent directory, which mixed committed files with `draft/` and + * `pending_upload/` subfolders. Custom apps that iterate this URL will now + * see only fully-committed attachments. + * @returns {Promise} e.g. `file:///.../attachments/synced/` + */ + getAttachmentsUri: function () {}, + + /** + * Base `file://` URL for the custom app bundle root (`DocumentDirectory/app/`, trailing slash). + * @returns {Promise} App directory URL for extensions, question_types, etc. + */ + getCustomAppUri: function () {}, + + /** + * Primary `file://` URL for downloaded form specs (`DocumentDirectory/forms/`, trailing slash). + * Some bundles also use files under the custom app `forms/` subdirectory. + * @returns {Promise} Forms directory URL + */ + getFormSpecsUri: function () {}, + + /** + * Persist an observation **without opening Formplayer** (headless write). + * Intended for custom apps that create or update records programmatically. + * Uses the same persistence path as a Formplayer submit, including promotion + * of any referenced draft attachments. Provide `observationId` to update an + * existing row; omit it to create a new one. + * @since 1.3.0 + * @param {PersistObservationInput} input - The observation to persist + * @returns {Promise} The stored observation id and data + */ + persistObservation: function (input) {}, + + /** + * Trigger a synchronization with Synkronus (pull + push). + * Resolves when the sync completes; rejects if a sync is already in progress + * or the sync fails. Attachments are excluded by default for speed. + * @since 1.3.0 + * @returns {Promise} The final data version after sync + */ + sync: function (options) {}, + + /** + * Probe connectivity to the configured Synkronus server (`GET /health`). + * Never rejects for an offline device — returns `{ online: false }` so callers + * can branch on connectivity without try/catch. Suitable for the + * "verify subject details when online, fall back when offline" pattern. + * @since 1.3.0 + * @returns {Promise} The current connectivity status + */ + getConnectivityStatus: function () {}, + + /** + * Read the device's last-known Synkronus data revision (`current_version` + * from the most recent successful sync). Reflects server-stream alignment + * only — not unsynced local edits. Use with periodic polling or after + * {@link sync} to detect when another device has pushed changes. + * @since 1.4.0 + * @returns {Promise} Non-negative revision count (0 if never synced) + */ + getCurrentDataRevisionCount: function () {}, }; // Make the API available globally in browser environments diff --git a/formulus/scripts/generateInjectionScript.ts b/formulus/scripts/generateInjectionScript.ts index c11897b7c..3cc4140e6 100644 --- a/formulus/scripts/generateInjectionScript.ts +++ b/formulus/scripts/generateInjectionScript.ts @@ -474,11 +474,22 @@ function extractMethods(sourceFile: ts.SourceFile): MethodInfo[] { jsDocText.end, ); - // Parse the JSDoc to extract the description and tags + // Parse the JSDoc to extract the description and tags. + // Strip the comment delimiters up-front so single-line comments + // (`/** text */`) and inline closings don't leak into the parsed + // description — otherwise we emit nested/duplicated `*/`, producing + // malformed (and sometimes unparseable) generated JSDoc. const lines = rawJsDoc + .replace(/^\s*\/\*\*+/, '') // opening /** (or /***) + .replace(/\*+\/\s*$/, '') // closing */ .split('\n') - .map(line => line.trim().replace(/^\* ?/, '').trim()) - .filter(line => line !== '/**' && line !== '*/'); + .map(line => + line + .trim() + .replace(/^\*+ ?/, '') + .trim(), + ) + .filter(line => line.length > 0); const description: string[] = []; const tags: JSDocTag[] = []; diff --git a/formulus/src/webview/FormulusMessageHandlers.ts b/formulus/src/webview/FormulusMessageHandlers.ts index a75244d88..caa3f61ae 100644 --- a/formulus/src/webview/FormulusMessageHandlers.ts +++ b/formulus/src/webview/FormulusMessageHandlers.ts @@ -724,6 +724,7 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers { scopeKey?: string; startAt?: number; peek?: boolean; + options?: { startAt?: number; peek?: boolean }; }, ): Promise => { const scopeKey = @@ -732,14 +733,14 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers { : typeof payload?.scopeKey === 'string' ? payload.scopeKey : ''; - const startAt = + // The WebView injection nests options under `options`; fall back to flat + // top-level fields for back-compat with older/string callers. + const opts = typeof payload === 'object' && payload != null - ? payload.startAt - : undefined; - const peek = - typeof payload === 'object' && payload != null - ? payload.peek + ? (payload.options ?? payload) : undefined; + const startAt = opts?.startAt; + const peek = opts?.peek; return sequenceCounterService.allocate(scopeKey, { startAt, peek }); }, From 8f04f1823395933150e20447f24515ddb240b8fc Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Fri, 19 Jun 2026 17:04:01 +0000 Subject: [PATCH 2/3] fix(formplayer): edit handling --- desktop/src/lib/formPreviewBridge.ts | 13 +- desktop/src/pages/WorkbenchCustomAppPage.tsx | 10 +- .../src/types/FormulusInterfaceDefinition.ts | 4 + .../assets/webview/FormulusInjectionScript.js | 3387 +++++++---------- formulus/assets/webview/formulus-api.js | 434 +-- .../webview/FormulusInterfaceDefinition.ts | 4 + .../src/webview/FormulusMessageHandlers.ts | 5 +- 7 files changed, 1706 insertions(+), 2151 deletions(-) diff --git a/desktop/src/lib/formPreviewBridge.ts b/desktop/src/lib/formPreviewBridge.ts index adcf2e56a..af03ac3d1 100644 --- a/desktop/src/lib/formPreviewBridge.ts +++ b/desktop/src/lib/formPreviewBridge.ts @@ -134,6 +134,7 @@ export type FormPreviewBridgeContext = { formType: string; params: Record; savedData: Record; + observationId?: string; }) => void; /** * Form preview: defer `openFormplayer_response` until nested finalize/cancel. @@ -423,9 +424,14 @@ export async function handleFormPreviewBridgeMessage( subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; + observationId?: string | null; } | undefined; const subObservationMode = Boolean(options?.subObservationMode); + const observationId = + typeof options?.observationId === 'string' + ? options.observationId.trim() + : ''; if (subObservationMode && ctx.onDeferOpenSubObservation) { const parentIframe = resolveBridgeReplyIframe(eventSource, ctx); @@ -455,7 +461,12 @@ export async function handleFormPreviewBridgeMessage( } if (ctx.onOpenFormplayerNavigate) { - ctx.onOpenFormplayerNavigate({ formType, params, savedData }); + ctx.onOpenFormplayerNavigate({ + formType, + params, + savedData, + ...(observationId ? { observationId } : {}), + }); reply('openFormplayer', { result: { status: 'cancelled', diff --git a/desktop/src/pages/WorkbenchCustomAppPage.tsx b/desktop/src/pages/WorkbenchCustomAppPage.tsx index 5261ed4ce..a832707f7 100644 --- a/desktop/src/pages/WorkbenchCustomAppPage.tsx +++ b/desktop/src/pages/WorkbenchCustomAppPage.tsx @@ -57,14 +57,20 @@ export function WorkbenchCustomAppPage() { formType: string; params: Record; savedData: Record; + observationId?: string; }) => { + const explicitId = + typeof payload.observationId === 'string' + ? payload.observationId.trim() + : ''; const sd = payload.savedData; const observationId = - typeof sd.observationId === 'string' + explicitId || + (typeof sd.observationId === 'string' ? sd.observationId : typeof sd.id === 'string' ? sd.id - : ''; + : ''); navigate('/workbench/form-preview', { state: { formPreviewEdit: { diff --git a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts index a4028f8f5..3be0c2205 100644 --- a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts +++ b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts @@ -394,6 +394,9 @@ export interface FormulusInterface { * `window.formulusSessionContext`. Draft bypass is not a param key — use * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). * @param {Object} savedData - Previously saved form data (for editing) + * @param {Object} [options] - Session options (not persisted as observation data) + * @param {string|null} [options.observationId] - When set, finalize updates this + * observation instead of creating a new one * @returns {Promise} Promise that resolves when the form is completed/closed with result details */ openFormplayer( @@ -404,6 +407,7 @@ export interface FormulusInterface { subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; + observationId?: string | null; }, ): Promise; diff --git a/formulus/assets/webview/FormulusInjectionScript.js b/formulus/assets/webview/FormulusInjectionScript.js index 844fc7a2e..5c071dd1e 100644 --- a/formulus/assets/webview/FormulusInjectionScript.js +++ b/formulus/assets/webview/FormulusInjectionScript.js @@ -1,51 +1,38 @@ // Auto-generated from FormulusInterfaceDefinition.ts // Do not edit directly - this file will be overwritten -// Last generated: 2026-06-19T16:46:35.984Z +// Last generated: 2026-06-19T16:58:11.324Z -(function () { +(function() { // Enhanced API availability detection and recovery function getFormulus() { // Check multiple locations where the API might exist - return ( - globalThis.formulus || - (typeof window !== 'undefined' ? window.formulus : undefined) - ); + return globalThis.formulus || (typeof window !== 'undefined' ? window.formulus : undefined); } function isFormulusAvailable() { const api = getFormulus(); - return ( - api && typeof api === 'object' && typeof api.getVersion === 'function' - ); + return api && typeof api === 'object' && typeof api.getVersion === 'function'; } // Idempotent guard to avoid double-initialization when scripts are reinjected - if (globalThis.__formulusBridgeInitialized) { + if ((globalThis).__formulusBridgeInitialized) { if (isFormulusAvailable()) { - console.debug( - 'Formulus bridge already initialized and functional. Skipping duplicate injection.', - ); + console.debug('Formulus bridge already initialized and functional. Skipping duplicate injection.'); return; } else { - console.warn( - 'Formulus bridge flag is set but API is not functional. Proceeding with re-injection...', - ); + console.warn('Formulus bridge flag is set but API is not functional. Proceeding with re-injection...'); } } // If API already exists and is functional, skip injection if (isFormulusAvailable()) { - console.debug( - 'Formulus interface already exists and is functional. Skipping injection.', - ); + console.debug('Formulus interface already exists and is functional. Skipping injection.'); return; } // If API exists but is not functional, log warning and proceed with re-injection if (getFormulus()) { - console.warn( - 'Formulus interface exists but appears non-functional. Re-injecting...', - ); + console.warn('Formulus interface exists but appears non-functional. Re-injecting...'); } // Helper function to handle callbacks @@ -61,7 +48,7 @@ // Initialize callbacks const callbacks = {}; - + // Global function to handle responses from React Native function handleMessage(event) { try { @@ -74,2087 +61,1625 @@ // console.warn('Global handleMessage: Received message with unexpected data type:', typeof event.data, event.data); return; // Or handle error, but for now, just return to avoid breaking others. } - + // Handle callbacks - if ( - data.type === 'callback' && - data.callbackId && - callbacks[data.callbackId] - ) { + if (data.type === 'callback' && data.callbackId && callbacks[data.callbackId]) { handleCallback(callbacks[data.callbackId], data.data); delete callbacks[data.callbackId]; } - + // Handle specific callbacks - - if ( - data.type === 'onFormulusReady' && - globalThis.formulusCallbacks?.onFormulusReady - ) { + + + if (data.type === 'onFormulusReady' && globalThis.formulusCallbacks?.onFormulusReady) { handleCallback(globalThis.formulusCallbacks.onFormulusReady); } } catch (e) { - console.error( - 'Global handleMessage: Error processing message:', - e, - 'Raw event.data:', - event.data, - ); + console.error('Global handleMessage: Error processing message:', e, 'Raw event.data:', event.data); } } - + // Set up message listener document.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage); // Initialize the formulus interface globalThis.formulus = { - // getVersion: => Promise - getVersion: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getVersion callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getVersion callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getVersion_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + // getVersion: => Promise + getVersion: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getVersion callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getVersion callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getVersion' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getVersion_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getVersion' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getVersion', messageId, - }), - ); - }); - }, - - // getAvailableForms: => Promise - getAvailableForms: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getAvailableForms callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getAvailableForms callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getAvailableForms_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getAvailableForms: => Promise + getAvailableForms: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getAvailableForms callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getAvailableForms callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getAvailableForms' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getAvailableForms_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getAvailableForms' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getAvailableForms', messageId, - }), - ); - }); - }, - - // openFormplayer: formType: string, params: Record, savedData: Record, options: { subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; } => Promise - openFormplayer: function (formType, params, savedData, options) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('openFormplayer callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'openFormplayer callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'openFormplayer_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // openFormplayer: formType: string, params: Record, savedData: Record, options: { subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; observationId?: string; } => Promise + openFormplayer: function(formType, params, savedData, options) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('openFormplayer callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('openFormplayer callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'openFormplayer' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'openFormplayer_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'openFormplayer' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'openFormplayer', messageId, - formType: formType, + formType: formType, params: params, savedData: savedData, - options: options, - }), - ); - }); - }, - - // getObservations: formType: string, isDraft: boolean, includeDeleted: boolean => Promise - getObservations: function (formType, isDraft, includeDeleted) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getObservations callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getObservations callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getObservations_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + options: options + })); + + }); + }, + + // getObservations: formType: string, isDraft: boolean, includeDeleted: boolean => Promise + getObservations: function(formType, isDraft, includeDeleted) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getObservations callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getObservations callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getObservations' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getObservations_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getObservations' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getObservations', messageId, - formType: formType, + formType: formType, isDraft: isDraft, - includeDeleted: includeDeleted, - }), - ); - }); - }, - - // getObservationsByQuery: options: { formType: string; isDraft?: boolean; includeDeleted?: boolean; filter?: ObservationFilter; whereClause?: string; } => Promise - getObservationsByQuery: function (options) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getObservationsByQuery callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getObservationsByQuery callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getObservationsByQuery_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + includeDeleted: includeDeleted + })); + + }); + }, + + // getObservationsByQuery: options: { formType: string; isDraft?: boolean; includeDeleted?: boolean; filter?: ObservationFilter; whereClause?: string; } => Promise + getObservationsByQuery: function(options) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getObservationsByQuery callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getObservationsByQuery callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getObservationsByQuery' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getObservationsByQuery_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getObservationsByQuery' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getObservationsByQuery', messageId, - options: options, - }), - ); - }); - }, - - // submitObservation: formType: string, finalData: Record => Promise - submitObservation: function (formType, finalData) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('submitObservation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'submitObservation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'submitObservation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + options: options + })); + + }); + }, + + // submitObservation: formType: string, finalData: Record => Promise + submitObservation: function(formType, finalData) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('submitObservation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('submitObservation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'submitObservation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'submitObservation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'submitObservation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'submitObservation', messageId, - formType: formType, - finalData: finalData, - }), - ); - }); - }, - - // updateObservation: observationId: string, formType: string, finalData: Record => Promise - updateObservation: function (observationId, formType, finalData) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('updateObservation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'updateObservation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'updateObservation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + formType: formType, + finalData: finalData + })); + + }); + }, + + // updateObservation: observationId: string, formType: string, finalData: Record => Promise + updateObservation: function(observationId, formType, finalData) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('updateObservation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('updateObservation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'updateObservation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'updateObservation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'updateObservation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'updateObservation', messageId, - observationId: observationId, + observationId: observationId, formType: formType, - finalData: finalData, - }), - ); - }); - }, - - // requestCamera: fieldId: string => Promise - requestCamera: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestCamera callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestCamera callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestCamera_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + finalData: finalData + })); + + }); + }, + + // requestCamera: fieldId: string => Promise + requestCamera: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestCamera callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestCamera callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestCamera' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestCamera_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestCamera' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestCamera', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestLocation: fieldId: string => Promise - requestLocation: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestLocation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestLocation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestLocation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestLocation: fieldId: string => Promise + requestLocation: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestLocation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestLocation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestLocation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestLocation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestLocation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestLocation', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // getCachedLocation: fieldId: string => Promise - getCachedLocation: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getCachedLocation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getCachedLocation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getCachedLocation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // getCachedLocation: fieldId: string => Promise + getCachedLocation: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getCachedLocation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getCachedLocation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getCachedLocation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getCachedLocation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getCachedLocation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getCachedLocation', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // allocateSequence: scopeKey: string, options: { startAt?: number; peek?: boolean; } => Promise - allocateSequence: function (scopeKey, options) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('allocateSequence callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'allocateSequence callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'allocateSequence_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // allocateSequence: scopeKey: string, options: { startAt?: number; peek?: boolean; } => Promise + allocateSequence: function(scopeKey, options) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('allocateSequence callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('allocateSequence callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'allocateSequence' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'allocateSequence_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'allocateSequence' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'allocateSequence', messageId, - scopeKey: scopeKey, - options: options, - }), - ); - }); - }, - - // watchLocation: fieldId: string => Promise<{ status: "started" | "error"; message?: string; }> - watchLocation: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('watchLocation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'watchLocation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'watchLocation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + scopeKey: scopeKey, + options: options + })); + + }); + }, + + // watchLocation: fieldId: string => Promise<{ status: "started" | "error"; message?: string; }> + watchLocation: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('watchLocation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('watchLocation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'watchLocation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'watchLocation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'watchLocation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'watchLocation', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // stopWatchLocation: fieldId: string => Promise - stopWatchLocation: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('stopWatchLocation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'stopWatchLocation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'stopWatchLocation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // stopWatchLocation: fieldId: string => Promise + stopWatchLocation: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('stopWatchLocation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('stopWatchLocation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'stopWatchLocation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'stopWatchLocation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'stopWatchLocation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'stopWatchLocation', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestFile: fieldId: string => Promise - requestFile: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestFile callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestFile callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestFile_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestFile: fieldId: string => Promise + requestFile: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestFile callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestFile callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestFile' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestFile_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestFile' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestFile', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // launchIntent: fieldId: string, intentSpec: Record => Promise - launchIntent: function (fieldId, intentSpec) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('launchIntent callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'launchIntent callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'launchIntent_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // launchIntent: fieldId: string, intentSpec: Record => Promise + launchIntent: function(fieldId, intentSpec) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('launchIntent callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('launchIntent callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'launchIntent' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'launchIntent_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'launchIntent' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'launchIntent', messageId, - fieldId: fieldId, - intentSpec: intentSpec, - }), - ); - }); - }, - - // callSubform: fieldId: string, formType: string, options: Record => Promise - callSubform: function (fieldId, formType, options) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('callSubform callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'callSubform callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'callSubform_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId, + intentSpec: intentSpec + })); + + }); + }, + + // callSubform: fieldId: string, formType: string, options: Record => Promise + callSubform: function(fieldId, formType, options) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('callSubform callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('callSubform callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'callSubform' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'callSubform_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'callSubform' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'callSubform', messageId, - fieldId: fieldId, + fieldId: fieldId, formType: formType, - options: options, - }), - ); - }); - }, - - // requestAudio: fieldId: string => Promise - requestAudio: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestAudio callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestAudio callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestAudio_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + options: options + })); + + }); + }, + + // requestAudio: fieldId: string => Promise + requestAudio: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestAudio callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestAudio callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestAudio' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestAudio_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestAudio' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestAudio', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestVideo: fieldId: string => Promise - requestVideo: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestVideo callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestVideo callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestVideo_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestVideo: fieldId: string => Promise + requestVideo: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestVideo callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestVideo callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestVideo' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestVideo_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestVideo' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestVideo', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestQrcode: fieldId: string => Promise - requestQrcode: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestQrcode callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestQrcode callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestQrcode_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestQrcode: fieldId: string => Promise + requestQrcode: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestQrcode callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestQrcode callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestQrcode' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestQrcode_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestQrcode' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestQrcode', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestBiometric: fieldId: string => Promise - requestBiometric: function (fieldId) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestBiometric callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestBiometric callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestBiometric_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestBiometric: fieldId: string => Promise + requestBiometric: function(fieldId) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestBiometric callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestBiometric callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestBiometric' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestBiometric_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestBiometric' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestBiometric', messageId, - fieldId: fieldId, - }), - ); - }); - }, - - // requestConnectivityStatus: => Promise - requestConnectivityStatus: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestConnectivityStatus callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestConnectivityStatus callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestConnectivityStatus_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fieldId: fieldId + })); + + }); + }, + + // requestConnectivityStatus: => Promise + requestConnectivityStatus: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestConnectivityStatus callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestConnectivityStatus callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestConnectivityStatus' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestConnectivityStatus_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestConnectivityStatus' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestConnectivityStatus', messageId, - }), - ); - }); - }, - - // requestSyncStatus: => Promise - requestSyncStatus: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('requestSyncStatus callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'requestSyncStatus callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'requestSyncStatus_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // requestSyncStatus: => Promise + requestSyncStatus: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('requestSyncStatus callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('requestSyncStatus callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'requestSyncStatus' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'requestSyncStatus_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'requestSyncStatus' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'requestSyncStatus', messageId, - }), - ); - }); - }, - - // runLocalModel: fieldId: string, modelId: string, input: Record => Promise - runLocalModel: function (fieldId, modelId, input) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('runLocalModel callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'runLocalModel callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'runLocalModel_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // runLocalModel: fieldId: string, modelId: string, input: Record => Promise + runLocalModel: function(fieldId, modelId, input) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('runLocalModel callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('runLocalModel callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'runLocalModel' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'runLocalModel_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'runLocalModel' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'runLocalModel', messageId, - fieldId: fieldId, + fieldId: fieldId, modelId: modelId, - input: input, - }), - ); - }); - }, - - // getCurrentUser: => Promise<{ username: string; displayName?: string; role?: "read-only" | "read-write" | "admin"; }> - getCurrentUser: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getCurrentUser callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getCurrentUser callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getCurrentUser_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + input: input + })); + + }); + }, + + // getCurrentUser: => Promise<{ username: string; displayName?: string; role?: "read-only" | "read-write" | "admin"; }> + getCurrentUser: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getCurrentUser callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getCurrentUser callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getCurrentUser' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getCurrentUser_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getCurrentUser' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getCurrentUser', messageId, - }), - ); - }); - }, - - // getThemeMode: => Promise<"light" | "dark" | "system"> - getThemeMode: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getThemeMode callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getThemeMode callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getThemeMode_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getThemeMode: => Promise<"light" | "dark" | "system"> + getThemeMode: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getThemeMode callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getThemeMode callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getThemeMode' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getThemeMode_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getThemeMode' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getThemeMode', messageId, - }), - ); - }); - }, - - // getAttachmentUri: fileName: string | AttachmentDisplayDescriptor => Promise - getAttachmentUri: function (fileName) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getAttachmentUri callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getAttachmentUri callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getAttachmentUri_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getAttachmentUri: fileName: string | AttachmentDisplayDescriptor => Promise + getAttachmentUri: function(fileName) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getAttachmentUri callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getAttachmentUri callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getAttachmentUri' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getAttachmentUri_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getAttachmentUri' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getAttachmentUri', messageId, - fileName: fileName, - }), - ); - }); - }, - - // getAttachmentsUri: => Promise - getAttachmentsUri: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getAttachmentsUri callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getAttachmentsUri callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getAttachmentsUri_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + fileName: fileName + })); + + }); + }, + + // getAttachmentsUri: => Promise + getAttachmentsUri: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getAttachmentsUri callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getAttachmentsUri callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getAttachmentsUri' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getAttachmentsUri_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getAttachmentsUri' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getAttachmentsUri', messageId, - }), - ); - }); - }, - - // getCustomAppUri: => Promise - getCustomAppUri: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getCustomAppUri callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getCustomAppUri callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getCustomAppUri_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getCustomAppUri: => Promise + getCustomAppUri: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getCustomAppUri callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getCustomAppUri callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getCustomAppUri' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getCustomAppUri_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getCustomAppUri' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getCustomAppUri', messageId, - }), - ); - }); - }, - - // getFormSpecsUri: => Promise - getFormSpecsUri: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getFormSpecsUri callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getFormSpecsUri callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getFormSpecsUri_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getFormSpecsUri: => Promise + getFormSpecsUri: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getFormSpecsUri callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getFormSpecsUri callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getFormSpecsUri' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getFormSpecsUri_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getFormSpecsUri' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getFormSpecsUri', messageId, - }), - ); - }); - }, - - // persistObservation: input: PersistObservationInput => Promise - persistObservation: function (input) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('persistObservation callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'persistObservation callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'persistObservation_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // persistObservation: input: PersistObservationInput => Promise + persistObservation: function(input) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('persistObservation callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('persistObservation callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'persistObservation' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'persistObservation_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'persistObservation' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'persistObservation', messageId, - input: input, - }), - ); - }); - }, - - // sync: options: { includeAttachments?: boolean; } => Promise - sync: function (options) { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('sync callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'sync callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if (data.type === 'sync_response' && data.messageId === messageId) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + input: input + })); + + }); + }, + + // sync: options: { includeAttachments?: boolean; } => Promise + sync: function(options) { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('sync callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('sync callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'sync' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'sync_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'sync' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'sync', messageId, - options: options, - }), - ); - }); - }, - - // getConnectivityStatus: => Promise - getConnectivityStatus: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getConnectivityStatus callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getConnectivityStatus callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getConnectivityStatus_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + options: options + })); + + }); + }, + + // getConnectivityStatus: => Promise + getConnectivityStatus: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getConnectivityStatus callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getConnectivityStatus callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getConnectivityStatus' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getConnectivityStatus_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getConnectivityStatus' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getConnectivityStatus', messageId, - }), - ); - }); - }, - - // getCurrentDataRevisionCount: => Promise - getCurrentDataRevisionCount: function () { - return new Promise((resolve, reject) => { - const messageId = - 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - - // Add response handler for methods that return values - - const callback = event => { - try { - let data; - if (typeof event.data === 'string') { - data = JSON.parse(event.data); - } else if (typeof event.data === 'object' && event.data !== null) { - data = event.data; // Already an object - } else { - // console.warn('getCurrentDataRevisionCount callback: Received response with unexpected data type:', typeof event.data, event.data); - window.removeEventListener('message', callback); // Clean up listener - reject( - new Error( - 'getCurrentDataRevisionCount callback: Received response with unexpected data type. Raw: ' + - String(event.data), - ), - ); - return; - } - if ( - data.type === 'getCurrentDataRevisionCount_response' && - data.messageId === messageId - ) { - window.removeEventListener('message', callback); - if (data.error) { - reject(new Error(data.error)); + + })); + + }); + }, + + // getCurrentDataRevisionCount: => Promise + getCurrentDataRevisionCount: function() { + return new Promise((resolve, reject) => { + const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); + + // Add response handler for methods that return values + + const callback = (event) => { + try { + let data; + if (typeof event.data === 'string') { + data = JSON.parse(event.data); + } else if (typeof event.data === 'object' && event.data !== null) { + data = event.data; // Already an object } else { - resolve(data.result); + // console.warn('getCurrentDataRevisionCount callback: Received response with unexpected data type:', typeof event.data, event.data); + window.removeEventListener('message', callback); // Clean up listener + reject(new Error('getCurrentDataRevisionCount callback: Received response with unexpected data type. Raw: ' + String(event.data))); + return; } - } - } catch (e) { - console.error( - "'getCurrentDataRevisionCount' callback: Error processing response:", - e, - 'Raw event.data:', - event.data, - ); - window.removeEventListener('message', callback); // Ensure listener is removed on error too - reject(e); - } - }; - window.addEventListener('message', callback); - - // Send the message to React Native - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ + if (data.type === 'getCurrentDataRevisionCount_response' && data.messageId === messageId) { + window.removeEventListener('message', callback); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(data.result); + } + } + } catch (e) { + console.error("'getCurrentDataRevisionCount' callback: Error processing response:" , e, "Raw event.data:", event.data); + window.removeEventListener('message', callback); // Ensure listener is removed on error too + reject(e); + } + }; + window.addEventListener('message', callback); + + + // Send the message to React Native + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ type: 'getCurrentDataRevisionCount', messageId, - }), - ); - }); - }, + + })); + + }); + }, }; - + // Register the callback handler with the window object globalThis.formulusCallbacks = {}; - + // Notify that the interface is ready console.log('Formulus interface initialized'); - globalThis.__formulusBridgeInitialized = true; + (globalThis).__formulusBridgeInitialized = true; // Simple API availability check for internal use function requestApiReinjection() { console.log('Formulus: Requesting re-injection from host...'); if (globalThis.ReactNativeWebView) { - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ - type: 'requestApiReinjection', - timestamp: Date.now(), - }), - ); + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'requestApiReinjection', + timestamp: Date.now() + })); } } globalThis.__formulusRequestApiReinjection = requestApiReinjection; // Notify React Native that the interface is ready if (globalThis.ReactNativeWebView) { - globalThis.ReactNativeWebView.postMessage( - JSON.stringify({ - type: 'onFormulusReady', - }), - ); + globalThis.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'onFormulusReady' + })); } - + // Make the API available globally in browser environments if (typeof window !== 'undefined') { window.formulus = globalThis.formulus; } + })(); diff --git a/formulus/assets/webview/formulus-api.js b/formulus/assets/webview/formulus-api.js index 2c2ab2ed6..2aee71918 100644 --- a/formulus/assets/webview/formulus-api.js +++ b/formulus/assets/webview/formulus-api.js @@ -1,16 +1,16 @@ /** * Formulus API Interface (JavaScript Version) - * + * * This file provides type information and documentation for the Formulus API * that's available in the WebView context as `globalThis.formulus`. - * + * * This file is auto-generated from FormulusInterfaceDefinition.ts - * Last generated: 2026-06-19T16:46:36.433Z - * + * Last generated: 2026-06-19T16:58:11.756Z + * * @example * // In your JavaScript file: * /// - * + * * // Now you'll get autocompletion and type hints in IDEs that support JSDoc * globalThis.formulus.getVersion().then(version => { * console.log('Formulus version:', version); @@ -29,266 +29,268 @@ */ const FormulusAPI = { /** - * Get the current version of the Formulus bridge API (the interface contract - * version, e.g. for {@link isCompatibleVersion} checks). - * @returns {Promise} The API version (semver) - */ - getVersion: function () {}, + * Get the current version of the Formulus bridge API (the interface contract + * version, e.g. for {@link isCompatibleVersion} checks). + * @returns {Promise} The API version (semver) + */ + getVersion: function() {}, /** - * Get a list of available forms - * @returns {Promise} Array of form information objects - */ - getAvailableForms: function () {}, + * Get a list of available forms + * @returns {Promise} Array of form information objects + */ + getAvailableForms: function() {}, + + /** + * Open Formplayer with the specified form + * Reserved keys are not treated as observation data: + * `defaultData` (prefill), `theme`/`darkMode`/`themeColors` (theming), and + * `context` — a read-only **session context** object (device role, selected cluster, + * ...) that Formplayer never persists and exposes to extensions as + * `window.formulusSessionContext`. Draft bypass is not a param key — use + * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). + * observation instead of creating a new one + * @param {string} formType - The identifier of the formtype to open + * @param {Object} params - Additional parameters for form initialization. + * @param {Object} savedData - Previously saved form data (for editing) + * @returns {Promise} Promise that resolves when the form is completed/closed with result details + */ + openFormplayer: function(formType, params, savedData, options) {}, /** - * Open Formplayer with the specified form - * Reserved keys are not treated as observation data: - * `defaultData` (prefill), `theme`/`darkMode`/`themeColors` (theming), and - * `context` — a read-only **session context** object (device role, selected cluster, - * ...) that Formplayer never persists and exposes to extensions as - * `window.formulusSessionContext`. Draft bypass is not a param key — use - * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). - * @param {string} formType - The identifier of the formtype to open - * @param {Object} params - Additional parameters for form initialization. - * @param {Object} savedData - Previously saved form data (for editing) - * @returns {Promise} Promise that resolves when the form is completed/closed with result details - */ - openFormplayer: function (formType, params, savedData, options) {}, + * Get observations for a specific form + * @param {string} formType - The identifier of the formtype + * @returns {Promise} Array of form observations + */ + getObservations: function(formType, isDraft, includeDeleted) {}, /** - * Get observations for a specific form - * @param {string} formType - The identifier of the formtype - * @returns {Promise} Array of form observations - */ - getObservations: function (formType, isDraft, includeDeleted) {}, + * Get observations with optional WHERE clause filtering (for dynamic choice lists). + * Supports format: data.field = 'value' AND data.other = 'value' + * Age filtering via age_from_dob(data.dob) is handled client-side in formplayer. + * @returns {Promise} Array of filtered observations + */ + getObservationsByQuery: function(options) {}, /** - * Get observations with optional WHERE clause filtering (for dynamic choice lists). - * Supports format: data.field = 'value' AND data.other = 'value' - * Age filtering via age_from_dob(data.dob) is handled client-side in formplayer. - * @returns {Promise} Array of filtered observations - */ - getObservationsByQuery: function (options) {}, + * Submit a completed form + * @param {string} formType - The identifier of the formtype + * @param {Object} finalData - The final form data to submit + * @returns {Promise} The observationId of the submitted form + */ + submitObservation: function(formType, finalData) {}, /** - * Submit a completed form - * @param {string} formType - The identifier of the formtype - * @param {Object} finalData - The final form data to submit - * @returns {Promise} The observationId of the submitted form - */ - submitObservation: function (formType, finalData) {}, + * Update an existing form + * @param {string} observationId - The identifier of the observation + * @param {string} formType - The identifier of the formtype + * @param {Object} finalData - The final form data to update + * @returns {Promise} The observationId of the updated form + */ + updateObservation: function(observationId, formType, finalData) {}, /** - * Update an existing form - * @param {string} observationId - The identifier of the observation - * @param {string} formType - The identifier of the formtype - * @param {Object} finalData - The final form data to update - * @returns {Promise} The observationId of the updated form - */ - updateObservation: function (observationId, formType, finalData) {}, + * Request camera access for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with camera result or rejects on error/cancellation + */ + requestCamera: function(fieldId) {}, /** - * Request camera access for a field - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with camera result or rejects on error/cancellation - */ - requestCamera: function (fieldId) {}, + * Request location for a field (captures into the form GPS field). + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with location result or rejects on error + */ + requestLocation: function(fieldId) {}, /** - * Request location for a field (captures into the form GPS field). - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with location result or rejects on error - */ - requestLocation: function (fieldId) {}, + * Return the best cached device fix (if any) without forcing a new GPS read. + */ + getCachedLocation: function(fieldId) {}, /** - * Return the best cached device fix (if any) without forcing a new GPS read. - */ - getCachedLocation: function (fieldId) {}, + * Allocate the next monotonic integer in a device-local scope. + * Formulus prepends `device:{deviceId}:` to the app-authored `scopeKey` suffix. + */ + allocateSequence: function(scopeKey, options) {}, /** - * Allocate the next monotonic integer in a device-local scope. - * Formulus prepends `device:{deviceId}:` to the app-authored `scopeKey` suffix. - */ - allocateSequence: function (scopeKey, options) {}, + * Subscribe to location updates while the custom app WebView is active. + * Updates arrive as `locationWatchUpdate` window messages. + */ + watchLocation: function(fieldId) {}, /** - * Subscribe to location updates while the custom app WebView is active. - * Updates arrive as `locationWatchUpdate` window messages. - */ - watchLocation: function (fieldId) {}, + * Stop a prior `watchLocation` subscription for `fieldId`. + */ + stopWatchLocation: function(fieldId) {}, /** - * Stop a prior `watchLocation` subscription for `fieldId`. - */ - stopWatchLocation: function (fieldId) {}, + * Request file selection for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with file result or rejects on error/cancellation + */ + requestFile: function(fieldId) {}, /** - * Request file selection for a field - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with file result or rejects on error/cancellation - */ - requestFile: function (fieldId) {}, + * Launch an external intent + * @param {string} fieldId - The ID of the field + * @param {Object} intentSpec - The intent specification + * @returns {Promise} + */ + launchIntent: function(fieldId, intentSpec) {}, /** - * Launch an external intent - * @param {string} fieldId - The ID of the field - * @param {Object} intentSpec - The intent specification - * @returns {Promise} - */ - launchIntent: function (fieldId, intentSpec) {}, + * Call a subform + * @param {string} fieldId - The ID of the field + * @param {string} formType - The ID of the subform + * @param {Object} options - Additional options for the subform + * @returns {Promise} + */ + callSubform: function(fieldId, formType, options) {}, /** - * Call a subform - * @param {string} fieldId - The ID of the field - * @param {string} formType - The ID of the subform - * @param {Object} options - Additional options for the subform - * @returns {Promise} - */ - callSubform: function (fieldId, formType, options) {}, + * Request audio recording for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with audio result or rejects on error/cancellation + */ + requestAudio: function(fieldId) {}, /** - * Request audio recording for a field - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with audio result or rejects on error/cancellation - */ - requestAudio: function (fieldId) {}, + * Request video recording for a field (camera / picker — host-defined). + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with video result or rejects on error/cancellation + */ + requestVideo: function(fieldId) {}, /** - * Request video recording for a field (camera / picker — host-defined). - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with video result or rejects on error/cancellation - */ - requestVideo: function (fieldId) {}, + * Request QR code scanning for a field + * @param {string} fieldId - The ID of the field + * @returns {Promise} Promise that resolves with QR code result or rejects on error/cancellation + */ + requestQrcode: function(fieldId) {}, /** - * Request QR code scanning for a field - * @param {string} fieldId - The ID of the field - * @returns {Promise} Promise that resolves with QR code result or rejects on error/cancellation - */ - requestQrcode: function (fieldId) {}, + * Request biometric authentication + * @param {string} fieldId - The ID of the field + * @returns {Promise} + */ + requestBiometric: function(fieldId) {}, /** - * Request biometric authentication - * @param {string} fieldId - The ID of the field - * @returns {Promise} - */ - requestBiometric: function (fieldId) {}, + * Request the current connectivity status + * @returns {Promise} + */ + requestConnectivityStatus: function() {}, /** - * Request the current connectivity status - * @returns {Promise} - */ - requestConnectivityStatus: function () {}, + * Request the current sync status + * @returns {Promise} + */ + requestSyncStatus: function() {}, /** - * Request the current sync status - * @returns {Promise} - */ - requestSyncStatus: function () {}, + * Run a local ML model + * @param {string} fieldId - The ID of the field + * @param {string} modelId - The ID of the model to run + * @param {Object} input - The input data for the model + * @returns {Promise} + */ + runLocalModel: function(fieldId, modelId, input) {}, /** - * Run a local ML model - * @param {string} fieldId - The ID of the field - * @param {string} modelId - The ID of the model to run - * @param {Object} input - The input data for the model - * @returns {Promise} - */ - runLocalModel: function (fieldId, modelId, input) {}, + * Get information about the currently authenticated user. + * When no one is logged in, resolves with `{ username: '' }` (does not reject). + * @returns {Promise<{username: string, displayName?: string, role?: 'read-only' | 'read-write' | 'admin'} + */ + getCurrentUser: function() {}, /** - * Get information about the currently authenticated user. - * When no one is logged in, resolves with `{ username: '' }` (does not reject). - * @returns {Promise<{username: string, displayName?: string, role?: 'read-only' | 'read-write' | 'admin'} - */ - getCurrentUser: function () {}, + * Get the current theme mode (System / Light / Dark) so custom apps can match the host app. + * @returns {Promise<'light' | 'dark' | 'system'>} Current theme mode; 'system' means follow device preference. + */ + getThemeMode: function() {}, + + /** + * Resolve an attachment to a WebView-loadable URL (`file://`, `http(s):`, or host-specific). + * **String `fileName`:** basename only (e.g. `photo.filename`). Lookup order, first hit wins: + * 1. `attachments/draft/` — unsaved capture (formplayer preview) + * 2. `attachments/synced/` — canonical committed / downloaded copy + * 3. `attachments/pending/` — queued for upload (fallback only) + * Legacy locations (`attachments/` and `attachments/pending_upload/`) are also checked. + * Path segments and ".." are rejected. + * **`AttachmentDisplayDescriptor`:** `{ filename }` basename only (same lookup as a string argument). + * @returns {Promise} Display URL, or `null` if none + */ + getAttachmentUri: function(fileName) {}, + + /** + * Base `file://` URL for the canonical attachments directory (trailing slash). + * Returns the `synced/` subfolder — only committed/downloaded files are + * iterable from here. Drafts and the upload queue are excluded by design so + * custom apps can safely list this directory. + * **Breaking change (v2 layout):** this used to return the `attachments/` + * parent directory, which mixed committed files with `draft/` and + * `pending_upload/` subfolders. Custom apps that iterate this URL will now + * see only fully-committed attachments. + * @returns {Promise} e.g. `file:///.../attachments/synced/` + */ + getAttachmentsUri: function() {}, /** - * Get the current theme mode (System / Light / Dark) so custom apps can match the host app. - * @returns {Promise<'light' | 'dark' | 'system'>} Current theme mode; 'system' means follow device preference. - */ - getThemeMode: function () {}, - + * Base `file://` URL for the custom app bundle root (`DocumentDirectory/app/`, trailing slash). + * @returns {Promise} App directory URL for extensions, question_types, etc. + */ + getCustomAppUri: function() {}, + /** - * Resolve an attachment to a WebView-loadable URL (`file://`, `http(s):`, or host-specific). - * **String `fileName`:** basename only (e.g. `photo.filename`). Lookup order, first hit wins: - * 1. `attachments/draft/` — unsaved capture (formplayer preview) - * 2. `attachments/synced/` — canonical committed / downloaded copy - * 3. `attachments/pending/` — queued for upload (fallback only) - * Legacy locations (`attachments/` and `attachments/pending_upload/`) are also checked. - * Path segments and ".." are rejected. - * **`AttachmentDisplayDescriptor`:** `{ filename }` basename only (same lookup as a string argument). - * @returns {Promise} Display URL, or `null` if none - */ - getAttachmentUri: function (fileName) {}, - - /** - * Base `file://` URL for the canonical attachments directory (trailing slash). - * Returns the `synced/` subfolder — only committed/downloaded files are - * iterable from here. Drafts and the upload queue are excluded by design so - * custom apps can safely list this directory. - * **Breaking change (v2 layout):** this used to return the `attachments/` - * parent directory, which mixed committed files with `draft/` and - * `pending_upload/` subfolders. Custom apps that iterate this URL will now - * see only fully-committed attachments. - * @returns {Promise} e.g. `file:///.../attachments/synced/` - */ - getAttachmentsUri: function () {}, - - /** - * Base `file://` URL for the custom app bundle root (`DocumentDirectory/app/`, trailing slash). - * @returns {Promise} App directory URL for extensions, question_types, etc. - */ - getCustomAppUri: function () {}, - - /** - * Primary `file://` URL for downloaded form specs (`DocumentDirectory/forms/`, trailing slash). - * Some bundles also use files under the custom app `forms/` subdirectory. - * @returns {Promise} Forms directory URL - */ - getFormSpecsUri: function () {}, - - /** - * Persist an observation **without opening Formplayer** (headless write). - * Intended for custom apps that create or update records programmatically. - * Uses the same persistence path as a Formplayer submit, including promotion - * of any referenced draft attachments. Provide `observationId` to update an - * existing row; omit it to create a new one. - * @since 1.3.0 - * @param {PersistObservationInput} input - The observation to persist - * @returns {Promise} The stored observation id and data - */ - persistObservation: function (input) {}, - - /** - * Trigger a synchronization with Synkronus (pull + push). - * Resolves when the sync completes; rejects if a sync is already in progress - * or the sync fails. Attachments are excluded by default for speed. - * @since 1.3.0 - * @returns {Promise} The final data version after sync - */ - sync: function (options) {}, - - /** - * Probe connectivity to the configured Synkronus server (`GET /health`). - * Never rejects for an offline device — returns `{ online: false }` so callers - * can branch on connectivity without try/catch. Suitable for the - * "verify subject details when online, fall back when offline" pattern. - * @since 1.3.0 - * @returns {Promise} The current connectivity status - */ - getConnectivityStatus: function () {}, - - /** - * Read the device's last-known Synkronus data revision (`current_version` - * from the most recent successful sync). Reflects server-stream alignment - * only — not unsynced local edits. Use with periodic polling or after - * {@link sync} to detect when another device has pushed changes. - * @since 1.4.0 - * @returns {Promise} Non-negative revision count (0 if never synced) - */ - getCurrentDataRevisionCount: function () {}, + * Primary `file://` URL for downloaded form specs (`DocumentDirectory/forms/`, trailing slash). + * Some bundles also use files under the custom app `forms/` subdirectory. + * @returns {Promise} Forms directory URL + */ + getFormSpecsUri: function() {}, + + /** + * Persist an observation **without opening Formplayer** (headless write). + * Intended for custom apps that create or update records programmatically. + * Uses the same persistence path as a Formplayer submit, including promotion + * of any referenced draft attachments. Provide `observationId` to update an + * existing row; omit it to create a new one. + * @since 1.3.0 + * @param {PersistObservationInput} input - The observation to persist + * @returns {Promise} The stored observation id and data + */ + persistObservation: function(input) {}, + + /** + * Trigger a synchronization with Synkronus (pull + push). + * Resolves when the sync completes; rejects if a sync is already in progress + * or the sync fails. Attachments are excluded by default for speed. + * @since 1.3.0 + * @returns {Promise} The final data version after sync + */ + sync: function(options) {}, + + /** + * Probe connectivity to the configured Synkronus server (`GET /health`). + * Never rejects for an offline device — returns `{ online: false }` so callers + * can branch on connectivity without try/catch. Suitable for the + * "verify subject details when online, fall back when offline" pattern. + * @since 1.3.0 + * @returns {Promise} The current connectivity status + */ + getConnectivityStatus: function() {}, + + /** + * Read the device's last-known Synkronus data revision (`current_version` + * from the most recent successful sync). Reflects server-stream alignment + * only — not unsynced local edits. Use with periodic polling or after + * {@link sync} to detect when another device has pushed changes. + * @since 1.4.0 + * @returns {Promise} Non-negative revision count (0 if never synced) + */ + getCurrentDataRevisionCount: function() {}, + }; // Make the API available globally in browser environments diff --git a/formulus/src/webview/FormulusInterfaceDefinition.ts b/formulus/src/webview/FormulusInterfaceDefinition.ts index a4028f8f5..3be0c2205 100644 --- a/formulus/src/webview/FormulusInterfaceDefinition.ts +++ b/formulus/src/webview/FormulusInterfaceDefinition.ts @@ -394,6 +394,9 @@ export interface FormulusInterface { * `window.formulusSessionContext`. Draft bypass is not a param key — use * `options.skipDraftSelection` on {@link openFormplayer} (same as `skipFinalize`). * @param {Object} savedData - Previously saved form data (for editing) + * @param {Object} [options] - Session options (not persisted as observation data) + * @param {string|null} [options.observationId] - When set, finalize updates this + * observation instead of creating a new one * @returns {Promise} Promise that resolves when the form is completed/closed with result details */ openFormplayer( @@ -404,6 +407,7 @@ export interface FormulusInterface { subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; + observationId?: string | null; }, ): Promise; diff --git a/formulus/src/webview/FormulusMessageHandlers.ts b/formulus/src/webview/FormulusMessageHandlers.ts index caa3f61ae..72ae442f9 100644 --- a/formulus/src/webview/FormulusMessageHandlers.ts +++ b/formulus/src/webview/FormulusMessageHandlers.ts @@ -1425,6 +1425,7 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers { subObservationMode?: boolean; skipFinalize?: boolean; skipDraftSelection?: boolean; + observationId?: string | null; returnOnly?: boolean; }; /** @deprecated Legacy key; prefer subObservationMode */ @@ -1443,11 +1444,13 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers { const skipDraftSelection = Boolean( data.options?.skipDraftSelection || data.skipDraftSelection, ); + const observationId = + data.observationId ?? data.options?.observationId ?? null; return startFormplayerOperation( data.formType, data.params, data.savedData, - data.observationId ?? null, + observationId, subObservationMode, skipFinalize, skipDraftSelection, From f26804c79dc58f46b3089e87c903c8ce44c0ad52 Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Fri, 19 Jun 2026 18:29:24 +0000 Subject: [PATCH 3/3] fix(formplayer): fix auto change init. data might drop init. values --- .../SubObservationQuestionRenderer.tsx | 100 +++++++----------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/formulus-formplayer/src/renderers/SubObservationQuestionRenderer.tsx b/formulus-formplayer/src/renderers/SubObservationQuestionRenderer.tsx index d8a4b18f1..e08130a0c 100644 --- a/formulus-formplayer/src/renderers/SubObservationQuestionRenderer.tsx +++ b/formulus-formplayer/src/renderers/SubObservationQuestionRenderer.tsx @@ -238,6 +238,42 @@ const SubObservationQuestionRendererInner: React.FC = ({ ], ); + const refreshSubObservationWindowContext = useCallback( + (rows: Record[]) => { + const root = jsonForms.core?.data; + if (!root || typeof root !== 'object' || !path) return; + const parentData = writeDataPath( + root as Record, + path, + rows, + ); + const pv = resolveParentValue(formData, parentValuePath); + const contextTemplate = optionalRecordMap(config.subObservationContext); + const mergeConfigRaw = config.subObservationContextMerge; + const mergeConfig = + mergeConfigRaw && typeof mergeConfigRaw === 'object' + ? mergeConfigRaw + : undefined; + if (contextTemplate || mergeConfig) { + const refreshed = refreshSubObservationContextFromFormData( + parentData, + contextTemplate, + pv, + mergeConfig as SubObservationContextMergeConfig, + ); + writeSubObservationContextToWindow(refreshed); + } + }, + [ + jsonForms.core?.data, + path, + formData, + parentValuePath, + config.subObservationContext, + config.subObservationContextMerge, + ], + ); + const mergeSubmittedRow = useCallback( (result: FormCompletionResult) => { if ( @@ -249,43 +285,10 @@ const SubObservationQuestionRendererInner: React.FC = ({ const row = result.formData as Record; const next = [...getCurrentRows(), row]; pushSorted(next); - - const root = jsonForms.core?.data; - if (root && typeof root === 'object' && path) { - const parentData = writeDataPath( - root as Record, - path, - next, - ); - const pv = resolveParentValue(formData, parentValuePath); - const contextTemplate = optionalRecordMap(config.subObservationContext); - const mergeConfigRaw = config.subObservationContextMerge; - const mergeConfig = - mergeConfigRaw && typeof mergeConfigRaw === 'object' - ? mergeConfigRaw - : undefined; - if (contextTemplate || mergeConfig) { - const refreshed = refreshSubObservationContextFromFormData( - parentData, - contextTemplate, - pv, - mergeConfig as SubObservationContextMergeConfig, - ); - writeSubObservationContextToWindow(refreshed); - } - } + refreshSubObservationWindowContext(next); return true; }, - [ - getCurrentRows, - pushSorted, - jsonForms.core?.data, - path, - formData, - parentValuePath, - config.subObservationContext, - config.subObservationContextMerge, - ], + [getCurrentRows, pushSorted, refreshSubObservationWindowContext], ); const handleAdd = useCallback(async () => { @@ -371,31 +374,7 @@ const SubObservationQuestionRendererInner: React.FC = ({ i === index ? (result.formData as Record) : r, ); pushSorted(updated); - const root = jsonForms.core?.data; - if (root && typeof root === 'object' && path) { - const parentData = writeDataPath( - root as Record, - path, - updated, - ); - const contextTemplate = optionalRecordMap( - config.subObservationContext, - ); - const mergeConfigRaw = config.subObservationContextMerge; - const mergeConfig = - mergeConfigRaw && typeof mergeConfigRaw === 'object' - ? mergeConfigRaw - : undefined; - if (contextTemplate || mergeConfig) { - const refreshed = refreshSubObservationContextFromFormData( - parentData, - contextTemplate, - pv, - mergeConfig as SubObservationContextMergeConfig, - ); - writeSubObservationContextToWindow(refreshed); - } - } + refreshSubObservationWindowContext(updated); } } catch (e) { setError( @@ -414,6 +393,7 @@ const SubObservationQuestionRendererInner: React.FC = ({ config, getCurrentRows, pushSorted, + refreshSubObservationWindowContext, ], );