344 lines
9.0 KiB
TypeScript
344 lines
9.0 KiB
TypeScript
/* 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<string, string>,
|
|
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<string, unknown>,
|
|
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>,
|
|
): 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;
|
|
};
|