/* eslint-disable @typescript-eslint/ban-types */ /* * An AppsmithPromise is a mock promise class to replicate promise functionalities * in the Appsmith world. * * To mimic the async nature of promises, we will return back * action descriptors that get resolved in the main thread and then go back to action * execution flow as the workflow is designed. * * Whenever an async call needs to run, it is wrapped around this promise descriptor * and sent to the main thread to execute. * * new Promise(() => { * return Api1.run() * }) * .then(() => { * return Api2.run() * }) * .catch(() => { * return showMessage('An Error Occurred', 'error') * }) * * { * type: "APPSMITH_PROMISE", * payload: { * executor: [{ * type: "EXECUTE_ACTION", * payload: { actionId: "..." } * }] * then: ["() => { return Api2.run() }"], * catch: "() => { return showMessage('An Error Occurred', 'error) }" * } * } * * * * */ import { ActionDispatcher, DataTree, DataTreeEntity, } from "entities/DataTree/dataTreeFactory"; import _ from "lodash"; import { getEntityNameAndPropertyPath, isAction, isTrueObject, } from "./evaluationUtils"; import { ActionDescription, ActionTriggerType, PromiseActionDescription, } from "entities/DataTree/actionTriggers"; import { NavigationTargetType } from "sagas/ActionExecution/NavigateActionSaga"; export type AppsmithPromisePayload = { executor: ActionDescription[]; then: string[]; catch?: string; finally?: string; }; export const pusher: ActionDispatcher = function( this: { triggers: ActionDescription[]; isPromise: boolean }, action: any, ...payload: any[] ) { const actionPayload = action(...payload); if (actionPayload instanceof AppsmithPromise) { this.triggers.push(actionPayload.action); return actionPayload; } return action; }; export const pusherOverride = (revert = false) => { self.actionPaths.forEach((actionPath) => { const { entityName, propertyPath } = getEntityNameAndPropertyPath( actionPath, ); const promiseThis = { triggers: promiseTriggers, isPromise: true }; const globalThis = { triggers: self.triggers, isPromise: false }; const overrideThis = revert ? globalThis : promiseThis; if (entityName === actionPath) { const action = _.get(DATA_TREE_FUNCTIONS, actionPath); if (action) { _.set(self, actionPath, pusher.bind(overrideThis, action)); } } else { const entity = _.get(self, entityName); const funcCreator = DATA_TREE_FUNCTIONS[propertyPath]; if (typeof funcCreator === "object" && "qualifier" in funcCreator) { const func = funcCreator.func(entity); _.set(self, actionPath, pusher.bind(overrideThis, func)); } } }); }; let promiseTriggers: ActionDescription[] = []; export class AppsmithPromise { action: PromiseActionDescription = { type: ActionTriggerType.PROMISE, payload: { executor: [], then: [], }, }; triggerReference?: number; constructor(executor: ActionDescription[] | (() => ActionDescription[])) { if (typeof executor === "function") { pusherOverride(); executor(); this.action.payload.executor = [...promiseTriggers]; promiseTriggers = []; pusherOverride(true); } else { this.action.payload.executor = executor; } this._attachToSelfTriggers(); return this; } private removeDuplicates() { self.triggers = self.triggers.filter((trigger) => { return !this.action.payload.executor.includes(trigger); }); } private _attachToSelfTriggers() { if (self.triggers) { this.removeDuplicates(); if (_.isNumber(this.triggerReference)) { self.triggers[this.triggerReference] = this.action; } else { self.triggers.push(this.action); this.triggerReference = self.triggers.length - 1; } } } then(executor?: Function) { if (executor) { this.action.payload.then.push(`{{ ${executor.toString()} }}`); this._attachToSelfTriggers(); } return this; } catch(executor: Function) { if (executor) { this.action.payload.catch = `{{ ${executor.toString()} }}`; this._attachToSelfTriggers(); } return this; } finally(executor: Function) { if (executor) { this.action.payload.finally = `{{ ${executor.toString()} }}`; this._attachToSelfTriggers(); } return this; } static all(actions: ActionDescription[]) { return new AppsmithPromise(actions); } } const DATA_TREE_FUNCTIONS: Record< string, | ActionDispatcher | { qualifier: (entity: DataTreeEntity) => boolean; func: (entity: DataTreeEntity) => ActionDispatcher; } > = { navigateTo: function( pageNameOrUrl: string, params: Record, target?: NavigationTargetType, ) { return new AppsmithPromise([ { type: ActionTriggerType.NAVIGATE_TO, payload: { pageNameOrUrl, params, target }, }, ]); }, showAlert: function( message: string, style: "info" | "success" | "warning" | "error" | "default", ) { return new AppsmithPromise([ { type: ActionTriggerType.SHOW_ALERT, payload: { message, style }, }, ]); }, showModal: function(modalName: string) { return new AppsmithPromise([ { type: ActionTriggerType.SHOW_MODAL_BY_NAME, payload: { modalName }, }, ]); }, closeModal: function(modalName: string) { return new AppsmithPromise([ { type: ActionTriggerType.CLOSE_MODAL, payload: { modalName }, }, ]); }, storeValue: function(key: string, value: string, persist = true) { // momentarily store this value in local state to support loops _.set(self, `appsmith.store[${key}]`, value); return new AppsmithPromise([ { type: ActionTriggerType.STORE_VALUE, payload: { key, value, persist }, }, ]); }, download: function(data: string, name: string, type: string) { return new AppsmithPromise([ { type: ActionTriggerType.DOWNLOAD, payload: { data, name, type }, }, ]); }, copyToClipboard: function( data: string, options?: { debug?: boolean; format?: string }, ) { return new AppsmithPromise([ { type: ActionTriggerType.COPY_TO_CLIPBOARD, payload: { data, options: { debug: options?.debug, format: options?.format }, }, }, ]); }, resetWidget: function(widgetName: string, resetChildren = true) { return new AppsmithPromise([ { type: ActionTriggerType.RESET_WIDGET_META_RECURSIVE_BY_NAME, payload: { widgetName, resetChildren }, }, ]); }, run: { qualifier: (entity) => isAction(entity), func: (entity) => function( onSuccessOrParams?: Function | Record, onError?: Function, params = {}, ) { const isOldSignature = typeof onSuccessOrParams === "function"; const isNewSignature = isTrueObject(onSuccessOrParams); const runActionPromise = new AppsmithPromise([ { type: ActionTriggerType.RUN_PLUGIN_ACTION, payload: { actionId: isAction(entity) ? entity.actionId : "", params: isNewSignature ? onSuccessOrParams : params, }, }, ]); if (isOldSignature && typeof onSuccessOrParams === "function") { if (onSuccessOrParams) runActionPromise.then(onSuccessOrParams); if (onError) runActionPromise.catch(onError); } return runActionPromise; }, }, clear: { qualifier: (entity) => isAction(entity), func: (entity) => () => { return new AppsmithPromise([ { type: ActionTriggerType.CLEAR_PLUGIN_ACTION, payload: { actionId: isAction(entity) ? entity.actionId : "", }, }, ]); }, }, }; declare global { interface Window { triggers: ActionDescription[]; actionPaths: string[]; } } export const enhanceDataTreeWithFunctions = ( dataTree: Readonly, ): DataTree => { const withFunction: DataTree = _.cloneDeep(dataTree); self.triggers = []; self.actionPaths = []; Object.entries(DATA_TREE_FUNCTIONS).forEach(([name, funcOrFuncCreator]) => { if ( typeof funcOrFuncCreator === "object" && "qualifier" in funcOrFuncCreator ) { Object.entries(dataTree).forEach(([entityName, entity]) => { if (funcOrFuncCreator.qualifier(entity)) { const func = funcOrFuncCreator.func(entity); const funcName = `${entityName}.${name}`; _.set( withFunction, funcName, pusher.bind({ triggers: self.triggers, isPromise: false }, func), ); self.actionPaths.push(funcName); } }); } else { withFunction[name] = pusher.bind( { triggers: self.triggers, isPromise: false }, funcOrFuncCreator, ); self.actionPaths.push(name); } }); return withFunction; };