Skip to content
Merged
11 changes: 7 additions & 4 deletions formulus-formplayer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,10 @@ function App() {
newObservationDraftSessionKey?: string | null,
) => {
try {
if (initData.observationId != null) {
if (initData.returnOnly) {
// Embedded child form: data lives only in memory until returned to parent; no local drafts.
setDraftSessionKey(null);
} else if (initData.observationId != null) {
setDraftSessionKey(null);
} else if (newObservationDraftSessionKey !== undefined) {
setDraftSessionKey(newObservationDraftSessionKey);
Expand Down Expand Up @@ -590,7 +593,7 @@ function App() {
// Check if this is a new form (no savedData) and if drafts exist
const hasExistingSavedData =
savedData && Object.keys(savedData).length > 0;
if (!hasExistingSavedData) {
if (!initData.returnOnly && !hasExistingSavedData) {
const availableDrafts = draftService.getDraftsForForm(
receivedFormType,
(formSchema as any)?.version,
Expand Down Expand Up @@ -948,8 +951,8 @@ function App() {
({ data: newData }: { data: FormData }) => {
setData(newData);

// Save draft data whenever form data changes
if (formInitData) {
// Save draft data whenever form data changes (skip embedded return-only child forms)
if (formInitData && !formInitData.returnOnly) {
draftService.saveDraft(
formInitData.formType,
newData,
Expand Down
2 changes: 2 additions & 0 deletions formulus-formplayer/src/types/FormulusInterfaceDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface FormInitData {
formSchema?: unknown;
uiSchema?: unknown;
operationId?: string;
returnOnly?: boolean; // For embedded child forms: return JSON without saving to DB
extensions?: ExtensionMetadata;
customQuestionTypes?: {
custom_types: Record<string, { source: string }>;
Expand Down Expand Up @@ -284,6 +285,7 @@ export interface FormulusInterface {
formType: string,
params: Record<string, unknown>,
savedData: Record<string, unknown>,
options?: { returnOnly?: boolean },
): Promise<FormCompletionResult>;

/**
Expand Down
150 changes: 103 additions & 47 deletions formulus/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import QRScannerModal from './src/components/QRScannerModal';
import SignatureCaptureModal from './src/components/SignatureCaptureModal';
import MainAppNavigator from './src/navigation/MainAppNavigator';
import { FormInitData } from './src/webview/FormulusInterfaceDefinition.ts';
import { FormSpec } from './src/services';

/**
* Inner component that consumes the AppTheme context to build a dynamic
Expand Down Expand Up @@ -58,13 +59,60 @@ function AppInner(): React.JSX.Element {
onResult: (result: unknown) => void;
} | null>(null);

const [formplayerVisible, setFormplayerVisible] = useState(false);
const formplayerModalRef = React.useRef<FormplayerModalHandle>(null);
const formplayerVisibleRef = React.useRef(false);
type FormplayerStackEntry = {
id: string;
formSpec: FormSpec;
params: Record<string, unknown> | null;
observationId: string | null;
savedData: Record<string, unknown> | null;
operationId: string | null;
returnOnly?: boolean; // For child forms opened from linkedtable
};

useEffect(() => {
formplayerVisibleRef.current = formplayerVisible;
}, [formplayerVisible]);
const [formplayerStack, setFormplayerStack] = useState<
FormplayerStackEntry[]
>([]);
const formplayerModalRefs = React.useRef(
new Map<string, FormplayerModalHandle | null>(),
);

const initializeStackEntry = React.useCallback(
(entry: FormplayerStackEntry) => {
let attempt = 0;

const tryInitialize = () => {
const modalHandle = formplayerModalRefs.current.get(entry.id);
if (!modalHandle) {
if (attempt < 20) {
attempt += 1;
setTimeout(tryInitialize, 100);
}
return;
}

setTimeout(() => {
modalHandle.initializeForm(
entry.formSpec,
entry.params,
entry.observationId,
entry.savedData,
entry.operationId,
entry.returnOnly, // ← Pass returnOnly flag
);
}, 200);
};

tryInitialize();
},
[],
);

const closeFormplayerEntry = React.useCallback((entryId: string) => {
formplayerModalRefs.current.delete(entryId);
setFormplayerStack(current =>
current.filter(entry => entry.id !== entryId),
);
}, []);

useEffect(() => {
FormService.getInstance();
Expand Down Expand Up @@ -92,19 +140,14 @@ function AppInner(): React.JSX.Element {
);

const handleOpenFormplayer = async (config: FormInitData) => {
// If formplayer is already visible, close it first to allow opening a new form
if (formplayerVisibleRef.current) {
console.log(
'[App] Formplayer already visible, closing first before opening new form',
);
formplayerVisibleRef.current = false;
setFormplayerVisible(false);
// Wait for modal to close before proceeding
await new Promise<void>(resolve => setTimeout(() => resolve(), 300));
}

const { formType, observationId, params, savedData, operationId } =
config;
const {
formType,
observationId,
params,
savedData,
operationId,
returnOnly,
} = config;

try {
const formService = await FormService.getInstance();
Expand All @@ -127,21 +170,21 @@ function AppInner(): React.JSX.Element {
return;
}

// Set visible state first to mount the modal
formplayerVisibleRef.current = true;
setFormplayerVisible(true);
const entryId =
operationId ||
`${formType}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
const entry: FormplayerStackEntry = {
id: entryId,
formSpec,
params: params || null,
observationId: observationId || null,
savedData: savedData || null,
operationId: operationId || null,
returnOnly: returnOnly || false, // Include returnOnly flag
};

// Wait for modal to mount and WebView to start loading before initializing form
// This ensures the WebView ref is available and the modal is visible
setTimeout(() => {
formplayerModalRef.current?.initializeForm(
formSpec,
params || null,
observationId || null,
savedData || null,
operationId || null,
);
}, 200);
setFormplayerStack(current => [...current, entry]);
initializeStackEntry(entry);
} catch (error) {
console.error('[App] Error opening formplayer:', error);
Alert.alert(
Expand All @@ -150,15 +193,19 @@ function AppInner(): React.JSX.Element {
error instanceof Error ? error.message : 'Unknown error'
}`,
);
// Reset state on error
formplayerVisibleRef.current = false;
setFormplayerVisible(false);
}
};

const handleCloseFormplayer = () => {
formplayerVisibleRef.current = false;
setFormplayerVisible(false);
setFormplayerStack(current => {
if (current.length === 0) {
return current;
}
const next = current.slice(0, -1);
const removed = current[current.length - 1];
formplayerModalRefs.current.delete(removed.id);
return next;
});
};

appEvents.addListener(
Expand All @@ -182,7 +229,7 @@ function AppInner(): React.JSX.Element {
);
appEvents.removeListener('closeFormplayer', handleCloseFormplayer);
};
}, []);
}, [closeFormplayerEntry, initializeStackEntry]);

return (
<>
Expand All @@ -192,14 +239,23 @@ function AppInner(): React.JSX.Element {
/>
<NavigationContainer theme={navigationTheme}>
<MainAppNavigator />
<FormplayerModal
ref={formplayerModalRef}
visible={formplayerVisible}
onClose={() => {
formplayerVisibleRef.current = false;
setFormplayerVisible(false);
}}
/>
{formplayerStack.map((entry, index) => (
<FormplayerModal
key={entry.id}
ref={instance => {
if (instance) {
formplayerModalRefs.current.set(entry.id, instance);
} else {
formplayerModalRefs.current.delete(entry.id);
}
}}
visible={true}
isActive={index === formplayerStack.length - 1}
onClose={() => {
closeFormplayerEntry(entry.id);
}}
/>
))}
</NavigationContainer>

<QRScannerModal
Expand Down
Loading
Loading