diff --git a/apps/kitchen-sink/src/ensemble/screens/actions.yaml b/apps/kitchen-sink/src/ensemble/screens/actions.yaml index 930e517ca..19c112c6e 100644 --- a/apps/kitchen-sink/src/ensemble/screens/actions.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/actions.yaml @@ -11,6 +11,9 @@ View: ensemble.storage.set('dummyId', '1'); ensemble.invokeAPI('getDummyProduct', {id: ensemble.storage.get('dummyId')}); ensemble.storage.set('value',1); + const varr = ensemble.storage.get('var') ?? 0; + console.log("Actions onLoad", varr) + ensemble.storage.set('var', varr + 1); body: Column: @@ -189,7 +192,7 @@ View: Column: children: - Text: - text: Modal Screen title + text: Modal Screen title ${ensemble.storage.get('var')} - Text: text: Modal Screen subtitle styles: @@ -198,7 +201,7 @@ View: onModalDismiss: executeCode: body: | - console.log('Modal Screen Dismissed'); + console.log('Modal Screen Dismissed', ensemble.storage.get('var')); styles: height: 100% width: 500px @@ -303,35 +306,41 @@ View: onDialogDismiss: executeCode: body: | - console.log('Dialog 1 Dismissed'); + console.log('Dialog 1 Dismissed', ensemble.storage.get('var')); body: - Button: - label: ${'Tap me to ' + showDialogText.text} - onTap: - showDialog: - options: - verticalOffset: 0.6 - onDialogDismiss: - executeCode: - body: | - console.log('Dialog 2 Dismissed'); - widget: - Button: - label: Tap me to open one more dialog - onTap: - showDialog: - options: - horizontalOffset: -0.8 - height: 100% - widget: - Column: - styles: - margin: 100px - children: - - Button: - label: Click me to close all dialogs - onTap: - closeAllDialogs: + Column: + children: + - Button: + label: increment var + onTap: ensemble.storage.set('var', ensemble.storage.get('var')+1) + - Button: + label: ${'Tap me to ' + showDialogText.text} + onTap: + showDialog: + options: + verticalOffset: 0.6 + onDialogDismiss: + executeCode: + body: | + console.log('Dialog 2 Dismissed'); + widget: + Button: + label: Tap me to open one more dialog + onTap: + showDialog: + options: + horizontalOffset: -0.8 + height: 100% + widget: + Column: + styles: + margin: 100px + children: + - Button: + label: Click me to close all dialogs + onTap: + closeAllDialogs: + - Markdown: text: You can still access the current screen's scope from a dialog. - Column: diff --git a/apps/kitchen-sink/src/ensemble/screens/forms.yaml b/apps/kitchen-sink/src/ensemble/screens/forms.yaml index 1a658682f..54b109abd 100644 --- a/apps/kitchen-sink/src/ensemble/screens/forms.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/forms.yaml @@ -16,6 +16,9 @@ View: { value: "val 2", label: "lab 2" }, { value: "val 3", label: "lab 3" }, ]); + const varr = ensemble.storage.get('var') ?? 0; + console.log("Forms onLoad", varr) + ensemble.storage.set('var', varr + 1); onComplete: invokeAPI: name: getData diff --git a/packages/framework/src/evaluate/binding.ts b/packages/framework/src/evaluate/binding.ts index 323722a4b..69f49090a 100644 --- a/packages/framework/src/evaluate/binding.ts +++ b/packages/framework/src/evaluate/binding.ts @@ -115,6 +115,8 @@ export const createBindingAtom = ( rawJsExpression.includes("ensemble.storage") ? get(screenStorageAtom) : undefined, + undefined, + get, ), user: rawJsExpression.includes("ensemble.user") ? get(userAtom) diff --git a/packages/framework/src/hooks/useCommandCallback.ts b/packages/framework/src/hooks/useCommandCallback.ts index 564b0131e..e750acfea 100644 --- a/packages/framework/src/hooks/useCommandCallback.ts +++ b/packages/framework/src/hooks/useCommandCallback.ts @@ -64,8 +64,10 @@ export const useCommandCallback = < const theme = get(themeAtom); const user = get(userAtom); - const storageApi = createStorageApi(storage, (next) => - set(screenStorageAtom, next), + const storageApi = createStorageApi( + storage, + (next) => set(screenStorageAtom, next), + get, ); const customWidgets = diff --git a/packages/framework/src/hooks/useEnsembleStorage.ts b/packages/framework/src/hooks/useEnsembleStorage.ts index b4afd8f74..3faac3f7c 100644 --- a/packages/framework/src/hooks/useEnsembleStorage.ts +++ b/packages/framework/src/hooks/useEnsembleStorage.ts @@ -1,3 +1,4 @@ +import type { Getter } from "jotai"; import { atom, useAtom } from "jotai"; import { createJSONStorage, atomWithStorage } from "jotai/utils"; import { assign, get as lodashGet, has, isObject, merge } from "lodash-es"; @@ -57,6 +58,7 @@ export const useEnsembleStorage = (): EnsembleStorage => { export const createStorageApi = ( storage?: { [key: string]: unknown }, setStorage?: (storage: { [key: string]: unknown }) => void, + get?: Getter, ): EnsembleStorage => { return { set: (key: string, value: unknown): void => { @@ -68,6 +70,7 @@ export const createStorageApi = ( setStorage?.(update); }, get: (key: string): unknown => { + if (get) return get(screenStorageAtom)[key]; return storage?.[key]; }, delete: (key: string): unknown => { diff --git a/packages/runtime/src/runtime/hooks/useCloseAllDialogs.ts b/packages/runtime/src/runtime/hooks/useCloseAllDialogs.ts index 64edf7fd0..3acd41140 100644 --- a/packages/runtime/src/runtime/hooks/useCloseAllDialogs.ts +++ b/packages/runtime/src/runtime/hooks/useCloseAllDialogs.ts @@ -1,17 +1,25 @@ -import { useContext, useMemo } from "react"; -import { type EnsembleActionHookResult } from "@ensembleui/react-framework"; +import { useContext } from "react"; +import { + type EnsembleActionHookResult, + useCommandCallback, +} from "@ensembleui/react-framework"; +import { useNavigate } from "react-router-dom"; import { ModalContext } from "../modal"; import type { EnsembleActionHook } from "./useEnsembleAction"; export const useCloseAllDialogs: EnsembleActionHook< EnsembleActionHookResult > = () => { - const { closeAllModals } = useContext(ModalContext) || {}; + const modalContext = useContext(ModalContext); + const navigate = useNavigate(); - return useMemo( - () => ({ - callback: () => closeAllModals?.(), - }), - [], + const callback = useCommandCallback( + () => { + modalContext?.closeAllModals(); + }, + { navigate }, + [modalContext], ); + + return { callback }; }; diff --git a/packages/runtime/src/runtime/hooks/useCloseAllModalScreens.ts b/packages/runtime/src/runtime/hooks/useCloseAllModalScreens.ts index ccc072e7f..1cdfde11e 100644 --- a/packages/runtime/src/runtime/hooks/useCloseAllModalScreens.ts +++ b/packages/runtime/src/runtime/hooks/useCloseAllModalScreens.ts @@ -1,17 +1,25 @@ -import { useContext, useMemo } from "react"; -import { type EnsembleActionHookResult } from "@ensembleui/react-framework"; +import { useContext } from "react"; +import { + type EnsembleActionHookResult, + useCommandCallback, +} from "@ensembleui/react-framework"; +import { useNavigate } from "react-router-dom"; import { ModalContext } from "../modal"; import type { EnsembleActionHook } from "./useEnsembleAction"; export const useCloseAllScreens: EnsembleActionHook< EnsembleActionHookResult > = () => { - const { closeAllScreens } = useContext(ModalContext) || {}; + const modalContext = useContext(ModalContext); + const navigate = useNavigate(); - return useMemo( - () => ({ - callback: () => closeAllScreens?.(), - }), - [], + const callback = useCommandCallback( + () => { + modalContext?.closeAllScreens(); + }, + { navigate }, + [modalContext], ); + + return { callback }; }; diff --git a/packages/runtime/src/runtime/hooks/useEnsembleAction.tsx b/packages/runtime/src/runtime/hooks/useEnsembleAction.tsx index 5d5856475..712b45466 100644 --- a/packages/runtime/src/runtime/hooks/useEnsembleAction.tsx +++ b/packages/runtime/src/runtime/hooks/useEnsembleAction.tsx @@ -395,6 +395,8 @@ export const useShowDialog: EnsembleActionHook = ( const { openModal } = useContext(ModalContext) || {}; const ensembleAction = useEnsembleAction(action?.onDialogDismiss); const customScope = useCustomScope(); + const navigate = useNavigate(); + const screenModel = useScreenModel(); if (!action?.widget && !action?.body) throw new Error("ShowDialog Action requires a widget to be specified"); @@ -404,8 +406,10 @@ export const useShowDialog: EnsembleActionHook = ( [action.widget, action.body], ); - const callback = useCallback( - (args: unknown): void => { + const callback = useCommandCallback( + (evalContext, ...args) => { + const context = merge({}, evalContext, customScope, args[0]); + const modalOptions = getShowDialogOptions( action.options, ensembleAction?.callback, @@ -423,21 +427,25 @@ export const useShowDialog: EnsembleActionHook = ( value={merge( {}, customScope, - isObject(args) ? (args as CustomScope) : undefined, + isObject(args[0]) ? (args[0] as CustomScope) : undefined, )} > {EnsembleRuntime.render([widget])} , modalOptions, true, - merge( - {}, - customScope, - isObject(args) ? (args as CustomScope) : undefined, - ), + context, ); }, - [action.options, customScope, ensembleAction?.callback, openModal, widget], + { navigate }, + [ + action.options, + customScope, + ensembleAction?.callback, + openModal, + widget, + screenModel, + ], ); return { callback }; @@ -451,6 +459,7 @@ export const usePickFiles: EnsembleActionHook = ( const [isComplete, setIsComplete] = useState(); const onCompleteAction = useEnsembleAction(onComplete); const onErrorAction = useEnsembleAction(onError); + const navigate = useNavigate(); const reset = useCallback(() => { if (inputEl) { @@ -550,14 +559,18 @@ export const usePickFiles: EnsembleActionHook = ( onErrorAction?.callback, ]); - const callback = useCallback(() => { - try { - inputEl.click(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - } - }, [inputEl]); + const callback = useCommandCallback( + () => { + try { + inputEl.click(); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }, + { navigate }, + [inputEl], + ); return { callback }; }; @@ -670,10 +683,15 @@ export const useUploadFiles: EnsembleActionHook = ( export const useNavigateBack: EnsembleActionHook = () => { const modalContext = useContext(ModalContext); + const navigate = useNavigate(); - const callback = useCallback((): void => { - modalContext?.navigateBack(); - }, [modalContext]); + const callback = useCommandCallback( + () => { + modalContext?.navigateBack(); + }, + { navigate }, + [modalContext], + ); return { callback }; }; @@ -683,17 +701,20 @@ export const useActionGroup: EnsembleActionHook = ( ) => { // This ensures hooks are fired in consistent order const actions = useMemo(() => action?.actions ?? [], [action]); + const navigate = useNavigate(); const execActs = actions.map((act: EnsembleAction) => { // eslint-disable-next-line react-hooks/rules-of-hooks return useEnsembleAction(act); }); - const callback = useCallback( - (args: unknown): void => { - execActs.forEach((act) => act?.callback(args)); + const callback = useCommandCallback( + (evalContext, ...args) => { + const context = merge({}, evalContext, args[0]); + execActs.forEach((act) => act?.callback(context)); }, - [actions], + { navigate }, + [execActs], ); return { callback }; @@ -703,44 +724,38 @@ export const useDispatchEvent: EnsembleActionHook = ( action, ) => { const eventName = keys(action)[0]; - const [isComplete, setIsComplete] = useState(); - const [context, setContext] = useState(); const eventData = (action ? action[eventName] : {}) as { [key: string]: unknown; }; const eventScope = useCustomEventScope(); + const navigate = useNavigate(); + const screenModel = useScreenModel(); - const evaluatedInputs = useEvaluate(eventData, { context }); - + // Use a separate hook call for each event action const events = get(eventScope, eventName) as { [key: string]: unknown; }; - // Use a separate hook call for each event action - const ensembleActions = Object.keys(events || {}).map((customAction) => + const ensembleActions = Object.keys(events).map((customAction) => // eslint-disable-next-line react-hooks/rules-of-hooks useEnsembleAction({ [customAction]: events[customAction] }), ); - useEffect(() => { - if (isComplete !== false) { - return; - } - - ensembleActions.forEach((act) => - act?.callback({ - ...evaluatedInputs, - ...(context as { [key: string]: unknown }), - }), - ); - - setIsComplete(true); - }, [ensembleActions, evaluatedInputs, isComplete]); + const callback = useCommandCallback( + (evalContext, ...args) => { + const context = merge({}, evalContext, args[0]); + const evaluatedData = evaluateDeep(eventData, screenModel, context); - const callback = useCallback((args: unknown): void => { - setContext(args); - setIsComplete(false); - }, []); + ensembleActions.forEach((act) => + act?.callback({ + ...evaluatedData, + ...context, + }), + ); + }, + { navigate }, + [ensembleActions, eventData, screenModel], + ); return { callback }; }; @@ -762,60 +777,45 @@ export const useConditionalAction: EnsembleActionHook< // eslint-disable-next-line react-hooks/rules-of-hooks return useEnsembleAction(condition.action); }); - const [isComplete, setIsComplete] = useState(); - const [context, setContext] = useState(); - const [trueActionIndex, setTrueActionIndex] = useState(); - const evaluatedStatements = useEvaluate( - conditionStatements as unknown as { [key: string]: unknown }, - { - context, - }, - ); - - useEffect(() => { - if (trueActionIndex === undefined) { - return; - } - - execActs[trueActionIndex]?.callback(context); - setTrueActionIndex(undefined); - }, [context, execActs, trueActionIndex]); + const navigate = useNavigate(); + const screenModel = useScreenModel(); - useEffect(() => { - if (!action || isComplete !== false) { - return; - } + const callback = useCommandCallback( + (evalContext, ...args) => { + const context = merge({}, evalContext, args[0]); - const index = Object.keys(evaluatedStatements).find( - (key) => evaluatedStatements[key] === true, - ); + // Evaluate conditions + const evaluatedStatements = evaluateDeep( + conditionStatements as unknown as { [key: string]: unknown }, + screenModel, + context, + ); - let trueIndex: number | undefined; - if (index !== undefined) { - trueIndex = toNumber(index); - } + const index = Object.keys(evaluatedStatements).find( + (key) => evaluatedStatements[key] === true, + ); - if (trueIndex === undefined || trueIndex < 0) { - // check if last condition is 'else' - const lastCondition = last(action.conditions); - if (lastCondition && "else" in lastCondition) { - trueIndex = action.conditions.length - 1; + let trueIndex: number | undefined; + if (index !== undefined) { + trueIndex = toNumber(index); } - // if no condition is true, return - else { - setIsComplete(true); - return; - } - } - setTrueActionIndex(trueIndex); - setIsComplete(true); - }, [action, evaluatedStatements, isComplete, context]); + if (trueIndex === undefined || trueIndex < 0) { + // check if last condition is 'else' + const lastCondition = last(action.conditions); + if (lastCondition && "else" in lastCondition) { + trueIndex = action.conditions.length - 1; + } else { + // if no condition is true, return + return; + } + } - const callback = useCallback((args: unknown): void => { - setContext(args); - setIsComplete(false); - }, []); + execActs[trueIndex]?.callback(context); + }, + { navigate }, + [action.conditions, conditionStatements, execActs, screenModel], + ); return { callback }; }; diff --git a/packages/runtime/src/runtime/hooks/useNavigateExternalScreen.ts b/packages/runtime/src/runtime/hooks/useNavigateExternalScreen.ts index 5fe7fde91..5d4b13c8c 100644 --- a/packages/runtime/src/runtime/hooks/useNavigateExternalScreen.ts +++ b/packages/runtime/src/runtime/hooks/useNavigateExternalScreen.ts @@ -1,9 +1,11 @@ -import { useEffect, useMemo, useState } from "react"; -import { isString } from "lodash-es"; +import { isString, cloneDeep, merge } from "lodash-es"; import { - useEvaluate, + evaluateDeep, + useCommandCallback, + useScreenModel, type NavigateExternalScreen, } from "@ensembleui/react-framework"; +import { useNavigate } from "react-router-dom"; // eslint-disable-next-line import/no-cycle import { openExternalScreen } from "../navigation"; import { type EnsembleActionHook } from "./useEnsembleAction"; @@ -11,34 +13,31 @@ import { type EnsembleActionHook } from "./useEnsembleAction"; export const useNavigateExternalScreen: EnsembleActionHook< NavigateExternalScreen > = (action) => { - const [screenNavigated, setScreenNavigated] = useState(); - const [context, setContext] = useState<{ [key: string]: unknown }>(); - const evaluatedInputs = useEvaluate( - isString(action) ? { url: action } : { ...action }, - { context }, - ); + const navigate = useNavigate(); + const screenModel = useScreenModel(); - const navigateScreen = useMemo(() => { - if (!action) { - return; - } + const callback = useCommandCallback( + (evalContext, ...args) => { + if (!action) return; - const callback = (args: unknown): void => { - setScreenNavigated(false); - setContext(args as { [key: string]: unknown }); - }; + const context = merge({}, evalContext, args[0]); - return { callback }; - }, [action]); + const evaluatedInputs = evaluateDeep( + isString(action) ? { url: action } : cloneDeep({ ...action }), + screenModel, + context, + ); - useEffect(() => { - if (!evaluatedInputs.url || screenNavigated !== false) { - return; - } + if (!evaluatedInputs.url) return; - setScreenNavigated(true); - return openExternalScreen(evaluatedInputs as NavigateExternalScreen); - }, [evaluatedInputs, screenNavigated]); + // Type assertion is safe here because we've verified url exists + return openExternalScreen( + evaluatedInputs as unknown as NavigateExternalScreen, + ); + }, + { navigate }, + [action, screenModel], + ); - return navigateScreen; + return { callback }; }; diff --git a/packages/runtime/src/runtime/hooks/useNavigateModal.tsx b/packages/runtime/src/runtime/hooks/useNavigateModal.tsx index a73ac6fa1..cb728d3a1 100644 --- a/packages/runtime/src/runtime/hooks/useNavigateModal.tsx +++ b/packages/runtime/src/runtime/hooks/useNavigateModal.tsx @@ -56,6 +56,7 @@ export const useNavigateModalScreen: EnsembleActionHook< evaluatedInputs, title, ensembleAction?.callback, + context, ); }, { navigate }, diff --git a/packages/runtime/src/runtime/navigation.tsx b/packages/runtime/src/runtime/navigation.tsx index e3a7d3561..cf516c4cd 100644 --- a/packages/runtime/src/runtime/navigation.tsx +++ b/packages/runtime/src/runtime/navigation.tsx @@ -59,6 +59,9 @@ export const navigateModalScreen = ( inputs?: { [key: string]: unknown }, title?: React.ReactNode[], onClose?: () => void, + context?: { + [key: string]: unknown; + }, ): void => { const hasOptions = !isString(action); const screenName = hasOptions ? action.name : action; @@ -84,6 +87,8 @@ export const navigateModalScreen = ( screen={matchingScreen} />, modalOptions, + false, + context, ); };